sabberworm / php-css-parser
Parser for CSS Files written in PHP
Installs: 123 341 566
Dependents: 70
Suggesters: 1
Security: 1
Stars: 1 763
Watchers: 27
Forks: 148
Open Issues: 117
Requires
- php: ^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
- ext-iconv: *
Requires (Dev)
- phpunit/phpunit: 5.7.27 || 6.5.14 || 7.5.20 || 8.5.40
Suggests
- ext-mbstring: for parsing UTF-8 CSS
- dev-main / 9.0.x-dev
- v8.x-dev
- v8.7.0
- v8.6.0
- v8.5.2
- v8.5.1
- 8.5.0
- 8.4.0
- 8.3.1
- 8.3.0
- 8.2.1
- 8.2.0
- 8.1.1
- 8.1.0
- 8.0.1
- 8.0.0
- 7.0.4
- 7.0.3
- 7.0.2
- 7.0.1
- 7.0.0
- 6.0.2
- 6.0.1
- 6.0.0
- 5.2.1
- 5.2.0
- 5.1.3
- 5.1.2
- 5.1.1
- 5.1.0
- 5.0.9
- 5.0.8
- 5.0.7
- 5.0.6
- 5.0.5
- 5.0.4
- 5.0.3
- 5.0.2
- 5.0.1
- 5.0.0
- 4.0.1
- 4.0.0
- 3.0.1
- 3.0.0
- 2.0.1
- 2.0.0
- 1.0.1
- 1.0.0
- dev-task/phpunit-update
- dev-dependabot/composer/rector/rector-1.2.9
- dev-task/fixers
- dev-feature/automatic-mermaid
- dev-task/phpstan-level-2
- dev-cleanup/declaration-block-test
- dev-bugfix/style-value-strings
- dev-fix-set-get-charset
- dev-selector-parsing
- dev-refactor-parsing-logic
This package is auto-updated.
Last update: 2024-11-05 10:19:35 UTC
README
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;}
Class diagram
API and deprecation policy
Please have a look at our API and deprecation policy.
Contributing
Contributions in the form of bug reports, feature requests, or pull requests are more than welcome. 🙏 Please have a look at our contribution guidelines to learn more about how to contribute to PHP-CSS-Parser.
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. - ziegenberg for general housekeeping and cleanup.
- 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 continuous integration (CI) checks for this project (including unit tests),
- run
composer install
to install the development dependencies managed with Composer; - run
phive install
to install the development dependencies managed with PHIVE; - run
composer ci
to run all static and dynamic CI checks.
Details of other Composer scripts available (e.g. to run one specific CI check) are provided with composer list
.