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
composer require sabberworm/php-css-parser
Extraction
To use the CSS Parser, create a new instance. The constructor takes the following form:
new \Sabberworm\CSS\Parser($css);
To read a file, for example, you’d do the following:
$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 will only be used if the CSS file does not contain an @charset
declaration. UTF-8 is the default, so you won’t have to create a settings object at all if you don’t intend to change that.
$settings = \Sabberworm\CSS\Settings::create()
->withDefaultCharset('windows-1252');
$parser = new \Sabberworm\CSS\Parser($css, $settings);
Strict parsing
To have the parser throw an exception when encountering invalid/unknown constructs (as opposed to trying to ignore them and carry on parsing), supply a thusly configured \Sabberworm\CSS\Settings
object:
$parser = new \Sabberworm\CSS\Parser(
file_get_contents('somefile.css'),
\Sabberworm\CSS\Settings::create()->beStrict()
);
Note that this will also disable a workaround for parsing the unquoted variant of the legacy IE-specific filter
rule.
Disable multibyte functions
To achieve faster parsing, you can choose to have PHP-CSS-Parser use regular string functions instead of mb_*
functions. This should work fine in most cases, even for UTF-8 files, as all the multibyte characters are in string literals. Still it’s not recommended using this with input you have no control over as it’s not thoroughly covered by test cases.
$settings = \Sabberworm\CSS\Settings::create()->withMultibyteSupport(false);
$parser = new \Sabberworm\CSS\Parser($css, $settings);
Manipulation
The resulting data structure consists mainly of five basic types: CSSList
, RuleSet
, Rule
, Selector
and Value
. There are two additional types used: Import
and Charset
, which you won’t use often.
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.
To access the items stored in a CSSList
– like the document you got back when calling $parser->parse()
–, use getContents()
, then iterate over that collection and use instanceof
to check whether you’re dealing with another CSSList
, a RuleSet
, a Import
or a Charset
.
To append a new item (selector, media query, etc.) to an existing CSSList
, construct it using the constructor for this class and use the append($oItem)
method.
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 for generic at-rules which are not covered by specific classes, i.e., not@import
,@charset
or@media
. A common example for this is@font-face
.DeclarationBlock
– aRuleSet
constrained by aSelector
; 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
or a rule name; optionally suffixed by a dash to remove all related rules).
Rule
Rule
s just have a string key (the rule) and 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 numericsize
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 inURL("")
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 multivalued rules likefont: bold 12px/3 Helvetica, Verdana, sans-serif;
(where the value would be a whitespace-separated list of the primitive valuebold
, 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 function’s arguments. Also handles equals-sign-separated argument lists likefilter: 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 the selectors are. Aliased asgetAllSelectors()
.getAllRuleSets()
– does what it says; no matter how deeply nested the rule sets are.getAllValues()
– finds allValue
objects insideRule
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
$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
$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
$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()
:
$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
:
$format = \Sabberworm\CSS\OutputFormat::create()
->indentWithSpaces(4)->setSpaceBetweenRules("\n");
print $cssDocument->render($format);
Or use one of the predefined formats:
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
@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())
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()
)
@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
#header {
margin: 10px 2em 1cm 2%;
font-family: Verdana, Helvetica, "Gill Sans", sans-serif;
color: red !important;
}
Structure (var_dump())
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()
)
#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;}
Contributors/Thanks to
- oliverklee for lots of refactorings, code modernizations and CI integrations
- raxbg for contributions to parse
calc
, grid lines, and various bugfixes. - westonruter for bugfixes and improvements.
- FMCorz for many patches and suggestions, for being able to parse comments and IE hacks (in lenient mode).
- Lullabot for a patch that allows to know the line number for each parsed token.
- ju1ius for the specificity parsing code and the ability to expand/compact shorthand properties.
- ossinkine for a 150 time performance boost.
- GaryJones for lots of input and https://css-specificity.info/.
- docteurklein for output formatting and
CSSList->remove()
inspiration. - nicolopignatelli for PSR-0 compatibility.
- diegoembarcadero for keyframe at-rule parsing.
- goetas for @namespace at-rule support.
- View full list
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
.