diff --git a/composer.json b/composer.json index a2447af3..52214935 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,5 @@ { - "name": "leafo/lessphp", + "name": "tocsick/lessphp", "type": "library", "description": "lessphp is a compiler for LESS written in PHP.", "homepage": "http://leafo.net/lessphp/", diff --git a/lessc.inc.php b/lessc.inc.php index 2292f219..bc64c10f 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -51,6 +51,12 @@ class lessc { public $mPrefix = '$'; // prefix of abstract blocks public $parentSelector = '&'; + static public $lengths = array( "px", "m", "cm", "mm", "in", "pt", "pc" ); + static public $times = array( "s", "ms" ); + static public $angles = array( "rad", "deg", "grad", "turn" ); + + static public $lengths_to_base = array( 1, 3779.52755906, 37.79527559, 3.77952756, 96, 1.33333333, 16 ); + public $importDisabled = false; public $importDir = ''; @@ -885,7 +891,7 @@ public function compileValue($value) { protected function lib_pow($args) { list($base, $exp) = $this->assertArgs($args, 2, "pow"); - return pow($this->assertNumber($base), $this->assertNumber($exp)); + return array( "number", pow($this->assertNumber($base), $this->assertNumber($exp)), $args[2][0][2] ); } protected function lib_pi() { @@ -894,9 +900,67 @@ protected function lib_pi() { protected function lib_mod($args) { list($a, $b) = $this->assertArgs($args, 2, "mod"); - return $this->assertNumber($a) % $this->assertNumber($b); + return array( "number", $this->assertNumber($a) % $this->assertNumber($b), $args[2][0][2] ); + } + + protected function lib_convert($args) { + list($value, $to) = $this->assertArgs($args, 2, "convert"); + + // If it's a keyword, grab the string version instead + if( is_array( $to ) && $to[0] == "keyword" ) + $to = $to[1]; + + return $this->convert( $value, $to ); + } + + protected function lib_abs($num) { + return array( "number", abs($this->assertNumber($num)), $num[2] ); + } + + protected function lib_min($args) { + $values = $this->assertMinArgs($args, 1, "min"); + + $first_format = $values[0][2]; + + $min_index = 0; + $min_value = $values[0][1]; + + for( $a = 0; $a < sizeof( $values ); $a++ ) + { + $converted = $this->convert( $values[$a], $first_format ); + + if( $converted[1] < $min_value ) + { + $min_index = $a; + $min_value = $values[$a][1]; + } + } + + return $values[ $min_index ]; + } + + protected function lib_max($args) { + $values = $this->assertMinArgs($args, 1, "max"); + + $first_format = $values[0][2]; + + $max_index = 0; + $max_value = $values[0][1]; + + for( $a = 0; $a < sizeof( $values ); $a++ ) + { + $converted = $this->convert( $values[$a], $first_format ); + + if( $converted[1] > $max_value ) + { + $max_index = $a; + $max_value = $values[$a][1]; + } + } + + return $values[ $max_index ]; } - + protected function lib_tan($num) { return tan($this->assertNumber($num)); } @@ -1306,6 +1370,21 @@ public function assertArgs($value, $expectedArgs, $name="") { return $values; } } + + public function assertMinArgs($value, $expectedMinArgs, $name="") { + if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); + $values = $value[2]; + $numValues = count($values); + if ($expectedMinArgs > $numValues) { + if ($name) { + $name = $name . ": "; + } + + $this->throwError("${name}expecting at least $expectedMinArgs arguments, got $numValues"); + } + + return $values; + } protected function toHSL($color) { if ($color[0] == 'hsl') return $color; @@ -1674,7 +1753,79 @@ protected function stringConcatenate($left, $right) { return $strRight; } } - + + protected function convert( $number, $to ) + { + $value = $this->assertNumber( $number ); + $from = $number[2]; + + // easy out + if( $from == $to ) + return $number; + + // check if the from value is a length + if( ( $from_index = array_search( $from, self::$lengths ) ) !== false ) { + // make sure to value is too + if( in_array( $to, self::$lengths ) ) { + // do the actual conversion + $to_index = array_search( $to, self::$lengths ); + $px = $value * self::$lengths_to_base[ $from_index ]; + $result = $px * ( 1 / self::$lengths_to_base[ $to_index ] ); + + $result = round( $result, 8 ); + return array( "number", $result, $to ); + } + } + + // do the same check for times + if( in_array( $from, self::$times ) ) { + if( in_array( $to, self::$times ) ) { + // currently only ms and s are valid + if( $to == "ms" ) + $result = $value * 1000; + else + $result = $value / 1000; + + $result = round( $result, 8 ); + return array( "number", $result, $to ); + } + } + + // lastly check for an angle + if( in_array( $from, self::$angles ) ) { + // convert whatever angle it is into degrees + if( $from == "rad" ) + $deg = rad2deg( $value ); + + else if( $from == "turn" ) + $deg = $value * 360; + + else if( $from == "grad" ) + $deg = $value / (400 / 360); + + else + $deg = $value; + + // Then convert it from degrees into desired unit + if( $to == "deg" ) + $result = $deg; + + if( $to == "rad" ) + $result = deg2rad( $deg ); + + if( $to == "turn" ) + $result = $value / 360; + + if( $to == "grad" ) + $result = $value * (400 / 360); + + $result = round( $result, 8 ); + return array( "number", $result, $to ); + } + + // we don't know how to convert these + $this->throwError( "Cannot convert {$from} to {$to}" ); + } // make sure a color's components don't go out of bounds protected function fixColor($c) { diff --git a/tests/ErrorHandlingTest.php b/tests/ErrorHandlingTest.php index de02f065..2adb0d51 100644 --- a/tests/ErrorHandlingTest.php +++ b/tests/ErrorHandlingTest.php @@ -78,4 +78,44 @@ public function testGuardUnmatchedType() { '.selector { .colors-only("string value"); }' ); } + + /** + * @expectedException Exception + * @expectedExceptionMessage expecting at least 1 arguments, got 0 + */ + public function testMinNoArguments() { + $this->compile( + '.selector{ min: min(); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage expecting at least 1 arguments, got 0 + */ + public function testMaxNoArguments() { + $this->compile( + '.selector{ max: max(); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Cannot convert % to px + */ + public function testMaxIncompatibleTypes() { + $this->compile( + '.selector{ max: max( 10px, 5% ); }' + ); + } + + /** + * @expectedException Exception + * @expectedExceptionMessage Cannot convert px to s + */ + public function testConvertIncompatibleTypes() { + $this->compile( + '.selector{ convert: convert( 10px, s ); }' + ); + } } diff --git a/tests/InputTest.php b/tests/InputTest.php index 32db95bc..c0ce1ce0 100644 --- a/tests/InputTest.php +++ b/tests/InputTest.php @@ -44,7 +44,7 @@ public function testInputFile($inFname) { $input = file_get_contents($inFname); $output = file_get_contents($outFname); - + $this->assertEquals($output, $this->less->parse($input)); } diff --git a/tests/inputs/builtins.less b/tests/inputs/builtins.less index f9301e04..661335a2 100644 --- a/tests/inputs/builtins.less +++ b/tests/inputs/builtins.less @@ -72,8 +72,22 @@ format { pow: pow(2,4); pi: pi(); - mod: mod(14,10); - + mod1: mod(14,10); + mod2: mod(14px,10); + + abs1: abs(-5px); + abs2: abs(5%); + abs3: abs(-10); + + min1: min(2px,5px,10px); + min2: min(5,1,8); + + max1: max(2%,20%,5%); + max2: max(5,4,10); + + max-convert: max(1px,1cm); + min-convert: min(1px,1cm); + tan: tan(1); cos: cos(1); sin: sin(1); @@ -85,6 +99,27 @@ format { sqrt: sqrt(8); } +#convert { + px2cm: convert(1px,cm); + px2mm: convert(1px,mm); + px2in: convert(1px,in); + px2pt: convert(1px,pt); + px2pc: convert(1px,pc); + + cm2px: convert(1cm,px); + in2px: convert(1in,px); + cm2in: convert(1cm,in); + + s2ms: convert(1s,ms); + ms2s: convert(1ms,s); + + deg2rad: convert(1deg,rad); + rad2deg: convert(1rad,deg); + deg2turn: convert(1deg,turn); + turn2deg: convert(1turn,deg); + deg2grad: convert(1deg,grad); + grad2deg: convert(1grad,deg); +} #unit { @unit: "em"; diff --git a/tests/outputs/builtins.css b/tests/outputs/builtins.css index 6ac21f2c..95bad4d0 100644 --- a/tests/outputs/builtins.css +++ b/tests/outputs/builtins.css @@ -44,7 +44,17 @@ format { ex4: 2; pow: 16; pi: 3.1415926535898; - mod: 4; + mod1: 4; + mod2: 4px; + abs1: 5px; + abs2: 5%; + abs3: 10; + min1: 2px; + min2: 1; + max1: 20%; + max2: 10; + max-convert: 1cm; + min-convert: 1px; tan: 1.5574077246549; cos: 0.54030230586814; sin: 0.8414709848079; @@ -53,6 +63,24 @@ format { asin: 1.5707963267949rad; sqrt: 2.8284271247462; } +#convert { + px2cm: 0.02645833cm; + px2mm: 0.26458333mm; + px2in: 0.01041667in; + px2pt: 0.75pt; + px2pc: 0.0625pc; + cm2px: 37.79527559px; + in2px: 96px; + cm2in: 0.39370079in; + s2ms: 1000ms; + ms2s: 0.001s; + deg2rad: 0.01745329rad; + rad2deg: 57.29577951deg; + deg2turn: 0.00277778turn; + turn2deg: 360deg; + deg2grad: 1.11111111grad; + grad2deg: 0.9deg; +} #unit { unit-lit: 10; unit-arg: 10s;