From 58197b24783f21720588d593d8a6e2f207bb01e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:36:55 +0200 Subject: [PATCH 01/38] Avoid using output buffer --- scss.inc.php | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 062b2d6a..fa813c96 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -69,9 +69,7 @@ function compile($code, $name=null) { $this->compileRoot($tree); $this->flattenSelectors($this->scope); - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); + $out = $this->formatter->block($this->scope); setlocale(LC_NUMERIC, $locale); return $out; @@ -3359,12 +3357,14 @@ public function property($name, $value) { } public function block($block) { - if (empty($block->lines) && empty($block->children)) return; + if (empty($block->lines) && empty($block->children)) return ''; $inner = $pre = $this->indentStr(); + + $ret = ''; if (!empty($block->selectors)) { - echo $pre . + $ret .= $pre . implode($this->tagSeparator, $block->selectors) . $this->open . $this->break; $this->indentLevel++; @@ -3373,21 +3373,22 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); + $ret .= $inner . implode($glue, $block->lines); if (!empty($block->children)) { - echo $this->break; + $ret .= $this->break; } } foreach ($block->children as $child) { - $this->block($child); + $ret .= $this->block($child); } if (!empty($block->selectors)) { $this->indentLevel--; - if (empty($block->children)) echo $this->break; - echo $pre . $this->close . $this->break; + if (empty($block->children)) $ret .= $this->break; + $ret .= $pre . $this->close . $this->break; } + return $ret; } } @@ -3421,10 +3422,12 @@ public function block($block) { if ($block->type == "root") { $this->adjustAllChildren($block); } + + $ret = ''; $inner = $pre = $this->indentStr($block->depth - 1); if (!empty($block->selectors)) { - echo $pre . + $ret .= $pre . implode($this->tagSeparator, $block->selectors) . $this->open . $this->break; $this->indentLevel++; @@ -3433,20 +3436,20 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); - if (!empty($block->children)) echo $this->break; + $ret .= $inner . implode($glue, $block->lines); + if (!empty($block->children)) $ret .= $this->break; } foreach ($block->children as $i => $child) { // echo "*** block: ".$block->depth." child: ".$child->depth."\n"; - $this->block($child); + $ret .= $this->block($child); if ($i < count($block->children) - 1) { - echo $this->break; + $ret .= $this->break; if (isset($block->children[$i + 1])) { $next = $block->children[$i + 1]; if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) { - echo $this->break; + $ret .= $this->break; } } } @@ -3454,12 +3457,13 @@ public function block($block) { if (!empty($block->selectors)) { $this->indentLevel--; - echo $this->close; + $ret .= $this->close; } if ($block->type == "root") { - echo $this->break; + $ret .= $this->break; } + return $ret; } } From a7a6311b5a5e838bdb275418ca17a9611fa15c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:46:06 +0200 Subject: [PATCH 02/38] Add formatter option to remove trailing semicolon in each block --- scss.inc.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index fa813c96..ae1e415b 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3343,6 +3343,8 @@ class scss_formatter { public $close = "}"; public $tagSeparator = ", "; public $assignSeparator = ": "; + + public $removeTrailingSemicolon = false; public function __construct() { $this->indentLevel = 0; @@ -3374,6 +3376,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); + + if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); + } + if (!empty($block->children)) { $ret .= $this->break; } @@ -3437,6 +3444,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); + + if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); + } + if (!empty($block->children)) $ret .= $this->break; } @@ -3472,6 +3484,7 @@ class scss_formatter_compressed extends scss_formatter { public $tagSeparator = ","; public $assignSeparator = ":"; public $break = ""; + public $removeTrailingSemicolon = true; public function indentStr($n = 0) { return ""; From 0fd6170ba89671b9c60ed268227cf17638a14d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:50:45 +0200 Subject: [PATCH 03/38] Add formatter option to use shorter css color names --- scss.inc.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index ae1e415b..3bf9ee77 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -922,6 +922,14 @@ protected function compileValue($value) { $h = '#' . $h[1] . $h[3] . $h[5]; } + if ($this->formatter->replaceColorNames) { + // Convert hex color to css color name if shorter (e.g. #f00 to red) + $name = array_search($r.','.$g.','.$b, self::$cssColors, true); + if ($name !== false && strlen($name) < strlen($h)) { + $h = $name; + } + } + return $h; case "number": return round($value[1], self::$numberPrecision) . $value[2]; @@ -3345,6 +3353,7 @@ class scss_formatter { public $assignSeparator = ": "; public $removeTrailingSemicolon = false; + public $replaceColorNames = false; public function __construct() { $this->indentLevel = 0; @@ -3485,6 +3494,7 @@ class scss_formatter_compressed extends scss_formatter { public $assignSeparator = ":"; public $break = ""; public $removeTrailingSemicolon = true; + public $replaceColorNames = true; public function indentStr($n = 0) { return ""; From 5697f8dce5186284ad6047e897e980106bf799c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 02:30:29 +0200 Subject: [PATCH 04/38] Add formatter option to omit unit for zero/null numbers --- scss.inc.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 3bf9ee77..521e7866 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -932,7 +932,11 @@ protected function compileValue($value) { return $h; case "number": - return round($value[1], self::$numberPrecision) . $value[2]; + $num = round($value[1], self::$numberPrecision); + if ($this->formatter->omitZeroUnit && $num == 0) { + return 0; + } + return $num . $value[2]; case "string": return $value[1] . $this->compileStringContent($value) . $value[1]; case "function": @@ -3354,6 +3358,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; + public $omitZeroUnit = false; public function __construct() { $this->indentLevel = 0; @@ -3495,6 +3500,7 @@ class scss_formatter_compressed extends scss_formatter { public $break = ""; public $removeTrailingSemicolon = true; public $replaceColorNames = true; + public $omitZeroUnit = true; public function indentStr($n = 0) { return ""; From a86922d918d94ee7dd1dfc90da28b3c0ad07043c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 10:59:16 +0200 Subject: [PATCH 05/38] Add formatter option to strip comments --- scss.inc.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 521e7866..917a76b4 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -445,7 +445,9 @@ protected function compileChild($child, $out) { $this->compileValue($child[2])); break; case "comment": - $out->lines[] = $child[1]; + if(!$this->formatter->stripComments){ + $out->lines[] = $child[1]; + } break; case "mixin": case "function": @@ -3359,6 +3361,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; public $omitZeroUnit = false; + public $stripComments = false; public function __construct() { $this->indentLevel = 0; @@ -3501,6 +3504,7 @@ class scss_formatter_compressed extends scss_formatter { public $removeTrailingSemicolon = true; public $replaceColorNames = true; public $omitZeroUnit = true; + public $stripComments = true; public function indentStr($n = 0) { return ""; From 2caffe3cedf6cad1649dc6a287ea7829e41d4944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 14:58:47 +0200 Subject: [PATCH 06/38] Fix removing semicolons before new blocks --- scss.inc.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 917a76b4..6e246430 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3393,13 +3393,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); - - if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { - $ret = substr($ret, 0, -1); - } - + if (!empty($block->children)) { $ret .= $this->break; + } else if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); } } @@ -3461,12 +3459,12 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); - - if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + + if (!empty($block->children)) { + $ret .= $this->break; + } else if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { $ret = substr($ret, 0, -1); } - - if (!empty($block->children)) $ret .= $this->break; } foreach ($block->children as $i => $child) { From 6a18e5d513c639c1717b37e043e42fb788540fc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 15:35:35 +0200 Subject: [PATCH 07/38] Refactor formatting numbers and colors to be done by formatter --- scss.inc.php | 63 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 6e246430..295839b0 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -24,7 +24,6 @@ class scssc { "function" => "^", ); - static protected $numberPrecision = 3; static protected $unitTable = array( "in" => array( "in" => 1, @@ -912,33 +911,11 @@ protected function compileValue($value) { $r = round($r); $g = round($g); $b = round($b); + $a = (count($value) == 5) ? $value[4] : 1; - if (count($value) == 5 && $value[4] != 1) { // rgba - return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')'; - } - - $h = sprintf("#%02x%02x%02x", $r, $g, $b); - - // Converting hex color to short notation (e.g. #003399 to #039) - if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { - $h = '#' . $h[1] . $h[3] . $h[5]; - } - - if ($this->formatter->replaceColorNames) { - // Convert hex color to css color name if shorter (e.g. #f00 to red) - $name = array_search($r.','.$g.','.$b, self::$cssColors, true); - if ($name !== false && strlen($name) < strlen($h)) { - $h = $name; - } - } - - return $h; + return $this->formatter->color($r, $g, $b, $a); case "number": - $num = round($value[1], self::$numberPrecision); - if ($this->formatter->omitZeroUnit && $num == 0) { - return 0; - } - return $num . $value[2]; + return $this->formatter->number($value[1],$value[2]); case "string": return $value[1] . $this->compileStringContent($value) . $value[1]; case "function": @@ -1919,7 +1896,7 @@ protected function lib_comparable($args) { return true; // TODO: THIS } - static protected $cssColors = array( + public static $cssColors = array( 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', @@ -3362,6 +3339,7 @@ class scss_formatter { public $replaceColorNames = false; public $omitZeroUnit = false; public $stripComments = false; + public $numberPrecision = 3; public function __construct() { $this->indentLevel = 0; @@ -3374,6 +3352,37 @@ public function indentStr($n = 0) { public function property($name, $value) { return $name . $this->assignSeparator . $value . ";"; } + + public function color($r, $g, $b, $a) { + if ($a != 1) { // rgba + return 'rgba('.$r.', '.$g.', '.$b.', '.$a.')'; + } + + $h = sprintf("#%02x%02x%02x", $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + if ($this->replaceColorNames) { + // Convert hex color to css color name if shorter (e.g. #f00 to red) + $name = array_search($r.','.$g.','.$b, scssc::$cssColors, true); + if ($name !== false && strlen($name) < strlen($h)) { + $h = $name; + } + } + + return $h; + } + + public function number($num, $unit) { + $num = round($num, $this->numberPrecision); + if ($this->omitZeroUnit && $num == 0) { + return 0; + } + return $num . $unit; + } public function block($block) { if (empty($block->lines) && empty($block->children)) return ''; From 3500a05cebe25d7de9600255e0ec9d167fe72d77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 15:42:51 +0200 Subject: [PATCH 08/38] Sanitize color alpha channel value --- scss.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 295839b0..9b010c79 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -911,7 +911,7 @@ protected function compileValue($value) { $r = round($r); $g = round($g); $b = round($b); - $a = (count($value) == 5) ? $value[4] : 1; + $a = (count($value) == 5) ? max(0,min($value[4],1)) : 1; return $this->formatter->color($r, $g, $b, $a); case "number": From e8095506f18a3f2c4fd9ffa837abe67082b454c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 16:15:22 +0200 Subject: [PATCH 09/38] Compress rgba output according to formatter rules --- scss.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 9b010c79..e969b02b 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3354,8 +3354,8 @@ public function property($name, $value) { } public function color($r, $g, $b, $a) { - if ($a != 1) { // rgba - return 'rgba('.$r.', '.$g.', '.$b.', '.$a.')'; + if (($a = $this->number($a, null)) != 1) { // rgba + return 'rgba('.$r.$this->tagSeparator.$g.$this->tagSeparator.$b.$this->tagSeparator.$a.')'; } $h = sprintf("#%02x%02x%02x", $r, $g, $b); From 16262f1d45e5f190f3133e1dd9e6b9eb52a214f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 23:56:09 +0200 Subject: [PATCH 10/38] Add parser option to omit leading zeros --- scss.inc.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index e969b02b..9721b87d 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3338,6 +3338,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; public $omitZeroUnit = false; + public $omitZeroLeading = false; public $stripComments = false; public $numberPrecision = 3; @@ -3381,6 +3382,13 @@ public function number($num, $unit) { if ($this->omitZeroUnit && $num == 0) { return 0; } + if ($this->omitZeroLeading) { + if ($num > 0 && $num < 1) { + $num = substr($num, 1); + } else if ($num < 0 && $num > -1) { + $num = '-' . substr($num, 2); + } + } return $num . $unit; } @@ -3511,6 +3519,7 @@ class scss_formatter_compressed extends scss_formatter { public $removeTrailingSemicolon = true; public $replaceColorNames = true; public $omitZeroUnit = true; + public $omitZeroLeading = true; public $stripComments = true; public function indentStr($n = 0) { From 4ea76b64477f4adbee40b279c620bfcb3b6a34aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:24:16 +0200 Subject: [PATCH 11/38] Add support for 'transparent' color keyword See: http://www.w3.org/TR/css3-color/#transparent --- scss.inc.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 9721b87d..b0b2d9ec 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1334,8 +1334,10 @@ protected function coerceColor($value) { case "keyword": $name = $value[1]; if (isset(self::$cssColors[$name])) { - list($r, $g, $b) = explode(',', self::$cssColors[$name]); - return array('color', $r, $g, $b); + $color = explode(',', self::$cssColors[$name]); + // array('color', $r, $g, $b); + array_unshift($color, 'color'); + return $color; } return null; } @@ -1897,6 +1899,7 @@ protected function lib_comparable($args) { } public static $cssColors = array( + 'transparent' => '0,0,0,0', 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', @@ -3356,6 +3359,9 @@ public function property($name, $value) { public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba + if ($a == 0 && $this->replaceColorNames) { + return 'transparent'; + } return 'rgba('.$r.$this->tagSeparator.$g.$this->tagSeparator.$b.$this->tagSeparator.$a.')'; } From f185bb3af2793cd3ed82edc1d171eafa695beab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:45:14 +0200 Subject: [PATCH 12/38] Fix color keywords to actually be transparent --- scss.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index b0b2d9ec..cbada8be 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1332,7 +1332,7 @@ protected function coerceColor($value) { switch ($value[0]) { case "color": return $value; case "keyword": - $name = $value[1]; + $name = strtolower($value[1]); if (isset(self::$cssColors[$name])) { $color = explode(',', self::$cssColors[$name]); // array('color', $r, $g, $b); From a294defe7ec5de228bce54d6a978d860ea56168a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:51:56 +0200 Subject: [PATCH 13/38] Fix assertion arguments --- tests/ApiTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ApiTest.php b/tests/ApiTest.php index b4406784..333d23e6 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -14,14 +14,14 @@ public function testUserFunction() { }); $this->assertEquals( - $this->compile("result: add-two(10, 20);"), - "result: 30;"); + "result: 30;", + $this->compile("result: add-two(10, 20);")); } public function testImportMissing(){ $this->assertEquals( - $this->compile('@import "missing";'), - '@import "missing";'); + '@import "missing";', + $this->compile('@import "missing";')); } public function testImportCustomCallback(){ @@ -30,8 +30,8 @@ public function testImportCustomCallback(){ }); $this->assertEquals( - $this->compile('@import "variables.css";'), - trim(file_get_contents(__DIR__.'/outputs/variables.css'))); + trim(file_get_contents(__DIR__.'/outputs/variables.css')), + $this->compile('@import "variables.css";')); } public function compile($str) { From 5a1e61335aa48b42c4165a22e1c8082d5a62eaff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 01:16:15 +0200 Subject: [PATCH 14/38] Add unit tests for compressed formatter --- tests/CompressTest.php | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/CompressTest.php diff --git a/tests/CompressTest.php b/tests/CompressTest.php new file mode 100644 index 00000000..0f831d55 --- /dev/null +++ b/tests/CompressTest.php @@ -0,0 +1,88 @@ +scss = new scssc(); + $this->scss->setFormatter(new scss_formatter_compressed()); + } + + public function testCompressTwice(){ + $code = file_get_contents(__DIR__.'/inputs/variables.scss'); + + $result = $this->scss->compile($code); + + $this->assertEquals($result,$this->scss->compile($result)); + } + + public function testCompressFormat(){ + // check removing trailing semicolons + $this->assertEquals('@import "missing";a{border:0;content:";";border:0}b{border:0}',$this->scss->compile('@import "missing";a{border:0;content:";";border:0;}b{border:0;}')); + + // check removing excessive whitespace + $this->assertEquals('body,a,a:hover{border:0}',$this->scss->compile('body, a, a:hover { border: 0 ; }')); + + // check removing empty blocks and comments + $this->assertEquals('a{display:hidden}',$this->scss->compile(' /* comment */ b{a{ }} strong{;;;} a{display:hidden;;;} b{/*inner comment*/}')); + } + + /** + * @dataProvider equalColorsProvider + */ + public function testCompressColor($in,$out) { + $this->assertEquals('color:'.$out,$this->scss->compile('color:'.$in)); + } + + public function equalColorsProvider() { + return $this->prepareSet(array( + 'red' => 'red', // unchanged color keyword as shortest + '#000' => '#000', // unchanged short hex + '#00ff00' => '#0f0', // short hex + '#7abAaA' => '#7abaaa', // always use lowercase hex + 'rgb(255,255,255)' => '#fff', // short hex + 'rgb(123,121,121)' => '#7b7979', // hex instead of RGB notation + 'rgb( 0 , 0 , 255 )' => '#00f', // short hex for excessive whitespace + 'rgba(123,121,121,1)' => '#7b7979', // full alpha => use hex + 'rgba(123,121,121,2)' => '#7b7979', // cap excessive alpha to full alpha + 'rgba(123,121,121,0.9999)' => '#7b7979', // round alpha component + 'rgba(123,121,121,0.5)' => 'rgba(123,121,121,.5)', // remove useless leading zero + 'rgba(123,121,121,0)' => 'transparent', // zero alpha => shorthand transparent color keyword + 'opacify(transparent, 1)' => '#000', // fully opaque 'transparent' is actually black + 'opacify(transparent, 0.3)' => 'rgba(0,0,0,.3)', // work with transparent color keyword + 'opacify(WhiTe,1)' => '#fff' // color keywords are actually case insensitive + )); + } + + /** + * @dataProvider equalNumbersProvider + */ + public function testCompressNumber($in,$out){ + $this->assertEquals('padding:'.$out,$this->scss->compile('padding:'.$in)); + } + + public function equalNumbersProvider(){ + return $this->prepareSet(array( + '14px' => '14px', // unchanged + '0px' => '0', // remove zero unit + '0em' => '0', + '0%' => '0', + '1.5em' => '1.5em', + '0.5em' => '.5em', // remove leading zero + '0.50pt' => '.5pt', // remove leading and trailing zero and keep unit + '-4.0' => '-4', // remove useless fraction + '4.000px' => '4px', + '4.9999' => '5', // round to next number + '4.99' => '4.99', + '-0.2pt' => '-.2pt', // remove leading zero for negative numbers + '-0pt' => '0', // negative zero is still zero + '+4%' => '4%', // remove useless postive number marker + '+0em' => '0' + )); + } + + private function prepareSet($set){ + return array_map(function($in, $out) { return array($in, $out); }, array_keys($set), $set); + } + +} From c91f0f368aedaa665710be3afb5527b8403f72c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:30:03 +0200 Subject: [PATCH 15/38] Compress whitespace when joining lists --- scss.inc.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index cbada8be..e8bb225a 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -929,7 +929,7 @@ protected function compileValue($value) { foreach ($items as &$item) { $item = $this->compileValue($item); } - return implode("$delim ", $items); + return $this->formatter->implodeList($delim, $items); case "interpolated": # node created by extractInterpolation list(, $interpolate, $left, $right) = $value; list(,, $whiteLeft, $whiteRight) = $interpolate; @@ -3356,7 +3356,11 @@ public function indentStr($n = 0) { public function property($name, $value) { return $name . $this->assignSeparator . $value . ";"; } - + + public function implodeList($delim, $items) { + return implode("$delim ", $items); + } + public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba if ($a == 0 && $this->replaceColorNames) { @@ -3531,6 +3535,14 @@ class scss_formatter_compressed extends scss_formatter { public function indentStr($n = 0) { return ""; } + + public function implodeList($delim, $list) { + // no delimiter => actually a whitespace separated list (as in "margin") + if ($delim == "") { + $delim = " "; + } + return implode($delim, $list); + } } class scss_server { From 96a52fdbf90b55a8eb07a56230508ea1ac11d502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:35:38 +0200 Subject: [PATCH 16/38] Compress unneeded whitespace around descendant selectors --- scss.inc.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index e8bb225a..5cc10927 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -324,7 +324,7 @@ protected function evalSelectorPart($piece) { protected function compileSelector($selector) { if (!is_array($selector)) return $selector; // media and the like - return implode(" ", array_map( + return $this->formatter->implodeSelectors(array_map( array($this, "compileSelectorPart"), $selector)); } @@ -3361,6 +3361,10 @@ public function implodeList($delim, $items) { return implode("$delim ", $items); } + public function implodeSelectors($selectors) { + return implode(" ", $selectors); + } + public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba if ($a == 0 && $this->replaceColorNames) { @@ -3543,6 +3547,25 @@ public function implodeList($delim, $list) { } return implode($delim, $list); } + + public function implodeSelectors($selectors){ + $ret = ''; + $ws = false; + foreach($selectors as $selector){ + // do not use whitespace around descendant selectors + if (in_array($selector,array('+','~','>'),true)) { + $ws = false; + }else{ + if ($ws) { + $ret .= ' '; + } else { + $ws = true; + } + } + $ret .= $selector; + } + return $ret; + } } class scss_server { From 4d055c79b674bac75337a4f22dc5d70fe0a6b2f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:42:40 +0200 Subject: [PATCH 17/38] Additional tests for whitespace --- tests/CompressTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/CompressTest.php b/tests/CompressTest.php index 0f831d55..5974e0a4 100644 --- a/tests/CompressTest.php +++ b/tests/CompressTest.php @@ -25,6 +25,18 @@ public function testCompressFormat(){ // check removing empty blocks and comments $this->assertEquals('a{display:hidden}',$this->scss->compile(' /* comment */ b{a{ }} strong{;;;} a{display:hidden;;;} b{/*inner comment*/}')); + + // do not mess around with attribute selectors + // http://www.w3.org/TR/CSS2/selector.html#attribute-selectors + $this->assertEquals('input[type="image"][disabled]{opacity:.5}',$this->scss->compile(' input[type="image"][disabled] { opacity: 0.5; } ')); + + // remove optional whitespace for child selectors, but keep whitespace for descendant selectors + // http://www.w3.org/TR/CSS2/selector.html#child-selectors + $this->assertEquals('a+b,c>d,e f{display:none}a.b>c d:not(.e){border:0}',$this->scss->compile(' a + b , c > d , e f { display: none; } a.b > c d:not(.e) { border: 0; }')); + + // remove whitespace around list delimiter, but keep whitespace between space separated values + $this->assertEquals('*{a:red,green,blue}a{margin:0 0 0 0}',$this->scss->compile('* { a: red, green , blue ; } a { margin: 0 0 0 0; }')); + } /** From faa0cd0ab8d3199dd476779e985230ed6d11ce73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 16:22:29 +0200 Subject: [PATCH 18/38] Test compressing media queries --- tests/CompressTest.php | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/CompressTest.php b/tests/CompressTest.php index 5974e0a4..86b35e35 100644 --- a/tests/CompressTest.php +++ b/tests/CompressTest.php @@ -7,7 +7,7 @@ public function setUp() { $this->scss = new scssc(); $this->scss->setFormatter(new scss_formatter_compressed()); } - + public function testCompressTwice(){ $code = file_get_contents(__DIR__.'/inputs/variables.scss'); @@ -15,7 +15,7 @@ public function testCompressTwice(){ $this->assertEquals($result,$this->scss->compile($result)); } - + public function testCompressFormat(){ // check removing trailing semicolons $this->assertEquals('@import "missing";a{border:0;content:";";border:0}b{border:0}',$this->scss->compile('@import "missing";a{border:0;content:";";border:0;}b{border:0;}')); @@ -36,16 +36,36 @@ public function testCompressFormat(){ // remove whitespace around list delimiter, but keep whitespace between space separated values $this->assertEquals('*{a:red,green,blue}a{margin:0 0 0 0}',$this->scss->compile('* { a: red, green , blue ; } a { margin: 0 0 0 0; }')); - } - + + /** + * @dataProvider equalMediaProvider + */ + public function testMediaQueries($in,$out){ + $this->assertEquals('@media '.$out.'{a{border:0}}',$this->scss->compile('@media '.$in.' {a{border:0}}')); + } + + public function equalMediaProvider() { + // http://www.w3.org/TR/css3-mediaqueries/ + return $this->prepareSet(array( + 'only screen' => 'only screen', // unchanged + ' only screen ' => 'only screen', // remove optional whitespace +// 'screen, print' => 'screen,print', // remove whitespace between OR'ed queries + '(min-width: 100px )' => '(min-width:100px)', // check media features + '(min-width: 0px)' => '(min-width:0)', // remove zero unit + 'only screen and (min-width: 0px) and (max-width: 1000px)' => 'only screen and (min-width:0) and (max-width:1000px)', +// 'handheld, only screen and (max-width: 1000px)' => 'handheld,only screen and (max-width:1000px)', +// 'screen and (device-aspect-ratio: 16/9) , print and (min-resolution: 300dpi)' => 'screen and (device-aspect-ratio:16/9),print and (min-resolution:300dpi)' + )); + } + /** * @dataProvider equalColorsProvider */ public function testCompressColor($in,$out) { $this->assertEquals('color:'.$out,$this->scss->compile('color:'.$in)); } - + public function equalColorsProvider() { return $this->prepareSet(array( 'red' => 'red', // unchanged color keyword as shortest @@ -65,14 +85,14 @@ public function equalColorsProvider() { 'opacify(WhiTe,1)' => '#fff' // color keywords are actually case insensitive )); } - + /** * @dataProvider equalNumbersProvider */ public function testCompressNumber($in,$out){ $this->assertEquals('padding:'.$out,$this->scss->compile('padding:'.$in)); } - + public function equalNumbersProvider(){ return $this->prepareSet(array( '14px' => '14px', // unchanged From 64514a636b393af1c2c8832e32a3ecc50278ceae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Tue, 16 Oct 2012 00:58:35 +0200 Subject: [PATCH 19/38] Obey formatter rules for media query values --- scss.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 5cc10927..19d590b1 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -361,7 +361,7 @@ protected function compileMediaQuery($query) { break; case "mediaExp": if (isset($q[2])) { - $parts[] = "($q[1]: " . $this->compileValue($q[2]) . ")"; + $parts[] = "($q[1]" . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; } else { $parts[] = "($q[1])"; } From eeb68613d7186ff2d28fd5723daed0dd39183ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:36:55 +0200 Subject: [PATCH 20/38] Avoid using output buffer --- scss.inc.php | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 4c3dbd49..55ac21f8 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -69,9 +69,7 @@ function compile($code, $name=null) { $this->compileRoot($tree); $this->flattenSelectors($this->scope); - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); + $out = $this->formatter->block($this->scope); setlocale(LC_NUMERIC, $locale); return $out; @@ -3374,12 +3372,14 @@ public function property($name, $value) { } public function block($block) { - if (empty($block->lines) && empty($block->children)) return; + if (empty($block->lines) && empty($block->children)) return ''; $inner = $pre = $this->indentStr(); + + $ret = ''; if (!empty($block->selectors)) { - echo $pre . + $ret .= $pre . implode($this->tagSeparator, $block->selectors) . $this->open . $this->break; $this->indentLevel++; @@ -3388,21 +3388,22 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); + $ret .= $inner . implode($glue, $block->lines); if (!empty($block->children)) { - echo $this->break; + $ret .= $this->break; } } foreach ($block->children as $child) { - $this->block($child); + $ret .= $this->block($child); } if (!empty($block->selectors)) { $this->indentLevel--; - if (empty($block->children)) echo $this->break; - echo $pre . $this->close . $this->break; + if (empty($block->children)) $ret .= $this->break; + $ret .= $pre . $this->close . $this->break; } + return $ret; } } @@ -3436,10 +3437,12 @@ public function block($block) { if ($block->type == "root") { $this->adjustAllChildren($block); } + + $ret = ''; $inner = $pre = $this->indentStr($block->depth - 1); if (!empty($block->selectors)) { - echo $pre . + $ret .= $pre . implode($this->tagSeparator, $block->selectors) . $this->open . $this->break; $this->indentLevel++; @@ -3448,20 +3451,20 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); - if (!empty($block->children)) echo $this->break; + $ret .= $inner . implode($glue, $block->lines); + if (!empty($block->children)) $ret .= $this->break; } foreach ($block->children as $i => $child) { // echo "*** block: ".$block->depth." child: ".$child->depth."\n"; - $this->block($child); + $ret .= $this->block($child); if ($i < count($block->children) - 1) { - echo $this->break; + $ret .= $this->break; if (isset($block->children[$i + 1])) { $next = $block->children[$i + 1]; if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) { - echo $this->break; + $ret .= $this->break; } } } @@ -3469,12 +3472,13 @@ public function block($block) { if (!empty($block->selectors)) { $this->indentLevel--; - echo $this->close; + $ret .= $this->close; } if ($block->type == "root") { - echo $this->break; + $ret .= $this->break; } + return $ret; } } From 403e85745c6d5709daa876a9c5230642af4444fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:46:06 +0200 Subject: [PATCH 21/38] Add formatter option to remove trailing semicolon in each block --- scss.inc.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index 55ac21f8..6397b108 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3358,6 +3358,8 @@ class scss_formatter { public $close = "}"; public $tagSeparator = ", "; public $assignSeparator = ": "; + + public $removeTrailingSemicolon = false; public function __construct() { $this->indentLevel = 0; @@ -3389,6 +3391,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); + + if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); + } + if (!empty($block->children)) { $ret .= $this->break; } @@ -3452,6 +3459,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); + + if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); + } + if (!empty($block->children)) $ret .= $this->break; } @@ -3487,6 +3499,7 @@ class scss_formatter_compressed extends scss_formatter { public $tagSeparator = ","; public $assignSeparator = ":"; public $break = ""; + public $removeTrailingSemicolon = true; public function indentStr($n = 0) { return ""; From f949874e3f2b2b1a1cf6fe04135047ce118106c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 01:50:45 +0200 Subject: [PATCH 22/38] Add formatter option to use shorter css color names --- scss.inc.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index 6397b108..d5866ce9 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -930,6 +930,14 @@ protected function compileValue($value) { $h = '#' . $h[1] . $h[3] . $h[5]; } + if ($this->formatter->replaceColorNames) { + // Convert hex color to css color name if shorter (e.g. #f00 to red) + $name = array_search($r.','.$g.','.$b, self::$cssColors, true); + if ($name !== false && strlen($name) < strlen($h)) { + $h = $name; + } + } + return $h; case "number": return round($value[1], self::$numberPrecision) . $value[2]; @@ -3360,6 +3368,7 @@ class scss_formatter { public $assignSeparator = ": "; public $removeTrailingSemicolon = false; + public $replaceColorNames = false; public function __construct() { $this->indentLevel = 0; @@ -3500,6 +3509,7 @@ class scss_formatter_compressed extends scss_formatter { public $assignSeparator = ":"; public $break = ""; public $removeTrailingSemicolon = true; + public $replaceColorNames = true; public function indentStr($n = 0) { return ""; From bb5e9bed49e01e6be7cbd98e7ba6258d889248ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 02:30:29 +0200 Subject: [PATCH 23/38] Add formatter option to omit unit for zero/null numbers --- scss.inc.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index d5866ce9..6a6e69af 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -940,7 +940,11 @@ protected function compileValue($value) { return $h; case "number": - return round($value[1], self::$numberPrecision) . $value[2]; + $num = round($value[1], self::$numberPrecision); + if ($this->formatter->omitZeroUnit && $num == 0) { + return 0; + } + return $num . $value[2]; case "string": return $value[1] . $this->compileStringContent($value) . $value[1]; case "function": @@ -3369,6 +3373,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; + public $omitZeroUnit = false; public function __construct() { $this->indentLevel = 0; @@ -3510,6 +3515,7 @@ class scss_formatter_compressed extends scss_formatter { public $break = ""; public $removeTrailingSemicolon = true; public $replaceColorNames = true; + public $omitZeroUnit = true; public function indentStr($n = 0) { return ""; From 6014b49eddfbbad969bd281b1276bd25543152f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 7 Oct 2012 10:59:16 +0200 Subject: [PATCH 24/38] Add formatter option to strip comments --- scss.inc.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 6a6e69af..3fb8973a 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -453,7 +453,9 @@ protected function compileChild($child, $out) { $this->compileValue($child[2])); break; case "comment": - $out->lines[] = $child[1]; + if(!$this->formatter->stripComments){ + $out->lines[] = $child[1]; + } break; case "mixin": case "function": @@ -3374,6 +3376,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; public $omitZeroUnit = false; + public $stripComments = false; public function __construct() { $this->indentLevel = 0; @@ -3516,6 +3519,7 @@ class scss_formatter_compressed extends scss_formatter { public $removeTrailingSemicolon = true; public $replaceColorNames = true; public $omitZeroUnit = true; + public $stripComments = true; public function indentStr($n = 0) { return ""; From fd61c505ebac696e07a771af088327a12a655a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 14:58:47 +0200 Subject: [PATCH 25/38] Fix removing semicolons before new blocks --- scss.inc.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 3fb8973a..2fa2e64d 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3408,13 +3408,11 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); - - if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { - $ret = substr($ret, 0, -1); - } - + if (!empty($block->children)) { $ret .= $this->break; + } else if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + $ret = substr($ret, 0, -1); } } @@ -3476,12 +3474,12 @@ public function block($block) { if (!empty($block->lines)) { $glue = $this->break.$inner; $ret .= $inner . implode($glue, $block->lines); - - if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { + + if (!empty($block->children)) { + $ret .= $this->break; + } else if ($this->removeTrailingSemicolon && substr($ret, -1) === ';') { $ret = substr($ret, 0, -1); } - - if (!empty($block->children)) $ret .= $this->break; } foreach ($block->children as $i => $child) { From 937c318af50e15d1f3230865cfedcc129856f63e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 15:35:35 +0200 Subject: [PATCH 26/38] Refactor formatting numbers and colors to be done by formatter --- scss.inc.php | 63 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index 2fa2e64d..581f44be 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -24,7 +24,6 @@ class scssc { "function" => "^", ); - static protected $numberPrecision = 3; static protected $unitTable = array( "in" => array( "in" => 1, @@ -920,33 +919,11 @@ protected function compileValue($value) { $r = round($r); $g = round($g); $b = round($b); + $a = (count($value) == 5) ? $value[4] : 1; - if (count($value) == 5 && $value[4] != 1) { // rgba - return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')'; - } - - $h = sprintf("#%02x%02x%02x", $r, $g, $b); - - // Converting hex color to short notation (e.g. #003399 to #039) - if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { - $h = '#' . $h[1] . $h[3] . $h[5]; - } - - if ($this->formatter->replaceColorNames) { - // Convert hex color to css color name if shorter (e.g. #f00 to red) - $name = array_search($r.','.$g.','.$b, self::$cssColors, true); - if ($name !== false && strlen($name) < strlen($h)) { - $h = $name; - } - } - - return $h; + return $this->formatter->color($r, $g, $b, $a); case "number": - $num = round($value[1], self::$numberPrecision); - if ($this->formatter->omitZeroUnit && $num == 0) { - return 0; - } - return $num . $value[2]; + return $this->formatter->number($value[1],$value[2]); case "string": return $value[1] . $this->compileStringContent($value) . $value[1]; case "function": @@ -1934,7 +1911,7 @@ protected function lib_comparable($args) { return true; // TODO: THIS } - static protected $cssColors = array( + public static $cssColors = array( 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', @@ -3377,6 +3354,7 @@ class scss_formatter { public $replaceColorNames = false; public $omitZeroUnit = false; public $stripComments = false; + public $numberPrecision = 3; public function __construct() { $this->indentLevel = 0; @@ -3389,6 +3367,37 @@ public function indentStr($n = 0) { public function property($name, $value) { return $name . $this->assignSeparator . $value . ";"; } + + public function color($r, $g, $b, $a) { + if ($a != 1) { // rgba + return 'rgba('.$r.', '.$g.', '.$b.', '.$a.')'; + } + + $h = sprintf("#%02x%02x%02x", $r, $g, $b); + + // Converting hex color to short notation (e.g. #003399 to #039) + if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { + $h = '#' . $h[1] . $h[3] . $h[5]; + } + + if ($this->replaceColorNames) { + // Convert hex color to css color name if shorter (e.g. #f00 to red) + $name = array_search($r.','.$g.','.$b, scssc::$cssColors, true); + if ($name !== false && strlen($name) < strlen($h)) { + $h = $name; + } + } + + return $h; + } + + public function number($num, $unit) { + $num = round($num, $this->numberPrecision); + if ($this->omitZeroUnit && $num == 0) { + return 0; + } + return $num . $unit; + } public function block($block) { if (empty($block->lines) && empty($block->children)) return ''; From 3ac7b4b73481c601d0f64c95b4260d101c4ea7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 15:42:51 +0200 Subject: [PATCH 27/38] Sanitize color alpha channel value --- scss.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 581f44be..accd5b30 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -919,7 +919,7 @@ protected function compileValue($value) { $r = round($r); $g = round($g); $b = round($b); - $a = (count($value) == 5) ? $value[4] : 1; + $a = (count($value) == 5) ? max(0,min($value[4],1)) : 1; return $this->formatter->color($r, $g, $b, $a); case "number": From 4e40c72513ae5847eccb67bae71a18d1bdd8618b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 16:15:22 +0200 Subject: [PATCH 28/38] Compress rgba output according to formatter rules --- scss.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index accd5b30..acec2219 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3369,8 +3369,8 @@ public function property($name, $value) { } public function color($r, $g, $b, $a) { - if ($a != 1) { // rgba - return 'rgba('.$r.', '.$g.', '.$b.', '.$a.')'; + if (($a = $this->number($a, null)) != 1) { // rgba + return 'rgba('.$r.$this->tagSeparator.$g.$this->tagSeparator.$b.$this->tagSeparator.$a.')'; } $h = sprintf("#%02x%02x%02x", $r, $g, $b); From acd7b3a254f25c6e59b3b8913ad83837e5ab113c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 14 Oct 2012 23:56:09 +0200 Subject: [PATCH 29/38] Add parser option to omit leading zeros --- scss.inc.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/scss.inc.php b/scss.inc.php index acec2219..e589351a 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -3353,6 +3353,7 @@ class scss_formatter { public $removeTrailingSemicolon = false; public $replaceColorNames = false; public $omitZeroUnit = false; + public $omitZeroLeading = false; public $stripComments = false; public $numberPrecision = 3; @@ -3396,6 +3397,13 @@ public function number($num, $unit) { if ($this->omitZeroUnit && $num == 0) { return 0; } + if ($this->omitZeroLeading) { + if ($num > 0 && $num < 1) { + $num = substr($num, 1); + } else if ($num < 0 && $num > -1) { + $num = '-' . substr($num, 2); + } + } return $num . $unit; } @@ -3526,6 +3534,7 @@ class scss_formatter_compressed extends scss_formatter { public $removeTrailingSemicolon = true; public $replaceColorNames = true; public $omitZeroUnit = true; + public $omitZeroLeading = true; public $stripComments = true; public function indentStr($n = 0) { From 470422b206ee63887eb97bf27421d9a16584c75a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:24:16 +0200 Subject: [PATCH 30/38] Add support for 'transparent' color keyword See: http://www.w3.org/TR/css3-color/#transparent --- scss.inc.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index e589351a..5b06d121 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1349,8 +1349,10 @@ protected function coerceColor($value) { case "keyword": $name = $value[1]; if (isset(self::$cssColors[$name])) { - list($r, $g, $b) = explode(',', self::$cssColors[$name]); - return array('color', $r, $g, $b); + $color = explode(',', self::$cssColors[$name]); + // array('color', $r, $g, $b); + array_unshift($color, 'color'); + return $color; } return null; } @@ -1912,6 +1914,7 @@ protected function lib_comparable($args) { } public static $cssColors = array( + 'transparent' => '0,0,0,0', 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', @@ -3371,6 +3374,9 @@ public function property($name, $value) { public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba + if ($a == 0 && $this->replaceColorNames) { + return 'transparent'; + } return 'rgba('.$r.$this->tagSeparator.$g.$this->tagSeparator.$b.$this->tagSeparator.$a.')'; } From 1e769cf3c4ac84c48af6a02213d7f45c8c6753fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:45:14 +0200 Subject: [PATCH 31/38] Fix color keywords to actually be transparent --- scss.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index 5b06d121..bcf366b2 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1347,7 +1347,7 @@ protected function coerceColor($value) { switch ($value[0]) { case "color": return $value; case "keyword": - $name = $value[1]; + $name = strtolower($value[1]); if (isset(self::$cssColors[$name])) { $color = explode(',', self::$cssColors[$name]); // array('color', $r, $g, $b); From 2f3eeba868f73f2ea63eb20ed7b4cc333cbee01d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 00:51:56 +0200 Subject: [PATCH 32/38] Fix assertion arguments --- tests/ApiTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/ApiTest.php b/tests/ApiTest.php index b4406784..333d23e6 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -14,14 +14,14 @@ public function testUserFunction() { }); $this->assertEquals( - $this->compile("result: add-two(10, 20);"), - "result: 30;"); + "result: 30;", + $this->compile("result: add-two(10, 20);")); } public function testImportMissing(){ $this->assertEquals( - $this->compile('@import "missing";'), - '@import "missing";'); + '@import "missing";', + $this->compile('@import "missing";')); } public function testImportCustomCallback(){ @@ -30,8 +30,8 @@ public function testImportCustomCallback(){ }); $this->assertEquals( - $this->compile('@import "variables.css";'), - trim(file_get_contents(__DIR__.'/outputs/variables.css'))); + trim(file_get_contents(__DIR__.'/outputs/variables.css')), + $this->compile('@import "variables.css";')); } public function compile($str) { From 3313429bdabf0ff8080b9c5f9b08888ea9dfccc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 01:16:15 +0200 Subject: [PATCH 33/38] Add unit tests for compressed formatter --- tests/CompressTest.php | 88 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/CompressTest.php diff --git a/tests/CompressTest.php b/tests/CompressTest.php new file mode 100644 index 00000000..0f831d55 --- /dev/null +++ b/tests/CompressTest.php @@ -0,0 +1,88 @@ +scss = new scssc(); + $this->scss->setFormatter(new scss_formatter_compressed()); + } + + public function testCompressTwice(){ + $code = file_get_contents(__DIR__.'/inputs/variables.scss'); + + $result = $this->scss->compile($code); + + $this->assertEquals($result,$this->scss->compile($result)); + } + + public function testCompressFormat(){ + // check removing trailing semicolons + $this->assertEquals('@import "missing";a{border:0;content:";";border:0}b{border:0}',$this->scss->compile('@import "missing";a{border:0;content:";";border:0;}b{border:0;}')); + + // check removing excessive whitespace + $this->assertEquals('body,a,a:hover{border:0}',$this->scss->compile('body, a, a:hover { border: 0 ; }')); + + // check removing empty blocks and comments + $this->assertEquals('a{display:hidden}',$this->scss->compile(' /* comment */ b{a{ }} strong{;;;} a{display:hidden;;;} b{/*inner comment*/}')); + } + + /** + * @dataProvider equalColorsProvider + */ + public function testCompressColor($in,$out) { + $this->assertEquals('color:'.$out,$this->scss->compile('color:'.$in)); + } + + public function equalColorsProvider() { + return $this->prepareSet(array( + 'red' => 'red', // unchanged color keyword as shortest + '#000' => '#000', // unchanged short hex + '#00ff00' => '#0f0', // short hex + '#7abAaA' => '#7abaaa', // always use lowercase hex + 'rgb(255,255,255)' => '#fff', // short hex + 'rgb(123,121,121)' => '#7b7979', // hex instead of RGB notation + 'rgb( 0 , 0 , 255 )' => '#00f', // short hex for excessive whitespace + 'rgba(123,121,121,1)' => '#7b7979', // full alpha => use hex + 'rgba(123,121,121,2)' => '#7b7979', // cap excessive alpha to full alpha + 'rgba(123,121,121,0.9999)' => '#7b7979', // round alpha component + 'rgba(123,121,121,0.5)' => 'rgba(123,121,121,.5)', // remove useless leading zero + 'rgba(123,121,121,0)' => 'transparent', // zero alpha => shorthand transparent color keyword + 'opacify(transparent, 1)' => '#000', // fully opaque 'transparent' is actually black + 'opacify(transparent, 0.3)' => 'rgba(0,0,0,.3)', // work with transparent color keyword + 'opacify(WhiTe,1)' => '#fff' // color keywords are actually case insensitive + )); + } + + /** + * @dataProvider equalNumbersProvider + */ + public function testCompressNumber($in,$out){ + $this->assertEquals('padding:'.$out,$this->scss->compile('padding:'.$in)); + } + + public function equalNumbersProvider(){ + return $this->prepareSet(array( + '14px' => '14px', // unchanged + '0px' => '0', // remove zero unit + '0em' => '0', + '0%' => '0', + '1.5em' => '1.5em', + '0.5em' => '.5em', // remove leading zero + '0.50pt' => '.5pt', // remove leading and trailing zero and keep unit + '-4.0' => '-4', // remove useless fraction + '4.000px' => '4px', + '4.9999' => '5', // round to next number + '4.99' => '4.99', + '-0.2pt' => '-.2pt', // remove leading zero for negative numbers + '-0pt' => '0', // negative zero is still zero + '+4%' => '4%', // remove useless postive number marker + '+0em' => '0' + )); + } + + private function prepareSet($set){ + return array_map(function($in, $out) { return array($in, $out); }, array_keys($set), $set); + } + +} From b0efae2db9b6f465ebb1f0108af3a30e2d019ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:30:03 +0200 Subject: [PATCH 34/38] Compress whitespace when joining lists --- scss.inc.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scss.inc.php b/scss.inc.php index bcf366b2..cb0331d8 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -937,7 +937,7 @@ protected function compileValue($value) { foreach ($items as &$item) { $item = $this->compileValue($item); } - return implode("$delim ", $items); + return $this->formatter->implodeList($delim, $items); case "interpolated": # node created by extractInterpolation list(, $interpolate, $left, $right) = $value; list(,, $whiteLeft, $whiteRight) = $interpolate; @@ -3371,7 +3371,11 @@ public function indentStr($n = 0) { public function property($name, $value) { return $name . $this->assignSeparator . $value . ";"; } - + + public function implodeList($delim, $items) { + return implode("$delim ", $items); + } + public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba if ($a == 0 && $this->replaceColorNames) { @@ -3546,6 +3550,14 @@ class scss_formatter_compressed extends scss_formatter { public function indentStr($n = 0) { return ""; } + + public function implodeList($delim, $list) { + // no delimiter => actually a whitespace separated list (as in "margin") + if ($delim == "") { + $delim = " "; + } + return implode($delim, $list); + } } class scss_server { From 7675799717ac89ffe0cad124151651440e92c966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:35:38 +0200 Subject: [PATCH 35/38] Compress unneeded whitespace around descendant selectors --- scss.inc.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/scss.inc.php b/scss.inc.php index cb0331d8..051b2bf1 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -324,7 +324,7 @@ protected function evalSelectorPart($piece) { protected function compileSelector($selector) { if (!is_array($selector)) return $selector; // media and the like - return implode(" ", array_map( + return $this->formatter->implodeSelectors(array_map( array($this, "compileSelectorPart"), $selector)); } @@ -3376,6 +3376,10 @@ public function implodeList($delim, $items) { return implode("$delim ", $items); } + public function implodeSelectors($selectors) { + return implode(" ", $selectors); + } + public function color($r, $g, $b, $a) { if (($a = $this->number($a, null)) != 1) { // rgba if ($a == 0 && $this->replaceColorNames) { @@ -3558,6 +3562,25 @@ public function implodeList($delim, $list) { } return implode($delim, $list); } + + public function implodeSelectors($selectors){ + $ret = ''; + $ws = false; + foreach($selectors as $selector){ + // do not use whitespace around descendant selectors + if (in_array($selector,array('+','~','>'),true)) { + $ws = false; + }else{ + if ($ws) { + $ret .= ' '; + } else { + $ws = true; + } + } + $ret .= $selector; + } + return $ret; + } } class scss_server { From c3958f889426049498682fc3d1fc507a8931a1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 14:42:40 +0200 Subject: [PATCH 36/38] Additional tests for whitespace --- tests/CompressTest.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/CompressTest.php b/tests/CompressTest.php index 0f831d55..5974e0a4 100644 --- a/tests/CompressTest.php +++ b/tests/CompressTest.php @@ -25,6 +25,18 @@ public function testCompressFormat(){ // check removing empty blocks and comments $this->assertEquals('a{display:hidden}',$this->scss->compile(' /* comment */ b{a{ }} strong{;;;} a{display:hidden;;;} b{/*inner comment*/}')); + + // do not mess around with attribute selectors + // http://www.w3.org/TR/CSS2/selector.html#attribute-selectors + $this->assertEquals('input[type="image"][disabled]{opacity:.5}',$this->scss->compile(' input[type="image"][disabled] { opacity: 0.5; } ')); + + // remove optional whitespace for child selectors, but keep whitespace for descendant selectors + // http://www.w3.org/TR/CSS2/selector.html#child-selectors + $this->assertEquals('a+b,c>d,e f{display:none}a.b>c d:not(.e){border:0}',$this->scss->compile(' a + b , c > d , e f { display: none; } a.b > c d:not(.e) { border: 0; }')); + + // remove whitespace around list delimiter, but keep whitespace between space separated values + $this->assertEquals('*{a:red,green,blue}a{margin:0 0 0 0}',$this->scss->compile('* { a: red, green , blue ; } a { margin: 0 0 0 0; }')); + } /** From 69fc72397ad7be90c0b7e8e688e6aa2477642687 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Mon, 15 Oct 2012 16:22:29 +0200 Subject: [PATCH 37/38] Test compressing media queries --- tests/CompressTest.php | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/CompressTest.php b/tests/CompressTest.php index 5974e0a4..86b35e35 100644 --- a/tests/CompressTest.php +++ b/tests/CompressTest.php @@ -7,7 +7,7 @@ public function setUp() { $this->scss = new scssc(); $this->scss->setFormatter(new scss_formatter_compressed()); } - + public function testCompressTwice(){ $code = file_get_contents(__DIR__.'/inputs/variables.scss'); @@ -15,7 +15,7 @@ public function testCompressTwice(){ $this->assertEquals($result,$this->scss->compile($result)); } - + public function testCompressFormat(){ // check removing trailing semicolons $this->assertEquals('@import "missing";a{border:0;content:";";border:0}b{border:0}',$this->scss->compile('@import "missing";a{border:0;content:";";border:0;}b{border:0;}')); @@ -36,16 +36,36 @@ public function testCompressFormat(){ // remove whitespace around list delimiter, but keep whitespace between space separated values $this->assertEquals('*{a:red,green,blue}a{margin:0 0 0 0}',$this->scss->compile('* { a: red, green , blue ; } a { margin: 0 0 0 0; }')); - } - + + /** + * @dataProvider equalMediaProvider + */ + public function testMediaQueries($in,$out){ + $this->assertEquals('@media '.$out.'{a{border:0}}',$this->scss->compile('@media '.$in.' {a{border:0}}')); + } + + public function equalMediaProvider() { + // http://www.w3.org/TR/css3-mediaqueries/ + return $this->prepareSet(array( + 'only screen' => 'only screen', // unchanged + ' only screen ' => 'only screen', // remove optional whitespace +// 'screen, print' => 'screen,print', // remove whitespace between OR'ed queries + '(min-width: 100px )' => '(min-width:100px)', // check media features + '(min-width: 0px)' => '(min-width:0)', // remove zero unit + 'only screen and (min-width: 0px) and (max-width: 1000px)' => 'only screen and (min-width:0) and (max-width:1000px)', +// 'handheld, only screen and (max-width: 1000px)' => 'handheld,only screen and (max-width:1000px)', +// 'screen and (device-aspect-ratio: 16/9) , print and (min-resolution: 300dpi)' => 'screen and (device-aspect-ratio:16/9),print and (min-resolution:300dpi)' + )); + } + /** * @dataProvider equalColorsProvider */ public function testCompressColor($in,$out) { $this->assertEquals('color:'.$out,$this->scss->compile('color:'.$in)); } - + public function equalColorsProvider() { return $this->prepareSet(array( 'red' => 'red', // unchanged color keyword as shortest @@ -65,14 +85,14 @@ public function equalColorsProvider() { 'opacify(WhiTe,1)' => '#fff' // color keywords are actually case insensitive )); } - + /** * @dataProvider equalNumbersProvider */ public function testCompressNumber($in,$out){ $this->assertEquals('padding:'.$out,$this->scss->compile('padding:'.$in)); } - + public function equalNumbersProvider(){ return $this->prepareSet(array( '14px' => '14px', // unchanged From bb33f93b0b37a90b85d6738d9b1718a11302743a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 28 Oct 2012 01:57:19 +0200 Subject: [PATCH 38/38] Enable tests for media query lists --- tests/CompressTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/CompressTest.php b/tests/CompressTest.php index 86b35e35..82f638fc 100644 --- a/tests/CompressTest.php +++ b/tests/CompressTest.php @@ -50,12 +50,12 @@ public function equalMediaProvider() { return $this->prepareSet(array( 'only screen' => 'only screen', // unchanged ' only screen ' => 'only screen', // remove optional whitespace -// 'screen, print' => 'screen,print', // remove whitespace between OR'ed queries + 'screen, print' => 'screen,print', // remove whitespace between OR'ed queries '(min-width: 100px )' => '(min-width:100px)', // check media features '(min-width: 0px)' => '(min-width:0)', // remove zero unit 'only screen and (min-width: 0px) and (max-width: 1000px)' => 'only screen and (min-width:0) and (max-width:1000px)', -// 'handheld, only screen and (max-width: 1000px)' => 'handheld,only screen and (max-width:1000px)', -// 'screen and (device-aspect-ratio: 16/9) , print and (min-resolution: 300dpi)' => 'screen and (device-aspect-ratio:16/9),print and (min-resolution:300dpi)' + 'handheld, only screen and (max-width: 1000px)' => 'handheld,only screen and (max-width:1000px)', + 'screen and (device-aspect-ratio: 16/9) , print and (min-resolution: 300dpi)' => 'screen and (device-aspect-ratio:16/9),print and (min-resolution:300dpi)' )); }