Skip to content

Instantly share code, notes, and snippets.

@mikewlange
Last active February 23, 2018 10:33
Show Gist options
  • Save mikewlange/81f0fd11748e2d144fecadfc47a05ead to your computer and use it in GitHub Desktop.
Save mikewlange/81f0fd11748e2d144fecadfc47a05ead to your computer and use it in GitHub Desktop.
<?php
/***************************************************************
* CSS REMOVE UNUSED RULES CLASS
* Run only in AMP pages
* Auther: Mike Lange
**************************************************************
HOW TO USE - BASIC:
$tmp = new AmpRemoveUnusedCss();
$tmp->process($htmlcode); //must be full htmlcode, with <style amp-custom> tag and the <body> content
echo $tmp->result();
HOW TO VIEW REPORT:
$tmp = new AmpRemoveUnusedCss(1); //set 1 or TRUE to get full report, or void or 0 or FALSE to get simple report
$tmp->process($htmlcode);
echo $tmp->report();
ONLY MINIFY CSS (can be used in no-AMP pages, too)
You also can just only minify CSS by calling (it removes useless white spaces, but it does not remove unused CSS rules):
$tmp = new AmpRemoveUnusedCss();
$css_minified = $tmp->minify($css);
TO-DO : process @media
**************************************************************/
class AmpRemoveUnusedCss
{
private $include_selectors = array('div','span','a','p','body','ul','li','nav'); //this tags will be ignored in process
private $rules_to_remove = array();
private $amphtml = "";
private $newline = "\n";
private $result = "";
private $stats = array();
private $fullstats = false;
function __construct($fullstats=false)
{
$this->fullstats = $fullstats;
}
public function process($html, $newline = "\n")
{
$this->amphtml = $html;
$this->newline = $newline;
if($this->fullstats)
$this->stats['source_length'] = strlen($html);
//collect style rules that are not in html code
$html = preg_replace_callback("#(<style amp-custom>)(.+)(</style>)#Usmi", array($this, '_amp_css_find_rules_callback'), $html);
foreach($this->rules_to_remove as $rule=>$remove)
{
if(!$remove)
unset($this->rules_to_remove[$rule]);
}
//remove only unused rules in css
$html = preg_replace_callback("#(<style amp-custom>)(.+)(</style>)#Usmi", array($this, '_amp_remove_css_callback'), $html);
$this->result = $html;
}
public function result()
{
return $this->result;
}
public function report($htmlformat=true)
{
$r = $htmlformat?"<pre><ul>":"";
$tag = $htmlformat?"<li>":"";
if(isset($this->stats['source_length']))
$r .= $tag."Source CSS length = ".$this->stats['source_length']."\n";
if(isset($this->stats['source_minified_length']))
$r .= $tag."After minify length = ".$this->stats['source_minified_length']."\n";
if(isset($this->stats['src_rules_to_remove']))
$r .= $tag."Removed rules by type = ".var_export($this->stats['src_rules_to_remove'], true)."\n";
if(isset($this->stats['num_rules_found']))
$r .= $tag . "Total rules in source = ".$this->stats['num_rules_found']."\n";
if(isset($this->stats['keywords_removed']))
$r .= $tag . "Keywords removed = ".$this->stats['keywords_removed']."\n";
if(isset($this->stats['lines_removed']))
$r .= $tag . "Complete CSS lines removed = ".$this->stats['lines_removed']."\n";
if(isset($this->stats['final_length']))
$r .= $tag . "Final length = ".$this->stats['final_length']."\n";
if(isset($this->stats['source_minified_length'])&&isset($this->stats['final_length']))
{
$orig = $this->stats['source_minified_length'];
$final = $this->stats['final_length'];
$r .= $tag . "A total of ".($orig-$final)." characters removed (". round(10000*$final/($orig+.00001))/100 ."%)\n";
}
if($this->fullstats && $this->rules_to_remove)
{
$r .= $tag . "CSS Rules that has been removed: \n " . implode("\n ", array_keys($this->rules_to_remove));
}
$r .= $htmlformat?"</ul></pre>":"";
return $r;
}
//1st STEP: FIND CSS RULES, by .class, by tag, and by #id
function _amp_css_find_rules_callback($matches)
{
$css = $matches[2];
$css = preg_replace("/\!important/", "", $css); //remove !important because is not allowed in AMP
$css = $this->minify( $css ); //we need minize here to get correct format
if($this->fullstats)
$this->stats['source_minified_length'] = strlen($css);
preg_match_all("#@[^{]+{([^{}]+{[^}]*})*}|([\.\#a-z][^{]*{[^}]*})#m", $css, $rules);
$selectors = array(
'cls'=> array('prefix'=>".", 'regex_css'=>"/\.([a-z][a-z0-9\-_]*)[^a-z0-9\-_]/Usim", 'find_in_html'=>"/<[^>]*\s+class=['\"]\s*(?:([^'\"]+)\b\s*)*['\"]/ism"),
'id'=> array('prefix'=>"#", 'regex_css'=>"/#([a-z][a-z0-9\-_]*)[^a-z0-9\-_]/im", 'find_in_html'=>"/<[^>]*\s+id=['\"]([^'\"]+)['\"]/im"),
'tags'=> array('prefix'=>"", 'regex_css'=>"/^([^\.#@:][a-z0-9\-_]*)\b/im", 'find_in_html'=>"/<([a-z][^\s><]*)\b/i")
);
$items = array();
foreach($selectors as $selector=>$params)
{
preg_match_all($params['find_in_html'], $this->amphtml, $ids);
if($selector=='cls')
{
$items[$selector] = array();
foreach($ids[1] as $classes)
{
$tmp = preg_split("/\s/", $classes, -1, PREG_SPLIT_NO_EMPTY);
$items[$selector] = array_merge($items[$selector], $tmp);
}
}
else
$items[$selector] = $ids[1];
}
//Remove count stats
$n = array('tags'=>0, 'id'=>0, 'cls'=>0);
$nrules = array();
$ids = array();
foreach($rules[0] as $x=>$rule)
{
$media = strpos($rule, "@");
if($media!==FALSE) continue; //TO-DO
$pos = strpos($rule, "{");
if($pos===FALSE||$pos==0) continue;
$leftrule = substr($rule, 0, $pos+1);
$tmp = array();
$sels = preg_split("#[,\s]#", $leftrule, -1, PREG_SPLIT_NO_EMPTY);
foreach($sels as $sel)
{
$sel .= "{";
$nrules[$sel] = 1;
}
}
$string = implode("\n", array_keys($nrules));
foreach($selectors as $selector=>$params)
{
preg_match_all($params['regex_css'], $string, $tmp);
$tmp[1] = array_unique($tmp[1]);
$ids[$selector] = $tmp[1];
foreach($ids[$selector] as $j=>$id)
{
$token = $params['prefix'].$id;
if(isset($this->rules_to_remove[$token])) continue;
if(in_array($token, $this->include_selectors))
{
$this->rules_to_remove[$token] = 0;
continue;
}
if(!in_array($id, $items[$selector]))
{
$this->rules_to_remove[$token] = 1;
$n[$selector]++;
}
else
$this->rules_to_remove[$token] = 0;
}
}
$this->stats['src_rules_to_remove']=$n;
$this->stats['num_rules_found']=count($nrules);
return $matches[1].$css.$matches[3]; //return minimized css
}
//2nd STEP: remove unused rules in CSS source
private function _amp_remove_css_callback($matches)
{
$css = $matches[2];
preg_match_all("#@[a-z][^{]+{([^{}]+{[^}]*})*}|([\.\#a-z][^{]*{[^}]*})#im", $css, $rules);
//stats
$lineremoved = 0;
$keyremoved = 0;
foreach($rules[0] as $x=>$rule)
{
$media = strpos($rule, "@");
if($media!==FALSE)
{
//TO-DO
}
else
{
$pos = strpos($rule, "{");
$leftrule = substr($rule, 0, $pos);
$rightrule = substr($rule, $pos);
$rtmp = preg_split("#[,]#m", $leftrule, -1, PREG_SPLIT_NO_EMPTY);
$found = false;
foreach($rtmp as $i=>$trule)
{
$trule = trim($trule);
$r = preg_split("/(?:[\s>]*)([#\.:]?[a-z][a-z0-9\-_]*)([\[\(].*[\]\)])?/i", $trule, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
foreach($r as $id)
{
if(isset($this->rules_to_remove[$id]))
{
$found = true;
unset($rtmp[$i]);
$keyremoved++;
}
}
}
if(!$rtmp||!count($rtmp))
{
unset($rules[0][$x]);
$lineremoved++;
}
else
{
if($found)
{
$rules[0][$x] = implode(",", $rtmp). $rightrule;
}
}
}
}
$this->stats['lines_removed'] = $lineremoved;
$this->stats['keywords_removed'] = $lineremoved;
$r = implode($this->newline,$rules[0]);
$this->stats['final_length'] = strlen($r);
return $matches[1].$r.$matches[3];
}
private function minify( $css )
{
$css = preg_replace( "#[\r\n]#m", ' ', $css );
$css = preg_replace('!/\*.*?\*/!s', '', $css); //strip comments
$css = preg_replace( '#\s+#m', ' ', $css );
$css = preg_replace( '/-?\b0px/', '0', $css );
$css = str_replace( '; ', ';', $css );
$css = str_replace( ': ', ':', $css );
$css = preg_replace( '#\s*>\s*#', '>', $css );
$css = str_replace( ' {', '{', $css );
$css = str_replace( '{ ', '{', $css );
$css = str_replace( ', ', ',', $css );
$css = str_replace( '} ', '}', $css );
$css = str_replace( ' }', '}', $css );
$css = str_replace( ';}', '}', $css );
$css = str_replace(' )', ')', $css);
$css = str_replace('( ', '(', $css);
$css = preg_replace('/\bwhite([\s;}])\b/', '#fff$1', $css); //avoid change white-space keyword
$css = preg_replace('/\bblack\b/', '#000', $css);
$css = preg_replace('#(}+)#', "$1\n", $css);
$css = trim($css);
return $css;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment