Update besar apk finance

This commit is contained in:
Wian Drs
2026-09-11 16:03:00 +07:00
parent d42483b350
commit 4ef15c22ae
897 changed files with 98056 additions and 15979 deletions
+189 -7
View File
@@ -3,6 +3,9 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
Please also have a look at our
[API and deprecation policy](docs/API-and-deprecation-policy.md).
## x.y.z
### Added
@@ -15,6 +18,183 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Fixed
### Documentation
## 9.4.0: Deprecations and bugfixes
### Deprecated
- Deprecate support for PHP 7.2 and 7.3 (#1565)
### Fixed
- Allow CSS containing only whitespace or comments (#1593)
- Only allow strings as `LineName` components (#1590)
## 9.3.0: Support for modern CSS at-rules and autoloading bugfix
### Added
- Add support for modern CSS at-rules: `@layer`, `@scope`, and
`@starting-style` (#1549)
### Fixed
- Avoid double autoloading of class aliases (#1552)
### Documentation
## 9.2.0: New features and deprecations
### Added
- Add `OutputFormat::setSpaceAroundSelectorCombinator()` (#1504)
- Add support for escaped quotes in the selectors (#1485, #1489)
- Provide line number in exception message for mismatched parentheses in
selector (#1435)
- Add support for CSS container queries (#1400)
### Changed
- `RuleSet\RuleContainer` is renamed to `RuleSet\DeclarationList` (#1530, #1539)
- Methods like `setRule()` in `RuleSet` and `DeclarationBlock` have been renamed
to `setDeclaration()`, etc. (#1521)
- `Rule\Rule` class is renamed to `Property\Declaration`
(#1508, #1512, #1513, #1522)
- `Rule::setRule()` and `getRule()` are replaced with `setPropertyName()` and
`getPropertyName()` (#1506)
- `Selector` is now represented as a sequence of `Selector\Component` objects
which can be accessed via `getComponents()`, manipulated individually, or set
via `setComponents()` (#1478, #1486, #1487, #1488, #1494, #1496, #1536, #1537)
- `Selector::setSelector()` and `Selector` constructor will now throw exception
upon provision of an invalid selectior (#1498, #1502)
- Clean up extra whitespace in CSS selector (#1398)
- The array keys passed to `DeclarationBlock::setSelectors()` are no longer
preserved (#1407)
### Deprecated
- `RuleSet\RuleContainer` is deprecated; use `RuleSet\DeclarationList` instead
(#1530)
- Methods like `setRule()` in `RuleSet` and `DeclarationBlock` are deprecated;
there are direct replacements such as `setDeclaration()` (#1521)
- `Rule\Rule` class is deprecated; `Property\Declaration` is a direct
replacement (#1508)
- `Rule::setRule()` and `getRule()` are deprecated and replaced with
`setPropertyName()` and `getPropertyName()` (#1506, #1519)
### Fixed
- Do not escape characters that do not need escaping in CSS string (#1444)
- Reject selector comprising only whitespace (#1433)
- Improve recovery parsing when a rogue `}` is encountered (#1425, #1426)
- Parse comment(s) immediately preceding a selector (#1421, #1424)
- Parse consecutive comments (#1421)
- Support attribute selectors with values containing commas in
`DeclarationBlock::setSelectors()` (#1419)
- Allow `removeDeclarationBlockBySelector()` to be order-insensitve (#1406)
- Fix parsing of `calc` expressions when a newline immediately precedes or
follows a `+` or `-` operator (#1399)
- Use typesafe versions of PHP functions (#1379, #1380, #1382, #1383, #1384)
## 9.1.0: Add support for PHP 8.5
### Added
- Add support for PHP 8.5 (#1355)
### Fixed
- Improve performance of selector validation
(avoiding silent PCRE catastrophic failure) (#1372)
- Use typesafe versions of PHP functions (#1368, #1370)
## 9.0.0: New features, deprecation removals and bug fixes
### Added
- Interface `RuleContainer` for `RuleSet` `Rule` manipulation methods (#1256)
- Partial support for CSS Color Module Level 4:
- `rgb` and `rgba`, and `hsl` and `hsla` are now aliases (#797)
- Parse color functions that use the "modern" syntax (#800)
- Render RGB functions with "modern" syntax when required (#840)
- Support `none` as color function component value (#859)
- Add a class diagram to the README (#482)
- Add more tests (#449)
### Changed
- `DeclarationBlock` no longer extends `RuleSet` and instead has a `RuleSet` as
a property; use `getRuleSet()` to access it directly (#1194)
- The default line (and column) number is now `null` (not zero) (#1288)
- `setPosition()` (in `Rule` and other classes) now has fluent interface,
returning itself (#1259)
- `RuleSet::removeRule()` now only allows `Rule` as the parameter
(implementing classes are `AtRuleSet` and `DeclarationBlock`);
use `removeMatchingRules()` or `removeAllRules()` for other functions (#1255)
- `RuleSet::getRules()` and `getRulesAssoc()` now only allow `string` or `null`
as the parameter (implementing classes are `AtRuleSet` and `DeclarationBlock`)
(#1253)
- Initialize `KeyFrame` properties to sensible defaults (#1146)
- Make `OutputFormat` `final` (#1128)
- Make `Selector` a `Renderable` (#1017)
- Only allow `string` for some `OutputFormat` properties (#885)
- Use more native type declarations and strict mode
(#641, #772, #774, #778, #804, #841, #873, #875, #891, #922, #923, #933, #958,
#964, #967, #1000, #1044, #1134, #1136, #1137, #1139, #1140, #1141, #1145,
#1162, #1163, #1166, #1172, #1174, #1178, #1179, #1181, #1183, #1184, #1186,
#1187, #1190, #1192, #1193, #1203)
- Add visibility to all class/interface constants (#469)
### Removed
- Remove `getLineNo()` from these classes (use `getLineNumber()` instead):
`Comment`, `CSSList`, `SourceException`, `Charset`, `CSSNamespace`, `Import`,
`Rule`, `DeclarationBlock`, `RuleSet`, `CSSFunction`, `Value` (#1258)
- Remove `Rule::getColNo()` (use `getColumnNumber()` instead) (#1287)
- Passing a string as the first argument to `getAllValues()` is no longer
supported and will not work;
the search pattern should now be passed as the second argument (#1243)
- Passing a Boolean as the second argument to `getAllValues()` is no longer
supported and will not work; the flag for searching in function arguments
should now be passed as the third argument (#1243)
- Remove `__toString()` (#1046)
- Drop magic method forwarding in `OutputFormat` (#898)
- Drop `atRuleArgs()` from the `AtRule` interface (#1141)
- Remove `OutputFormat::get()` and `::set()` (#1108, #1110)
- Drop special support for vendor prefixes (#1083)
- Remove the IE hack in `Rule` (#995)
- Drop `getLineNo()` from the `Renderable` interface (#1038)
- Remove `OutputFormat::level()` (#874)
- Remove expansion of shorthand properties (#838)
- Remove `Parser::setCharset/getCharset` (#808)
- Remove `Rule::getValues()` (#582)
- Remove `Rule::setValues()` (#562)
- Remove `Document::getAllSelectors()` (#561)
- Remove `DeclarationBlock::getSelector()` (#559)
- Remove `DeclarationBlock::setSelector()` (#560)
- Drop support for PHP < 7.2 (#420)
### Fixed
- Remove trailing semicolon from declaration blocks with 'compact'
`OutputFormat` (#1345)
- Parse selector functions (like `:not`) with comma-separated arguments (#1292)
- Parse quoted attribute selector value containing comma (#1323)
- Allow comma in selectors (e.g. `:not(html, body)`) (#1293)
- Insert `Rule` before sibling even with different property name
(in `RuleSet::addRule()`) (#1270)
- Ensure `RuleSet::addRule()` sets non-negative column number when sibling
provided (#1268)
- Don't render `rgb` colors with percentage values using hex notation (#803)
### Documentation
- Add an API and deprecation policy (#720)
@ziegenberg is a new contributor to this release and did a lot of the heavy
lifting. Thanks! :heart:
## 8.9.0: New features, bug fixes and deprecations
### Added
@@ -108,21 +288,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Added
- Add support for PHP 8.4 (#675, #701, #746, #751)
- Add support for PHP 8.4 (#643, #657)
### Changed
- Mark parsing-internal classes and methods as `@internal` (#711)
- Mark parsing-internal classes and methods as `@internal` (#674)
- Block installations on unsupported higher PHP versions (#691)
### Deprecated
- Deprecate the expansion of shorthand properties (#719)
- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#703)
- Deprecate the expansion of shorthand properties
(#578, #580, #579, #577, #576, #575, #574, #573, #572, #571, #570, #569, #566,
#567, #558, #714)
- Deprecate `Parser::setCharset()` and `Parser::getCharset()` (#688)
### Fixed
- Fix type errors in PHP strict mode (#695)
- Fix type errors in PHP strict mode (#664)
## 8.6.0
@@ -134,14 +316,14 @@ This project adheres to [Semantic Versioning](https://semver.org/).
### Changed
- Improve performance of Value::parseValue with many delimiters by refactoring
- Improve performance of `Value::parseValue` with many delimiters by refactoring
to remove `array_search()` (#413)
## 8.5.2
### Changed
- Mark all class constants as `@internal` (#500)
- Mark all class constants as `@internal` (#472)
### Fixed
+318 -114
View File
@@ -1,6 +1,7 @@
# PHP CSS Parser
[![Build Status](https://github.com/sabberworm/PHP-CSS-Parser/workflows/CI/badge.svg?branch=master)](https://github.com/sabberworm/PHP-CSS-Parser/actions/)
[![Build Status](https://github.com/MyIntervals/PHP-CSS-Parser/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/MyIntervals/PHP-CSS-Parser/actions/)
[![Coverage Status](https://coveralls.io/repos/github/MyIntervals/PHP-CSS-Parser/badge.svg?branch=main)](https://coveralls.io/github/MyIntervals/PHP-CSS-Parser?branch=main)
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
@@ -158,7 +159,7 @@ foreach($cssDocument->getAllRuleSets() as $oRuleSet) {
// Note that the added dash will make this remove all rules starting with
// `font-` (like `font-size`, `font-weight`, etc.) as well as a potential
// `font` rule.
$oRuleSet->removeRule('font-');
$oRuleSet->removeRule('font-');
$oRuleSet->removeRule('cursor');
}
```
@@ -220,44 +221,44 @@ html, body {
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
protected $aContents =>
protected $contents =>
array(4) {
[0] =>
class Sabberworm\CSS\Property\Charset#6 (2) {
private $sCharset =>
private $charset =>
class Sabberworm\CSS\Value\CSSString#5 (2) {
private $sString =>
private $string =>
string(5) "utf-8"
protected $iLineNo =>
protected $lineNumber =>
int(1)
}
protected $iLineNo =>
protected $lineNumber =>
int(1)
}
[1] =>
class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) {
private $sType =>
private $type =>
string(9) "font-face"
private $sArgs =>
private $arguments =>
string(0) ""
private $aRules =>
private $rules =>
array(2) {
'font-family' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#8 (4) {
private $sRule =>
private $rule =>
string(11) "font-family"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\CSSString#9 (2) {
private $sString =>
private $string =>
string(10) "CrassRoots"
protected $iLineNo =>
protected $lineNumber =>
int(4)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(4)
}
}
@@ -265,76 +266,76 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#10 (4) {
private $sRule =>
private $rule =>
string(3) "src"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\URL#11 (2) {
private $oURL =>
private $url =>
class Sabberworm\CSS\Value\CSSString#12 (2) {
private $sString =>
private $string =>
string(15) "../media/cr.ttf"
protected $iLineNo =>
protected $lineNumber =>
int(5)
}
protected $iLineNo =>
protected $lineNumber =>
int(5)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(5)
}
}
}
protected $iLineNo =>
protected $lineNumber =>
int(3)
}
[2] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) {
private $aSelectors =>
private $selectors =>
array(2) {
[0] =>
class Sabberworm\CSS\Property\Selector#14 (2) {
private $sSelector =>
private $selector =>
string(4) "html"
private $iSpecificity =>
private $specificity =>
NULL
}
[1] =>
class Sabberworm\CSS\Property\Selector#15 (2) {
private $sSelector =>
private $selector =>
string(4) "body"
private $iSpecificity =>
private $specificity =>
NULL
}
}
private $aRules =>
private $rules =>
array(1) {
'font-size' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
private $sRule =>
private $rule =>
string(9) "font-size"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\Size#17 (4) {
private $fSize =>
private $size =>
double(1.6)
private $sUnit =>
private $unit =>
string(2) "em"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(9)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(9)
}
}
}
protected $iLineNo =>
protected $lineNumber =>
int(8)
}
[3] =>
@@ -343,96 +344,96 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
string(9) "keyframes"
private $animationName =>
string(6) "mymove"
protected $aContents =>
protected $contents =>
array(2) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) {
private $aSelectors =>
private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#20 (2) {
private $sSelector =>
private $selector =>
string(4) "from"
private $iSpecificity =>
private $specificity =>
NULL
}
}
private $aRules =>
private $rules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#21 (4) {
private $sRule =>
private $rule =>
string(3) "top"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\Size#22 (4) {
private $fSize =>
private $size =>
double(0)
private $sUnit =>
private $unit =>
string(2) "px"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(13)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(13)
}
}
}
protected $iLineNo =>
protected $lineNumber =>
int(13)
}
[1] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) {
private $aSelectors =>
private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#24 (2) {
private $sSelector =>
private $selector =>
string(2) "to"
private $iSpecificity =>
private $specificity =>
NULL
}
}
private $aRules =>
private $rules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#25 (4) {
private $sRule =>
private $rule =>
string(3) "top"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\Size#26 (4) {
private $fSize =>
private $size =>
double(200)
private $sUnit =>
private $unit =>
string(2) "px"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(14)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(14)
}
}
}
protected $iLineNo =>
protected $lineNumber =>
int(14)
}
}
protected $iLineNo =>
protected $lineNumber =>
int(12)
}
}
protected $iLineNo =>
protected $lineNumber =>
int(1)
}
@@ -466,85 +467,85 @@ html, body {font-size: 1.6em;}
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
protected $aContents =>
protected $contents =>
array(1) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) {
private $aSelectors =>
private $selectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#6 (2) {
private $sSelector =>
private $selector =>
string(7) "#header"
private $iSpecificity =>
private $specificity =>
NULL
}
}
private $aRules =>
private $rules =>
array(3) {
'margin' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#7 (4) {
private $sRule =>
private $rule =>
string(6) "margin"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\RuleValueList#12 (3) {
protected $aComponents =>
protected $components =>
array(4) {
[0] =>
class Sabberworm\CSS\Value\Size#8 (4) {
private $fSize =>
private $size =>
double(10)
private $sUnit =>
private $unit =>
string(2) "px"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
[1] =>
class Sabberworm\CSS\Value\Size#9 (4) {
private $fSize =>
private $size =>
double(2)
private $sUnit =>
private $unit =>
string(2) "em"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
[2] =>
class Sabberworm\CSS\Value\Size#10 (4) {
private $fSize =>
private $size =>
double(1)
private $sUnit =>
private $unit =>
string(2) "cm"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
[3] =>
class Sabberworm\CSS\Value\Size#11 (4) {
private $fSize =>
private $size =>
double(2)
private $sUnit =>
private $unit =>
string(1) "%"
private $bIsColorComponent =>
private $isColorComponent =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
}
protected $sSeparator =>
protected $separator =>
string(1) " "
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(2)
}
}
@@ -552,11 +553,11 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#13 (4) {
private $sRule =>
private $rule =>
string(11) "font-family"
private $mValue =>
private $value =>
class Sabberworm\CSS\Value\RuleValueList#15 (3) {
protected $aComponents =>
protected $components =>
array(4) {
[0] =>
string(7) "Verdana"
@@ -564,9 +565,9 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
string(9) "Helvetica"
[2] =>
class Sabberworm\CSS\Value\CSSString#14 (2) {
private $sString =>
private $string =>
string(9) "Gill Sans"
protected $iLineNo =>
protected $lineNumber =>
int(3)
}
[3] =>
@@ -574,12 +575,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
}
protected $sSeparator =>
string(1) ","
protected $iLineNo =>
protected $lineNumber =>
int(3)
}
private $bIsImportant =>
private $isImportant =>
bool(false)
protected $iLineNo =>
protected $lineNumber =>
int(3)
}
}
@@ -587,22 +588,22 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
private $sRule =>
private $rule =>
string(5) "color"
private $mValue =>
private $value =>
string(3) "red"
private $bIsImportant =>
private $isImportant =>
bool(true)
protected $iLineNo =>
protected $lineNumber =>
int(4)
}
}
}
protected $iLineNo =>
protected $lineNumber =>
int(1)
}
}
protected $iLineNo =>
protected $lineNumber =>
int(1)
}
@@ -615,6 +616,207 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;}
```
## Class diagram
```mermaid
classDiagram
direction LR
class Anchor {
}
class AtRule {
<<interface>>
}
class AtRuleBlockList {
}
class AtRuleSet {
}
class CSSBlockList {
<<abstract>>
}
class CSSElement {
<<interface>>
}
class CSSFunction {
}
class CSSList {
<<abstract>>
}
class CSSListItem {
<<interface>>
}
class CSSNamespace {
}
class CSSString {
}
class CalcFunction {
}
class CalcRuleValueList {
}
class Charset {
}
class Color {
}
class Comment {
}
class Commentable {
<<interface>>
}
class DeclarationBlock {
}
class Document {
}
class Import {
}
class KeyFrame {
}
class KeyframeSelector {
}
class LineName {
}
class OutputException {
}
class OutputFormat {
}
class OutputFormatter {
}
class Parser {
}
class ParserState {
}
class Positionable {
<<interface>>
}
class PrimitiveValue {
<<abstract>>
}
class Renderable {
<<interface>>
}
class Rule {
}
class RuleContainer {
<<interface>>
}
class RuleSet {
}
class RuleValueList {
}
class Selector {
}
class Settings {
}
class Size {
}
class SourceException {
}
class SpecificityCalculator {
}
class URL {
}
class UnexpectedEOFException {
}
class UnexpectedTokenException {
}
class Value {
<<abstract>>
}
class ValueList {
<<abstract>>
}
Anchor ..> ParserState: dependency
CSSListItem <|-- AtRule: inheritance
AtRule <|.. AtRuleBlockList: realization
CSSBlockList <|-- AtRuleBlockList: inheritance
AtRule <|.. AtRuleSet: realization
RuleSet <|-- AtRuleSet: inheritance
CSSList <|-- CSSBlockList: inheritance
Renderable <|-- CSSElement: inheritance
ValueList <|-- CSSFunction: inheritance
CSSElement <|.. CSSList: realization
CSSListItem <|.. CSSList: realization
CSSList ..> Charset: dependency
CSSList ..> Import: dependency
Positionable <|.. CSSList: realization
Commentable <|-- CSSListItem: inheritance
Renderable <|-- CSSListItem: inheritance
AtRule <|.. CSSNamespace: realization
Positionable <|.. CSSNamespace: realization
PrimitiveValue <|-- CSSString: inheritance
CSSFunction <|-- CalcFunction: inheritance
RuleValueList <|-- CalcRuleValueList: inheritance
AtRule <|.. Charset: realization
Charset ..> CSSString: dependency
Positionable <|.. Charset: realization
CSSFunction <|-- Color: inheritance
Positionable <|.. Comment: realization
Renderable <|.. Comment: realization
CSSElement <|.. DeclarationBlock: realization
CSSListItem <|.. DeclarationBlock: realization
Positionable <|.. DeclarationBlock: realization
RuleContainer <|.. DeclarationBlock: realization
DeclarationBlock ..> RuleSet : dependency
DeclarationBlock ..> Selector: dependency
CSSBlockList <|-- Document: inheritance
AtRule <|.. Import: realization
Positionable <|.. Import: realization
AtRule <|.. KeyFrame: realization
CSSList <|-- KeyFrame: inheritance
Selector <|-- KeyframeSelector: inheritance
ValueList <|-- LineName: inheritance
SourceException <|-- OutputException: inheritance
OutputFormat ..> OutputFormatter: dependency
OutputFormatter ..> OutputFormat: dependency
Parser ..> ParserState: dependency
ParserState ..> Settings: dependency
Value <|-- PrimitiveValue: inheritance
CSSElement <|.. Rule: realization
Commentable <|.. Rule: realization
Positionable <|.. Rule: realization
Rule ..> RuleValueList: dependency
CSSElement <|.. RuleSet: realization
CSSListItem <|.. RuleSet: realization
Positionable <|.. RuleSet: realization
RuleSet ..> Rule: dependency
RuleContainer <|.. RuleSet: realization
ValueList <|-- RuleValueList: inheritance
Renderable <|.. Selector: realization
PrimitiveValue <|-- Size: inheritance
Exception <|-- SourceException: inheritance
Positionable <|.. SourceException: realization
URL ..> CSSString: dependency
PrimitiveValue <|-- URL: inheritance
UnexpectedTokenException <|-- UnexpectedEOFException: inheritance
SourceException <|-- UnexpectedTokenException: inheritance
CSSElement <|.. Value: realization
Positionable <|.. Value: realization
Value <|-- ValueList: inheritance
CSSList ..> CSSList: dependency
CSSList ..> Comment: dependency
CSSList ..> RuleSet: dependency
CSSNamespace ..> Comment: dependency
Charset ..> Comment: dependency
Import ..> Comment: dependency
OutputFormat ..> OutputFormat: dependency
Rule ..> Comment: dependency
RuleSet ..> Comment: dependency
ValueList ..> Value: dependency
```
## API and deprecation policy
Please have a look at our
[API and deprecation policy](docs/API-and-deprecation-policy.md).
## Contributing
Contributions in the form of bug reports, feature requests, or pull requests are
more than welcome. :pray: Please have a look at our
[contribution guidelines](CONTRIBUTING.md) to learn more about how to
contribute to PHP-CSS-Parser.
## Contributors/Thanks to
* [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations
@@ -628,10 +830,12 @@ class Sabberworm\CSS\CSSList\Document#4 (2) {
* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration.
* [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility.
* [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing.
* [goetas](https://github.com/goetas) for @namespace at-rule support.
* [goetas](https://github.com/goetas) for `@namespace` at-rule support.
* [ziegenberg](https://github.com/ziegenberg) for general housekeeping and cleanup.
* [View full list](https://github.com/sabberworm/PHP-CSS-Parser/contributors)
## Misc
* Legacy Support: The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag.
* Running Tests: To run all unit tests for this project, run `composer install` to install phpunit and use `./vendor/bin/phpunit`.
### Legacy Support
The latest pre-PSR-0 version of this project can be checked with the `0.9.0` tag.
+99 -9
View File
@@ -1,14 +1,13 @@
{
"name": "sabberworm/php-css-parser",
"type": "library",
"description": "Parser for CSS Files written in PHP",
"license": "MIT",
"type": "library",
"keywords": [
"parser",
"css",
"stylesheet"
],
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"license": "MIT",
"authors": [
{
"name": "Raphael Schweikert"
@@ -22,13 +21,24 @@
"email": "jake.github@qzdesign.co.uk"
}
],
"homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"require": {
"php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0",
"ext-iconv": "*"
"php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
"ext-iconv": "*",
"thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
},
"require-dev": {
"phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41",
"rawr/cross-data-providers": "^2.0.0"
"php-parallel-lint/php-parallel-lint": "1.4.0",
"phpstan/extension-installer": "1.4.3",
"phpstan/phpstan": "1.12.33 || 2.2.2",
"phpstan/phpstan-phpunit": "1.4.2 || 2.0.16",
"phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11",
"phpunit/phpunit": "8.5.52",
"rawr/phpunit-data-provider": "3.3.1",
"rector/rector": "1.2.10 || 2.4.6",
"rector/type-perfect": "1.0.0 || 2.1.3",
"squizlabs/php_codesniffer": "4.0.1",
"thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3"
},
"suggest": {
"ext-mbstring": "for parsing UTF-8 CSS"
@@ -36,16 +46,96 @@
"autoload": {
"psr-4": {
"Sabberworm\\CSS\\": "src/"
}
},
"files": [
"src/Rule/Rule.php",
"src/RuleSet/RuleContainer.php"
]
},
"autoload-dev": {
"psr-4": {
"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"
"dev-main": "9.5.x-dev"
}
},
"scripts": {
"check": [
"@check:static",
"@check:dynamic"
],
"check:composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock --dry-run",
"check:dynamic": [
"@check:tests"
],
"check:php:cs-fixer": "\"./.phive/php-cs-fixer\" fix --config=./Build/php-cs-fixer/config.php -v --dry-run --diff --show-progress=dots Build bin src tests",
"check:php:lint": "parallel-lint Build bin src tests",
"check:php:rector": "rector process --no-progress-bar --dry-run --config=./Build/rector/config.php",
"check:php:sniff": "phpcs --standard=./Build/phpcs/config.xml Build bin src tests",
"check:php:stan": "phpstan --no-progress -v --configuration=./Build/phpstan/phpstan.neon",
"check:static": [
"@check:composer:normalize",
"@check:php:lint",
"@check:php:rector",
"@check:php:sniff",
"@check:php:stan",
"@check:php:cs-fixer"
],
"check:tests": [
"@check:tests:unit"
],
"check:tests:coverage": "phpunit --config=./Build/phpunit/config.xml --do-not-cache-result --coverage-clover=coverage.xml",
"check:tests:sof": "phpunit --stop-on-failure --config=./Build/phpunit/config.xml --do-not-cache-result",
"check:tests:unit": "phpunit --config=./Build/phpunit/config.xml --do-not-cache-result",
"fix": [
"@fix:php"
],
"fix:composer:normalize": "\"./.phive/composer-normalize\" --no-check-lock",
"fix:php": [
"@fix:composer:normalize",
"@fix:php:rector",
"@fix:php:sniff",
"@fix:php:cs-fixer"
],
"fix:php:cs-fixer": "\"./.phive/php-cs-fixer\" fix --config=./Build/php-cs-fixer/config.php Build bin src tests",
"fix:php:rector": "rector process --config=./Build/rector/config.php",
"fix:php:sniff": "phpcbf --standard=./Build/phpcs/config.xml Build bin src tests",
"phpstan:baseline": "phpstan --configuration=./Build/phpstan/phpstan.neon --allow-empty-baseline --generate-baseline=./Build/phpstan/phpstan-baseline.neon",
"phpstan:clearcache": "phpstan clear-result-cache"
},
"scripts-descriptions": {
"check": "Runs all dynamic and static code checks.",
"check:composer:normalize": "Checks the composer.json.",
"check:dynamic": "Runs all dynamic code checks (i.e., currently, the unit tests).",
"check:php:cs-fixer": "Checks the code style with the PHP Coding Standards Fixer (PHP-CS-Fixer).",
"check:php:lint": "Lints the PHP files for syntax errors.",
"check:php:rector": "Checks for code for changes by Rector.",
"check:php:sniff": "Checks the code style with PHP_CodeSniffer (PHPCS).",
"check:php:stan": "Checks the PHP types using PHPStan.",
"check:static": "Runs all static code checks (syntax, style, types).",
"check:tests": "Runs all dynamic tests (i.e., currently, the unit tests).",
"check:tests:coverage": "Runs the unit tests with code coverage.",
"check:tests:sof": "Runs the unit tests and stops at the first failure.",
"check:tests:unit": "Runs the unit tests.",
"fix": "Runs all automatic code style fixes.",
"fix:composer:normalize": "Normalizes composer.json file content.",
"fix:php": "Runs all fixers for the PHP code.",
"fix:php:cs-fixer": "Fixes the code style with PHP-CS-Fixer.",
"fix:php:rector": "Updates the code with Rector.",
"fix:php:sniff": "Fixes the code style with PHP_CodeSniffer.",
"phpstan:baseline": "Updates the PHPStan baseline file to match the code.",
"phpstan:clearcache": "Clears the PHPStan cache."
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
@@ -11,77 +13,59 @@ use Sabberworm\CSS\Property\AtRule;
class AtRuleBlockList extends CSSBlockList implements AtRule
{
/**
* @var string
* @var non-empty-string
*/
private $sType;
private $type;
/**
* @var string
*/
private $sArgs;
private $arguments;
/**
* @param string $sType
* @param string $sArgs
* @param int $iLineNo
* @param non-empty-string $type
* @param int<1, max>|null $lineNumber
*/
public function __construct($sType, $sArgs = '', $iLineNo = 0)
public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
parent::__construct($lineNumber);
$this->type = $type;
$this->arguments = $arguments;
}
/**
* @return string
* @return non-empty-string
*/
public function atRuleName()
public function atRuleName(): string
{
return $this->sType;
return $this->type;
}
public function atRuleArgs(): string
{
return $this->arguments;
}
/**
* @return string
* @return non-empty-string
*/
public function atRuleArgs()
public function render(OutputFormat $outputFormat): string
{
return $this->sArgs;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= $oOutputFormat->sBeforeAtRuleBlock;
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
$formatter = $outputFormat->getFormatter();
$result = $formatter->comments($this);
$result .= $outputFormat->getContentBeforeAtRuleBlock();
$arguments = $this->arguments;
if ($arguments !== '') {
$arguments = ' ' . $arguments;
}
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
$result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
$result .= $this->renderListContents($outputFormat);
$result .= '}';
$result .= $outputFormat->getContentAfterAtRuleBlock();
return $result;
}
/**
* @return bool
*/
public function isRootList()
public function isRootList(): bool
{
return false;
}
+127 -134
View File
@@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\Property\Declaration;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\DeclarationList;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Value\CSSFunction;
use Sabberworm\CSS\Value\Value;
@@ -20,171 +23,161 @@ use Sabberworm\CSS\Value\ValueList;
abstract class CSSBlockList extends CSSList
{
/**
* @param int $iLineNo
* Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
*
* @return list<DeclarationBlock>
*/
public function __construct($iLineNo = 0)
public function getAllDeclarationBlocks(): array
{
parent::__construct($iLineNo);
}
/**
* @param array<int, DeclarationBlock> $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<int, RuleSet> $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);
}
}
}
/**
* Returns all `Value` objects found recursively in `Rule`s in the tree.
*
* @param CSSElement|string|null $element
* This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
* If a string is given, it is used as a rule name filter.
* Passing a string for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
* use the following parameter to pass a rule name filter instead.
* @param string|bool|null $ruleSearchPatternOrSearchInFunctionArguments
* This allows filtering rules by property name
* (e.g. if "color" is passed, only `Value`s from `color` properties will be returned,
* or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself,
* will be returned).
* If a Boolean is provided, it is treated as the `$searchInFunctionArguments` argument.
* Passing a Boolean for this parameter is deprecated in version 8.9.0, and will not work from v9.0;
* use the `$searchInFunctionArguments` parameter instead.
* @param bool $searchInFunctionArguments whether to also return Value objects used as Function arguments.
*
* @return array<int, Value>
*
* @see RuleSet->getRules()
*/
public function getAllValues(
$element = null,
$ruleSearchPatternOrSearchInFunctionArguments = null,
$searchInFunctionArguments = false
) {
if (\is_bool($ruleSearchPatternOrSearchInFunctionArguments)) {
$searchInFunctionArguments = $ruleSearchPatternOrSearchInFunctionArguments;
$searchString = null;
} else {
$searchString = $ruleSearchPatternOrSearchInFunctionArguments;
}
if ($element === null) {
$element = $this;
} elseif (\is_string($element)) {
$searchString = $element;
$element = $this;
}
$result = [];
$this->allValues($element, $result, $searchString, $searchInFunctionArguments);
foreach ($this->contents as $item) {
if ($item instanceof DeclarationBlock) {
$result[] = $item;
} elseif ($item instanceof CSSBlockList) {
$result = \array_merge($result, $item->getAllDeclarationBlocks());
}
}
return $result;
}
/**
* @param CSSElement|string $oElement
* @param array<int, Value> $aResult
* @param string|null $sSearchString
* @param bool $bSearchInFunctionArguments
* Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
*
* @return void
* @return list<RuleSet>
*/
protected function allValues($oElement, array &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false)
public function getAllRuleSets(): array
{
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
$result = [];
foreach ($this->contents as $item) {
if ($item instanceof RuleSet) {
$result[] = $item;
} elseif ($item instanceof CSSBlockList) {
$result = \array_merge($result, $item->getAllRuleSets());
} elseif ($item instanceof DeclarationBlock) {
$result[] = $item->getRuleSet();
}
} 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;
}
return $result;
}
/**
* @param array<int, Selector> $aResult
* @param string|null $sSpecificitySearch
* Returns all `Value` objects found recursively in `Declaration`s in the tree.
*
* @return void
* @param CSSElement|null $element
* This is the `CSSList` or `RuleSet` to start the search from (defaults to the whole document).
* @param string|null $ruleSearchPattern
* This allows filtering rules by property name
* (e.g. if "color" is passed, only `Value`s from `color` properties will be returned,
* or if "font-" is provided, `Value`s from all font rules, like `font-size`, and including `font` itself,
* will be returned).
* @param bool $searchInFunctionArguments whether to also return `Value` objects used as `CSSFunction` arguments.
*
* @return list<Value>
*
* @see RuleSet->getRules()
*/
protected function allSelectors(array &$aResult, $sSpecificitySearch = null)
{
/** @var array<int, DeclarationBlock> $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];
public function getAllValues(
?CSSElement $element = null,
?string $ruleSearchPattern = null,
bool $searchInFunctionArguments = false
): array {
$element = $element ?? $this;
$result = [];
if ($element instanceof CSSBlockList) {
foreach ($element->getContents() as $contentItem) {
// Statement at-rules are skipped since they do not contain values.
if ($contentItem instanceof CSSElement) {
$result = \array_merge(
$result,
$this->getAllValues($contentItem, $ruleSearchPattern, $searchInFunctionArguments)
);
}
}
} elseif ($element instanceof DeclarationList) {
foreach ($element->getRules($ruleSearchPattern) as $rule) {
$result = \array_merge(
$result,
$this->getAllValues($rule, $ruleSearchPattern, $searchInFunctionArguments)
);
}
} elseif ($element instanceof Declaration) {
$value = $element->getValue();
// `string` values are discarded.
if ($value instanceof CSSElement) {
$result = \array_merge(
$result,
$this->getAllValues($value, $ruleSearchPattern, $searchInFunctionArguments)
);
}
} elseif ($element instanceof ValueList) {
if ($searchInFunctionArguments || !($element instanceof CSSFunction)) {
foreach ($element->getListComponents() as $component) {
// `string` components are discarded.
if ($component instanceof CSSElement) {
$result = \array_merge(
$result,
$this->getAllValues($component, $ruleSearchPattern, $searchInFunctionArguments)
);
}
$iTargetSpecificity = (int)$iTargetSpecificity;
$iSelectorSpecificity = $oSelector->getSpecificity();
$bMatches = false;
switch ($sComparator) {
}
}
} elseif ($element instanceof Value) {
$result[] = $element;
}
return $result;
}
/**
* @return list<Selector>
*/
protected function getAllSelectors(?string $specificitySearch = null): array
{
$result = [];
foreach ($this->getAllDeclarationBlocks() as $declarationBlock) {
foreach ($declarationBlock->getSelectors() as $selector) {
if ($specificitySearch === null) {
$result[] = $selector;
} else {
$comparator = '===';
$expressionParts = \explode(' ', $specificitySearch);
$targetSpecificity = $expressionParts[0];
if (\count($expressionParts) > 1) {
$comparator = $expressionParts[0];
$targetSpecificity = $expressionParts[1];
}
$targetSpecificity = (int) $targetSpecificity;
$selectorSpecificity = $selector->getSpecificity();
switch ($comparator) {
case '<=':
$bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
$comparatorMatched = $selectorSpecificity <= $targetSpecificity;
break;
case '<':
$bMatches = $iSelectorSpecificity < $iTargetSpecificity;
$comparatorMatched = $selectorSpecificity < $targetSpecificity;
break;
case '>=':
$bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
$comparatorMatched = $selectorSpecificity >= $targetSpecificity;
break;
case '>':
$bMatches = $iSelectorSpecificity > $iTargetSpecificity;
$comparatorMatched = $selectorSpecificity > $targetSpecificity;
break;
default:
$bMatches = $iSelectorSpecificity === $iTargetSpecificity;
$comparatorMatched = $selectorSpecificity === $targetSpecificity;
break;
}
if ($bMatches) {
$aResult[] = $oSelector;
if ($comparatorMatched) {
$result[] = $selector;
}
}
}
}
return $result;
}
}
+234 -252
View File
@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
@@ -17,293 +18,276 @@ use Sabberworm\CSS\Property\Charset;
use Sabberworm\CSS\Property\CSSNamespace;
use Sabberworm\CSS\Property\Import;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;
use function Safe\preg_match;
/**
* This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),
* `RuleSet`s as well as other `CSSList` objects.
*
* It can also contain `Import` and `Charset` objects stemming from at-rules.
*
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
* so those interfaces must also be implemented by concrete subclasses.
*/
abstract class CSSList implements Commentable, CSSElement, Positionable
abstract class CSSList implements CSSElement, CSSListItem, Positionable
{
use CommentContainer;
use Position;
/**
* @var array<array-key, Comment>
* @var array<int<0, max>, CSSListItem>
*
* @internal since 8.8.0
*/
protected $aComments;
protected $contents = [];
/**
* @var array<int, RuleSet|CSSList|Import|Charset>
*
* @internal since 8.8.0
* @param int<1, max>|null $lineNumber
*/
protected $aContents;
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
$this->aComments = [];
$this->aContents = [];
$this->setPosition($iLineNo);
$this->setPosition($lineNumber);
}
/**
* @return void
*
* @throws UnexpectedTokenException
* @throws SourceException
*
* @internal since V8.8.0
*/
public static function parseList(ParserState $oParserState, CSSList $oList)
public static function parseList(ParserState $parserState, CSSList $list): void
{
$bIsRoot = $oList instanceof Document;
if (is_string($oParserState)) {
$oParserState = new ParserState($oParserState, Settings::create());
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
$aComments = [];
while (!$oParserState->isEnd()) {
$aComments = array_merge($aComments, $oParserState->consumeWhiteSpace());
$oListItem = null;
if ($bLenientParsing) {
$isRoot = $list instanceof Document;
$usesLenientParsing = $parserState->getSettings()->usesLenientParsing();
$comments = [];
$parserState->consumeWhiteSpace($comments);
while (!$parserState->isEnd()) {
$listItem = null;
if ($usesLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
$positionBeforeParse = $parserState->currentColumn();
$listItem = self::parseListItem($parserState, $list);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
$listItem = false;
// If the failed parsing did not consume anything that was to come ...
if ($parserState->currentColumn() === $positionBeforeParse) {
// ... the unexpected token needs to be skipped, otherwise there'll be an infinite loop.
$parserState->consume(1);
}
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
$listItem = self::parseListItem($parserState, $list);
}
if ($oListItem === null) {
if ($listItem === null) {
// List parsing finished
return;
}
if ($oListItem) {
$oListItem->addComments($aComments);
$oList->append($oListItem);
if ($listItem) {
$listItem->addComments($comments);
$list->append($listItem);
}
$aComments = $oParserState->consumeWhiteSpace();
$comments = [];
$parserState->consumeWhiteSpace($comments);
}
$oList->addComments($aComments);
if (!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
$list->addComments($comments);
if (!$isRoot && !$usesLenientParsing) {
throw new SourceException('Unexpected end of document', $parserState->currentLine());
}
}
/**
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
* @return CSSListItem|false|null
* If `null` is returned, it means the end of the list has been reached.
* If `false` is returned, it means an invalid item has been encountered,
* but parsing of the next item should proceed.
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseListItem(ParserState $oParserState, CSSList $oList)
private static function parseListItem(ParserState $parserState, CSSList $list)
{
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if ($oAtRule instanceof Charset) {
if (!$bIsRoot) {
$isRoot = $list instanceof Document;
if ($parserState->comes('@')) {
$atRule = self::parseAtRule($parserState);
if ($atRule instanceof Charset) {
if (!$isRoot) {
throw new UnexpectedTokenException(
'@charset may only occur in root document',
'',
'custom',
$oParserState->currentLine()
$parserState->currentLine()
);
}
if (count($oList->getContents()) > 0) {
if (\count($list->getContents()) > 0) {
throw new UnexpectedTokenException(
'@charset must be the first parseable token in a document',
'',
'custom',
$oParserState->currentLine()
$parserState->currentLine()
);
}
$oParserState->setCharset($oAtRule->getCharset());
$parserState->setCharset($atRule->getCharset());
}
return $oAtRule;
} elseif ($oParserState->comes('}')) {
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
return DeclarationBlock::parse($oParserState);
return $atRule;
} elseif ($parserState->comes('}')) {
if ($isRoot) {
if ($parserState->getSettings()->usesLenientParsing()) {
$parserState->consume(1);
return self::parseListItem($parserState, $list);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
throw new SourceException('Unopened {', $parserState->currentLine());
}
} else {
// End of list
return null;
}
} else {
return DeclarationBlock::parse($oParserState, $oList);
return DeclarationBlock::parse($parserState, $list) ?? false;
}
}
/**
* @param ParserState $oParserState
*
* @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
*
* @throws SourceException
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
private static function parseAtRule(ParserState $oParserState)
private static function parseAtRule(ParserState $parserState): ?CSSListItem
{
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
}
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
} elseif ($sIdentifier === 'charset') {
$oCharsetString = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consumeUntil([';', ParserState::EOF], true, true);
return new Charset($oCharsetString, $iIdentifierLineNum);
} elseif (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
}
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());
$parserState->consume('@');
$identifier = $parserState->parseIdentifier();
$identifierLineNumber = $parserState->currentLine();
$parserState->consumeWhiteSpace();
if ($identifier === 'import') {
$location = URL::parse($parserState);
$parserState->consumeWhiteSpace();
$mediaQuery = null;
if (!$parserState->comes(';')) {
$mediaQuery = \trim($parserState->consumeUntil([';', ParserState::EOF]));
if ($mediaQuery === '') {
$mediaQuery = null;
}
}
$bUseRuleSet = true;
foreach (explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if (self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
$parserState->consumeUntil([';', ParserState::EOF], true, true);
return new Import($location, $mediaQuery, $identifierLineNumber);
} elseif ($identifier === 'charset') {
$charsetString = CSSString::parse($parserState);
$parserState->consumeWhiteSpace();
$parserState->consumeUntil([';', ParserState::EOF], true, true);
return new Charset($charsetString, $identifierLineNumber);
} elseif (self::identifierIs($identifier, 'keyframes')) {
$result = new KeyFrame($identifierLineNumber);
$result->setVendorKeyFrame($identifier);
$result->setAnimationName(\trim($parserState->consumeUntil('{', false, true)));
CSSList::parseList($parserState, $result);
if ($parserState->comes('}')) {
$parserState->consume('}');
}
return $result;
} elseif ($identifier === 'namespace') {
$prefix = null;
$url = Value::parsePrimitiveValue($parserState);
if (!$parserState->comes(';')) {
$prefix = $url;
$url = Value::parsePrimitiveValue($parserState);
}
$parserState->consumeUntil([';', ParserState::EOF], true, true);
if ($prefix !== null && !\is_string($prefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $prefix, 'custom', $identifierLineNumber);
}
if (!($url instanceof CSSString || $url instanceof URL)) {
throw new UnexpectedTokenException(
'Wrong namespace url of invalid type',
$url,
'custom',
$identifierLineNumber
);
}
return new CSSNamespace($url, $prefix, $identifierLineNumber);
} else {
// Unknown other at rule (font-face or such)
$arguments = \trim($parserState->consumeUntil('{', false, true));
if (\substr_count($arguments, '(') !== \substr_count($arguments, ')')) {
if ($parserState->getSettings()->usesLenientParsing()) {
return null;
} else {
throw new SourceException('Unmatched brace count in media query', $parserState->currentLine());
}
}
$useRuleSet = true;
foreach (AtRule::BLOCK_RULES as $blockRuleName) {
if (self::identifierIs($identifier, $blockRuleName)) {
$useRuleSet = false;
break;
}
}
if ($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
if ($useRuleSet) {
$atRule = new AtRuleSet($identifier, $arguments, $identifierLineNumber);
RuleSet::parseRuleSet($parserState, $atRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
if ($oParserState->comes('}')) {
$oParserState->consume('}');
$atRule = new AtRuleBlockList($identifier, $arguments, $identifierLineNumber);
CSSList::parseList($parserState, $atRule);
if ($parserState->comes('}')) {
$parserState->consume('}');
}
}
return $oAtRule;
return $atRule;
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed.
* We need to check for these versions too.
*
* @param string $sIdentifier
* @param string $sMatch
*
* @return bool
*/
private static function identifierIs($sIdentifier, $sMatch)
private static function identifierIs(string $identifier, string $match): bool
{
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
if (\strcasecmp($identifier, $match) === 0) {
return true;
}
return preg_match("/^(-\\w+-)?$match$/i", $identifier) === 1;
}
/**
* Prepends an item to the list of contents.
*
* @param RuleSet|CSSList|Import|Charset $oItem
*
* @return void
*/
public function prepend($oItem)
public function prepend(CSSListItem $item): void
{
array_unshift($this->aContents, $oItem);
\array_unshift($this->contents, $item);
}
/**
* Appends an item to the list of contents.
*
* @param RuleSet|CSSList|Import|Charset $oItem
*
* @return void
*/
public function append($oItem)
public function append(CSSListItem $item): void
{
$this->aContents[] = $oItem;
$this->contents[] = $item;
}
/**
* Splices the list of contents.
*
* @param int $iOffset
* @param int $iLength
* @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
*
* @return void
* @param array<int, CSSListItem> $replacement
*/
public function splice($iOffset, $iLength = null, $mReplacement = null)
public function splice(int $offset, ?int $length = null, ?array $replacement = null): void
{
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
\array_splice($this->contents, $offset, $length, $replacement);
}
/**
* Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,
* the item is appended at the end.
*
* @param RuleSet|CSSList|Import|Charset $item
* @param RuleSet|CSSList|Import|Charset $sibling
*/
public function insertBefore($item, $sibling)
public function insertBefore(CSSListItem $item, CSSListItem $sibling): void
{
if (in_array($sibling, $this->aContents, true)) {
if (\in_array($sibling, $this->contents, true)) {
$this->replace($sibling, [$item, $sibling]);
} else {
$this->append($item);
@@ -313,52 +297,53 @@ abstract class CSSList implements Commentable, CSSElement, Positionable
/**
* Removes an item from the CSS list.
*
* @param RuleSet|Import|Charset|CSSList $oItemToRemove
* May be a RuleSet (most likely a DeclarationBlock), a Import,
* a Charset or another CSSList (most likely a MediaQuery)
* @param CSSListItem $itemToRemove
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`,
* a `Charset` or another `CSSList` (most likely a `MediaQuery`)
*
* @return bool whether the item was removed
*/
public function remove($oItemToRemove)
public function remove(CSSListItem $itemToRemove): bool
{
$iKey = array_search($oItemToRemove, $this->aContents, true);
if ($iKey !== false) {
unset($this->aContents[$iKey]);
$key = \array_search($itemToRemove, $this->contents, true);
if ($key !== false) {
unset($this->contents[$key]);
return true;
}
return false;
}
/**
* Replaces an item from the CSS list.
*
* @param RuleSet|Import|Charset|CSSList $oOldItem
* @param CSSListItem $oldItem
* May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
* or another `CSSList` (most likely a `MediaQuery`)
*
* @return bool
* @param CSSListItem|array<CSSListItem> $newItem
*/
public function replace($oOldItem, $mNewItem)
public function replace(CSSListItem $oldItem, $newItem): bool
{
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
if (is_array($mNewItem)) {
array_splice($this->aContents, $iKey, 1, $mNewItem);
$key = \array_search($oldItem, $this->contents, true);
if ($key !== false) {
if (\is_array($newItem)) {
\array_splice($this->contents, $key, 1, $newItem);
} else {
array_splice($this->aContents, $iKey, 1, [$mNewItem]);
\array_splice($this->contents, $key, 1, [$newItem]);
}
return true;
}
return false;
}
/**
* @param array<int, RuleSet|Import|Charset|CSSList> $aContents
* @param array<int, CSSListItem> $contents
*/
public function setContents(array $aContents)
public function setContents(array $contents): void
{
$this->aContents = [];
foreach ($aContents as $content) {
$this->contents = [];
foreach ($contents as $content) {
$this->append($content);
}
}
@@ -366,131 +351,128 @@ abstract class CSSList implements Commentable, CSSElement, Positionable
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
*
* @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
* @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
*
* @return void
* @param DeclarationBlock|array<Selector>|string $selectors the selectors to match
* @param bool $removeAll whether to stop at the first declaration block found or remove all blocks
*/
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false)
public function removeDeclarationBlockBySelector($selectors, bool $removeAll = false): void
{
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
if ($selectors instanceof DeclarationBlock) {
$selectors = $selectors->getSelectors();
}
if (!is_array($mSelector)) {
$mSelector = explode(',', $mSelector);
if (!\is_array($selectors)) {
$selectors = \explode(',', $selectors);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!($mSel instanceof Selector)) {
if (!Selector::isValid($mSel)) {
foreach ($selectors as &$selector) {
if (!($selector instanceof Selector)) {
if (!Selector::isValid($selector)) {
throw new UnexpectedTokenException(
"Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
$mSel,
"custom"
$selector,
'custom'
);
}
$mSel = new Selector($mSel);
$selector = new Selector($selector);
}
}
foreach ($this->aContents as $iKey => $mItem) {
if (!($mItem instanceof DeclarationBlock)) {
foreach ($this->contents as $key => $item) {
if (!($item instanceof DeclarationBlock)) {
continue;
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
if (self::selectorsMatch($item->getSelectors(), $selectors)) {
unset($this->contents[$key]);
if (!$removeAll) {
return;
}
}
}
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
protected function renderListContents(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
}
/**
* @return string
*/
protected function renderListContents(OutputFormat $oOutputFormat)
{
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
$result = '';
$isFirst = true;
$nextLevelFormat = $outputFormat;
if (!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
$nextLevelFormat = $outputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function () use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
$nextLevelFormatter = $nextLevelFormat->getFormatter();
$formatter = $outputFormat->getFormatter();
foreach ($this->contents as $listItem) {
$renderedCss = $formatter->safely(static function () use ($nextLevelFormat, $listItem): string {
return $listItem->render($nextLevelFormat);
});
if ($sRendered === null) {
if ($renderedCss === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
if ($isFirst) {
$isFirst = false;
$result .= $nextLevelFormatter->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
$result .= $nextLevelFormatter->spaceBetweenBlocks();
}
$sResult .= $sRendered;
$result .= $renderedCss;
}
if (!$bIsFirst) {
if (!$isFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
$result .= $formatter->spaceAfterBlocks();
}
return $sResult;
return $result;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
*
* @return bool
*/
abstract public function isRootList();
abstract public function isRootList(): bool;
/**
* Returns the stored items.
*
* @return array<int, RuleSet|Import|Charset|CSSList>
* @return array<int<0, max>, CSSListItem>
*/
public function getContents()
public function getContents(): array
{
return $this->aContents;
return $this->contents;
}
/**
* @param array<array-key, Comment> $aComments
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function addComments(array $aComments)
public function getArrayRepresentation(): array
{
$this->aComments = array_merge($this->aComments, $aComments);
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
/**
* @return array<array-key, Comment>
* @param list<Selector> $selectors1
* @param list<Selector> $selectors2
*/
public function getComments()
private static function selectorsMatch(array $selectors1, array $selectors2): bool
{
return $this->aComments;
$selectorStrings1 = self::getSelectorStrings($selectors1);
$selectorStrings2 = self::getSelectorStrings($selectors2);
\sort($selectorStrings1);
\sort($selectorStrings2);
return $selectorStrings1 === $selectorStrings2;
}
/**
* @param array<array-key, Comment> $aComments
* @param list<Selector> $selectors
*
* @return void
* @return list<string>
*/
public function setComments(array $aComments)
private static function getSelectorStrings(array $selectors): array
{
$this->aComments = $aComments;
return \array_map(
static function (Selector $selector): string {
return $selector->getSelector();
},
$selectors
);
}
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Renderable;
/**
* Represents anything that can be in the `$contents` of a `CSSList`.
*
* The interface does not define any methods to implement.
* It's purpose is to allow a single type to be specified for `CSSList::$contents` and manipulation methods thereof.
* It extends `Commentable` and `Renderable` because all `CSSListItem`s are both.
* This allows implementations to call methods from those interfaces without any additional type checks.
*/
interface CSSListItem extends Commentable, Renderable {}
+16 -102
View File
@@ -1,13 +1,13 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
/**
* This class represents the root of a parsed CSS file. It contains all top-level CSS contents: mostly declaration
@@ -16,64 +16,16 @@ use Sabberworm\CSS\RuleSet\RuleSet;
class Document extends CSSBlockList
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
/**
* @return Document
*
* @throws SourceException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): Document
{
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
$document = new Document($parserState->currentLine());
CSSList::parseList($parserState, $document);
/**
* Gets all `DeclarationBlock` objects recursively, no matter how deeply nested the selectors are.
* Aliased as `getAllSelectors()`.
*
* @return array<int, DeclarationBlock>
*/
public function getAllDeclarationBlocks()
{
/** @var array<int, DeclarationBlock> $aResult */
$aResult = [];
$this->allDeclarationBlocks($aResult);
return $aResult;
}
/**
* Gets all `DeclarationBlock` objects recursively.
*
* @return array<int, DeclarationBlock>
*
* @deprecated will be removed in version 9.0; use `getAllDeclarationBlocks()` instead
*/
public function getAllSelectors()
{
return $this->getAllDeclarationBlocks();
}
/**
* Returns all `RuleSet` objects recursively found in the tree, no matter how deeply nested the rule sets are.
*
* @return array<int, RuleSet>
*/
public function getAllRuleSets()
{
/** @var array<int, RuleSet> $aResult */
$aResult = [];
$this->allRuleSets($aResult);
return $aResult;
return $document;
}
/**
@@ -82,69 +34,31 @@ class Document extends CSSBlockList
* Note that this does not yield the full `DeclarationBlock` that the selector belongs to
* (and, currently, there is no way to get to that).
*
* @param string|null $sSpecificitySearch
* @param string|null $specificitySearch
* An optional filter by specificity.
* May contain a comparison operator and a number or just a number (defaults to "==").
*
* @return array<int, Selector>
* @return list<Selector>
*
* @example `getSelectorsBySpecificity('>= 100')`
*
*/
public function getSelectorsBySpecificity($sSpecificitySearch = null)
public function getSelectorsBySpecificity(?string $specificitySearch = null): array
{
/** @var array<int, Selector> $aResult */
$aResult = [];
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
/**
* Expands all shorthand properties to their long value.
*
* @return void
*
* @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function expandShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
/**
* Create shorthands properties whenever possible.
*
* @return void
*
* @deprecated since 8.7.0, will be removed without substitution in version 9.0 in #511
*/
public function createShorthands()
{
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
return $this->getAllSelectors($specificitySearch);
}
/**
* Overrides `render()` to make format argument optional.
*
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat = null)
public function render(?OutputFormat $outputFormat = null): string
{
if ($oOutputFormat === null) {
$oOutputFormat = new OutputFormat();
if ($outputFormat === null) {
$outputFormat = new OutputFormat();
}
return $oOutputFormat->comments($this) . $this->renderListContents($oOutputFormat);
return $outputFormat->getFormatter()->comments($this) . $this->renderListContents($outputFormat);
}
/**
* @return bool
*/
public function isRootList()
public function isRootList(): bool
{
return true;
}
+27 -49
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\OutputFormat;
@@ -8,101 +10,77 @@ use Sabberworm\CSS\Property\AtRule;
class KeyFrame extends CSSList implements AtRule
{
/**
* @var string|null
* @var non-empty-string
*/
private $vendorKeyFrame;
private $vendorKeyFrame = 'keyframes';
/**
* @var string|null
* @var non-empty-string
*/
private $animationName;
private $animationName = 'none';
/**
* @param int $iLineNo
* @param non-empty-string $vendorKeyFrame
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
/**
* @param string $vendorKeyFrame
*/
public function setVendorKeyFrame($vendorKeyFrame)
public function setVendorKeyFrame(string $vendorKeyFrame): void
{
$this->vendorKeyFrame = $vendorKeyFrame;
}
/**
* @return string|null
* @return non-empty-string
*/
public function getVendorKeyFrame()
public function getVendorKeyFrame(): string
{
return $this->vendorKeyFrame;
}
/**
* @param string $animationName
* @param non-empty-string $animationName
*/
public function setAnimationName($animationName)
public function setAnimationName(string $animationName): void
{
$this->animationName = $animationName;
}
/**
* @return string|null
* @return non-empty-string
*/
public function getAnimationName()
public function getAnimationName(): string
{
return $this->animationName;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
$formatter = $outputFormat->getFormatter();
$result = $formatter->comments($this);
$result .= "@{$this->vendorKeyFrame} {$this->animationName}{$formatter->spaceBeforeOpeningBrace()}{";
$result .= $this->renderListContents($outputFormat);
$result .= '}';
return $result;
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sResult .= "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderListContents($oOutputFormat);
$sResult .= '}';
return $sResult;
}
/**
* @return bool
*/
public function isRootList()
public function isRootList(): bool
{
return false;
}
/**
* @return string|null
* @return non-empty-string
*/
public function atRuleName()
public function atRuleName(): string
{
return $this->vendorKeyFrame;
}
/**
* @return string|null
* @return non-empty-string
*/
public function atRuleArgs()
public function atRuleArgs(): string
{
return $this->animationName;
}
+32 -34
View File
@@ -1,68 +1,66 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\ShortClassNameProvider;
class Comment implements Positionable, Renderable
{
use Position;
use ShortClassNameProvider;
/**
* @var string
*
* @internal since 8.8.0
*/
protected $sComment;
protected $commentText;
/**
* @param string $sComment
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($sComment = '', $iLineNo = 0)
public function __construct(string $commentText = '', ?int $lineNumber = null)
{
$this->sComment = $sComment;
$this->setPosition($iLineNo);
$this->commentText = $commentText;
$this->setPosition($lineNumber);
}
public function getComment(): string
{
return $this->commentText;
}
public function setComment(string $commentText): void
{
$this->commentText = $commentText;
}
/**
* @return string
* @return non-empty-string
*/
public function getComment()
public function render(OutputFormat $outputFormat): string
{
return $this->sComment;
return '/*' . $this->commentText . '*/';
}
/**
* @param string $sComment
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function setComment($sComment)
public function getArrayRepresentation(): array
{
$this->sComment = $sComment;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return '/*' . $this->sComment . '*/';
return [
'class' => $this->getShortClassName(),
// "contents" is the term used in the W3C specs:
// https://www.w3.org/TR/CSS22/syndata.html#comments
'contents' => $this->commentText,
];
}
}
@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Comment;
/**
* Provides a standard reusable implementation of `Commentable`.
*
* @internal
*
* @phpstan-require-implements Commentable
*/
trait CommentContainer
{
/**
* @var list<Comment>
*/
protected $comments = [];
/**
* @param list<Comment> $comments
*/
public function addComments(array $comments): void
{
$this->comments = \array_merge($this->comments, $comments);
}
/**
* @return list<Comment>
*/
public function getComments(): array
{
return $this->comments;
}
/**
* @param list<Comment> $comments
*/
public function setComments(array $comments): void
{
$this->comments = $comments;
}
}
+11 -10
View File
@@ -1,25 +1,26 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Comment;
/**
* A standard implementation of this interface is available in the `CommentContainer` trait.
*/
interface Commentable
{
/**
* @param array<array-key, Comment> $aComments
*
* @return void
* @param list<Comment> $comments
*/
public function addComments(array $aComments);
public function addComments(array $comments): void;
/**
* @return array<array-key, Comment>
* @return list<Comment>
*/
public function getComments();
public function getComments(): array;
/**
* @param array<array-key, Comment> $aComments
*
* @return void
* @param list<Comment> $comments
*/
public function setComments(array $aComments);
public function setComments(array $comments): void;
}
+556 -215
View File
@@ -1,32 +1,22 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
/**
* Extending this class is deprecated in version 8.8.0; it will be made `final` in version 9.0.0.
*
* @method OutputFormat setSemicolonAfterLastRule(bool $bSemicolonAfterLastRule) Set whether semicolons are added after
* last rule.
*/
class OutputFormat
final class OutputFormat
{
/**
* Value format: `"` means double-quote, `'` means single-quote
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
* @var '"'|"'"
*/
public $sStringQuotingType = '"';
private $stringQuotingType = '"';
/**
* Output RGB colors in hash notation if possible
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bRGBHashNotation = true;
private $usesRgbHashNotation = true;
/**
* Declaration format
@@ -34,404 +24,755 @@ class OutputFormat
* Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bSemicolonAfterLastRule = true;
private $renderSemicolonAfterLastRule = true;
/**
* Spacing
* Note that these strings are not sanity-checked: the value should only consist of whitespace
* Any newline character will be indented according to the current level.
* The triples (After, Before, Between) can be set using a wildcard (e.g. `$oFormat->set('Space*Rules', "\n");`)
* The triples (After, Before, Between) can be set using a wildcard
* (e.g. `$outputFormat->set('Space*Rules', "\n");`)
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterRuleName = ' ';
private $spaceAfterRuleName = ' ';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeRules = '';
private $spaceBeforeRules = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterRules = '';
private $spaceAfterRules = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBetweenRules = '';
private $spaceBetweenRules = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeBlocks = '';
private $spaceBeforeBlocks = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterBlocks = '';
private $spaceAfterBlocks = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBetweenBlocks = "\n";
private $spaceBetweenBlocks = "\n";
/**
* Content injected in and around at-rule blocks.
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sBeforeAtRuleBlock = '';
private $contentBeforeAtRuleBlock = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterAtRuleBlock = '';
private $contentAfterAtRuleBlock = '';
/**
* This is what’s printed before and after the comma if a declaration block contains multiple selectors.
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeSelectorSeparator = '';
private $spaceBeforeSelectorSeparator = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceAfterSelectorSeparator = ' ';
private $spaceAfterSelectorSeparator = ' ';
/**
* @var string
*/
private $spaceAroundSelectorCombinator = ' ';
/**
* This is what’s inserted before the separator in value lists, by default.
*
* `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
* To set the spacing for specific separators, use {@see $aSpaceBeforeListArgumentSeparators} instead.
*
* @var string|array<non-empty-string, string>
*
* @internal since 8.8.0, will be made private in 9.0.0
* @var string
*/
public $sSpaceBeforeListArgumentSeparator = '';
private $spaceBeforeListArgumentSeparator = '';
/**
* Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
*
* @var array<non-empty-string, string>
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $aSpaceBeforeListArgumentSeparators = [];
private $spaceBeforeListArgumentSeparators = [];
/**
* This is what’s inserted after the separator in value lists, by default.
*
* `array` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
* To set the spacing for specific separators, use {@see $aSpaceAfterListArgumentSeparators} instead.
*
* @var string|array<non-empty-string, string>
*
* @internal since 8.8.0, will be made private in 9.0.0
* @var string
*/
public $sSpaceAfterListArgumentSeparator = '';
private $spaceAfterListArgumentSeparator = '';
/**
* Keys are separators (e.g. `,`). Values are the space sequence to insert, or an empty string.
*
* @var array<non-empty-string, string>
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $aSpaceAfterListArgumentSeparators = [];
private $spaceAfterListArgumentSeparators = [];
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sSpaceBeforeOpeningBrace = ' ';
private $spaceBeforeOpeningBrace = ' ';
/**
* Content injected in and around declaration blocks.
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sBeforeDeclarationBlock = '';
private $contentBeforeDeclarationBlock = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterDeclarationBlockSelectors = '';
private $contentAfterDeclarationBlockSelectors = '';
/**
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sAfterDeclarationBlock = '';
private $contentAfterDeclarationBlock = '';
/**
* Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $sIndentation = "\t";
private $indentation = "\t";
/**
* Output exceptions.
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bIgnoreExceptions = false;
private $shouldIgnoreExceptions = false;
/**
* Render comments for lists and RuleSets
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bRenderComments = false;
private $shouldRenderComments = false;
/**
* @var OutputFormatter|null
*/
private $oFormatter = null;
private $outputFormatter;
/**
* @var OutputFormat|null
*/
private $oNextLevelFormat = null;
private $nextLevelFormat;
/**
* @var int
* @var int<0, max>
*/
private $iIndentationLevel = 0;
private $indentationLevel = 0;
/**
* @internal since V8.8.0. Use the factory methods `create()`, `createCompact()`, or `createPretty()` instead.
* @return '"'|"'"
*
* @internal
*/
public function __construct()
public function getStringQuotingType(): string
{
return $this->stringQuotingType;
}
/**
* @param string $sName
* @param '"'|"'" $quotingType
*
* @return string|null
*
* @deprecated since 8.8.0, will be removed in 9.0.0. Use specific getters instead.
* @return $this fluent interface
*/
public function get($sName)
public function setStringQuotingType(string $quotingType): self
{
$aVarPrefixes = ['a', 's', 'm', 'b', 'f', 'o', 'c', 'i'];
foreach ($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix . ucfirst($sName);
if (isset($this->$sFieldName)) {
return $this->$sFieldName;
}
}
return null;
$this->stringQuotingType = $quotingType;
return $this;
}
/**
* @param array<array-key, string>|string $aNames
* @param mixed $mValue
*
* @return self|false
*
* @deprecated since 8.8.0, will be removed in 9.0.0. Use specific setters instead.
* @internal
*/
public function set($aNames, $mValue)
public function usesRgbHashNotation(): bool
{
$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;
return $this->usesRgbHashNotation;
}
/**
* @param string $sMethodName
* @param array<array-key, mixed> $aArguments
*
* @return mixed
*
* @throws \Exception
* @return $this fluent interface
*/
public function __call($sMethodName, array $aArguments)
public function setRGBHashNotation(bool $usesRgbHashNotation): self
{
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)) {
// @deprecated since 8.8.0, will be removed in 9.0.0. Call the method on the formatter directly instead.
return call_user_func_array([$this->getFormatter(), $sMethodName], $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: ' . $sMethodName);
}
$this->usesRgbHashNotation = $usesRgbHashNotation;
return $this;
}
/**
* @param int $iNumber
*
* @return self
* @internal
*/
public function indentWithTabs($iNumber = 1)
public function shouldRenderSemicolonAfterLastRule(): bool
{
return $this->setIndentation(str_repeat("\t", $iNumber));
return $this->renderSemicolonAfterLastRule;
}
/**
* @param int $iNumber
*
* @return self
* @return $this fluent interface
*/
public function indentWithSpaces($iNumber = 2)
public function setSemicolonAfterLastRule(bool $renderSemicolonAfterLastRule): self
{
return $this->setIndentation(str_repeat(" ", $iNumber));
$this->renderSemicolonAfterLastRule = $renderSemicolonAfterLastRule;
return $this;
}
/**
* @return OutputFormat
* @internal
*/
public function getSpaceAfterRuleName(): string
{
return $this->spaceAfterRuleName;
}
/**
* @return $this fluent interface
*/
public function setSpaceAfterRuleName(string $whitespace): self
{
$this->spaceAfterRuleName = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceBeforeRules(): string
{
return $this->spaceBeforeRules;
}
/**
* @return $this fluent interface
*/
public function setSpaceBeforeRules(string $whitespace): self
{
$this->spaceBeforeRules = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceAfterRules(): string
{
return $this->spaceAfterRules;
}
/**
* @return $this fluent interface
*/
public function setSpaceAfterRules(string $whitespace): self
{
$this->spaceAfterRules = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceBetweenRules(): string
{
return $this->spaceBetweenRules;
}
/**
* @return $this fluent interface
*/
public function setSpaceBetweenRules(string $whitespace): self
{
$this->spaceBetweenRules = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceBeforeBlocks(): string
{
return $this->spaceBeforeBlocks;
}
/**
* @return $this fluent interface
*/
public function setSpaceBeforeBlocks(string $whitespace): self
{
$this->spaceBeforeBlocks = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceAfterBlocks(): string
{
return $this->spaceAfterBlocks;
}
/**
* @return $this fluent interface
*/
public function setSpaceAfterBlocks(string $whitespace): self
{
$this->spaceAfterBlocks = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceBetweenBlocks(): string
{
return $this->spaceBetweenBlocks;
}
/**
* @return $this fluent interface
*/
public function setSpaceBetweenBlocks(string $whitespace): self
{
$this->spaceBetweenBlocks = $whitespace;
return $this;
}
/**
* @internal
*/
public function getContentBeforeAtRuleBlock(): string
{
return $this->contentBeforeAtRuleBlock;
}
/**
* @return $this fluent interface
*/
public function setBeforeAtRuleBlock(string $content): self
{
$this->contentBeforeAtRuleBlock = $content;
return $this;
}
/**
* @internal
*/
public function getContentAfterAtRuleBlock(): string
{
return $this->contentAfterAtRuleBlock;
}
/**
* @return $this fluent interface
*/
public function setAfterAtRuleBlock(string $content): self
{
$this->contentAfterAtRuleBlock = $content;
return $this;
}
/**
* @internal
*/
public function getSpaceBeforeSelectorSeparator(): string
{
return $this->spaceBeforeSelectorSeparator;
}
/**
* @return $this fluent interface
*/
public function setSpaceBeforeSelectorSeparator(string $whitespace): self
{
$this->spaceBeforeSelectorSeparator = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceAfterSelectorSeparator(): string
{
return $this->spaceAfterSelectorSeparator;
}
/**
* @return $this fluent interface
*/
public function setSpaceAfterSelectorSeparator(string $whitespace): self
{
$this->spaceAfterSelectorSeparator = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceAroundSelectorCombinator(): string
{
return $this->spaceAroundSelectorCombinator;
}
/**
* The spacing set is also used for the descendent combinator, which is whitespace only,
* unless an empty string is set, in which case a space will be used.
*
* @return $this fluent interface
*/
public function setSpaceAroundSelectorCombinator(string $whitespace): self
{
$this->spaceAroundSelectorCombinator = $whitespace;
return $this;
}
/**
* @internal
*/
public function getSpaceBeforeListArgumentSeparator(): string
{
return $this->spaceBeforeListArgumentSeparator;
}
/**
* @return $this fluent interface
*/
public function setSpaceBeforeListArgumentSeparator(string $whitespace): self
{
$this->spaceBeforeListArgumentSeparator = $whitespace;
return $this;
}
/**
* @return array<non-empty-string, string>
*
* @internal
*/
public function getSpaceBeforeListArgumentSeparators(): array
{
return $this->spaceBeforeListArgumentSeparators;
}
/**
* @param array<non-empty-string, string> $separatorSpaces
*
* @return $this fluent interface
*/
public function setSpaceBeforeListArgumentSeparators(array $separatorSpaces): self
{
$this->spaceBeforeListArgumentSeparators = $separatorSpaces;
return $this;
}
/**
* @internal
*/
public function getSpaceAfterListArgumentSeparator(): string
{
return $this->spaceAfterListArgumentSeparator;
}
/**
* @return $this fluent interface
*/
public function setSpaceAfterListArgumentSeparator(string $whitespace): self
{
$this->spaceAfterListArgumentSeparator = $whitespace;
return $this;
}
/**
* @return array<non-empty-string, string>
*
* @internal
*/
public function getSpaceAfterListArgumentSeparators(): array
{
return $this->spaceAfterListArgumentSeparators;
}
/**
* @param array<non-empty-string, string> $separatorSpaces
*
* @return $this fluent interface
*/
public function setSpaceAfterListArgumentSeparators(array $separatorSpaces): self
{
$this->spaceAfterListArgumentSeparators = $separatorSpaces;
return $this;
}
/**
* @internal
*/
public function getSpaceBeforeOpeningBrace(): string
{
return $this->spaceBeforeOpeningBrace;
}
/**
* @return $this fluent interface
*/
public function setSpaceBeforeOpeningBrace(string $whitespace): self
{
$this->spaceBeforeOpeningBrace = $whitespace;
return $this;
}
/**
* @internal
*/
public function getContentBeforeDeclarationBlock(): string
{
return $this->contentBeforeDeclarationBlock;
}
/**
* @return $this fluent interface
*/
public function setBeforeDeclarationBlock(string $content): self
{
$this->contentBeforeDeclarationBlock = $content;
return $this;
}
/**
* @internal
*/
public function getContentAfterDeclarationBlockSelectors(): string
{
return $this->contentAfterDeclarationBlockSelectors;
}
/**
* @return $this fluent interface
*/
public function setAfterDeclarationBlockSelectors(string $content): self
{
$this->contentAfterDeclarationBlockSelectors = $content;
return $this;
}
/**
* @internal
*/
public function getContentAfterDeclarationBlock(): string
{
return $this->contentAfterDeclarationBlock;
}
/**
* @return $this fluent interface
*/
public function setAfterDeclarationBlock(string $content): self
{
$this->contentAfterDeclarationBlock = $content;
return $this;
}
/**
* @internal
*/
public function getIndentation(): string
{
return $this->indentation;
}
/**
* @return $this fluent interface
*/
public function setIndentation(string $indentation): self
{
$this->indentation = $indentation;
return $this;
}
/**
* @internal
*/
public function shouldIgnoreExceptions(): bool
{
return $this->shouldIgnoreExceptions;
}
/**
* @return $this fluent interface
*/
public function setIgnoreExceptions(bool $ignoreExceptions): self
{
$this->shouldIgnoreExceptions = $ignoreExceptions;
return $this;
}
/**
* @internal
*/
public function shouldRenderComments(): bool
{
return $this->shouldRenderComments;
}
/**
* @return $this fluent interface
*/
public function setRenderComments(bool $renderComments): self
{
$this->shouldRenderComments = $renderComments;
return $this;
}
/**
* @return int<0, max>
*
* @internal
*/
public function getIndentationLevel(): int
{
return $this->indentationLevel;
}
/**
* @param int<1, max> $numberOfTabs
*
* @return $this fluent interface
*/
public function indentWithTabs(int $numberOfTabs = 1): self
{
return $this->setIndentation(\str_repeat("\t", $numberOfTabs));
}
/**
* @param int<1, max> $numberOfSpaces
*
* @return $this fluent interface
*/
public function indentWithSpaces(int $numberOfSpaces = 2): self
{
return $this->setIndentation(\str_repeat(' ', $numberOfSpaces));
}
/**
* @internal since V8.8.0
*/
public function nextLevel()
public function nextLevel(): self
{
if ($this->oNextLevelFormat === null) {
$this->oNextLevelFormat = clone $this;
$this->oNextLevelFormat->iIndentationLevel++;
$this->oNextLevelFormat->oFormatter = null;
if ($this->nextLevelFormat === null) {
$this->nextLevelFormat = clone $this;
$this->nextLevelFormat->indentationLevel++;
$this->nextLevelFormat->outputFormatter = null;
}
return $this->oNextLevelFormat;
return $this->nextLevelFormat;
}
/**
* @return void
*/
public function beLenient()
public function beLenient(): void
{
$this->bIgnoreExceptions = true;
$this->shouldIgnoreExceptions = true;
}
/**
* @return OutputFormatter
*
* @internal since 8.8.0
*/
public function getFormatter()
public function getFormatter(): OutputFormatter
{
if ($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
if ($this->outputFormatter === null) {
$this->outputFormatter = new OutputFormatter($this);
}
return $this->oFormatter;
}
/**
* @return int
*
* @deprecated #869 since version V8.8.0, will be removed in V9.0.0. Use `getIndentationLevel()` instead.
*/
public function level()
{
return $this->iIndentationLevel;
return $this->outputFormatter;
}
/**
* Creates an instance of this class without any particular formatting settings.
*
* @return self
*/
public static function create()
public static function create(): self
{
return new OutputFormat();
}
/**
* Creates an instance of this class with a preset for compact formatting.
*
* @return self
*/
public static function createCompact()
public static function createCompact(): self
{
$format = self::create();
$format->set('Space*Rules', "")
->set('Space*Blocks', "")
$format
->setSpaceBeforeRules('')
->setSpaceBetweenRules('')
->setSpaceAfterRules('')
->setSpaceBeforeBlocks('')
->setSpaceBetweenBlocks('')
->setSpaceAfterBlocks('')
->setSpaceAfterRuleName('')
->setSpaceBeforeOpeningBrace('')
->setSpaceAfterSelectorSeparator('')
->setSpaceAroundSelectorCombinator('')
->setSemicolonAfterLastRule(false)
->setRenderComments(false);
return $format;
}
/**
* Creates an instance of this class with a preset for pretty formatting.
*
* @return self
*/
public static function createPretty()
public static function createPretty(): self
{
$format = self::create();
$format->set('Space*Rules', "\n")
->set('Space*Blocks', "\n")
$format
->setSpaceBeforeRules("\n")
->setSpaceBetweenRules("\n")
->setSpaceAfterRules("\n")
->setSpaceBeforeBlocks("\n")
->setSpaceBetweenBlocks("\n\n")
->set('SpaceAfterListArgumentSeparators', [',' => ' '])
->setSpaceAfterBlocks("\n")
->setSpaceAfterListArgumentSeparators([',' => ' '])
->setRenderComments(true);
return $format;
}
}
+113 -146
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
use Sabberworm\CSS\Comment\Commentable;
@@ -13,256 +15,221 @@ class OutputFormatter
/**
* @var OutputFormat
*/
private $oFormat;
private $outputFormat;
public function __construct(OutputFormat $oFormat)
public function __construct(OutputFormat $outputFormat)
{
$this->oFormat = $oFormat;
$this->outputFormat = $outputFormat;
}
/**
* @param string $sName
* @param string|null $sType
* @param non-empty-string $name
*
* @return string
* @throws \InvalidArgumentException
*/
public function space($sName, $sType = null)
public function space(string $name): string
{
$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);
}
switch ($name) {
case 'AfterRuleName':
$spaceString = $this->outputFormat->getSpaceAfterRuleName();
break;
case 'BeforeRules':
$spaceString = $this->outputFormat->getSpaceBeforeRules();
break;
case 'AfterRules':
$spaceString = $this->outputFormat->getSpaceAfterRules();
break;
case 'BetweenRules':
$spaceString = $this->outputFormat->getSpaceBetweenRules();
break;
case 'BeforeBlocks':
$spaceString = $this->outputFormat->getSpaceBeforeBlocks();
break;
case 'AfterBlocks':
$spaceString = $this->outputFormat->getSpaceAfterBlocks();
break;
case 'BetweenBlocks':
$spaceString = $this->outputFormat->getSpaceBetweenBlocks();
break;
case 'BeforeSelectorSeparator':
$spaceString = $this->outputFormat->getSpaceBeforeSelectorSeparator();
break;
case 'AfterSelectorSeparator':
$spaceString = $this->outputFormat->getSpaceAfterSelectorSeparator();
break;
case 'BeforeOpeningBrace':
$spaceString = $this->outputFormat->getSpaceBeforeOpeningBrace();
break;
case 'BeforeListArgumentSeparator':
$spaceString = $this->outputFormat->getSpaceBeforeListArgumentSeparator();
break;
case 'AfterListArgumentSeparator':
$spaceString = $this->outputFormat->getSpaceAfterListArgumentSeparator();
break;
default:
throw new \InvalidArgumentException("Unknown space type: $name", 1740049248);
}
return $this->prepareSpace($sSpaceString);
return $this->prepareSpace($spaceString);
}
/**
* @return string
*/
public function spaceAfterRuleName()
public function spaceAfterRuleName(): string
{
return $this->space('AfterRuleName');
}
/**
* @return string
*/
public function spaceBeforeRules()
public function spaceBeforeRules(): string
{
return $this->space('BeforeRules');
}
/**
* @return string
*/
public function spaceAfterRules()
public function spaceAfterRules(): string
{
return $this->space('AfterRules');
}
/**
* @return string
*/
public function spaceBetweenRules()
public function spaceBetweenRules(): string
{
return $this->space('BetweenRules');
}
/**
* @return string
*/
public function spaceBeforeBlocks()
public function spaceBeforeBlocks(): string
{
return $this->space('BeforeBlocks');
}
/**
* @return string
*/
public function spaceAfterBlocks()
public function spaceAfterBlocks(): string
{
return $this->space('AfterBlocks');
}
/**
* @return string
*/
public function spaceBetweenBlocks()
public function spaceBetweenBlocks(): string
{
return $this->space('BetweenBlocks');
}
/**
* @return string
*/
public function spaceBeforeSelectorSeparator()
public function spaceBeforeSelectorSeparator(): string
{
return $this->space('BeforeSelectorSeparator');
}
/**
* @return string
*/
public function spaceAfterSelectorSeparator()
public function spaceAfterSelectorSeparator(): string
{
return $this->space('AfterSelectorSeparator');
}
/**
* @param string $sSeparator
*
* @return string
* @param non-empty-string $separator
*/
public function spaceBeforeListArgumentSeparator($sSeparator)
public function spaceBeforeListArgumentSeparator(string $separator): string
{
$spaceForSeparator = $this->oFormat->getSpaceBeforeListArgumentSeparators();
if (isset($spaceForSeparator[$sSeparator])) {
return $spaceForSeparator[$sSeparator];
}
$spaceForSeparator = $this->outputFormat->getSpaceBeforeListArgumentSeparators();
return $this->space('BeforeListArgumentSeparator', $sSeparator);
return $spaceForSeparator[$separator] ?? $this->space('BeforeListArgumentSeparator');
}
/**
* @param string $sSeparator
*
* @return string
* @param non-empty-string $separator
*/
public function spaceAfterListArgumentSeparator($sSeparator)
public function spaceAfterListArgumentSeparator(string $separator): string
{
$spaceForSeparator = $this->oFormat->getSpaceAfterListArgumentSeparators();
if (isset($spaceForSeparator[$sSeparator])) {
return $spaceForSeparator[$sSeparator];
}
$spaceForSeparator = $this->outputFormat->getSpaceAfterListArgumentSeparators();
return $this->space('AfterListArgumentSeparator', $sSeparator);
return $spaceForSeparator[$separator] ?? $this->space('AfterListArgumentSeparator');
}
/**
* @return string
*/
public function spaceBeforeOpeningBrace()
public function spaceBeforeOpeningBrace(): string
{
return $this->space('BeforeOpeningBrace');
}
/**
* Runs the given code, either swallowing or passing exceptions, depending on the `bIgnoreExceptions` setting.
*
* @param string $cCode the name of the function to call
*
* @return string|null
* Runs the given code, either swallowing or passing exceptions, depending on the `ignoreExceptions` setting.
*/
public function safely($cCode)
public function safely(callable $callable): ?string
{
if ($this->oFormat->get('IgnoreExceptions')) {
if ($this->outputFormat->shouldIgnoreExceptions()) {
// If output exceptions are ignored, run the code with exception guards
try {
return $cCode();
return $callable();
} catch (OutputException $e) {
return null;
} // Do nothing
} else {
// Run the code as-is
return $cCode();
return $callable();
}
}
/**
* Clone of the `implode` function, but calls `render` with the current output format instead of `__toString()`.
* Clone of the `implode` function, but calls `render` with the current output format.
*
* @param string $sSeparator
* @param array<array-key, Renderable|string> $aValues
* @param bool $bIncreaseLevel
*
* @return string
* @param array<array-key, Renderable|string> $values
*/
public function implode($sSeparator, array $aValues, $bIncreaseLevel = false)
public function implode(string $separator, array $values, bool $increaseLevel = false): string
{
$sResult = '';
$oFormat = $this->oFormat;
if ($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
$result = '';
$outputFormat = $this->outputFormat;
if ($increaseLevel) {
$outputFormat = $outputFormat->nextLevel();
}
$bIsFirst = true;
foreach ($aValues as $mValue) {
if ($bIsFirst) {
$bIsFirst = false;
$isFirst = true;
foreach ($values as $value) {
if ($isFirst) {
$isFirst = false;
} else {
$sResult .= $sSeparator;
$result .= $separator;
}
if ($mValue instanceof Renderable) {
$sResult .= $mValue->render($oFormat);
if ($value instanceof Renderable) {
$result .= $value->render($outputFormat);
} else {
$sResult .= $mValue;
$result .= $value;
}
}
return $sResult;
return $result;
}
/**
* @param string $sString
*
* @return string
*/
public function removeLastSemicolon($sString)
public function removeLastSemicolon(string $string): string
{
if ($this->oFormat->get('SemicolonAfterLastRule')) {
return $sString;
if ($this->outputFormat->shouldRenderSemicolonAfterLastRule()) {
return $string;
}
$sString = explode(';', $sString);
if (count($sString) < 2) {
return $sString[0];
$parts = \explode(';', $string);
if (\count($parts) < 2) {
return $parts[0];
}
$sLast = array_pop($sString);
$sNextToLast = array_pop($sString);
array_push($sString, $sNextToLast . $sLast);
return implode(';', $sString);
$lastPart = \array_pop($parts);
$nextToLastPart = \array_pop($parts);
\array_push($parts, $nextToLastPart . $lastPart);
return \implode(';', $parts);
}
/**
*
* @param array<Commentable> $aComments
*
* @return string
*/
public function comments(Commentable $oCommentable)
public function comments(Commentable $commentable): string
{
if (!$this->oFormat->bRenderComments) {
if (!$this->outputFormat->shouldRenderComments()) {
return '';
}
$sResult = '';
$aComments = $oCommentable->getComments();
$iLastCommentIndex = count($aComments) - 1;
$result = '';
$comments = $commentable->getComments();
$lastCommentIndex = \count($comments) - 1;
foreach ($aComments as $i => $oComment) {
$sResult .= $oComment->render($this->oFormat);
$sResult .= $i === $iLastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
foreach ($comments as $i => $comment) {
$result .= $comment->render($this->outputFormat);
$result .= $i === $lastCommentIndex ? $this->spaceAfterBlocks() : $this->spaceBetweenBlocks();
}
return $sResult;
return $result;
}
/**
* @param string $sSpaceString
*
* @return string
*/
private function prepareSpace($sSpaceString)
private function prepareSpace(string $spaceString): string
{
return str_replace("\n", "\n" . $this->indent(), $sSpaceString);
return \str_replace("\n", "\n" . $this->indent(), $spaceString);
}
/**
* @return string
*/
private function indent()
private function indent(): string
{
return str_repeat($this->oFormat->sIndentation, $this->oFormat->getIndentationLevel());
return \str_repeat($this->outputFormat->getIndentation(), $this->outputFormat->getIndentationLevel());
}
}
+11 -39
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
use Sabberworm\CSS\CSSList\Document;
@@ -14,57 +16,27 @@ class Parser
/**
* @var ParserState
*/
private $oParserState;
private $parserState;
/**
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
* @param Settings|null $oParserSettings
* @param int $iLineNo the line number (starting from 1, not from 0)
* @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
* @param int<1, max> $lineNumber the line number (starting from 1, not from 0)
*/
public function __construct($sText, $oParserSettings = null, $iLineNo = 1)
public function __construct(string $text, ?Settings $parserSettings = null, int $lineNumber = 1)
{
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
if ($parserSettings === null) {
$parserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sCharset
*
* @return void
*
* @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
*/
public function setCharset($sCharset)
{
$this->oParserState->setCharset($sCharset);
}
/**
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
*
* @return void
*
* @deprecated since 8.7.0, will be removed in version 9.0.0 with #687
*/
public function getCharset()
{
// Note: The `return` statement is missing here. This is a bug that needs to be fixed.
$this->oParserState->getCharset();
$this->parserState = new ParserState($text, $parserSettings, $lineNumber);
}
/**
* Parses the CSS provided to the constructor and creates a `Document` from it.
*
* @return Document
*
* @throws SourceException
*/
public function parse()
public function parse(): Document
{
return Document::parse($this->oParserState);
return Document::parse($this->parserState);
}
}
+12 -14
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
/**
@@ -8,30 +10,26 @@ namespace Sabberworm\CSS\Parsing;
class Anchor
{
/**
* @var int
* @var int<0, max>
*/
private $iPosition;
private $position;
/**
* @var \Sabberworm\CSS\Parsing\ParserState
* @var ParserState
*/
private $oParserState;
private $parserState;
/**
* @param int $iPosition
* @param \Sabberworm\CSS\Parsing\ParserState $oParserState
* @param int<0, max> $position
*/
public function __construct($iPosition, ParserState $oParserState)
public function __construct(int $position, ParserState $parserState)
{
$this->iPosition = $iPosition;
$this->oParserState = $oParserState;
$this->position = $position;
$this->parserState = $parserState;
}
/**
* @return void
*/
public function backtrack()
public function backtrack(): void
{
$this->oParserState->setPosition($this->iPosition);
$this->parserState->setPosition($this->position);
}
}
@@ -1,18 +1,10 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parser attempts to print something invalid.
*/
class OutputException extends SourceException
{
/**
* @param string $sMessage
* @param int $iLineNo
*/
public function __construct($sMessage, $iLineNo = 0)
{
parent::__construct($sMessage, $iLineNo);
}
}
final class OutputException extends SourceException {}
+254 -305
View File
@@ -1,220 +1,173 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Settings;
use function Safe\iconv;
use function Safe\preg_match;
use function Safe\preg_split;
/**
* @internal since 8.7.0
*/
class ParserState
{
/**
* @var null
*
* @internal since 8.5.2
*/
const EOF = null;
public const EOF = null;
/**
* @var Settings
*/
private $oParserSettings;
private $parserSettings;
/**
* @var string
*/
private $sText;
private $text;
/**
* @var array<int, string>
*/
private $aText;
private $characters;
/**
* @var int
* @var int<0, max>
*/
private $iCurrentPosition;
private $currentPosition = 0;
/**
* will only be used if the CSS does not contain an `@charset` declaration
*
* @var string
*/
private $sCharset;
private $charset;
/**
* @var int
* @var int<1, max> $lineNumber
*/
private $iLength;
private $lineNumber;
/**
* @var int
* @param string $text the complete CSS as text (i.e., usually the contents of a CSS file)
* @param int<1, max> $lineNumber
*/
private $iLineNo;
/**
* @param string $sText the complete CSS as text (i.e., usually the contents of a CSS file)
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
public function __construct(string $text, Settings $parserSettings, int $lineNumber = 1)
{
$this->oParserSettings = $oParserSettings;
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
$this->setCharset($this->oParserSettings->sDefaultCharset);
$this->parserSettings = $parserSettings;
$this->text = $text;
$this->lineNumber = $lineNumber;
$this->setCharset($this->parserSettings->getDefaultCharset());
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sCharset
*
* @return void
*/
public function setCharset($sCharset)
public function setCharset(string $charset): void
{
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
if (is_array($this->aText)) {
$this->iLength = count($this->aText);
}
$this->charset = $charset;
$this->characters = $this->strsplit($this->text);
}
/**
* Returns the charset that is used if the CSS does not contain an `@charset` declaration.
*
* @return string
* @return int<1, max>
*/
public function getCharset()
public function currentLine(): int
{
return $this->sCharset;
return $this->lineNumber;
}
/**
* @return int
* @return int<0, max>
*/
public function currentLine()
public function currentColumn(): int
{
return $this->iLineNo;
return $this->currentPosition;
}
public function getSettings(): Settings
{
return $this->parserSettings;
}
public function anchor(): Anchor
{
return new Anchor($this->currentPosition, $this);
}
/**
* @return int
* @param int<0, max> $position
*/
public function currentColumn()
public function setPosition(int $position): void
{
return $this->iCurrentPosition;
$this->currentPosition = $position;
}
/**
* @return Settings
*/
public function getSettings()
{
return $this->oParserSettings;
}
/**
* @return \Sabberworm\CSS\Parsing\Anchor
*/
public function anchor()
{
return new Anchor($this->iCurrentPosition, $this);
}
/**
* @param int $iPosition
*
* @return void
*/
public function setPosition($iPosition)
{
$this->iCurrentPosition = $iPosition;
}
/**
* @param bool $bIgnoreCase
*
* @return string
* @return non-empty-string
*
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public function parseIdentifier($bIgnoreCase = true)
public function parseIdentifier(bool $ignoreCase = true): string
{
if ($this->isEnd()) {
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
throw new UnexpectedEOFException('', '', 'identifier', $this->lineNumber);
}
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
$result = $this->parseCharacter(true);
if ($result === null) {
throw new UnexpectedTokenException('', $this->peek(5), 'identifier', $this->lineNumber);
}
$sCharacter = null;
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
$sResult .= $sCharacter;
while (!$this->isEnd() && ($character = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_-]/Sux', $character) !== 0) {
$result .= $character;
} else {
$sResult .= '\\' . $sCharacter;
$result .= '\\' . $character;
}
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
if ($ignoreCase) {
$result = $this->strtolower($result);
}
return $sResult;
return $result;
}
/**
* @param bool $bIsForIdentifier
*
* @return string|null
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public function parseCharacter($bIsForIdentifier)
public function parseCharacter(bool $isForIdentifier): ?string
{
if ($this->peek() === '\\') {
if (
$bIsForIdentifier && $this->oParserSettings->bLenientParsing
&& ($this->comes('\0') || $this->comes('\9'))
) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\n') || $this->comes('\r')) {
if ($this->comes('\\n') || $this->comes('\\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
$hexCodePoint = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($hexCodePoint) < 6) {
// Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('\r\n')) {
if (preg_match('/\\s/isSu', $this->peek()) !== 0) {
if ($this->comes('\\r\\n')) {
$this->consume(2);
} else {
$this->consume(1);
}
}
}
$iUnicode = intval($sUnicode, 16);
$sUtf32 = "";
$codePoint = \intval($hexCodePoint, 16);
$utf32EncodedCharacter = '';
for ($i = 0; $i < 4; ++$i) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
$utf32EncodedCharacter .= \chr($codePoint & 0xff);
$codePoint = $codePoint >> 8;
}
return iconv('utf-32le', $this->sCharset, $sUtf32);
return iconv('utf-32le', $this->charset, $utf32EncodedCharacter);
}
if ($bIsForIdentifier) {
$peek = ord($this->peek());
if ($isForIdentifier) {
$peek = \ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (
($peek >= 97 && $peek <= 122)
@@ -229,116 +182,147 @@ class ParserState
} else {
return $this->consume(1);
}
return null;
}
/**
* @return array<int, Comment>|void
* Consumes whitespace and/or comments until the next non-whitespace character that isn't a slash opening a comment.
*
* @param list<Comment> $comments Any comments consumed will be appended to this array.
*
* @return string the whitespace consumed, without the comments
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @phpstan-impure
* This method may change the state of the object by advancing the internal position;
* it does not simply 'get' a value.
*/
public function consumeWhiteSpace()
public function consumeWhiteSpace(array &$comments = []): string
{
$aComments = [];
$consumed = '';
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
$consumed .= $this->consume(1);
}
if ($this->oParserSettings->bLenientParsing) {
if ($this->parserSettings->usesLenientParsing()) {
try {
$oComment = $this->consumeComment();
$comment = $this->consumeComment();
} catch (UnexpectedEOFException $e) {
$this->iCurrentPosition = $this->iLength;
return $aComments;
$this->currentPosition = \count($this->characters);
break;
}
} else {
$oComment = $this->consumeComment();
$comment = $this->consumeComment();
}
if ($oComment !== false) {
$aComments[] = $oComment;
if ($comment instanceof Comment) {
$comments[] = $comment;
}
} while ($oComment !== false);
return $aComments;
} while ($comment instanceof Comment);
return $consumed;
}
/**
* @param string $sString
* @param bool $bCaseInsensitive
*
* @return bool
* @param non-empty-string $string
*/
public function comes($sString, $bCaseInsensitive = false)
public function comes(string $string, bool $caseInsensitive = false): bool
{
$sPeek = $this->peek(strlen($sString));
return ($sPeek == '')
? false
: $this->streql($sPeek, $sString, $bCaseInsensitive);
$peek = $this->peek(\strlen($string));
return ($peek !== '') && $this->streql($peek, $string, $caseInsensitive);
}
/**
* @param int $iLength
* @param int $iOffset
*
* @return string
* @param int<1, max> $length
* @param int<0, max> $offset
*/
public function peek($iLength = 1, $iOffset = 0)
public function peek(int $length = 1, int $offset = 0): string
{
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
$offset += $this->currentPosition;
if ($offset >= \count($this->characters)) {
return '';
}
return $this->substr($iOffset, $iLength);
return $this->substr($offset, $length);
}
/**
* @param int $mValue
*
* @return string
* @param string|int<1, max> $value
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consume($mValue = 1)
public function consume($value = 1): string
{
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
if (\is_string($value)) {
$numberOfLines = \substr_count($value, "\n");
$length = $this->strlen($value);
if (!$this->streql($this->substr($this->currentPosition, $length), $value)) {
throw new UnexpectedTokenException(
$value,
$this->peek(\max($length, 5)),
'literal',
$this->lineNumber
);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
$this->lineNumber += $numberOfLines;
$this->currentPosition += $this->strlen($value);
$result = $value;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
if ($this->currentPosition + $value > \count($this->characters)) {
throw new UnexpectedEOFException((string) $value, $this->peek(5), 'count', $this->lineNumber);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
$result = $this->substr($this->currentPosition, $value);
$numberOfLines = \substr_count($result, "\n");
$this->lineNumber += $numberOfLines;
$this->currentPosition += $value;
}
return $result;
}
/**
* @param string $mExpression
* @param int|null $iMaxLength
* If the possibly-expected next content is next, consume it.
*
* @return string
* @param non-empty-string $nextContent
*
* @return bool whether the possibly-expected content was found and consumed
*/
public function consumeIfComes(string $nextContent): bool
{
$length = $this->strlen($nextContent);
if (!$this->streql($this->substr($this->currentPosition, $length), $nextContent)) {
return false;
}
$numberOfLines = \substr_count($nextContent, "\n");
$this->lineNumber += $numberOfLines;
$this->currentPosition += $this->strlen($nextContent);
return true;
}
/**
* @param string $expression
* @param int<1, max>|null $maximumLength
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeExpression($mExpression, $iMaxLength = null)
public function consumeExpression(string $expression, ?int $maximumLength = null): string
{
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
$matches = null;
$input = ($maximumLength !== null) ? $this->peek($maximumLength) : $this->inputLeft();
if (preg_match($expression, $input, $matches, PREG_OFFSET_CAPTURE) !== 1) {
throw new UnexpectedTokenException($expression, $this->peek(5), 'expression', $this->lineNumber);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
return $this->consume($matches[0][0]);
}
/**
@@ -346,13 +330,14 @@ class ParserState
*/
public function consumeComment()
{
$mComment = false;
$lineNumber = $this->lineNumber;
$comment = null;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
$comment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
$comment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
@@ -360,193 +345,157 @@ class ParserState
}
}
if ($mComment !== false) {
// We skip the * which was included in the comment.
return new Comment(substr($mComment, 1), $iLineNo);
}
return $mComment;
// We skip the * which was included in the comment.
return \is_string($comment) ? new Comment(\substr($comment, 1), $lineNumber) : false;
}
/**
* @return bool
*/
public function isEnd()
public function isEnd(): bool
{
return $this->iCurrentPosition >= $this->iLength;
return $this->currentPosition >= \count($this->characters);
}
/**
* @param array<array-key, string>|string $aEnd
* @param string $bIncludeEnd
* @param string $consumeEnd
* @param array<int, Comment> $comments
*
* @return string
* @param list<string|self::EOF>|string|self::EOF $stopCharacters
* @param list<Comment> $comments
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = [])
{
$aEnd = is_array($aEnd) ? $aEnd : [$aEnd];
$out = '';
$start = $this->iCurrentPosition;
public function consumeUntil(
$stopCharacters,
bool $includeEnd = false,
bool $consumeEnd = false,
array &$comments = []
): string {
$stopCharacters = \is_array($stopCharacters) ? $stopCharacters : [$stopCharacters];
$consumedCharacters = '';
$start = $this->currentPosition;
$comments = \array_merge($comments, $this->consumeComments());
while (!$this->isEnd()) {
$char = $this->consume(1);
if (in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
$character = $this->consume(1);
if (\in_array($character, $stopCharacters, true)) {
if ($includeEnd) {
$consumedCharacters .= $character;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
$this->currentPosition -= $this->strlen($character);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
return $consumedCharacters;
}
$consumedCharacters .= $character;
$comments = \array_merge($comments, $this->consumeComments());
}
if (in_array(self::EOF, $aEnd)) {
return $out;
if (\in_array(self::EOF, $stopCharacters, true)) {
return $consumedCharacters;
}
$this->iCurrentPosition = $start;
$this->currentPosition = $start;
throw new UnexpectedEOFException(
'One of ("' . implode('","', $aEnd) . '")',
'One of ("' . \implode('","', $stopCharacters) . '")',
$this->peek(5),
'search',
$this->iLineNo
$this->lineNumber
);
}
/**
* @return string
*/
private function inputLeft()
private function inputLeft(): string
{
return $this->substr($this->iCurrentPosition, -1);
return $this->substr($this->currentPosition, -1);
}
public function streql(string $string1, string $string2, bool $caseInsensitive = true): bool
{
return $caseInsensitive
? ($this->strtolower($string1) === $this->strtolower($string2))
: ($string1 === $string2);
}
/**
* @param string $sString1
* @param string $sString2
* @param bool $bCaseInsensitive
*
* @return bool
* @param int<1, max> $numberOfCharacters
*/
public function streql($sString1, $sString2, $bCaseInsensitive = true)
public function backtrack(int $numberOfCharacters): void
{
if ($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
$this->currentPosition -= $numberOfCharacters;
}
/**
* @param int $iAmount
*
* @return void
* @return int<0, max>
*/
public function backtrack($iAmount)
public function strlen(string $string): int
{
$this->iCurrentPosition -= $iAmount;
return $this->parserSettings->hasMultibyteSupport()
? \mb_strlen($string, $this->charset)
: \strlen($string);
}
/**
* @param string $sString
*
* @return int
* @param int<0, max> $offset
*/
public function strlen($sString)
private function substr(int $offset, int $length): string
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strlen($sString, $this->sCharset);
} else {
return strlen($sString);
if ($length < 0) {
$length = \count($this->characters) - $offset + $length;
}
if ($offset + $length > \count($this->characters)) {
$length = \count($this->characters) - $offset;
}
$result = '';
while ($length > 0) {
$result .= $this->characters[$offset];
$offset++;
$length--;
}
return $result;
}
/**
* @param int $iStart
* @param int $iLength
*
* @return string
* @return ($string is non-empty-string ? non-empty-string : string)
*/
private function substr($iStart, $iLength)
private function strtolower(string $string): string
{
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;
return $this->parserSettings->hasMultibyteSupport()
? \mb_strtolower($string, $this->charset)
: \strtolower($string);
}
/**
* @param string $sString
*
* @return string
* @return list<string>
*/
private function strtolower($sString)
private function strsplit(string $string): array
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strtolower($sString, $this->sCharset);
} else {
return strtolower($sString);
}
}
/**
* @param string $sString
*
* @return array<int, string>
*/
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);
if ($this->parserSettings->hasMultibyteSupport()) {
if ($this->streql($this->charset, 'utf-8')) {
$result = preg_split('//u', $string, -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);
$length = \mb_strlen($string, $this->charset);
$result = [];
for ($i = 0; $i < $length; ++$i) {
$result[] = \mb_substr($string, $i, 1, $this->charset);
}
return $aResult;
}
} else {
if ($sString === '') {
return [];
} else {
return str_split($sString);
}
$result = ($string !== '') ? \str_split($string) : [];
}
return $result;
}
/**
* @param string $sString
* @param string $sNeedle
* @param int $iOffset
*
* @return int|false
* @return list<Comment>
*/
private function strpos($sString, $sNeedle, $iOffset)
private function consumeComments(): array
{
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return strpos($sString, $sNeedle, $iOffset);
$comments = [];
while (true) {
$comment = $this->consumeComment();
if ($comment instanceof Comment) {
$comments[] = $comment;
} else {
return $comments;
}
}
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
use Sabberworm\CSS\Position\Position;
@@ -10,15 +12,14 @@ class SourceException extends \Exception implements Positionable
use Position;
/**
* @param string $sMessage
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($sMessage, $iLineNo = 0)
public function __construct(string $message, ?int $lineNumber = null)
{
$this->setPosition($iLineNo);
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
$this->setPosition($lineNumber);
if ($lineNumber !== null) {
$message .= " [line no: $lineNumber]";
}
parent::__construct($sMessage);
parent::__construct($message);
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
/**
@@ -7,6 +9,4 @@ namespace Sabberworm\CSS\Parsing;
*
* Extends `UnexpectedTokenException` in order to preserve backwards compatibility.
*/
class UnexpectedEOFException extends UnexpectedTokenException
{
}
final class UnexpectedEOFException extends UnexpectedTokenException {}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Parsing;
/**
@@ -8,44 +10,22 @@ namespace Sabberworm\CSS\Parsing;
class UnexpectedTokenException extends SourceException
{
/**
* @var string
* @param 'literal'|'identifier'|'count'|'expression'|'search'|'custom' $matchType
* @param int<1, max>|null $lineNumber
*/
private $sExpected;
/**
* @var string
*/
private $sFound;
/**
* Possible values: literal, identifier, count, expression, search
*
* @var string
*/
private $sMatchType;
/**
* @param string $sExpected
* @param string $sFound
* @param string $sMatchType
* @param int $iLineNo
*/
public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0)
public function __construct(string $expected, string $found, string $matchType = 'literal', ?int $lineNumber = null)
{
$this->sExpected = $sExpected;
$this->sFound = $sFound;
$this->sMatchType = $sMatchType;
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
if ($this->sMatchType === 'search') {
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} elseif ($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}”";
} elseif ($this->sMatchType === 'custom') {
$sMessage = trim("$sExpected $sFound");
$message = "Token “{$expected}” ({$matchType}) not found. Got “{$found}”.";
if ($matchType === 'search') {
$message = "Search for “{$expected}” returned no results. Context: “{$found}”.";
} elseif ($matchType === 'count') {
$message = "Next token was expected to have {$expected} chars. Context: “{$found}”.";
} elseif ($matchType === 'identifier') {
$message = "Identifier expected. Got “{$found}”";
} elseif ($matchType === 'custom') {
$message = \trim("$expected $found");
}
parent::__construct($sMessage, $iLineNo);
parent::__construct($message, $lineNumber);
}
}
+9 -26
View File
@@ -26,47 +26,30 @@ trait Position
/**
* @return int<1, max>|null
*/
public function getLineNumber()
public function getLineNumber(): ?int
{
return $this->lineNumber;
}
/**
* @return int<0, max>
*/
public function getLineNo()
{
$lineNumber = $this->getLineNumber();
return $lineNumber !== null ? $lineNumber : 0;
}
/**
* @return int<0, max>|null
*/
public function getColumnNumber()
public function getColumnNumber(): ?int
{
return $this->columnNumber;
}
/**
* @return int<0, max>
*/
public function getColNo()
{
$columnNumber = $this->getColumnNumber();
return $columnNumber !== null ? $columnNumber : 0;
}
/**
* @param int<0, max>|null $lineNumber
* @param int<1, max>|null $lineNumber
* @param int<0, max>|null $columnNumber
*
* @return $this fluent interface
*/
public function setPosition($lineNumber, $columnNumber = null)
public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable
{
// The conditional is for backwards compatibility (backcompat); `0` will not be allowed in future.
$this->lineNumber = $lineNumber !== 0 ? $lineNumber : null;
$this->lineNumber = $lineNumber;
$this->columnNumber = $columnNumber;
return $this;
}
}
@@ -14,32 +14,18 @@ interface Positionable
/**
* @return int<1, max>|null
*/
public function getLineNumber();
/**
* @return int<0, max>
*
* @deprecated in version 8.9.0, will be removed in v9.0. Use `getLineNumber()` instead.
*/
public function getLineNo();
public function getLineNumber(): ?int;
/**
* @return int<0, max>|null
*/
public function getColumnNumber();
public function getColumnNumber(): ?int;
/**
* @return int<0, max>
*
* @deprecated in version 8.9.0, will be removed in v9.0. Use `getColumnNumber()` instead.
*/
public function getColNo();
/**
* @param int<0, max>|null $lineNumber
* Providing zero for this parameter is deprecated in version 8.9.0, and will not be supported from v9.0.
* Use `null` instead when no line number is available.
* @param int<1, max>|null $lineNumber
* @param int<0, max>|null $columnNumber
*
* @return $this fluent interface
*/
public function setPosition($lineNumber, $columnNumber = null);
public function setPosition(?int $lineNumber, ?int $columnNumber = null): Positionable;
}
+21 -22
View File
@@ -1,38 +1,37 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\CSSList\CSSListItem;
interface AtRule extends Renderable, Commentable
/**
* Note that `CSSListItem` extends both `Commentable` and `Renderable`,
* so concrete classes implementing this interface must also implement those.
*/
interface AtRule extends CSSListItem
{
/**
* Since there are more set rules than block rules,
* we’re whitelisting the block rules and have anything else be treated as a set rule.
*
* @var string
*
* @internal since 8.5.2
*/
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
public const BLOCK_RULES = [
'media',
'document',
'supports',
'region-style',
'font-feature-values',
'container',
'layer',
'scope',
'starting-style',
];
/**
* … and more font-specific ones (to be used inside font-feature-values)
*
* @var string
*
* @internal since 8.5.2
* @return non-empty-string
*/
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
/**
* @return string|null
*/
public function atRuleName();
/**
* @return string|null
*/
public function atRuleArgs();
public function atRuleName(): string;
}
@@ -1,156 +1,114 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
/**
* `CSSNamespace` represents an `@namespace` rule.
*/
class CSSNamespace implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var string
* @var CSSString|URL
*/
private $mUrl;
private $url;
/**
* @var string
* @var string|null
*/
private $sPrefix;
private $prefix;
/**
* @var int
* @param CSSString|URL $url
* @param int<1, max>|null $lineNumber
*/
private $iLineNo;
/**
* @var array<array-key, Comment>
*
* @internal since 8.8.0
*/
protected $aComments;
/**
* @param string $mUrl
* @param string|null $sPrefix
* @param int $iLineNo
*/
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0)
public function __construct($url, ?string $prefix = null, ?int $lineNumber = null)
{
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->setPosition($iLineNo);
$this->aComments = [];
$this->url = $url;
$this->prefix = $prefix;
$this->setPosition($lineNumber);
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
return '@namespace ' . ($this->prefix === null ? '' : $this->prefix . ' ')
. $this->url->render($outputFormat) . ';';
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return '@namespace ' . ($this->sPrefix === null ? '' : $this->sPrefix . ' ')
. $this->mUrl->render($oOutputFormat) . ';';
}
/**
* @return string
* @return CSSString|URL
*/
public function getUrl()
{
return $this->mUrl;
return $this->url;
}
/**
* @return string|null
*/
public function getPrefix()
public function getPrefix(): ?string
{
return $this->sPrefix;
return $this->prefix;
}
/**
* @param string $mUrl
*
* @return void
* @param CSSString|URL $url
*/
public function setUrl($mUrl)
public function setUrl($url): void
{
$this->mUrl = $mUrl;
$this->url = $url;
}
/**
* @param string $sPrefix
*
* @return void
*/
public function setPrefix($sPrefix)
public function setPrefix(string $prefix): void
{
$this->sPrefix = $sPrefix;
$this->prefix = $prefix;
}
/**
* @return string
* @return non-empty-string
*/
public function atRuleName()
public function atRuleName(): string
{
return 'namespace';
}
/**
* @return array<int, string>
* @return array{0: CSSString|URL|non-empty-string, 1?: CSSString|URL}
*/
public function atRuleArgs()
public function atRuleArgs(): array
{
$aResult = [$this->mUrl];
if ($this->sPrefix) {
array_unshift($aResult, $this->sPrefix);
$result = [$this->url];
if (\is_string($this->prefix) && $this->prefix !== '') {
\array_unshift($result, $this->prefix);
}
return $aResult;
return $result;
}
/**
* @param array<array-key, Comment> $aComments
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function addComments(array $aComments)
public function getArrayRepresentation(): array
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
return [
'class' => $this->getShortClassName(),
// We're using `uri` here instead of `url` to better match the spec.
'uri' => $this->url->getArrayRepresentation(),
'prefix' => $this->prefix,
];
}
}
+34 -80
View File
@@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\CSSString;
/**
@@ -18,118 +21,69 @@ use Sabberworm\CSS\Value\CSSString;
*/
class Charset implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var CSSString
*/
private $oCharset;
private $charset;
/**
* @var int
*
* @internal since 8.8.0
* @param int<1, max>|null $lineNumber
*/
protected $iLineNo;
/**
* @var array<array-key, Comment>
*
* @internal since 8.8.0
*/
protected $aComments;
/**
* @param CSSString $oCharset
* @param int $iLineNo
*/
public function __construct(CSSString $oCharset, $iLineNo = 0)
public function __construct(CSSString $charset, ?int $lineNumber = null)
{
$this->oCharset = $oCharset;
$this->setPosition($iLineNo);
$this->aComments = [];
$this->charset = $charset;
$this->setPosition($lineNumber);
}
/**
* @param string|CSSString $oCharset
*
* @return void
* @param string|CSSString $charset
*/
public function setCharset($sCharset)
public function setCharset($charset): void
{
$sCharset = $sCharset instanceof CSSString ? $sCharset : new CSSString($sCharset);
$this->oCharset = $sCharset;
$charset = $charset instanceof CSSString ? $charset : new CSSString($charset);
$this->charset = $charset;
}
public function getCharset(): string
{
return $this->charset->getString();
}
/**
* @return string
* @return non-empty-string
*/
public function getCharset()
public function render(OutputFormat $outputFormat): string
{
return $this->oCharset->getString();
return "{$outputFormat->getFormatter()->comments($this)}@charset {$this->charset->render($outputFormat)};";
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return "{$oOutputFormat->comments($this)}@charset {$this->oCharset->render($oOutputFormat)};";
}
/**
* @return string
*/
public function atRuleName()
public function atRuleName(): string
{
return 'charset';
}
/**
* @return string
*/
public function atRuleArgs()
public function atRuleArgs(): CSSString
{
return $this->oCharset;
return $this->charset;
}
/**
* @param array<array-key, Comment> $aComments
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function addComments(array $aComments)
public function getArrayRepresentation(): array
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
return [
'class' => $this->getShortClassName(),
'charset' => $this->charset->getArrayRepresentation(),
];
}
}
@@ -0,0 +1,240 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
use function Safe\preg_match;
/**
* `Declaration`s just have a string key (the property name) and a 'Value'.
*
* In CSS, `Declaration`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
class Declaration implements Commentable, CSSElement, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var non-empty-string
*/
private $propertyName;
/**
* @var RuleValueList|string|null
*/
private $value;
/**
* @var bool
*/
private $isImportant = false;
/**
* @param non-empty-string $propertyName
* @param int<1, max>|null $lineNumber
* @param int<0, max>|null $columnNumber
*/
public function __construct(string $propertyName, ?int $lineNumber = null, ?int $columnNumber = null)
{
$this->propertyName = $propertyName;
$this->setPosition($lineNumber, $columnNumber);
}
/**
* @param list<Comment> $commentsBefore
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $parserState, array $commentsBefore = []): self
{
$comments = $commentsBefore;
$parserState->consumeWhiteSpace($comments);
$declaration = new self(
$parserState->parseIdentifier(!$parserState->comes('--')),
$parserState->currentLine(),
$parserState->currentColumn()
);
$parserState->consumeWhiteSpace($comments);
$declaration->setComments($comments);
$parserState->consume(':');
$value = Value::parseValue($parserState, self::getDelimitersForPropertyValue($declaration->getPropertyName()));
$declaration->setValue($value);
$parserState->consumeWhiteSpace();
if ($parserState->comes('!')) {
$parserState->consume('!');
$parserState->consumeWhiteSpace();
$parserState->consume('important');
$declaration->setIsImportant(true);
}
$parserState->consumeWhiteSpace();
while ($parserState->comes(';')) {
$parserState->consume(';');
}
return $declaration;
}
/**
* Returns a list of delimiters (or separators).
* The first item is the innermost separator (or, put another way, the highest-precedence operator).
* The sequence continues to the outermost separator (or lowest-precedence operator).
*
* @param non-empty-string $propertyName
*
* @return list<non-empty-string>
*/
private static function getDelimitersForPropertyValue(string $propertyName): array
{
if (preg_match('/^font($|-)/', $propertyName) === 1) {
return [',', '/', ' '];
}
switch ($propertyName) {
case 'src':
return [' ', ','];
default:
return [',', ' ', '/'];
}
}
/**
* @param non-empty-string $propertyName
*/
public function setPropertyName(string $propertyName): void
{
$this->propertyName = $propertyName;
}
/**
* @return non-empty-string
*/
public function getPropertyName(): string
{
return $this->propertyName;
}
/**
* @param non-empty-string $propertyName
*
* @deprecated in v9.2, will be removed in v10.0; use `setPropertyName()` instead.
*/
public function setRule(string $propertyName): void
{
$this->propertyName = $propertyName;
}
/**
* @return non-empty-string
*
* @deprecated in v9.2, will be removed in v10.0; use `getPropertyName()` instead.
*/
public function getRule(): string
{
return $this->propertyName;
}
/**
* @return RuleValueList|string|null
*/
public function getValue()
{
return $this->value;
}
/**
* @param RuleValueList|string|null $value
*/
public function setValue($value): void
{
$this->value = $value;
}
/**
* Adds a value to the existing value. Value will be appended if a `RuleValueList` exists of the given type.
* Otherwise, the existing value will be wrapped by one.
*
* @param RuleValueList|array<int, RuleValueList> $value
*/
public function addValue($value, string $type = ' '): void
{
if (!\is_array($value)) {
$value = [$value];
}
if (!($this->value instanceof RuleValueList) || $this->value->getListSeparator() !== $type) {
$currentValue = $this->value;
$this->value = new RuleValueList($type, $this->getLineNumber());
if ($currentValue !== null && $currentValue !== '') {
$this->value->addListComponent($currentValue);
}
}
foreach ($value as $valueItem) {
$this->value->addListComponent($valueItem);
}
}
public function setIsImportant(bool $isImportant): void
{
$this->isImportant = $isImportant;
}
public function getIsImportant(): bool
{
return $this->isImportant;
}
/**
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
$formatter = $outputFormat->getFormatter();
$result = "{$formatter->comments($this)}{$this->propertyName}:{$formatter->spaceAfterRuleName()}";
if ($this->value instanceof Value) { // Can also be a ValueList
$result .= $this->value->render($outputFormat);
} else {
$result .= $this->value;
}
if ($this->isImportant) {
$result .= ' !important';
}
$result .= ';';
return $result;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'propertyName' => $this->propertyName,
// We're using the term "property value" here to match the wording used in the specs:
// https://www.w3.org/TR/CSS22/syndata.html#declaration
'propertyValue' => $this->value,
'important' => $this->isImportant,
];
}
}
+51 -91
View File
@@ -1,11 +1,14 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use Sabberworm\CSS\Value\URL;
/**
@@ -13,130 +16,87 @@ use Sabberworm\CSS\Value\URL;
*/
class Import implements AtRule, Positionable
{
use CommentContainer;
use Position;
use ShortClassNameProvider;
/**
* @var URL
*/
private $oLocation;
private $location;
/**
* @var string
* @var string|null
*/
private $sMediaQuery;
private $mediaQuery;
/**
* @var array<array-key, Comment>
*
* @internal since 8.8.0
* @param int<1, max>|null $lineNumber
*/
protected $aComments;
/**
* @param URL $oLocation
* @param string $sMediaQuery
* @param int $iLineNo
*/
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0)
public function __construct(URL $location, ?string $mediaQuery, ?int $lineNumber = null)
{
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->setPosition($iLineNo);
$this->aComments = [];
$this->location = $location;
$this->mediaQuery = $mediaQuery;
$this->setPosition($lineNumber);
}
public function setLocation(URL $location): void
{
$this->location = $location;
}
public function getLocation(): URL
{
return $this->location;
}
/**
* @param URL $oLocation
*
* @return void
* @return non-empty-string
*/
public function setLocation($oLocation)
public function render(OutputFormat $outputFormat): string
{
$this->oLocation = $oLocation;
return $outputFormat->getFormatter()->comments($this) . '@import ' . $this->location->render($outputFormat)
. ($this->mediaQuery === null ? '' : ' ' . $this->mediaQuery) . ';';
}
/**
* @return URL
* @return non-empty-string
*/
public function getLocation()
{
return $this->oLocation;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return $oOutputFormat->comments($this) . "@import " . $this->oLocation->render($oOutputFormat)
. ($this->sMediaQuery === null ? '' : ' ' . $this->sMediaQuery) . ';';
}
/**
* @return string
*/
public function atRuleName()
public function atRuleName(): string
{
return 'import';
}
/**
* @return array<int, URL|string>
* @return array{0: URL, 1?: non-empty-string}
*/
public function atRuleArgs()
public function atRuleArgs(): array
{
$aResult = [$this->oLocation];
if ($this->sMediaQuery) {
array_push($aResult, $this->sMediaQuery);
$result = [$this->location];
if (\is_string($this->mediaQuery) && $this->mediaQuery !== '') {
$result[] = $this->mediaQuery;
}
return $aResult;
return $result;
}
public function getMediaQuery(): ?string
{
return $this->mediaQuery;
}
/**
* @param array<array-key, Comment> $aComments
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function addComments(array $aComments)
public function getArrayRepresentation(): array
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
}
/**
* @return string
*/
public function getMediaQuery()
{
return $this->sMediaQuery;
return [
'class' => $this->getShortClassName(),
// We're using the term "uri" here to match the wording used in the specs:
// https://www.w3.org/TR/CSS22/cascade.html#at-import
'uri' => $this->location->getArrayRepresentation(),
];
}
}
@@ -1,25 +1,45 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
class KeyframeSelector extends Selector
{
/**
* regexp for specificity calculations
*
* @var string
* This differs from the parent class:
* - comma is not allowed unless escaped or quoted;
* - percentage value is allowed by itself.
*
* @internal since 8.5.2
*/
const SELECTOR_VALIDATION_RX = '/
^(
(?:
[a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"]
)*
)|
(\d+%) # keyframe animation progress percentage (e.g. 50%)
$
/ux';
public const SELECTOR_VALIDATION_RX = '/
^(
(?:
# any sequence of valid unescaped characters, except quotes
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>]++
|
# one or more escaped characters
(?:\\\\.)++
|
# quoted text, like in `[id="example"]`
(?:
# opening quote
([\'"])
(?:
# sequence of characters except closing quote or backslash
(?:(?!\\g{-1}|\\\\).)++
|
# one or more escaped characters
(?:\\\\.)++
)*+ # zero or more times
# closing quote or end (unmatched quote is currently allowed)
(?:\\g{-1}|$)
)
)*+ # zero or more times
|
# keyframe animation progress percentage (e.g. 50%), untrimmed
\\s*+(\\d++%)\\s*+
)$
/ux';
}
+212 -106
View File
@@ -1,148 +1,254 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\Selector\Combinator;
use Sabberworm\CSS\Property\Selector\Component;
use Sabberworm\CSS\Property\Selector\CompoundSelector;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Settings;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
/**
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this
* class.
*/
class Selector
class Selector implements Renderable
{
/**
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\.[\w]+) # classes
|
\[(\w+) # attributes
|
(\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix';
use ShortClassNameProvider;
/**
* regexp for specificity calculations
*
* @var string
*
* @internal
*/
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
/**
* regexp for specificity calculations
*
* @var string
*
* @internal since 8.5.2
*/
const SELECTOR_VALIDATION_RX = '/
public const SELECTOR_VALIDATION_RX = '/
^(
# not whitespace only
(?!\\s*+$)
(?:
[a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters
(?:\\\\.)? # a single escaped character
(?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"]
)*
# any sequence of valid unescaped characters, except quotes
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
|
# one or more escaped characters
(?:\\\\.)++
|
# quoted text, like in `[id="example"]`
(?:
# opening quote
([\'"])
(?:
# sequence of characters except closing quote or backslash
(?:(?!\\g{-1}|\\\\).)++
|
# one or more escaped characters
(?:\\\\.)++
)*+ # zero or more times
# closing quote or end (unmatched quote is currently allowed)
(?:\\g{-1}|$)
)
)*+ # zero or more times
)$
/ux';
/**
* @var string
* @var non-empty-list<Component>
*/
private $sSelector;
private $components;
/**
* @var int|null
*/
private $iSpecificity;
/**
* @param string $sSelector
*
* @return bool
*
* @internal since V8.8.0
*/
public static function isValid($sSelector)
public static function isValid(string $selector): bool
{
return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
// Note: We need to use `static::` here as the constant is overridden in the `KeyframeSelector` class.
$numberOfMatches = preg_match(static::SELECTOR_VALIDATION_RX, $selector);
return $numberOfMatches === 1;
}
/**
* @param string $sSelector
* @param bool $bCalculateSpecificity @deprecated since V8.8.0, will be removed in V9.0.0
* @param non-empty-string|non-empty-list<Component> $selector
* Providing a string is deprecated in version 9.2 and will not work from v10.0
*
* @throws UnexpectedTokenException if the selector is not valid
*/
public function __construct($sSelector, $bCalculateSpecificity = false)
final public function __construct($selector)
{
$this->setSelector($sSelector);
if ($bCalculateSpecificity) {
$this->getSpecificity();
if (\is_string($selector)) {
$this->setSelector($selector);
} else {
$this->setComponents($selector);
}
}
/**
* @return string
*/
public function getSelector()
{
return $this->sSelector;
}
/**
* @param string $sSelector
* @param list<Comment> $comments
*
* @return void
*/
public function setSelector($sSelector)
{
$this->sSelector = trim($sSelector);
$this->iSpecificity = null;
}
/**
* @return string
* @return non-empty-list<Component>
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @throws UnexpectedTokenException
*/
public function __toString()
private static function parseComponents(ParserState $parserState, array &$comments = []): array
{
return $this->getSelector();
}
// Whitespace is a descendent combinator, not allowed around a compound selector.
// (It is allowed within, e.g. as part of a string or within a function like `:not()`.)
// Gobble any up now to get a clean start.
$parserState->consumeWhiteSpace($comments);
/**
* @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;
$selectorParts = [];
while (true) {
try {
$selectorParts[] = CompoundSelector::parse($parserState, $comments);
} catch (UnexpectedTokenException $e) {
if ($selectorParts !== [] && \end($selectorParts)->getValue() === ' ') {
// The whitespace was not a descendent combinator, and was, in fact, arbitrary,
// after the end of the selector. Discard it.
\array_pop($selectorParts);
break;
} else {
throw $e;
}
}
try {
$selectorParts[] = Combinator::parse($parserState, $comments);
} catch (UnexpectedTokenException $e) {
// End of selector has been reached.
break;
}
}
return $this->iSpecificity;
return $selectorParts;
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$selectorParts = self::parseComponents($parserState, $comments);
// Check that the selector has been fully parsed:
if (!\in_array($parserState->peek(), ['{', '}', ',', ''], true)) {
throw new UnexpectedTokenException(
'`,`, `{`, `}` or EOF',
$parserState->peek(5),
'literal',
$parserState->currentLine()
);
}
return new static($selectorParts);
}
/**
* @return non-empty-list<Component>
*/
public function getComponents(): array
{
return $this->components;
}
/**
* @param non-empty-list<Component> $components
* This should be an alternating sequence of `CompoundSelector` and `Combinator`, starting and ending with a
* `CompoundSelector`, and may be a single `CompoundSelector`.
*/
public function setComponents(array $components): self
{
$this->components = $components;
return $this;
}
/**
* @return non-empty-string
*
* @deprecated in version 9.2, will be removed in v10.0. Use either `getComponents()` or `render()` instead.
*/
public function getSelector(): string
{
return $this->render(new OutputFormat());
}
/**
* @param non-empty-string $selector
*
* @throws UnexpectedTokenException if the selector is not valid
*
* @deprecated in version 9.2, will be removed in v10.0. Use `setComponents()` instead.
*/
public function setSelector(string $selector): void
{
$parserState = new ParserState($selector, Settings::create());
$components = self::parseComponents($parserState);
// Check that the selector has been fully parsed:
if (!$parserState->isEnd()) {
throw new UnexpectedTokenException(
'EOF',
$parserState->peek(5),
'literal'
);
}
$this->components = $components;
}
/**
* @return int<0, max>
*/
public function getSpecificity(): int
{
return \array_sum(
\array_map(
static function (Component $component): int {
return $component->getSpecificity();
},
$this->components
)
);
}
public function render(OutputFormat $outputFormat): string
{
return \implode(
'',
\array_map(
static function (Component $component) use ($outputFormat): string {
return $component->render($outputFormat);
},
$this->components
)
);
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'components' => \array_map(
static function (Component $component): array {
return $component->getArrayRepresentation();
},
$this->components
),
];
}
}
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* Class representing a CSS selector combinator (space, `>`, `+`, or `~`).
*
* @phpstan-type ValidCombinatorValue ' '|'>'|'+'|'~'
*/
class Combinator implements Component
{
use ShortClassNameProvider;
/**
* @var ValidCombinatorValue
*/
private $value;
/**
* @param ValidCombinatorValue $value
*/
public function __construct(string $value)
{
$this->setValue($value);
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$consumedWhitespace = $parserState->consumeWhiteSpace($comments);
$nextToken = $parserState->peek();
if (\in_array($nextToken, ['>', '+', '~'], true)) {
$value = $nextToken;
$parserState->consume(1);
$parserState->consumeWhiteSpace($comments);
} elseif ($consumedWhitespace !== '') {
$value = ' ';
} else {
throw new UnexpectedTokenException(
'combinator',
$nextToken,
'literal',
$parserState->currentLine()
);
}
return new self($value);
}
/**
* @return ValidCombinatorValue
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param non-empty-string $value
*
* @throws \UnexpectedValueException if `$value` is not either space, '>', '+' or '~'
*/
public function setValue(string $value): void
{
if (!\in_array($value, [' ', '>', '+', '~'], true)) {
throw new \UnexpectedValueException('`' . $value . '` is not a valid selector combinator.');
}
$this->value = $value;
}
/**
* @return int<0, max>
*/
public function getSpecificity(): int
{
return 0;
}
public function render(OutputFormat $outputFormat): string
{
$spacing = $outputFormat->getSpaceAroundSelectorCombinator();
if ($this->value === ' ') {
$rendering = $spacing !== '' ? $spacing : ' ';
} else {
$rendering = $spacing . $this->value . $spacing;
}
return $rendering;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'value' => $this->value,
];
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Renderable;
/**
* This interface is for a class that represents a part of a selector which is either a compound selector (or a simple
* selector, which is effectively a compound selector without any compounding) or a selector combinator.
*
* It allows a selector to be represented as an array of objects that implement this interface.
* This is the formal definition:
* selector = compound-selector [combinator, compound-selector]*
*
* The selector is comprised of an array of alternating types that can't be easily represented in a type-safe manner
* without this.
*
* 'Selector component' is not a known grammar in the spec, but a convenience for the implementation.
*
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Selectors/Selector_structure
* @see https://www.w3.org/TR/selectors-4/#structure
*/
interface Component extends Renderable
{
/**
* @return non-empty-string
*/
public function getValue(): string;
/**
* @param non-empty-string $value
*/
public function setValue(string $value): void;
/**
* @return int<0, max>
*/
public function getSpecificity(): int;
}
@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
/**
* Class representing a CSS compound selector.
* Selectors have to be split at combinators (space, `>`, `+`, `~`) before being passed to this class.
*/
class CompoundSelector implements Component
{
use ShortClassNameProvider;
private const PARSER_STOP_CHARACTERS = [
'{',
'}',
'\'',
'"',
'(',
')',
'[',
']',
',',
' ',
"\t",
"\n",
"\r",
'>',
'+',
'~',
ParserState::EOF,
'', // `ParserState::peek()` returns empty string rather than `ParserState::EOF` when end of string is reached
];
private const SELECTOR_VALIDATION_RX = '/
^
# not starting with whitespace
(?!\\s)
(?:
(?:
# any sequence of valid unescaped characters, except quotes
[a-zA-Z0-9\\x{00A0}-\\x{FFFF}_^$|*=~\\[\\]()\\-\\s\\.:#+>,]++
|
# one or more escaped characters
(?:\\\\.)++
|
# quoted text, like in `[id="example"]`
(?:
# opening quote
([\'"])
(?:
# sequence of characters except closing quote or backslash
(?:(?!\\g{-1}|\\\\).)++
|
# one or more escaped characters
(?:\\\\.)++
)*+ # zero or more times
# closing quote or end (unmatched quote is currently allowed)
(?:\\g{-1}|$)
)
)++ # one or more times
|
# keyframe animation progress percentage (e.g. 50%)
(?:\\d++%)
)
# not ending with whitespace
(?<!\\s)
$
/ux';
/**
* @var non-empty-string
*/
private $value;
/**
* @param non-empty-string $value
*/
public function __construct(string $value)
{
$this->setValue($value);
}
/**
* @param list<Comment> $comments
*
* @throws UnexpectedTokenException
*
* @internal
*/
public static function parse(ParserState $parserState, array &$comments = []): self
{
$selectorParts = [];
$stringWrapperCharacter = null;
$functionNestingLevel = 0;
$isWithinAttribute = false;
while (true) {
$selectorParts[] = $parserState->consumeUntil(self::PARSER_STOP_CHARACTERS, false, false, $comments);
$nextCharacter = $parserState->peek();
switch ($nextCharacter) {
case '':
// EOF
break 2;
case '\'':
// The fallthrough is intentional.
case '"':
$lastPart = \end($selectorParts);
$backslashCount = \strspn(\strrev($lastPart), '\\');
$quoteIsEscaped = ($backslashCount % 2 === 1);
if (!$quoteIsEscaped) {
if (!\is_string($stringWrapperCharacter)) {
$stringWrapperCharacter = $nextCharacter;
} elseif ($stringWrapperCharacter === $nextCharacter) {
$stringWrapperCharacter = null;
}
}
break;
case '(':
if (!\is_string($stringWrapperCharacter)) {
++$functionNestingLevel;
}
break;
case ')':
if (!\is_string($stringWrapperCharacter)) {
if ($functionNestingLevel <= 0) {
throw new UnexpectedTokenException(
'anything but',
')',
'literal',
$parserState->currentLine()
);
}
--$functionNestingLevel;
}
break;
case '[':
if (!\is_string($stringWrapperCharacter)) {
if ($isWithinAttribute) {
throw new UnexpectedTokenException(
'anything but',
'[',
'literal',
$parserState->currentLine()
);
}
$isWithinAttribute = true;
}
break;
case ']':
if (!\is_string($stringWrapperCharacter)) {
if (!$isWithinAttribute) {
throw new UnexpectedTokenException(
'anything but',
']',
'literal',
$parserState->currentLine()
);
}
$isWithinAttribute = false;
}
break;
case '{':
// The fallthrough is intentional.
case '}':
if (!\is_string($stringWrapperCharacter)) {
break 2;
}
break;
case ',':
// The fallthrough is intentional.
case ' ':
// The fallthrough is intentional.
case "\t":
// The fallthrough is intentional.
case "\n":
// The fallthrough is intentional.
case "\r":
// The fallthrough is intentional.
case '>':
// The fallthrough is intentional.
case '+':
// The fallthrough is intentional.
case '~':
if (!\is_string($stringWrapperCharacter) && $functionNestingLevel === 0 && !$isWithinAttribute) {
break 2;
}
break;
}
$selectorParts[] = $parserState->consume(1);
}
if ($functionNestingLevel !== 0) {
throw new UnexpectedTokenException(')', $nextCharacter, 'literal', $parserState->currentLine());
}
if (\is_string($stringWrapperCharacter)) {
throw new UnexpectedTokenException(
$stringWrapperCharacter,
$nextCharacter,
'literal',
$parserState->currentLine()
);
}
$value = \implode('', $selectorParts);
if ($value === '') {
throw new UnexpectedTokenException('selector', $nextCharacter, 'literal', $parserState->currentLine());
}
if (!self::isValid($value)) {
throw new UnexpectedTokenException(
'Selector component is not valid:',
'`' . $value . '`',
'custom',
$parserState->currentLine()
);
}
return new self($value);
}
/**
* @return non-empty-string
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param non-empty-string $value
*
* @throws \UnexpectedValueException if `$value` contains invalid characters or has surrounding whitespce
*/
public function setValue(string $value): void
{
if (!self::isValid($value)) {
throw new \UnexpectedValueException('`' . $value . '` is not a valid compound selector.');
}
$this->value = $value;
}
/**
* @return int<0, max>
*/
public function getSpecificity(): int
{
return SpecificityCalculator::calculate($this->value);
}
public function render(OutputFormat $outputFormat): string
{
return $this->getValue();
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'value' => $this->value,
];
}
private static function isValid(string $value): bool
{
$numberOfMatches = preg_match(self::SELECTOR_VALIDATION_RX, $value);
return $numberOfMatches === 1;
}
}
@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Property\Selector;
use function Safe\preg_match_all;
/**
* Utility class to calculate the specificity of a CSS selector.
*
* The results are cached to avoid recalculating the specificity of the same selector multiple times.
*/
final class SpecificityCalculator
{
/**
* regexp for specificity calculations
*/
private const NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX = '/
(\\.[\\w]+) # classes
|
\\[(\\w+) # attributes
|
(\\:( # pseudo classes
link|visited|active
|hover|focus
|lang
|target
|enabled|disabled|checked|indeterminate
|root
|nth-child|nth-last-child|nth-of-type|nth-last-of-type
|first-child|last-child|first-of-type|last-of-type
|only-child|only-of-type
|empty|contains
))
/ix';
/**
* regexp for specificity calculations
*/
private const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\\s\\+\\>\\~]+)[\\w]+ # elements
|
\\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
/**
* @var array<string, int<0, max>>
*/
private static $cache = [];
/**
* Calculates the specificity of the given CSS selector.
*
* @return int<0, max>
*
* @internal
*/
public static function calculate(string $selector): int
{
if (!isset(self::$cache[$selector])) {
$a = 0;
/// @todo should exclude \# as well as "#"
$matches = null;
$b = \substr_count($selector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $selector, $matches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $selector, $matches);
self::$cache[$selector] = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}
return self::$cache[$selector];
}
/**
* Clears the cache in order to lower memory usage.
*/
public static function clearCache(): void
{
self::$cache = [];
}
}
+6 -14
View File
@@ -1,25 +1,17 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
interface Renderable
{
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString();
public function render(OutputFormat $outputFormat): string;
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat);
/**
* @return int
*/
public function getLineNo();
public function getArrayRepresentation(): array;
}
+16 -375
View File
@@ -1,383 +1,24 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Rule;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
use Sabberworm\CSS\Property\Declaration;
/**
* `Rule`s just have a string key (the rule) and a 'Value'.
*
* In CSS, `Rule`s are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
class Rule implements Commentable, CSSElement, Positionable
{
use Position;
use function Safe\class_alias;
/**
* @var string
*/
private $sRule;
/**
* @var RuleValueList|string|null
*/
private $mValue;
/**
* @var bool
*/
private $bIsImportant;
/**
* @var array<int, int>
*/
private $aIeHack;
/**
* @var array<array-key, Comment>
*
* @internal since 8.8.0
*/
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->setPosition($iLineNo, $iColNo);
$this->aComments = [];
}
/**
* @param array<int, Comment> $commentsBeforeRule
*
* @return Rule
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $commentsBeforeRule = [])
{
$aComments = \array_merge($commentsBeforeRule, $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(';');
}
return $oRule;
}
/**
* Returns a list of delimiters (or separators).
* The first item is the innermost separator (or, put another way, the highest-precedence operator).
* The sequence continues to the outermost separator (or lowest-precedence operator).
*
* @param string $sRule
*
* @return list<non-empty-string>
*/
private static function listDelimiterForRule($sRule)
{
if (preg_match('/^font($|-)/', $sRule)) {
return [',', '/', ' '];
}
switch ($sRule) {
case 'src':
return [' ', ','];
default:
return [',', ' ', '/'];
}
}
/**
* @param string $sRule
*
* @return void
*/
public function setRule($sRule)
{
$this->sRule = $sRule;
}
/**
* @return string
*/
public function getRule()
{
return $this->sRule;
}
/**
* @return RuleValueList|string|null
*/
public function getValue()
{
return $this->mValue;
}
/**
* @param RuleValueList|string|null $mValue
*
* @return void
*/
public function setValue($mValue)
{
$this->mValue = $mValue;
}
/**
* @param array<array-key, array<array-key, RuleValueList>> $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<int, array<int, RuleValueList>>
*
* @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<int, RuleValueList> $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->getLineNumber());
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
/**
* @param int $iModifier
*
* @return void
*
* @deprecated since V8.8.0, will be removed in V9.0
*/
public function addIeHack($iModifier)
{
$this->aIeHack[] = $iModifier;
}
/**
* @param array<int, int> $aModifiers
*
* @return void
*
* @deprecated since V8.8.0, will be removed in V9.0
*/
public function setIeHack(array $aModifiers)
{
$this->aIeHack = $aModifiers;
}
/**
* @return array<int, int>
*
* @deprecated since V8.8.0, will be removed in V9.0
*/
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
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sResult = "{$oOutputFormat->comments($this)}{$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<array-key, Comment> $aComments
*
* @return void
*/
public function addComments(array $aComments)
{
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array<array-key, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<array-key, Comment> $aComments
*
* @return void
*/
public function setComments(array $aComments)
{
$this->aComments = $aComments;
// @phpstan-ignore function.impossibleType
if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) {
class_alias(Declaration::class, Rule::class);
// The test is expected to evaluate to false,
// but allows for the deprecation notice to be picked up by IDEs like PHPStorm.
// @phpstan-ignore booleanNot.alwaysTrue, booleanAnd.alwaysTrue, function.impossibleType
if (!\class_exists(Rule::class, false) && !\interface_exists(Rule::class, false)) {
/**
* @deprecated in v9.2, will be removed in v10.0. Use `Property\Declaration` instead, which is a direct
* replacement.
*/
class Rule {}
}
}
+44 -43
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\OutputFormat;
@@ -14,68 +16,67 @@ use Sabberworm\CSS\Property\AtRule;
class AtRuleSet extends RuleSet implements AtRule
{
/**
* @var string
* @var non-empty-string
*/
private $sType;
private $type;
/**
* @var string
*/
private $sArgs;
private $arguments;
/**
* @param string $sType
* @param string $sArgs
* @param int $iLineNo
* @param non-empty-string $type
* @param int<1, max>|null $lineNumber
*/
public function __construct($sType, $sArgs = '', $iLineNo = 0)
public function __construct(string $type, string $arguments = '', ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
parent::__construct($lineNumber);
$this->type = $type;
$this->arguments = $arguments;
}
/**
* @return string
* @return non-empty-string
*/
public function atRuleName()
public function atRuleName(): string
{
return $this->sType;
return $this->type;
}
public function atRuleArgs(): string
{
return $this->arguments;
}
/**
* @return string
* @return non-empty-string
*/
public function atRuleArgs()
public function render(OutputFormat $outputFormat): string
{
return $this->sArgs;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sResult = $oOutputFormat->comments($this);
$sArgs = $this->sArgs;
if ($sArgs) {
$sArgs = ' ' . $sArgs;
$formatter = $outputFormat->getFormatter();
$result = $formatter->comments($this);
$arguments = $this->arguments;
if ($arguments !== '') {
$arguments = ' ' . $arguments;
}
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= $this->renderRules($oOutputFormat);
$sResult .= '}';
return $sResult;
$result .= "@{$this->type}$arguments{$formatter->spaceBeforeOpeningBrace()}{";
$result .= $this->renderDeclarations($outputFormat);
$result .= '}';
return $result;
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
$arrayRepresentation = parent::getArrayRepresentation();
$arrayRepresentation['atRuleName'] = $this->type;
$arrayRepresentation['arguments'] = $this->arguments;
return $arrayRepresentation;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Property\Declaration;
/**
* Represents a CSS item that contains `Declaration`s, defining the methods to manipulate them.
*/
interface DeclarationList
{
public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void;
public function removeDeclaration(Declaration $declarationToRemove): void;
public function removeMatchingDeclarations(string $searchPattern): void;
public function removeAllDeclarations(): void;
/**
* @param array<Declaration> $declarations
*/
public function setDeclarations(array $declarations): void;
/**
* @return array<int<0, max>, Declaration>
*/
public function getDeclarations(?string $searchPattern = null): array;
/**
* @return array<string, Declaration>
*/
public function getDeclarationsAssociative(?string $searchPattern = null): array;
/**
* @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead.
*/
public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void;
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead.
*/
public function removeRule(Declaration $declarationToRemove): void;
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead.
*/
public function removeMatchingRules(string $searchPattern): void;
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead.
*/
public function removeAllRules(): void;
/**
* @param array<Declaration> $declarations
*
* @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead.
*/
public function setRules(array $declarations): void;
/**
* @return array<int<0, max>, Declaration>
*
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead.
*/
public function getRules(?string $searchPattern = null): array;
/**
* @return array<string, Declaration>
*
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead.
*/
public function getRulesAssoc(?string $searchPattern = null): array;
}
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Property\Declaration;
/**
* Provides a mapping of the deprecated methods in a `DeclarationList` to their renamed replacements.
*/
trait LegacyDeclarationListMethods
{
/**
* @deprecated in v9.2, will be removed in v10.0; use `addDeclaration()` instead.
*/
public function addRule(Declaration $declarationToAdd, ?Declaration $sibling = null): void
{
$this->addDeclaration($declarationToAdd, $sibling);
}
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeDeclaration()` instead.
*/
public function removeRule(Declaration $declarationToRemove): void
{
$this->removeDeclaration($declarationToRemove);
}
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeMatchingDeclarations()` instead.
*/
public function removeMatchingRules(string $searchPattern): void
{
$this->removeMatchingDeclarations($searchPattern);
}
/**
* @deprecated in v9.2, will be removed in v10.0; use `removeAllDeclarations()` instead.
*/
public function removeAllRules(): void
{
$this->removeAllDeclarations();
}
/**
* @param array<Declaration> $declarations
*
* @deprecated in v9.2, will be removed in v10.0; use `setDeclarations()` instead.
*/
public function setRules(array $declarations): void
{
$this->setDeclarations($declarations);
}
/**
* @return array<int<0, max>, Declaration>
*
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarations()` instead.
*/
public function getRules(?string $searchPattern = null): array
{
return $this->getDeclarations($searchPattern);
}
/**
* @return array<string, Declaration>
*
* @deprecated in v9.2, will be removed in v10.0; use `getDeclarationsAssociative()` instead.
*/
public function getRulesAssoc(?string $searchPattern = null): array
{
return $this->getDeclarationsAssociative($searchPattern);
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\RuleSet;
use function Safe\class_alias;
// @phpstan-ignore function.impossibleType
if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) {
class_alias(DeclarationList::class, RuleContainer::class);
// The test is expected to evaluate to false,
// but allows for the deprecation notice to be picked up by IDEs like PHPStorm.
// @phpstan-ignore booleanNot.alwaysTrue, booleanAnd.alwaysTrue, function.impossibleType
if (!\class_exists(RuleContainer::class, false) && !\interface_exists(RuleContainer::class, false)) {
/**
* @deprecated in v9.2, will be removed in v10.0. Use `DeclarationList` instead, which is a direct replacement.
*/
interface RuleContainer {}
}
}
+243 -194
View File
@@ -1,88 +1,85 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Comment\CommentContainer;
use Sabberworm\CSS\CSSElement;
use Sabberworm\CSS\CSSList\CSSListItem;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Property\Declaration;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* This class is a container for individual 'Rule's.
* This class is a container for individual `Declaration`s.
*
* The most common form of a rule set is one constrained by a selector, i.e., a `DeclarationBlock`.
* However, unknown `AtRule`s (like `@font-face`) are rule sets as well.
*
* If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $rule)`, `getRules()` and `removeRule($rule)`
* (which accepts either a `Rule` or a rule name; optionally suffixed by a dash to remove all related rules).
* If you want to manipulate a `RuleSet`,
* use the methods `addDeclaration()`, `getDeclarations()`, `removeDeclaration()`, `removeMatchingDeclarations()`, etc.
*
* Note that `CSSListItem` extends both `Commentable` and `Renderable`, so those interfaces must also be implemented.
*/
abstract class RuleSet implements CSSElement, Commentable, Positionable
class RuleSet implements CSSElement, CSSListItem, Positionable, DeclarationList
{
use CommentContainer;
use LegacyDeclarationListMethods;
use Position;
use ShortClassNameProvider;
/**
* the rules in this rule set, using the property name as the key,
* with potentially multiple rules per property name.
* the declarations in this rule set, using the property name as the key,
* with potentially multiple declarations per property name.
*
* @var array<string, array<int<0, max>, Rule>>
* @var array<string, array<int<0, max>, Declaration>>
*/
private $aRules;
private $declarations = [];
/**
* @var array<array-key, Comment>
*
* @internal since 8.8.0
* @param int<1, max>|null $lineNumber
*/
protected $aComments;
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
$this->aRules = [];
$this->setPosition($iLineNo);
$this->aComments = [];
$this->setPosition($lineNumber);
}
/**
* @return void
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
public static function parseRuleSet(ParserState $parserState, RuleSet $ruleSet): void
{
while ($oParserState->comes(';')) {
$oParserState->consume(';');
while ($parserState->comes(';')) {
$parserState->consume(';');
}
while (true) {
$commentsBeforeRule = $oParserState->consumeWhiteSpace();
if ($oParserState->comes('}')) {
$commentsBeforeDeclaration = [];
$parserState->consumeWhiteSpace($commentsBeforeDeclaration);
if ($parserState->comes('}')) {
break;
}
$oRule = null;
if ($oParserState->getSettings()->bLenientParsing) {
$declaration = null;
if ($parserState->getSettings()->usesLenientParsing()) {
try {
$oRule = Rule::parse($oParserState, $commentsBeforeRule);
$declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
$consumedText = $parserState->consumeUntil(["\n", ';', '}'], true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if ($oParserState->streql(substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
if ($parserState->streql(\substr($consumedText, -1), '}')) {
$parserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
while ($parserState->comes(';')) {
$parserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
@@ -91,267 +88,319 @@ abstract class RuleSet implements CSSElement, Commentable, Positionable
}
}
} else {
$oRule = Rule::parse($oParserState, $commentsBeforeRule);
$declaration = Declaration::parse($parserState, $commentsBeforeDeclaration);
}
if ($oRule) {
$oRuleSet->addRule($oRule);
if ($declaration instanceof Declaration) {
$ruleSet->addDeclaration($declaration);
}
}
$oParserState->consume('}');
$parserState->consume('}');
}
/**
* @param Rule|null $oSibling
*
* @return void
* @throws \UnexpectedValueException
* if the last `Declaration` is needed as a basis for setting position, but does not have a valid position,
* which should never happen
*/
public function addRule(Rule $oRule, $oSibling = null)
public function addDeclaration(Declaration $declarationToAdd, ?Declaration $sibling = null): void
{
$sRule = $oRule->getRule();
if (!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = [];
$propertyName = $declarationToAdd->getPropertyName();
if (!isset($this->declarations[$propertyName])) {
$this->declarations[$propertyName] = [];
}
$iPosition = count($this->aRules[$sRule]);
$position = \count($this->declarations[$propertyName]);
if ($oSibling !== null) {
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
if ($iSiblingPos !== false) {
$iPosition = $iSiblingPos;
$oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
}
}
if ($oRule->getLineNumber() === null) {
//this node is added manually, give it the next best line
$columnNumber = $oRule->getColNo();
$rules = $this->getRules();
$pos = count($rules);
if ($pos > 0) {
$last = $rules[$pos - 1];
$oRule->setPosition($last->getLineNo() + 1, $columnNumber);
if ($sibling !== null) {
$siblingPosition = \array_search($sibling, $this->declarations[$propertyName], true);
if ($siblingPosition !== false) {
$siblingIsInSet = true;
$position = $siblingPosition;
} else {
$oRule->setPosition(1, $columnNumber);
$siblingIsInSet = $this->hasDeclaration($sibling);
if ($siblingIsInSet) {
// Maintain ordering within `$this->declarations[$propertyName]`
// by inserting before first `Declaration` with a same-or-later position than the sibling.
foreach ($this->declarations[$propertyName] as $index => $declaration) {
if (self::comparePositionable($declaration, $sibling) >= 0) {
$position = $index;
break;
}
}
}
}
if ($siblingIsInSet) {
// Increment column number of all existing declarations on same line, starting at sibling
$siblingLineNumber = $sibling->getLineNumber();
$siblingColumnNumber = $sibling->getColumnNumber();
foreach ($this->declarations as $declarationsForAProperty) {
foreach ($declarationsForAProperty as $declaration) {
if (
$declaration->getLineNumber() === $siblingLineNumber
&& $declaration->getColumnNumber() >= $siblingColumnNumber
) {
$declaration->setPosition($siblingLineNumber, $declaration->getColumnNumber() + 1);
}
}
}
$declarationToAdd->setPosition($siblingLineNumber, $siblingColumnNumber);
}
} elseif ($oRule->getColumnNumber() === null) {
$oRule->setPosition($oRule->getLineNumber(), 0);
}
array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
if ($declarationToAdd->getLineNumber() === null) {
//this node is added manually, give it the next best line
$columnNumber = $declarationToAdd->getColumnNumber() ?? 0;
$declarations = $this->getDeclarations();
$declarationsCount = \count($declarations);
if ($declarationsCount > 0) {
$last = $declarations[$declarationsCount - 1];
$lastsLineNumber = $last->getLineNumber();
if (!\is_int($lastsLineNumber)) {
throw new \UnexpectedValueException(
'A Declaration without a line number was found during addDeclaration',
1750718399
);
}
$declarationToAdd->setPosition($lastsLineNumber + 1, $columnNumber);
} else {
$declarationToAdd->setPosition(1, $columnNumber);
}
} elseif ($declarationToAdd->getColumnNumber() === null) {
$declarationToAdd->setPosition($declarationToAdd->getLineNumber(), 0);
}
\array_splice($this->declarations[$propertyName], $position, 0, [$declarationToAdd]);
}
/**
* Returns all rules matching the given rule name
* Returns all declarations matching the given property name
*
* @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
* @example $ruleSet->getDeclarations('font') // returns array(0 => $declaration, …) or array().
*
* @example $oRuleSet->getRules('font-')
* //returns an array of all rules either beginning with font- or matching font.
* @example $ruleSet->getDeclarations('font-')
* //returns an array of all declarations 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
* @param string|null $searchPattern
* Pattern to search for. If null, returns all declarations.
* If the pattern ends with a dash, all declarations starting with the pattern are returned
* as well as one matching the pattern with the dash excluded.
* Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
* Call `getRules($rule->getRule())` instead.
*
* @return array<int, Rule>
* @return array<int<0, max>, Declaration>
*/
public function getRules($mRule = null)
public function getDeclarations(?string $searchPattern = null): array
{
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
/** @var array<int, Rule> $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.
$result = [];
foreach ($this->declarations as $propertyName => $declarations) {
// Either no search pattern was given
// or the search pattern matches the found declaration's property name exactly
// or the search pattern ends in “-”
// ... and the found declaration's property name starts with the search pattern
if (
!$mRule || $sName === $mRule
$searchPattern === null || $propertyName === $searchPattern
|| (
strrpos($mRule, '-') === strlen($mRule) - strlen('-')
&& (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
&& (\strpos($propertyName, $searchPattern) === 0
|| $propertyName === \substr($searchPattern, 0, -1))
)
) {
$aResult = array_merge($aResult, $aRules);
$result = \array_merge($result, $declarations);
}
}
usort($aResult, function (Rule $first, Rule $second) {
if ($first->getLineNo() === $second->getLineNo()) {
return $first->getColNo() - $second->getColNo();
}
return $first->getLineNo() - $second->getLineNo();
});
return $aResult;
\usort($result, [self::class, 'comparePositionable']);
return $result;
}
/**
* Overrides all the rules of this set.
* Overrides all the declarations of this set.
*
* @param array<array-key, Rule> $aRules The rules to override with.
*
* @return void
* @param array<Declaration> $declarations
*/
public function setRules(array $aRules)
public function setDeclarations(array $declarations): void
{
$this->aRules = [];
foreach ($aRules as $rule) {
$this->addRule($rule);
$this->declarations = [];
foreach ($declarations as $declaration) {
$this->addDeclaration($declaration);
}
}
/**
* 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.
* Returns all declarations with property names matching the given pattern and returns them in an associative array
* with the property names 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.
* containing the rgba-valued declaration while `getDeclarations()` 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
* @param string|null $searchPattern
* Pattern to search for. If null, returns all declarations. If the pattern ends with a dash,
* all declarations starting with the pattern are returned as well as one matching the pattern with the dash
* excluded.
* Passing a `Rule` for this parameter is deprecated in version 8.9.0, and will not work from v9.0.
* Call `getRulesAssoc($rule->getRule())` instead.
*
* @return array<string, Rule>
* @return array<string, Declaration>
*/
public function getRulesAssoc($mRule = null)
public function getDeclarationsAssociative(?string $searchPattern = null): array
{
/** @var array<string, Rule> $aResult */
$aResult = [];
foreach ($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
/** @var array<string, Declaration> $result */
$result = [];
foreach ($this->getDeclarations($searchPattern) as $declaration) {
$result[$declaration->getPropertyName()] = $declaration;
}
return $aResult;
return $result;
}
/**
* Removes a `Rule` from this `RuleSet` by identity.
*
* @param Rule|string|null $mRule
* `Rule` to remove.
* Passing a `string` or `null` is deprecated in version 8.9.0, and will no longer work from v9.0.
* Use `removeMatchingRules()` or `removeAllRules()` instead.
* Removes a `Declaration` from this `RuleSet` by identity.
*/
public function removeRule($mRule)
public function removeDeclaration(Declaration $declarationToRemove): void
{
if ($mRule instanceof Rule) {
$sRule = $mRule->getRule();
if (!isset($this->aRules[$sRule])) {
return;
$nameOfPropertyToRemove = $declarationToRemove->getPropertyName();
if (!isset($this->declarations[$nameOfPropertyToRemove])) {
return;
}
foreach ($this->declarations[$nameOfPropertyToRemove] as $key => $declaration) {
if ($declaration === $declarationToRemove) {
unset($this->declarations[$nameOfPropertyToRemove][$key]);
}
foreach ($this->aRules[$sRule] as $iKey => $oRule) {
if ($oRule === $mRule) {
unset($this->aRules[$sRule][$iKey]);
}
}
} elseif ($mRule !== null) {
$this->removeMatchingRules($mRule);
} else {
$this->removeAllRules();
}
}
/**
* Removes rules by property name or search pattern.
* Removes declarations by property name or search pattern.
*
* @param string $searchPattern
* pattern to remove.
* If the pattern ends in a dash,
* all rules starting with the pattern are removed as well as one matching the pattern with the dash
* all declarations starting with the pattern are removed as well as one matching the pattern with the dash
* excluded.
*/
public function removeMatchingRules($searchPattern)
public function removeMatchingDeclarations(string $searchPattern): void
{
foreach ($this->aRules as $propertyName => $rules) {
// Either the search rule matches the found rule exactly
// or the search rule ends in “-” and the found rule starts with the search rule or equals it
// (without the trailing dash).
foreach ($this->declarations as $propertyName => $declarations) {
// Either the search pattern matches the found declaration's property name exactly
// or the search pattern ends in “-” and the found declaration's property name starts with the search
// pattern or equals it (without the trailing dash).
if (
$propertyName === $searchPattern
|| (\strrpos($searchPattern, '-') === \strlen($searchPattern) - \strlen('-')
&& (\strpos($propertyName, $searchPattern) === 0
|| $propertyName === \substr($searchPattern, 0, -1)))
) {
unset($this->aRules[$propertyName]);
unset($this->declarations[$propertyName]);
}
}
}
public function removeAllRules()
public function removeAllDeclarations(): void
{
$this->aRules = [];
$this->declarations = [];
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @internal
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
return $this->renderDeclarations($outputFormat);
}
/**
* @return string
*/
protected function renderRules(OutputFormat $oOutputFormat)
protected function renderDeclarations(OutputFormat $outputFormat): string
{
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat->nextLevel();
foreach ($this->getRules() as $oRule) {
$sRendered = $oNextLevel->safely(function () use ($oRule, $oNextLevel) {
return $oRule->render($oNextLevel);
});
if ($sRendered === null) {
$result = '';
$isFirst = true;
$nextLevelFormat = $outputFormat->nextLevel();
foreach ($this->getDeclarations() as $declaration) {
$nextLevelFormatter = $nextLevelFormat->getFormatter();
$renderedDeclaration = $nextLevelFormatter->safely(
static function () use ($declaration, $nextLevelFormat): string {
return $declaration->render($nextLevelFormat);
}
);
if ($renderedDeclaration === null) {
continue;
}
if ($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeRules();
if ($isFirst) {
$isFirst = false;
$result .= $nextLevelFormatter->spaceBeforeRules();
} else {
$sResult .= $oNextLevel->spaceBetweenRules();
$result .= $nextLevelFormatter->spaceBetweenRules();
}
$sResult .= $sRendered;
$result .= $renderedDeclaration;
}
if (!$bIsFirst) {
$formatter = $outputFormat->getFormatter();
if (!$isFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
$result .= $formatter->spaceAfterRules();
}
return $oOutputFormat->removeLastSemicolon($sResult);
return $formatter->removeLastSemicolon($result);
}
/**
* @param array<string, Comment> $aComments
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function addComments(array $aComments)
public function getArrayRepresentation(): array
{
$this->aComments = array_merge($this->aComments, $aComments);
$declarationsArrayRepresentation = [];
foreach ($this->declarations as $propertyName => $declarationsForOneProperty) {
$declarationsArrayRepresentation[$propertyName] = \array_map(
function (Declaration $declaration): array {
return $declaration->getArrayRepresentation();
},
$declarationsForOneProperty
);
}
return [
'class' => $this->getShortClassName(),
'declarations' => $declarationsArrayRepresentation,
];
}
/**
* @return array<string, Comment>
*/
public function getComments()
{
return $this->aComments;
}
/**
* @param array<string, Comment> $aComments
* @return int negative if `$first` is before `$second`; zero if they have the same position; positive otherwise
*
* @return void
* @throws \UnexpectedValueException if either argument does not have a valid position, which should never happen
*/
public function setComments(array $aComments)
private static function comparePositionable(Positionable $first, Positionable $second): int
{
$this->aComments = $aComments;
$firstsLineNumber = $first->getLineNumber();
$secondsLineNumber = $second->getLineNumber();
if (!\is_int($firstsLineNumber) || !\is_int($secondsLineNumber)) {
throw new \UnexpectedValueException(
'A Declaration without a line number was passed to comparePositionable',
1750637683
);
}
if ($firstsLineNumber === $secondsLineNumber) {
$firstsColumnNumber = $first->getColumnNumber();
$secondsColumnNumber = $second->getColumnNumber();
if (!\is_int($firstsColumnNumber) || !\is_int($secondsColumnNumber)) {
throw new \UnexpectedValueException(
'A Declaration without a column number was passed to comparePositionable',
1750637761
);
}
return $firstsColumnNumber - $secondsColumnNumber;
}
return $firstsLineNumber - $secondsLineNumber;
}
private function hasDeclaration(Declaration $declaration): bool
{
foreach ($this->declarations as $declarationsForAProperty) {
if (\in_array($declaration, $declarationsForAProperty, true)) {
return true;
}
}
return false;
}
}
+49 -31
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
/**
@@ -16,38 +18,29 @@ class Settings
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bMultibyteSupport;
private $multibyteSupport;
/**
* The default charset for the CSS if no `@charset` declaration is found. Defaults to utf-8.
*
* @var string
*
* @internal since 8.8.0, will be made private in 9.0.0
* @var non-empty-string
*/
public $sDefaultCharset = 'utf-8';
private $defaultCharset = 'utf-8';
/**
* Whether the parser silently ignore invalid rules instead of choking on them.
*
* @var bool
*
* @internal since 8.8.0, will be made private in 9.0.0
*/
public $bLenientParsing = true;
private $lenientParsing = true;
private function __construct()
{
$this->bMultibyteSupport = extension_loaded('mbstring');
$this->multibyteSupport = \extension_loaded('mbstring');
}
/**
* @return self new instance
*/
public static function create()
public static function create(): self
{
return new Settings();
}
@@ -58,49 +51,74 @@ class Settings
* If `true` (`mbstring` extension must be enabled), will use (slower) `mb_strlen`, `mb_convert_case`, `mb_substr`
* and `mb_strpos` functions. Otherwise, the normal (ASCII-Only) functions will be used.
*
* @param bool $bMultibyteSupport
*
* @return self fluent interface
* @return $this fluent interface
*/
public function withMultibyteSupport($bMultibyteSupport = true)
public function withMultibyteSupport(bool $multibyteSupport = true): self
{
$this->bMultibyteSupport = $bMultibyteSupport;
$this->multibyteSupport = $multibyteSupport;
return $this;
}
/**
* Sets the charset to be used if the CSS does not contain an `@charset` declaration.
*
* @param string $sDefaultCharset
* @param non-empty-string $defaultCharset
*
* @return self fluent interface
* @return $this fluent interface
*/
public function withDefaultCharset($sDefaultCharset)
public function withDefaultCharset(string $defaultCharset): self
{
$this->sDefaultCharset = $sDefaultCharset;
$this->defaultCharset = $defaultCharset;
return $this;
}
/**
* Configures whether the parser should silently ignore invalid rules.
*
* @param bool $bLenientParsing
*
* @return self fluent interface
* @return $this fluent interface
*/
public function withLenientParsing($bLenientParsing = true)
public function withLenientParsing(bool $usesLenientParsing = true): self
{
$this->bLenientParsing = $bLenientParsing;
$this->lenientParsing = $usesLenientParsing;
return $this;
}
/**
* Configures the parser to choke on invalid rules.
*
* @return self fluent interface
* @return $this fluent interface
*/
public function beStrict()
public function beStrict(): self
{
return $this->withLenientParsing(false);
}
/**
* @internal
*/
public function hasMultibyteSupport(): bool
{
return $this->multibyteSupport;
}
/**
* @return non-empty-string
*
* @internal
*/
public function getDefaultCharset(): string
{
return $this->defaultCharset;
}
/**
* @internal
*/
public function usesLenientParsing(): bool
{
return $this->lenientParsing;
}
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS;
/**
* Provides a method to obtain the short name of the instantiated class (i.e. without namespace prefix).
*
* @internal
*/
trait ShortClassNameProvider
{
/**
* @return non-empty-string
*/
private function getShortClassName(): string
{
return (new \ReflectionClass($this))->getShortName();
}
}
+73 -50
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -15,95 +17,116 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CSSFunction extends ValueList
{
/**
* @var string
* @var non-empty-string
*
* @internal since 8.8.0
*/
protected $sName;
protected $name;
/**
* @param string $sName
* @param RuleValueList|array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aArguments
* @param string $sSeparator
* @param int $iLineNo
* @param non-empty-string $name
* @param RuleValueList|array<Value|string> $arguments
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0)
public function __construct(string $name, $arguments, string $separator = ',', ?int $lineNumber = null)
{
if ($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
if ($arguments instanceof RuleValueList) {
$separator = $arguments->getListSeparator();
$arguments = $arguments->getListComponents();
}
$this->sName = $sName;
$this->setPosition($iLineNo); // TODO: redundant?
parent::__construct($aArguments, $sSeparator, $iLineNo);
$this->name = $name;
$this->setPosition($lineNumber); // TODO: redundant?
parent::__construct($arguments, $separator, $lineNumber);
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CSSFunction
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
$name = self::parseName($parserState, $ignoreCase);
$parserState->consume('(');
$arguments = self::parseArguments($parserState);
$result = new CSSFunction($name, $arguments, ',', $parserState->currentLine());
$parserState->consume(')');
return $result;
}
/**
* @return string
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function getName()
private static function parseName(ParserState $parserState, bool $ignoreCase = false): string
{
return $this->sName;
return $parserState->parseIdentifier($ignoreCase);
}
/**
* @param string $sName
* @return Value|string
*
* @return void
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public function setName($sName)
private static function parseArguments(ParserState $parserState)
{
$this->sName = $sName;
return Value::parseValue($parserState, ['=', ' ', ',']);
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return non-empty-string
*/
public function getArguments()
public function getName(): string
{
return $this->aComponents;
return $this->name;
}
/**
* @return string
* @param non-empty-string $name
*/
public function setName(string $name): void
{
$this->name = $name;
}
/**
* @return array<Value|string>
*/
public function getArguments(): array
{
return $this->components;
}
/**
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
$arguments = parent::render($outputFormat);
return "{$this->name}({$arguments})";
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @internal
*/
public function __toString()
public function getArrayRepresentation(): array
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
return \array_merge(
[
'class' => 'placeholder',
'name' => $this->name,
],
parent::getArrayRepresentation()
);
}
}
+67 -59
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,6 +9,9 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
/**
* This class is a wrapper for quoted strings to distinguish them from keywords.
@@ -15,102 +20,105 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
*/
class CSSString extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* @var string
*/
private $sString;
private $string;
/**
* @param string $sString
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($sString, $iLineNo = 0)
public function __construct(string $string, ?int $lineNumber = null)
{
$this->sString = $sString;
parent::__construct($iLineNo);
$this->string = $string;
parent::__construct($lineNumber);
}
/**
* @return CSSString
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): CSSString
{
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} elseif ($sBegin === '"') {
$sQuote = '"';
$begin = $parserState->peek();
$quote = null;
if ($begin === "'") {
$quote = "'";
} elseif ($begin === '"') {
$quote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
if ($quote !== null) {
$parserState->consume($quote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
$result = '';
if ($quote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
while (preg_match('/[\\s{}()<>\\[\\]]/isu', $parserState->peek()) === 0) {
$result .= $parserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
while (!$parserState->comes($quote)) {
$content = $parserState->parseCharacter(false);
if ($content === null) {
throw new SourceException(
"Non-well-formed quoted string {$oParserState->peek(3)}",
$oParserState->currentLine()
"Non-well-formed quoted string {$parserState->peek(3)}",
$parserState->currentLine()
);
}
$sResult .= $sContent;
$result .= $content;
}
$oParserState->consume($sQuote);
$parserState->consume($quote);
}
return new CSSString($sResult, $oParserState->currentLine());
return new CSSString($result, $parserState->currentLine());
}
public function setString(string $string): void
{
$this->string = $string;
}
public function getString(): string
{
return $this->string;
}
/**
* @param string $sString
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
return $outputFormat->getStringQuotingType()
. $this->escape($this->string, $outputFormat)
. $outputFormat->getStringQuotingType();
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return void
* @internal
*/
public function setString($sString)
public function getArrayRepresentation(): array
{
$this->sString = $sString;
return [
'class' => $this->getShortClassName(),
// We're using the term "contents" here to make the difference to the class more clear.
'contents' => $this->string,
];
}
/**
* @return string
*/
public function getString()
private function escape(string $string, OutputFormat $outputFormat): string
{
return $this->sString;
}
$charactersToEscape = '\\';
$charactersToEscape .= ($outputFormat->getStringQuotingType() === '"' ? '"' : "'");
$withEscapedQuotes = \addcslashes($string, $charactersToEscape);
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
$withNewlineEncoded = \str_replace("\n", '\\A', $withEscapedQuotes);
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
return $withNewlineEncoded;
}
}
+61 -67
View File
@@ -1,115 +1,109 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* Support for `-webkit-calc` and `-moz-calc` is deprecated in version 8.8.0, and will be removed in version 9.0.0.
*/
use function Safe\preg_match;
class CalcFunction extends CSSFunction
{
/**
* @var int
*
* @internal
*/
const T_OPERAND = 1;
private const T_OPERAND = 1;
private const T_OPERATOR = 2;
/**
* @var int
*
* @internal
*/
const T_OPERATOR = 2;
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CalcFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = $oParserState->parseIdentifier();
if ($oParserState->peek() != '(') {
$operators = ['+', '-', '*', '/'];
$function = $parserState->parseIdentifier();
if ($parserState->peek() !== '(') {
// Found ; or end of line before an opening bracket
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
} elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
throw new UnexpectedTokenException('(', $parserState->peek(), 'literal', $parserState->currentLine());
} elseif ($function !== 'calc') {
// Found invalid calc definition. Example calc (...
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
throw new UnexpectedTokenException('calc', $function, 'literal', $parserState->currentLine());
}
$oParserState->consume('(');
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = null;
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
if ($oParserState->isEnd() && $iNestingLevel === 0) {
$parserState->consume('(');
$calcRuleValueList = new CalcRuleValueList($parserState->currentLine());
$list = new RuleValueList(',', $parserState->currentLine());
$nestingLevel = 0;
$lastComponentType = null;
while (!$parserState->comes(')') || $nestingLevel > 0) {
if ($parserState->isEnd() && $nestingLevel === 0) {
break;
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
if ($parserState->comes('(')) {
$nestingLevel++;
$calcRuleValueList->addListComponent($parserState->consume(1));
$parserState->consumeWhiteSpace();
continue;
} elseif ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
$oParserState->consumeWhiteSpace();
} elseif ($parserState->comes(')')) {
$nestingLevel--;
$calcRuleValueList->addListComponent($parserState->consume(1));
$parserState->consumeWhiteSpace();
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
if ($lastComponentType !== CalcFunction::T_OPERAND) {
$value = Value::parsePrimitiveValue($parserState);
$calcRuleValueList->addListComponent($value);
$lastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if (\in_array($parserState->peek(), $operators, true)) {
if (($parserState->comes('-') || $parserState->comes('+'))) {
if (
$oParserState->peek(1, -1) != ' '
|| !($oParserState->comes('- ')
|| $oParserState->comes('+ '))
preg_match('/\\s/', $parserState->peek(1, -1)) !== 1
|| preg_match('/\\s/', $parserState->peek(1, 1)) !== 1
) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
" {$parserState->peek()} ",
$parserState->peek(1, -1) . $parserState->peek(2),
'literal',
$oParserState->currentLine()
$parserState->currentLine()
);
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
$calcRuleValueList->addListComponent($parserState->consume(1));
$lastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
\sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oParserState->peek()
\implode(', ', $operators),
$parserState->peek()
),
'',
'custom',
$oParserState->currentLine()
$parserState->currentLine()
);
}
}
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
if (!$oParserState->isEnd()) {
$oParserState->consume(')');
$list->addListComponent($calcRuleValueList);
if (!$parserState->isEnd()) {
$parserState->consume(')');
}
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
return new CalcFunction($function, $list, ',', $parserState->currentLine());
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,20 +9,25 @@ use Sabberworm\CSS\OutputFormat;
class CalcRuleValueList extends RuleValueList
{
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
parent::__construct(',', $iLineNo);
parent::__construct(',', $lineNumber);
}
public function render(OutputFormat $outputFormat): string
{
return $outputFormat->getFormatter()->implode(' ', $this->components);
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
return $oOutputFormat->implode(' ', $this->aComponents);
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
}
+331 -117
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -14,173 +16,385 @@ use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class Color extends CSSFunction
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
* @param int $iLineNo
* @param array<non-empty-string, Value|string> $colorValues
* @param int<1, max>|null $lineNumber
*/
public function __construct(array $aColor, $iLineNo = 0)
public function __construct(array $colorValues, ?int $lineNumber = null)
{
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
parent::__construct(\implode('', \array_keys($colorValues)), $colorValues, ',', $lineNumber);
}
/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return Color|CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
public static function parse(ParserState $parserState, bool $ignoreCase = false): CSSFunction
{
$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];
}
return $parserState->comes('#')
? self::parseHexColor($parserState)
: self::parseColorFunction($parserState);
}
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()
),
];
} 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()
);
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseHexColor(ParserState $parserState): Color
{
$parserState->consume('#');
$hexValue = $parserState->parseIdentifier(false);
if ($parserState->strlen($hexValue) === 3) {
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2];
} elseif ($parserState->strlen($hexValue) === 4) {
$hexValue = $hexValue[0] . $hexValue[0] . $hexValue[1] . $hexValue[1] . $hexValue[2] . $hexValue[2]
. $hexValue[3] . $hexValue[3];
}
if ($parserState->strlen($hexValue) === 8) {
$colorValues = [
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
'a' => new Size(
\round(self::mapRange(\intval($hexValue[6] . $hexValue[7], 16), 0, 255, 0, 1), 2),
null,
true,
$parserState->currentLine()
),
];
} elseif ($parserState->strlen($hexValue) === 6) {
$colorValues = [
'r' => new Size(\intval($hexValue[0] . $hexValue[1], 16), null, true, $parserState->currentLine()),
'g' => new Size(\intval($hexValue[2] . $hexValue[3], 16), null, true, $parserState->currentLine()),
'b' => new Size(\intval($hexValue[4] . $hexValue[5], 16), null, true, $parserState->currentLine()),
];
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
throw new UnexpectedTokenException(
'Invalid hex color value',
$hexValue,
'custom',
$parserState->currentLine()
);
}
$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);
}
return new Color($colorValues, $parserState->currentLine());
}
if ($bContainsVar && $oParserState->comes(')')) {
// With a var argument the function can have fewer arguments
break;
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseColorFunction(ParserState $parserState): CSSFunction
{
$colorValues = [];
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
$colorMode = $parserState->parseIdentifier(true);
$parserState->consumeWhiteSpace();
$parserState->consume('(');
// CSS Color Module Level 4 says that `rgb` and `rgba` are now aliases; likewise `hsl` and `hsla`.
// So, attempt to parse with the `a`, and allow for it not being there.
switch ($colorMode) {
case 'rgb':
$colorModeForParsing = 'rgba';
$mayHaveOptionalAlpha = true;
break;
case 'hsl':
$colorModeForParsing = 'hsla';
$mayHaveOptionalAlpha = true;
break;
case 'rgba':
// This is handled identically to the following case.
case 'hsla':
$colorModeForParsing = $colorMode;
$mayHaveOptionalAlpha = true;
break;
default:
$colorModeForParsing = $colorMode;
$mayHaveOptionalAlpha = false;
}
$containsVar = false;
$containsNone = false;
$isLegacySyntax = false;
$expectedArgumentCount = $parserState->strlen($colorModeForParsing);
for ($argumentIndex = 0; $argumentIndex < $expectedArgumentCount; ++$argumentIndex) {
$parserState->consumeWhiteSpace();
$valueKey = $colorModeForParsing[$argumentIndex];
if ($parserState->comes('var')) {
$colorValues[$valueKey] = CSSFunction::parseIdentifierOrFunction($parserState);
$containsVar = true;
} elseif (!$isLegacySyntax && $parserState->comes('none')) {
$colorValues[$valueKey] = $parserState->parseIdentifier();
$containsNone = true;
} else {
$colorValues[$valueKey] = Size::parse($parserState, true);
}
$oParserState->consume(')');
if ($bContainsVar) {
return new CSSFunction($sColorMode, array_values($aColor), ',', $oParserState->currentLine());
// This must be done first, to consume comments as well, so that the `comes` test will work.
$parserState->consumeWhiteSpace();
// With a `var` argument, the function can have fewer arguments.
// And as of CSS Color Module Level 4, the alpha argument is optional.
$canCloseNow
= $containsVar
|| ($mayHaveOptionalAlpha && $argumentIndex >= $expectedArgumentCount - 2);
if ($canCloseNow && $parserState->comes(')')) {
break;
}
// "Legacy" syntax is comma-delimited, and does not allow the `none` keyword.
// "Modern" syntax is space-delimited, with `/` as alpha delimiter.
// They cannot be mixed.
if ($argumentIndex === 0 && !$containsNone) {
// An immediate closing parenthesis is not valid.
if ($parserState->comes(')')) {
throw new UnexpectedTokenException(
'Color function with no arguments',
'',
'custom',
$parserState->currentLine()
);
}
$isLegacySyntax = $parserState->comes(',');
}
if ($isLegacySyntax && $argumentIndex < ($expectedArgumentCount - 1)) {
$parserState->consume(',');
}
// In the "modern" syntax, the alpha value must be delimited with `/`.
if (!$isLegacySyntax) {
if ($containsVar) {
// If the `var` substitution encompasses more than one argument,
// the alpha deliminator may come at any time.
if ($parserState->comes('/')) {
$parserState->consume('/');
}
} elseif (($colorModeForParsing[$argumentIndex + 1] ?? '') === 'a') {
// Alpha value is the next expected argument.
// Since a closing parenthesis was not found, a `/` separator is now required.
$parserState->consume('/');
}
}
}
return new Color($aColor, $oParserState->currentLine());
$parserState->consume(')');
return $containsVar
? new CSSFunction($colorMode, \array_values($colorValues), ',', $parserState->currentLine())
: new Color($colorValues, $parserState->currentLine());
}
/**
* @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)
private static function mapRange(float $value, float $fromMin, float $fromMax, float $toMin, float $toMax): float
{
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
$fromRange = $fromMax - $fromMin;
$toRange = $toMax - $toMin;
$multiplier = $toRange / $fromRange;
$newValue = $value - $fromMin;
$newValue *= $multiplier;
return $newValue + $toMin;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return array<non-empty-string, Value|string>
*/
public function getColor()
public function getColor(): array
{
return $this->aComponents;
return $this->components;
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aColor
*
* @return void
* @param array<non-empty-string, Value|string> $colorValues
*/
public function setColor(array $aColor)
public function setColor(array $colorValues): void
{
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
$this->setName(\implode('', \array_keys($colorValues)));
$this->components = $colorValues;
}
/**
* @return string
* @return non-empty-string
*/
public function getColorDescription()
public function getColorDescription(): string
{
return $this->getName();
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
if ($this->shouldRenderAsHex($outputFormat)) {
return $this->renderAsHex();
}
if ($this->shouldRenderInModernSyntax()) {
return $this->renderInModernSyntax($outputFormat);
}
return parent::render($outputFormat);
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
// 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);
throw new \BadMethodCallException('`getArrayRepresentation` is not yet implemented for `' . self::class . '`');
}
private function shouldRenderAsHex(OutputFormat $outputFormat): bool
{
return
$outputFormat->usesRgbHashNotation()
&& $this->getRealName() === 'rgb'
&& $this->allComponentsAreNumbers();
}
/**
* The function name is a concatenation of the array keys of the components, which is passed to the constructor.
* However, this can be changed by calling {@see CSSFunction::setName},
* so is not reliable in situations where it's necessary to determine the function name based on the components.
*/
private function getRealName(): string
{
return \implode('', \array_keys($this->components));
}
/**
* Test whether all color components are absolute numbers (CSS type `number`), not percentages or anything else.
* If any component is not an instance of `Size`, the method will also return `false`.
*/
private function allComponentsAreNumbers(): bool
{
foreach ($this->components as $component) {
if (!($component instanceof Size) || $component->getUnit() !== null) {
return false;
}
}
return parent::render($oOutputFormat);
return true;
}
/**
* Note that this method assumes the following:
* - The `components` array has keys for `r`, `g` and `b`;
* - The values in the array are all instances of `Size`.
*
* Errors will be triggered or thrown if this is not the case.
*
* @return non-empty-string
*/
private function renderAsHex(): string
{
$result = \sprintf(
'%02x%02x%02x',
$this->components['r']->getSize(),
$this->components['g']->getSize(),
$this->components['b']->getSize()
);
$canUseShortVariant = ($result[0] === $result[1]) && ($result[2] === $result[3]) && ($result[4] === $result[5]);
return '#' . ($canUseShortVariant ? $result[0] . $result[2] . $result[4] : $result);
}
/**
* The "legacy" syntax does not allow RGB colors to have a mixture of `percentage`s and `number`s,
* and does not allow `none` as any component value.
*
* The "legacy" and "modern" monikers are part of the formal W3C syntax.
* See the following for more information:
* - {@link
* https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb#formal_syntax
* Description of the formal syntax for `rgb()` on MDN
* };
* - {@link
* https://www.w3.org/TR/css-color-4/#rgb-functions
* The same in the CSS Color Module Level 4 W3C Candidate Recommendation Draft
* } (as of 13 February 2024, at time of writing).
*/
private function shouldRenderInModernSyntax(): bool
{
if ($this->hasNoneAsComponentValue()) {
return true;
}
if (!$this->colorFunctionMayHaveMixedValueTypes($this->getRealName())) {
return false;
}
$hasPercentage = false;
$hasNumber = false;
foreach ($this->components as $key => $value) {
if ($key === 'a') {
// Alpha can have units that don't match those of the RGB components in the "legacy" syntax.
// So it is not necessary to check it. It's also always last, hence `break` rather than `continue`.
break;
}
if (!($value instanceof Size)) {
// Unexpected, unknown, or modified via the API
return false;
}
$unit = $value->getUnit();
// `switch` only does loose comparison
if ($unit === null) {
$hasNumber = true;
} elseif ($unit === '%') {
$hasPercentage = true;
} else {
// Invalid unit
return false;
}
}
return $hasPercentage && $hasNumber;
}
private function hasNoneAsComponentValue(): bool
{
return \in_array('none', $this->components, true);
}
/**
* Some color functions, such as `rgb`,
* may have a mixture of `percentage`, `number`, or possibly other types in their arguments.
*
* Note that this excludes the alpha component, which is treated separately.
*/
private function colorFunctionMayHaveMixedValueTypes(string $function): bool
{
$functionsThatMayHaveMixedValueTypes = ['rgb', 'rgba'];
return \in_array($function, $functionsThatMayHaveMixedValueTypes, true);
}
/**
* @return non-empty-string
*/
private function renderInModernSyntax(OutputFormat $outputFormat): string
{
// Maybe not yet without alpha, but will be...
$componentsWithoutAlpha = $this->components;
\end($componentsWithoutAlpha);
if (\key($componentsWithoutAlpha) === 'a') {
$alpha = $this->components['a'];
unset($componentsWithoutAlpha['a']);
}
$formatter = $outputFormat->getFormatter();
$arguments = $formatter->implode(' ', $componentsWithoutAlpha);
if (isset($alpha)) {
$separator = $formatter->spaceBeforeListArgumentSeparator('/')
. '/' . $formatter->spaceAfterListArgumentSeparator('/');
$arguments = $formatter->implode($separator, [$arguments, $alpha]);
}
return $this->getName() . '(' . $arguments . ')';
}
}
+26 -32
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,64 +9,56 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
/**
* A name for a named CSS grid line.
*
* @see https://www.w3.org/TR/css-grid-1/#line-name
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Grid_layout/Named_grid_lines
*/
class LineName extends ValueList
{
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
* @param int $iLineNo
* @param array<string> $components
* @param int<1, max>|null $lineNumber
*/
public function __construct(array $aComponents = [], $iLineNo = 0)
public function __construct(array $components = [], ?int $lineNumber = null)
{
parent::__construct($aComponents, ' ', $iLineNo);
parent::__construct($components, ' ', $lineNumber);
}
/**
* @return LineName
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): LineName
{
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = [];
$parserState->consume('[');
$parserState->consumeWhiteSpace();
$names = [];
do {
if ($oParserState->getSettings()->bLenientParsing) {
if ($parserState->getSettings()->usesLenientParsing()) {
try {
$aNames[] = $oParserState->parseIdentifier();
$names[] = $parserState->parseIdentifier();
} catch (UnexpectedTokenException $e) {
if (!$oParserState->comes(']')) {
if (!$parserState->comes(']')) {
throw $e;
}
}
} else {
$aNames[] = $oParserState->parseIdentifier();
$names[] = $parserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
$parserState->consumeWhiteSpace();
} while (!$parserState->comes(']'));
$parserState->consume(']');
return new LineName($names, $parserState->currentLine());
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
public function render(OutputFormat $outputFormat): string
{
return '[' . parent::render(OutputFormat::createCompact()) . ']';
}
@@ -1,14 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
abstract class PrimitiveValue extends Value
{
/**
* @param int $iLineNo
*/
public function __construct($iLineNo = 0)
{
parent::__construct($iLineNo);
}
}
abstract class PrimitiveValue extends Value {}
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
/**
@@ -10,11 +12,11 @@ namespace Sabberworm\CSS\Value;
class RuleValueList extends ValueList
{
/**
* @param string $sSeparator
* @param int $iLineNo
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($sSeparator = ',', $iLineNo = 0)
public function __construct(string $separator = ',', ?int $lineNumber = null)
{
parent::__construct([], $sSeparator, $iLineNo);
parent::__construct([], $separator, $lineNumber);
}
}
+91 -115
View File
@@ -1,25 +1,29 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
use function Safe\preg_replace;
/**
* A `Size` consists of a numeric `size` value and a unit.
*/
class Size extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* vh/vw/vm(ax)/vmin/rem are absolute insofar as they don’t scale to the immediate parent (only the viewport)
*
* @var array<int, string>
*
* @internal
*/
const ABSOLUTE_SIZE_UNITS = [
private const ABSOLUTE_SIZE_UNITS = [
'px',
'pt',
'pc',
@@ -37,211 +41,183 @@ class Size extends PrimitiveValue
'rem',
];
/**
* @var array<int, string>
*
* @internal
*/
const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
private const RELATIVE_SIZE_UNITS = ['%', 'em', 'ex', 'ch', 'fr'];
private const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
* @var array<int, string>
*
* @internal
*/
const NON_SIZE_UNITS = ['deg', 'grad', 'rad', 's', 'ms', 'turn', 'Hz', 'kHz'];
/**
* @var array<int, array<string, string>>|null
* @var array<int<1, max>, array<lowercase-string, non-empty-string>>|null
*/
private static $SIZE_UNITS = null;
/**
* @var float
*/
private $fSize;
private $size;
/**
* @var string|null
*/
private $sUnit;
private $unit;
/**
* @var bool
*/
private $bIsColorComponent;
private $isColorComponent;
/**
* @param float|int|string $fSize
* @param string|null $sUnit
* @param bool $bIsColorComponent
* @param int $iLineNo
* @param float|int|string $size
* @param int<1, max>|null $lineNumber
*/
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0)
public function __construct($size, ?string $unit = null, bool $isColorComponent = false, ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->fSize = (float)$fSize;
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
parent::__construct($lineNumber);
$this->size = (float) $size;
$this->unit = $unit;
$this->isColorComponent = $isColorComponent;
}
/**
* @param bool $bIsColorComponent
*
* @return Size
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState, $bIsColorComponent = false)
public static function parse(ParserState $parserState, bool $isColorComponent = false): Size
{
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
$size = '';
if ($parserState->comes('-')) {
$size .= $parserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.') || $oParserState->comes('e', true)) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} elseif ($oParserState->comes('e', true)) {
$sLookahead = $oParserState->peek(1, 1);
if (is_numeric($sLookahead) || $sLookahead === '+' || $sLookahead === '-') {
$sSize .= $oParserState->consume(2);
while (\is_numeric($parserState->peek()) || $parserState->comes('.') || $parserState->comes('e', true)) {
if ($parserState->comes('.')) {
$size .= $parserState->consume('.');
} elseif ($parserState->comes('e', true)) {
$lookahead = $parserState->peek(1, 1);
if (\is_numeric($lookahead) || $lookahead === '+' || $lookahead === '-') {
$size .= $parserState->consume(2);
} else {
break; // Reached the unit part of the number like "em" or "ex"
}
} else {
$sSize .= $oParserState->consume(1);
$size .= $parserState->consume(1);
}
}
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach ($aSizeUnits as $iLength => &$aValues) {
$sKey = strtolower($oParserState->peek($iLength));
if (array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
$unit = null;
$sizeUnits = self::getSizeUnits();
foreach ($sizeUnits as $length => &$values) {
$key = \strtolower($parserState->peek($length));
if (\array_key_exists($key, $values)) {
if (($unit = $values[$key]) !== null) {
$parserState->consume($length);
break;
}
}
}
return new Size((float)$sSize, $sUnit, $bIsColorComponent, $oParserState->currentLine());
return new Size((float) $size, $unit, $isColorComponent, $parserState->currentLine());
}
/**
* @return array<int, array<string, string>>
* @return array<int<1, max>, array<lowercase-string, non-empty-string>>
*/
private static function getSizeUnits()
private static function getSizeUnits(): array
{
if (!is_array(self::$SIZE_UNITS)) {
if (!\is_array(self::$SIZE_UNITS)) {
self::$SIZE_UNITS = [];
foreach (array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS) as $val) {
$iSize = strlen($val);
if (!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = [];
$sizeUnits = \array_merge(self::ABSOLUTE_SIZE_UNITS, self::RELATIVE_SIZE_UNITS, self::NON_SIZE_UNITS);
foreach ($sizeUnits as $sizeUnit) {
$tokenLength = \strlen($sizeUnit);
if (!isset(self::$SIZE_UNITS[$tokenLength])) {
self::$SIZE_UNITS[$tokenLength] = [];
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
self::$SIZE_UNITS[$tokenLength][\strtolower($sizeUnit)] = $sizeUnit;
}
krsort(self::$SIZE_UNITS, SORT_NUMERIC);
\krsort(self::$SIZE_UNITS, SORT_NUMERIC);
}
return self::$SIZE_UNITS;
}
/**
* @param string $sUnit
*
* @return void
*/
public function setUnit($sUnit)
public function setUnit(string $unit): void
{
$this->sUnit = $sUnit;
$this->unit = $unit;
}
public function getUnit(): ?string
{
return $this->unit;
}
/**
* @return string|null
* @param float|int|string $size
*/
public function getUnit()
public function setSize($size): void
{
return $this->sUnit;
$this->size = (float) $size;
}
/**
* @param float|int|string $fSize
*/
public function setSize($fSize)
public function getSize(): float
{
$this->fSize = (float)$fSize;
return $this->size;
}
/**
* @return float
*/
public function getSize()
public function isColorComponent(): bool
{
return $this->fSize;
}
/**
* @return bool
*/
public function isColorComponent()
{
return $this->bIsColorComponent;
return $this->isColorComponent;
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
*
* @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
* Returns `false` if the unit is an angle, a duration, a frequency, or the number is a component in a `Color`
* object.
*/
public function isSize()
public function isSize(): bool
{
if (in_array($this->sUnit, self::NON_SIZE_UNITS, true)) {
if (\in_array($this->unit, self::NON_SIZE_UNITS, true)) {
return false;
}
return !$this->isColorComponent();
}
/**
* @return bool
*/
public function isRelative()
public function isRelative(): bool
{
if (in_array($this->sUnit, self::RELATIVE_SIZE_UNITS, true)) {
if (\in_array($this->unit, self::RELATIVE_SIZE_UNITS, true)) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
if ($this->unit === null && $this->size !== 0.0) {
return true;
}
return false;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @return non-empty-string
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
$locale = \localeconv();
$decimalPoint = \preg_quote($locale['decimal_point'], '/');
$size = preg_match('/[\\d\\.]+e[+-]?\\d+/i', (string) $this->size) === 1
? preg_replace("/$decimalPoint?0+$/", '', \sprintf('%f', $this->size)) : (string) $this->size;
return preg_replace(["/$decimalPoint/", '/^(-?)0\\./'], ['.', '$1.'], $size) . ($this->unit ?? '');
}
/**
* @param OutputFormat|null $oOutputFormat
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @return string
* @internal
*/
public function render($oOutputFormat)
public function getArrayRepresentation(): array
{
$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)) : (string)$this->fSize;
return preg_replace(["/$sPoint/", "/^(-?)0\./"], ['.', '$1.'], $sSize)
. ($this->sUnit === null ? '' : $this->sUnit);
return [
'class' => $this->getShortClassName(),
// 'number' is the official W3C terminology (not 'size')
'number' => $this->size,
'unit' => $this->unit,
];
}
}
+48 -48
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
@@ -7,95 +9,93 @@ use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* This class represents URLs in CSS. `URL`s always output in `URL("")` notation.
*/
class URL extends PrimitiveValue
{
use ShortClassNameProvider;
/**
* @var CSSString
*/
private $oURL;
private $url;
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct(CSSString $oURL, $iLineNo = 0)
public function __construct(CSSString $url, ?int $lineNumber = null)
{
parent::__construct($iLineNo);
$this->oURL = $oURL;
parent::__construct($lineNumber);
$this->url = $url;
}
/**
* @return URL
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*
* @internal since V8.8.0
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $parserState): URL
{
$oAnchor = $oParserState->anchor();
$sIdentifier = '';
$anchor = $parserState->anchor();
$identifier = '';
for ($i = 0; $i < 3; $i++) {
$sChar = $oParserState->parseCharacter(true);
if ($sChar === null) {
$character = $parserState->parseCharacter(true);
if ($character === null) {
break;
}
$sIdentifier .= $sChar;
$identifier .= $character;
}
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$useUrl = $parserState->streql($identifier, 'url');
if ($useUrl) {
$parserState->consumeWhiteSpace();
$parserState->consume('(');
} else {
$oAnchor->backtrack();
$anchor->backtrack();
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
$parserState->consumeWhiteSpace();
$result = new URL(CSSString::parse($parserState), $parserState->currentLine());
if ($useUrl) {
$parserState->consumeWhiteSpace();
$parserState->consume(')');
}
return $oResult;
return $result;
}
/**
* @return void
*/
public function setURL(CSSString $oURL)
public function setURL(CSSString $url): void
{
$this->oURL = $oURL;
$this->url = $url;
}
/**
* @return CSSString
*/
public function getURL()
public function getURL(): CSSString
{
return $this->oURL;
return $this->url;
}
/**
* @return string
* @return non-empty-string
*/
public function render(OutputFormat $outputFormat): string
{
return "url({$this->url->render($outputFormat)})";
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
* @internal
*/
public function __toString()
public function getArrayRepresentation(): array
{
return $this->render(new OutputFormat());
}
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return "url({$this->oURL->render($oOutputFormat)})";
return [
'class' => $this->getShortClassName(),
// We're using the term "uri" here to match the wording used in the specs:
// https://www.w3.org/TR/CSS22/syndata.html#uri
'uri' => $this->url->getArrayRepresentation(),
];
}
}
+115 -105
View File
@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\CSSElement;
@@ -9,6 +11,9 @@ use Sabberworm\CSS\Parsing\UnexpectedEOFException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Position\Position;
use Sabberworm\CSS\Position\Positionable;
use Sabberworm\CSS\ShortClassNameProvider;
use function Safe\preg_match;
/**
* Abstract base class for specific classes of CSS values: `Size`, `Color`, `CSSString` and `URL`, and another
@@ -17,96 +22,94 @@ use Sabberworm\CSS\Position\Positionable;
abstract class Value implements CSSElement, Positionable
{
use Position;
use ShortClassNameProvider;
/**
* @param int $iLineNo
* @param int<1, max>|null $lineNumber
*/
public function __construct($iLineNo = 0)
public function __construct(?int $lineNumber = null)
{
$this->setPosition($iLineNo);
$this->setPosition($lineNumber);
}
/**
* @param array<array-key, string> $aListDelimiters
* @param array<non-empty-string> $listDelimiters
*
* @return RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string
* @return Value|string
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*
* @internal since V8.8.0
*/
public static function parseValue(ParserState $oParserState, array $aListDelimiters = [])
public static function parseValue(ParserState $parserState, array $listDelimiters = [])
{
/** @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aStack */
$aStack = [];
$oParserState->consumeWhiteSpace();
/** @var list<Value|string> $stack */
$stack = [];
$parserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|| $oParserState->comes(')')
|| $oParserState->comes('\\')
|| $oParserState->isEnd())
!($parserState->comes('}') || $parserState->comes(';') || $parserState->comes('!')
|| $parserState->comes(')')
|| $parserState->isEnd())
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
if (\count($stack) > 0) {
$foundDelimiter = false;
foreach ($listDelimiters as $delimiter) {
if ($parserState->comes($delimiter)) {
\array_push($stack, $parserState->consume($delimiter));
$parserState->consumeWhiteSpace();
$foundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
if (!$foundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
\array_push($stack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
\array_push($stack, self::parsePrimitiveValue($parserState));
$parserState->consumeWhiteSpace();
}
// Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
$iStackLength = count($aStack);
if ($iStackLength === 1) {
return $aStack[0];
foreach ($listDelimiters as $delimiter) {
$stackSize = \count($stack);
if ($stackSize === 1) {
return $stack[0];
}
$aNewStack = [];
for ($iStartPosition = 0; $iStartPosition < $iStackLength; ++$iStartPosition) {
if ($iStartPosition === ($iStackLength - 1) || $sDelimiter !== $aStack[$iStartPosition + 1]) {
$aNewStack[] = $aStack[$iStartPosition];
$newStack = [];
for ($offset = 0; $offset < $stackSize; ++$offset) {
if ($offset === ($stackSize - 1) || $delimiter !== $stack[$offset + 1]) {
$newStack[] = $stack[$offset];
continue;
}
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 3; $i < $iStackLength; $i += 2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
$length = 2; //Number of elements to be joined
for ($i = $offset + 3; $i < $stackSize; $i += 2, ++$length) {
if ($delimiter !== $stack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition; $i - $iStartPosition < $iLength * 2; $i += 2) {
$oList->addListComponent($aStack[$i]);
$list = new RuleValueList($delimiter, $parserState->currentLine());
for ($i = $offset; $i - $offset < $length * 2; $i += 2) {
$list->addListComponent($stack[$i]);
}
$aNewStack[] = $oList;
$iStartPosition += $iLength * 2 - 2;
$newStack[] = $list;
$offset += $length * 2 - 2;
}
$aStack = $aNewStack;
$stack = $newStack;
}
if (!isset($aStack[0])) {
if (!isset($stack[0])) {
throw new UnexpectedTokenException(
" {$oParserState->peek()} ",
$oParserState->peek(1, -1) . $oParserState->peek(2),
" {$parserState->peek()} ",
$parserState->peek(1, -1) . $parserState->peek(2),
'literal',
$oParserState->currentLine()
$parserState->currentLine()
);
}
return $aStack[0];
return $stack[0];
}
/**
* @param bool $bIgnoreCase
*
* @return CSSFunction|string
*
* @throws UnexpectedEOFException
@@ -114,27 +117,23 @@ abstract class Value implements CSSElement, Positionable
*
* @internal since V8.8.0
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
public static function parseIdentifierOrFunction(ParserState $parserState, bool $ignoreCase = false)
{
$oAnchor = $oParserState->anchor();
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$anchor = $parserState->anchor();
$result = $parserState->parseIdentifier($ignoreCase);
if ($oParserState->comes('(')) {
$oAnchor->backtrack();
if ($oParserState->streql('url', $mResult)) {
$mResult = URL::parse($oParserState);
} elseif (
$oParserState->streql('calc', $mResult)
|| $oParserState->streql('-webkit-calc', $mResult)
|| $oParserState->streql('-moz-calc', $mResult)
) {
$mResult = CalcFunction::parse($oParserState);
if ($parserState->comes('(')) {
$anchor->backtrack();
if ($parserState->streql('url', $result)) {
$result = URL::parse($parserState);
} elseif ($parserState->streql('calc', $result)) {
$result = CalcFunction::parse($parserState);
} else {
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
$result = CSSFunction::parse($parserState, $ignoreCase);
}
}
return $mResult;
return $result;
}
/**
@@ -146,73 +145,84 @@ abstract class Value implements CSSElement, Positionable
*
* @internal since V8.8.0
*/
public static function parsePrimitiveValue(ParserState $oParserState)
public static function parsePrimitiveValue(ParserState $parserState)
{
$oValue = null;
$oParserState->consumeWhiteSpace();
$parserState->consumeWhiteSpace();
if (
is_numeric($oParserState->peek())
|| ($oParserState->comes('-.')
&& is_numeric($oParserState->peek(1, 2)))
|| (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))
\is_numeric($parserState->peek())
|| ($parserState->comes('-.')
&& \is_numeric($parserState->peek(1, 2)))
|| (($parserState->comes('-') || $parserState->comes('.')) && \is_numeric($parserState->peek(1, 1)))
) {
$oValue = Size::parse($oParserState);
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} elseif ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} elseif ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
$value = Size::parse($parserState);
} elseif ($parserState->comes('#') || $parserState->comes('rgb', true) || $parserState->comes('hsl', true)) {
$value = Color::parse($parserState);
} elseif ($parserState->comes("'") || $parserState->comes('"')) {
$value = CSSString::parse($parserState);
} elseif ($parserState->comes('progid:') && $parserState->getSettings()->usesLenientParsing()) {
$value = self::parseMicrosoftFilter($parserState);
} elseif ($parserState->comes('[')) {
$value = LineName::parse($parserState);
} elseif ($parserState->comes('U+')) {
$value = self::parseUnicodeRangeValue($parserState);
} else {
$sNextChar = $oParserState->peek(1);
$nextCharacter = $parserState->peek(1);
try {
$oValue = self::parseIdentifierOrFunction($oParserState);
$value = self::parseIdentifierOrFunction($parserState);
} catch (UnexpectedTokenException $e) {
if (\in_array($sNextChar, ['+', '-', '*', '/'], true)) {
$oValue = $oParserState->consume(1);
if (\in_array($nextCharacter, ['+', '-', '*', '/'], true)) {
$value = $parserState->consume(1);
} else {
throw $e;
}
}
}
$oParserState->consumeWhiteSpace();
return $oValue;
$parserState->consumeWhiteSpace();
return $value;
}
/**
* @return CSSFunction
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
* @internal
*/
private static function parseMicrosoftFilter(ParserState $oParserState)
public function getArrayRepresentation(): array
{
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, [',', '=']);
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
return [
'class' => $this->getShortClassName(),
];
}
/**
* @return string
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseUnicodeRangeValue(ParserState $oParserState)
private static function parseMicrosoftFilter(ParserState $parserState): CSSFunction
{
$iCodepointMaxLength = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
$function = $parserState->consumeUntil('(', false, true);
$arguments = Value::parseValue($parserState, [',', '=']);
return new CSSFunction($function, $arguments, ',', $parserState->currentLine());
}
/**
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
private static function parseUnicodeRangeValue(ParserState $parserState): string
{
$codepointMaxLength = 6; // Code points outside BMP can use up to six digits
$range = '';
$parserState->consume('U+');
do {
if ($oParserState->comes('-')) {
$iCodepointMaxLength = 13; // Max length is 2 six digit code points + the dash(-) between them
if ($parserState->comes('-')) {
$codepointMaxLength = 13; // Max length is 2 six-digit code points + the dash(-) between them
}
$sRange .= $oParserState->consume(1);
} while (strlen($sRange) < $iCodepointMaxLength && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
$range .= $parserState->consume(1);
} while (
(\strlen($range) < $codepointMaxLength) && (preg_match('/[A-Fa-f0-9\\?-]/', $parserState->peek()) === 1)
);
return "U+{$range}";
}
}
+64 -54
View File
@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\ShortClassNameProvider;
/**
* A `ValueList` represents a lists of `Value`s, separated by some separation character
@@ -12,103 +15,110 @@ use Sabberworm\CSS\OutputFormat;
*/
abstract class ValueList extends Value
{
use ShortClassNameProvider;
/**
* @var array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @var array<Value|string>
*
* @internal since 8.8.0
*/
protected $aComponents;
protected $components;
/**
* @var string
* @var non-empty-string
*
* @internal since 8.8.0
*/
protected $sSeparator;
protected $separator;
/**
* phpcs:ignore Generic.Files.LineLength
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>|RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $aComponents
* @param string $sSeparator
* @param int $iLineNo
* @param array<Value|string>|Value|string $components
* @param non-empty-string $separator
* @param int<1, max>|null $lineNumber
*/
public function __construct($aComponents = [], $sSeparator = ',', $iLineNo = 0)
public function __construct($components = [], $separator = ',', ?int $lineNumber = null)
{
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = [$aComponents];
parent::__construct($lineNumber);
if (!\is_array($components)) {
$components = [$components];
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
$this->components = $components;
$this->separator = $separator;
}
/**
* @param RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string $mComponent
*
* @return void
* @param Value|string $component
*/
public function addListComponent($mComponent)
public function addListComponent($component): void
{
$this->aComponents[] = $mComponent;
$this->components[] = $component;
}
/**
* @return array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string>
* @return array<Value|string>
*/
public function getListComponents()
public function getListComponents(): array
{
return $this->aComponents;
return $this->components;
}
/**
* @param array<int, RuleValueList|CSSFunction|CSSString|LineName|Size|URL|string> $aComponents
*
* @return void
* @param array<Value|string> $components
*/
public function setListComponents(array $aComponents)
public function setListComponents(array $components): void
{
$this->aComponents = $aComponents;
$this->components = $components;
}
/**
* @return string
* @return non-empty-string
*/
public function getListSeparator()
public function getListSeparator(): string
{
return $this->sSeparator;
return $this->separator;
}
/**
* @param string $sSeparator
*
* @return void
* @param non-empty-string $separator
*/
public function setListSeparator($sSeparator)
public function setListSeparator(string $separator): void
{
$this->sSeparator = $sSeparator;
$this->separator = $separator;
}
/**
* @return string
*
* @deprecated in V8.8.0, will be removed in V9.0.0. Use `render` instead.
*/
public function __toString()
public function render(OutputFormat $outputFormat): string
{
return $this->render(new OutputFormat());
}
$formatter = $outputFormat->getFormatter();
/**
* @param OutputFormat|null $oOutputFormat
*
* @return string
*/
public function render($oOutputFormat)
{
return $oOutputFormat->implode(
$oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator
. $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator),
$this->aComponents
return $formatter->implode(
$formatter->spaceBeforeListArgumentSeparator($this->separator) . $this->separator
. $formatter->spaceAfterListArgumentSeparator($this->separator),
$this->components
);
}
/**
* @return array<string, bool|int|float|string|array<mixed>|null>
*
* @internal
*/
public function getArrayRepresentation(): array
{
return [
'class' => $this->getShortClassName(),
'components' => \array_map(
/**
* @parm Value|string $component
*/
function ($component): array {
if (\is_string($component)) {
return ['class' => 'string', 'value' => $component];
}
return $component->getArrayRepresentation();
},
$this->components
),
'separator' => $this->separator,
];
}
}