2023-09-21 09:50:32 +00:00

633 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PHP CSS Parser
[![Build Status](https://github.com/sabberworm/PHP-CSS-Parser/workflows/CI/badge.svg?branch=master)](https://github.com/sabberworm/PHP-CSS-Parser/actions/)
A Parser for CSS Files written in PHP. Allows extraction of CSS files into a data structure, manipulation of said structure and output as (optimized) CSS.
## Usage
### Installation using Composer
```bash
composer 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($css);
```
To read a file, for example, youd do the following:
```php
$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
$cssDocument = $parser->parse();
```
The resulting CSS document structure can be manipulated prior to being output.
### 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
$settings = \Sabberworm\CSS\Settings::create()
->withDefaultCharset('windows-1252');
$parser = new \Sabberworm\CSS\Parser($css, $settings);
```
#### Strict parsing
To have the parser choke on invalid rules, supply a thusly configured `\Sabberworm\CSS\Settings` object:
```php
$parser = new \Sabberworm\CSS\Parser(
file_get_contents('somefile.css'),
\Sabberworm\CSS\Settings::create()->beStrict()
);
```
#### Disable multibyte functions
To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of `mb_*` functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still its not recommended using this with input you have no control over as its not thoroughly covered by test cases.
```php
$settings = \Sabberworm\CSS\Settings::create()->withMultibyteSupport(false);
$parser = new \Sabberworm\CSS\Parser($css, $settings);
```
### Manipulation
The resulting data structure consists mainly of five basic types: `CSSList`, `RuleSet`, `Rule`, `Selector` and `Value`. There are two additional types used: `Import` and `Charset`, which you 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 an output device matching the contained media query.
To access the items stored in a `CSSList` like the document you got back when calling `$parser->parse()` , use `getContents()`, then iterate over that collection and use instanceof to check whether 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 $rule)`, `getRules()` and `removeRule($rule)` (which accepts either a `Rule` instance or a rule name; optionally suffixed by a dash to remove all related rules).
#### Rule
`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($type)`, `removeAttributesOfType($type)`)
* Real multibyte support. Currently, only multibyte charsets whose first 255 code points take up only one byte and are identical with ASCII are supported (yes, UTF-8 fits this description).
* Named color support (using `Color` instead of an anonymous string literal)
## Use cases
### Use `Parser` to prepend an ID to all selectors
```php
$myId = "#my_id";
$parser = new \Sabberworm\CSS\Parser($css);
$cssDocument = $parser->parse();
foreach ($cssDocument->getAllDeclarationBlocks() as $block) {
foreach ($block->getSelectors() as $selector) {
// Loop over all selector parts (the comma-separated strings in a
// selector) and prepend the ID.
$selector->setSelector($myId.' '.$selector->getSelector());
}
}
```
### Shrink all absolute sizes to half
```php
$parser = new \Sabberworm\CSS\Parser($css);
$cssDocument = $parser->parse();
foreach ($cssDocument->getAllValues() as $value) {
if ($value instanceof CSSSize && !$value->isRelative()) {
$value->setSize($value->getSize() / 2);
}
}
```
### Remove unwanted rules
```php
$parser = new \Sabberworm\CSS\Parser($css);
$cssDocument = $parser->parse();
foreach($cssDocument->getAllRuleSets() as $oRuleSet) {
// Note that the added dash will make this remove all rules starting with
// `font-` (like `font-size`, `font-weight`, etc.) as well as a potential
// `font-rule`.
$oRuleSet->removeRule('font-');
$oRuleSet->removeRule('cursor');
}
```
### Output
To output the entire CSS document into a variable, just use `->render()`:
```php
$parser = new \Sabberworm\CSS\Parser(file_get_contents('somefile.css'));
$cssDocument = $parser->parse();
print $cssDocument->render();
```
If you want to format the output, pass an instance of type `\Sabberworm\CSS\OutputFormat`:
```php
$format = \Sabberworm\CSS\OutputFormat::create()
->indentWithSpaces(4)->setSpaceBetweenRules("\n");
print $cssDocument->render($format);
```
Or use one of the predefined formats:
```php
print $cssDocument->render(Sabberworm\CSS\OutputFormat::createPretty());
print $cssDocument->render(Sabberworm\CSS\OutputFormat::createCompact());
```
To see what you can do with output formatting, look at the tests in `tests/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
* [oliverklee](https://github.com/oliverklee) for lots of refactorings, code modernizations and CI integrations
* [raxbg](https://github.com/raxbg) for contributions to parse `calc`, grid lines, and various bugfixes.
* [westonruter](https://github.com/westonruter) for bugfixes and improvements.
* [FMCorz](https://github.com/FMCorz) for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode).
* [Lullabot](https://github.com/Lullabot) for a patch that allows to know the line number for each parsed token.
* [ju1ius](https://github.com/ju1ius) for the specificity parsing code and the ability to expand/compact shorthand properties.
* [ossinkine](https://github.com/ossinkine) for a 150 time performance boost.
* [GaryJones](https://github.com/GaryJones) for lots of input and [https://css-specificity.info/](https://css-specificity.info/).
* [docteurklein](https://github.com/docteurklein) for output formatting and `CSSList->remove()` inspiration.
* [nicolopignatelli](https://github.com/nicolopignatelli) for PSR-0 compatibility.
* [diegoembarcadero](https://github.com/diegoembarcadero) for keyframe at-rule parsing.
* [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/bin/phpunit`.