Home

Awesome

PHP CSS Parser

Build Status Coverage Status

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:

Note: A CSSList can contain other CSSLists (and Imports as well as a Charset), while a RuleSet can only contain Rules.

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

Rules 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:

There is another abstract subclass of Value, ValueList: A ValueList represents a lists of Values, separated by some separation character (mostly ,, whitespace, or /).

There are two types of ValueLists:

Convenience methods

There are a few convenience methods on Document to ease finding, manipulating and deleting rules:

To-Do

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

<details> <summary><b>Structure (<code>var_dump()</code>)</b></summary>
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)
}

</details>

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

<details> <summary><b>Structure (<code>var_dump()</code>)</b></summary>
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)
}

</details>

Output (render())

#header {margin: 10px 2em 1cm 2%;font-family: Verdana,Helvetica,"Gill Sans",sans-serif;color: red !important;}

Class diagram

classDiagram
    direction LR

    %% Start of the part generated from the PHP code using tasuku43/mermaid-class-diagram

    class Renderable {
        <<interface>>
    }
    class DeclarationBlock {
    }
    class RuleSet {
        <<abstract>>
    }
    class AtRuleSet {
    }
    class KeyframeSelector {
    }
    class AtRule {
        <<interface>>
    }
    class Charset {
    }
    class Import {
    }
    class Selector {
    }
    class CSSNamespace {
    }
    class Settings {
    }
    class Rule {
    }
    class Parser {
    }
    class OutputFormatter {
    }
    class OutputFormat {
    }
    class OutputException {
    }
    class UnexpectedEOFException {
    }
    class SourceException {
    }
    class UnexpectedTokenException {
    }
    class ParserState {
    }
    class Anchor {
    }
    class CSSBlockList {
        <<abstract>>
    }
    class Document {
    }
    class CSSList {
        <<abstract>>
    }
    class KeyFrame {
    }
    class AtRuleBlockList {
    }
    class Color {
    }
    class URL {
    }
    class CalcRuleValueList {
    }
    class ValueList {
        <<abstract>>
    }
    class CalcFunction {
    }
    class LineName {
    }
    class Value {
        <<abstract>>
    }
    class Size {
    }
    class CSSString {
    }
    class PrimitiveValue {
        <<abstract>>
    }
    class CSSFunction {
    }
    class RuleValueList {
    }
    class Commentable {
        <<interface>>
    }
    class Comment {
    }

    RuleSet <|-- DeclarationBlock: inheritance
    Renderable <|.. RuleSet: realization
    Commentable <|.. RuleSet: realization
    RuleSet <|-- AtRuleSet: inheritance
    AtRule <|.. AtRuleSet: realization
    Selector <|-- KeyframeSelector: inheritance
    Renderable <|-- AtRule: inheritance
    Commentable <|-- AtRule: inheritance
    AtRule <|.. Charset: realization
    AtRule <|.. Import: realization
    AtRule <|.. CSSNamespace: realization
    Renderable <|.. Rule: realization
    Commentable <|.. Rule: realization
    SourceException <|-- OutputException: inheritance
    UnexpectedTokenException <|-- UnexpectedEOFException: inheritance
    Exception <|-- SourceException: inheritance
    SourceException <|-- UnexpectedTokenException: inheritance
    CSSList <|-- CSSBlockList: inheritance
    CSSBlockList <|-- Document: inheritance
    Renderable <|.. CSSList: realization
    Commentable <|.. CSSList: realization
    CSSList <|-- KeyFrame: inheritance
    AtRule <|.. KeyFrame: realization
    CSSBlockList <|-- AtRuleBlockList: inheritance
    AtRule <|.. AtRuleBlockList: realization
    CSSFunction <|-- Color: inheritance
    PrimitiveValue <|-- URL: inheritance
    RuleValueList <|-- CalcRuleValueList: inheritance
    Value <|-- ValueList: inheritance
    CSSFunction <|-- CalcFunction: inheritance
    ValueList <|-- LineName: inheritance
    Renderable <|.. Value: realization
    PrimitiveValue <|-- Size: inheritance
    PrimitiveValue <|-- CSSString: inheritance
    Value <|-- PrimitiveValue: inheritance
    ValueList <|-- CSSFunction: inheritance
    ValueList <|-- RuleValueList: inheritance
    Renderable <|.. Comment: realization

    %% end of the generated part


    Anchor --> "1" ParserState : oParserState
    CSSList --> "*" CSSList : aContents
    CSSList --> "*" Charset : aContents
    CSSList --> "*" Comment : aComments
    CSSList --> "*" Import : aContents
    CSSList --> "*" RuleSet : aContents
    CSSNamespace --> "*" Comment : aComments
    Charset --> "*" Comment : aComments
    Charset --> "1" CSSString : oCharset
    DeclarationBlock --> "*" Selector : aSelectors
    Import --> "*" Comment : aComments
    OutputFormat --> "1" OutputFormat : oNextLevelFormat
    OutputFormat --> "1" OutputFormatter : oFormatter
    OutputFormatter --> "1" OutputFormat : oFormat
    Parser --> "1" ParserState : oParserState
    ParserState --> "1" Settings : oParserSettings
    Rule --> "*" Comment : aComments
    Rule --> "1" RuleValueList : mValue
    RuleSet --> "*" Comment : aComments
    RuleSet --> "*" Rule : aRules
    URL --> "1" CSSString : oURL
    ValueList --> "*" Value : aComponents

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. :pray: Please have a look at our contribution guidelines to learn more about how to contribute to PHP-CSS-Parser.

Contributors/Thanks to

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

Details of other Composer scripts available (e.g. to run one specific CI check) are provided with composer list.