lib/Ajde/Document/Processor/Css/lib/maximizer/CSS3Maximizer.php
<?php
/*
CSS3Maximizer : v0.1 : mudcu.be
------------------------------------
Adds compatibility for vendors through proprietary CSS properties. No hassle!
Use your favorite syntax and CSS3Maximizer fills in the holes.
CSS3 Color Module
------------------
#00FF00 // all browsers
hsl(120, 100%, 50%); //
hsla(120, 100%, 50%, 1); //
rgb(0, 255, 0); //
rgb(0, 100%, 0); //
rgba(0, 255, 0%, 1); //
rgba(0, 100%, 0%, 1); //
CSS3 Gradient Module
---------------------
linear-gradient(yellow, blue);
linear-gradient(to top, blue, yellow);
linear-gradient(180deg, yellow, blue);
linear-gradient(to bottom, yellow 0%, blue 100%);
-webkit-gradient(linear, left top, left bottom, color-stop(0%, #444444), color-stop(100%, #999999)); // Saf4+, Chrome
-webkit-gradient(linear, left top, left bottom, from(#444444), to(#999999)); // Saf4+, Chrome
-webkit-linear-gradient(top, #444444, #999999); // Chrome 10+, Saf5.1+
-moz-linear-gradient(top, #444444, #999999); // FF3.6
-ms-linear-gradient(top, #444444, #999999); // IE10
-o-linear-gradient(top, #444444, #999999); // Opera 11.10+
filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='#444444', EndColorStr='#999999'); // IE6–IE9
CSS3 Properties
----------------
background-clip
----------------
-moz-background-clip: padding;
-webkit-background-clip: padding-box;
background-clip: padding-box;
background-size
----------------
-moz-background-size: 100% 100%; // FF3.6
-webkit-background-size: 100% 100%; // Saf3-4
background-size: 100% 100%; // Opera, IE9, Saf5, Chrome, FF4
border-radius
--------------
-moz-border-radius: 12px; // FF1-3.6
-webkit-border-radius: 12px; // Saf3-4, iOS 1-3.2, Android <1.6
border-radius: 12px; // Opera 10.5, IE9, Saf5, Chrome, FF4, iOS 4, Android 2.1+
box-shadow
-----------
-moz-box-shadow: 0px 0px 4px #ffffff; // FF3.5 - 3.6
-webkit-box-shadow: 0px 0px 4px #ffffff; // Saf3.0+, Chrome
box-shadow: 0px 0px 4px #ffffff; // Opera 10.5, IE9, FF4+, Chrome 10+
transition
-----------
-moz-transition: all 0.3s ease-out; // FF4+
-o-transition: all 0.3s ease-out; // Opera 10.5+
-webkit-transition: all 0.3s ease-out; // Saf3.2+, Chrome
-ms-transition: all 0.3s ease-out; // IE10?
transition: all 0.3s ease-out;
transform
----------
-moz-transform: rotate(7.5deg); // FF3.5+
-o-transform: rotate(7.5deg); // Opera 10.5
-webkit-transform: rotate(7.5deg); // Saf3.1+, Chrome
-ms-transform: rotate(7.5deg); // IE9
transform: rotate(7.5deg);
user-select
------------
user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-webkit-user-select: none;
TODO
-----
Maximize filter "opacity" for Internet Explorer:
opacity: 0 === filter: alpha(opacity=0);
*/
class CSS3Maximizer
{
private $ColorSpace;
private $code;
public function __construct()
{
$this->ColorSpace = new ColorSpace();
}
/* Direct conversions of properties between vendors */
private $defAlias = [
'background-clip' => [
'background-clip',
'-moz-background-clip',
'-webkit-background-clip',
],
'background-size' => [
'background-size',
'-moz-background-size',
'-webkit-background-size',
],
'border-radius' => [
'border-radius',
'-moz-border-radius',
'-webkit-border-radius',
],
'box-shadow' => [
'box-shadow',
'-moz-box-shadow',
'-webkit-box-shadow',
],
'text-shadow' => [
'text-shadow',
'-moz-text-shadow',
],
'transition' => [
'transition',
'-moz-transition',
'-ms-transition',
'-o-transition',
'-webkit-transition',
],
'transition-property' => [
'transition-property',
'-moz-transition-property',
'-ms-transition-property',
'-o-transition-property',
'-webkit-transition-property',
],
'transition-duration' => [
'transition-duration',
'-moz-transition-duration',
'-ms-transition-duration',
'-o-transition-duration',
'-webkit-transition-duration',
],
'transform' => [
'transform',
'-moz-transform',
'-ms-transform',
'-o-transform',
'-webkit-transform',
],
'user-select' => [
'user-select',
'-khtml-user-select',
'-moz-user-select',
'-o-user-select',
'-webkit-user-select',
],
];
private $defStaticProps = [
'0',
'none',
'transparent',
'inherit',
];
// Color Properties
private $defColorFallback = [
'color', // single value, rgba with hex fallback
'background-color', // single value, rgba with hex fallback
'border', // multiple values, rgba with hex fallback
'border-color', // multiple values, rgba with hex fallback
];
private $defColorProperties = [
'color', // single value, rgba with hex fallback
'background-color', // single value, rgba with hex fallback
'border', // multiple values, rgba with hex fallback
'border-color', // multiple values, rgba with hex fallback
'box-shadow', // multiple values, can always use rgba
'text-shadow', // multiple values, can always use rgba
];
// Gradient Properties
private $defGradientProperties = [
'background',
'background-image',
];
private $defGradientLinear = [
'-webkit-gradient',
'-webkit-linear-gradient',
'-moz-linear-gradient',
'-ms-linear-gradient',
'-o-linear-gradient',
'linear-gradient',
'filter',
];
/* Color parsing and standardization */
private function splitByColor($value)
{
$values = [];
while (strlen($value)) {
$ishex = strpos($value, '#');
$isother = strpos($value, ')');
if ($ishex === false) {
$ishex = 99999999999;
}
if ($isother === false) {
$isother = 99999999999;
}
if ($ishex < $isother) {
$comma = strpos($value, ',');
if ($comma === false) { // end of the line
array_push($values, $value);
$value = '';
} else { // split by comma [hex]
array_push($values, substr($value, 0, $comma));
$value = trim(substr($value, $comma + 1));
}
} else { // split by comma [rgb, rgba, hsl, hsla]
array_push($values, substr($value, 0, $isother + 1));
$value = trim(substr($value, $isother + 2));
}
}
return $values;
}
private function parseColors($colors, $doFallback = false)
{
$fallback = [];
foreach ($colors as $key => $value) {
$tmp = str_replace(' ', ' ', trim($value));
if (in_array($tmp, $this->defStaticProps)) { // none, inherit, transparent
return $tmp;
}
$pos = strpos($tmp, '(');
if ($pos === false) { // split color from properties [hex]
$pos = strpos($tmp, '#');
$first = substr($tmp, 0, $pos);
$color = substr($tmp, $pos);
} else { // split color from properties [rgb, rgba, hsl or hsla]
$pos = substr($tmp, 0, $pos);
$backpos = strrpos($pos, ' ');
if ($backpos === false) {
$backpos = -1;
}
$first = substr($pos, 0, $backpos + 1);
$color = substr($tmp, $backpos + 1);
}
$color = $this->parseColor($color);
if ($doFallback) { // include hex fallback when alpha is present
if ($color['rgba']) {
$fallback[$key] = $first.$color['hex'];
$colors[$key] = $first.$color['rgba'];
} else {
$fallback[$key] = $first.$color['hex'];
$colors[$key] = $first.$color['hex'];
}
} else {
if ($color['rgba']) {
$colors[$key] = $first.$color['rgba'];
} else { // everything is supported in hex!
$colors[$key] = $first.$color['hex'];
}
}
}
$colors = implode($colors, ', ');
$fallback = implode($fallback, ', ');
if ($doFallback && $colors !== $fallback) { // include fallback
return [
'hex' => $fallback,
'rgba' => $colors,
];
} else { // no fallback necessary
return $colors;
}
}
private function parseColor($color)
{
$color = trim($color);
if (strpos($color, '(')) { // rgb, rgba, hsl or hsla
$first = strpos($color, '(');
$type = substr($color, 0, $first);
$color = substr($color, $first + 1, -1);
$color = explode(',', $color);
$alpha = isset($color[3]) ? floatval($color[3]) : 1;
switch ($type) { // regularize to rgba and hex
case 'rgb':
case 'rgba':
if (strpos($color[0], '%')) {
$color[0] = round(intval($color[0]) / 100 * 255);
$color[1] = round(intval($color[1]) / 100 * 255);
$color[2] = round(intval($color[2]) / 100 * 255);
}
$color = [
R => $color[0],
G => $color[1],
B => $color[2],
];
break;
case 'hsl': // convert to rgb()
case 'hsla': // convert to rgba()
$color = $this->ColorSpace->HSL_RGB([
H => $color[0],
S => $color[1],
L => $color[2],
]);
break;
default: // hex
break;
}
$hex = '#'.$this->ColorSpace->HEX_STRING($this->ColorSpace->RGB_HEX($color));
if ($alpha === 1) {
return ['hex' => $hex];
} else { // requires alpha
$r = max(0, min(255, round($color[R])));
$g = max(0, min(255, round($color[G])));
$b = max(0, min(255, round($color[B])));
return [
'rgba' => 'rgba('.$r.', '.$g.', '.$b.', '.$alpha.')',
'hex' => $hex,
];
}
} else {
return [
'hex' => $color,
];
}
}
/* Gradient parsing and standardization */
private function splitGradient($value)
{
$values = [];
while (strlen($value)) {
$ishex = strpos($value, ',');
$isother = strpos($value, '(');
if ($ishex === false) {
$ishex = 99999999999;
}
if ($isother === false) {
$isother = 99999999999;
}
if ($ishex < $isother) {
$comma = strpos($value, ',');
array_push($values, substr($value, 0, $comma));
$value = trim(substr($value, $comma + 1));
} else { // split by comma [rgb, rgba, hsl, hsla]
$isother = strpos($value, ')');
if ($isother === false) {
array_push($values, $value);
$value = '';
} else {
$stop = substr($value, 0, $isother + 1);
if (substr_count($stop, '(') === 2) {
$isother += 1;
$stop = $stop.')';
}
array_push($values, $stop);
$value = trim(substr($value, $isother + 2));
}
}
}
return $values;
}
private function Webkit_Gradient_Position($value)
{
switch ($value) {
case 'top':
return ['y' => -1];
case 'left':
return ['x' => -1];
case 'bottom':
return ['y' => 1];
case 'right':
return ['x' => 1];
default: // center
return [];
}
}
private function Webkit_to_W3C_Gradient($value)
{
///--- webkit supports out-of-order color-stops (others fail)...
array_shift($value); // type of gradient [assume linear]
$start = explode(' ', array_shift($value));
$end = explode(' ', array_shift($value));
$aIsSame = $start[0] == $end[0];
$bIsSame = $start[1] == $end[1];
if ($aIsSame && !$bIsSame) {
$start = 'top';
} else {
if (!$aIsSame && $bIsSame) {
$start = 'left';
} else {
if (!$aIsSame && !$bIsSame) { // convert to angle
$p1 = array_merge(
['x' => 0, 'y' => 0],
$this->Webkit_Gradient_Position($start[0]),
$this->Webkit_Gradient_Position($start[1])
);
$p2 = array_merge(
['x' => 0, 'y' => 0],
$this->Webkit_Gradient_Position($end[0]),
$this->Webkit_Gradient_Position($end[1])
);
$dy = $p2[y] - $p1[y];
$dx = $p2[x] - $p1[x];
$start = round(rad2deg(atan2($dy, $dx))).'deg';
} else { // is "left"
$start = 'left';
}
}
}
$values = [];
$moz = [];
//
foreach ($value as $key) {
$type = substr($key, 0, strpos($key, '('));
$key = substr($key, strpos($key, '(') + 1);
if ($type == 'from') {
$position = '0%';
$color = substr($key, 0, -1);
} else {
if ($type == 'to') {
$position = '100%';
$color = substr($key, 0, -1);
} else {
$key = explode(',', $key, 2);
$position = $key[0];
if (!strpos($position, '%')) {
$position = round($position * 100).'%';
}
$color = substr($key[1], 0, -1);
}
}
$color = $this->parseColor($color);
if ($color['rgba']) {
array_push($values, $color['rgba'].' '.$position);
} else {
array_push($values, $color['hex'].' '.$position);
}
array_push($moz, $color['hex'].' '.$position);
}
return [
'microsoft' => [substr(reset($moz), 0, 7), substr(end($moz), 0, 7)],
'moz' => $start.', '.implode($moz, ', '),
'w3c' => $start.', '.implode($values, ', '),
];
}
private function W3C_to_Webkit_Gradient($value)
{
$start = array_shift($value);
switch ($start) {
case 'top':
$start = 'center top, center bottom, ';
break;
case 'left':
$start = 'left center, right center, ';
break;
default: // angle
$start = deg2rad(intval($start));
$x = round(cos($start) * 100);
$y = round(sin($start) * 100);
$start = $x.'% 0%, 0% '.$y.'%, ';
break;
}
$count = count($value) - 1;
$values = [];
foreach ($value as $n => $key) {
$key = explode(' ', $key);
$color = $this->parseColor($key[0]);
if ($color['rgba']) {
$color = $color['rgba'];
} else {
$color = $color['hex'];
}
$position = $key[1];
if (gettype($position) == 'NULL') {
$position = round($n / $count * 100).'%';
}
if ($n === 0) {
$first = $color;
array_push($values, "from({$color})");
} else {
if ($n === $count) {
$last = $color;
array_push($values, "to({$color})");
} else {
array_push($values, "color-stop({$position}, {$color})");
}
}
}
return [
'microsoft' => [$first, $last],
'webkit' => 'linear, '.$start.implode($values, ', '),
];
}
private function parseGradient($property, $value)
{
$type = substr($value, 0, strpos($value, '('));
$tmp = substr($value, strpos($value, '(') + 1, -1);
$values = [];
if ($type == '-webkit-gradient') { // convert from webkit to other
$value = $this->Webkit_to_W3C_Gradient($this->splitGradient($tmp));
$value['webkit'] = $tmp;
} else { // convert from other to webkit
$value = $this->W3C_to_Webkit_Gradient($this->splitGradient($tmp));
$value['w3c'] = $tmp;
}
foreach ($this->defGradientLinear as $key) {
if ($key == '-webkit-gradient') {
$values[$key] = $key.'('.$value['webkit'].')';
} else {
if ($key == 'filter') {
$color = $value['microsoft'];
$values[$key] = "filter: progid:DXImageTransform.Microsoft.gradient(startColorStr='{$color[0]}', EndColorStr='{$color[1]}')";
} else {
$values[$key] = $key.'('.$value['w3c'].')';
}
}
}
return $values;
}
/* Convert CSS to Object */
private function ParseCSS($str)
{
$css = [];
$str = preg_replace("/\/\*(.*)?\*\//Usi", '', $str);
$parts = explode('}', $str);
$skipping = false;
if (count($parts) > 0) {
foreach ($parts as $part) {
list($keystr, $codestr) = explode('{', $part);
// @media queries [skip for now]
if ($skipping) {
if (substr_count($part, '{') === 0) {
$skipping = false;
$css[$id[0]] .= "}\r";
} else {
$css[$id[0]] .= $part."}\r";
continue;
}
}
if (substr(trim($part), 0, 1) === '@') {
$id = explode(',', trim($keystr));
$css[$id[0]] = trim($part)."\r\t}";
$skipping = true;
continue;
}
// everything else
$keys = explode(',', trim($keystr));
if (count($keys) === 0) {
continue;
}
foreach ($keys as $key) {
if (strlen($key) === 0) {
continue;
}
$key = str_replace("\n", '', $key);
$key = str_replace('\\', '', $key);
$codestr = trim($codestr);
if (!isset($css[$key])) {
$css[$key] = [];
}
// only match ; without surrounding quotes
$codestr = preg_replace('/(?:["\'].*(;).*["\'])/e',
'str_replace(";","{{{RETAIN_SEPERATOR}}}","$0")', $codestr);
$codes = explode(';', $codestr);
if (count($codes) === 0) {
continue;
}
foreach ($codes as $code) {
// put back ; which were within surrounding quotes
$code = str_replace('{{{RETAIN_SEPERATOR}}}', ';', $code);
$code = trim($code);
list($codekey, $codevalue) = explode(':', $code, 2);
if (strlen($codekey) === 0) {
continue;
}
array_push($css[$key], [
'type' => trim($codekey),
'value' => trim($codevalue),
]
);
}
}
}
}
return $css;
}
/* Generate compatibility between vendors */
public function clean($config)
{
$css = isset($config['css']) ? $config['css'] : '';
$url = isset($config['url']) ? $config['url'] : '';
$compress = isset($config['compress']) ? $config['compress'] : '';
// load from file and write file
if (strpos($css, '.css') && is_file($css)) {
$this->code = file_get_contents($css);
} else {
$this->code = $css;
}
$cssObject = [];
$cssText = '';
$css = $this->ParseCSS($this->code);
// run through properties and add appropriate compatibility
foreach ($css as $cssID => $cssProperties) {
$properties = [];
if (gettype($cssProperties) === 'string') {
$cssObject[$cssID] = $cssProperties;
continue;
}
foreach ($cssProperties as $value) {
$type = $value['type'];
$value = $value['value'];
if (in_array($type, $this->defGradientProperties)) {
if (substr(trim($value), 0, 4) === 'url(') {
$value = substr(trim($value), 5, -1);
$value = str_replace(["'", '"'], [''], $value);
$value = 'url("'.$url.$value.'")';
} else {
if (strpos($value, 'gradient') !== false) { // background-gradient
$value = $this->parseGradient($type, $value);
} else { // background-color as "background"
$doFallback = in_array($type, $this->defColorFallback);
$value = $this->parseColors($this->splitByColor($value), $doFallback);
}
}
} else {
if (in_array($type, $this->defColorProperties)) {
$doFallback = in_array($type, $this->defColorFallback);
$value = $this->parseColors($this->splitByColor($value), $doFallback);
}
}
$alias = [];
foreach ($this->defAlias as $property) {
if (in_array($type, $property)) { // direct conversion between vendors
foreach ($property as $key) {
if ($key == '-moz-transition') {
$tmp = explode(' ', $value);
if ($tmp[0] == 'transform') {
$tmp[0] = '-moz-transform';
}
$tmp = implode($tmp, ' ');
$alias[$key] = $tmp;
} else {
if ($key == '-moz-transition-property') {
$tmp = $value;
if ($tmp == 'transform') {
$tmp = '-moz-transform';
}
$alias[$key] = $tmp;
} else {
$alias[$key] = $value;
}
}
}
}
}
if (count($alias)) {
$value = $alias;
}
$merged = false;
foreach ($properties as $key => $property) {
$typeof = gettype($property['value']);
if ($property['type'] == $type && gettype($value) === 'array') {
if ($typeof == 'string') {
$property['value'] = [
'hex' => $property['value'],
];
}
$properties[$key]['value'] = array_merge(
$property['value'],
$value
);
$merged = true;
} else {
if ($typeof == 'array' && $property['value'][$type]) {
if ($type === 'filter') {
$value = [
'filter' => $type.': '.$value,
];
}
$properties[$key]['value'] = array_merge(
$property['value'],
$value
);
$merged = true;
} else {
}
}
}
if ($merged === false) {
array_push($properties, [
'type' => $type,
'value' => $value,
]);
}
}
$cssObject[$cssID] = $properties;
}
$newline = $compress ? '' : "\n";
$space = $compress ? '' : ' ';
$tab = $compress ? '' : "\t";
// composite $cssObject into $cssText
$cssArray = [];
foreach ($cssObject as $cssID => $cssProperties) {
if (gettype($cssProperties) === 'string') {
$cssText = substr($cssProperties, strpos($cssProperties, '{') + 1);
$cssText = "\t".trim(substr($cssText, 0, strrpos($cssText, '}')))."\n";
array_push($cssArray, [
'text' => $cssText,
'key' => $cssID,
]);
continue;
}
$cssText = '';
foreach ($cssProperties as $value) {
$type = $value['type'];
$value = $value['value'];
if (gettype($value) == 'string') { // general properties
if ($compress) {
$value = str_replace(', ', ',', $value);
}
$value = str_replace(['\"', "\'"], ['"', "'"], $value);
$cssText .= $tab.$type.":{$space}".$value.";{$newline}";
} else { // multiple values
foreach ($value as $key => $tmp) {
if ($compress) {
$tmp = str_replace(', ', ',', $tmp);
}
$tmp = str_replace(['\"', "\'"], ['"', "'"], $tmp);
if ($key == 'hex' || $key == 'rgba' || in_array($key,
$this->defGradientLinear)
) { // color or gradient variants
if ($key == 'filter') { // microsoft values
$cssText .= $tab.$tmp.";{$newline}";
} else {
$cssText .= $tab.$type.":{$space}".$tmp.";{$newline}";
}
} else { // direct conversion of vender variants
$cssText .= $tab.$key.":{$space}".$tmp.";{$newline}";
}
}
}
}
array_push($cssArray, [
'text' => $cssText,
'key' => $cssID,
]);
}
$cssText = '';
foreach ($cssArray as $n => $value) {
$cssID = $value['key'];
$content1 = $cssArray[$n]['text'];
$content2 = $cssArray[$n + 1]['text'];
if ($content1 === $content2) {
$cssText .= $cssID.$space.','.$newline;
} else {
$cssText .= $cssID.$space.'{'.$newline;
$cssText .= $content1;
$cssText .= '}'.$newline;
}
}
return $cssText;
}
}