CSS (A CSS parser and minifier written in PHP)
A CSS parser, beautifier and minifier written in PHP. It supports the following features
- multibyte characters encoding
- sourcemap
- multiprocessing: process large CSS input very fast
- CSS Nesting module
- partially implemented CSS Syntax module level 3
- partial CSS validation
- CSS colors module level 4
- parse and render CSS
- optimize css:
- merge duplicate rules
- remove duplicate declarations
- remove empty rules
- compute css shorthand (margin, padding, outline, border-radius, font, background)
- process @import document to reduce the number of HTTP requests
- remove @charset directive
- query api with xpath like or class name syntax
- traverser api to transform the css and ast
- command line utility
install using Composer
$ composer require tbela99/css
$ composer require "tbela99/css:dev-php56-backport"
- PHP version >= 8.0 on master branch.
- PHP version >= 5.6 supported in this branch
- mbstring extension
h1 {
color: green;
color: blue;
color: black;
}
h1 {
color: #000;
color: aliceblue;
}
PHP Code
use \TBela\CSS\Parser;
$parser = new Parser();
$parser->setContent('
h1 {
color: green;
color: blue;
color: black;
}
h1 {
color: #000;
color: aliceblue;
}');
echo $parser->parse();
Result
h1 {
color: #f0f8ff;
}
Parse the css file and generate the AST
use \TBela\CSS\Parser;
use \TBela\CSS\Renderer;
$parser = new Parser($css);
$element = $parser->parse();
// append an existing css file
$parser->append('https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css');
// append css string
$parser->appendContent($css_string);
// pretty print css
$css = (string) $element;
// minified output
$renderer = new Renderer([
'compress' => true,
'convert_color' => 'hex',
'css_level' => 4,
'sourcemap' => true,
'allow_duplicate_declarations' => false
]);
// fast
$css = $renderer->renderAst($parser);
// or
$css = $renderer->renderAst($parser->getAst());
// slow
$css = $renderer->render($element);
// generate sourcemap -> css/all.css.map
$renderer->save($element, 'css/all.css');
// save as json
file_put_contents('style.json', json_encode($element));
Load the AST and generate css code
use \TBela\CSS\Renderer;
// fastest way to render css
$beautify = (new Renderer())->renderAst($parser->setContent($css)->getAst());
// or
$beautify = (new Renderer())->renderAst($parser->setContent($css));
// or
$css = (new Renderer())->renderAst(json_decode(file_get_contents('style.json')));
use \TBela\CSS\Renderer;
$ast = json_decode(file_get_contents('style.json'));
$renderer = new Renderer([
'convert_color' => true,
'compress' => true, // minify the output
'remove_empty_nodes' => true // remove empty css classes
]);
$css = $renderer->renderAst($ast);
$renderer = new Renderer([
'sourcemap' => true
]);
// call save and specify the file name
// generate sourcemap -> css/all.css.map
$renderer->save($element, 'css/all.css');
Example: get all background and background-image declarations that contain an image url
$element = Element::fromUrl('https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css');
foreach ($element->query('[@name=background][@value*="url("]|[@name=background-image][@value*="url("]') as $p) {
echo "$p\n";
}
result
.form-select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c
/svg%3e")
}
.form-check-input:checked[type=checkbox] {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/s
vg%3e")
}
...
Example: Extract Font-src declaration
CSS source
@font-face {
font-family: "Bitstream Vera Serif Bold";
src: url("/static/styles/libs/font-awesome/fonts/fontawesome-webfont.fdf491ce5ff5.woff");
}
body {
background-color: green;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
}
h1 {
color: #fff;
font-size: 50px;
font-family: Arial, Helvetica, sans-serif;
font-weight: bold;
}
@media print {
@font-face {
font-family: MaHelvetica;
src: local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf);
font-weight: bold;
}
body {
font-family: "Bitstream Vera Serif Bold", serif;
}
p {
font-size: 12px;
color: #000;
text-align: left;
}
@font-face {
font-family: Arial, MaHelvetica;
src: local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf);
font-weight: bold;
}
}
PHP source
use \TBela\CSS\Parser;
$parser = new Parser();
$parser->setContent($css);
$stylesheet = $parser->parse();
// get @font-face nodes by class names
$nodes = $stylesheet->queryByClassNames('@font-face, .foo .bar');
// or
// get all src properties in a @font-face rule
$nodes = $stylesheet->query('@font-face/src');
echo implode("\n", array_map('trim', $nodes));
result
@font-face {
src: url("/static/styles/libs/font-awesome/fonts/fontawesome-webfont.fdf491ce5ff5.woff");
}
@media print {
@font-face {
src: local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf);
}
}
@media print {
@font-face {
src: local("Helvetica Neue Bold"), local("HelveticaNeue-Bold"),
url(MgOpenModernaBold.ttf);
}
}
render optimized css
$stylesheet->setChildren(array_map(function ($node) { return $node->copy()->getRoot(); }, $nodes));
$stylesheet->deduplicate();
echo $stylesheet;
result
@font-face {
src: url(/static/styles/libs/font-awesome/fonts/fontawesome-webfont.fdf491ce5ff5.woff)
}
@media print {
@font-face {
src: local("Helvetica Neue Bold"), local(HelveticaNeue-Bold), url(MgOpenModernaBold.ttf)
}
}
table.colortable {
& td {
text-align:center;
&.c { text-transform:uppercase }
&:first-child, &:first-child + td { border:1px solid black }
}
& th {
text-align:center;
background:black;
color:white;
}
}
render CSS nesting
use TBela\CSS\Parser;
echo new Parser($css);
result
table.colortable {
& td {
text-align: center;
&.c {
text-transform: uppercase
}
&:first-child,
&:first-child+td {
border: 1px solid #000
}
}
& th {
text-align: center;
background: #000;
color: #fff
}
}
convert nesting CSS to older representation
use TBela\CSS\Parser;
use \TBela\CSS\Renderer;
$renderer = new Renderer( ['legacy_rendering' => true]);
echo $renderer->renderAst(new Parser($css));
result
table.colortable td {
text-align: center
}
table.colortable td.c {
text-transform: uppercase
}
table.colortable td:first-child,
table.colortable td:first-child+td {
border: 1px solid #000
}
table.colortable th {
text-align: center;
background: #000;
color: #fff
}
The traverser will iterate over all the nodes and process them with the callbacks provided. It will return a new tree Example using ast
use TBela\CSS\Ast\Traverser;
use TBela\CSS\Parser;
use TBela\CSS\Renderer;
$parser = (new Parser())->load('ast/media.css');
$traverser = new Traverser();
$renderer = new Renderer(['remove_empty_nodes' => true]);
$ast = $parser->getAst();
// remove @media print
$traverser->on('enter', function ($node) {
if ($node->type == 'AtRule' && $node->name == 'media' && $node->value == 'print') {
return Traverser::IGNORE_NODE;
}
});
$newAst = $traverser->traverse($ast);
echo $renderer->renderAst($newAst);
Example using an Element instance
use TBela\CSS\Ast\Traverser;
use TBela\CSS\Parser;
use TBela\CSS\Renderer;
$parser = (new Parser())->load('ast/media.css');
$traverser = new Traverser();
$renderer = new Renderer(['remove_empty_nodes' => true]);
$element = $parser->parse();
// remove @media print
$traverser->on('enter', function ($node) {
if ($node->type == 'AtRule' && $node->name == 'media' && $node->value == 'print') {
return Traverser::IGNORE_NODE;
}
});
$newElement = $traverser->traverse($element);
echo $renderer->renderAst($newElement);
use \TBela\CSS\Element\Stylesheet;
$stylesheet = new Stylesheet();
$rule = $stylesheet->addRule('div');
$rule->addDeclaration('background-color', 'white');
$rule->addDeclaration('color', 'black');
echo $stylesheet;
output
div {
background-color: #fff;
color: #000;
}
$media = $stylesheet->addAtRule('media', 'print');
$media->append($rule);
output
@media print {
div {
background-color: #fff;
color: #000;
}
}
$div = $stylesheet->addRule('div');
$div->addDeclaration('max-width', '100%');
$div->addDeclaration('border-width', '0px');
output
@media print {
div {
background-color: #fff;
color: #000;
}
}
div {
max-width: 100%;
border-width: 0;
}
$media->append($div);
output
@media print {
div {
background-color: #fff;
color: #000;
}
div {
max-width: 100%;
border-width: 0;
}
}
$stylesheet->insert($div, 0);
output
div {
max-width: 100%;
border-width: 0;
}
@media print {
div {
background-color: #fff;
color: #000;
}
}
Adding existing css
// append css string
$stylesheet->appendCss($css_string);
// append css file
$stylesheet->append('style/main.css');
// append url
$stylesheet->append('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/brands.min.css');
The renderer class provides utility methods to format css data
$css = \TBela\CSS\Renderer::fromFile($url_or_file, $renderOptions = [], $parseOptions = []);
#
$css = \TBela\CSS\Renderer::fromString($css, $renderOptions = [], $parseOptions = []);
parsing and rendering ast is 3x faster than parsing an element.
use \TBela\CSS\Element\Parser;
use \TBela\CSS\Element\Renderer;
$parser = new Parser($css);
// parse and render
echo (string) $parser;
// or render minified css
$renderer = new Renderer(['compress' => true]);
echo $renderer->renderAst($parser);
# or
echo $renderer->renderAst($parser->getAst());
# or
// slower - will build a stylesheet object
echo $renderer->render($parser->parse());
- flatten_import: process @import directive and import the content into the css document. default to false.
- allow_duplicate_rules: allow duplicated rules. By default, duplicate rules except @font-face are merged
- allow_duplicate_declarations: allow duplicated declarations in the same rule.
- capture_errors: silently capture parse error if true, otherwise throw a parse exception. Default to true
- remove_comments: remove comments.
- preserve_license: preserve comments starting with '/*!'
- compress: minify output, will also remove comments
- remove_empty_nodes: do not render empty css nodes
- compute_shorthand: compute shorthand declaration
- charset: preserve @charset. default to false
- glue: the line separator character. default to '\n'
- indent: character used to pad lines in css, default to a space character
- convert_color: convert colors to a format between hex, hsl, rgb, hwb and device-cmyk
- css_level: produce CSS color level 3 or 4. default to 4
- allow_duplicate_declarations: allow duplicate declarations.
- legacy_rendering: convert nesting css. default false
the command line utility is located at './cli/css-parser'
$ ./cli/css-parser -h
Usage:
$ css-parser [OPTIONS] [PARAMETERS]
-v, --version print version number
-h print help
--help print extended help
Parse options:
-e, --capture-errors ignore parse error
-f, --file input css file or url
-m, --flatten-import process @import
-I, --input-format input format: json (ast), serialize (PHP serialized ast)
-d, --parse-allow-duplicate-declarations allow duplicate declaration
-p, --parse-allow-duplicate-rules allow duplicate rule
-P, --parse-children-process maximum children process
-M, --parse-multi-processing enable multi-processing parser
Render options:
-a, --ast dump ast as JSON
-S, --charset remove @charset
-c, --compress minify output
-u, --compute-shorthand compute shorthand properties
-t, --convert-color convert colors
-l, --css-level css color module
-G, --legacy-rendering convert nested css syntax
-o, --output output file name
-F, --output-format output export format. string (css), json (ast), serialize (PHP serialized ast), json-array, serialize-array, requires --input-format
-L, --preserve-license preserve license comments
-C, --remove-comments remove comments
-E, --remove-empty-nodes remove empty nodes
-r, --render-allow-duplicate-declarations render duplicate declarations
-R, --render-multi-processing enable multi-processing renderer
-s, --sourcemap generate sourcemap, requires --file
$ ./cli/css-parser 'a, div {display:none} b {}' -c
#
$ echo 'a, div {display:none} b {}' | ./cli/css-parser -c
$ ./cli/css-parser -f nested.css -c
#
$ ./cli/css-parser -f 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/brands.min.css' -c
$ ./cli/css-parser -f nested.css -f 'https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.5.0/css/bootstrap.min.css' -c -a
#
$ ./cli/css-parser 'a, div {display:none} b {}' -c -a
#
$ echo 'a, div {display:none} b {}' | ./cli/css-parser -c -a
The full documentation can be found here
Thanks to Jetbrains for providing a free PhpStorm license
This was originally a PHP port of https://github.com/reworkcss/css