Skip to content

Commit

Permalink
Initial code.
Browse files Browse the repository at this point in the history
  • Loading branch information
Jelle-S committed Dec 26, 2016
1 parent 1ea22e1 commit 29b1c7e
Show file tree
Hide file tree
Showing 7 changed files with 529 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
language: php
php:
- "5.4"
- "5.3"
- "5.5"
- "5.6"
- "7.0"
- "nightly"
- hhvm
script: phpunit .
sudo: false

21 changes: 21 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "jelle-s/cssoptimizer",
"description": "Optimizes & minifies css files or strings.",
"type": "library",
"require": {
"sabberworm/php-css-parser": "^8.1",
"nicmart/Tree": "^0.2.7",
"jelle-s/arraykeycombiner": "^1.0"
},
"license": "GPL-3.0+",
"authors": [
{
"name": "Jelle Sebreghts",
"email": "[email protected]"
}
],
"minimum-stability": "stable",
"autoload": {
"psr-4": {"Jelle_S\\CssOptimizer\\": "src/"}
}
}
93 changes: 93 additions & 0 deletions example/example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

include_once '../vendor/autoload.php';
$css = <<<EOT
div {
display:block;
background: transparent;
}
div .black-with-border {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 0;
}
div .black-with-border-and-margin {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 5px 0;
}
h1 {
font-weight: bold;
text-decoration: underline;
padding: 5px;
color: #d3d3d3;
margin: 0;
text-align: left;
}
h2 {
font-weight: bold;
text-decoration: underline;
padding: 5px;
color: #d3d3d3;
margin: 0;
text-align: right;
}
h3 {
font-weight: bold;
text-decoration: underline;
padding: 5px;
color: #d3d3d3;
margin: 3px;
text-align: right;
}
@media only screen and (min-width:16.5em) {
div .black-with-border {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 0;
}
div .black-with-border-and-margin {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 5px 0;
}
}
@media only screen and (min-width:36em) {
div .black-with-border {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 0;
}
div .black-with-border-and-margin {
border: 1px solid black;
color: black;
font-weight: bold;
text-decoration: underline;
margin: 5px 0;
}
}
EOT;
$optimizer = new Jelle_S\CssOptimizer\CssOptimizer($css, 3);
file_put_contents('style.min.css', $optimizer->renderMinifiedCSS());


$optimizer = new Jelle_S\CssOptimizer\CssOptimizer($css, 3);
file_put_contents('style.scss', $optimizer->renderSCSS());
1 change: 1 addition & 0 deletions example/style.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions example/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
div{display:block;background:transparent none repeat 0% 0% scroll;.black-with-border{color:black;font-weight:bold;text-decoration:underline;margin:0;border:1px black;}.black-with-border-and-margin{color:black;font-weight:bold;text-decoration:underline;margin:5px 0;border:1px black;}}h1{font-weight:bold;text-decoration:underline;color:#d3d3d3;text-align:left;margin:0;padding:5px;}h2{font-weight:bold;text-decoration:underline;color:#d3d3d3;text-align:right;margin:0;padding:5px;}h3{font-weight:bold;text-decoration:underline;color:#d3d3d3;text-align:right;margin:3px;padding:5px;}@media only screen and (min-width:16.5em){div{.black-with-border{color:black;font-weight:bold;text-decoration:underline;margin:0;border:1px black;}.black-with-border-and-margin{color:black;font-weight:bold;text-decoration:underline;margin:5px 0;border:1px black;}}}@media only screen and (min-width:36em){div{.black-with-border{color:black;font-weight:bold;text-decoration:underline;margin:0;border:1px black;}.black-with-border-and-margin{color:black;font-weight:bold;text-decoration:underline;margin:5px 0;border:1px black;}}}
236 changes: 236 additions & 0 deletions src/CssOptimizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
<?php

namespace Jelle_S\CssOptimizer;

use Jelle_S\CssOptimizer\Tree\CssNode;
use Jelle_S\Util\Combiner\ArrayKeyCombiner;
use Sabberworm\CSS\CSSList\CSSList;
use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parser;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use SebastianBergmann\CodeCoverage\Exception;

/**
* Optimize css by minifying and combining selectors.
*
* @author Jelle Sebreghts
*/
class CssOptimizer {

/**
* The css parser.
*
* @var \Sabberworm\CSS\Parser
*/
protected $cssParser;

/**
* Selector tree.
*
* @var \Jelle_S\CssOptimizer\Tree\CssNode
*/
protected $selectorTree;

/**
* The maximum number of combinations to try for finding intersections.
*
* @var int
*/
protected $combinationLimit;

/**
* The threshold. The minimum size of the intersections to search for.
*
* @var int
*/
protected $threshold;

/**
* Creates a CssOptimizer.
*
* @param string $css
* The raw css or path to the css file to optimize.
* @param int $threshold
* The threshold. The minimum size of the intersections to search for.
* @param int $combinationLimit
* The maximum number of combinations to try for finding intersections.
*/
public function __construct($css, $threshold = 5, $combinationLimit = NULL) {
$this->threshold = $threshold;
if (is_file($css)) {
$css = file_get_contents($css);
}
// Try to keep some sort of sane default based on the amount of data we get.
if (is_null($combinationLimit)) {
$combinationLimit = round(PHP_INT_MAX / pow((strlen($css) * 10) / $this->threshold, 3));
}
$this->combinationLimit = $combinationLimit;
$this->cssParser = new Parser($css);
$this->selectorTree = new CssNode();
}

/**
* Optimizes the given css using the contructor parameters.
*/
protected function optimize() {
$this->buildSelectorTree();
$flattened = $this->asFlattenedArray();
$arrays_to_optimize = array();
foreach ($flattened as $key => $flat) {
if (is_array(reset($flat))) {
$arrays_to_optimize[$key] = $flat;
}
else {
$arrays_to_optimize['_main'][$key] = $flat;
}
}
$optimized_arrs = [];
foreach ($arrays_to_optimize as $key => $arrs) {
$optimized_arrs[$key] = $this->optimizeArrays($arrs);
}
if (isset($optimized_arrs['_main'])) {
$optimized_arrs += $optimized_arrs['_main'];
unset($optimized_arrs['_main']);
}
$css = '';
foreach ($optimized_arrs as $key => $arr) {
$css .= $key . '{';
foreach ($arr as $k => $a) {
if (is_array($a)) {
$css .= $k . '{';
foreach ($a as $prop => $val) {
$css .= $prop . ':' . $val . ';';
}
$css .= '}';
}
else {
$css .= $k . ':' . $a . ';';
}
}
$css .= '}';
}
$this->cssParser = new Parser($css);
}

/**
* Helper function to optimize array representing css.
*
* @param array $arrs
* An array of arrays representing css blocks.
*
* @return array
* The optimized arrays.
*/
protected function optimizeArrays($arrs) {
$combiner = new ArrayKeyCombiner($arrs, $this->threshold, $this->combinationLimit);
return $combiner->combine();
}

/**
* Get the current css tree as a flattened array
*
* @return array
* The flattened array.
*/
protected function asFlattenedArray() {
return $this->selectorTree->asFlattenedArray();
}

/**
* Render the current css tree.
*
* @return string
* The current css tree rendered as a css string.
*/
public function renderMinifiedCSS() {
$this->optimize();
$doc = $this->cssParser->parse();
$doc->createShorthands();
return $doc->render(OutputFormat::createCompact());
}

/**
* Render the current css tree as SCSS.
*
* @return string
* The current css tree rendered as a scss string.
*/
public function renderSCSS() {
$this->buildSelectorTree();
return $this->selectorTree->renderSCSS();
}

/**
* Build the css tree for a list.
*
* @param \Sabberworm\CSS\CSSList\CSSList $list
* The css list to create the tree for.
* @param \Jelle_S\CssOptimizer\Tree\CssNode $parent
* The parent to attach the built tree to.
*
* @throws Exception
* When we encounter an unsupported css element.
*/
protected function buildSelectorTree(CSSList $list = NULL, CssNode $parent = NULL) {
if (is_null($list)) {
$list = $this->cssParser->parse();
}
if (is_null($parent)) {
$parent = $this->selectorTree;
}
foreach ($list->getContents() as $content) {
switch (TRUE) {
case $content instanceof CSSList:
if ($content instanceof AtRule) {
$child = $parent->find(["@{$content->atRuleName()} {$content->atRuleArgs()}"]);
$child->setBlock(TRUE);
$this->buildSelectorTree($content, $child);
}
else {
throw new Exception('Not supported');
}
break;
case $content instanceof RuleSet:
$this->addRuleSetToSelectorTree($content, $parent);
break;
}
}
}

/**
* Add a css rule set to the css tree.
*
* @param \Sabberworm\CSS\RuleSet\RuleSet $ruleset
* The rule set to add.
* @param \Jelle_S\CssOptimizer\Tree\CssNode $parent
* The parent to add it to.
*
* @throws \Exception
* When we encounter an unsupported ruleset.
*/
protected function addRuleSetToSelectorTree(RuleSet $ruleset, CssNode $parent) {
if ($ruleset instanceof AtRuleSet) {
$child = $parent->find(["@{$ruleset->atRuleName()} {$ruleset->atRuleArgs()}"]);
$child->setBlock(TRUE);
$child->addData($ruleset);
}
else if ($ruleset instanceof DeclarationBlock) {
$ruleset->expandShorthands();
foreach ($ruleset->getSelectors() as $selector) {
// Remove spaces around:
// - child selector: '>'
// - sibling selector: '~'
// - adjacent selector: '+'
$selector = preg_replace("/\s*([\>\~\+])\s*/", "$1", (string) $selector);
$child = $parent->find(array_filter(explode(' ', $selector)));
$child->addData($ruleset);
}
}
else {
throw new Exception('Not supported');
}
}
}
Loading

0 comments on commit 29b1c7e

Please sign in to comment.