Subiendo proyecto completo sin restricciones de git ignore

This commit is contained in:
Jose Sanchez
2023-08-17 11:44:02 -04:00
parent a0d4f5ba3b
commit 20f1c60600
19921 changed files with 2509159 additions and 45 deletions

View File

@@ -0,0 +1 @@
vendor/

View File

@@ -0,0 +1,22 @@
language: php
php:
- "5.4"
- "5.5"
- "5.6"
- "7.0"
- "7.1"
- "7.2"
- "7.3"
- hhvm
sudo: false
dist: trusty
matrix:
include:
-
php: "5.3"
dist: precise
sudo: false
before_script: rm composer.lock && composer install
script: ./vendor/bin/phpunit --coverage-clover build/coverage/xml
after_script: ./vendor/bin/codacycoverage clover build/coverage/xml

View File

@@ -0,0 +1,224 @@
# Revision History
## 8.0
### 8.0.0 (2016-06-30)
* Store source CSS line numbers in tokens and parsing exceptions.
* *No deprecations*
#### Backwards-incompatible changes
* Unrecoverable parser errors throw an exception of type `Sabberworm\CSS\Parsing\SourceException` instead of `\Exception`.
### 8.1.0 (2016-07-19)
* Comments are no longer silently ignored but stored with the object with which they appear (no render support, though). Thanks to @FMCorz.
* The IE hacks using `\0` and `\9` can now be parsed (and rendered) in lenient mode. Thanks (again) to @FMCorz.
* Media queries with or without spaces before the query are parsed. Still no *real* parsing support, though. Sorry…
* PHPUnit is now listed as a dev-dependency in composer.json.
* *No backwards-incompatible changes*
* *No deprecations*
### 8.2.0 (2018-07-13)
* Support parsing `calc()`, thanks to @raxbg.
* Support parsing grid-lines, again thanks to @raxbg.
* Support parsing legacy IE filters (`progid:`) in lenient mode, thanks to @FMCorz
* Performance improvements parsing large files, again thanks to @FMCorz
* *No backwards-incompatible changes*
* *No deprecations*
### 8.3.0 (2019-02-22)
* Refactor parsing logic to mostly reside in the class files whose data structure is to be parsed (this should eventually allow us to unit-test specific parts of the parsing logic individually).
* Fix error in parsing `calc` expessions when the first operand is a negative number, thanks to @raxbg.
* Support parsing CSS4 colors in hex notation with alpha values, thanks to @raxbg.
* Swallow more errors in lenient mode, thanks to @raxbg.
* Allow specifying arbitrary strings to output before and after declaration blocks, thanks to @westonruter.
* *No backwards-incompatible changes*
* *No deprecations*
## 7.0
### 7.0.0 (2015-08-24)
* Compatibility with PHP 7. Well timed, eh?
* *No deprecations*
#### Backwards-incompatible changes
* The `Sabberworm\CSS\Value\String` class has been renamed to `Sabberworm\CSS\Value\CSSString`.
### 7.0.1 (2015-12-25)
* No more suppressed `E_NOTICE`
* *No backwards-incompatible changes*
* *No deprecations*
### 7.0.2 (2016-02-11)
* 150 time performance boost thanks to @[ossinkine](https://github.com/ossinkine)
* *No backwards-incompatible changes*
* *No deprecations*
### 7.0.3 (2016-04-27)
* Fixed parsing empty CSS when multibyte is off
* *No backwards-incompatible changes*
* *No deprecations*
## 6.0
### 6.0.0 (2014-07-03)
* Format output using Sabberworm\CSS\OutputFormat
* *No backwards-incompatible changes*
#### Deprecations
* The parse() method replaces __toString with an optional argument (instance of the OutputFormat class)
### 6.0.1 (2015-08-24)
* Remove some declarations in interfaces incompatible with PHP 5.3 (< 5.3.9)
* *No deprecations*
## 5.0
### 5.0.0 (2013-03-20)
* Correctly parse all known CSS 3 units (including Hz and kHz).
* Output RGB colors in short (#aaa or #ababab) notation
* Be case-insensitive when parsing identifiers.
* *No deprecations*
#### Backwards-incompatible changes
* `Sabberworm\CSS\Value\Color`s `__toString` method overrides `CSSList`s to maybe return something other than `type(value, …)` (see above).
### 5.0.1 (2013-03-20)
* Internal cleanup
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.2 (2013-03-21)
* CHANGELOG.md file added to distribution
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.3 (2013-03-21)
* More size units recognized
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.4 (2013-03-21)
* Dont output floats with locale-aware separator chars
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.5 (2013-04-17)
* Initial support for lenient parsing (setting this parser option will catch some exceptions internally and recover the parsers state as neatly as possible).
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.6 (2013-05-31)
* Fix broken unit test
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.7 (2013-08-04)
* Fix broken decimal point output optimization
* *No backwards-incompatible changes*
* *No deprecations*
### 5.0.8 (2013-08-15)
* Make default settings multibyte parsing option dependent on whether or not the mbstring extension is actually installed.
* *No backwards-incompatible changes*
* *No deprecations*
### 5.1.0 (2013-10-24)
* Performance enhancements by Michael M Slusarz
* More rescue entry points for lenient parsing (unexpected tokens between declaration blocks and unclosed comments)
* *No backwards-incompatible changes*
* *No deprecations*
### 5.1.1 (2013-10-28)
* Updated CHANGELOG.md to reflect changes since 5.0.4
* *No backwards-incompatible changes*
* *No deprecations*
### 5.1.2 (2013-10-30)
* Remove the use of consumeUntil in comment parsing. This makes it possible to parse comments such as `/** Perfectly valid **/`
* Add fr relative size unit
* Fix some issues with HHVM
* *No backwards-incompatible changes*
* *No deprecations*
### 5.2.0 (2014-06-30)
* Support removing a selector from a declaration block using `$oBlock->removeSelector($mSelector)`
* Introduce a specialized exception (Sabberworm\CSS\Parsing\OuputException) for exceptions during output rendering
* *No deprecations*
#### Backwards-incompatible changes
* Outputting a declaration block that has no selectors throws an OuputException instead of outputting an invalid ` {…}` into the CSS document.
## 4.0
### 4.0.0 (2013-03-19)
* Support for more @-rules
* Generic interface `Sabberworm\CSS\Property\AtRule`, implemented by all @-rule classes
* *No deprecations*
#### Backwards-incompatible changes
* `Sabberworm\CSS\RuleSet\AtRule` renamed to `Sabberworm\CSS\RuleSet\AtRuleSet`
* `Sabberworm\CSS\CSSList\MediaQuery` renamed to `Sabberworm\CSS\RuleSet\CSSList\AtRuleBlockList` with differing semantics and API (which also works for other block-list-based @-rules like `@supports`).
## 3.0
### 3.0.0 (2013-03-06)
* Support for lenient parsing (on by default)
* *No deprecations*
#### Backwards-incompatible changes
* All properties (like whether or not to use `mb_`-functions, which default charset to use and new whether or not to be forgiving when parsing) are now encapsulated in an instance of `Sabberworm\CSS\Settings` which can be passed as the second argument to `Sabberworm\CSS\Parser->__construct()`.
* Specifying a charset as the second argument to `Sabberworm\CSS\Parser->__construct()` is no longer supported. Use `Sabberworm\CSS\Settings::create()->withDefaultCharset('some-charset')` instead.
* Setting `Sabberworm\CSS\Parser->bUseMbFunctions` has no effect. Use `Sabberworm\CSS\Settings::create()->withMultibyteSupport(true/false)` instead.
* `Sabberworm\CSS\Parser->parse()` may throw a `Sabberworm\CSS\Parsing\UnexpectedTokenException` when in strict parsing mode.
## 2.0
### 2.0.0 (2013-01-29)
* Allow multiple rules of the same type per rule set
#### Backwards-incompatible changes
* `Sabberworm\CSS\RuleSet->getRules()` returns an index-based array instead of an associative array. Use `Sabberworm\CSS\RuleSet->getRulesAssoc()` (which eliminates duplicate rules and lets the later rule of the same name win).
* `Sabberworm\CSS\RuleSet->removeRule()` works as it did before except when passed an instance of `Sabberworm\CSS\Rule\Rule`, in which case it would only remove the exact rule given instead of all the rules of the same type. To get the old behaviour, use `Sabberworm\CSS\RuleSet->removeRule($oRule->getRule()`;
## 1.0
Initial release of a stable public API.
## 0.9
Last version not to use PSR-0 project organization semantics.

2310
vendor/sabberworm/php-css-parser/Doxyfile vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,642 @@
PHP CSS Parser
--------------
[![build status](https://api.travis-ci.org/sabberworm/PHP-CSS-Parser.svg)](https://travis-ci.org/sabberworm/PHP-CSS-Parser) [![HHVM Status](http://hhvm.h4cc.de/badge/sabberworm/php-css-parser.svg)](http://hhvm.h4cc.de/package/sabberworm/php-css-parser)
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
## Usage
### Installation using composer
Add php-css-parser to your composer.json
```json
{
"require": {
"sabberworm/php-css-parser": "*"
}
}
```
### Extraction
To use the CSS Parser, create a new instance. The constructor takes the following form:
```php
new Sabberworm\CSS\Parser($sText);
```
To read a file, for example, youd do the following:
```php
$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
$oCssDocument = $oCssParser->parse();
```
The resulting CSS document structure can be manipulated prior to being output.
### Options
#### Charset
The charset option is used only if no @charset declaration is found in the CSS file. UTF-8 is the default, so you wont have to create a settings object at all if you dont intend to change that.
```php
$oSettings = Sabberworm\CSS\Settings::create()->withDefaultCharset('windows-1252');
new Sabberworm\CSS\Parser($sText, $oSettings);
```
#### Strict parsing
To have the parser choke on invalid rules, supply a thusly configured Sabberworm\CSS\Settings object:
```php
$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css'), Sabberworm\CSS\Settings::create()->beStrict());
```
#### Disable multibyte functions
To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still its not recommended to use this with input you have no control over as its not thoroughly covered by test cases.
```php
$oSettings = Sabberworm\CSS\Settings::create()->withMultibyteSupport(false);
new Sabberworm\CSS\Parser($sText, $oSettings);
```
### Manipulation
The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset` which you wont use often.
#### CSSList
`CSSList` represents a generic CSS container, most likely containing declaration blocks (rule sets with a selector) but it may also contain at-rules, charset declarations, etc. `CSSList` has the following concrete subtypes:
* `Document` representing the root of a CSS file.
* `MediaQuery` represents a subsection of a CSSList that only applies to a output device matching the contained media query.
To access the items stored in a `CSSList` like the document you got back when calling `$oCssParser->parse()` , use `getContents()`, then iterate over that collection and use instanceof to check whether youre dealing with another `CSSList`, a `RuleSet`, a `Import` or a `Charset`.
To append a new item (selector, media query, etc.) to an existing `CSSList`, construct it using the constructor for this class and use the `append($oItem)` method.
#### RuleSet
`RuleSet` is a container for individual rules. The most common form of a rule set is one constrained by a selector. The following concrete subtypes exist:
* `AtRuleSet` for generic at-rules which do not match the ones specifically mentioned like @import, @charset or @media. A common example for this is @font-face.
* `DeclarationBlock` a RuleSet constrained by a `Selector`; contains an array of selector objects (comma-separated in the CSS) as well as the rules to be applied to the matching elements.
Note: A `CSSList` can contain other `CSSList`s (and `Import`s as well as a `Charset`) while a `RuleSet` can only contain `Rule`s.
If you want to manipulate a `RuleSet`, use the methods `addRule(Rule $oRule)`, `getRules()` and `removeRule($mRule)` (which accepts either a Rule instance or a rule name; optionally suffixed by a dash to remove all related rules).
#### Rule
`Rule`s just have a key (the rule) and a value. These values are all instances of a `Value`.
#### Value
`Value` is an abstract class that only defines the `render` method. The concrete subclasses for atomic value types are:
* `Size` consists of a numeric `size` value and a unit.
* `Color` colors can be input in the form #rrggbb, #rgb or schema(val1, val2, …) but are always stored as an array of ('s' => val1, 'c' => val2, 'h' => val3, …) and output in the second form.
* `CSSString` this is just a wrapper for quoted strings to distinguish them from keywords; always output with double quotes.
* `URL` URLs in CSS; always output in URL("") notation.
There is another abstract subclass of `Value`, `ValueList`. A `ValueList` represents a lists of `Value`s, separated by some separation character (mostly `,`, whitespace, or `/`). There are two types of `ValueList`s:
* `RuleValueList` The default type, used to represent all multi-valued rules like `font: bold 12px/3 Helvetica, Verdana, sans-serif;` (where the value would be a whitespace-separated list of the primitive value `bold`, a slash-separated list and a comma-separated list).
* `CSSFunction` A special kind of value that also contains a function name and where the values are the functions arguments. Also handles equals-sign-separated argument lists like `filter: alpha(opacity=90);`.
#### Convenience methods
There are a few convenience methods on Document to ease finding, manipulating and deleting rules:
* `getAllDeclarationBlocks()` does what it says; no matter how deeply nested your selectors are. Aliased as `getAllSelectors()`.
* `getAllRuleSets()` does what it says; no matter how deeply nested your rule sets are.
* `getAllValues()` finds all `Value` objects inside `Rule`s.
## To-Do
* More convenience methods [like `selectorsWithElement($sId/Class/TagName)`, `attributesOfType($sType)`, `removeAttributesOfType($sType)`]
* Real multibyte support. Currently only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description).
* Named color support (using `Color` instead of an anonymous string literal)
## Use cases
### Use `Parser` to prepend an id to all selectors
```php
$sMyId = "#my_id";
$oParser = new Sabberworm\CSS\Parser($sText);
$oCss = $oParser->parse();
foreach($oCss->getAllDeclarationBlocks() as $oBlock) {
foreach($oBlock->getSelectors() as $oSelector) {
//Loop over all selector parts (the comma-separated strings in a selector) and prepend the id
$oSelector->setSelector($sMyId.' '.$oSelector->getSelector());
}
}
```
### Shrink all absolute sizes to half
```php
$oParser = new Sabberworm\CSS\Parser($sText);
$oCss = $oParser->parse();
foreach($oCss->getAllValues() as $mValue) {
if($mValue instanceof CSSSize && !$mValue->isRelative()) {
$mValue->setSize($mValue->getSize()/2);
}
}
```
### Remove unwanted rules
```php
$oParser = new Sabberworm\CSS\Parser($sText);
$oCss = $oParser->parse();
foreach($oCss->getAllRuleSets() as $oRuleSet) {
$oRuleSet->removeRule('font-'); //Note that the added dash will make this remove all rules starting with font- (like font-size, font-weight, etc.) as well as a potential font-rule
$oRuleSet->removeRule('cursor');
}
```
### Output
To output the entire CSS document into a variable, just use `->render()`:
```php
$oCssParser = new Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
$oCssDocument = $oCssParser->parse();
print $oCssDocument->render();
```
If you want to format the output, pass an instance of type `Sabberworm\CSS\OutputFormat`:
```php
$oFormat = Sabberworm\CSS\OutputFormat::create()->indentWithSpaces(4)->setSpaceBetweenRules("\n");
print $oCssDocument->render($oFormat);
```
Or use one of the predefined formats:
```php
print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createPretty());
print $oCssDocument->render(Sabberworm\CSS\OutputFormat::createCompact());
```
To see what you can do with output formatting, look at the tests in `tests/Sabberworm/CSS/OutputFormatTest.php`.
## Examples
### Example 1 (At-Rules)
#### Input
```css
@charset "utf-8";
@font-face {
font-family: "CrassRoots";
src: url("../media/cr.ttf")
}
html, body {
font-size: 1.6em
}
@keyframes mymove {
from { top: 0px; }
to { top: 200px; }
}
```
#### Structure (`var_dump()`)
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
protected $aContents =>
array(4) {
[0] =>
class Sabberworm\CSS\Property\Charset#6 (2) {
private $sCharset =>
class Sabberworm\CSS\Value\CSSString#5 (2) {
private $sString =>
string(5) "utf-8"
protected $iLineNo =>
int(1)
}
protected $iLineNo =>
int(1)
}
[1] =>
class Sabberworm\CSS\RuleSet\AtRuleSet#7 (4) {
private $sType =>
string(9) "font-face"
private $sArgs =>
string(0) ""
private $aRules =>
array(2) {
'font-family' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#8 (4) {
private $sRule =>
string(11) "font-family"
private $mValue =>
class Sabberworm\CSS\Value\CSSString#9 (2) {
private $sString =>
string(10) "CrassRoots"
protected $iLineNo =>
int(4)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(4)
}
}
'src' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#10 (4) {
private $sRule =>
string(3) "src"
private $mValue =>
class Sabberworm\CSS\Value\URL#11 (2) {
private $oURL =>
class Sabberworm\CSS\Value\CSSString#12 (2) {
private $sString =>
string(15) "../media/cr.ttf"
protected $iLineNo =>
int(5)
}
protected $iLineNo =>
int(5)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(5)
}
}
}
protected $iLineNo =>
int(3)
}
[2] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#13 (3) {
private $aSelectors =>
array(2) {
[0] =>
class Sabberworm\CSS\Property\Selector#14 (2) {
private $sSelector =>
string(4) "html"
private $iSpecificity =>
NULL
}
[1] =>
class Sabberworm\CSS\Property\Selector#15 (2) {
private $sSelector =>
string(4) "body"
private $iSpecificity =>
NULL
}
}
private $aRules =>
array(1) {
'font-size' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
private $sRule =>
string(9) "font-size"
private $mValue =>
class Sabberworm\CSS\Value\Size#17 (4) {
private $fSize =>
double(1.6)
private $sUnit =>
string(2) "em"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(9)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(9)
}
}
}
protected $iLineNo =>
int(8)
}
[3] =>
class Sabberworm\CSS\CSSList\KeyFrame#18 (4) {
private $vendorKeyFrame =>
string(9) "keyframes"
private $animationName =>
string(6) "mymove"
protected $aContents =>
array(2) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#19 (3) {
private $aSelectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#20 (2) {
private $sSelector =>
string(4) "from"
private $iSpecificity =>
NULL
}
}
private $aRules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#21 (4) {
private $sRule =>
string(3) "top"
private $mValue =>
class Sabberworm\CSS\Value\Size#22 (4) {
private $fSize =>
double(0)
private $sUnit =>
string(2) "px"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(13)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(13)
}
}
}
protected $iLineNo =>
int(13)
}
[1] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#23 (3) {
private $aSelectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#24 (2) {
private $sSelector =>
string(2) "to"
private $iSpecificity =>
NULL
}
}
private $aRules =>
array(1) {
'top' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#25 (4) {
private $sRule =>
string(3) "top"
private $mValue =>
class Sabberworm\CSS\Value\Size#26 (4) {
private $fSize =>
double(200)
private $sUnit =>
string(2) "px"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(14)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(14)
}
}
}
protected $iLineNo =>
int(14)
}
}
protected $iLineNo =>
int(12)
}
}
protected $iLineNo =>
int(1)
}
```
#### Output (`render()`)
```css
@charset "utf-8";
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
html, body {font-size: 1.6em;}
@keyframes mymove {from {top: 0px;}
to {top: 200px;}}
```
### Example 2 (Values)
#### Input
```css
#header {
margin: 10px 2em 1cm 2%;
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
color: red !important;
}
```
#### Structure (`var_dump()`)
```php
class Sabberworm\CSS\CSSList\Document#4 (2) {
protected $aContents =>
array(1) {
[0] =>
class Sabberworm\CSS\RuleSet\DeclarationBlock#5 (3) {
private $aSelectors =>
array(1) {
[0] =>
class Sabberworm\CSS\Property\Selector#6 (2) {
private $sSelector =>
string(7) "#header"
private $iSpecificity =>
NULL
}
}
private $aRules =>
array(3) {
'margin' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#7 (4) {
private $sRule =>
string(6) "margin"
private $mValue =>
class Sabberworm\CSS\Value\RuleValueList#12 (3) {
protected $aComponents =>
array(4) {
[0] =>
class Sabberworm\CSS\Value\Size#8 (4) {
private $fSize =>
double(10)
private $sUnit =>
string(2) "px"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(2)
}
[1] =>
class Sabberworm\CSS\Value\Size#9 (4) {
private $fSize =>
double(2)
private $sUnit =>
string(2) "em"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(2)
}
[2] =>
class Sabberworm\CSS\Value\Size#10 (4) {
private $fSize =>
double(1)
private $sUnit =>
string(2) "cm"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(2)
}
[3] =>
class Sabberworm\CSS\Value\Size#11 (4) {
private $fSize =>
double(2)
private $sUnit =>
string(1) "%"
private $bIsColorComponent =>
bool(false)
protected $iLineNo =>
int(2)
}
}
protected $sSeparator =>
string(1) " "
protected $iLineNo =>
int(2)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(2)
}
}
'font-family' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#13 (4) {
private $sRule =>
string(11) "font-family"
private $mValue =>
class Sabberworm\CSS\Value\RuleValueList#15 (3) {
protected $aComponents =>
array(4) {
[0] =>
string(7) "Verdana"
[1] =>
string(9) "Helvetica"
[2] =>
class Sabberworm\CSS\Value\CSSString#14 (2) {
private $sString =>
string(9) "Gill Sans"
protected $iLineNo =>
int(3)
}
[3] =>
string(10) "sans-serif"
}
protected $sSeparator =>
string(1) ","
protected $iLineNo =>
int(3)
}
private $bIsImportant =>
bool(false)
protected $iLineNo =>
int(3)
}
}
'color' =>
array(1) {
[0] =>
class Sabberworm\CSS\Rule\Rule#16 (4) {
private $sRule =>
string(5) "color"
private $mValue =>
string(3) "red"
private $bIsImportant =>
bool(true)
protected $iLineNo =>
int(4)
}
}
}
protected $iLineNo =>
int(1)
}
}
protected $iLineNo =>
int(1)
}
```
#### Output (`render()`)
```css
#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;}
```
## Contributors/Thanks to
* [raxbg](https://github.com/raxbg) for contributions to parse `calc`, grid lines, and various bugfixes.
* [westonruter](https://github.com/westonruter) for bugfixes and improvements.
* [FMCorz](https://github.com/FMCorz) for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode).
* [Lullabot](https://github.com/Lullabot) for a patch that allows to know the line number for each parsed token.
* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties.
* [ossinkine](https://github.com/ossinkine) for a 150 time performance boost.
* [GaryJones](https://github.com/GaryJones) for lots of input and [http://css-specificity.info/](http://css-specificity.info/).
* [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.
* [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/phpunit/phpunit/phpunit`.
## License
PHP-CSS-Parser is freely distributable under the terms of an MIT-style license.
Copyright (c) 2011 Raphael Schweikert, http://sabberworm.com/
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,21 @@
{
"name": "sabberworm/php-css-parser",
"type": "library",
"description": "Parser for CSS Files written in PHP",
"keywords": ["parser", "css", "stylesheet"],
"homepage": "http://www.sabberworm.com/blog/2010/6/10/php-css-parser",
"license": "MIT",
"authors": [
{"name": "Raphael Schweikert"}
],
"require": {
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "~4.8",
"codacy/coverage": "^1.4"
},
"autoload": {
"psr-0": { "Sabberworm\\CSS": "lib/" }
}
}

1478
vendor/sabberworm/php-css-parser/composer.lock generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Property\AtRule;
/**
* A BlockList constructed by an unknown @-rule. @media rules are rendered into AtRuleBlockList objects.
*/
class AtRuleBlockList extends CSSBlockList implements AtRule {
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = $oOutputFormat->sBeforeAtRuleBlock;
$sResult .= "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterAtRuleBlock;
return $sResult;
}
public function isRootList() {
return false;
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\ValueList;
use Sabberworm\CSS\Value\CSSFunction;
/**
* A CSSBlockList is a CSSList whose DeclarationBlocks are guaranteed to contain valid declaration blocks or at-rules.
* Most CSSLists conform to this category but some at-rules (such as @keyframes) do not.
*/
abstract class CSSBlockList extends CSSList {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
protected function allDeclarationBlocks(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allDeclarationBlocks($aResult);
}
}
}
protected function allRuleSets(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof RuleSet) {
$aResult[] = $mContent;
} else if ($mContent instanceof CSSBlockList) {
$mContent->allRuleSets($aResult);
}
}
}
protected function allValues($oElement, &$aResult, $sSearchString = null, $bSearchInFunctionArguments = false) {
if ($oElement instanceof CSSBlockList) {
foreach ($oElement->getContents() as $oContent) {
$this->allValues($oContent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof RuleSet) {
foreach ($oElement->getRules($sSearchString) as $oRule) {
$this->allValues($oRule, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
} else if ($oElement instanceof Rule) {
$this->allValues($oElement->getValue(), $aResult, $sSearchString, $bSearchInFunctionArguments);
} else if ($oElement instanceof ValueList) {
if ($bSearchInFunctionArguments || !($oElement instanceof CSSFunction)) {
foreach ($oElement->getListComponents() as $mComponent) {
$this->allValues($mComponent, $aResult, $sSearchString, $bSearchInFunctionArguments);
}
}
} else {
//Non-List Value or CSSString (CSS identifier)
$aResult[] = $oElement;
}
}
protected function allSelectors(&$aResult, $sSpecificitySearch = null) {
$aDeclarationBlocks = array();
$this->allDeclarationBlocks($aDeclarationBlocks);
foreach ($aDeclarationBlocks as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
if ($sSpecificitySearch === null) {
$aResult[] = $oSelector;
} else {
$sComparator = '===';
$aSpecificitySearch = explode(' ', $sSpecificitySearch);
$iTargetSpecificity = $aSpecificitySearch[0];
if(count($aSpecificitySearch) > 1) {
$sComparator = $aSpecificitySearch[0];
$iTargetSpecificity = $aSpecificitySearch[1];
}
$iTargetSpecificity = (int)$iTargetSpecificity;
$iSelectorSpecificity = $oSelector->getSpecificity();
$bMatches = false;
switch($sComparator) {
case '<=':
$bMatches = $iSelectorSpecificity <= $iTargetSpecificity;
break;
case '<':
$bMatches = $iSelectorSpecificity < $iTargetSpecificity;
break;
case '>=':
$bMatches = $iSelectorSpecificity >= $iTargetSpecificity;
break;
case '>':
$bMatches = $iSelectorSpecificity > $iTargetSpecificity;
break;
default:
$bMatches = $iSelectorSpecificity === $iTargetSpecificity;
break;
}
if ($bMatches) {
$aResult[] = $oSelector;
}
}
}
}
}
}

View File

@@ -0,0 +1,348 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\AtRule;
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\Value\CSSString;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;
/**
* A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
* Also, it may contain Import and Charset objects stemming from @-rules.
*/
abstract class CSSList implements Renderable, Commentable {
protected $aComments;
protected $aContents;
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->aComments = array();
$this->aContents = array();
$this->iLineNo = $iLineNo;
}
public static function parseList(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if(is_string($oParserState)) {
$oParserState = new ParserState($oParserState);
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
while(!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$oListItem = null;
if($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if($oListItem === null) {
// List parsing finished
return;
}
if($oListItem) {
$oListItem->setComments($comments);
$oList->append($oListItem);
}
$oParserState->consumeWhiteSpace();
}
if(!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
private static function parseListItem(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if($oAtRule instanceof Charset) {
if(!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
}
if(count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} else if ($oParserState->comes('}')) {
$oParserState->consume('}');
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('}')) $oParserState->consume('}');
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
return null;
}
} else {
return DeclarationBlock::parse($oParserState);
}
}
private static function parseAtRule(ParserState $oParserState) {
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = $oParserState->consumeUntil(';');
}
$oParserState->consume(';');
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consume(';');
return new Charset($sCharset, $iIdentifierLineNum);
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
return $oResult;
} else if ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consume(';');
if ($sPrefix !== null && !is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
//Unknown other at rule (font-face or such)
$sArgs = trim($oParserState->consumeUntil('{', false, true));
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
if($oParserState->getSettings()->bLenientParsing) {
return NULL;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$bUseRuleSet = true;
foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
break;
}
}
if($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
}
return $oAtRule;
}
}
/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
*/
private static function identifierIs($sIdentifier, $sMatch) {
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* Prepend item to list of contents.
*
* @param object $oItem Item.
*/
public function prepend($oItem) {
array_unshift($this->aContents, $oItem);
}
/**
* Append item to list of contents.
*
* @param object $oItem Item.
*/
public function append($oItem) {
$this->aContents[] = $oItem;
}
/**
* Splice the list of contents.
*
* @param int $iOffset Offset.
* @param int $iLength Length. Optional.
* @param RuleSet[] $mReplacement Replacement. Optional.
*/
public function splice($iOffset, $iLength = null, $mReplacement = null) {
array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
}
/**
* Removes an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
* @return bool Whether the item was removed.
*/
public function remove($oItemToRemove) {
$iKey = array_search($oItemToRemove, $this->aContents, true);
if ($iKey !== false) {
unset($this->aContents[$iKey]);
return true;
}
return false;
}
/**
* Replaces an item from the CSS list.
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
*/
public function replace($oOldItem, $oNewItem) {
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
array_splice($this->aContents, $iKey, 1, $oNewItem);
return true;
}
return false;
}
/**
* Set the contents.
* @param array $aContents Objects to set as content.
*/
public function setContents(array $aContents) {
$this->aContents = array();
foreach ($aContents as $content) {
$this->append($content);
}
}
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
* @param array|string $mSelector The selectors to match.
* @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks
*/
public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) {
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
}
if (!is_array($mSelector)) {
$mSelector = explode(',', $mSelector);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!($mSel instanceof Selector)) {
$mSel = new Selector($mSel);
}
}
foreach ($this->aContents as $iKey => $mItem) {
if (!($mItem instanceof DeclarationBlock)) {
continue;
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
return;
}
}
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
if(!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
}
$sResult .= $sRendered;
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
}
return $sResult;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
*/
public abstract function isRootList();
public function getContents() {
return $this->aContents;
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Parsing\ParserState;
/**
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
*/
class Document extends CSSBlockList {
/**
* Document constructor.
* @param int $iLineNo
*/
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState) {
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}
/**
* Gets all DeclarationBlock objects recursively.
*/
public function getAllDeclarationBlocks() {
$aResult = array();
$this->allDeclarationBlocks($aResult);
return $aResult;
}
/**
* @deprecated use getAllDeclarationBlocks()
*/
public function getAllSelectors() {
return $this->getAllDeclarationBlocks();
}
/**
* Returns all RuleSet objects found recursively in the tree.
*/
public function getAllRuleSets() {
$aResult = array();
$this->allRuleSets($aResult);
return $aResult;
}
/**
* Returns all Value objects found recursively in the tree.
* @param (object|string) $mElement the CSSList or RuleSet to start the search from (defaults to the whole document). If a string is given, it is used as rule name filter (@see{RuleSet->getRules()}).
* @param (bool) $bSearchInFunctionArguments whether to also return Value objects used as Function arguments.
*/
public function getAllValues($mElement = null, $bSearchInFunctionArguments = false) {
$sSearchString = null;
if ($mElement === null) {
$mElement = $this;
} else if (is_string($mElement)) {
$sSearchString = $mElement;
$mElement = $this;
}
$aResult = array();
$this->allValues($mElement, $aResult, $sSearchString, $bSearchInFunctionArguments);
return $aResult;
}
/**
* Returns all Selector objects found recursively in the tree.
* Note that this does not yield the full DeclarationBlock that the selector belongs to (and, currently, there is no way to get to that).
* @param $sSpecificitySearch An optional filter by specificity. May contain a comparison operator and a number or just a number (defaults to "==").
* @example getSelectorsBySpecificity('>= 100')
*/
public function getSelectorsBySpecificity($sSpecificitySearch = null) {
$aResult = array();
$this->allSelectors($aResult, $sSpecificitySearch);
return $aResult;
}
/**
* Expands all shorthand properties to their long value
*/
public function expandShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandShorthands();
}
}
/**
* Create shorthands properties whenever possible
*/
public function createShorthands() {
foreach ($this->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createShorthands();
}
}
// Override render() to make format argument optional
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat = null) {
if($oOutputFormat === null) {
$oOutputFormat = new \Sabberworm\CSS\OutputFormat();
}
return parent::render($oOutputFormat);
}
public function isRootList() {
return true;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Property\AtRule;
class KeyFrame extends CSSList implements AtRule {
private $vendorKeyFrame;
private $animationName;
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
public function setVendorKeyFrame($vendorKeyFrame) {
$this->vendorKeyFrame = $vendorKeyFrame;
}
public function getVendorKeyFrame() {
return $this->vendorKeyFrame;
}
public function setAnimationName($animationName) {
$this->animationName = $animationName;
}
public function getAnimationName() {
return $this->animationName;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "@{$this->vendorKeyFrame} {$this->animationName}{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
public function isRootList() {
return false;
}
public function atRuleName() {
return $this->vendorKeyFrame;
}
public function atRuleArgs() {
return $this->animationName;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Sabberworm\CSS\Comment;
use Sabberworm\CSS\Renderable;
class Comment implements Renderable {
protected $iLineNo;
protected $sComment;
public function __construct($sComment = '', $iLineNo = 0) {
$this->sComment = $sComment;
$this->iLineNo = $iLineNo;
}
/**
* @return string
*/
public function getComment() {
return $this->sComment;
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
/**
* @return string
*/
public function setComment($sComment) {
$this->sComment = $sComment;
}
/**
* @return string
*/
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
/**
* @return string
*/
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '/*' . $this->sComment . '*/';
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Sabberworm\CSS\Comment;
interface Commentable {
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments);
/**
* @return array
*/
public function getComments();
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments);
}

View File

@@ -0,0 +1,322 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Parsing\OutputException;
/**
* Class OutputFormat
*
* @method OutputFormat setSemicolonAfterLastRule( bool $bSemicolonAfterLastRule ) Set whether semicolons are added after last rule.
*/
class OutputFormat {
/**
* Value format
*/
// " means double-quote, ' means single-quote
public $sStringQuotingType = '"';
// Output RGB colors in hash notation if possible
public $bRGBHashNotation = true;
/**
* Declaration format
*/
// Semicolon after the last rule of a declaration block can be omitted. To do that, set this false.
public $bSemicolonAfterLastRule = 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");`)
*/
public $sSpaceAfterRuleName = ' ';
public $sSpaceBeforeRules = '';
public $sSpaceAfterRules = '';
public $sSpaceBetweenRules = '';
public $sSpaceBeforeBlocks = '';
public $sSpaceAfterBlocks = '';
public $sSpaceBetweenBlocks = "\n";
// Content injected in and around @-rule blocks.
public $sBeforeAtRuleBlock = '';
public $sAfterAtRuleBlock = '';
// This is whats printed before and after the comma if a declaration block contains multiple selectors.
public $sSpaceBeforeSelectorSeparator = '';
public $sSpaceAfterSelectorSeparator = ' ';
// This is whats printed after the comma of value lists
public $sSpaceBeforeListArgumentSeparator = '';
public $sSpaceAfterListArgumentSeparator = '';
public $sSpaceBeforeOpeningBrace = ' ';
// Content injected in and around declaration blocks.
public $sBeforeDeclarationBlock = '';
public $sAfterDeclarationBlockSelectors = '';
public $sAfterDeclarationBlock = '';
/**
* Indentation
*/
// Indentation character(s) per level. Only applicable if newlines are used in any of the spacing settings.
public $sIndentation = "\t";
/**
* Output exceptions.
*/
public $bIgnoreExceptions = false;
private $oFormatter = null;
private $oNextLevelFormat = null;
private $iIndentationLevel = 0;
public function __construct() {
}
public function get($sName) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
foreach($aVarPrefixes as $sPrefix) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
return $this->$sFieldName;
}
}
return null;
}
public function set($aNames, $mValue) {
$aVarPrefixes = array('a', 's', 'm', 'b', 'f', 'o', 'c', 'i');
if(is_string($aNames) && strpos($aNames, '*') !== false) {
$aNames = array(str_replace('*', 'Before', $aNames), str_replace('*', 'Between', $aNames), str_replace('*', 'After', $aNames));
} else if(!is_array($aNames)) {
$aNames = array($aNames);
}
foreach($aVarPrefixes as $sPrefix) {
$bDidReplace = false;
foreach($aNames as $sName) {
$sFieldName = $sPrefix.ucfirst($sName);
if(isset($this->$sFieldName)) {
$this->$sFieldName = $mValue;
$bDidReplace = true;
}
}
if($bDidReplace) {
return $this;
}
}
// Break the chain so the user knows this option is invalid
return false;
}
public function __call($sMethodName, $aArguments) {
if(strpos($sMethodName, 'set') === 0) {
return $this->set(substr($sMethodName, 3), $aArguments[0]);
} else if(strpos($sMethodName, 'get') === 0) {
return $this->get(substr($sMethodName, 3));
} else if(method_exists('\\Sabberworm\\CSS\\OutputFormatter', $sMethodName)) {
return call_user_func_array(array($this->getFormatter(), $sMethodName), $aArguments);
} else {
throw new \Exception('Unknown OutputFormat method called: '.$sMethodName);
}
}
public function indentWithTabs($iNumber = 1) {
return $this->setIndentation(str_repeat("\t", $iNumber));
}
public function indentWithSpaces($iNumber = 2) {
return $this->setIndentation(str_repeat(" ", $iNumber));
}
public function nextLevel() {
if($this->oNextLevelFormat === null) {
$this->oNextLevelFormat = clone $this;
$this->oNextLevelFormat->iIndentationLevel++;
$this->oNextLevelFormat->oFormatter = null;
}
return $this->oNextLevelFormat;
}
public function beLenient() {
$this->bIgnoreExceptions = true;
}
public function getFormatter() {
if($this->oFormatter === null) {
$this->oFormatter = new OutputFormatter($this);
}
return $this->oFormatter;
}
public function level() {
return $this->iIndentationLevel;
}
/**
* Create format.
*
* @return OutputFormat Format.
*/
public static function create() {
return new OutputFormat();
}
/**
* Create compact format.
*
* @return OutputFormat Format.
*/
public static function createCompact() {
$format = self::create();
$format->set('Space*Rules', "")->set('Space*Blocks', "")->setSpaceAfterRuleName('')->setSpaceBeforeOpeningBrace('')->setSpaceAfterSelectorSeparator('');
return $format;
}
/**
* Create pretty format.
*
* @return OutputFormat Format.
*/
public static function createPretty() {
$format = self::create();
$format->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setSpaceBetweenBlocks("\n\n")->set('SpaceAfterListArgumentSeparator', array('default' => '', ',' => ' '));
return $format;
}
}
class OutputFormatter {
private $oFormat;
public function __construct(OutputFormat $oFormat) {
$this->oFormat = $oFormat;
}
public function space($sName, $sType = null) {
$sSpaceString = $this->oFormat->get("Space$sName");
// If $sSpaceString is an array, we have multple values configured depending on the type of object the space applies to
if(is_array($sSpaceString)) {
if($sType !== null && isset($sSpaceString[$sType])) {
$sSpaceString = $sSpaceString[$sType];
} else {
$sSpaceString = reset($sSpaceString);
}
}
return $this->prepareSpace($sSpaceString);
}
public function spaceAfterRuleName() {
return $this->space('AfterRuleName');
}
public function spaceBeforeRules() {
return $this->space('BeforeRules');
}
public function spaceAfterRules() {
return $this->space('AfterRules');
}
public function spaceBetweenRules() {
return $this->space('BetweenRules');
}
public function spaceBeforeBlocks() {
return $this->space('BeforeBlocks');
}
public function spaceAfterBlocks() {
return $this->space('AfterBlocks');
}
public function spaceBetweenBlocks() {
return $this->space('BetweenBlocks');
}
public function spaceBeforeSelectorSeparator() {
return $this->space('BeforeSelectorSeparator');
}
public function spaceAfterSelectorSeparator() {
return $this->space('AfterSelectorSeparator');
}
public function spaceBeforeListArgumentSeparator($sSeparator) {
return $this->space('BeforeListArgumentSeparator', $sSeparator);
}
public function spaceAfterListArgumentSeparator($sSeparator) {
return $this->space('AfterListArgumentSeparator', $sSeparator);
}
public function spaceBeforeOpeningBrace() {
return $this->space('BeforeOpeningBrace');
}
/**
* Runs the given code, either swallowing or passing exceptions, depending on the bIgnoreExceptions setting.
*/
public function safely($cCode) {
if($this->oFormat->get('IgnoreExceptions')) {
// If output exceptions are ignored, run the code with exception guards
try {
return $cCode();
} catch (OutputException $e) {
return null;
} //Do nothing
} else {
// Run the code as-is
return $cCode();
}
}
/**
* Clone of the implode function but calls ->render with the current output format instead of __toString()
*/
public function implode($sSeparator, $aValues, $bIncreaseLevel = false) {
$sResult = '';
$oFormat = $this->oFormat;
if($bIncreaseLevel) {
$oFormat = $oFormat->nextLevel();
}
$bIsFirst = true;
foreach($aValues as $mValue) {
if($bIsFirst) {
$bIsFirst = false;
} else {
$sResult .= $sSeparator;
}
if($mValue instanceof \Sabberworm\CSS\Renderable) {
$sResult .= $mValue->render($oFormat);
} else {
$sResult .= $mValue;
}
}
return $sResult;
}
public function removeLastSemicolon($sString) {
if($this->oFormat->get('SemicolonAfterLastRule')) {
return $sString;
}
$sString = explode(';', $sString);
if(count($sString) < 2) {
return $sString[0];
}
$sLast = array_pop($sString);
$sNextToLast = array_pop($sString);
array_push($sString, $sNextToLast.$sLast);
return implode(';', $sString);
}
private function prepareSpace($sSpaceString) {
return str_replace("\n", "\n".$this->indent(), $sSpaceString);
}
private function indent() {
return str_repeat($this->oFormat->sIndentation, $this->oFormat->level());
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\Parsing\ParserState;
/**
* Parser class parses CSS from text into a data structure.
*/
class Parser {
private $oParserState;
/**
* Parser constructor.
* Note that that iLineNo starts from 1 and not 0
*
* @param $sText
* @param Settings|null $oParserSettings
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
$this->oParserState = new ParserState($sText, $oParserSettings, $iLineNo);
}
public function setCharset($sCharset) {
$this->oParserState->setCharset($sCharset);
}
public function getCharset() {
$this->oParserState->getCharset();
}
public function parse() {
return Document::parse($this->oParserState);
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers attempts to print something invalid
*/
class OutputException extends SourceException {
public function __construct($sMessage, $iLineNo = 0) {
parent::__construct($sMessage, $iLineNo);
}
}

View File

@@ -0,0 +1,310 @@
<?php
namespace Sabberworm\CSS\Parsing;
use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Settings;
class ParserState {
private $oParserSettings;
private $sText;
private $aText;
private $iCurrentPosition;
private $sCharset;
private $iLength;
private $iLineNo;
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) {
$this->oParserSettings = $oParserSettings;
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
$this->setCharset($this->oParserSettings->sDefaultCharset);
}
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
$this->aText = $this->strsplit($this->sText);
$this->iLength = count($this->aText);
}
public function getCharset() {
$this->oParserHelper->getCharset();
return $this->sCharset;
}
public function currentLine() {
return $this->iLineNo;
}
public function getSettings() {
return $this->oParserSettings;
}
public function parseIdentifier($bIgnoreCase = true) {
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
$sResult .= $sCharacter;
}
if ($bIgnoreCase) {
$sResult = $this->strtolower($sResult);
}
return $sResult;
}
public function parseCharacter($bIsForIdentifier) {
if ($this->peek() === '\\') {
if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
// Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
return null;
}
$this->consume('\\');
if ($this->comes('\n') || $this->comes('\r')) {
return '';
}
if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
return $this->consume(1);
}
$sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
if ($this->strlen($sUnicode) < 6) {
//Consume whitespace after incomplete unicode escape
if (preg_match('/\\s/isSu', $this->peek())) {
if ($this->comes('\r\n')) {
$this->consume(2);
} else {
$this->consume(1);
}
}
}
$iUnicode = intval($sUnicode, 16);
$sUtf32 = "";
for ($i = 0; $i < 4; ++$i) {
$sUtf32 .= chr($iUnicode & 0xff);
$iUnicode = $iUnicode >> 8;
}
return iconv('utf-32le', $this->sCharset, $sUtf32);
}
if ($bIsForIdentifier) {
$peek = ord($this->peek());
// Ranges: a-z A-Z 0-9 - _
if (($peek >= 97 && $peek <= 122) ||
($peek >= 65 && $peek <= 90) ||
($peek >= 48 && $peek <= 57) ||
($peek === 45) ||
($peek === 95) ||
($peek > 0xa1)) {
return $this->consume(1);
}
} else {
return $this->consume(1);
}
return null;
}
public function consumeWhiteSpace() {
$comments = array();
do {
while (preg_match('/\\s/isSu', $this->peek()) === 1) {
$this->consume(1);
}
if($this->oParserSettings->bLenientParsing) {
try {
$oComment = $this->consumeComment();
} catch(UnexpectedTokenException $e) {
// When we cant find the end of a comment, we assume the document is finished.
$this->iCurrentPosition = $this->iLength;
return;
}
} else {
$oComment = $this->consumeComment();
}
if ($oComment !== false) {
$comments[] = $oComment;
}
} while($oComment !== false);
return $comments;
}
public function comes($sString, $bCaseInsensitive = false) {
$sPeek = $this->peek(strlen($sString));
return ($sPeek == '')
? false
: $this->streql($sPeek, $sString, $bCaseInsensitive);
}
public function peek($iLength = 1, $iOffset = 0) {
$iOffset += $this->iCurrentPosition;
if ($iOffset >= $this->iLength) {
return '';
}
return $this->substr($iOffset, $iLength);
}
public function consume($mValue = 1) {
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
}
public function consumeExpression($mExpression, $iMaxLength = null) {
$aMatches = null;
$sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}
/**
* @return false|Comment
*/
public function consumeComment() {
$mComment = false;
if ($this->comes('/*')) {
$iLineNo = $this->iLineNo;
$this->consume(1);
$mComment = '';
while (($char = $this->consume(1)) !== '') {
$mComment .= $char;
if ($this->comes('*/')) {
$this->consume(2);
break;
}
}
}
if ($mComment !== false) {
// We skip the * which was included in the comment.
return new Comment(substr($mComment, 1), $iLineNo);
}
return $mComment;
}
public function isEnd() {
return $this->iCurrentPosition >= $this->iLength;
}
public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
$aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
$out = '';
$start = $this->iCurrentPosition;
while (($char = $this->consume(1)) !== '') {
if (in_array($char, $aEnd)) {
if ($bIncludeEnd) {
$out .= $char;
} elseif (!$consumeEnd) {
$this->iCurrentPosition -= $this->strlen($char);
}
return $out;
}
$out .= $char;
if ($comment = $this->consumeComment()) {
$comments[] = $comment;
}
}
$this->iCurrentPosition = $start;
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
}
private function inputLeft() {
return $this->substr($this->iCurrentPosition, -1);
}
public function streql($sString1, $sString2, $bCaseInsensitive = true) {
if($bCaseInsensitive) {
return $this->strtolower($sString1) === $this->strtolower($sString2);
} else {
return $sString1 === $sString2;
}
}
public function backtrack($iAmount) {
$this->iCurrentPosition -= $iAmount;
}
public function strlen($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strlen($sString, $this->sCharset);
} else {
return strlen($sString);
}
}
private function substr($iStart, $iLength) {
if ($iLength < 0) {
$iLength = $this->iLength - $iStart + $iLength;
}
if ($iStart + $iLength > $this->iLength) {
$iLength = $this->iLength - $iStart;
}
$sResult = '';
while ($iLength > 0) {
$sResult .= $this->aText[$iStart];
$iStart++;
$iLength--;
}
return $sResult;
}
private function strtolower($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strtolower($sString, $this->sCharset);
} else {
return strtolower($sString);
}
}
private function strsplit($sString) {
if ($this->oParserSettings->bMultibyteSupport) {
if ($this->streql($this->sCharset, 'utf-8')) {
return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
} else {
$iLength = mb_strlen($sString, $this->sCharset);
$aResult = array();
for ($i = 0; $i < $iLength; ++$i) {
$aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
}
return $aResult;
}
} else {
if($sString === '') {
return array();
} else {
return str_split($sString);
}
}
}
private function strpos($sString, $sNeedle, $iOffset) {
if ($this->oParserSettings->bMultibyteSupport) {
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
} else {
return strpos($sString, $sNeedle, $iOffset);
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Sabberworm\CSS\Parsing;
class SourceException extends \Exception {
private $iLineNo;
public function __construct($sMessage, $iLineNo = 0) {
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}
public function getLineNo() {
return $this->iLineNo;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Sabberworm\CSS\Parsing;
/**
* Thrown if the CSS parsers encounters a token it did not expect
*/
class UnexpectedTokenException extends SourceException {
private $sExpected;
private $sFound;
// Possible values: literal, identifier, count, expression, search
private $sMatchType;
public function __construct($sExpected, $sFound, $sMatchType = 'literal', $iLineNo = 0) {
$this->sExpected = $sExpected;
$this->sFound = $sFound;
$this->sMatchType = $sMatchType;
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
if($this->sMatchType === 'search') {
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
} else if($this->sMatchType === 'count') {
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
} else if($this->sMatchType === 'identifier') {
$sMessage = "Identifier expected. Got “{$sFound}";
} else if($this->sMatchType === 'custom') {
$sMessage = trim("$sExpected $sFound");
}
parent::__construct($sMessage, $iLineNo);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Comment\Commentable;
interface AtRule extends Renderable, Commentable {
// Since there are more set rules than block rules, were whitelisting the block rules and have anything else be treated as a set rule.
const BLOCK_RULES = 'media/document/supports/region-style/font-feature-values';
// …and more font-specific ones (to be used inside font-feature-values)
const SET_RULES = 'font-face/counter-style/page/swash/styleset/annotation';
public function atRuleName();
public function atRuleArgs();
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* CSSNamespace represents an @namespace rule.
*/
class CSSNamespace implements AtRule {
private $mUrl;
private $sPrefix;
private $iLineNo;
protected $aComments;
public function __construct($mUrl, $sPrefix = null, $iLineNo = 0) {
$this->mUrl = $mUrl;
$this->sPrefix = $sPrefix;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '@namespace '.($this->sPrefix === null ? '' : $this->sPrefix.' ').$this->mUrl->render($oOutputFormat).';';
}
public function getUrl() {
return $this->mUrl;
}
public function getPrefix() {
return $this->sPrefix;
}
public function setUrl($mUrl) {
$this->mUrl = $mUrl;
}
public function setPrefix($sPrefix) {
$this->sPrefix = $sPrefix;
}
public function atRuleName() {
return 'namespace';
}
public function atRuleArgs() {
$aResult = array($this->mUrl);
if($this->sPrefix) {
array_unshift($aResult, $this->sPrefix);
}
return $aResult;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* Class representing an @charset rule.
* The following restrictions apply:
* • May not be found in any CSSList other than the Document.
* • May only appear at the very top of a Documents contents.
* • Must not appear more than once.
*/
class Charset implements AtRule {
private $sCharset;
protected $iLineNo;
protected $aComment;
public function __construct($sCharset, $iLineNo = 0) {
$this->sCharset = $sCharset;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setCharset($sCharset) {
$this->sCharset = $sCharset;
}
public function getCharset() {
return $this->sCharset;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@charset {$this->sCharset->render($oOutputFormat)};";
}
public function atRuleName() {
return 'charset';
}
public function atRuleArgs() {
return $this->sCharset;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Sabberworm\CSS\Property;
use Sabberworm\CSS\Value\URL;
/**
* Class representing an @import rule.
*/
class Import implements AtRule {
private $oLocation;
private $sMediaQuery;
protected $iLineNo;
protected $aComments;
public function __construct(URL $oLocation, $sMediaQuery, $iLineNo = 0) {
$this->oLocation = $oLocation;
$this->sMediaQuery = $sMediaQuery;
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setLocation($oLocation) {
$this->oLocation = $oLocation;
}
public function getLocation() {
return $this->oLocation;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "@import ".$this->oLocation->render($oOutputFormat).($this->sMediaQuery === null ? '' : ' '.$this->sMediaQuery).';';
}
public function atRuleName() {
return 'import';
}
public function atRuleArgs() {
$aResult = array($this->oLocation);
if($this->sMediaQuery) {
array_push($aResult, $this->sMediaQuery);
}
return $aResult;
}
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
public function getComments() {
return $this->aComments;
}
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Sabberworm\CSS\Property;
/**
* Class representing a single CSS selector. Selectors have to be split by the comma prior to being passed into this class.
*/
class Selector {
//Regexes for specificity calculations
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';
const ELEMENTS_AND_PSEUDO_ELEMENTS_RX = '/
((^|[\s\+\>\~]+)[\w]+ # elements
|
\:{1,2}( # pseudo-elements
after|before|first-letter|first-line|selection
))
/ix';
private $sSelector;
private $iSpecificity;
public function __construct($sSelector, $bCalculateSpecificity = false) {
$this->setSelector($sSelector);
if ($bCalculateSpecificity) {
$this->getSpecificity();
}
}
public function getSelector() {
return $this->sSelector;
}
public function setSelector($sSelector) {
$this->sSelector = trim($sSelector);
$this->iSpecificity = null;
}
public function __toString() {
return $this->getSelector();
}
public function getSpecificity() {
if ($this->iSpecificity === null) {
$a = 0;
/// @todo should exclude \# as well as "#"
$aMatches = null;
$b = substr_count($this->sSelector, '#');
$c = preg_match_all(self::NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX, $this->sSelector, $aMatches);
$d = preg_match_all(self::ELEMENTS_AND_PSEUDO_ELEMENTS_RX, $this->sSelector, $aMatches);
$this->iSpecificity = ($a * 1000) + ($b * 100) + ($c * 10) + $d;
}
return $this->iSpecificity;
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Sabberworm\CSS;
interface Renderable {
public function __toString();
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
public function getLineNo();
}

View File

@@ -0,0 +1,236 @@
<?php
namespace Sabberworm\CSS\Rule;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
/**
* RuleSets contains Rule objects which always have a key and a value.
* In CSS, Rules are expressed as follows: “key: value[0][0] value[0][1], value[1][0] value[1][1];”
*/
class Rule implements Renderable, Commentable {
private $sRule;
private $mValue;
private $bIsImportant;
private $aIeHack;
protected $iLineNo;
protected $aComments;
public function __construct($sRule, $iLineNo = 0) {
$this->sRule = $sRule;
$this->mValue = null;
$this->bIsImportant = false;
$this->aIeHack = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
public static function parse(ParserState $oParserState) {
$aComments = $oParserState->consumeWhiteSpace();
$oRule = new Rule($oParserState->parseIdentifier(), $oParserState->currentLine());
$oRule->setComments($aComments);
$oRule->addComments($oParserState->consumeWhiteSpace());
$oParserState->consume(':');
$oValue = Value::parseValue($oParserState, self::listDelimiterForRule($oRule->getRule()));
$oRule->setValue($oValue);
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('\\')) {
$oParserState->consume('\\');
$oRule->addIeHack($oParserState->consume());
$oParserState->consumeWhiteSpace();
}
}
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('!')) {
$oParserState->consume('!');
$oParserState->consumeWhiteSpace();
$oParserState->consume('important');
$oRule->setIsImportant(true);
}
$oParserState->consumeWhiteSpace();
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
$oParserState->consumeWhiteSpace();
return $oRule;
}
private static function listDelimiterForRule($sRule) {
if (preg_match('/^font($|-)/', $sRule)) {
return array(',', '/', ' ');
}
return array(',', ' ', '/');
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function setRule($sRule) {
$this->sRule = $sRule;
}
public function getRule() {
return $this->sRule;
}
public function getValue() {
return $this->mValue;
}
public function setValue($mValue) {
$this->mValue = $mValue;
}
/**
* @deprecated Old-Style 2-dimensional array given. Retained for (some) backwards-compatibility. Use setValue() instead and wrapp the value inside a RuleValueList if necessary.
*/
public function setValues($aSpaceSeparatedValues) {
$oSpaceSeparatedList = null;
if (count($aSpaceSeparatedValues) > 1) {
$oSpaceSeparatedList = new RuleValueList(' ', $this->iLineNo);
}
foreach ($aSpaceSeparatedValues as $aCommaSeparatedValues) {
$oCommaSeparatedList = null;
if (count($aCommaSeparatedValues) > 1) {
$oCommaSeparatedList = new RuleValueList(',', $this->iLineNo);
}
foreach ($aCommaSeparatedValues as $mValue) {
if (!$oSpaceSeparatedList && !$oCommaSeparatedList) {
$this->mValue = $mValue;
return $mValue;
}
if ($oCommaSeparatedList) {
$oCommaSeparatedList->addListComponent($mValue);
} else {
$oSpaceSeparatedList->addListComponent($mValue);
}
}
if (!$oSpaceSeparatedList) {
$this->mValue = $oCommaSeparatedList;
return $oCommaSeparatedList;
} else {
$oSpaceSeparatedList->addListComponent($oCommaSeparatedList);
}
}
$this->mValue = $oSpaceSeparatedList;
return $oSpaceSeparatedList;
}
/**
* @deprecated Old-Style 2-dimensional array returned. Retained for (some) backwards-compatibility. Use getValue() instead and check for the existance of a (nested set of) ValueList object(s).
*/
public function getValues() {
if (!$this->mValue instanceof RuleValueList) {
return array(array($this->mValue));
}
if ($this->mValue->getListSeparator() === ',') {
return array($this->mValue->getListComponents());
}
$aResult = array();
foreach ($this->mValue->getListComponents() as $mValue) {
if (!$mValue instanceof RuleValueList || $mValue->getListSeparator() !== ',') {
$aResult[] = array($mValue);
continue;
}
if ($this->mValue->getListSeparator() === ' ' || count($aResult) === 0) {
$aResult[] = array();
}
foreach ($mValue->getListComponents() as $mValue) {
$aResult[count($aResult) - 1][] = $mValue;
}
}
return $aResult;
}
/**
* Adds a value to the existing value. Value will be appended if a RuleValueList exists of the given type. Otherwise, the existing value will be wrapped by one.
*/
public function addValue($mValue, $sType = ' ') {
if (!is_array($mValue)) {
$mValue = array($mValue);
}
if (!$this->mValue instanceof RuleValueList || $this->mValue->getListSeparator() !== $sType) {
$mCurrentValue = $this->mValue;
$this->mValue = new RuleValueList($sType, $this->iLineNo);
if ($mCurrentValue) {
$this->mValue->addListComponent($mCurrentValue);
}
}
foreach ($mValue as $mValueItem) {
$this->mValue->addListComponent($mValueItem);
}
}
public function addIeHack($iModifier) {
$this->aIeHack[] = $iModifier;
}
public function setIeHack(array $aModifiers) {
$this->aIeHack = $aModifiers;
}
public function getIeHack() {
return $this->aIeHack;
}
public function setIsImportant($bIsImportant) {
$this->bIsImportant = $bIsImportant;
}
public function getIsImportant() {
return $this->bIsImportant;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = "{$this->sRule}:{$oOutputFormat->spaceAfterRuleName()}";
if ($this->mValue instanceof Value) { //Can also be a ValueList
$sResult .= $this->mValue->render($oOutputFormat);
} else {
$sResult .= $this->mValue;
}
if (!empty($this->aIeHack)) {
$sResult .= ' \\' . implode('\\', $this->aIeHack);
}
if ($this->bIsImportant) {
$sResult .= ' !important';
}
$sResult .= ';';
return $sResult;
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Property\AtRule;
/**
* A RuleSet constructed by an unknown @-rule. @font-face rules are rendered into AtRuleSet objects.
*/
class AtRuleSet extends RuleSet implements AtRule {
private $sType;
private $sArgs;
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
public function atRuleName() {
return $this->sType;
}
public function atRuleArgs() {
return $this->sArgs;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sArgs = $this->sArgs;
if($sArgs) {
$sArgs = ' ' . $sArgs;
}
$sResult = "@{$this->sType}$sArgs{$oOutputFormat->spaceBeforeOpeningBrace()}{";
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
return $sResult;
}
}

View File

@@ -0,0 +1,628 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\OutputException;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\RuleValueList;
use Sabberworm\CSS\Value\Value;
use Sabberworm\CSS\Value\Size;
use Sabberworm\CSS\Value\Color;
use Sabberworm\CSS\Value\URL;
/**
* Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
* Declaration blocks usually appear directly inside a Document or another CSSList (mostly a MediaQuery).
*/
class DeclarationBlock extends RuleSet {
private $aSelectors;
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->aSelectors = array();
}
public static function parse(ParserState $oParserState) {
$aComments = array();
$oResult = new DeclarationBlock($oParserState->currentLine());
$oResult->setSelector($oParserState->consumeUntil('{', false, true, $aComments));
$oResult->setComments($aComments);
RuleSet::parseRuleSet($oParserState, $oResult);
return $oResult;
}
public function setSelectors($mSelector) {
if (is_array($mSelector)) {
$this->aSelectors = $mSelector;
} else {
$this->aSelectors = explode(',', $mSelector);
}
foreach ($this->aSelectors as $iKey => $mSelector) {
if (!($mSelector instanceof Selector)) {
$this->aSelectors[$iKey] = new Selector($mSelector);
}
}
}
// remove one of the selector of the block
public function removeSelector($mSelector) {
if($mSelector instanceof Selector) {
$mSelector = $mSelector->getSelector();
}
foreach($this->aSelectors as $iKey => $oSelector) {
if($oSelector->getSelector() === $mSelector) {
unset($this->aSelectors[$iKey]);
return true;
}
}
return false;
}
/**
* @deprecated use getSelectors()
*/
public function getSelector() {
return $this->getSelectors();
}
/**
* @deprecated use setSelectors()
*/
public function setSelector($mSelector) {
$this->setSelectors($mSelector);
}
/**
* Get selectors.
*
* @return Selector[] Selectors.
*/
public function getSelectors() {
return $this->aSelectors;
}
/**
* Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
* */
public function expandShorthands() {
// border must be expanded before dimensions
$this->expandBorderShorthand();
$this->expandDimensionsShorthand();
$this->expandFontShorthand();
$this->expandBackgroundShorthand();
$this->expandListStyleShorthand();
}
/**
* Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
* */
public function createShorthands() {
$this->createBackgroundShorthand();
$this->createDimensionsShorthand();
// border must be shortened after dimensions
$this->createBorderShorthand();
$this->createFontShorthand();
$this->createListStyleShorthand();
}
/**
* Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
* Additional splitting happens in expandDimensionsShorthand
* Multiple borders are not yet supported as of 3
* */
public function expandBorderShorthand() {
$aBorderRules = array(
'border', 'border-left', 'border-right', 'border-top', 'border-bottom'
);
$aBorderSizes = array(
'thin', 'medium', 'thick'
);
$aRules = $this->getRulesAssoc();
foreach ($aBorderRules as $sBorderRule) {
if (!isset($aRules[$sBorderRule]))
continue;
$oRule = $aRules[$sBorderRule];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if ($mValue instanceof Value) {
$mNewValue = clone $mValue;
} else {
$mNewValue = $mValue;
}
if ($mValue instanceof Size) {
$sNewRuleName = $sBorderRule . "-width";
} else if ($mValue instanceof Color) {
$sNewRuleName = $sBorderRule . "-color";
} else {
if (in_array($mValue, $aBorderSizes)) {
$sNewRuleName = $sBorderRule . "-width";
} else/* if(in_array($mValue, $aBorderStyles)) */ {
$sNewRuleName = $sBorderRule . "-style";
}
}
$oNewRule = new Rule($sNewRuleName, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue(array($mNewValue));
$this->addRule($oNewRule);
}
$this->removeRule($sBorderRule);
}
}
/**
* Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
* into their constituent parts.
* Handles margin, padding, border-color, border-style and border-width.
* */
public function expandDimensionsShorthand() {
$aExpansions = array(
'margin' => 'margin-%s',
'padding' => 'padding-%s',
'border-color' => 'border-%s-color',
'border-style' => 'border-%s-style',
'border-width' => 'border-%s-width'
);
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
if (!isset($aRules[$sProperty]))
continue;
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
$top = $right = $bottom = $left = null;
switch (count($aValues)) {
case 1:
$top = $right = $bottom = $left = $aValues[0];
break;
case 2:
$top = $bottom = $aValues[0];
$left = $right = $aValues[1];
break;
case 3:
$top = $aValues[0];
$left = $right = $aValues[1];
$bottom = $aValues[2];
break;
case 4:
$top = $aValues[0];
$right = $aValues[1];
$bottom = $aValues[2];
$left = $aValues[3];
break;
}
foreach (array('top', 'right', 'bottom', 'left') as $sPosition) {
$oNewRule = new Rule(sprintf($sExpanded, $sPosition), $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue(${$sPosition});
$this->addRule($oNewRule);
}
$this->removeRule($sProperty);
}
}
/**
* Convert shorthand font declarations
* (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
* into their constituent parts.
* */
public function expandFontShorthand() {
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font']))
return;
$oRule = $aRules['font'];
// reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
$aFontProperties = array(
'font-style' => 'normal',
'font-variant' => 'normal',
'font-weight' => 'normal',
'font-size' => 'normal',
'line-height' => 'normal'
);
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if (in_array($mValue, array('normal', 'inherit'))) {
foreach (array('font-style', 'font-weight', 'font-variant') as $sProperty) {
if (!isset($aFontProperties[$sProperty])) {
$aFontProperties[$sProperty] = $mValue;
}
}
} else if (in_array($mValue, array('italic', 'oblique'))) {
$aFontProperties['font-style'] = $mValue;
} else if ($mValue == 'small-caps') {
$aFontProperties['font-variant'] = $mValue;
} else if (
in_array($mValue, array('bold', 'bolder', 'lighter'))
|| ($mValue instanceof Size
&& in_array($mValue->getSize(), range(100, 900, 100)))
) {
$aFontProperties['font-weight'] = $mValue;
} else if ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
list($oSize, $oHeight) = $mValue->getListComponents();
$aFontProperties['font-size'] = $oSize;
$aFontProperties['line-height'] = $oHeight;
} else if ($mValue instanceof Size && $mValue->getUnit() !== null) {
$aFontProperties['font-size'] = $mValue;
} else {
$aFontProperties['font-family'] = $mValue;
}
}
foreach ($aFontProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue($mValue);
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('font');
}
/*
* Convert shorthand background declarations
* (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
* into their constituent parts.
* @see http://www.w3.org/TR/21/colors.html#propdef-background
* */
public function expandBackgroundShorthand() {
$aRules = $this->getRulesAssoc();
if (!isset($aRules['background']))
return;
$oRule = $aRules['background'];
$aBgProperties = array(
'background-color' => array('transparent'), 'background-image' => array('none'),
'background-repeat' => array('repeat'), 'background-attachment' => array('scroll'),
'background-position' => array(new Size(0, '%', null, false, $this->iLineNo), new Size(0, '%', null, false, $this->iLineNo))
);
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('background');
return;
}
$iNumBgPos = 0;
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if ($mValue instanceof URL) {
$aBgProperties['background-image'] = $mValue;
} else if ($mValue instanceof Color) {
$aBgProperties['background-color'] = $mValue;
} else if (in_array($mValue, array('scroll', 'fixed'))) {
$aBgProperties['background-attachment'] = $mValue;
} else if (in_array($mValue, array('repeat', 'no-repeat', 'repeat-x', 'repeat-y'))) {
$aBgProperties['background-repeat'] = $mValue;
} else if (in_array($mValue, array('left', 'center', 'right', 'top', 'bottom'))
|| $mValue instanceof Size
) {
if ($iNumBgPos == 0) {
$aBgProperties['background-position'][0] = $mValue;
$aBgProperties['background-position'][1] = 'center';
} else {
$aBgProperties['background-position'][$iNumBgPos] = $mValue;
}
$iNumBgPos++;
}
}
foreach ($aBgProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('background');
}
public function expandListStyleShorthand() {
$aListProperties = array(
'list-style-type' => 'disc',
'list-style-position' => 'outside',
'list-style-image' => 'none'
);
$aListStyleTypes = array(
'none', 'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal',
'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin',
'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic',
'hiragana', 'hira-gana-iroha', 'katakana-iroha', 'katakana'
);
$aListStylePositions = array(
'inside', 'outside'
);
$aRules = $this->getRulesAssoc();
if (!isset($aRules['list-style']))
return;
$oRule = $aRules['list-style'];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if (count($aValues) == 1 && $aValues[0] == 'inherit') {
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->addValue('inherit');
$oNewRule->setIsImportant($oRule->getIsImportant());
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
return;
}
foreach ($aValues as $mValue) {
if (!$mValue instanceof Value) {
$mValue = mb_strtolower($mValue);
}
if ($mValue instanceof Url) {
$aListProperties['list-style-image'] = $mValue;
} else if (in_array($mValue, $aListStyleTypes)) {
$aListProperties['list-style-types'] = $mValue;
} else if (in_array($mValue, $aListStylePositions)) {
$aListProperties['list-style-position'] = $mValue;
}
}
foreach ($aListProperties as $sProperty => $mValue) {
$oNewRule = new Rule($sProperty, $this->iLineNo);
$oNewRule->setIsImportant($oRule->getIsImportant());
$oNewRule->addValue($mValue);
$this->addRule($oNewRule);
}
$this->removeRule('list-style');
}
public function createShorthandProperties(array $aProperties, $sShorthand) {
$aRules = $this->getRulesAssoc();
$aNewValues = array();
foreach ($aProperties as $sProperty) {
if (!isset($aRules[$sProperty]))
continue;
$oRule = $aRules[$sProperty];
if (!$oRule->getIsImportant()) {
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
foreach ($aValues as $mValue) {
$aNewValues[] = $mValue;
}
$this->removeRule($sProperty);
}
}
if (count($aNewValues)) {
$oNewRule = new Rule($sShorthand, $this->iLineNo);
foreach ($aNewValues as $mValue) {
$oNewRule->addValue($mValue);
}
$this->addRule($oNewRule);
}
}
public function createBackgroundShorthand() {
$aProperties = array(
'background-color', 'background-image', 'background-repeat',
'background-position', 'background-attachment'
);
$this->createShorthandProperties($aProperties, 'background');
}
public function createListStyleShorthand() {
$aProperties = array(
'list-style-type', 'list-style-position', 'list-style-image'
);
$this->createShorthandProperties($aProperties, 'list-style');
}
/**
* Combine border-color, border-style and border-width into border
* Should be run after create_dimensions_shorthand!
* */
public function createBorderShorthand() {
$aProperties = array(
'border-width', 'border-style', 'border-color'
);
$this->createShorthandProperties($aProperties, 'border');
}
/*
* Looks for long format CSS dimensional properties
* (margin, padding, border-color, border-style and border-width)
* and converts them into shorthand CSS properties.
* */
public function createDimensionsShorthand() {
$aPositions = array('top', 'right', 'bottom', 'left');
$aExpansions = array(
'margin' => 'margin-%s',
'padding' => 'padding-%s',
'border-color' => 'border-%s-color',
'border-style' => 'border-%s-style',
'border-width' => 'border-%s-width'
);
$aRules = $this->getRulesAssoc();
foreach ($aExpansions as $sProperty => $sExpanded) {
$aFoldable = array();
foreach ($aRules as $sRuleName => $oRule) {
foreach ($aPositions as $sPosition) {
if ($sRuleName == sprintf($sExpanded, $sPosition)) {
$aFoldable[$sRuleName] = $oRule;
}
}
}
// All four dimensions must be present
if (count($aFoldable) == 4) {
$aValues = array();
foreach ($aPositions as $sPosition) {
$oRule = $aRules[sprintf($sExpanded, $sPosition)];
$mRuleValue = $oRule->getValue();
$aRuleValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aRuleValues[] = $mRuleValue;
} else {
$aRuleValues = $mRuleValue->getListComponents();
}
$aValues[$sPosition] = $aRuleValues;
}
$oNewRule = new Rule($sProperty, $this->iLineNo);
if ((string) $aValues['left'][0] == (string) $aValues['right'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['bottom'][0]) {
if ((string) $aValues['top'][0] == (string) $aValues['left'][0]) {
// All 4 sides are equal
$oNewRule->addValue($aValues['top']);
} else {
// Top and bottom are equal, left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
}
} else {
// Only left and right are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
}
} else {
// No sides are equal
$oNewRule->addValue($aValues['top']);
$oNewRule->addValue($aValues['left']);
$oNewRule->addValue($aValues['bottom']);
$oNewRule->addValue($aValues['right']);
}
$this->addRule($oNewRule);
foreach ($aPositions as $sPosition) {
$this->removeRule(sprintf($sExpanded, $sPosition));
}
}
}
}
/**
* Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
* tries to convert them into a shorthand CSS <tt>font</tt> property.
* At least font-size AND font-family must be present in order to create a shorthand declaration.
* */
public function createFontShorthand() {
$aFontProperties = array(
'font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'
);
$aRules = $this->getRulesAssoc();
if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
return;
}
$oNewRule = new Rule('font', $this->iLineNo);
foreach (array('font-style', 'font-variant', 'font-weight') as $sProperty) {
if (isset($aRules[$sProperty])) {
$oRule = $aRules[$sProperty];
$mRuleValue = $oRule->getValue();
$aValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aValues[] = $mRuleValue;
} else {
$aValues = $mRuleValue->getListComponents();
}
if ($aValues[0] !== 'normal') {
$oNewRule->addValue($aValues[0]);
}
}
}
// Get the font-size value
$oRule = $aRules['font-size'];
$mRuleValue = $oRule->getValue();
$aFSValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aFSValues[] = $mRuleValue;
} else {
$aFSValues = $mRuleValue->getListComponents();
}
// But wait to know if we have line-height to add it
if (isset($aRules['line-height'])) {
$oRule = $aRules['line-height'];
$mRuleValue = $oRule->getValue();
$aLHValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aLHValues[] = $mRuleValue;
} else {
$aLHValues = $mRuleValue->getListComponents();
}
if ($aLHValues[0] !== 'normal') {
$val = new RuleValueList('/', $this->iLineNo);
$val->addListComponent($aFSValues[0]);
$val->addListComponent($aLHValues[0]);
$oNewRule->addValue($val);
}
} else {
$oNewRule->addValue($aFSValues[0]);
}
$oRule = $aRules['font-family'];
$mRuleValue = $oRule->getValue();
$aFFValues = array();
if (!$mRuleValue instanceof RuleValueList) {
$aFFValues[] = $mRuleValue;
} else {
$aFFValues = $mRuleValue->getListComponents();
}
$oFFValue = new RuleValueList(',', $this->iLineNo);
$oFFValue->setListComponents($aFFValues);
$oNewRule->addValue($oFFValue);
$this->addRule($oNewRule);
foreach ($aFontProperties as $sProperty) {
$this->removeRule($sProperty);
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
if(count($this->aSelectors) === 0) {
// If all the selectors have been removed, this declaration block becomes invalid
throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
}
$sResult = $oOutputFormat->sBeforeDeclarationBlock;
$sResult .= $oOutputFormat->implode($oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(), $this->aSelectors);
$sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
$sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
$sResult .= parent::render($oOutputFormat);
$sResult .= '}';
$sResult .= $oOutputFormat->sAfterDeclarationBlock;
return $sResult;
}
}

View File

@@ -0,0 +1,212 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\Rule\Rule;
/**
* RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
* However, unknown At-Rules (like @font-face) are also rule sets.
*/
abstract class RuleSet implements Renderable, Commentable {
private $aRules;
protected $iLineNo;
protected $aComments;
public function __construct($iLineNo = 0) {
$this->aRules = array();
$this->iLineNo = $iLineNo;
$this->aComments = array();
}
public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet) {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
while (!$oParserState->comes('}')) {
$oRule = null;
if($oParserState->getSettings()->bLenientParsing) {
try {
$oRule = Rule::parse($oParserState);
} catch (UnexpectedTokenException $e) {
try {
$sConsume = $oParserState->consumeUntil(array("\n", ";", '}'), true);
// We need to “unfind” the matches to the end of the ruleSet as this will be matched later
if($oParserState->streql(substr($sConsume, -1), '}')) {
$oParserState->backtrack(1);
} else {
while ($oParserState->comes(';')) {
$oParserState->consume(';');
}
}
} catch (UnexpectedTokenException $e) {
// Weve reached the end of the document. Just close the RuleSet.
return;
}
}
} else {
$oRule = Rule::parse($oParserState);
}
if($oRule) {
$oRuleSet->addRule($oRule);
}
}
$oParserState->consume('}');
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
public function addRule(Rule $oRule, Rule $oSibling = null) {
$sRule = $oRule->getRule();
if(!isset($this->aRules[$sRule])) {
$this->aRules[$sRule] = array();
}
$iPosition = count($this->aRules[$sRule]);
if ($oSibling !== null) {
$iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
if ($iSiblingPos !== false) {
$iPosition = $iSiblingPos;
}
}
array_splice($this->aRules[$sRule], $iPosition, 0, array($oRule));
}
/**
* Returns all rules matching the given rule name
* @param (null|string|Rule) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* @example $oRuleSet->getRules('font-') //returns an array of all rules either beginning with font- or matching font.
* @example $oRuleSet->getRules('font') //returns array(0 => $oRule, …) or array().
* @return Rule[] Rules.
*/
public function getRules($mRule = null) {
if ($mRule instanceof Rule) {
$mRule = $mRule->getRule();
}
$aResult = array();
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule.
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
$aResult = array_merge($aResult, $aRules);
}
}
return $aResult;
}
/**
* Override all the rules of this set.
* @param Rule[] $aRules The rules to override with.
*/
public function setRules(array $aRules) {
$this->aRules = array();
foreach ($aRules as $rule) {
$this->addRule($rule);
}
}
/**
* Returns all rules matching the given pattern and returns them in an associative array with the rules name as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
* @param (string) $mRule pattern to search for. If null, returns all rules. if the pattern ends with a dash, all rules starting with the pattern are returned as well as one matching the pattern with the dash excluded. passing a Rule behaves like calling getRules($mRule->getRule()).
* Note: This method loses some information: Calling this (with an argument of 'background-') on a declaration block like { background-color: green; background-color; rgba(0, 127, 0, 0.7); } will only yield an associative array containing the rgba-valued rule while @link{getRules()} would yield an indexed array containing both.
* @return Rule[] Rules.
*/
public function getRulesAssoc($mRule = null) {
$aResult = array();
foreach($this->getRules($mRule) as $oRule) {
$aResult[$oRule->getRule()] = $oRule;
}
return $aResult;
}
/**
* Remove a rule from this RuleSet. This accepts all the possible values that @link{getRules()} accepts. If given a Rule, it will only remove this particular rule (by identity). If given a name, it will remove all rules by that name. Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would remove all rules with the same name. To get the old behvaiour, use removeRule($oRule->getRule()).
* @param (null|string|Rule) $mRule pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash, all rules starting with the pattern are removed as well as one matching the pattern with the dash excluded. Passing a Rule behaves matches by identity.
*/
public function removeRule($mRule) {
if($mRule instanceof Rule) {
$sRule = $mRule->getRule();
if(!isset($this->aRules[$sRule])) {
return;
}
foreach($this->aRules[$sRule] as $iKey => $oRule) {
if($oRule === $mRule) {
unset($this->aRules[$sRule][$iKey]);
}
}
} else {
foreach($this->aRules as $sName => $aRules) {
// Either no search rule is given or the search rule matches the found rule exactly or the search rule ends in “-” and the found rule starts with the search rule or equals it (without the trailing dash).
if(!$mRule || $sName === $mRule || (strrpos($mRule, '-') === strlen($mRule) - strlen('-') && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))) {
unset($this->aRules[$sName]);
}
}
}
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sResult = '';
$bIsFirst = true;
foreach ($this->aRules as $aRules) {
foreach($aRules as $oRule) {
$sRendered = $oOutputFormat->safely(function() use ($oRule, $oOutputFormat) {
return $oRule->render($oOutputFormat->nextLevel());
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
} else {
$sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
}
$sResult .= $sRendered;
}
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterRules();
}
return $oOutputFormat->removeLastSemicolon($sResult);
}
/**
* @param array $aComments Array of comments.
*/
public function addComments(array $aComments) {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
* @return array
*/
public function getComments() {
return $this->aComments;
}
/**
* @param array $aComments Array containing Comment objects.
*/
public function setComments(array $aComments) {
$this->aComments = $aComments;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Rule\Rule;
/**
* Parser settings class.
*
* Configure parser behaviour here.
*/
class Settings {
/**
* Multi-byte string support. 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.
*/
public $bMultibyteSupport;
/**
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
*/
public $sDefaultCharset = 'utf-8';
/**
* Lenient parsing. When used (which is true by default), the parser will not choke on unexpected tokens but simply ignore them.
*/
public $bLenientParsing = true;
private function __construct() {
$this->bMultibyteSupport = extension_loaded('mbstring');
}
public static function create() {
return new Settings();
}
public function withMultibyteSupport($bMultibyteSupport = true) {
$this->bMultibyteSupport = $bMultibyteSupport;
return $this;
}
public function withDefaultCharset($sDefaultCharset) {
$this->sDefaultCharset = $sDefaultCharset;
return $this;
}
public function withLenientParsing($bLenientParsing = true) {
$this->bLenientParsing = $bLenientParsing;
return $this;
}
public function beStrict() {
return $this->withLenientParsing(false);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Sabberworm\CSS\Value;
class CSSFunction extends ValueList {
protected $sName;
public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0) {
if($aArguments instanceof RuleValueList) {
$sSeparator = $aArguments->getListSeparator();
$aArguments = $aArguments->getListComponents();
}
$this->sName = $sName;
$this->iLineNo = $iLineNo;
parent::__construct($aArguments, $sSeparator, $iLineNo);
}
public function getName() {
return $this->sName;
}
public function setName($sName) {
$this->sName = $sName;
}
public function getArguments() {
return $this->aComponents;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$aArguments = parent::render($oOutputFormat);
return "{$this->sName}({$aArguments})";
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
class CSSString extends PrimitiveValue {
private $sString;
public function __construct($sString, $iLineNo = 0) {
$this->sString = $sString;
parent::__construct($iLineNo);
}
public static function parse(ParserState $oParserState) {
$sBegin = $oParserState->peek();
$sQuote = null;
if ($sBegin === "'") {
$sQuote = "'";
} else if ($sBegin === '"') {
$sQuote = '"';
}
if ($sQuote !== null) {
$oParserState->consume($sQuote);
}
$sResult = "";
$sContent = null;
if ($sQuote === null) {
// Unquoted strings end in whitespace or with braces, brackets, parentheses
while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $oParserState->peek())) {
$sResult .= $oParserState->parseCharacter(false);
}
} else {
while (!$oParserState->comes($sQuote)) {
$sContent = $oParserState->parseCharacter(false);
if ($sContent === null) {
throw new SourceException("Non-well-formed quoted string {$oParserState->peek(3)}", $oParserState->currentLine());
}
$sResult .= $sContent;
}
$oParserState->consume($sQuote);
}
return new CSSString($sResult, $oParserState->currentLine());
}
public function setString($sString) {
$this->sString = $sString;
}
public function getString() {
return $this->sString;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$sString = addslashes($this->sString);
$sString = str_replace("\n", '\A', $sString);
return $oOutputFormat->getStringQuotingType() . $sString . $oOutputFormat->getStringQuotingType();
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class CalcFunction extends CSSFunction {
const T_OPERAND = 1;
const T_OPERATOR = 2;
public static function parse(ParserState $oParserState) {
$aOperators = array('+', '-', '*', '/');
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = NULL;
while(!$oParserState->comes(')') || $iNestingLevel > 0) {
$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
} else if ($oParserState->comes(')')) {
$iNestingLevel--;
$oCalcList->addListComponent($oParserState->consume(1));
continue;
}
if ($iLastComponentType != CalcFunction::T_OPERAND) {
$oVal = Value::parsePrimitiveValue($oParserState);
$oCalcList->addListComponent($oVal);
$iLastComponentType = CalcFunction::T_OPERAND;
} else {
if (in_array($oParserState->peek(), $aOperators)) {
if (($oParserState->comes('-') || $oParserState->comes('+'))) {
if ($oParserState->peek(1, -1) != ' ' || !($oParserState->comes('- ') || $oParserState->comes('+ '))) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
}
$oCalcList->addListComponent($oParserState->consume(1));
$iLastComponentType = CalcFunction::T_OPERATOR;
} else {
throw new UnexpectedTokenException(
sprintf(
'Next token was expected to be an operand of type %s. Instead "%s" was found.',
implode(', ', $aOperators),
$oVal
),
'',
'custom',
$oParserState->currentLine()
);
}
}
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Sabberworm\CSS\Value;
class CalcRuleValueList extends RuleValueList {
public function __construct($iLineNo = 0) {
parent::__construct(array(), ',', $iLineNo);
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode(' ', $this->aComponents);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class Color extends CSSFunction {
public function __construct($aColor, $iLineNo = 0) {
parent::__construct(implode('', array_keys($aColor)), $aColor, ',', $iLineNo);
}
public static function parse(ParserState $oParserState) {
$aColor = array();
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];
} else if ($oParserState->strlen($sValue) === 4) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2] . $sValue[3] . $sValue[3];
}
if ($oParserState->strlen($sValue) === 8) {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine()),
'a' => new Size(round(self::mapRange(intval($sValue[6] . $sValue[7], 16), 0, 255, 0, 1), 2), null, true, $oParserState->currentLine())
);
} else {
$aColor = array(
'r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $oParserState->currentLine()),
'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $oParserState->currentLine()),
'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $oParserState->currentLine())
);
}
} else {
$sColorMode = $oParserState->parseIdentifier(true);
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
$iLength = $oParserState->strlen($sColorMode);
for ($i = 0; $i < $iLength; ++$i) {
$oParserState->consumeWhiteSpace();
$aColor[$sColorMode[$i]] = Size::parse($oParserState, true);
$oParserState->consumeWhiteSpace();
if ($i < ($iLength - 1)) {
$oParserState->consume(',');
}
}
$oParserState->consume(')');
}
return new Color($aColor, $oParserState->currentLine());
}
private static function mapRange($fVal, $fFromMin, $fFromMax, $fToMin, $fToMax) {
$fFromRange = $fFromMax - $fFromMin;
$fToRange = $fToMax - $fToMin;
$fMultiplier = $fToRange / $fFromRange;
$fNewVal = $fVal - $fFromMin;
$fNewVal *= $fMultiplier;
return $fNewVal + $fToMin;
}
public function getColor() {
return $this->aComponents;
}
public function setColor($aColor) {
$this->setName(implode('', array_keys($aColor)));
$this->aComponents = $aColor;
}
public function getColorDescription() {
return $this->getName();
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
// Shorthand RGB color values
if($oOutputFormat->getRGBHashNotation() && implode('', array_keys($this->aComponents)) === 'rgb') {
$sResult = sprintf(
'%02x%02x%02x',
$this->aComponents['r']->getSize(),
$this->aComponents['g']->getSize(),
$this->aComponents['b']->getSize()
);
return '#'.(($sResult[0] == $sResult[1]) && ($sResult[2] == $sResult[3]) && ($sResult[4] == $sResult[5]) ? "$sResult[0]$sResult[2]$sResult[4]" : $sResult);
}
return parent::render($oOutputFormat);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class LineName extends ValueList {
public function __construct($aComponents = array(), $iLineNo = 0) {
parent::__construct($aComponents, ' ', $iLineNo);
}
public static function parse(ParserState $oParserState) {
$oParserState->consume('[');
$oParserState->consumeWhiteSpace();
$aNames = array();
do {
if($oParserState->getSettings()->bLenientParsing) {
try {
$aNames[] = $oParserState->parseIdentifier();
} catch(UnexpectedTokenException $e) {}
} else {
$aNames[] = $oParserState->parseIdentifier();
}
$oParserState->consumeWhiteSpace();
} while (!$oParserState->comes(']'));
$oParserState->consume(']');
return new LineName($aNames, $oParserState->currentLine());
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return '[' . parent::render(\Sabberworm\CSS\OutputFormat::createCompact()) . ']';
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Sabberworm\CSS\Value;
abstract class PrimitiveValue extends Value {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Sabberworm\CSS\Value;
class RuleValueList extends ValueList {
public function __construct($sSeparator = ',', $iLineNo = 0) {
parent::__construct(array(), $sSeparator, $iLineNo);
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class Size extends PrimitiveValue {
const ABSOLUTE_SIZE_UNITS = 'px/cm/mm/mozmm/in/pt/pc/vh/vw/vm/vmin/vmax/rem'; //vh/vw/vm(ax)/vmin/rem are absolute insofar as they dont scale to the immediate parent (only the viewport)
const RELATIVE_SIZE_UNITS = '%/em/ex/ch/fr';
const NON_SIZE_UNITS = 'deg/grad/rad/s/ms/turns/Hz/kHz';
private static $SIZE_UNITS = null;
private $fSize;
private $sUnit;
private $bIsColorComponent;
public function __construct($fSize, $sUnit = null, $bIsColorComponent = false, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->fSize = floatval($fSize);
$this->sUnit = $sUnit;
$this->bIsColorComponent = $bIsColorComponent;
}
public static function parse(ParserState $oParserState, $bIsColorComponent = false) {
$sSize = '';
if ($oParserState->comes('-')) {
$sSize .= $oParserState->consume('-');
}
while (is_numeric($oParserState->peek()) || $oParserState->comes('.')) {
if ($oParserState->comes('.')) {
$sSize .= $oParserState->consume('.');
} else {
$sSize .= $oParserState->consume(1);
}
}
$sUnit = null;
$aSizeUnits = self::getSizeUnits();
foreach($aSizeUnits as $iLength => &$aValues) {
$sKey = strtolower($oParserState->peek($iLength));
if(array_key_exists($sKey, $aValues)) {
if (($sUnit = $aValues[$sKey]) !== null) {
$oParserState->consume($iLength);
break;
}
}
}
return new Size(floatval($sSize), $sUnit, $bIsColorComponent, $oParserState->currentLine());
}
private static function getSizeUnits() {
if(self::$SIZE_UNITS === null) {
self::$SIZE_UNITS = array();
foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
$iSize = strlen($val);
if(!isset(self::$SIZE_UNITS[$iSize])) {
self::$SIZE_UNITS[$iSize] = array();
}
self::$SIZE_UNITS[$iSize][strtolower($val)] = $val;
}
// FIXME: Should we not order the longest units first?
ksort(self::$SIZE_UNITS, SORT_NUMERIC);
}
return self::$SIZE_UNITS;
}
public function setUnit($sUnit) {
$this->sUnit = $sUnit;
}
public function getUnit() {
return $this->sUnit;
}
public function setSize($fSize) {
$this->fSize = floatval($fSize);
}
public function getSize() {
return $this->fSize;
}
public function isColorComponent() {
return $this->bIsColorComponent;
}
/**
* Returns whether the number stored in this Size really represents a size (as in a length of something on screen).
* @return false if the unit an angle, a duration, a frequency or the number is a component in a Color object.
*/
public function isSize() {
if (in_array($this->sUnit, explode('/', self::NON_SIZE_UNITS))) {
return false;
}
return !$this->isColorComponent();
}
public function isRelative() {
if (in_array($this->sUnit, explode('/', self::RELATIVE_SIZE_UNITS))) {
return true;
}
if ($this->sUnit === null && $this->fSize != 0) {
return true;
}
return false;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
$l = localeconv();
$sPoint = preg_quote($l['decimal_point'], '/');
return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
class URL extends PrimitiveValue {
private $oURL;
public function __construct(CSSString $oURL, $iLineNo = 0) {
parent::__construct($iLineNo);
$this->oURL = $oURL;
}
public static function parse(ParserState $oParserState) {
$bUseUrl = $oParserState->comes('url', true);
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
if ($bUseUrl) {
$oParserState->consumeWhiteSpace();
$oParserState->consume(')');
}
return $oResult;
}
public function setURL(CSSString $oURL) {
$this->oURL = $oURL;
}
public function getURL() {
return $this->oURL;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return "url({$this->oURL->render($oOutputFormat)})";
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Sabberworm\CSS\Value;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Renderable;
abstract class Value implements Renderable {
protected $iLineNo;
public function __construct($iLineNo = 0) {
$this->iLineNo = $iLineNo;
}
public static function parseValue(ParserState $oParserState, $aListDelimiters = array()) {
$aStack = array();
$oParserState->consumeWhiteSpace();
//Build a list of delimiters and parsed values
while (!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!') || $oParserState->comes(')') || $oParserState->comes('\\'))) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
foreach ($aListDelimiters as $sDelimiter) {
if ($oParserState->comes($sDelimiter)) {
array_push($aStack, $oParserState->consume($sDelimiter));
$oParserState->consumeWhiteSpace();
$bFoundDelimiter = true;
break;
}
}
if (!$bFoundDelimiter) {
//Whitespace was the list delimiter
array_push($aStack, ' ');
}
}
array_push($aStack, self::parsePrimitiveValue($oParserState));
$oParserState->consumeWhiteSpace();
}
//Convert the list to list objects
foreach ($aListDelimiters as $sDelimiter) {
if (count($aStack) === 1) {
return $aStack[0];
}
$iStartPosition = null;
while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
$iLength = 2; //Number of elements to be joined
for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
if ($sDelimiter !== $aStack[$i]) {
break;
}
}
$oList = new RuleValueList($sDelimiter, $oParserState->currentLine());
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
$oList->addListComponent($aStack[$i]);
}
array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
}
}
if (!isset($aStack[0])) {
throw new UnexpectedTokenException(" {$oParserState->peek()} ", $oParserState->peek(1, -1) . $oParserState->peek(2), 'literal', $oParserState->currentLine());
}
return $aStack[0];
}
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false) {
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
}
return $sResult;
}
public static function parsePrimitiveValue(ParserState $oParserState) {
$oValue = null;
$oParserState->consumeWhiteSpace();
if (is_numeric($oParserState->peek()) || ($oParserState->comes('-.') && is_numeric($oParserState->peek(1, 2))) || (($oParserState->comes('-') || $oParserState->comes('.')) && is_numeric($oParserState->peek(1, 1)))) {
$oValue = Size::parse($oParserState);
} else if ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} else if ($oParserState->comes('url', true)) {
$oValue = URL::parse($oParserState);
} else if ($oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true) || $oParserState->comes('-moz-calc', true)) {
$oValue = CalcFunction::parse($oParserState);
} else if ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} else if ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
$oValue = self::parseMicrosoftFilter($oParserState);
} else if ($oParserState->comes("[")) {
$oValue = LineName::parse($oParserState);
} else if ($oParserState->comes("U+")) {
$oValue = self::parseUnicodeRangeValue($oParserState);
} else {
$oValue = self::parseIdentifierOrFunction($oParserState);
}
$oParserState->consumeWhiteSpace();
return $oValue;
}
private static function parseMicrosoftFilter(ParserState $oParserState) {
$sFunction = $oParserState->consumeUntil('(', false, true);
$aArguments = Value::parseValue($oParserState, array(',', '='));
return new CSSFunction($sFunction, $aArguments, ',', $oParserState->currentLine());
}
private static function parseUnicodeRangeValue(ParserState $oParserState) {
$iCodepointMaxLenth = 6; // Code points outside BMP can use up to six digits
$sRange = "";
$oParserState->consume("U+");
do {
if ($oParserState->comes('-')) $iCodepointMaxLenth = 13; // Max length is 2 six digit code points + the dash(-) between them
$sRange .= $oParserState->consume(1);
} while (strlen($sRange) < $iCodepointMaxLenth && preg_match("/[A-Fa-f0-9\?-]/", $oParserState->peek()));
return "U+{$sRange}";
}
/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}
//Methods are commented out because re-declaring them here is a fatal error in PHP < 5.3.9
//public abstract function __toString();
//public abstract function render(\Sabberworm\CSS\OutputFormat $oOutputFormat);
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Sabberworm\CSS\Value;
abstract class ValueList extends Value {
protected $aComponents;
protected $sSeparator;
public function __construct($aComponents = array(), $sSeparator = ',', $iLineNo = 0) {
parent::__construct($iLineNo);
if (!is_array($aComponents)) {
$aComponents = array($aComponents);
}
$this->aComponents = $aComponents;
$this->sSeparator = $sSeparator;
}
public function addListComponent($mComponent) {
$this->aComponents[] = $mComponent;
}
public function getListComponents() {
return $this->aComponents;
}
public function setListComponents($aComponents) {
$this->aComponents = $aComponents;
}
public function getListSeparator() {
return $this->sSeparator;
}
public function setListSeparator($sSeparator) {
$this->sSeparator = $sSeparator;
}
public function __toString() {
return $this->render(new \Sabberworm\CSS\OutputFormat());
}
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
return $oOutputFormat->implode($oOutputFormat->spaceBeforeListArgumentSeparator($this->sSeparator) . $this->sSeparator . $oOutputFormat->spaceAfterListArgumentSeparator($this->sSeparator), $this->aComponents);
}
}

View File

@@ -0,0 +1,10 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.5/phpunit.xsd"
bootstrap="tests/bootstrap.php">
<testsuites>
<testsuite name="Project Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,27 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Parser;
class AtRuleBlockListTest extends \PHPUnit_Framework_TestCase {
public function testMediaQueries() {
$sCss = '@media(min-width: 768px){.class{color:red}}';
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
$aContents = $oDoc->getContents();
$oMediaQuery = $aContents[0];
$this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function');
$this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value');
$sCss = '@media (min-width: 768px) {.class{color:red}}';
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
$aContents = $oDoc->getContents();
$oMediaQuery = $aContents[0];
$this->assertSame('media', $oMediaQuery->atRuleName(), 'Does not interpret the type as a function');
$this->assertSame('(min-width: 768px)', $oMediaQuery->atRuleArgs(), 'The media query is the value');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Sabberworm\CSS\CSSList;
use Sabberworm\CSS\Parser;
class DocumentTest extends \PHPUnit_Framework_TestCase {
public function testOverrideContents() {
$sCss = '.thing { left: 10px; }';
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
$aContents = $oDoc->getContents();
$this->assertCount(1, $aContents);
$sCss2 = '.otherthing { right: 10px; }';
$oParser2 = new Parser($sCss);
$oDoc2 = $oParser2->parse();
$aContents2 = $oDoc2->getContents();
$oDoc->setContents(array($aContents[0], $aContents2[0]));
$aFinalContents = $oDoc->getContents();
$this->assertCount(2, $aFinalContents);
}
}

View File

@@ -0,0 +1,170 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\OutputFormat;
global $TEST_CSS;
$TEST_CSS = <<<EOT
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;
background: white;
}
@media screen {
.main {
background-size: 100% 100%;
font-size: 1.3em;
background-color: #fff;
}
}
EOT;
class OutputFormatTest extends \PHPUnit_Framework_TestCase {
private $oParser;
private $oDocument;
function setUp() {
global $TEST_CSS;
$this->oParser = new Parser($TEST_CSS);
$this->oDocument = $this->oParser->parse();
}
public function testPlain() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render());
}
public function testCompact() {
$this->assertSame('.main,.test{font:italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background:white;}@media screen{.main{background-size:100% 100%;font-size:1.3em;background-color:#fff;}}', $this->oDocument->render(OutputFormat::createCompact()));
}
public function testPretty() {
global $TEST_CSS;
$this->assertSame($TEST_CSS, $this->oDocument->render(OutputFormat::createPretty()));
}
public function testSpaceAfterListArgumentSeparator() {
$this->assertSame('.main, .test {font: italic normal bold 16px/ 1.2 "Helvetica", Verdana, sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(" ")));
}
public function testSpaceAfterListArgumentSeparatorComplex() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica", Verdana, sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterListArgumentSeparator(array('default' => ' ', ',' => "\t", '/' => '', ' ' => ''))));
}
public function testSpaceAfterSelectorSeparator() {
$this->assertSame('.main,
.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterSelectorSeparator("\n")));
}
public function testStringQuotingType() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 \'Helvetica\',Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setStringQuotingType("'")));
}
public function testRGBHashNotation() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: rgb(255,255,255);}}', $this->oDocument->render(OutputFormat::create()->setRGBHashNotation(false)));
}
public function testSemicolonAfterLastRule() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff}}', $this->oDocument->render(OutputFormat::create()->setSemicolonAfterLastRule(false)));
}
public function testSpaceAfterRuleName() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceAfterRuleName("\t")));
}
public function testSpaceRules() {
$this->assertSame('.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
background: white;
}
@media screen {.main {
background-size: 100% 100%;
font-size: 1.3em;
background-color: #fff;
}}', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")));
}
public function testSpaceBlocks() {
$this->assertSame('
.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {
.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}
}
', $this->oDocument->render(OutputFormat::create()->set('Space*Blocks', "\n")));
}
public function testSpaceBoth() {
$this->assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
background: white;
}
@media screen {
.main {
background-size: 100% 100%;
font-size: 1.3em;
background-color: #fff;
}
}
', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")));
}
public function testSpaceBetweenBlocks() {
$this->assertSame('.main, .test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceBetweenBlocks('')));
}
public function testIndentation() {
$this->assertSame('
.main, .test {
font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;
background: white;
}
@media screen {
.main {
background-size: 100% 100%;
font-size: 1.3em;
background-color: #fff;
}
}
', $this->oDocument->render(OutputFormat::create()->set('Space*Rules', "\n")->set('Space*Blocks', "\n")->setIndentation('')));
}
public function testSpaceBeforeBraces() {
$this->assertSame('.main, .test{font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen{.main{background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setSpaceBeforeOpeningBrace('')));
}
/**
* @expectedException Sabberworm\CSS\Parsing\OutputException
*/
public function testIgnoreExceptionsOff() {
$aBlocks = $this->oDocument->getAllDeclarationBlocks();
$oFirstBlock = $aBlocks[0];
$oFirstBlock->removeSelector('.main');
$this->assertSame('.test {font: italic normal bold 16px/1.2 "Helvetica",Verdana,sans-serif;background: white;}
@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false)));
$oFirstBlock->removeSelector('.test');
$this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(false));
}
public function testIgnoreExceptionsOn() {
$aBlocks = $this->oDocument->getAllDeclarationBlocks();
$oFirstBlock = $aBlocks[0];
$oFirstBlock->removeSelector('.main');
$oFirstBlock->removeSelector('.test');
$this->assertSame('@media screen {.main {background-size: 100% 100%;font-size: 1.3em;background-color: #fff;}}', $this->oDocument->render(OutputFormat::create()->setIgnoreExceptions(true)));
}
}

View File

@@ -0,0 +1,714 @@
<?php
namespace Sabberworm\CSS;
use Sabberworm\CSS\CSSList\KeyFrame;
use Sabberworm\CSS\Value\Size;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
class ParserTest extends \PHPUnit_Framework_TestCase {
function testFiles() {
$sDirectory = dirname(__FILE__) . '/../../files';
if ($rHandle = opendir($sDirectory)) {
/* This is the correct way to loop over the directory. */
while (false !== ($sFileName = readdir($rHandle))) {
if (strpos($sFileName, '.') === 0) {
continue;
}
if (strrpos($sFileName, '.css') !== strlen($sFileName) - strlen('.css')) {
continue;
}
if (strpos($sFileName, '-') === 0) {
//Either a file which SHOULD fail (at least in strict mode) or a future test of a as-of-now missing feature
continue;
}
$oParser = new Parser(file_get_contents($sDirectory . DIRECTORY_SEPARATOR . $sFileName));
try {
$this->assertNotEquals('', $oParser->parse()->render());
} catch (\Exception $e) {
$this->fail($e);
}
}
closedir($rHandle);
}
}
/**
* @depends testFiles
*/
function testColorParsing() {
$oDoc = $this->parsedStructureForFile('colortest');
foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
if (!$oRuleSet instanceof DeclarationBlock) {
continue;
}
$sSelector = $oRuleSet->getSelectors();
$sSelector = $sSelector[0]->getSelector();
if ($sSelector === '#mine') {
$aColorRule = $oRuleSet->getRules('color');
$oColor = $aColorRule[0]->getValue();
$this->assertSame('red', $oColor);
$aColorRule = $oRuleSet->getRules('background-');
$oColor = $aColorRule[0]->getValue();
$this->assertEquals(array('r' => new Size(35.0, null, true, $oColor->getLineNo()), 'g' => new Size(35.0, null, true, $oColor->getLineNo()), 'b' => new Size(35.0, null, true, $oColor->getLineNo())), $oColor->getColor());
$aColorRule = $oRuleSet->getRules('border-color');
$oColor = $aColorRule[0]->getValue();
$this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(230.0, null, true, $oColor->getLineNo())), $oColor->getColor());
$oColor = $aColorRule[1]->getValue();
$this->assertEquals(array('r' => new Size(10.0, null, true, $oColor->getLineNo()), 'g' => new Size(100.0, null, true, $oColor->getLineNo()), 'b' => new Size(231.0, null, true, $oColor->getLineNo()), 'a' => new Size("0000.3", null, true, $oColor->getLineNo())), $oColor->getColor());
$aColorRule = $oRuleSet->getRules('outline-color');
$oColor = $aColorRule[0]->getValue();
$this->assertEquals(array('r' => new Size(34.0, null, true, $oColor->getLineNo()), 'g' => new Size(34.0, null, true, $oColor->getLineNo()), 'b' => new Size(34.0, null, true, $oColor->getLineNo())), $oColor->getColor());
} else if($sSelector === '#yours') {
$aColorRule = $oRuleSet->getRules('background-color');
$oColor = $aColorRule[0]->getValue();
$this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo())), $oColor->getColor());
$oColor = $aColorRule[1]->getValue();
$this->assertEquals(array('h' => new Size(220.0, null, true, $oColor->getLineNo()), 's' => new Size(10.0, '%', true, $oColor->getLineNo()), 'l' => new Size(220.0, '%', true, $oColor->getLineNo()), 'a' => new Size(0000.3, null, true, $oColor->getLineNo())), $oColor->getColor());
}
}
foreach ($oDoc->getAllValues('color') as $sColor) {
$this->assertSame('red', $sColor);
}
$this->assertSame('#mine {color: red;border-color: #0a64e6;border-color: rgba(10,100,231,.3);outline-color: #222;background-color: #232323;}
#yours {background-color: hsl(220,10%,220%);background-color: hsla(220,10%,220%,.3);}', $oDoc->render());
}
function testUnicodeParsing() {
$oDoc = $this->parsedStructureForFile('unicode');
foreach ($oDoc->getAllDeclarationBlocks() as $oRuleSet) {
$sSelector = $oRuleSet->getSelectors();
$sSelector = $sSelector[0]->getSelector();
if (substr($sSelector, 0, strlen('.test-')) !== '.test-') {
continue;
}
$aContentRules = $oRuleSet->getRules('content');
$aContents = $aContentRules[0]->getValues();
$sString = $aContents[0][0]->__toString();
if ($sSelector == '.test-1') {
$this->assertSame('" "', $sString);
}
if ($sSelector == '.test-2') {
$this->assertSame('"é"', $sString);
}
if ($sSelector == '.test-3') {
$this->assertSame('" "', $sString);
}
if ($sSelector == '.test-4') {
$this->assertSame('"𝄞"', $sString);
}
if ($sSelector == '.test-5') {
$this->assertSame('"水"', $sString);
}
if ($sSelector == '.test-6') {
$this->assertSame('"¥"', $sString);
}
if ($sSelector == '.test-7') {
$this->assertSame('"\A"', $sString);
}
if ($sSelector == '.test-8') {
$this->assertSame('"\"\""', $sString);
}
if ($sSelector == '.test-9') {
$this->assertSame('"\"\\\'"', $sString);
}
if ($sSelector == '.test-10') {
$this->assertSame('"\\\'\\\\"', $sString);
}
if ($sSelector == '.test-11') {
$this->assertSame('"test"', $sString);
}
}
}
function testUnicodeRangeParsing() {
$oDoc = $this->parsedStructureForFile('unicode-range');
$sExpected = "@font-face {unicode-range: U+0100-024F,U+0259,U+1E??-2EFF,U+202F;}";
$this->assertSame($sExpected, $oDoc->render());
}
function testSpecificity() {
$oDoc = $this->parsedStructureForFile('specificity');
$oDeclarationBlock = $oDoc->getAllDeclarationBlocks();
$oDeclarationBlock = $oDeclarationBlock[0];
$aSelectors = $oDeclarationBlock->getSelectors();
foreach ($aSelectors as $oSelector) {
switch ($oSelector->getSelector()) {
case "#test .help":
$this->assertSame(110, $oSelector->getSpecificity());
break;
case "#file":
$this->assertSame(100, $oSelector->getSpecificity());
break;
case ".help:hover":
$this->assertSame(20, $oSelector->getSpecificity());
break;
case "ol li::before":
$this->assertSame(3, $oSelector->getSpecificity());
break;
case "li.green":
$this->assertSame(11, $oSelector->getSpecificity());
break;
default:
$this->fail("specificity: untested selector " . $oSelector->getSelector());
}
}
$this->assertEquals(array(new Selector('#test .help', true)), $oDoc->getSelectorsBySpecificity('> 100'));
$this->assertEquals(array(new Selector('#test .help', true), new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('>= 100'));
$this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('=== 100'));
$this->assertEquals(array(new Selector('#file', true)), $oDoc->getSelectorsBySpecificity('== 100'));
$this->assertEquals(array(new Selector('#file', true), new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('<= 100'));
$this->assertEquals(array(new Selector('.help:hover', true), new Selector('li.green', true), new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity('< 100'));
$this->assertEquals(array(new Selector('li.green', true)), $oDoc->getSelectorsBySpecificity('11'));
$this->assertEquals(array(new Selector('ol li::before', true)), $oDoc->getSelectorsBySpecificity(3));
}
function testManipulation() {
$oDoc = $this->parsedStructureForFile('atrules');
$this->assertSame('@charset "utf-8";
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
html, body {font-size: -.6em;}
@keyframes mymove {from {top: 0px;}
to {top: 200px;}}
@-moz-keyframes some-move {from {top: 0px;}
to {top: 200px;}}
@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {body {font-family: "Helvetica";}}
@page :pseudo-class {margin: 2in;}
@-moz-document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*") {body {color: purple;background: yellow;}}
@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}
@region-style #intro {p {color: blue;}}', $oDoc->render());
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
foreach ($oBlock->getSelectors() as $oSelector) {
//Loop over all selector parts (the comma-separated strings in a selector) and prepend the id
$oSelector->setSelector('#my_id ' . $oSelector->getSelector());
}
}
$this->assertSame('@charset "utf-8";
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
#my_id html, #my_id body {font-size: -.6em;}
@keyframes mymove {from {top: 0px;}
to {top: 200px;}}
@-moz-keyframes some-move {from {top: 0px;}
to {top: 200px;}}
@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {#my_id body {font-family: "Helvetica";}}
@page :pseudo-class {margin: 2in;}
@-moz-document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*") {#my_id body {color: purple;background: yellow;}}
@media screen and (orientation: landscape) {@-ms-viewport {width: 1024px;height: 768px;}}
@region-style #intro {#my_id p {color: blue;}}', $oDoc->render());
$oDoc = $this->parsedStructureForFile('values');
$this->assertSame('#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;font-size: 10px;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;}
body {color: green;font: 75% "Lucida Grande","Trebuchet MS",Verdana,sans-serif;}', $oDoc->render());
foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
$oRuleSet->removeRule('font-');
}
$this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;background-color: green;background-color: rgba(0,128,0,.7);frequency: 30Hz;}
body {color: green;}', $oDoc->render());
foreach ($oDoc->getAllRuleSets() as $oRuleSet) {
$oRuleSet->removeRule('background-');
}
$this->assertSame('#header {margin: 10px 2em 1cm 2%;color: red !important;frequency: 30Hz;}
body {color: green;}', $oDoc->render());
}
function testRuleGetters() {
$oDoc = $this->parsedStructureForFile('values');
$aBlocks = $oDoc->getAllDeclarationBlocks();
$oHeaderBlock = $aBlocks[0];
$oBodyBlock = $aBlocks[1];
$aHeaderRules = $oHeaderBlock->getRules('background-');
$this->assertSame(2, count($aHeaderRules));
$this->assertSame('background-color', $aHeaderRules[0]->getRule());
$this->assertSame('background-color', $aHeaderRules[1]->getRule());
$aHeaderRules = $oHeaderBlock->getRulesAssoc('background-');
$this->assertSame(1, count($aHeaderRules));
$this->assertSame(true, $aHeaderRules['background-color']->getValue() instanceof \Sabberworm\CSS\Value\Color);
$this->assertSame('rgba', $aHeaderRules['background-color']->getValue()->getColorDescription());
$oHeaderBlock->removeRule($aHeaderRules['background-color']);
$aHeaderRules = $oHeaderBlock->getRules('background-');
$this->assertSame(1, count($aHeaderRules));
$this->assertSame('green', $aHeaderRules[0]->getValue());
}
function testSlashedValues() {
$oDoc = $this->parsedStructureForFile('slashed');
$this->assertSame('.test {font: 12px/1.5 Verdana,Arial,sans-serif;border-radius: 5px 10px 5px 10px/10px 5px 10px 5px;}', $oDoc->render());
foreach ($oDoc->getAllValues(null) as $mValue) {
if ($mValue instanceof Size && $mValue->isSize() && !$mValue->isRelative()) {
$mValue->setSize($mValue->getSize() * 3);
}
}
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
$oRule = $oBlock->getRules('font');
$oRule = $oRule[0];
$oSpaceList = $oRule->getValue();
$this->assertEquals(' ', $oSpaceList->getListSeparator());
$oSlashList = $oSpaceList->getListComponents();
$oCommaList = $oSlashList[1];
$oSlashList = $oSlashList[0];
$this->assertEquals(',', $oCommaList->getListSeparator());
$this->assertEquals('/', $oSlashList->getListSeparator());
$oRule = $oBlock->getRules('border-radius');
$oRule = $oRule[0];
$oSlashList = $oRule->getValue();
$this->assertEquals('/', $oSlashList->getListSeparator());
$oSpaceList1 = $oSlashList->getListComponents();
$oSpaceList2 = $oSpaceList1[1];
$oSpaceList1 = $oSpaceList1[0];
$this->assertEquals(' ', $oSpaceList1->getListSeparator());
$this->assertEquals(' ', $oSpaceList2->getListSeparator());
}
$this->assertSame('.test {font: 36px/1.5 Verdana,Arial,sans-serif;border-radius: 15px 30px 15px 30px/30px 15px 30px 15px;}', $oDoc->render());
}
function testFunctionSyntax() {
$oDoc = $this->parsedStructureForFile('functions');
$sExpected = 'div.main {background-image: linear-gradient(#000,#fff);}
.collapser::before, .collapser::-moz-before, .collapser::-webkit-before {content: "»";font-size: 1.2em;margin-right: .2em;-moz-transition-property: -moz-transform;-moz-transition-duration: .2s;-moz-transform-origin: center 60%;}
.collapser.expanded::before, .collapser.expanded::-moz-before, .collapser.expanded::-webkit-before {-moz-transform: rotate(90deg);}
.collapser + * {height: 0;overflow: hidden;-moz-transition-property: height;-moz-transition-duration: .3s;}
.collapser.expanded + * {height: auto;}';
$this->assertSame($sExpected, $oDoc->render());
foreach ($oDoc->getAllValues(null, true) as $mValue) {
if ($mValue instanceof Size && $mValue->isSize()) {
$mValue->setSize($mValue->getSize() * 3);
}
}
$sExpected = str_replace(array('1.2em', '.2em', '60%'), array('3.6em', '.6em', '180%'), $sExpected);
$this->assertSame($sExpected, $oDoc->render());
foreach ($oDoc->getAllValues(null, true) as $mValue) {
if ($mValue instanceof Size && !$mValue->isRelative() && !$mValue->isColorComponent()) {
$mValue->setSize($mValue->getSize() * 2);
}
}
$sExpected = str_replace(array('.2s', '.3s', '90deg'), array('.4s', '.6s', '180deg'), $sExpected);
$this->assertSame($sExpected, $oDoc->render());
}
function testExpandShorthands() {
$oDoc = $this->parsedStructureForFile('expand-shorthands');
$sExpected = 'body {font: italic 500 14px/1.618 "Trebuchet MS",Georgia,serif;border: 2px solid #f0f;background: #ccc url("/images/foo.png") no-repeat left top;margin: 1em !important;padding: 2px 6px 3px;}';
$this->assertSame($sExpected, $oDoc->render());
$oDoc->expandShorthands();
$sExpected = 'body {margin-top: 1em !important;margin-right: 1em !important;margin-bottom: 1em !important;margin-left: 1em !important;padding-top: 2px;padding-right: 6px;padding-bottom: 3px;padding-left: 6px;border-top-color: #f0f;border-right-color: #f0f;border-bottom-color: #f0f;border-left-color: #f0f;border-top-style: solid;border-right-style: solid;border-bottom-style: solid;border-left-style: solid;border-top-width: 2px;border-right-width: 2px;border-bottom-width: 2px;border-left-width: 2px;font-style: italic;font-variant: normal;font-weight: 500;font-size: 14px;line-height: 1.618;font-family: "Trebuchet MS",Georgia,serif;background-color: #ccc;background-image: url("/images/foo.png");background-repeat: no-repeat;background-attachment: scroll;background-position: left top;}';
$this->assertSame($sExpected, $oDoc->render());
}
function testCreateShorthands() {
$oDoc = $this->parsedStructureForFile('create-shorthands');
$sExpected = 'body {font-size: 2em;font-family: Helvetica,Arial,sans-serif;font-weight: bold;border-width: 2px;border-color: #999;border-style: dotted;background-color: #fff;background-image: url("foobar.png");background-repeat: repeat-y;margin-top: 2px;margin-right: 3px;margin-bottom: 4px;margin-left: 5px;}';
$this->assertSame($sExpected, $oDoc->render());
$oDoc->createShorthands();
$sExpected = 'body {background: #fff url("foobar.png") repeat-y;margin: 2px 5px 4px 3px;border: 2px dotted #999;font: bold 2em Helvetica,Arial,sans-serif;}';
$this->assertSame($sExpected, $oDoc->render());
}
function testNamespaces() {
$oDoc = $this->parsedStructureForFile('namespaces');
$sExpected = '@namespace toto "http://toto.example.org";
@namespace "http://example.com/foo";
@namespace foo url("http://www.example.com/");
@namespace foo url("http://www.example.com/");
foo|test {gaga: 1;}
|test {gaga: 2;}';
$this->assertSame($sExpected, $oDoc->render());
}
function testInnerColors() {
$oDoc = $this->parsedStructureForFile('inner-color');
$sExpected = 'test {background: -webkit-gradient(linear,0 0,0 bottom,from(#006cad),to(hsl(202,100%,49%)));}';
$this->assertSame($sExpected, $oDoc->render());
}
function testPrefixedGradient() {
$oDoc = $this->parsedStructureForFile('webkit');
$sExpected = '.test {background: -webkit-linear-gradient(top right,white,black);}';
$this->assertSame($sExpected, $oDoc->render());
}
function testListValueRemoval() {
$oDoc = $this->parsedStructureForFile('atrules');
foreach ($oDoc->getContents() as $oItem) {
if ($oItem instanceof AtRule) {
$oDoc->remove($oItem);
continue;
}
}
$this->assertSame('html, body {font-size: -.6em;}', $oDoc->render());
$oDoc = $this->parsedStructureForFile('nested');
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
$oDoc->removeDeclarationBlockBySelector($oBlock, false);
break;
}
$this->assertSame('html {some-other: -test(val1);}
@media screen {html {some: -test(val2);}}
#unrelated {other: yes;}', $oDoc->render());
$oDoc = $this->parsedStructureForFile('nested');
foreach ($oDoc->getAllDeclarationBlocks() as $oBlock) {
$oDoc->removeDeclarationBlockBySelector($oBlock, true);
break;
}
$this->assertSame('@media screen {html {some: -test(val2);}}
#unrelated {other: yes;}', $oDoc->render());
}
/**
* @expectedException Sabberworm\CSS\Parsing\OutputException
*/
function testSelectorRemoval() {
$oDoc = $this->parsedStructureForFile('1readme');
$aBlocks = $oDoc->getAllDeclarationBlocks();
$oBlock1 = $aBlocks[0];
$this->assertSame(true, $oBlock1->removeSelector('html'));
$sExpected = '@charset "utf-8";
@font-face {font-family: "CrassRoots";src: url("../media/cr.ttf");}
body {font-size: 1.6em;}';
$this->assertSame($sExpected, $oDoc->render());
$this->assertSame(false, $oBlock1->removeSelector('html'));
$this->assertSame(true, $oBlock1->removeSelector('body'));
// This tries to output a declaration block without a selector and throws.
$oDoc->render();
}
function testComments() {
$oDoc = $this->parsedStructureForFile('comments');
$sExpected = '@import url("some/url.css") screen;
.foo, #bar {background-color: #000;}
@media screen {#foo.bar {position: absolute;}}';
$this->assertSame($sExpected, $oDoc->render());
}
function testUrlInFile() {
$oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(true));
$sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;}
body {background-url: url("http://somesite.com/images/someimage.gif");}';
$this->assertSame($sExpected, $oDoc->render());
}
function testHexAlphaInFile() {
$oDoc = $this->parsedStructureForFile('hex-alpha', Settings::create()->withMultibyteSupport(true));
$sExpected = 'div {background: rgba(17,34,51,.27);}
div {background: rgba(17,34,51,.27);}';
$this->assertSame($sExpected, $oDoc->render());
}
function testCalcInFile() {
$oDoc = $this->parsedStructureForFile('calc', Settings::create()->withMultibyteSupport(true));
$sExpected = 'div {width: calc(100% / 4);}
div {margin-top: calc(-120% - 4px);}
div {height: -webkit-calc(9 / 16 * 100%) !important;width: -moz-calc(( 50px - 50% ) * 2);}';
$this->assertSame($sExpected, $oDoc->render());
}
function testCalcNestedInFile() {
$oDoc = $this->parsedStructureForFile('calc-nested', Settings::create()->withMultibyteSupport(true));
$sExpected = '.test {font-size: calc(( 3 * 4px ) + -2px);top: calc(200px - calc(20 * 3px));}';
$this->assertSame($sExpected, $oDoc->render());
}
function testGridLineNameInFile() {
$oDoc = $this->parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true));
$sExpected = "div {grid-template-columns: [linename] 100px;}\nspan {grid-template-columns: [linename1 linename2] 100px;}";
$this->assertSame($sExpected, $oDoc->render());
}
function testEmptyGridLineNameLenientInFile() {
$oDoc = $this->parsedStructureForFile('empty-grid-linename');
$sExpected = '.test {grid-template-columns: [] 100px;}';
$this->assertSame($sExpected, $oDoc->render());
}
function testUnmatchedBracesInFile() {
$oDoc = $this->parsedStructureForFile('unmatched_braces', Settings::create()->withMultibyteSupport(true));
$sExpected = 'button, input, checkbox, textarea {outline: 0;margin: 0;}';
$this->assertSame($sExpected, $oDoc->render());
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testLineNameFailure() {
$this->parsedStructureForFile('-empty-grid-linename', Settings::create()->withLenientParsing(false));
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testCalcFailure() {
$this->parsedStructureForFile('-calc-no-space-around-minus', Settings::create()->withLenientParsing(false));
}
function testUrlInFileMbOff() {
$oDoc = $this->parsedStructureForFile('url', Settings::create()->withMultibyteSupport(false));
$sExpected = 'body {background: #fff url("http://somesite.com/images/someimage.gif") repeat top center;}
body {background-url: url("http://somesite.com/images/someimage.gif");}';
$this->assertSame($sExpected, $oDoc->render());
}
function testEmptyFile() {
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true));
$sExpected = '';
$this->assertSame($sExpected, $oDoc->render());
}
function testEmptyFileMbOff() {
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false));
$sExpected = '';
$this->assertSame($sExpected, $oDoc->render());
}
function testCharsetLenient1() {
$oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true));
$sExpected = '#id {prop: var(--val);}';
$this->assertSame($sExpected, $oDoc->render());
}
function testCharsetLenient2() {
$oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true));
$sExpected = '@media print {}';
$this->assertSame($sExpected, $oDoc->render());
}
function testTrailingWhitespace() {
$oDoc = $this->parsedStructureForFile('trailing-whitespace', Settings::create()->withLenientParsing(false));
$sExpected = 'div {width: 200px;}';
$this->assertSame($sExpected, $oDoc->render());
}
/**
* @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testCharsetFailure1() {
$this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false));
}
/**
* @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testCharsetFailure2() {
$this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false));
}
/**
* @expectedException \Sabberworm\CSS\Parsing\SourceException
*/
function testUnopenedClosingBracketFailure() {
$this->parsedStructureForFile('unopened-close-brackets', Settings::create()->withLenientParsing(false));
}
/**
* Ensure that a missing property value raises an exception.
*
* @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException
* @covers \Sabberworm\CSS\Value\Value::parseValue()
*/
function testMissingPropertyValueStrict() {
$this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(false));
}
/**
* Ensure that a missing property value is ignored when in lenient parsing mode.
*
* @covers \Sabberworm\CSS\Value\Value::parseValue()
*/
function testMissingPropertyValueLenient() {
$parsed = $this->parsedStructureForFile('missing-property-value', Settings::create()->withLenientParsing(true));
$rulesets = $parsed->getAllRuleSets();
$this->assertCount( 1, $rulesets );
$block = $rulesets[0];
$this->assertTrue( $block instanceof DeclarationBlock );
$this->assertEquals( array( 'div' ), $block->getSelectors() );
$rules = $block->getRules();
$this->assertCount( 1, $rules );
$rule = $rules[0];
$this->assertEquals( 'display', $rule->getRule() );
$this->assertEquals( 'inline-block', $rule->getValue() );
}
/**
* Parse structure for file.
*
* @param string $sFileName Filename.
* @param null|obJeCt $oSettings Settings.
*
* @return CSSList\Document Parsed document.
*/
function parsedStructureForFile($sFileName, $oSettings = null) {
$sFile = dirname(__FILE__) . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css";
$oParser = new Parser(file_get_contents($sFile), $oSettings);
return $oParser->parse();
}
/**
* @depends testFiles
*/
function testLineNumbersParsing() {
$oDoc = $this->parsedStructureForFile('line-numbers');
// array key is the expected line number
$aExpected = array(
1 => array('Sabberworm\CSS\Property\Charset'),
3 => array('Sabberworm\CSS\Property\CSSNamespace'),
5 => array('Sabberworm\CSS\RuleSet\AtRuleSet'),
11 => array('Sabberworm\CSS\RuleSet\DeclarationBlock'),
// Line Numbers of the inner declaration blocks
17 => array('Sabberworm\CSS\CSSList\KeyFrame', 18, 20),
23 => array('Sabberworm\CSS\Property\Import'),
25 => array('Sabberworm\CSS\RuleSet\DeclarationBlock')
);
$aActual = array();
foreach ($oDoc->getContents() as $oContent) {
$aActual[$oContent->getLineNo()] = array(get_class($oContent));
if ($oContent instanceof KeyFrame) {
foreach ($oContent->getContents() as $block) {
$aActual[$oContent->getLineNo()][] = $block->getLineNo();
}
}
}
$aUrlExpected = array(7, 26); // expected line numbers
$aUrlActual = array();
foreach ($oDoc->getAllValues() as $oValue) {
if ($oValue instanceof URL) {
$aUrlActual[] = $oValue->getLineNo();
}
}
// Checking for the multiline color rule lines 27-31
$aExpectedColorLines = array(28, 29, 30);
$aDeclBlocks = $oDoc->getAllDeclarationBlocks();
// Choose the 2nd one
$oDeclBlock = $aDeclBlocks[1];
$aRules = $oDeclBlock->getRules();
// Choose the 2nd one
$oColor = $aRules[1]->getValue();
$this->assertEquals(27, $aRules[1]->getLineNo());
foreach ($oColor->getColor() as $oSize) {
$aActualColorLines[] = $oSize->getLineNo();
}
$this->assertEquals($aExpectedColorLines, $aActualColorLines);
$this->assertEquals($aUrlExpected, $aUrlActual);
$this->assertEquals($aExpected, $aActual);
}
/**
* @expectedException \Sabberworm\CSS\Parsing\UnexpectedTokenException
* Credit: This test by @sabberworm (from https://github.com/sabberworm/PHP-CSS-Parser/pull/105#issuecomment-229643910 )
*/
function testUnexpectedTokenExceptionLineNo() {
$oParser = new Parser("\ntest: 1;", Settings::create()->beStrict());
try {
$oParser->parse();
} catch (UnexpectedTokenException $e) {
$this->assertSame(2, $e->getLineNo());
throw $e;
}
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testIeHacksStrictParsing() {
// We can't strictly parse IE hacks.
$this->parsedStructureForFile('ie-hacks', Settings::create()->beStrict());
}
function testIeHacksParsing() {
$oDoc = $this->parsedStructureForFile('ie-hacks', Settings::create()->withLenientParsing(true));
$sExpected = 'p {padding-right: .75rem \9;background-image: none \9;color: red \9\0;background-color: red \9\0;background-color: red \9\0 !important;content: "red \0";content: "red઼";}';
$this->assertEquals($sExpected, $oDoc->render());
}
/**
* @depends testFiles
*/
function testCommentExtracting() {
$oDoc = $this->parsedStructureForFile('comments');
$aNodes = $oDoc->getContents();
// Import property.
$importComments = $aNodes[0]->getComments();
$this->assertCount(1, $importComments);
$this->assertEquals("*\n * Comments Hell.\n ", $importComments[0]->getComment());
// Declaration block.
$fooBarBlock = $aNodes[1];
$fooBarBlockComments = $fooBarBlock->getComments();
// TODO Support comments in selectors.
// $this->assertCount(2, $fooBarBlockComments);
// $this->assertEquals("* Number 4 *", $fooBarBlockComments[0]->getComment());
// $this->assertEquals("* Number 5 *", $fooBarBlockComments[1]->getComment());
// Declaration rules.
$fooBarRules = $fooBarBlock->getRules();
$fooBarRule = $fooBarRules[0];
$fooBarRuleComments = $fooBarRule->getComments();
$this->assertCount(1, $fooBarRuleComments);
$this->assertEquals(" Number 6 ", $fooBarRuleComments[0]->getComment());
// Media property.
$mediaComments = $aNodes[2]->getComments();
$this->assertCount(0, $mediaComments);
// Media children.
$mediaRules = $aNodes[2]->getContents();
$fooBarComments = $mediaRules[0]->getComments();
$this->assertCount(1, $fooBarComments);
$this->assertEquals("* Number 10 *", $fooBarComments[0]->getComment());
// Media -> declaration -> rule.
$fooBarRules = $mediaRules[0]->getRules();
$fooBarChildComments = $fooBarRules[0]->getComments();
$this->assertCount(1, $fooBarChildComments);
$this->assertEquals("* Number 10b *", $fooBarChildComments[0]->getComment());
}
function testFlatCommentExtracting() {
$parser = new Parser('div {/*Find Me!*/left:10px; text-align:left;}');
$doc = $parser->parse();
$contents = $doc->getContents();
$divRules = $contents[0]->getRules();
$comments = $divRules[0]->getComments();
$this->assertCount(1, $comments);
$this->assertEquals("Find Me!", $comments[0]->getComment());
}
function testTopLevelCommentExtracting() {
$parser = new Parser('/*Find Me!*/div {left:10px; text-align:left;}');
$doc = $parser->parse();
$contents = $doc->getContents();
$comments = $contents[0]->getComments();
$this->assertCount(1, $comments);
$this->assertEquals("Find Me!", $comments[0]->getComment());
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
function testMicrosoftFilterStrictParsing() {
$oDoc = $this->parsedStructureForFile('ms-filter', Settings::create()->beStrict());
}
function testMicrosoftFilterParsing() {
$oDoc = $this->parsedStructureForFile('ms-filter');
$sExpected = ".test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#80000000\",endColorstr=\"#00000000\",GradientType=1);}";
$this->assertSame($sExpected, $oDoc->render());
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\Size;
class DeclarationBlockTest extends \PHPUnit_Framework_TestCase {
/**
* @dataProvider expandBorderShorthandProvider
* */
public function testExpandBorderShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandBorderShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function expandBorderShorthandProvider() {
return array(
array('body{ border: 2px solid #000 }', 'body {border-width: 2px;border-style: solid;border-color: #000;}'),
array('body{ border: none }', 'body {border-style: none;}'),
array('body{ border: 2px }', 'body {border-width: 2px;}'),
array('body{ border: #f00 }', 'body {border-color: #f00;}'),
array('body{ border: 1em solid }', 'body {border-width: 1em;border-style: solid;}'),
array('body{ margin: 1em; }', 'body {margin: 1em;}')
);
}
/**
* @dataProvider expandFontShorthandProvider
* */
public function testExpandFontShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandFontShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function expandFontShorthandProvider() {
return array(
array(
'body{ margin: 1em; }',
'body {margin: 1em;}'
),
array(
'body {font: 12px serif;}',
'body {font-style: normal;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}'
),
array(
'body {font: italic 12px serif;}',
'body {font-style: italic;font-variant: normal;font-weight: normal;font-size: 12px;line-height: normal;font-family: serif;}'
),
array(
'body {font: italic bold 12px serif;}',
'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: normal;font-family: serif;}'
),
array(
'body {font: italic bold 12px/1.6 serif;}',
'body {font-style: italic;font-variant: normal;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}'
),
array(
'body {font: italic small-caps bold 12px/1.6 serif;}',
'body {font-style: italic;font-variant: small-caps;font-weight: bold;font-size: 12px;line-height: 1.6;font-family: serif;}'
),
);
}
/**
* @dataProvider expandBackgroundShorthandProvider
* */
public function testExpandBackgroundShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandBackgroundShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function expandBackgroundShorthandProvider() {
return array(
array('body {border: 1px;}', 'body {border: 1px;}'),
array('body {background: #f00;}', 'body {background-color: #f00;background-image: none;background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'),
array('body {background: #f00 url("foobar.png");}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: repeat;background-attachment: scroll;background-position: 0% 0%;}'),
array('body {background: #f00 url("foobar.png") no-repeat;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: 0% 0%;}'),
array('body {background: #f00 url("foobar.png") no-repeat center;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: center center;}'),
array('body {background: #f00 url("foobar.png") no-repeat top left;}', 'body {background-color: #f00;background-image: url("foobar.png");background-repeat: no-repeat;background-attachment: scroll;background-position: top left;}'),
);
}
/**
* @dataProvider expandDimensionsShorthandProvider
* */
public function testExpandDimensionsShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->expandDimensionsShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function expandDimensionsShorthandProvider() {
return array(
array('body {border: 1px;}', 'body {border: 1px;}'),
array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'),
array('body {margin: 1em;}', 'body {margin-top: 1em;margin-right: 1em;margin-bottom: 1em;margin-left: 1em;}'),
array('body {margin: 1em 2em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 1em;margin-left: 2em;}'),
array('body {margin: 1em 2em 3em;}', 'body {margin-top: 1em;margin-right: 2em;margin-bottom: 3em;margin-left: 2em;}'),
);
}
/**
* @dataProvider createBorderShorthandProvider
* */
public function testCreateBorderShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createBorderShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function createBorderShorthandProvider() {
return array(
array('body {border-width: 2px;border-style: solid;border-color: #000;}', 'body {border: 2px solid #000;}'),
array('body {border-style: none;}', 'body {border: none;}'),
array('body {border-width: 1em;border-style: solid;}', 'body {border: 1em solid;}'),
array('body {margin: 1em;}', 'body {margin: 1em;}')
);
}
/**
* @dataProvider createFontShorthandProvider
* */
public function testCreateFontShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createFontShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function createFontShorthandProvider() {
return array(
array('body {font-size: 12px; font-family: serif}', 'body {font: 12px serif;}'),
array('body {font-size: 12px; font-family: serif; font-style: italic;}', 'body {font: italic 12px serif;}'),
array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold;}', 'body {font: italic bold 12px serif;}'),
array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6;}', 'body {font: italic bold 12px/1.6 serif;}'),
array('body {font-size: 12px; font-family: serif; font-style: italic; font-weight: bold; line-height: 1.6; font-variant: small-caps;}', 'body {font: italic small-caps bold 12px/1.6 serif;}'),
array('body {margin: 1em;}', 'body {margin: 1em;}')
);
}
/**
* @dataProvider createDimensionsShorthandProvider
* */
public function testCreateDimensionsShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createDimensionsShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function createDimensionsShorthandProvider() {
return array(
array('body {border: 1px;}', 'body {border: 1px;}'),
array('body {margin-top: 1px;}', 'body {margin-top: 1px;}'),
array('body {margin-top: 1em; margin-right: 1em; margin-bottom: 1em; margin-left: 1em;}', 'body {margin: 1em;}'),
array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 1em; margin-left: 2em;}', 'body {margin: 1em 2em;}'),
array('body {margin-top: 1em; margin-right: 2em; margin-bottom: 3em; margin-left: 2em;}', 'body {margin: 1em 2em 3em;}'),
);
}
/**
* @dataProvider createBackgroundShorthandProvider
* */
public function testCreateBackgroundShorthand($sCss, $sExpected) {
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
foreach ($oDoc->getAllDeclarationBlocks() as $oDeclaration) {
$oDeclaration->createBackgroundShorthand();
}
$this->assertSame(trim((string) $oDoc), $sExpected);
}
public function createBackgroundShorthandProvider() {
return array(
array('body {border: 1px;}', 'body {border: 1px;}'),
array('body {background-color: #f00;}', 'body {background: #f00;}'),
array('body {background-color: #f00;background-image: url(foobar.png);}', 'body {background: #f00 url("foobar.png");}'),
array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'),
array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;}', 'body {background: #f00 url("foobar.png") no-repeat;}'),
array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: center;}', 'body {background: #f00 url("foobar.png") no-repeat center;}'),
array('body {background-color: #f00;background-image: url(foobar.png);background-repeat: no-repeat;background-position: top left;}', 'body {background: #f00 url("foobar.png") no-repeat top left;}'),
);
}
public function testOverrideRules() {
$sCss = '.wrapper { left: 10px; text-align: left; }';
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
$oRule = new Rule('right');
$oRule->setValue('-10px');
$aContents = $oDoc->getContents();
$oWrapper = $aContents[0];
$this->assertCount(2, $oWrapper->getRules());
$aContents[0]->setRules(array($oRule));
$aRules = $oWrapper->getRules();
$this->assertCount(1, $aRules);
$this->assertEquals('right', $aRules[0]->getRule());
$this->assertEquals('-10px', $aRules[0]->getValue());
}
public function testRuleInsertion() {
$sCss = '.wrapper { left: 10px; text-align: left; }';
$oParser = new Parser($sCss);
$oDoc = $oParser->parse();
$aContents = $oDoc->getContents();
$oWrapper = $aContents[0];
$oFirst = $oWrapper->getRules('left');
$this->assertCount(1, $oFirst);
$oFirst = $oFirst[0];
$oSecond = $oWrapper->getRules('text-');
$this->assertCount(1, $oSecond);
$oSecond = $oSecond[0];
$oBefore = new Rule('left');
$oBefore->setValue(new Size(16, 'em'));
$oMiddle = new Rule('text-align');
$oMiddle->setValue(new Size(1));
$oAfter = new Rule('border-bottom-width');
$oAfter->setValue(new Size(1, 'px'));
$oWrapper->addRule($oAfter);
$oWrapper->addRule($oBefore, $oFirst);
$oWrapper->addRule($oMiddle, $oSecond);
$aRules = $oWrapper->getRules();
$this->assertSame($oBefore, $aRules[0]);
$this->assertSame($oFirst, $aRules[1]);
$this->assertSame($oMiddle, $aRules[2]);
$this->assertSame($oSecond, $aRules[3]);
$this->assertSame($oAfter, $aRules[4]);
$this->assertSame('.wrapper {left: 16em;left: 10px;text-align: 1;text-align: left;border-bottom-width: 1px;}', $oDoc->render());
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Sabberworm\CSS\RuleSet;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Settings;
class LenientParsingTest extends \PHPUnit_Framework_TestCase {
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
public function testFaultToleranceOff() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict());
$oParser->parse();
}
public function testFaultToleranceOn() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true));
$oResult = $oParser->parse();
$this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render());
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
public function testEndToken() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict());
$oParser->parse();
}
/**
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
*/
public function testEndToken2() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->beStrict());
$oParser->parse();
}
public function testEndTokenPositive() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true));
$oResult = $oParser->parse();
$this->assertSame("", $oResult->render());
}
public function testEndToken2Positive() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-end-token-2.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true));
$oResult = $oParser->parse();
$this->assertSame('#home .bg-layout {background-image: url("/bundles/main/img/bg1.png?5");}', $oResult->render());
}
public function testLocaleTrap() {
setlocale(LC_ALL, "pt_PT", "no");
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "-fault-tolerance.css";
$oParser = new Parser(file_get_contents($sFile), Settings::create()->withLenientParsing(true));
$oResult = $oParser->parse();
$this->assertSame('.test1 {}'."\n".'.test2 {hello: 2.2;hello: 2000000000000.2;}'."\n".'#test {}'."\n".'#test2 {help: none;}', $oResult->render());
}
public function testCaseInsensitivity() {
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "case-insensitivity.css";
$oParser = new Parser(file_get_contents($sFile));
$oResult = $oParser->parse();
$this->assertSame('@charset "utf-8";
@import url("test.css");
@media screen {}
#myid {case: insensitive !important;frequency: 30Hz;font-size: 1em;color: #ff0;color: hsl(40,40%,30%);font-family: Arial;}', $oResult->render());
}
}

View File

@@ -0,0 +1,10 @@
<?php
spl_autoload_register(function($class)
{
$file = __DIR__.'/../lib/'.strtr($class, '\\', '/').'.php';
if (file_exists($file)) {
require $file;
return true;
}
});

View File

@@ -0,0 +1 @@
div { width: calc(50% -8px); }

View File

@@ -0,0 +1,5 @@
#id {
prop: var(--val);
}
@charset 'utf-16';

View File

@@ -0,0 +1,3 @@
@media print {
@charset 'utf-16';
}

View File

@@ -0,0 +1 @@
.test { grid-template-columns: [] 100px; }

View File

@@ -0,0 +1 @@
#home .bg-layout { background-image: url(/bundles/main/img/bg1.png?5);};

View File

@@ -0,0 +1 @@
/* Test comment

View File

@@ -0,0 +1,15 @@
.test1 {
//gaga: hello;
}
.test2 {
*hello: 1;
hello: 2.2;
hello: 2000000000000.2;
}
#test {
#hello: 1}
#test2 {
help: none;

View File

@@ -0,0 +1,9 @@
.some[selectors-may='contain-a-{'] {
}
@media only screen and (min-width: 200px) {
.test {
prop: val;
}
}

View File

@@ -0,0 +1,10 @@
@charset "utf-8";
@font-face {
font-family: "CrassRoots";
src: url("../media/cr.ttf")
}
html, body {
font-size: 1.6em
}

View File

@@ -0,0 +1,5 @@
#header {
margin: 10px 2em 1cm 2%;
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
color: red !important;
}

View File

@@ -0,0 +1,57 @@
@charset "utf-8";
@font-face {
font-family: "CrassRoots";
src: url("../media/cr.ttf")
}
html, body {
font-size: -0.6em
}
@keyframes mymove {
from { top: 0px; }
to { top: 200px; }
}
@-moz-keyframes some-move {
from { top: 0px; }
to { top: 200px; }
}
@supports ( (perspective: 10px) or (-moz-perspective: 10px) or (-webkit-perspective: 10px) or (-ms-perspective: 10px) or (-o-perspective: 10px) ) {
body {
font-family: 'Helvetica';
}
}
@page :pseudo-class {
margin:2in;
}
@-moz-document url(http://www.w3.org/),
url-prefix(http://www.w3.org/Style/),
domain(mozilla.org),
regexp("https:.*") {
/* CSS rules here apply to:
+ The page "http://www.w3.org/".
+ Any page whose URL begins with "http://www.w3.org/Style/"
+ Any page whose URL's host is "mozilla.org" or ends with
".mozilla.org"
+ Any page whose URL starts with "https:" */
/* make the above-mentioned pages really ugly */
body { color: purple; background: yellow; }
}
@media screen and (orientation: landscape) {
@-ms-viewport {
width: 1024px;
height: 768px;
}
/* CSS for landscape layout goes here */
}
@region-style #intro {
p { color: blue; }
}

View File

@@ -0,0 +1,4 @@
.test {
font-size: calc((3 * 4px) + -2px);
top: calc(200px - calc(20 * 3px));
}

View File

@@ -0,0 +1,6 @@
div { width: calc(100% / 4); }
div { margin-top: calc(-120% - 4px); }
div {
height: -webkit-calc(9/16 * 100%)!important;
width: -moz-calc((50px - 50%)*2);
}

View File

@@ -0,0 +1,15 @@
@CharSet "utf-8";
@IMPORT uRL(test.css);
@MEDIA screen {
}
#myid {
CaSe: insensitive !imPORTANT;
frequency: 30hz;
font-size: 1EM;
color: RGB(255, 255, 0);
color: hSL(40, 40%, 30%);
font-Family: Arial; /* The value needs to remain capitalized */
}

View File

@@ -0,0 +1,12 @@
#mine {
color: red;
border-color: rgb(10, 100, 230);
border-color: rgba(10, 100, 231, 0.3);
outline-color: #222;
background-color: #232323;
}
#yours {
background-color: hsl(220, 10%, 220%);
background-color: hsla(220, 10%, 220%, 0.3);
}

View File

@@ -0,0 +1,17 @@
/**
* Comments Hell.
*/
@import /* Number 1 */"some/url.css"/* Number 2 */ screen/* Number 3 */;
.foo, /* Number 4 */ #bar/* Number 5 */ {
background-color/* Number 6 */: #000/* Number 7 */;
}
@media /* Number 8 */screen /* Number 9 */{
/** Number 10 **/
#foo.bar {
/** Number 10b **/
position: absolute;/**/
}
}
/** Number 11 **/

View File

@@ -0,0 +1,6 @@
body {
font-size: 2em; font-family: Helvetica,Arial,sans-serif; font-weight: bold;
border-width: 2px; border-color: #999; border-style: dotted;
background-color: #fff; background-image: url('foobar.png'); background-repeat: repeat-y;
margin-top: 2px; margin-right: 3px; margin-bottom: 4px; margin-left: 5px;
}

View File

@@ -0,0 +1 @@
div.dokuwiki div.ajax_qsearch{position:absolute;right:237px;;width:200px;opacity:0.9;display:none;font-size:80%;line-height:1.2em;border:1px solid #8cacbb;background-color:#f7f9fa;text-align:left;padding:4px;}

View File

@@ -0,0 +1 @@
.test { grid-template-columns: [] 100px; }

View File

@@ -0,0 +1,7 @@
body {
font: italic 500 14px/1.618 "Trebuchet MS", Georgia, serif;
border: 2px solid #f0f;
background: #ccc url("/images/foo.png") no-repeat left top;
margin: 1em !important;
padding: 2px 6px 3px;
}

View File

@@ -0,0 +1,21 @@
div.main { background-image: linear-gradient(#000, #fff) }
.collapser::before,
.collapser::-moz-before,
.collapser::-webkit-before {
content: "»";
font-size: 1.2em;
margin-right: .2em;
-moz-transition-property: -moz-transform;
-moz-transition-duration: .2s;
-moz-transform-origin: center 60%;
}
.collapser.expanded::before,
.collapser.expanded::-moz-before,
.collapser.expanded::-webkit-before { -moz-transform: rotate(90deg) }
.collapser + * {
height: 0;
overflow: hidden;
-moz-transition-property: height;
-moz-transition-duration: .3s;
}
.collapser.expanded + * { height: auto }

View File

@@ -0,0 +1,2 @@
div { grid-template-columns: [ linename ] 100px; }
span { grid-template-columns: [ linename1 linename2 ] 100px; }

View File

@@ -0,0 +1,2 @@
div { background: #1234; }
div { background: #11223344; }

View File

@@ -0,0 +1,9 @@
p {
padding-right: .75rem \9;
background-image: none \9;
color:red\9\0;
background-color:red \9 \0;
background-color:red \9 \0 !important;
content: "red \9\0";
content: "red\0abc";
}

View File

@@ -0,0 +1,6 @@
.nav-thumb-wrapper:hover img, a.activeSlide img {
filter: alpha(opacity=100);
-moz-opacity: 1;
-khtml-opacity: 1;
opacity: 1;
}

View File

@@ -0,0 +1,8 @@
div.rating-cancel,div.star-rating{float:left;width:17px;height:15px;text-indent:-999em;cursor:pointer;display:block;background:transparent;overflow:hidden}
div.rating-cancel,div.rating-cancel a{background:url(images/delete.gif) no-repeat 0 -16px}
div.star-rating,div.star-rating a{background:url(images/star.gif) no-repeat 0 0px}
div.rating-cancel a,div.star-rating a{display:block;width:16px;height:100%;background-position:0 0px;border:0}
div.star-rating-on a{background-position:0 -16px!important}
div.star-rating-hover a{background-position:0 -32px}
div.star-rating-readonly a{cursor:default !important}
div.star-rating{background:transparent!important; overflow:hidden!important}

View File

@@ -0,0 +1,3 @@
test {
background: -webkit-gradient(linear, 0 0, 0 bottom, from(#006cad), to(hsl(202, 100%, 49%)));
}

View File

@@ -0,0 +1,32 @@
@charset "utf-8"; /* line 1 */
@namespace "http://toto.example.org"; /* line 3 */
@font-face { /* line 5 */
font-family: "CrassRoots";
src: url("http://example.com/media/cr.ttf") /* line 7 */
}
#header { /* line 11 */
margin: 10px 2em 1cm 2%;
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
color: red !important;
}
@keyframes mymove { /* line 17 */
from { top: 0px; } /* line 18 */
to { top: 200px; } /* line 20 */
}
@IMPORT uRL(test.css); /* line 23 */
body {
background: #FFFFFF url("http://somesite.com/images/someimage.gif") repeat top center; /* line 25 */
color: rgb( /* line 27 */
233, /* line 28 */
100, /* line 29 */
450 /* line 30 */
);
}

View File

@@ -0,0 +1,4 @@
div {
display: inline-block;
display:
}

View File

@@ -0,0 +1 @@
.test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);}

View File

@@ -0,0 +1,18 @@
/* From the spec at http://www.w3.org/TR/css3-namespace/ */
@namespace toto "http://toto.example.org";
@namespace "http://example.com/foo";
/* From an introduction at http://www.blooberry.com/indexdot/css/syntax/atrules/namespace.htm */
@namespace foo url("http://www.example.com/");
@namespace foo url('http://www.example.com/');
foo|test {
gaga: 1;
}
|test {
gaga: 2;
}

View File

@@ -0,0 +1,17 @@
html {
some: -test(val1);
}
html {
some-other: -test(val1);
}
@media screen {
html {
some: -test(val2);
}
}
#unrelated {
other: yes;
}

View File

@@ -0,0 +1,4 @@
.test {
font: 12px/1.5 Verdana, Arial, sans-serif;
border-radius: 5px 10px 5px 10px / 10px 5px 10px 5px;
}

View File

@@ -0,0 +1,7 @@
#test .help,
#file,
.help:hover,
li.green,
ol li::before {
font-family: Helvetica;
}

View File

@@ -0,0 +1,2 @@
div { width: 200px; }

View File

@@ -0,0 +1,3 @@
@font-face {
unicode-range: U+0100-024F, U+0259, U+1E??-2EFF, U+202F;
}

View File

@@ -0,0 +1,12 @@
.test-1 { content: "\20"; } /* Same as " " */
.test-2 { content: "\E9"; } /* Same as "é" */
.test-3 { content: "\0020"; } /* Same as " " */
.test-5 { content: "\6C34" } /* Same as "水" */
.test-6 { content: "\00A5" } /* Same as "¥" */
.test-7 { content: '\a' } /* Same as "\A" (Newline) */
.test-8 { content: "\"\22" } /* Same as "\"\"" */
.test-9 { content: "\"\27" } /* Same as ""\"\'"" */
.test-10 { content: "\'\\" } /* Same as "'\" */
.test-11 { content: "\test" } /* Same as "test" */
.test-4 { content: "\1D11E" } /* Beyond the Basic Multilingual Plane */

View File

@@ -0,0 +1,18 @@
button,input,checkbox,textarea {
outline: 0;
margin: 0;
}
@media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (-o-min-device-pixel-ratio:3/@media all and (orientation:portrait) {
#wrapper {
max-width:640px;
margin: 0 auto;
}
}
@media all and (orientation: landscape) {
#wrapper {
max-width:640px;
margin: 0 auto;
}
}

View File

@@ -0,0 +1,3 @@
}}}.blue{
background: #00F;
}

View File

@@ -0,0 +1,4 @@
body { background: #FFFFFF url("http://somesite.com/images/someimage.gif") repeat top center; }
body {
background-url: url("http://somesite.com/images/someimage.gif");
}

View File

@@ -0,0 +1,14 @@
#header {
margin: 10px 2em 1cm 2%;
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
font-size: 10px;
color: red !important;
background-color: green;
background-color: rgba(0,128,0,0.7);
frequency: 30Hz;
}
body {
color: green;
font: 75% "Lucida Grande", "Trebuchet MS", Verdana, sans-serif;
}

View File

@@ -0,0 +1 @@
.test { background:-webkit-linear-gradient(top right, white, black)}

View File

@@ -0,0 +1,3 @@
.test {
background-image : url ( 4px ) ;
}

View File

@@ -0,0 +1 @@
<phpunit bootstrap="bootstrap.php"></phpunit>

View File

@@ -0,0 +1,20 @@
#!/usr/bin/env php
<?php
require_once(dirname(__FILE__).'/bootstrap.php');
$sSource = file_get_contents('php://stdin');
$oParser = new Sabberworm\CSS\Parser($sSource);
$oDoc = $oParser->parse();
echo "\n".'#### Input'."\n\n```css\n";
print $sSource;
echo "\n```\n\n".'#### Structure (`var_dump()`)'."\n\n```php\n";
var_dump($oDoc);
echo "\n```\n\n".'#### Output (`render()`)'."\n\n```css\n";
print $oDoc->render();
echo "\n```\n";