Skip to content
/ css Public

A CSS parser and minifier and sourcemap generator written in PHP

License

Notifications You must be signed in to change notification settings

tbela99/css

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CSS (A CSS parser and minifier written in PHP)


CI Current version Packagist Documentation Known Vulnerabilities

A CSS parser, beautifier and minifier written in PHP. It supports the following features

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

Installation

install using Composer

PHP version >= 8.0

$ composer require tbela99/css

PHP version >= 5.6

$ composer require "tbela99/css:dev-php56-backport"

Requirements

  • PHP version >= 8.0 on master branch.
  • PHP version >= 5.6 supported in this branch
  • mbstring extension

Usage:

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);

Sourcemap generation

$renderer = new Renderer([
  'sourcemap' => true
  ]);

// call save and specify the file name
// generate sourcemap -> css/all.css.map
$renderer->save($element, 'css/all.css');

The CSS Query API

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)
 }
}

CSS Nesting

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 Api

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);

Build a CSS Document

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');

Performance

Utility methods

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 = []);

Manual parsing and rendering

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());

Parser Options

  • 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

Renderer Options

  • 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

Command line utility

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

Minify inline css

$ ./cli/css-parser 'a, div {display:none} b {}' -c
#
$ echo 'a, div {display:none} b {}' | ./cli/css-parser -c

Minify css file

$ ./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

Dump ast

$ ./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