From 0cda87d93053256aa8e4de34b8034bb881cc153b Mon Sep 17 00:00:00 2001 From: Danny van der Sluijs Date: Wed, 31 Jul 2013 14:03:15 +0200 Subject: [PATCH 01/28] Solves 302 in upstream. --- lessc.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lessc.inc.php b/lessc.inc.php index 5c81ad2a..ff887359 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -304,9 +304,9 @@ protected function sortProps($props, $split = false) { } if ($split) { - return array(array_merge($vars, $imports), $other); + return array(array_merge($imports, $vars), $other); } else { - return array_merge($vars, $imports, $other); + return array_merge($imports, $vars, $other); } } From 119d94329581e6d6f41627f22faf0cee0b21eb0f Mon Sep 17 00:00:00 2001 From: Camillus Gerard Cai Date: Fri, 11 Apr 2014 12:43:37 +0800 Subject: [PATCH 02/28] Added basic PHPUnit XML configuration. --- phpunit.xml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 phpunit.xml diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..5fea3261 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + From 6b7e83f2e96880b631c389bf7bbf47a02ccbe1cb Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Fri, 11 Apr 2014 13:20:07 -0700 Subject: [PATCH 03/28] hhvm passes now :) --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 810c42bb..586a11c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,4 @@ script: - phpunit tests matrix: - allow_failures: - - php: hhvm fast_finish: true From 84b1fda0d4de25ee4dce5324da8bff8d1f48f596 Mon Sep 17 00:00:00 2001 From: Ori Livneh Date: Tue, 22 Apr 2014 17:05:21 -0700 Subject: [PATCH 04/28] Set visibility of lessc::compileValue to 'public' lessc::compileValue is a useful helper for implementing custom functions. --- lessc.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lessc.inc.php b/lessc.inc.php index 9631a99f..c8b5f15f 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -819,7 +819,7 @@ protected function compileProp($prop, $block, $out) { * The input is expected to be reduced. This function will not work on * things like expressions and variables. */ - protected function compileValue($value) { + public function compileValue($value) { switch ($value[0]) { case 'list': // [1] - delimiter From ef9f8a06854ce32bcdd7d32c9e4449f00f62f888 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elan=20Ruusam=C3=A4e?= Date: Sat, 27 Sep 2014 15:54:09 +0300 Subject: [PATCH 05/28] Update composer.json add plessc to bin --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index 0f06ba09..d7e74d7c 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ "homepage": "http://leafo.net" } ], + "bin": ["plessc"], "autoload": { "classmap": ["lessc.inc.php"] }, From 0f5a7f5545d2bcf4e9fad9a228c8ad89cc9aa283 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Mon, 24 Nov 2014 18:39:20 +0000 Subject: [PATCH 06/28] bump version --- README.md | 6 +++--- docs/docs.md | 4 ++-- lessc.inc.php | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d39c3f8c..1379ddce 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# lessphp v0.4.0 -### +[![Build Status](https://travis-ci.org/leafo/lessphp.svg?branch=master)](https://travis-ci.org/leafo/lessphp) -[![Build Status](https://secure.travis-ci.org/leafo/lessphp.png)](http://travis-ci.org/leafo/lessphp) +# lessphp v0.5.0 +### `lessphp` is a compiler for LESS written in PHP. The documentation is great, so check it out: . diff --git a/docs/docs.md b/docs/docs.md index b6193d2f..112dc2ee 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -1,8 +1,8 @@ - title: v0.4.0 documentation + title: v0.5.0 documentation link_to_home: true -- -

Documentation v0.4.0

+

Documentation v0.5.0

$index
diff --git a/lessc.inc.php b/lessc.inc.php index 2f407c7f..2292f219 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -1,7 +1,7 @@ Date: Mon, 24 Nov 2014 19:21:51 +0000 Subject: [PATCH 07/28] tests: Set up phpunit via config file Use a config file for phpunit and specify the version and script via composer instead of harcoding for Travis CI. Also remove the obsolete self-update for composer that was needed for a bug in an older version of Travis CI workers. Fix indentation in composer.json (consistently use 4 spaces). Remove redundant arguments from composer install command. --- .travis.yml | 9 ++------- composer.json | 10 ++++++++-- phpunit.xml.dist | 8 ++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 phpunit.xml.dist diff --git a/.travis.yml b/.travis.yml index 586a11c1..28da1729 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,12 @@ language: php - php: - 5.3 - 5.4 - 5.5 - hhvm - before_script: - - composer self-update - - composer install --no-interaction --prefer-source --dev - + - composer install script: - - phpunit tests - + - composer test matrix: fast_finish: true diff --git a/composer.json b/composer.json index 0f06ba09..b6b466fa 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,8 @@ "description": "lessphp is a compiler for LESS written in PHP.", "homepage": "http://leafo.net/lessphp/", "license": [ - "MIT", - "GPL-3.0" + "MIT", + "GPL-3.0" ], "authors": [ { @@ -21,5 +21,11 @@ "branch-alias": { "dev-master": "0.4.x-dev" } + }, + "require-dev": { + "phpunit/phpunit": "4.3.*" + }, + "scripts": { + "test": "phpunit" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..5e209515 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,8 @@ + + + + + ./tests + + + From ce996f22841a9d2f7a5da95ae654df3763552791 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 24 Nov 2014 20:31:18 +0000 Subject: [PATCH 08/28] Allow newer versions of phpunit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b6b466fa..255a7669 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } }, "require-dev": { - "phpunit/phpunit": "4.3.*" + "phpunit/phpunit": "~4.3" }, "scripts": { "test": "phpunit" From 82b7831e0d3241c42ba5c419f5af16fb85434fc5 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 24 Nov 2014 20:32:34 +0000 Subject: [PATCH 09/28] Update .travis.yml --- .travis.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 28da1729..a6e78dd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: php + php: - 5.3 - 5.4 - 5.5 + - 5.6 - hhvm -before_script: - - composer install + +sudo: false + +install: + - travis_retry composer install --no-interaction --prefer-source + script: - composer test -matrix: - fast_finish: true From 554905131144915b0ef12ca4163e3ffab34cd093 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Mon, 15 Oct 2018 23:49:41 +0100 Subject: [PATCH 10/28] tests: Drop PHP 5.3 from matrix, add PHP 7 PHP 5.3 is EOL, and composer no longer supports it. For Travis CI, the build is also failing outstanding PRs due to PHP 5.3 requiring Ubuntu Precise (also EOL), whereas the default image uses uses Ubuntu Trusty. --- .travis.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a6e78dd8..0bac14bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,20 @@ language: php php: - - 5.3 - 5.4 - 5.5 - 5.6 - hhvm + - 7.0 + - 7.1 + - 7.2 + - 7.3 + +matrix: + allow_failures: + - php: 7.1 + - php: 7.2 + - php: 7.3 sudo: false From d953e5777e68fda96e0f7c3a23bb96a5d105a081 Mon Sep 17 00:00:00 2001 From: paladox Date: Thu, 11 Jun 2015 22:06:32 +0100 Subject: [PATCH 11/28] Update LICENSE * Remove outdated version (not needed). * Update year to last year of significant change or release by author. Closes #603. --- LICENSE | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 49c9ea41..0a07ad9f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -For ease of distribution, lessphp 0.4.0 is under a dual license. +For ease of distribution, lessphp is under a dual license. You are free to pick which one suits your needs. @@ -9,7 +9,7 @@ MIT LICENSE -Copyright (c) 2013 Leaf Corcoran, http://leafo.net/lessphp +Copyright (c) 2014Leaf Corcoran, http://leafo.net/lessphp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the From 0d4cfd966e4ac13e88df996b186edbfcb40005f2 Mon Sep 17 00:00:00 2001 From: Zimzat Date: Fri, 5 Dec 2014 15:17:25 -0600 Subject: [PATCH 12/28] docs: Fix 'checkedCompile' reference in readme and docs Closes #578. --- README.md | 2 +- docs/docs.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1379ddce..d3d87168 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ result or write it to the path specified by an optional second argument. echo $less->compileFile("input.less"); ``` -The `compileChecked` method is like `compileFile`, but it only compiles if the output +The `checkedCompile` method is like `compileFile`, but it only compiles if the output file doesn't exist or it's older than the input file: ```php diff --git a/docs/docs.md b/docs/docs.md index 112dc2ee..00a81d4b 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -1023,7 +1023,7 @@ result or write it to the path specified by an optional second argument. echo $less->compileFile("input.less"); ``` -The `compileChecked` method is like `compileFile`, but it only compiles if the output +The `checkedCompile` method is like `compileFile`, but it only compiles if the output file doesn't exist or it's older than the input file: ```php From ed6e4d8a0fc774dc4c28011a6424322792ed6fae Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 04:29:25 +0100 Subject: [PATCH 13/28] build: Remove outdated composer branch-alias --- composer.json | 5 ----- 1 file changed, 5 deletions(-) diff --git a/composer.json b/composer.json index a2447af3..19bf197f 100644 --- a/composer.json +++ b/composer.json @@ -18,11 +18,6 @@ "autoload": { "classmap": ["lessc.inc.php"] }, - "extra": { - "branch-alias": { - "dev-master": "0.4.x-dev" - } - }, "require-dev": { "phpunit/phpunit": "~4.3" }, From 601066eb2bd88cd104831222f3af1d2415a08a37 Mon Sep 17 00:00:00 2001 From: Brandon Kraft Date: Mon, 15 Oct 2018 22:33:54 -0500 Subject: [PATCH 14/28] Correct typo in patternMatch() comment Closes #601. --- lessc.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lessc.inc.php b/lessc.inc.php index 2292f219..521fcb55 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -570,7 +570,7 @@ protected function patternMatch($block, $orderedArgs, $keywordArgs) { return true; // not having enough is handled above } else { $numMatched = $i + 1; - // greater than becuase default values always match + // greater than because default values always match return $numMatched >= count($orderedArgs); } } From 0b0dd1031782463ccbe9cb0b1edd4168f9666906 Mon Sep 17 00:00:00 2001 From: Rafael Cano Date: Mon, 15 Oct 2018 22:36:54 -0500 Subject: [PATCH 15/28] lessify: more portable shebang via 'env' Same as was done for 'plessc' in 8b6e7a7 (#306). Closes #610. --- lessify | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lessify b/lessify index becc5388..f5928e77 100755 --- a/lessify +++ b/lessify @@ -1,4 +1,4 @@ -#!/usr/bin/php +#!/usr/bin/env php Date: Sun, 12 Nov 2017 07:49:27 +0200 Subject: [PATCH 16/28] Add tint() and shade() functions support See and . Closes #634. --- lessc.inc.php | 44 ++++++++++++++++++++++++++++++++++++++++ tests/inputs/colors.less | 5 +++++ tests/outputs/colors.css | 4 ++++ 3 files changed, 53 insertions(+) diff --git a/lessc.inc.php b/lessc.inc.php index 521fcb55..a5907691 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -1199,6 +1199,50 @@ protected function lib_percentage($arg) { return array("number", $num*100, "%"); } + /** + * Mix color with white in variable proportion. + * + * It is the same as calling `mix(#ffffff, @color, @weight)`. + * + * tint(@color, [@weight: 50%]); + * + * http://lesscss.org/functions/#color-operations-tint + * + * @return array Color + */ + protected function lib_tint($args) { + $white = ['color', 255, 255, 255]; + if ($args[0] == 'color') { + return $this->lib_mix([ 'list', ',', [$white, $args] ]); + } elseif ($args[0] == "list" && count($args[2]) == 2) { + return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]); + } else { + $this->throwError("tint expects (color, weight)"); + } + } + + /** + * Mix color with black in variable proportion. + * + * It is the same as calling `mix(#000000, @color, @weight)` + * + * shade(@color, [@weight: 50%]); + * + * http://lesscss.org/functions/#color-operations-shade + * + * @return array Color + */ + protected function lib_shade($args) { + $black = ['color', 0, 0, 0]; + if ($args[0] == 'color') { + return $this->lib_mix([ 'list', ',', [$black, $args] ]); + } elseif ($args[0] == "list" && count($args[2]) == 2) { + return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]); + } else { + $this->throwError("shade expects (color, weight)"); + } + } + // mixes two colors by weight // mix(@color1, @color2, [@weight: 50%]); // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method diff --git a/tests/inputs/colors.less b/tests/inputs/colors.less index 7fd47a3c..90d01728 100644 --- a/tests/inputs/colors.less +++ b/tests/inputs/colors.less @@ -37,6 +37,11 @@ body { two1: fadeout(#029f23, 10%); two2: fadein(#029f23, -10%); + tint1: tint(#e0e0e0); + tint2: tint(#e0e0e0, 40%); + + shade1: shade(#e0e0e0); + shade2: shade(#e0e0e0, 40%); three1: fadein(rgba(1,2,3, 0.5), 10%); three2: fadeout(rgba(1,2,3, 0.5), -10%); diff --git a/tests/outputs/colors.css b/tests/outputs/colors.css index 5310ffb0..9a6243c6 100644 --- a/tests/outputs/colors.css +++ b/tests/outputs/colors.css @@ -26,6 +26,10 @@ body { one2: #abcdef; two1: rgba(2,159,35,0.9); two2: rgba(2,159,35,0.9); + tint1: #f0f0f0; + tint2: #ececec; + shade1: #707070; + shade2: #868686; three1: rgba(1,2,3,0.6); three2: rgba(1,2,3,0.6); four1: rgba(1,2,3,0); From 598c96af5cacefcfbc218e907589331878011bb9 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 04:57:20 +0100 Subject: [PATCH 17/28] Fix broken luma() implementation Two issues: * The formula was incomplete. This is now fixed. * The method was returning a float instead of a color array, which was used internally for lib_contrast, but was invalid as return value to the parser directly. The internal utility is now separate from lib_luma. As side-effect, this also fixes the following for PHP 7.1: > PHP Warning: A non-numeric value encountered Closes #626. --- lessc.inc.php | 21 +++++++++++++++++---- tests/inputs/colors.less | 4 ++++ tests/outputs/colors.css | 3 +++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lessc.inc.php b/lessc.inc.php index a5907691..cfff748c 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -1302,22 +1302,35 @@ protected function lib_contrast($args) { $lightColor = $this->coerceColor($lightColor); //Figure out which is actually light and dark! - if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) { + if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) { $t = $lightColor; $lightColor = $darkColor; $darkColor = $t; } $inputColor_alpha = $this->lib_alpha($inputColor); - if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) { + if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) { return $lightColor; } return $darkColor; } + private function toLuma($color) { + list(, $r, $g, $b) = $this->coerceColor($color); + + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + + $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4); + $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4); + $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4); + + return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b); + } + protected function lib_luma($color) { - $color = $this->coerceColor($color); - return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255); + return array("number", round($this->toLuma($color) * 100, 8), "%"); } diff --git a/tests/inputs/colors.less b/tests/inputs/colors.less index 90d01728..588bfb69 100644 --- a/tests/inputs/colors.less +++ b/tests/inputs/colors.less @@ -100,6 +100,10 @@ fade { color2: contrast(#fff, red, blue); } +.luma { + color: luma(rgb(100, 200, 30)); +} + .percent { per: percentage(0.5); } diff --git a/tests/outputs/colors.css b/tests/outputs/colors.css index 9a6243c6..0826a135 100644 --- a/tests/outputs/colors.css +++ b/tests/outputs/colors.css @@ -69,6 +69,9 @@ fade { color1: #ff0000; color2: #0000ff; } +.luma { + color: 44.11161568%; +} .percent { per: 50%; } From c77692e78e556922e781aad81a766d8dee7498c0 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 04:59:30 +0100 Subject: [PATCH 18/28] build: Remove PHP 7 from 'allow_failures' PHP 7.1, 7.2 and 7.3 are passing as of 598c96af5cac. --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0bac14bb..65310f97 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,12 +10,6 @@ php: - 7.2 - 7.3 -matrix: - allow_failures: - - php: 7.1 - - php: 7.2 - - php: 7.3 - sudo: false install: From 4b18840a32263609f781060a19385e7cdda716e4 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 05:05:31 +0100 Subject: [PATCH 19/28] tests: Update to latest PHPUnit 4.x and use its namespaced classes Closes #616. --- composer.json | 2 +- tests/ApiTest.php | 2 +- tests/ErrorHandlingTest.php | 2 +- tests/InputTest.php | 2 +- tests/README.md | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 19bf197f..65da95be 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "classmap": ["lessc.inc.php"] }, "require-dev": { - "phpunit/phpunit": "~4.3" + "phpunit/phpunit": "^4.8.36" }, "scripts": { "test": "phpunit" diff --git a/tests/ApiTest.php b/tests/ApiTest.php index fe1bbffa..f8c543b9 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -2,7 +2,7 @@ require_once __DIR__ . "/../lessc.inc.php"; -class ApiTest extends PHPUnit_Framework_TestCase { +class ApiTest extends PHPUnit\Framework\TestCase { public function setUp() { $this->less = new lessc(); $this->less->importDir = array(__DIR__ . "/inputs/test-imports"); diff --git a/tests/ErrorHandlingTest.php b/tests/ErrorHandlingTest.php index de02f065..3a81d22e 100644 --- a/tests/ErrorHandlingTest.php +++ b/tests/ErrorHandlingTest.php @@ -1,7 +1,7 @@ less = new lessc(); } diff --git a/tests/InputTest.php b/tests/InputTest.php index 32db95bc..c0829a11 100644 --- a/tests/InputTest.php +++ b/tests/InputTest.php @@ -12,7 +12,7 @@ function _quote($str) { return preg_quote($str, "/"); } -class InputTest extends PHPUnit_Framework_TestCase { +class InputTest extends PHPUnit\Framework\TestCase { protected static $importDirs = array("inputs/test-imports"); protected static $testDirs = array( diff --git a/tests/README.md b/tests/README.md index 85a75c05..d0118d95 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,4 +1,4 @@ -lessphp uses [phpunit](https://github.com/sebastianbergmann/phpunit/) for its tests +lessphp uses [PHPUnit](https://github.com/sebastianbergmann/phpunit/) for its tests * `InputTest.php` iterates through all the less files in `inputs/`, compiles them, then compares the result with the respective file in `outputs/`. From e52c6209559d83b2ee55cc0759ea58d1cf8da5b9 Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Fri, 21 Feb 2014 17:34:25 +0100 Subject: [PATCH 20/28] build: Add lessify binary to composer.json Fixes #391. Closes #540. --- composer.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 65da95be..c8259655 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,10 @@ "homepage": "http://leafo.net" } ], - "bin": ["plessc"], + "bin": [ + "plessc", + "lessify" + ], "autoload": { "classmap": ["lessc.inc.php"] }, From f65faed0757e0f33d3f1a661399f2e1a17ec7e87 Mon Sep 17 00:00:00 2001 From: glensc Date: Fri, 12 Jun 2015 12:17:05 +0300 Subject: [PATCH 21/28] plessc: remove PHP close tag from Closes #605. --- lessify | 2 -- plessc | 2 -- 2 files changed, 4 deletions(-) diff --git a/lessify b/lessify index f5928e77..b447e624 100755 --- a/lessify +++ b/lessify @@ -19,5 +19,3 @@ try { } catch (exception $e) { exit("Fatal error: ".$e->getMessage()."\n"); } - - diff --git a/plessc b/plessc index 1871dc77..1745315f 100755 --- a/plessc +++ b/plessc @@ -246,5 +246,3 @@ try { err($fa.$ex->getMessage()); exit(1); } - -?> From b89ac6ad4c0bc0b1ff3b2e8d76a9ede47e512c8a Mon Sep 17 00:00:00 2001 From: Ecor Date: Mon, 15 Oct 2018 21:29:09 -0700 Subject: [PATCH 22/28] docs: Fix typo in example domain Duplicated TLD and trailing slash removal because the variable is used here: @{url}/bg.png so the url var should be: http://example.com instead of http://example.com.com/ Closes #566. --- docs/docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs.md b/docs/docs.md index 00a81d4b..4b80db49 100644 --- a/docs/docs.md +++ b/docs/docs.md @@ -1239,7 +1239,7 @@ One for PHP and one for LESS. ```php $less->setVariables(array( - "url" => "'http://example.com.com/'" + "url" => "'http://example.com'" )); echo $less->compile("body { background: url("@{url}/bg.png"); }"); From dcb1a8b80e896736762267d6792772b8f4fe7b32 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 05:47:09 +0100 Subject: [PATCH 23/28] tests: Remove duplicate phpunit.xml file --- phpunit.xml | 7 ------- phpunit.xml.dist | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 phpunit.xml diff --git a/phpunit.xml b/phpunit.xml deleted file mode 100644 index 5fea3261..00000000 --- a/phpunit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - tests - - - diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 5e209515..e1c737f4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + ./tests From 6ad7de0553bee8a2cbb81efca5df43430311772d Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 05:48:02 +0100 Subject: [PATCH 24/28] tests: Add phpcs to composer test --- .phpcs.xml | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ composer.json | 9 +++++++-- 2 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 .phpcs.xml diff --git a/.phpcs.xml b/.phpcs.xml new file mode 100644 index 00000000..f991189b --- /dev/null +++ b/.phpcs.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . + + + vendor + diff --git a/composer.json b/composer.json index c8259655..bb82cb98 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,14 @@ "classmap": ["lessc.inc.php"] }, "require-dev": { - "phpunit/phpunit": "^4.8.36" + "phpunit/phpunit": "^4.8.36", + "squizlabs/php_codesniffer": "3.3.2" }, "scripts": { - "test": "phpunit" + "test": [ + "phpunit", + "phpcs -p -s" + ], + "fix": "phpcbf" } } From cffe518d69b56c630c0d64fcce44c83931c0354a Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 06:02:07 +0100 Subject: [PATCH 25/28] tests: Apply PSR2 style for 'Generic.WhiteSpace.DisallowTabIndent' I'm already starting to regret this. Obviously, tabs are the only correct way of indenting code. But alas, PSR2 disagrees and being different makes the code harder to deal with for those familiar with PSR2. I'd be fine with switching to a different popular style preset, but we shouldn't have our own mix of PSR2 and something else, so for let's just go all-in on PSR2. --- .phpcs.xml | 2 - lessc.inc.php | 7516 +++++++++++++++++------------------ lessify | 14 +- lessify.inc.php | 820 ++-- tests/ApiTest.php | 132 +- tests/ErrorHandlingTest.php | 136 +- tests/InputTest.php | 148 +- tests/sort.php | 64 +- 8 files changed, 4415 insertions(+), 4417 deletions(-) diff --git a/.phpcs.xml b/.phpcs.xml index f991189b..8d0ffbd9 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -4,8 +4,6 @@ - - diff --git a/lessc.inc.php b/lessc.inc.php index cfff748c..f59e3f9b 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -38,3788 +38,3788 @@ * handling things like indentation. */ class lessc { - static public $VERSION = "v0.5.0"; + static public $VERSION = "v0.5.0"; - static public $TRUE = array("keyword", "true"); - static public $FALSE = array("keyword", "false"); + static public $TRUE = array("keyword", "true"); + static public $FALSE = array("keyword", "false"); - protected $libFunctions = array(); - protected $registeredVars = array(); - protected $preserveComments = false; + protected $libFunctions = array(); + protected $registeredVars = array(); + protected $preserveComments = false; - public $vPrefix = '@'; // prefix of abstract properties - public $mPrefix = '$'; // prefix of abstract blocks - public $parentSelector = '&'; + public $vPrefix = '@'; // prefix of abstract properties + public $mPrefix = '$'; // prefix of abstract blocks + public $parentSelector = '&'; - public $importDisabled = false; - public $importDir = ''; + public $importDisabled = false; + public $importDir = ''; - protected $numberPrecision = null; + protected $numberPrecision = null; - protected $allParsedFiles = array(); + protected $allParsedFiles = array(); - // set to the parser that generated the current line when compiling - // so we know how to create error messages - protected $sourceParser = null; - protected $sourceLoc = null; + // set to the parser that generated the current line when compiling + // so we know how to create error messages + protected $sourceParser = null; + protected $sourceLoc = null; - static protected $nextImportId = 0; // uniquely identify imports + static protected $nextImportId = 0; // uniquely identify imports - // attempts to find the path of an import url, returns null for css files - protected function findImport($url) { - foreach ((array)$this->importDir as $dir) { - $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; - if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { - return $file; - } - } - - return null; - } - - protected function fileExists($name) { - return is_file($name); - } - - static public function compressList($items, $delim) { - if (!isset($items[1]) && isset($items[0])) return $items[0]; - else return array('list', $delim, $items); - } - - static public function preg_quote($what) { - return preg_quote($what, '/'); - } - - protected function tryImport($importPath, $parentBlock, $out) { - if ($importPath[0] == "function" && $importPath[1] == "url") { - $importPath = $this->flattenList($importPath[2]); - } - - $str = $this->coerceString($importPath); - if ($str === null) return false; - - $url = $this->compileValue($this->lib_e($str)); - - // don't import if it ends in css - if (substr_compare($url, '.css', -4, 4) === 0) return false; - - $realPath = $this->findImport($url); - - if ($realPath === null) return false; - - if ($this->importDisabled) { - return array(false, "/* import disabled */"); - } - - if (isset($this->allParsedFiles[realpath($realPath)])) { - return array(false, null); - } - - $this->addParsedFile($realPath); - $parser = $this->makeParser($realPath); - $root = $parser->parse(file_get_contents($realPath)); - - // set the parents of all the block props - foreach ($root->props as $prop) { - if ($prop[0] == "block") { - $prop[1]->parent = $parentBlock; - } - } - - // copy mixins into scope, set their parents - // bring blocks from import into current block - // TODO: need to mark the source parser these came from this file - foreach ($root->children as $childName => $child) { - if (isset($parentBlock->children[$childName])) { - $parentBlock->children[$childName] = array_merge( - $parentBlock->children[$childName], - $child); - } else { - $parentBlock->children[$childName] = $child; - } - } - - $pi = pathinfo($realPath); - $dir = $pi["dirname"]; - - list($top, $bottom) = $this->sortProps($root->props, true); - $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); - - return array(true, $bottom, $parser, $dir); - } - - protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { - $oldSourceParser = $this->sourceParser; - - $oldImport = $this->importDir; - - // TODO: this is because the importDir api is stupid - $this->importDir = (array)$this->importDir; - array_unshift($this->importDir, $importDir); - - foreach ($props as $prop) { - $this->compileProp($prop, $block, $out); - } - - $this->importDir = $oldImport; - $this->sourceParser = $oldSourceParser; - } - - /** - * Recursively compiles a block. - * - * A block is analogous to a CSS block in most cases. A single LESS document - * is encapsulated in a block when parsed, but it does not have parent tags - * so all of it's children appear on the root level when compiled. - * - * Blocks are made up of props and children. - * - * Props are property instructions, array tuples which describe an action - * to be taken, eg. write a property, set a variable, mixin a block. - * - * The children of a block are just all the blocks that are defined within. - * This is used to look up mixins when performing a mixin. - * - * Compiling the block involves pushing a fresh environment on the stack, - * and iterating through the props, compiling each one. - * - * See lessc::compileProp() - * - */ - protected function compileBlock($block) { - switch ($block->type) { - case "root": - $this->compileRoot($block); - break; - case null: - $this->compileCSSBlock($block); - break; - case "media": - $this->compileMedia($block); - break; - case "directive": - $name = "@" . $block->name; - if (!empty($block->value)) { - $name .= " " . $this->compileValue($this->reduce($block->value)); - } - - $this->compileNestedBlock($block, array($name)); - break; - default: - $this->throwError("unknown block type: $block->type\n"); - } - } - - protected function compileCSSBlock($block) { - $env = $this->pushEnv(); - - $selectors = $this->compileSelectors($block->tags); - $env->selectors = $this->multiplySelectors($selectors); - $out = $this->makeOutputBlock(null, $env->selectors); - - $this->scope->children[] = $out; - $this->compileProps($block, $out); - - $block->scope = $env; // mixins carry scope with them! - $this->popEnv(); - } - - protected function compileMedia($media) { - $env = $this->pushEnv($media); - $parentScope = $this->mediaParent($this->scope); - - $query = $this->compileMediaQuery($this->multiplyMedia($env)); - - $this->scope = $this->makeOutputBlock($media->type, array($query)); - $parentScope->children[] = $this->scope; - - $this->compileProps($media, $this->scope); - - if (count($this->scope->lines) > 0) { - $orphanSelelectors = $this->findClosestSelectors(); - if (!is_null($orphanSelelectors)) { - $orphan = $this->makeOutputBlock(null, $orphanSelelectors); - $orphan->lines = $this->scope->lines; - array_unshift($this->scope->children, $orphan); - $this->scope->lines = array(); - } - } - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function mediaParent($scope) { - while (!empty($scope->parent)) { - if (!empty($scope->type) && $scope->type != "media") { - break; - } - $scope = $scope->parent; - } - - return $scope; - } - - protected function compileNestedBlock($block, $selectors) { - $this->pushEnv($block); - $this->scope = $this->makeOutputBlock($block->type, $selectors); - $this->scope->parent->children[] = $this->scope; - - $this->compileProps($block, $this->scope); - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function compileRoot($root) { - $this->pushEnv(); - $this->scope = $this->makeOutputBlock($root->type); - $this->compileProps($root, $this->scope); - $this->popEnv(); - } - - protected function compileProps($block, $out) { - foreach ($this->sortProps($block->props) as $prop) { - $this->compileProp($prop, $block, $out); - } - $out->lines = $this->deduplicate($out->lines); - } - - /** - * Deduplicate lines in a block. Comments are not deduplicated. If a - * duplicate rule is detected, the comments immediately preceding each - * occurence are consolidated. - */ - protected function deduplicate($lines) { - $unique = array(); - $comments = array(); - - foreach($lines as $line) { - if (strpos($line, '/*') === 0) { - $comments[] = $line; - continue; - } - if (!in_array($line, $unique)) { - $unique[] = $line; - } - array_splice($unique, array_search($line, $unique), 0, $comments); - $comments = array(); - } - return array_merge($unique, $comments); - } - - protected function sortProps($props, $split = false) { - $vars = array(); - $imports = array(); - $other = array(); - $stack = array(); - - foreach ($props as $prop) { - switch ($prop[0]) { - case "comment": - $stack[] = $prop; - break; - case "assign": - $stack[] = $prop; - if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { - $vars = array_merge($vars, $stack); - } else { - $other = array_merge($other, $stack); - } - $stack = array(); - break; - case "import": - $id = self::$nextImportId++; - $prop[] = $id; - $stack[] = $prop; - $imports = array_merge($imports, $stack); - $other[] = array("import_mixin", $id); - $stack = array(); - break; - default: - $stack[] = $prop; - $other = array_merge($other, $stack); - $stack = array(); - break; - } - } - $other = array_merge($other, $stack); - - if ($split) { - return array(array_merge($imports, $vars), $other); - } else { - return array_merge($imports, $vars, $other); - } - } - - protected function compileMediaQuery($queries) { - $compiledQueries = array(); - foreach ($queries as $query) { - $parts = array(); - foreach ($query as $q) { - switch ($q[0]) { - case "mediaType": - $parts[] = implode(" ", array_slice($q, 1)); - break; - case "mediaExp": - if (isset($q[2])) { - $parts[] = "($q[1]: " . - $this->compileValue($this->reduce($q[2])) . ")"; - } else { - $parts[] = "($q[1])"; - } - break; - case "variable": - $parts[] = $this->compileValue($this->reduce($q)); - break; - } - } - - if (count($parts) > 0) { - $compiledQueries[] = implode(" and ", $parts); - } - } - - $out = "@media"; - if (!empty($parts)) { - $out .= " " . - implode($this->formatter->selectorSeparator, $compiledQueries); - } - return $out; - } - - protected function multiplyMedia($env, $childQueries = null) { - if (is_null($env) || - !empty($env->block->type) && $env->block->type != "media") - { - return $childQueries; - } - - // plain old block, skip - if (empty($env->block->type)) { - return $this->multiplyMedia($env->parent, $childQueries); - } - - $out = array(); - $queries = $env->block->queries; - if (is_null($childQueries)) { - $out = $queries; - } else { - foreach ($queries as $parent) { - foreach ($childQueries as $child) { - $out[] = array_merge($parent, $child); - } - } - } - - return $this->multiplyMedia($env->parent, $out); - } - - protected function expandParentSelectors(&$tag, $replace) { - $parts = explode("$&$", $tag); - $count = 0; - foreach ($parts as &$part) { - $part = str_replace($this->parentSelector, $replace, $part, $c); - $count += $c; - } - $tag = implode($this->parentSelector, $parts); - return $count; - } - - protected function findClosestSelectors() { - $env = $this->env; - $selectors = null; - while ($env !== null) { - if (isset($env->selectors)) { - $selectors = $env->selectors; - break; - } - $env = $env->parent; - } - - return $selectors; - } - - - // multiply $selectors against the nearest selectors in env - protected function multiplySelectors($selectors) { - // find parent selectors - - $parentSelectors = $this->findClosestSelectors(); - if (is_null($parentSelectors)) { - // kill parent reference in top level selector - foreach ($selectors as &$s) { - $this->expandParentSelectors($s, ""); - } - - return $selectors; - } - - $out = array(); - foreach ($parentSelectors as $parent) { - foreach ($selectors as $child) { - $count = $this->expandParentSelectors($child, $parent); - - // don't prepend the parent tag if & was used - if ($count > 0) { - $out[] = trim($child); - } else { - $out[] = trim($parent . ' ' . $child); - } - } - } - - return $out; - } - - // reduces selector expressions - protected function compileSelectors($selectors) { - $out = array(); - - foreach ($selectors as $s) { - if (is_array($s)) { - list(, $value) = $s; - $out[] = trim($this->compileValue($this->reduce($value))); - } else { - $out[] = $s; - } - } - - return $out; - } - - protected function eq($left, $right) { - return $left == $right; - } - - protected function patternMatch($block, $orderedArgs, $keywordArgs) { - // match the guards if it has them - // any one of the groups must have all its guards pass for a match - if (!empty($block->guards)) { - $groupPassed = false; - foreach ($block->guards as $guardGroup) { - foreach ($guardGroup as $guard) { - $this->pushEnv(); - $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); - - $negate = false; - if ($guard[0] == "negate") { - $guard = $guard[1]; - $negate = true; - } - - $passed = $this->reduce($guard) == self::$TRUE; - if ($negate) $passed = !$passed; - - $this->popEnv(); - - if ($passed) { - $groupPassed = true; - } else { - $groupPassed = false; - break; - } - } - - if ($groupPassed) break; - } - - if (!$groupPassed) { - return false; - } - } - - if (empty($block->args)) { - return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); - } - - $remainingArgs = $block->args; - if ($keywordArgs) { - $remainingArgs = array(); - foreach ($block->args as $arg) { - if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { - continue; - } - - $remainingArgs[] = $arg; - } - } - - $i = -1; // no args - // try to match by arity or by argument literal - foreach ($remainingArgs as $i => $arg) { - switch ($arg[0]) { - case "lit": - if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { - return false; - } - break; - case "arg": - // no arg and no default value - if (!isset($orderedArgs[$i]) && !isset($arg[2])) { - return false; - } - break; - case "rest": - $i--; // rest can be empty - break 2; - } - } - - if ($block->isVararg) { - return true; // not having enough is handled above - } else { - $numMatched = $i + 1; - // greater than because default values always match - return $numMatched >= count($orderedArgs); - } - } - - protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { - $matches = null; - foreach ($blocks as $block) { - // skip seen blocks that don't have arguments - if (isset($skip[$block->id]) && !isset($block->args)) { - continue; - } - - if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { - $matches[] = $block; - } - } - - return $matches; - } - - // attempt to find blocks matched by path and args - protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { - if ($searchIn == null) return null; - if (isset($seen[$searchIn->id])) return null; - $seen[$searchIn->id] = true; - - $name = $path[0]; - - if (isset($searchIn->children[$name])) { - $blocks = $searchIn->children[$name]; - if (count($path) == 1) { - $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); - if (!empty($matches)) { - // This will return all blocks that match in the closest - // scope that has any matching block, like lessjs - return $matches; - } - } else { - $matches = array(); - foreach ($blocks as $subBlock) { - $subMatches = $this->findBlocks($subBlock, - array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); - - if (!is_null($subMatches)) { - foreach ($subMatches as $sm) { - $matches[] = $sm; - } - } - } - - return count($matches) > 0 ? $matches : null; - } - } - if ($searchIn->parent === $searchIn) return null; - return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); - } - - // sets all argument names in $args to either the default value - // or the one passed in through $values - protected function zipSetArgs($args, $orderedValues, $keywordValues) { - $assignedValues = array(); - - $i = 0; - foreach ($args as $a) { - if ($a[0] == "arg") { - if (isset($keywordValues[$a[1]])) { - // has keyword arg - $value = $keywordValues[$a[1]]; - } elseif (isset($orderedValues[$i])) { - // has ordered arg - $value = $orderedValues[$i]; - $i++; - } elseif (isset($a[2])) { - // has default value - $value = $a[2]; - } else { - $this->throwError("Failed to assign arg " . $a[1]); - $value = null; // :( - } - - $value = $this->reduce($value); - $this->set($a[1], $value); - $assignedValues[] = $value; - } else { - // a lit - $i++; - } - } - - // check for a rest - $last = end($args); - if ($last[0] == "rest") { - $rest = array_slice($orderedValues, count($args) - 1); - $this->set($last[1], $this->reduce(array("list", " ", $rest))); - } - - // wow is this the only true use of PHP's + operator for arrays? - $this->env->arguments = $assignedValues + $orderedValues; - } - - // compile a prop and update $lines or $blocks appropriately - protected function compileProp($prop, $block, $out) { - // set error position context - $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; - - switch ($prop[0]) { - case 'assign': - list(, $name, $value) = $prop; - if ($name[0] == $this->vPrefix) { - $this->set($name, $value); - } else { - $out->lines[] = $this->formatter->property($name, - $this->compileValue($this->reduce($value))); - } - break; - case 'block': - list(, $child) = $prop; - $this->compileBlock($child); - break; - case 'mixin': - list(, $path, $args, $suffix) = $prop; - - $orderedArgs = array(); - $keywordArgs = array(); - foreach ((array)$args as $arg) { - $argval = null; - switch ($arg[0]) { - case "arg": - if (!isset($arg[2])) { - $orderedArgs[] = $this->reduce(array("variable", $arg[1])); - } else { - $keywordArgs[$arg[1]] = $this->reduce($arg[2]); - } - break; - - case "lit": - $orderedArgs[] = $this->reduce($arg[1]); - break; - default: - $this->throwError("Unknown arg type: " . $arg[0]); - } - } - - $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); - - if ($mixins === null) { - $this->throwError("{$prop[1][0]} is undefined"); - } - - foreach ($mixins as $mixin) { - if ($mixin === $block && !$orderedArgs) { - continue; - } - - $haveScope = false; - if (isset($mixin->parent->scope)) { - $haveScope = true; - $mixinParentEnv = $this->pushEnv(); - $mixinParentEnv->storeParent = $mixin->parent->scope; - } - - $haveArgs = false; - if (isset($mixin->args)) { - $haveArgs = true; - $this->pushEnv(); - $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); - } - - $oldParent = $mixin->parent; - if ($mixin != $block) $mixin->parent = $block; - - foreach ($this->sortProps($mixin->props) as $subProp) { - if ($suffix !== null && - $subProp[0] == "assign" && - is_string($subProp[1]) && - $subProp[1]{0} != $this->vPrefix) - { - $subProp[2] = array( - 'list', ' ', - array($subProp[2], array('keyword', $suffix)) - ); - } - - $this->compileProp($subProp, $mixin, $out); - } - - $mixin->parent = $oldParent; - - if ($haveArgs) $this->popEnv(); - if ($haveScope) $this->popEnv(); - } - - break; - case 'raw': - $out->lines[] = $prop[1]; - break; - case "directive": - list(, $name, $value) = $prop; - $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; - break; - case "comment": - $out->lines[] = $prop[1]; - break; - case "import"; - list(, $importPath, $importId) = $prop; - $importPath = $this->reduce($importPath); - - if (!isset($this->env->imports)) { - $this->env->imports = array(); - } - - $result = $this->tryImport($importPath, $block, $out); - - $this->env->imports[$importId] = $result === false ? - array(false, "@import " . $this->compileValue($importPath).";") : - $result; - - break; - case "import_mixin": - list(,$importId) = $prop; - $import = $this->env->imports[$importId]; - if ($import[0] === false) { - if (isset($import[1])) { - $out->lines[] = $import[1]; - } - } else { - list(, $bottom, $parser, $importDir) = $import; - $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); - } - - break; - default: - $this->throwError("unknown op: {$prop[0]}\n"); - } - } - - - /** - * Compiles a primitive value into a CSS property value. - * - * Values in lessphp are typed by being wrapped in arrays, their format is - * typically: - * - * array(type, contents [, additional_contents]*) - * - * The input is expected to be reduced. This function will not work on - * things like expressions and variables. - */ - public function compileValue($value) { - switch ($value[0]) { - case 'list': - // [1] - delimiter - // [2] - array of values - return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); - case 'raw_color': - if (!empty($this->formatter->compressColors)) { - return $this->compileValue($this->coerceColor($value)); - } - return $value[1]; - case 'keyword': - // [1] - the keyword - return $value[1]; - case 'number': - list(, $num, $unit) = $value; - // [1] - the number - // [2] - the unit - if ($this->numberPrecision !== null) { - $num = round($num, $this->numberPrecision); - } - return $num . $unit; - case 'string': - // [1] - contents of string (includes quotes) - list(, $delim, $content) = $value; - foreach ($content as &$part) { - if (is_array($part)) { - $part = $this->compileValue($part); - } - } - return $delim . implode($content) . $delim; - case 'color': - // [1] - red component (either number or a %) - // [2] - green component - // [3] - blue component - // [4] - optional alpha component - list(, $r, $g, $b) = $value; - $r = round($r); - $g = round($g); - $b = round($b); - - if (count($value) == 5 && $value[4] != 1) { // rgba - return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; - } - - $h = sprintf("#%02x%02x%02x", $r, $g, $b); - - if (!empty($this->formatter->compressColors)) { - // 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]; - } - } - - return $h; - - case 'function': - list(, $name, $args) = $value; - return $name.'('.$this->compileValue($args).')'; - default: // assumed to be unit - $this->throwError("unknown value type: $value[0]"); - } - } - - protected function lib_pow($args) { - list($base, $exp) = $this->assertArgs($args, 2, "pow"); - return pow($this->assertNumber($base), $this->assertNumber($exp)); - } - - protected function lib_pi() { - return pi(); - } - - protected function lib_mod($args) { - list($a, $b) = $this->assertArgs($args, 2, "mod"); - return $this->assertNumber($a) % $this->assertNumber($b); - } - - protected function lib_tan($num) { - return tan($this->assertNumber($num)); - } - - protected function lib_sin($num) { - return sin($this->assertNumber($num)); - } - - protected function lib_cos($num) { - return cos($this->assertNumber($num)); - } - - protected function lib_atan($num) { - $num = atan($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_asin($num) { - $num = asin($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_acos($num) { - $num = acos($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_sqrt($num) { - return sqrt($this->assertNumber($num)); - } - - protected function lib_extract($value) { - list($list, $idx) = $this->assertArgs($value, 2, "extract"); - $idx = $this->assertNumber($idx); - // 1 indexed - if ($list[0] == "list" && isset($list[2][$idx - 1])) { - return $list[2][$idx - 1]; - } - } - - protected function lib_isnumber($value) { - return $this->toBool($value[0] == "number"); - } - - protected function lib_isstring($value) { - return $this->toBool($value[0] == "string"); - } - - protected function lib_iscolor($value) { - return $this->toBool($this->coerceColor($value)); - } - - protected function lib_iskeyword($value) { - return $this->toBool($value[0] == "keyword"); - } - - protected function lib_ispixel($value) { - return $this->toBool($value[0] == "number" && $value[2] == "px"); - } - - protected function lib_ispercentage($value) { - return $this->toBool($value[0] == "number" && $value[2] == "%"); - } - - protected function lib_isem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "em"); - } - - protected function lib_isrem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "rem"); - } - - protected function lib_rgbahex($color) { - $color = $this->coerceColor($color); - if (is_null($color)) - $this->throwError("color expected for rgbahex"); - - return sprintf("#%02x%02x%02x%02x", - isset($color[4]) ? $color[4]*255 : 255, - $color[1],$color[2], $color[3]); - } - - protected function lib_argb($color){ - return $this->lib_rgbahex($color); - } - - /** - * Given an url, decide whether to output a regular link or the base64-encoded contents of the file - * - * @param array $value either an argument list (two strings) or a single string - * @return string formatted url(), either as a link or base64-encoded - */ - protected function lib_data_uri($value) { - $mime = ($value[0] === 'list') ? $value[2][0][2] : null; - $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; - - $fullpath = $this->findImport($url); - - if($fullpath && ($fsize = filesize($fullpath)) !== false) { - // IE8 can't handle data uris larger than 32KB - if($fsize/1024 < 32) { - if(is_null($mime)) { - if(class_exists('finfo')) { // php 5.3+ - $finfo = new finfo(FILEINFO_MIME); - $mime = explode('; ', $finfo->file($fullpath)); - $mime = $mime[0]; - } elseif(function_exists('mime_content_type')) { // PHP 5.2 - $mime = mime_content_type($fullpath); - } - } - - if(!is_null($mime)) // fallback if the mime type is still unknown - $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); - } - } - - return 'url("'.$url.'")'; - } - - // utility func to unquote a string - protected function lib_e($arg) { - switch ($arg[0]) { - case "list": - $items = $arg[2]; - if (isset($items[0])) { - return $this->lib_e($items[0]); - } - $this->throwError("unrecognised input"); - case "string": - $arg[1] = ""; - return $arg; - case "keyword": - return $arg; - default: - return array("keyword", $this->compileValue($arg)); - } - } - - protected function lib__sprintf($args) { - if ($args[0] != "list") return $args; - $values = $args[2]; - $string = array_shift($values); - $template = $this->compileValue($this->lib_e($string)); - - $i = 0; - if (preg_match_all('/%[dsa]/', $template, $m)) { - foreach ($m[0] as $match) { - $val = isset($values[$i]) ? - $this->reduce($values[$i]) : array('keyword', ''); - - // lessjs compat, renders fully expanded color, not raw color - if ($color = $this->coerceColor($val)) { - $val = $color; - } - - $i++; - $rep = $this->compileValue($this->lib_e($val)); - $template = preg_replace('/'.self::preg_quote($match).'/', - $rep, $template, 1); - } - } - - $d = $string[0] == "string" ? $string[1] : '"'; - return array("string", $d, array($template)); - } - - protected function lib_floor($arg) { - $value = $this->assertNumber($arg); - return array("number", floor($value), $arg[2]); - } - - protected function lib_ceil($arg) { - $value = $this->assertNumber($arg); - return array("number", ceil($value), $arg[2]); - } - - protected function lib_round($arg) { - if($arg[0] != "list") { - $value = $this->assertNumber($arg); - return array("number", round($value), $arg[2]); - } else { - $value = $this->assertNumber($arg[2][0]); - $precision = $this->assertNumber($arg[2][1]); - return array("number", round($value, $precision), $arg[2][0][2]); - } - } - - protected function lib_unit($arg) { - if ($arg[0] == "list") { - list($number, $newUnit) = $arg[2]; - return array("number", $this->assertNumber($number), - $this->compileValue($this->lib_e($newUnit))); - } else { - return array("number", $this->assertNumber($arg), ""); - } - } - - /** - * Helper function to get arguments for color manipulation functions. - * takes a list that contains a color like thing and a percentage - */ - public function colorArgs($args) { - if ($args[0] != 'list' || count($args[2]) < 2) { - return array(array('color', 0, 0, 0), 0); - } - list($color, $delta) = $args[2]; - $color = $this->assertColor($color); - $delta = floatval($delta[1]); - - return array($color, $delta); - } - - protected function lib_darken($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_lighten($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_saturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_desaturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_spin($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - - $hsl[1] = $hsl[1] + $delta % 360; - if ($hsl[1] < 0) $hsl[1] += 360; - - return $this->toRGB($hsl); - } - - protected function lib_fadeout($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); - return $color; - } - - protected function lib_fadein($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); - return $color; - } - - protected function lib_hue($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[1]); - } - - protected function lib_saturation($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[2]); - } - - protected function lib_lightness($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[3]); - } - - // get the alpha of a color - // defaults to 1 for non-colors or colors without an alpha - protected function lib_alpha($value) { - if (!is_null($color = $this->coerceColor($value))) { - return isset($color[4]) ? $color[4] : 1; - } - } - - // set the alpha of the color - protected function lib_fade($args) { - list($color, $alpha) = $this->colorArgs($args); - $color[4] = $this->clamp($alpha / 100.0); - return $color; - } - - protected function lib_percentage($arg) { - $num = $this->assertNumber($arg); - return array("number", $num*100, "%"); - } - - /** - * Mix color with white in variable proportion. - * - * It is the same as calling `mix(#ffffff, @color, @weight)`. - * - * tint(@color, [@weight: 50%]); - * - * http://lesscss.org/functions/#color-operations-tint - * - * @return array Color - */ - protected function lib_tint($args) { - $white = ['color', 255, 255, 255]; - if ($args[0] == 'color') { - return $this->lib_mix([ 'list', ',', [$white, $args] ]); - } elseif ($args[0] == "list" && count($args[2]) == 2) { - return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]); - } else { - $this->throwError("tint expects (color, weight)"); - } - } - - /** - * Mix color with black in variable proportion. - * - * It is the same as calling `mix(#000000, @color, @weight)` - * - * shade(@color, [@weight: 50%]); - * - * http://lesscss.org/functions/#color-operations-shade - * - * @return array Color - */ - protected function lib_shade($args) { - $black = ['color', 0, 0, 0]; - if ($args[0] == 'color') { - return $this->lib_mix([ 'list', ',', [$black, $args] ]); - } elseif ($args[0] == "list" && count($args[2]) == 2) { - return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]); - } else { - $this->throwError("shade expects (color, weight)"); - } - } - - // mixes two colors by weight - // mix(@color1, @color2, [@weight: 50%]); - // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method - protected function lib_mix($args) { - if ($args[0] != "list" || count($args[2]) < 2) - $this->throwError("mix expects (color1, color2, weight)"); - - list($first, $second) = $args[2]; - $first = $this->assertColor($first); - $second = $this->assertColor($second); - - $first_a = $this->lib_alpha($first); - $second_a = $this->lib_alpha($second); - - if (isset($args[2][2])) { - $weight = $args[2][2][1] / 100.0; - } else { - $weight = 0.5; - } - - $w = $weight * 2 - 1; - $a = $first_a - $second_a; - - $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; - $w2 = 1.0 - $w1; - - $new = array('color', - $w1 * $first[1] + $w2 * $second[1], - $w1 * $first[2] + $w2 * $second[2], - $w1 * $first[3] + $w2 * $second[3], - ); - - if ($first_a != 1.0 || $second_a != 1.0) { - $new[] = $first_a * $weight + $second_a * ($weight - 1); - } - - return $this->fixColor($new); - } - - protected function lib_contrast($args) { - $darkColor = array('color', 0, 0, 0); - $lightColor = array('color', 255, 255, 255); - $threshold = 0.43; - - if ( $args[0] == 'list' ) { - $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; - $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; - $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; - $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; - } - else { - $inputColor = $this->assertColor($args); - } - - $inputColor = $this->coerceColor($inputColor); - $darkColor = $this->coerceColor($darkColor); - $lightColor = $this->coerceColor($lightColor); - - //Figure out which is actually light and dark! - if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) { - $t = $lightColor; - $lightColor = $darkColor; - $darkColor = $t; - } - - $inputColor_alpha = $this->lib_alpha($inputColor); - if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) { - return $lightColor; - } - return $darkColor; - } - - private function toLuma($color) { - list(, $r, $g, $b) = $this->coerceColor($color); - - $r = $r / 255; - $g = $g / 255; - $b = $b / 255; - - $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4); - $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4); - $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4); - - return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b); - } - - protected function lib_luma($color) { - return array("number", round($this->toLuma($color) * 100, 8), "%"); - } - - - public function assertColor($value, $error = "expected color value") { - $color = $this->coerceColor($value); - if (is_null($color)) $this->throwError($error); - return $color; - } - - public function assertNumber($value, $error = "expecting number") { - if ($value[0] == "number") return $value[1]; - $this->throwError($error); - } - - public function assertArgs($value, $expectedArgs, $name="") { - if ($expectedArgs == 1) { - return $value; - } else { - if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); - $values = $value[2]; - $numValues = count($values); - if ($expectedArgs != $numValues) { - if ($name) { - $name = $name . ": "; - } - - $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); - } - - return $values; - } - } - - protected function toHSL($color) { - if ($color[0] == 'hsl') return $color; - - $r = $color[1] / 255; - $g = $color[2] / 255; - $b = $color[3] / 255; - - $min = min($r, $g, $b); - $max = max($r, $g, $b); - - $L = ($min + $max) / 2; - if ($min == $max) { - $S = $H = 0; - } else { - if ($L < 0.5) - $S = ($max - $min)/($max + $min); - else - $S = ($max - $min)/(2.0 - $max - $min); - - if ($r == $max) $H = ($g - $b)/($max - $min); - elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); - elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); - - } - - $out = array('hsl', - ($H < 0 ? $H + 6 : $H)*60, - $S*100, - $L*100, - ); - - if (count($color) > 4) $out[] = $color[4]; // copy alpha - return $out; - } - - protected function toRGB_helper($comp, $temp1, $temp2) { - if ($comp < 0) $comp += 1.0; - elseif ($comp > 1) $comp -= 1.0; - - if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; - if (2 * $comp < 1) return $temp2; - if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; - - return $temp1; - } - - /** - * Converts a hsl array into a color value in rgb. - * Expects H to be in range of 0 to 360, S and L in 0 to 100 - */ - protected function toRGB($color) { - if ($color[0] == 'color') return $color; - - $H = $color[1] / 360; - $S = $color[2] / 100; - $L = $color[3] / 100; - - if ($S == 0) { - $r = $g = $b = $L; - } else { - $temp2 = $L < 0.5 ? - $L*(1.0 + $S) : - $L + $S - $L * $S; - - $temp1 = 2.0 * $L - $temp2; - - $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); - $g = $this->toRGB_helper($H, $temp1, $temp2); - $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); - } - - // $out = array('color', round($r*255), round($g*255), round($b*255)); - $out = array('color', $r*255, $g*255, $b*255); - if (count($color) > 4) $out[] = $color[4]; // copy alpha - return $out; - } - - protected function clamp($v, $max = 1, $min = 0) { - return min($max, max($min, $v)); - } - - /** - * Convert the rgb, rgba, hsl color literals of function type - * as returned by the parser into values of color type. - */ - protected function funcToColor($func) { - $fname = $func[1]; - if ($func[2][0] != 'list') return false; // need a list of arguments - $rawComponents = $func[2][2]; - - if ($fname == 'hsl' || $fname == 'hsla') { - $hsl = array('hsl'); - $i = 0; - foreach ($rawComponents as $c) { - $val = $this->reduce($c); - $val = isset($val[1]) ? floatval($val[1]) : 0; - - if ($i == 0) $clamp = 360; - elseif ($i < 3) $clamp = 100; - else $clamp = 1; - - $hsl[] = $this->clamp($val, $clamp); - $i++; - } - - while (count($hsl) < 4) $hsl[] = 0; - return $this->toRGB($hsl); - - } elseif ($fname == 'rgb' || $fname == 'rgba') { - $components = array(); - $i = 1; - foreach ($rawComponents as $c) { - $c = $this->reduce($c); - if ($i < 4) { - if ($c[0] == "number" && $c[2] == "%") { - $components[] = 255 * ($c[1] / 100); - } else { - $components[] = floatval($c[1]); - } - } elseif ($i == 4) { - if ($c[0] == "number" && $c[2] == "%") { - $components[] = 1.0 * ($c[1] / 100); - } else { - $components[] = floatval($c[1]); - } - } else break; - - $i++; - } - while (count($components) < 3) $components[] = 0; - array_unshift($components, 'color'); - return $this->fixColor($components); - } - - return false; - } - - protected function reduce($value, $forExpression = false) { - switch ($value[0]) { - case "interpolate": - $reduced = $this->reduce($value[1]); - $var = $this->compileValue($reduced); - $res = $this->reduce(array("variable", $this->vPrefix . $var)); - - if ($res[0] == "raw_color") { - $res = $this->coerceColor($res); - } - - if (empty($value[2])) $res = $this->lib_e($res); - - return $res; - case "variable": - $key = $value[1]; - if (is_array($key)) { - $key = $this->reduce($key); - $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); - } - - $seen =& $this->env->seenNames; - - if (!empty($seen[$key])) { - $this->throwError("infinite loop detected: $key"); - } - - $seen[$key] = true; - $out = $this->reduce($this->get($key)); - $seen[$key] = false; - return $out; - case "list": - foreach ($value[2] as &$item) { - $item = $this->reduce($item, $forExpression); - } - return $value; - case "expression": - return $this->evaluate($value); - case "string": - foreach ($value[2] as &$part) { - if (is_array($part)) { - $strip = $part[0] == "variable"; - $part = $this->reduce($part); - if ($strip) $part = $this->lib_e($part); - } - } - return $value; - case "escape": - list(,$inner) = $value; - return $this->lib_e($this->reduce($inner)); - case "function": - $color = $this->funcToColor($value); - if ($color) return $color; - - list(, $name, $args) = $value; - if ($name == "%") $name = "_sprintf"; - - $f = isset($this->libFunctions[$name]) ? - $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); - - if (is_callable($f)) { - if ($args[0] == 'list') - $args = self::compressList($args[2], $args[1]); - - $ret = call_user_func($f, $this->reduce($args, true), $this); - - if (is_null($ret)) { - return array("string", "", array( - $name, "(", $args, ")" - )); - } - - // convert to a typed value if the result is a php primitive - if (is_numeric($ret)) $ret = array('number', $ret, ""); - elseif (!is_array($ret)) $ret = array('keyword', $ret); - - return $ret; - } - - // plain function, reduce args - $value[2] = $this->reduce($value[2]); - return $value; - case "unary": - list(, $op, $exp) = $value; - $exp = $this->reduce($exp); - - if ($exp[0] == "number") { - switch ($op) { - case "+": - return $exp; - case "-": - $exp[1] *= -1; - return $exp; - } - } - return array("string", "", array($op, $exp)); - } - - if ($forExpression) { - switch ($value[0]) { - case "keyword": - if ($color = $this->coerceColor($value)) { - return $color; - } - break; - case "raw_color": - return $this->coerceColor($value); - } - } - - return $value; - } - - - // coerce a value for use in color operation - protected function coerceColor($value) { - switch($value[0]) { - case 'color': return $value; - case 'raw_color': - $c = array("color", 0, 0, 0); - $colorStr = substr($value[1], 1); - $num = hexdec($colorStr); - $width = strlen($colorStr) == 3 ? 16 : 256; - - for ($i = 3; $i > 0; $i--) { // 3 2 1 - $t = $num % $width; - $num /= $width; - - $c[$i] = $t * (256/$width) + $t * floor(16/$width); - } - - return $c; - case 'keyword': - $name = $value[1]; - if (isset(self::$cssColors[$name])) { - $rgba = explode(',', self::$cssColors[$name]); - - if(isset($rgba[3])) - return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); - - return array('color', $rgba[0], $rgba[1], $rgba[2]); - } - return null; - } - } - - // make something string like into a string - protected function coerceString($value) { - switch ($value[0]) { - case "string": - return $value; - case "keyword": - return array("string", "", array($value[1])); - } - return null; - } - - // turn list of length 1 into value type - protected function flattenList($value) { - if ($value[0] == "list" && count($value[2]) == 1) { - return $this->flattenList($value[2][0]); - } - return $value; - } - - public function toBool($a) { - if ($a) return self::$TRUE; - else return self::$FALSE; - } - - // evaluate an expression - protected function evaluate($exp) { - list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; - - $left = $this->reduce($left, true); - $right = $this->reduce($right, true); - - if ($leftColor = $this->coerceColor($left)) { - $left = $leftColor; - } - - if ($rightColor = $this->coerceColor($right)) { - $right = $rightColor; - } - - $ltype = $left[0]; - $rtype = $right[0]; - - // operators that work on all types - if ($op == "and") { - return $this->toBool($left == self::$TRUE && $right == self::$TRUE); - } - - if ($op == "=") { - return $this->toBool($this->eq($left, $right) ); - } - - if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { - return $str; - } - - // type based operators - $fname = "op_${ltype}_${rtype}"; - if (is_callable(array($this, $fname))) { - $out = $this->$fname($op, $left, $right); - if (!is_null($out)) return $out; - } - - // make the expression look it did before being parsed - $paddedOp = $op; - if ($whiteBefore) $paddedOp = " " . $paddedOp; - if ($whiteAfter) $paddedOp .= " "; - - return array("string", "", array($left, $paddedOp, $right)); - } - - protected function stringConcatenate($left, $right) { - if ($strLeft = $this->coerceString($left)) { - if ($right[0] == "string") { - $right[1] = ""; - } - $strLeft[2][] = $right; - return $strLeft; - } - - if ($strRight = $this->coerceString($right)) { - array_unshift($strRight[2], $left); - return $strRight; - } - } - - - // make sure a color's components don't go out of bounds - protected function fixColor($c) { - foreach (range(1, 3) as $i) { - if ($c[$i] < 0) $c[$i] = 0; - if ($c[$i] > 255) $c[$i] = 255; - } - - return $c; - } - - protected function op_number_color($op, $lft, $rgt) { - if ($op == '+' || $op == '*') { - return $this->op_color_number($op, $rgt, $lft); - } - } - - protected function op_color_number($op, $lft, $rgt) { - if ($rgt[0] == '%') $rgt[1] /= 100; - - return $this->op_color_color($op, $lft, - array_fill(1, count($lft) - 1, $rgt[1])); - } - - protected function op_color_color($op, $left, $right) { - $out = array('color'); - $max = count($left) > count($right) ? count($left) : count($right); - foreach (range(1, $max - 1) as $i) { - $lval = isset($left[$i]) ? $left[$i] : 0; - $rval = isset($right[$i]) ? $right[$i] : 0; - switch ($op) { - case '+': - $out[] = $lval + $rval; - break; - case '-': - $out[] = $lval - $rval; - break; - case '*': - $out[] = $lval * $rval; - break; - case '%': - $out[] = $lval % $rval; - break; - case '/': - if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); - $out[] = $lval / $rval; - break; - default: - $this->throwError('evaluate error: color op number failed on op '.$op); - } - } - return $this->fixColor($out); - } - - function lib_red($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for red()'); - } - - return $color[1]; - } - - function lib_green($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for green()'); - } - - return $color[2]; - } - - function lib_blue($color){ - $color = $this->coerceColor($color); - if (is_null($color)) { - $this->throwError('color expected for blue()'); - } - - return $color[3]; - } - - - // operator on two numbers - protected function op_number_number($op, $left, $right) { - $unit = empty($left[2]) ? $right[2] : $left[2]; - - $value = 0; - switch ($op) { - case '+': - $value = $left[1] + $right[1]; - break; - case '*': - $value = $left[1] * $right[1]; - break; - case '-': - $value = $left[1] - $right[1]; - break; - case '%': - $value = $left[1] % $right[1]; - break; - case '/': - if ($right[1] == 0) $this->throwError('parse error: divide by zero'); - $value = $left[1] / $right[1]; - break; - case '<': - return $this->toBool($left[1] < $right[1]); - case '>': - return $this->toBool($left[1] > $right[1]); - case '>=': - return $this->toBool($left[1] >= $right[1]); - case '=<': - return $this->toBool($left[1] <= $right[1]); - default: - $this->throwError('parse error: unknown number operator: '.$op); - } - - return array("number", $value, $unit); - } - - - /* environment functions */ - - protected function makeOutputBlock($type, $selectors = null) { - $b = new stdclass; - $b->lines = array(); - $b->children = array(); - $b->selectors = $selectors; - $b->type = $type; - $b->parent = $this->scope; - return $b; - } - - // the state of execution - protected function pushEnv($block = null) { - $e = new stdclass; - $e->parent = $this->env; - $e->store = array(); - $e->block = $block; - - $this->env = $e; - return $e; - } - - // pop something off the stack - protected function popEnv() { - $old = $this->env; - $this->env = $this->env->parent; - return $old; - } - - // set something in the current env - protected function set($name, $value) { - $this->env->store[$name] = $value; - } - - - // get the highest occurrence entry for a name - protected function get($name) { - $current = $this->env; - - $isArguments = $name == $this->vPrefix . 'arguments'; - while ($current) { - if ($isArguments && isset($current->arguments)) { - return array('list', ' ', $current->arguments); - } - - if (isset($current->store[$name])) - return $current->store[$name]; - else { - $current = isset($current->storeParent) ? - $current->storeParent : $current->parent; - } - } - - $this->throwError("variable $name is undefined"); - } - - // inject array of unparsed strings into environment as variables - protected function injectVariables($args) { - $this->pushEnv(); - $parser = new lessc_parser($this, __METHOD__); - foreach ($args as $name => $strValue) { - if ($name{0} != '@') $name = '@'.$name; - $parser->count = 0; - $parser->buffer = (string)$strValue; - if (!$parser->propertyValue($value)) { - throw new Exception("failed to parse passed in variable $name: $strValue"); - } - - $this->set($name, $value); - } - } - - /** - * Initialize any static state, can initialize parser for a file - * $opts isn't used yet - */ - public function __construct($fname = null) { - if ($fname !== null) { - // used for deprecated parse method - $this->_parseFile = $fname; - } - } - - public function compile($string, $name = null) { - $locale = setlocale(LC_NUMERIC, 0); - setlocale(LC_NUMERIC, "C"); - - $this->parser = $this->makeParser($name); - $root = $this->parser->parse($string); - - $this->env = null; - $this->scope = null; - - $this->formatter = $this->newFormatter(); - - if (!empty($this->registeredVars)) { - $this->injectVariables($this->registeredVars); - } - - $this->sourceParser = $this->parser; // used for error messages - $this->compileBlock($root); - - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); - setlocale(LC_NUMERIC, $locale); - return $out; - } - - public function compileFile($fname, $outFname = null) { - if (!is_readable($fname)) { - throw new Exception('load error: failed to find '.$fname); - } - - $pi = pathinfo($fname); - - $oldImport = $this->importDir; - - $this->importDir = (array)$this->importDir; - $this->importDir[] = $pi['dirname'].'/'; - - $this->addParsedFile($fname); - - $out = $this->compile(file_get_contents($fname), $fname); - - $this->importDir = $oldImport; - - if ($outFname !== null) { - return file_put_contents($outFname, $out); - } - - return $out; - } - - // compile only if changed input has changed or output doesn't exist - public function checkedCompile($in, $out) { - if (!is_file($out) || filemtime($in) > filemtime($out)) { - $this->compileFile($in, $out); - return true; - } - return false; - } - - /** - * Execute lessphp on a .less file or a lessphp cache structure - * - * The lessphp cache structure contains information about a specific - * less file having been parsed. It can be used as a hint for future - * calls to determine whether or not a rebuild is required. - * - * The cache structure contains two important keys that may be used - * externally: - * - * compiled: The final compiled CSS - * updated: The time (in seconds) the CSS was last compiled - * - * The cache structure is a plain-ol' PHP associative array and can - * be serialized and unserialized without a hitch. - * - * @param mixed $in Input - * @param bool $force Force rebuild? - * @return array lessphp cache structure - */ - public function cachedCompile($in, $force = false) { - // assume no root - $root = null; - - if (is_string($in)) { - $root = $in; - } elseif (is_array($in) and isset($in['root'])) { - if ($force or ! isset($in['files'])) { - // If we are forcing a recompile or if for some reason the - // structure does not contain any file information we should - // specify the root to trigger a rebuild. - $root = $in['root']; - } elseif (isset($in['files']) and is_array($in['files'])) { - foreach ($in['files'] as $fname => $ftime ) { - if (!file_exists($fname) or filemtime($fname) > $ftime) { - // One of the files we knew about previously has changed - // so we should look at our incoming root again. - $root = $in['root']; - break; - } - } - } - } else { - // TODO: Throw an exception? We got neither a string nor something - // that looks like a compatible lessphp cache structure. - return null; - } - - if ($root !== null) { - // If we have a root value which means we should rebuild. - $out = array(); - $out['root'] = $root; - $out['compiled'] = $this->compileFile($root); - $out['files'] = $this->allParsedFiles(); - $out['updated'] = time(); - return $out; - } else { - // No changes, pass back the structure - // we were given initially. - return $in; - } - - } - - // parse and compile buffer - // This is deprecated - public function parse($str = null, $initialVariables = null) { - if (is_array($str)) { - $initialVariables = $str; - $str = null; - } - - $oldVars = $this->registeredVars; - if ($initialVariables !== null) { - $this->setVariables($initialVariables); - } - - if ($str == null) { - if (empty($this->_parseFile)) { - throw new exception("nothing to parse"); - } - - $out = $this->compileFile($this->_parseFile); - } else { - $out = $this->compile($str); - } - - $this->registeredVars = $oldVars; - return $out; - } - - protected function makeParser($name) { - $parser = new lessc_parser($this, $name); - $parser->writeComments = $this->preserveComments; - - return $parser; - } - - public function setFormatter($name) { - $this->formatterName = $name; - } - - protected function newFormatter() { - $className = "lessc_formatter_lessjs"; - if (!empty($this->formatterName)) { - if (!is_string($this->formatterName)) - return $this->formatterName; - $className = "lessc_formatter_$this->formatterName"; - } - - return new $className; - } - - public function setPreserveComments($preserve) { - $this->preserveComments = $preserve; - } - - public function registerFunction($name, $func) { - $this->libFunctions[$name] = $func; - } - - public function unregisterFunction($name) { - unset($this->libFunctions[$name]); - } - - public function setVariables($variables) { - $this->registeredVars = array_merge($this->registeredVars, $variables); - } - - public function unsetVariable($name) { - unset($this->registeredVars[$name]); - } - - public function setImportDir($dirs) { - $this->importDir = (array)$dirs; - } - - public function addImportDir($dir) { - $this->importDir = (array)$this->importDir; - $this->importDir[] = $dir; - } - - public function allParsedFiles() { - return $this->allParsedFiles; - } - - public function addParsedFile($file) { - $this->allParsedFiles[realpath($file)] = filemtime($file); - } - - /** - * Uses the current value of $this->count to show line and line number - */ - public function throwError($msg = null) { - if ($this->sourceLoc >= 0) { - $this->sourceParser->throwError($msg, $this->sourceLoc); - } - throw new exception($msg); - } - - // compile file $in to file $out if $in is newer than $out - // returns true when it compiles, false otherwise - public static function ccompile($in, $out, $less = null) { - if ($less === null) { - $less = new self; - } - return $less->checkedCompile($in, $out); - } - - public static function cexecute($in, $force = false, $less = null) { - if ($less === null) { - $less = new self; - } - return $less->cachedCompile($in, $force); - } - - static protected $cssColors = array( - 'aliceblue' => '240,248,255', - 'antiquewhite' => '250,235,215', - 'aqua' => '0,255,255', - 'aquamarine' => '127,255,212', - 'azure' => '240,255,255', - 'beige' => '245,245,220', - 'bisque' => '255,228,196', - 'black' => '0,0,0', - 'blanchedalmond' => '255,235,205', - 'blue' => '0,0,255', - 'blueviolet' => '138,43,226', - 'brown' => '165,42,42', - 'burlywood' => '222,184,135', - 'cadetblue' => '95,158,160', - 'chartreuse' => '127,255,0', - 'chocolate' => '210,105,30', - 'coral' => '255,127,80', - 'cornflowerblue' => '100,149,237', - 'cornsilk' => '255,248,220', - 'crimson' => '220,20,60', - 'cyan' => '0,255,255', - 'darkblue' => '0,0,139', - 'darkcyan' => '0,139,139', - 'darkgoldenrod' => '184,134,11', - 'darkgray' => '169,169,169', - 'darkgreen' => '0,100,0', - 'darkgrey' => '169,169,169', - 'darkkhaki' => '189,183,107', - 'darkmagenta' => '139,0,139', - 'darkolivegreen' => '85,107,47', - 'darkorange' => '255,140,0', - 'darkorchid' => '153,50,204', - 'darkred' => '139,0,0', - 'darksalmon' => '233,150,122', - 'darkseagreen' => '143,188,143', - 'darkslateblue' => '72,61,139', - 'darkslategray' => '47,79,79', - 'darkslategrey' => '47,79,79', - 'darkturquoise' => '0,206,209', - 'darkviolet' => '148,0,211', - 'deeppink' => '255,20,147', - 'deepskyblue' => '0,191,255', - 'dimgray' => '105,105,105', - 'dimgrey' => '105,105,105', - 'dodgerblue' => '30,144,255', - 'firebrick' => '178,34,34', - 'floralwhite' => '255,250,240', - 'forestgreen' => '34,139,34', - 'fuchsia' => '255,0,255', - 'gainsboro' => '220,220,220', - 'ghostwhite' => '248,248,255', - 'gold' => '255,215,0', - 'goldenrod' => '218,165,32', - 'gray' => '128,128,128', - 'green' => '0,128,0', - 'greenyellow' => '173,255,47', - 'grey' => '128,128,128', - 'honeydew' => '240,255,240', - 'hotpink' => '255,105,180', - 'indianred' => '205,92,92', - 'indigo' => '75,0,130', - 'ivory' => '255,255,240', - 'khaki' => '240,230,140', - 'lavender' => '230,230,250', - 'lavenderblush' => '255,240,245', - 'lawngreen' => '124,252,0', - 'lemonchiffon' => '255,250,205', - 'lightblue' => '173,216,230', - 'lightcoral' => '240,128,128', - 'lightcyan' => '224,255,255', - 'lightgoldenrodyellow' => '250,250,210', - 'lightgray' => '211,211,211', - 'lightgreen' => '144,238,144', - 'lightgrey' => '211,211,211', - 'lightpink' => '255,182,193', - 'lightsalmon' => '255,160,122', - 'lightseagreen' => '32,178,170', - 'lightskyblue' => '135,206,250', - 'lightslategray' => '119,136,153', - 'lightslategrey' => '119,136,153', - 'lightsteelblue' => '176,196,222', - 'lightyellow' => '255,255,224', - 'lime' => '0,255,0', - 'limegreen' => '50,205,50', - 'linen' => '250,240,230', - 'magenta' => '255,0,255', - 'maroon' => '128,0,0', - 'mediumaquamarine' => '102,205,170', - 'mediumblue' => '0,0,205', - 'mediumorchid' => '186,85,211', - 'mediumpurple' => '147,112,219', - 'mediumseagreen' => '60,179,113', - 'mediumslateblue' => '123,104,238', - 'mediumspringgreen' => '0,250,154', - 'mediumturquoise' => '72,209,204', - 'mediumvioletred' => '199,21,133', - 'midnightblue' => '25,25,112', - 'mintcream' => '245,255,250', - 'mistyrose' => '255,228,225', - 'moccasin' => '255,228,181', - 'navajowhite' => '255,222,173', - 'navy' => '0,0,128', - 'oldlace' => '253,245,230', - 'olive' => '128,128,0', - 'olivedrab' => '107,142,35', - 'orange' => '255,165,0', - 'orangered' => '255,69,0', - 'orchid' => '218,112,214', - 'palegoldenrod' => '238,232,170', - 'palegreen' => '152,251,152', - 'paleturquoise' => '175,238,238', - 'palevioletred' => '219,112,147', - 'papayawhip' => '255,239,213', - 'peachpuff' => '255,218,185', - 'peru' => '205,133,63', - 'pink' => '255,192,203', - 'plum' => '221,160,221', - 'powderblue' => '176,224,230', - 'purple' => '128,0,128', - 'red' => '255,0,0', - 'rosybrown' => '188,143,143', - 'royalblue' => '65,105,225', - 'saddlebrown' => '139,69,19', - 'salmon' => '250,128,114', - 'sandybrown' => '244,164,96', - 'seagreen' => '46,139,87', - 'seashell' => '255,245,238', - 'sienna' => '160,82,45', - 'silver' => '192,192,192', - 'skyblue' => '135,206,235', - 'slateblue' => '106,90,205', - 'slategray' => '112,128,144', - 'slategrey' => '112,128,144', - 'snow' => '255,250,250', - 'springgreen' => '0,255,127', - 'steelblue' => '70,130,180', - 'tan' => '210,180,140', - 'teal' => '0,128,128', - 'thistle' => '216,191,216', - 'tomato' => '255,99,71', - 'transparent' => '0,0,0,0', - 'turquoise' => '64,224,208', - 'violet' => '238,130,238', - 'wheat' => '245,222,179', - 'white' => '255,255,255', - 'whitesmoke' => '245,245,245', - 'yellow' => '255,255,0', - 'yellowgreen' => '154,205,50' - ); + // attempts to find the path of an import url, returns null for css files + protected function findImport($url) { + foreach ((array)$this->importDir as $dir) { + $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; + if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { + return $file; + } + } + + return null; + } + + protected function fileExists($name) { + return is_file($name); + } + + static public function compressList($items, $delim) { + if (!isset($items[1]) && isset($items[0])) return $items[0]; + else return array('list', $delim, $items); + } + + static public function preg_quote($what) { + return preg_quote($what, '/'); + } + + protected function tryImport($importPath, $parentBlock, $out) { + if ($importPath[0] == "function" && $importPath[1] == "url") { + $importPath = $this->flattenList($importPath[2]); + } + + $str = $this->coerceString($importPath); + if ($str === null) return false; + + $url = $this->compileValue($this->lib_e($str)); + + // don't import if it ends in css + if (substr_compare($url, '.css', -4, 4) === 0) return false; + + $realPath = $this->findImport($url); + + if ($realPath === null) return false; + + if ($this->importDisabled) { + return array(false, "/* import disabled */"); + } + + if (isset($this->allParsedFiles[realpath($realPath)])) { + return array(false, null); + } + + $this->addParsedFile($realPath); + $parser = $this->makeParser($realPath); + $root = $parser->parse(file_get_contents($realPath)); + + // set the parents of all the block props + foreach ($root->props as $prop) { + if ($prop[0] == "block") { + $prop[1]->parent = $parentBlock; + } + } + + // copy mixins into scope, set their parents + // bring blocks from import into current block + // TODO: need to mark the source parser these came from this file + foreach ($root->children as $childName => $child) { + if (isset($parentBlock->children[$childName])) { + $parentBlock->children[$childName] = array_merge( + $parentBlock->children[$childName], + $child); + } else { + $parentBlock->children[$childName] = $child; + } + } + + $pi = pathinfo($realPath); + $dir = $pi["dirname"]; + + list($top, $bottom) = $this->sortProps($root->props, true); + $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); + + return array(true, $bottom, $parser, $dir); + } + + protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { + $oldSourceParser = $this->sourceParser; + + $oldImport = $this->importDir; + + // TODO: this is because the importDir api is stupid + $this->importDir = (array)$this->importDir; + array_unshift($this->importDir, $importDir); + + foreach ($props as $prop) { + $this->compileProp($prop, $block, $out); + } + + $this->importDir = $oldImport; + $this->sourceParser = $oldSourceParser; + } + + /** + * Recursively compiles a block. + * + * A block is analogous to a CSS block in most cases. A single LESS document + * is encapsulated in a block when parsed, but it does not have parent tags + * so all of it's children appear on the root level when compiled. + * + * Blocks are made up of props and children. + * + * Props are property instructions, array tuples which describe an action + * to be taken, eg. write a property, set a variable, mixin a block. + * + * The children of a block are just all the blocks that are defined within. + * This is used to look up mixins when performing a mixin. + * + * Compiling the block involves pushing a fresh environment on the stack, + * and iterating through the props, compiling each one. + * + * See lessc::compileProp() + * + */ + protected function compileBlock($block) { + switch ($block->type) { + case "root": + $this->compileRoot($block); + break; + case null: + $this->compileCSSBlock($block); + break; + case "media": + $this->compileMedia($block); + break; + case "directive": + $name = "@" . $block->name; + if (!empty($block->value)) { + $name .= " " . $this->compileValue($this->reduce($block->value)); + } + + $this->compileNestedBlock($block, array($name)); + break; + default: + $this->throwError("unknown block type: $block->type\n"); + } + } + + protected function compileCSSBlock($block) { + $env = $this->pushEnv(); + + $selectors = $this->compileSelectors($block->tags); + $env->selectors = $this->multiplySelectors($selectors); + $out = $this->makeOutputBlock(null, $env->selectors); + + $this->scope->children[] = $out; + $this->compileProps($block, $out); + + $block->scope = $env; // mixins carry scope with them! + $this->popEnv(); + } + + protected function compileMedia($media) { + $env = $this->pushEnv($media); + $parentScope = $this->mediaParent($this->scope); + + $query = $this->compileMediaQuery($this->multiplyMedia($env)); + + $this->scope = $this->makeOutputBlock($media->type, array($query)); + $parentScope->children[] = $this->scope; + + $this->compileProps($media, $this->scope); + + if (count($this->scope->lines) > 0) { + $orphanSelelectors = $this->findClosestSelectors(); + if (!is_null($orphanSelelectors)) { + $orphan = $this->makeOutputBlock(null, $orphanSelelectors); + $orphan->lines = $this->scope->lines; + array_unshift($this->scope->children, $orphan); + $this->scope->lines = array(); + } + } + + $this->scope = $this->scope->parent; + $this->popEnv(); + } + + protected function mediaParent($scope) { + while (!empty($scope->parent)) { + if (!empty($scope->type) && $scope->type != "media") { + break; + } + $scope = $scope->parent; + } + + return $scope; + } + + protected function compileNestedBlock($block, $selectors) { + $this->pushEnv($block); + $this->scope = $this->makeOutputBlock($block->type, $selectors); + $this->scope->parent->children[] = $this->scope; + + $this->compileProps($block, $this->scope); + + $this->scope = $this->scope->parent; + $this->popEnv(); + } + + protected function compileRoot($root) { + $this->pushEnv(); + $this->scope = $this->makeOutputBlock($root->type); + $this->compileProps($root, $this->scope); + $this->popEnv(); + } + + protected function compileProps($block, $out) { + foreach ($this->sortProps($block->props) as $prop) { + $this->compileProp($prop, $block, $out); + } + $out->lines = $this->deduplicate($out->lines); + } + + /** + * Deduplicate lines in a block. Comments are not deduplicated. If a + * duplicate rule is detected, the comments immediately preceding each + * occurence are consolidated. + */ + protected function deduplicate($lines) { + $unique = array(); + $comments = array(); + + foreach($lines as $line) { + if (strpos($line, '/*') === 0) { + $comments[] = $line; + continue; + } + if (!in_array($line, $unique)) { + $unique[] = $line; + } + array_splice($unique, array_search($line, $unique), 0, $comments); + $comments = array(); + } + return array_merge($unique, $comments); + } + + protected function sortProps($props, $split = false) { + $vars = array(); + $imports = array(); + $other = array(); + $stack = array(); + + foreach ($props as $prop) { + switch ($prop[0]) { + case "comment": + $stack[] = $prop; + break; + case "assign": + $stack[] = $prop; + if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { + $vars = array_merge($vars, $stack); + } else { + $other = array_merge($other, $stack); + } + $stack = array(); + break; + case "import": + $id = self::$nextImportId++; + $prop[] = $id; + $stack[] = $prop; + $imports = array_merge($imports, $stack); + $other[] = array("import_mixin", $id); + $stack = array(); + break; + default: + $stack[] = $prop; + $other = array_merge($other, $stack); + $stack = array(); + break; + } + } + $other = array_merge($other, $stack); + + if ($split) { + return array(array_merge($imports, $vars), $other); + } else { + return array_merge($imports, $vars, $other); + } + } + + protected function compileMediaQuery($queries) { + $compiledQueries = array(); + foreach ($queries as $query) { + $parts = array(); + foreach ($query as $q) { + switch ($q[0]) { + case "mediaType": + $parts[] = implode(" ", array_slice($q, 1)); + break; + case "mediaExp": + if (isset($q[2])) { + $parts[] = "($q[1]: " . + $this->compileValue($this->reduce($q[2])) . ")"; + } else { + $parts[] = "($q[1])"; + } + break; + case "variable": + $parts[] = $this->compileValue($this->reduce($q)); + break; + } + } + + if (count($parts) > 0) { + $compiledQueries[] = implode(" and ", $parts); + } + } + + $out = "@media"; + if (!empty($parts)) { + $out .= " " . + implode($this->formatter->selectorSeparator, $compiledQueries); + } + return $out; + } + + protected function multiplyMedia($env, $childQueries = null) { + if (is_null($env) || + !empty($env->block->type) && $env->block->type != "media") + { + return $childQueries; + } + + // plain old block, skip + if (empty($env->block->type)) { + return $this->multiplyMedia($env->parent, $childQueries); + } + + $out = array(); + $queries = $env->block->queries; + if (is_null($childQueries)) { + $out = $queries; + } else { + foreach ($queries as $parent) { + foreach ($childQueries as $child) { + $out[] = array_merge($parent, $child); + } + } + } + + return $this->multiplyMedia($env->parent, $out); + } + + protected function expandParentSelectors(&$tag, $replace) { + $parts = explode("$&$", $tag); + $count = 0; + foreach ($parts as &$part) { + $part = str_replace($this->parentSelector, $replace, $part, $c); + $count += $c; + } + $tag = implode($this->parentSelector, $parts); + return $count; + } + + protected function findClosestSelectors() { + $env = $this->env; + $selectors = null; + while ($env !== null) { + if (isset($env->selectors)) { + $selectors = $env->selectors; + break; + } + $env = $env->parent; + } + + return $selectors; + } + + + // multiply $selectors against the nearest selectors in env + protected function multiplySelectors($selectors) { + // find parent selectors + + $parentSelectors = $this->findClosestSelectors(); + if (is_null($parentSelectors)) { + // kill parent reference in top level selector + foreach ($selectors as &$s) { + $this->expandParentSelectors($s, ""); + } + + return $selectors; + } + + $out = array(); + foreach ($parentSelectors as $parent) { + foreach ($selectors as $child) { + $count = $this->expandParentSelectors($child, $parent); + + // don't prepend the parent tag if & was used + if ($count > 0) { + $out[] = trim($child); + } else { + $out[] = trim($parent . ' ' . $child); + } + } + } + + return $out; + } + + // reduces selector expressions + protected function compileSelectors($selectors) { + $out = array(); + + foreach ($selectors as $s) { + if (is_array($s)) { + list(, $value) = $s; + $out[] = trim($this->compileValue($this->reduce($value))); + } else { + $out[] = $s; + } + } + + return $out; + } + + protected function eq($left, $right) { + return $left == $right; + } + + protected function patternMatch($block, $orderedArgs, $keywordArgs) { + // match the guards if it has them + // any one of the groups must have all its guards pass for a match + if (!empty($block->guards)) { + $groupPassed = false; + foreach ($block->guards as $guardGroup) { + foreach ($guardGroup as $guard) { + $this->pushEnv(); + $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); + + $negate = false; + if ($guard[0] == "negate") { + $guard = $guard[1]; + $negate = true; + } + + $passed = $this->reduce($guard) == self::$TRUE; + if ($negate) $passed = !$passed; + + $this->popEnv(); + + if ($passed) { + $groupPassed = true; + } else { + $groupPassed = false; + break; + } + } + + if ($groupPassed) break; + } + + if (!$groupPassed) { + return false; + } + } + + if (empty($block->args)) { + return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); + } + + $remainingArgs = $block->args; + if ($keywordArgs) { + $remainingArgs = array(); + foreach ($block->args as $arg) { + if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { + continue; + } + + $remainingArgs[] = $arg; + } + } + + $i = -1; // no args + // try to match by arity or by argument literal + foreach ($remainingArgs as $i => $arg) { + switch ($arg[0]) { + case "lit": + if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { + return false; + } + break; + case "arg": + // no arg and no default value + if (!isset($orderedArgs[$i]) && !isset($arg[2])) { + return false; + } + break; + case "rest": + $i--; // rest can be empty + break 2; + } + } + + if ($block->isVararg) { + return true; // not having enough is handled above + } else { + $numMatched = $i + 1; + // greater than because default values always match + return $numMatched >= count($orderedArgs); + } + } + + protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { + $matches = null; + foreach ($blocks as $block) { + // skip seen blocks that don't have arguments + if (isset($skip[$block->id]) && !isset($block->args)) { + continue; + } + + if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { + $matches[] = $block; + } + } + + return $matches; + } + + // attempt to find blocks matched by path and args + protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { + if ($searchIn == null) return null; + if (isset($seen[$searchIn->id])) return null; + $seen[$searchIn->id] = true; + + $name = $path[0]; + + if (isset($searchIn->children[$name])) { + $blocks = $searchIn->children[$name]; + if (count($path) == 1) { + $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); + if (!empty($matches)) { + // This will return all blocks that match in the closest + // scope that has any matching block, like lessjs + return $matches; + } + } else { + $matches = array(); + foreach ($blocks as $subBlock) { + $subMatches = $this->findBlocks($subBlock, + array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); + + if (!is_null($subMatches)) { + foreach ($subMatches as $sm) { + $matches[] = $sm; + } + } + } + + return count($matches) > 0 ? $matches : null; + } + } + if ($searchIn->parent === $searchIn) return null; + return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); + } + + // sets all argument names in $args to either the default value + // or the one passed in through $values + protected function zipSetArgs($args, $orderedValues, $keywordValues) { + $assignedValues = array(); + + $i = 0; + foreach ($args as $a) { + if ($a[0] == "arg") { + if (isset($keywordValues[$a[1]])) { + // has keyword arg + $value = $keywordValues[$a[1]]; + } elseif (isset($orderedValues[$i])) { + // has ordered arg + $value = $orderedValues[$i]; + $i++; + } elseif (isset($a[2])) { + // has default value + $value = $a[2]; + } else { + $this->throwError("Failed to assign arg " . $a[1]); + $value = null; // :( + } + + $value = $this->reduce($value); + $this->set($a[1], $value); + $assignedValues[] = $value; + } else { + // a lit + $i++; + } + } + + // check for a rest + $last = end($args); + if ($last[0] == "rest") { + $rest = array_slice($orderedValues, count($args) - 1); + $this->set($last[1], $this->reduce(array("list", " ", $rest))); + } + + // wow is this the only true use of PHP's + operator for arrays? + $this->env->arguments = $assignedValues + $orderedValues; + } + + // compile a prop and update $lines or $blocks appropriately + protected function compileProp($prop, $block, $out) { + // set error position context + $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; + + switch ($prop[0]) { + case 'assign': + list(, $name, $value) = $prop; + if ($name[0] == $this->vPrefix) { + $this->set($name, $value); + } else { + $out->lines[] = $this->formatter->property($name, + $this->compileValue($this->reduce($value))); + } + break; + case 'block': + list(, $child) = $prop; + $this->compileBlock($child); + break; + case 'mixin': + list(, $path, $args, $suffix) = $prop; + + $orderedArgs = array(); + $keywordArgs = array(); + foreach ((array)$args as $arg) { + $argval = null; + switch ($arg[0]) { + case "arg": + if (!isset($arg[2])) { + $orderedArgs[] = $this->reduce(array("variable", $arg[1])); + } else { + $keywordArgs[$arg[1]] = $this->reduce($arg[2]); + } + break; + + case "lit": + $orderedArgs[] = $this->reduce($arg[1]); + break; + default: + $this->throwError("Unknown arg type: " . $arg[0]); + } + } + + $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); + + if ($mixins === null) { + $this->throwError("{$prop[1][0]} is undefined"); + } + + foreach ($mixins as $mixin) { + if ($mixin === $block && !$orderedArgs) { + continue; + } + + $haveScope = false; + if (isset($mixin->parent->scope)) { + $haveScope = true; + $mixinParentEnv = $this->pushEnv(); + $mixinParentEnv->storeParent = $mixin->parent->scope; + } + + $haveArgs = false; + if (isset($mixin->args)) { + $haveArgs = true; + $this->pushEnv(); + $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); + } + + $oldParent = $mixin->parent; + if ($mixin != $block) $mixin->parent = $block; + + foreach ($this->sortProps($mixin->props) as $subProp) { + if ($suffix !== null && + $subProp[0] == "assign" && + is_string($subProp[1]) && + $subProp[1]{0} != $this->vPrefix) + { + $subProp[2] = array( + 'list', ' ', + array($subProp[2], array('keyword', $suffix)) + ); + } + + $this->compileProp($subProp, $mixin, $out); + } + + $mixin->parent = $oldParent; + + if ($haveArgs) $this->popEnv(); + if ($haveScope) $this->popEnv(); + } + + break; + case 'raw': + $out->lines[] = $prop[1]; + break; + case "directive": + list(, $name, $value) = $prop; + $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; + break; + case "comment": + $out->lines[] = $prop[1]; + break; + case "import"; + list(, $importPath, $importId) = $prop; + $importPath = $this->reduce($importPath); + + if (!isset($this->env->imports)) { + $this->env->imports = array(); + } + + $result = $this->tryImport($importPath, $block, $out); + + $this->env->imports[$importId] = $result === false ? + array(false, "@import " . $this->compileValue($importPath).";") : + $result; + + break; + case "import_mixin": + list(,$importId) = $prop; + $import = $this->env->imports[$importId]; + if ($import[0] === false) { + if (isset($import[1])) { + $out->lines[] = $import[1]; + } + } else { + list(, $bottom, $parser, $importDir) = $import; + $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); + } + + break; + default: + $this->throwError("unknown op: {$prop[0]}\n"); + } + } + + + /** + * Compiles a primitive value into a CSS property value. + * + * Values in lessphp are typed by being wrapped in arrays, their format is + * typically: + * + * array(type, contents [, additional_contents]*) + * + * The input is expected to be reduced. This function will not work on + * things like expressions and variables. + */ + public function compileValue($value) { + switch ($value[0]) { + case 'list': + // [1] - delimiter + // [2] - array of values + return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); + case 'raw_color': + if (!empty($this->formatter->compressColors)) { + return $this->compileValue($this->coerceColor($value)); + } + return $value[1]; + case 'keyword': + // [1] - the keyword + return $value[1]; + case 'number': + list(, $num, $unit) = $value; + // [1] - the number + // [2] - the unit + if ($this->numberPrecision !== null) { + $num = round($num, $this->numberPrecision); + } + return $num . $unit; + case 'string': + // [1] - contents of string (includes quotes) + list(, $delim, $content) = $value; + foreach ($content as &$part) { + if (is_array($part)) { + $part = $this->compileValue($part); + } + } + return $delim . implode($content) . $delim; + case 'color': + // [1] - red component (either number or a %) + // [2] - green component + // [3] - blue component + // [4] - optional alpha component + list(, $r, $g, $b) = $value; + $r = round($r); + $g = round($g); + $b = round($b); + + if (count($value) == 5 && $value[4] != 1) { // rgba + return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; + } + + $h = sprintf("#%02x%02x%02x", $r, $g, $b); + + if (!empty($this->formatter->compressColors)) { + // 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]; + } + } + + return $h; + + case 'function': + list(, $name, $args) = $value; + return $name.'('.$this->compileValue($args).')'; + default: // assumed to be unit + $this->throwError("unknown value type: $value[0]"); + } + } + + protected function lib_pow($args) { + list($base, $exp) = $this->assertArgs($args, 2, "pow"); + return pow($this->assertNumber($base), $this->assertNumber($exp)); + } + + protected function lib_pi() { + return pi(); + } + + protected function lib_mod($args) { + list($a, $b) = $this->assertArgs($args, 2, "mod"); + return $this->assertNumber($a) % $this->assertNumber($b); + } + + protected function lib_tan($num) { + return tan($this->assertNumber($num)); + } + + protected function lib_sin($num) { + return sin($this->assertNumber($num)); + } + + protected function lib_cos($num) { + return cos($this->assertNumber($num)); + } + + protected function lib_atan($num) { + $num = atan($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_asin($num) { + $num = asin($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_acos($num) { + $num = acos($this->assertNumber($num)); + return array("number", $num, "rad"); + } + + protected function lib_sqrt($num) { + return sqrt($this->assertNumber($num)); + } + + protected function lib_extract($value) { + list($list, $idx) = $this->assertArgs($value, 2, "extract"); + $idx = $this->assertNumber($idx); + // 1 indexed + if ($list[0] == "list" && isset($list[2][$idx - 1])) { + return $list[2][$idx - 1]; + } + } + + protected function lib_isnumber($value) { + return $this->toBool($value[0] == "number"); + } + + protected function lib_isstring($value) { + return $this->toBool($value[0] == "string"); + } + + protected function lib_iscolor($value) { + return $this->toBool($this->coerceColor($value)); + } + + protected function lib_iskeyword($value) { + return $this->toBool($value[0] == "keyword"); + } + + protected function lib_ispixel($value) { + return $this->toBool($value[0] == "number" && $value[2] == "px"); + } + + protected function lib_ispercentage($value) { + return $this->toBool($value[0] == "number" && $value[2] == "%"); + } + + protected function lib_isem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "em"); + } + + protected function lib_isrem($value) { + return $this->toBool($value[0] == "number" && $value[2] == "rem"); + } + + protected function lib_rgbahex($color) { + $color = $this->coerceColor($color); + if (is_null($color)) + $this->throwError("color expected for rgbahex"); + + return sprintf("#%02x%02x%02x%02x", + isset($color[4]) ? $color[4]*255 : 255, + $color[1],$color[2], $color[3]); + } + + protected function lib_argb($color){ + return $this->lib_rgbahex($color); + } + + /** + * Given an url, decide whether to output a regular link or the base64-encoded contents of the file + * + * @param array $value either an argument list (two strings) or a single string + * @return string formatted url(), either as a link or base64-encoded + */ + protected function lib_data_uri($value) { + $mime = ($value[0] === 'list') ? $value[2][0][2] : null; + $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; + + $fullpath = $this->findImport($url); + + if($fullpath && ($fsize = filesize($fullpath)) !== false) { + // IE8 can't handle data uris larger than 32KB + if($fsize/1024 < 32) { + if(is_null($mime)) { + if(class_exists('finfo')) { // php 5.3+ + $finfo = new finfo(FILEINFO_MIME); + $mime = explode('; ', $finfo->file($fullpath)); + $mime = $mime[0]; + } elseif(function_exists('mime_content_type')) { // PHP 5.2 + $mime = mime_content_type($fullpath); + } + } + + if(!is_null($mime)) // fallback if the mime type is still unknown + $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); + } + } + + return 'url("'.$url.'")'; + } + + // utility func to unquote a string + protected function lib_e($arg) { + switch ($arg[0]) { + case "list": + $items = $arg[2]; + if (isset($items[0])) { + return $this->lib_e($items[0]); + } + $this->throwError("unrecognised input"); + case "string": + $arg[1] = ""; + return $arg; + case "keyword": + return $arg; + default: + return array("keyword", $this->compileValue($arg)); + } + } + + protected function lib__sprintf($args) { + if ($args[0] != "list") return $args; + $values = $args[2]; + $string = array_shift($values); + $template = $this->compileValue($this->lib_e($string)); + + $i = 0; + if (preg_match_all('/%[dsa]/', $template, $m)) { + foreach ($m[0] as $match) { + $val = isset($values[$i]) ? + $this->reduce($values[$i]) : array('keyword', ''); + + // lessjs compat, renders fully expanded color, not raw color + if ($color = $this->coerceColor($val)) { + $val = $color; + } + + $i++; + $rep = $this->compileValue($this->lib_e($val)); + $template = preg_replace('/'.self::preg_quote($match).'/', + $rep, $template, 1); + } + } + + $d = $string[0] == "string" ? $string[1] : '"'; + return array("string", $d, array($template)); + } + + protected function lib_floor($arg) { + $value = $this->assertNumber($arg); + return array("number", floor($value), $arg[2]); + } + + protected function lib_ceil($arg) { + $value = $this->assertNumber($arg); + return array("number", ceil($value), $arg[2]); + } + + protected function lib_round($arg) { + if($arg[0] != "list") { + $value = $this->assertNumber($arg); + return array("number", round($value), $arg[2]); + } else { + $value = $this->assertNumber($arg[2][0]); + $precision = $this->assertNumber($arg[2][1]); + return array("number", round($value, $precision), $arg[2][0][2]); + } + } + + protected function lib_unit($arg) { + if ($arg[0] == "list") { + list($number, $newUnit) = $arg[2]; + return array("number", $this->assertNumber($number), + $this->compileValue($this->lib_e($newUnit))); + } else { + return array("number", $this->assertNumber($arg), ""); + } + } + + /** + * Helper function to get arguments for color manipulation functions. + * takes a list that contains a color like thing and a percentage + */ + public function colorArgs($args) { + if ($args[0] != 'list' || count($args[2]) < 2) { + return array(array('color', 0, 0, 0), 0); + } + list($color, $delta) = $args[2]; + $color = $this->assertColor($color); + $delta = floatval($delta[1]); + + return array($color, $delta); + } + + protected function lib_darken($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[3] = $this->clamp($hsl[3] - $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_lighten($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[3] = $this->clamp($hsl[3] + $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_saturate($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[2] = $this->clamp($hsl[2] + $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_desaturate($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + $hsl[2] = $this->clamp($hsl[2] - $delta, 100); + return $this->toRGB($hsl); + } + + protected function lib_spin($args) { + list($color, $delta) = $this->colorArgs($args); + + $hsl = $this->toHSL($color); + + $hsl[1] = $hsl[1] + $delta % 360; + if ($hsl[1] < 0) $hsl[1] += 360; + + return $this->toRGB($hsl); + } + + protected function lib_fadeout($args) { + list($color, $delta) = $this->colorArgs($args); + $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); + return $color; + } + + protected function lib_fadein($args) { + list($color, $delta) = $this->colorArgs($args); + $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); + return $color; + } + + protected function lib_hue($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[1]); + } + + protected function lib_saturation($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[2]); + } + + protected function lib_lightness($color) { + $hsl = $this->toHSL($this->assertColor($color)); + return round($hsl[3]); + } + + // get the alpha of a color + // defaults to 1 for non-colors or colors without an alpha + protected function lib_alpha($value) { + if (!is_null($color = $this->coerceColor($value))) { + return isset($color[4]) ? $color[4] : 1; + } + } + + // set the alpha of the color + protected function lib_fade($args) { + list($color, $alpha) = $this->colorArgs($args); + $color[4] = $this->clamp($alpha / 100.0); + return $color; + } + + protected function lib_percentage($arg) { + $num = $this->assertNumber($arg); + return array("number", $num*100, "%"); + } + + /** + * Mix color with white in variable proportion. + * + * It is the same as calling `mix(#ffffff, @color, @weight)`. + * + * tint(@color, [@weight: 50%]); + * + * http://lesscss.org/functions/#color-operations-tint + * + * @return array Color + */ + protected function lib_tint($args) { + $white = ['color', 255, 255, 255]; + if ($args[0] == 'color') { + return $this->lib_mix([ 'list', ',', [$white, $args] ]); + } elseif ($args[0] == "list" && count($args[2]) == 2) { + return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]); + } else { + $this->throwError("tint expects (color, weight)"); + } + } + + /** + * Mix color with black in variable proportion. + * + * It is the same as calling `mix(#000000, @color, @weight)` + * + * shade(@color, [@weight: 50%]); + * + * http://lesscss.org/functions/#color-operations-shade + * + * @return array Color + */ + protected function lib_shade($args) { + $black = ['color', 0, 0, 0]; + if ($args[0] == 'color') { + return $this->lib_mix([ 'list', ',', [$black, $args] ]); + } elseif ($args[0] == "list" && count($args[2]) == 2) { + return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]); + } else { + $this->throwError("shade expects (color, weight)"); + } + } + + // mixes two colors by weight + // mix(@color1, @color2, [@weight: 50%]); + // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method + protected function lib_mix($args) { + if ($args[0] != "list" || count($args[2]) < 2) + $this->throwError("mix expects (color1, color2, weight)"); + + list($first, $second) = $args[2]; + $first = $this->assertColor($first); + $second = $this->assertColor($second); + + $first_a = $this->lib_alpha($first); + $second_a = $this->lib_alpha($second); + + if (isset($args[2][2])) { + $weight = $args[2][2][1] / 100.0; + } else { + $weight = 0.5; + } + + $w = $weight * 2 - 1; + $a = $first_a - $second_a; + + $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; + $w2 = 1.0 - $w1; + + $new = array('color', + $w1 * $first[1] + $w2 * $second[1], + $w1 * $first[2] + $w2 * $second[2], + $w1 * $first[3] + $w2 * $second[3], + ); + + if ($first_a != 1.0 || $second_a != 1.0) { + $new[] = $first_a * $weight + $second_a * ($weight - 1); + } + + return $this->fixColor($new); + } + + protected function lib_contrast($args) { + $darkColor = array('color', 0, 0, 0); + $lightColor = array('color', 255, 255, 255); + $threshold = 0.43; + + if ( $args[0] == 'list' ) { + $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; + $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; + $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; + $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; + } + else { + $inputColor = $this->assertColor($args); + } + + $inputColor = $this->coerceColor($inputColor); + $darkColor = $this->coerceColor($darkColor); + $lightColor = $this->coerceColor($lightColor); + + //Figure out which is actually light and dark! + if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) { + $t = $lightColor; + $lightColor = $darkColor; + $darkColor = $t; + } + + $inputColor_alpha = $this->lib_alpha($inputColor); + if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) { + return $lightColor; + } + return $darkColor; + } + + private function toLuma($color) { + list(, $r, $g, $b) = $this->coerceColor($color); + + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + + $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4); + $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4); + $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4); + + return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b); + } + + protected function lib_luma($color) { + return array("number", round($this->toLuma($color) * 100, 8), "%"); + } + + + public function assertColor($value, $error = "expected color value") { + $color = $this->coerceColor($value); + if (is_null($color)) $this->throwError($error); + return $color; + } + + public function assertNumber($value, $error = "expecting number") { + if ($value[0] == "number") return $value[1]; + $this->throwError($error); + } + + public function assertArgs($value, $expectedArgs, $name="") { + if ($expectedArgs == 1) { + return $value; + } else { + if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); + $values = $value[2]; + $numValues = count($values); + if ($expectedArgs != $numValues) { + if ($name) { + $name = $name . ": "; + } + + $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); + } + + return $values; + } + } + + protected function toHSL($color) { + if ($color[0] == 'hsl') return $color; + + $r = $color[1] / 255; + $g = $color[2] / 255; + $b = $color[3] / 255; + + $min = min($r, $g, $b); + $max = max($r, $g, $b); + + $L = ($min + $max) / 2; + if ($min == $max) { + $S = $H = 0; + } else { + if ($L < 0.5) + $S = ($max - $min)/($max + $min); + else + $S = ($max - $min)/(2.0 - $max - $min); + + if ($r == $max) $H = ($g - $b)/($max - $min); + elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); + elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); + + } + + $out = array('hsl', + ($H < 0 ? $H + 6 : $H)*60, + $S*100, + $L*100, + ); + + if (count($color) > 4) $out[] = $color[4]; // copy alpha + return $out; + } + + protected function toRGB_helper($comp, $temp1, $temp2) { + if ($comp < 0) $comp += 1.0; + elseif ($comp > 1) $comp -= 1.0; + + if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; + if (2 * $comp < 1) return $temp2; + if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; + + return $temp1; + } + + /** + * Converts a hsl array into a color value in rgb. + * Expects H to be in range of 0 to 360, S and L in 0 to 100 + */ + protected function toRGB($color) { + if ($color[0] == 'color') return $color; + + $H = $color[1] / 360; + $S = $color[2] / 100; + $L = $color[3] / 100; + + if ($S == 0) { + $r = $g = $b = $L; + } else { + $temp2 = $L < 0.5 ? + $L*(1.0 + $S) : + $L + $S - $L * $S; + + $temp1 = 2.0 * $L - $temp2; + + $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); + $g = $this->toRGB_helper($H, $temp1, $temp2); + $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); + } + + // $out = array('color', round($r*255), round($g*255), round($b*255)); + $out = array('color', $r*255, $g*255, $b*255); + if (count($color) > 4) $out[] = $color[4]; // copy alpha + return $out; + } + + protected function clamp($v, $max = 1, $min = 0) { + return min($max, max($min, $v)); + } + + /** + * Convert the rgb, rgba, hsl color literals of function type + * as returned by the parser into values of color type. + */ + protected function funcToColor($func) { + $fname = $func[1]; + if ($func[2][0] != 'list') return false; // need a list of arguments + $rawComponents = $func[2][2]; + + if ($fname == 'hsl' || $fname == 'hsla') { + $hsl = array('hsl'); + $i = 0; + foreach ($rawComponents as $c) { + $val = $this->reduce($c); + $val = isset($val[1]) ? floatval($val[1]) : 0; + + if ($i == 0) $clamp = 360; + elseif ($i < 3) $clamp = 100; + else $clamp = 1; + + $hsl[] = $this->clamp($val, $clamp); + $i++; + } + + while (count($hsl) < 4) $hsl[] = 0; + return $this->toRGB($hsl); + + } elseif ($fname == 'rgb' || $fname == 'rgba') { + $components = array(); + $i = 1; + foreach ($rawComponents as $c) { + $c = $this->reduce($c); + if ($i < 4) { + if ($c[0] == "number" && $c[2] == "%") { + $components[] = 255 * ($c[1] / 100); + } else { + $components[] = floatval($c[1]); + } + } elseif ($i == 4) { + if ($c[0] == "number" && $c[2] == "%") { + $components[] = 1.0 * ($c[1] / 100); + } else { + $components[] = floatval($c[1]); + } + } else break; + + $i++; + } + while (count($components) < 3) $components[] = 0; + array_unshift($components, 'color'); + return $this->fixColor($components); + } + + return false; + } + + protected function reduce($value, $forExpression = false) { + switch ($value[0]) { + case "interpolate": + $reduced = $this->reduce($value[1]); + $var = $this->compileValue($reduced); + $res = $this->reduce(array("variable", $this->vPrefix . $var)); + + if ($res[0] == "raw_color") { + $res = $this->coerceColor($res); + } + + if (empty($value[2])) $res = $this->lib_e($res); + + return $res; + case "variable": + $key = $value[1]; + if (is_array($key)) { + $key = $this->reduce($key); + $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); + } + + $seen =& $this->env->seenNames; + + if (!empty($seen[$key])) { + $this->throwError("infinite loop detected: $key"); + } + + $seen[$key] = true; + $out = $this->reduce($this->get($key)); + $seen[$key] = false; + return $out; + case "list": + foreach ($value[2] as &$item) { + $item = $this->reduce($item, $forExpression); + } + return $value; + case "expression": + return $this->evaluate($value); + case "string": + foreach ($value[2] as &$part) { + if (is_array($part)) { + $strip = $part[0] == "variable"; + $part = $this->reduce($part); + if ($strip) $part = $this->lib_e($part); + } + } + return $value; + case "escape": + list(,$inner) = $value; + return $this->lib_e($this->reduce($inner)); + case "function": + $color = $this->funcToColor($value); + if ($color) return $color; + + list(, $name, $args) = $value; + if ($name == "%") $name = "_sprintf"; + + $f = isset($this->libFunctions[$name]) ? + $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); + + if (is_callable($f)) { + if ($args[0] == 'list') + $args = self::compressList($args[2], $args[1]); + + $ret = call_user_func($f, $this->reduce($args, true), $this); + + if (is_null($ret)) { + return array("string", "", array( + $name, "(", $args, ")" + )); + } + + // convert to a typed value if the result is a php primitive + if (is_numeric($ret)) $ret = array('number', $ret, ""); + elseif (!is_array($ret)) $ret = array('keyword', $ret); + + return $ret; + } + + // plain function, reduce args + $value[2] = $this->reduce($value[2]); + return $value; + case "unary": + list(, $op, $exp) = $value; + $exp = $this->reduce($exp); + + if ($exp[0] == "number") { + switch ($op) { + case "+": + return $exp; + case "-": + $exp[1] *= -1; + return $exp; + } + } + return array("string", "", array($op, $exp)); + } + + if ($forExpression) { + switch ($value[0]) { + case "keyword": + if ($color = $this->coerceColor($value)) { + return $color; + } + break; + case "raw_color": + return $this->coerceColor($value); + } + } + + return $value; + } + + + // coerce a value for use in color operation + protected function coerceColor($value) { + switch($value[0]) { + case 'color': return $value; + case 'raw_color': + $c = array("color", 0, 0, 0); + $colorStr = substr($value[1], 1); + $num = hexdec($colorStr); + $width = strlen($colorStr) == 3 ? 16 : 256; + + for ($i = 3; $i > 0; $i--) { // 3 2 1 + $t = $num % $width; + $num /= $width; + + $c[$i] = $t * (256/$width) + $t * floor(16/$width); + } + + return $c; + case 'keyword': + $name = $value[1]; + if (isset(self::$cssColors[$name])) { + $rgba = explode(',', self::$cssColors[$name]); + + if(isset($rgba[3])) + return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); + + return array('color', $rgba[0], $rgba[1], $rgba[2]); + } + return null; + } + } + + // make something string like into a string + protected function coerceString($value) { + switch ($value[0]) { + case "string": + return $value; + case "keyword": + return array("string", "", array($value[1])); + } + return null; + } + + // turn list of length 1 into value type + protected function flattenList($value) { + if ($value[0] == "list" && count($value[2]) == 1) { + return $this->flattenList($value[2][0]); + } + return $value; + } + + public function toBool($a) { + if ($a) return self::$TRUE; + else return self::$FALSE; + } + + // evaluate an expression + protected function evaluate($exp) { + list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; + + $left = $this->reduce($left, true); + $right = $this->reduce($right, true); + + if ($leftColor = $this->coerceColor($left)) { + $left = $leftColor; + } + + if ($rightColor = $this->coerceColor($right)) { + $right = $rightColor; + } + + $ltype = $left[0]; + $rtype = $right[0]; + + // operators that work on all types + if ($op == "and") { + return $this->toBool($left == self::$TRUE && $right == self::$TRUE); + } + + if ($op == "=") { + return $this->toBool($this->eq($left, $right) ); + } + + if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { + return $str; + } + + // type based operators + $fname = "op_${ltype}_${rtype}"; + if (is_callable(array($this, $fname))) { + $out = $this->$fname($op, $left, $right); + if (!is_null($out)) return $out; + } + + // make the expression look it did before being parsed + $paddedOp = $op; + if ($whiteBefore) $paddedOp = " " . $paddedOp; + if ($whiteAfter) $paddedOp .= " "; + + return array("string", "", array($left, $paddedOp, $right)); + } + + protected function stringConcatenate($left, $right) { + if ($strLeft = $this->coerceString($left)) { + if ($right[0] == "string") { + $right[1] = ""; + } + $strLeft[2][] = $right; + return $strLeft; + } + + if ($strRight = $this->coerceString($right)) { + array_unshift($strRight[2], $left); + return $strRight; + } + } + + + // make sure a color's components don't go out of bounds + protected function fixColor($c) { + foreach (range(1, 3) as $i) { + if ($c[$i] < 0) $c[$i] = 0; + if ($c[$i] > 255) $c[$i] = 255; + } + + return $c; + } + + protected function op_number_color($op, $lft, $rgt) { + if ($op == '+' || $op == '*') { + return $this->op_color_number($op, $rgt, $lft); + } + } + + protected function op_color_number($op, $lft, $rgt) { + if ($rgt[0] == '%') $rgt[1] /= 100; + + return $this->op_color_color($op, $lft, + array_fill(1, count($lft) - 1, $rgt[1])); + } + + protected function op_color_color($op, $left, $right) { + $out = array('color'); + $max = count($left) > count($right) ? count($left) : count($right); + foreach (range(1, $max - 1) as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + case '-': + $out[] = $lval - $rval; + break; + case '*': + $out[] = $lval * $rval; + break; + case '%': + $out[] = $lval % $rval; + break; + case '/': + if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); + $out[] = $lval / $rval; + break; + default: + $this->throwError('evaluate error: color op number failed on op '.$op); + } + } + return $this->fixColor($out); + } + + function lib_red($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for red()'); + } + + return $color[1]; + } + + function lib_green($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for green()'); + } + + return $color[2]; + } + + function lib_blue($color){ + $color = $this->coerceColor($color); + if (is_null($color)) { + $this->throwError('color expected for blue()'); + } + + return $color[3]; + } + + + // operator on two numbers + protected function op_number_number($op, $left, $right) { + $unit = empty($left[2]) ? $right[2] : $left[2]; + + $value = 0; + switch ($op) { + case '+': + $value = $left[1] + $right[1]; + break; + case '*': + $value = $left[1] * $right[1]; + break; + case '-': + $value = $left[1] - $right[1]; + break; + case '%': + $value = $left[1] % $right[1]; + break; + case '/': + if ($right[1] == 0) $this->throwError('parse error: divide by zero'); + $value = $left[1] / $right[1]; + break; + case '<': + return $this->toBool($left[1] < $right[1]); + case '>': + return $this->toBool($left[1] > $right[1]); + case '>=': + return $this->toBool($left[1] >= $right[1]); + case '=<': + return $this->toBool($left[1] <= $right[1]); + default: + $this->throwError('parse error: unknown number operator: '.$op); + } + + return array("number", $value, $unit); + } + + + /* environment functions */ + + protected function makeOutputBlock($type, $selectors = null) { + $b = new stdclass; + $b->lines = array(); + $b->children = array(); + $b->selectors = $selectors; + $b->type = $type; + $b->parent = $this->scope; + return $b; + } + + // the state of execution + protected function pushEnv($block = null) { + $e = new stdclass; + $e->parent = $this->env; + $e->store = array(); + $e->block = $block; + + $this->env = $e; + return $e; + } + + // pop something off the stack + protected function popEnv() { + $old = $this->env; + $this->env = $this->env->parent; + return $old; + } + + // set something in the current env + protected function set($name, $value) { + $this->env->store[$name] = $value; + } + + + // get the highest occurrence entry for a name + protected function get($name) { + $current = $this->env; + + $isArguments = $name == $this->vPrefix . 'arguments'; + while ($current) { + if ($isArguments && isset($current->arguments)) { + return array('list', ' ', $current->arguments); + } + + if (isset($current->store[$name])) + return $current->store[$name]; + else { + $current = isset($current->storeParent) ? + $current->storeParent : $current->parent; + } + } + + $this->throwError("variable $name is undefined"); + } + + // inject array of unparsed strings into environment as variables + protected function injectVariables($args) { + $this->pushEnv(); + $parser = new lessc_parser($this, __METHOD__); + foreach ($args as $name => $strValue) { + if ($name{0} != '@') $name = '@'.$name; + $parser->count = 0; + $parser->buffer = (string)$strValue; + if (!$parser->propertyValue($value)) { + throw new Exception("failed to parse passed in variable $name: $strValue"); + } + + $this->set($name, $value); + } + } + + /** + * Initialize any static state, can initialize parser for a file + * $opts isn't used yet + */ + public function __construct($fname = null) { + if ($fname !== null) { + // used for deprecated parse method + $this->_parseFile = $fname; + } + } + + public function compile($string, $name = null) { + $locale = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, "C"); + + $this->parser = $this->makeParser($name); + $root = $this->parser->parse($string); + + $this->env = null; + $this->scope = null; + + $this->formatter = $this->newFormatter(); + + if (!empty($this->registeredVars)) { + $this->injectVariables($this->registeredVars); + } + + $this->sourceParser = $this->parser; // used for error messages + $this->compileBlock($root); + + ob_start(); + $this->formatter->block($this->scope); + $out = ob_get_clean(); + setlocale(LC_NUMERIC, $locale); + return $out; + } + + public function compileFile($fname, $outFname = null) { + if (!is_readable($fname)) { + throw new Exception('load error: failed to find '.$fname); + } + + $pi = pathinfo($fname); + + $oldImport = $this->importDir; + + $this->importDir = (array)$this->importDir; + $this->importDir[] = $pi['dirname'].'/'; + + $this->addParsedFile($fname); + + $out = $this->compile(file_get_contents($fname), $fname); + + $this->importDir = $oldImport; + + if ($outFname !== null) { + return file_put_contents($outFname, $out); + } + + return $out; + } + + // compile only if changed input has changed or output doesn't exist + public function checkedCompile($in, $out) { + if (!is_file($out) || filemtime($in) > filemtime($out)) { + $this->compileFile($in, $out); + return true; + } + return false; + } + + /** + * Execute lessphp on a .less file or a lessphp cache structure + * + * The lessphp cache structure contains information about a specific + * less file having been parsed. It can be used as a hint for future + * calls to determine whether or not a rebuild is required. + * + * The cache structure contains two important keys that may be used + * externally: + * + * compiled: The final compiled CSS + * updated: The time (in seconds) the CSS was last compiled + * + * The cache structure is a plain-ol' PHP associative array and can + * be serialized and unserialized without a hitch. + * + * @param mixed $in Input + * @param bool $force Force rebuild? + * @return array lessphp cache structure + */ + public function cachedCompile($in, $force = false) { + // assume no root + $root = null; + + if (is_string($in)) { + $root = $in; + } elseif (is_array($in) and isset($in['root'])) { + if ($force or ! isset($in['files'])) { + // If we are forcing a recompile or if for some reason the + // structure does not contain any file information we should + // specify the root to trigger a rebuild. + $root = $in['root']; + } elseif (isset($in['files']) and is_array($in['files'])) { + foreach ($in['files'] as $fname => $ftime ) { + if (!file_exists($fname) or filemtime($fname) > $ftime) { + // One of the files we knew about previously has changed + // so we should look at our incoming root again. + $root = $in['root']; + break; + } + } + } + } else { + // TODO: Throw an exception? We got neither a string nor something + // that looks like a compatible lessphp cache structure. + return null; + } + + if ($root !== null) { + // If we have a root value which means we should rebuild. + $out = array(); + $out['root'] = $root; + $out['compiled'] = $this->compileFile($root); + $out['files'] = $this->allParsedFiles(); + $out['updated'] = time(); + return $out; + } else { + // No changes, pass back the structure + // we were given initially. + return $in; + } + + } + + // parse and compile buffer + // This is deprecated + public function parse($str = null, $initialVariables = null) { + if (is_array($str)) { + $initialVariables = $str; + $str = null; + } + + $oldVars = $this->registeredVars; + if ($initialVariables !== null) { + $this->setVariables($initialVariables); + } + + if ($str == null) { + if (empty($this->_parseFile)) { + throw new exception("nothing to parse"); + } + + $out = $this->compileFile($this->_parseFile); + } else { + $out = $this->compile($str); + } + + $this->registeredVars = $oldVars; + return $out; + } + + protected function makeParser($name) { + $parser = new lessc_parser($this, $name); + $parser->writeComments = $this->preserveComments; + + return $parser; + } + + public function setFormatter($name) { + $this->formatterName = $name; + } + + protected function newFormatter() { + $className = "lessc_formatter_lessjs"; + if (!empty($this->formatterName)) { + if (!is_string($this->formatterName)) + return $this->formatterName; + $className = "lessc_formatter_$this->formatterName"; + } + + return new $className; + } + + public function setPreserveComments($preserve) { + $this->preserveComments = $preserve; + } + + public function registerFunction($name, $func) { + $this->libFunctions[$name] = $func; + } + + public function unregisterFunction($name) { + unset($this->libFunctions[$name]); + } + + public function setVariables($variables) { + $this->registeredVars = array_merge($this->registeredVars, $variables); + } + + public function unsetVariable($name) { + unset($this->registeredVars[$name]); + } + + public function setImportDir($dirs) { + $this->importDir = (array)$dirs; + } + + public function addImportDir($dir) { + $this->importDir = (array)$this->importDir; + $this->importDir[] = $dir; + } + + public function allParsedFiles() { + return $this->allParsedFiles; + } + + public function addParsedFile($file) { + $this->allParsedFiles[realpath($file)] = filemtime($file); + } + + /** + * Uses the current value of $this->count to show line and line number + */ + public function throwError($msg = null) { + if ($this->sourceLoc >= 0) { + $this->sourceParser->throwError($msg, $this->sourceLoc); + } + throw new exception($msg); + } + + // compile file $in to file $out if $in is newer than $out + // returns true when it compiles, false otherwise + public static function ccompile($in, $out, $less = null) { + if ($less === null) { + $less = new self; + } + return $less->checkedCompile($in, $out); + } + + public static function cexecute($in, $force = false, $less = null) { + if ($less === null) { + $less = new self; + } + return $less->cachedCompile($in, $force); + } + + static protected $cssColors = array( + 'aliceblue' => '240,248,255', + 'antiquewhite' => '250,235,215', + 'aqua' => '0,255,255', + 'aquamarine' => '127,255,212', + 'azure' => '240,255,255', + 'beige' => '245,245,220', + 'bisque' => '255,228,196', + 'black' => '0,0,0', + 'blanchedalmond' => '255,235,205', + 'blue' => '0,0,255', + 'blueviolet' => '138,43,226', + 'brown' => '165,42,42', + 'burlywood' => '222,184,135', + 'cadetblue' => '95,158,160', + 'chartreuse' => '127,255,0', + 'chocolate' => '210,105,30', + 'coral' => '255,127,80', + 'cornflowerblue' => '100,149,237', + 'cornsilk' => '255,248,220', + 'crimson' => '220,20,60', + 'cyan' => '0,255,255', + 'darkblue' => '0,0,139', + 'darkcyan' => '0,139,139', + 'darkgoldenrod' => '184,134,11', + 'darkgray' => '169,169,169', + 'darkgreen' => '0,100,0', + 'darkgrey' => '169,169,169', + 'darkkhaki' => '189,183,107', + 'darkmagenta' => '139,0,139', + 'darkolivegreen' => '85,107,47', + 'darkorange' => '255,140,0', + 'darkorchid' => '153,50,204', + 'darkred' => '139,0,0', + 'darksalmon' => '233,150,122', + 'darkseagreen' => '143,188,143', + 'darkslateblue' => '72,61,139', + 'darkslategray' => '47,79,79', + 'darkslategrey' => '47,79,79', + 'darkturquoise' => '0,206,209', + 'darkviolet' => '148,0,211', + 'deeppink' => '255,20,147', + 'deepskyblue' => '0,191,255', + 'dimgray' => '105,105,105', + 'dimgrey' => '105,105,105', + 'dodgerblue' => '30,144,255', + 'firebrick' => '178,34,34', + 'floralwhite' => '255,250,240', + 'forestgreen' => '34,139,34', + 'fuchsia' => '255,0,255', + 'gainsboro' => '220,220,220', + 'ghostwhite' => '248,248,255', + 'gold' => '255,215,0', + 'goldenrod' => '218,165,32', + 'gray' => '128,128,128', + 'green' => '0,128,0', + 'greenyellow' => '173,255,47', + 'grey' => '128,128,128', + 'honeydew' => '240,255,240', + 'hotpink' => '255,105,180', + 'indianred' => '205,92,92', + 'indigo' => '75,0,130', + 'ivory' => '255,255,240', + 'khaki' => '240,230,140', + 'lavender' => '230,230,250', + 'lavenderblush' => '255,240,245', + 'lawngreen' => '124,252,0', + 'lemonchiffon' => '255,250,205', + 'lightblue' => '173,216,230', + 'lightcoral' => '240,128,128', + 'lightcyan' => '224,255,255', + 'lightgoldenrodyellow' => '250,250,210', + 'lightgray' => '211,211,211', + 'lightgreen' => '144,238,144', + 'lightgrey' => '211,211,211', + 'lightpink' => '255,182,193', + 'lightsalmon' => '255,160,122', + 'lightseagreen' => '32,178,170', + 'lightskyblue' => '135,206,250', + 'lightslategray' => '119,136,153', + 'lightslategrey' => '119,136,153', + 'lightsteelblue' => '176,196,222', + 'lightyellow' => '255,255,224', + 'lime' => '0,255,0', + 'limegreen' => '50,205,50', + 'linen' => '250,240,230', + 'magenta' => '255,0,255', + 'maroon' => '128,0,0', + 'mediumaquamarine' => '102,205,170', + 'mediumblue' => '0,0,205', + 'mediumorchid' => '186,85,211', + 'mediumpurple' => '147,112,219', + 'mediumseagreen' => '60,179,113', + 'mediumslateblue' => '123,104,238', + 'mediumspringgreen' => '0,250,154', + 'mediumturquoise' => '72,209,204', + 'mediumvioletred' => '199,21,133', + 'midnightblue' => '25,25,112', + 'mintcream' => '245,255,250', + 'mistyrose' => '255,228,225', + 'moccasin' => '255,228,181', + 'navajowhite' => '255,222,173', + 'navy' => '0,0,128', + 'oldlace' => '253,245,230', + 'olive' => '128,128,0', + 'olivedrab' => '107,142,35', + 'orange' => '255,165,0', + 'orangered' => '255,69,0', + 'orchid' => '218,112,214', + 'palegoldenrod' => '238,232,170', + 'palegreen' => '152,251,152', + 'paleturquoise' => '175,238,238', + 'palevioletred' => '219,112,147', + 'papayawhip' => '255,239,213', + 'peachpuff' => '255,218,185', + 'peru' => '205,133,63', + 'pink' => '255,192,203', + 'plum' => '221,160,221', + 'powderblue' => '176,224,230', + 'purple' => '128,0,128', + 'red' => '255,0,0', + 'rosybrown' => '188,143,143', + 'royalblue' => '65,105,225', + 'saddlebrown' => '139,69,19', + 'salmon' => '250,128,114', + 'sandybrown' => '244,164,96', + 'seagreen' => '46,139,87', + 'seashell' => '255,245,238', + 'sienna' => '160,82,45', + 'silver' => '192,192,192', + 'skyblue' => '135,206,235', + 'slateblue' => '106,90,205', + 'slategray' => '112,128,144', + 'slategrey' => '112,128,144', + 'snow' => '255,250,250', + 'springgreen' => '0,255,127', + 'steelblue' => '70,130,180', + 'tan' => '210,180,140', + 'teal' => '0,128,128', + 'thistle' => '216,191,216', + 'tomato' => '255,99,71', + 'transparent' => '0,0,0,0', + 'turquoise' => '64,224,208', + 'violet' => '238,130,238', + 'wheat' => '245,222,179', + 'white' => '255,255,255', + 'whitesmoke' => '245,245,245', + 'yellow' => '255,255,0', + 'yellowgreen' => '154,205,50' + ); } // responsible for taking a string of LESS code and converting it into a // syntax tree class lessc_parser { - static protected $nextBlockId = 0; // used to uniquely identify blocks - - static protected $precedence = array( - '=<' => 0, - '>=' => 0, - '=' => 0, - '<' => 0, - '>' => 0, - - '+' => 1, - '-' => 1, - '*' => 2, - '/' => 2, - '%' => 2, - ); - - static protected $whitePattern; - static protected $commentMulti; - - static protected $commentSingle = "//"; - static protected $commentMultiLeft = "/*"; - static protected $commentMultiRight = "*/"; - - // regex string to match any of the operators - static protected $operatorString; - - // these properties will supress division unless it's inside parenthases - static protected $supressDivisionProps = - array('/border-radius$/i', '/^font$/i'); - - protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); - protected $lineDirectives = array("charset"); - - /** - * if we are in parens we can be more liberal with whitespace around - * operators because it must evaluate to a single value and thus is less - * ambiguous. - * - * Consider: - * property1: 10 -5; // is two numbers, 10 and -5 - * property2: (10 -5); // should evaluate to 5 - */ - protected $inParens = false; - - // caches preg escaped literals - static protected $literalCache = array(); - - public function __construct($lessc, $sourceName = null) { - $this->eatWhiteDefault = true; - // reference to less needed for vPrefix, mPrefix, and parentSelector - $this->lessc = $lessc; - - $this->sourceName = $sourceName; // name used for error messages - - $this->writeComments = false; - - if (!self::$operatorString) { - self::$operatorString = - '('.implode('|', array_map(array('lessc', 'preg_quote'), - array_keys(self::$precedence))).')'; - - $commentSingle = lessc::preg_quote(self::$commentSingle); - $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); - $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); - - self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; - self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; - } - } - - public function parse($buffer) { - $this->count = 0; - $this->line = 1; - - $this->env = null; // block stack - $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); - $this->pushSpecialBlock("root"); - $this->eatWhiteDefault = true; - $this->seenComments = array(); - - // trim whitespace on head - // if (preg_match('/^\s+/', $this->buffer, $m)) { - // $this->line += substr_count($m[0], "\n"); - // $this->buffer = ltrim($this->buffer); - // } - $this->whitespace(); - - // parse the entire file - while (false !== $this->parseChunk()); - - if ($this->count != strlen($this->buffer)) - $this->throwError(); - - // TODO report where the block was opened - if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) - throw new exception('parse error: unclosed block'); - - return $this->env; - } - - /** - * Parse a single chunk off the head of the buffer and append it to the - * current parse environment. - * Returns false when the buffer is empty, or when there is an error. - * - * This function is called repeatedly until the entire document is - * parsed. - * - * This parser is most similar to a recursive descent parser. Single - * functions represent discrete grammatical rules for the language, and - * they are able to capture the text that represents those rules. - * - * Consider the function lessc::keyword(). (all parse functions are - * structured the same) - * - * The function takes a single reference argument. When calling the - * function it will attempt to match a keyword on the head of the buffer. - * If it is successful, it will place the keyword in the referenced - * argument, advance the position in the buffer, and return true. If it - * fails then it won't advance the buffer and it will return false. - * - * All of these parse functions are powered by lessc::match(), which behaves - * the same way, but takes a literal regular expression. Sometimes it is - * more convenient to use match instead of creating a new function. - * - * Because of the format of the functions, to parse an entire string of - * grammatical rules, you can chain them together using &&. - * - * But, if some of the rules in the chain succeed before one fails, then - * the buffer position will be left at an invalid state. In order to - * avoid this, lessc::seek() is used to remember and set buffer positions. - * - * Before parsing a chain, use $s = $this->seek() to remember the current - * position into $s. Then if a chain fails, use $this->seek($s) to - * go back where we started. - */ - protected function parseChunk() { - if (empty($this->buffer)) return false; - $s = $this->seek(); - - if ($this->whitespace()) { - return true; - } - - // setting a property - if ($this->keyword($key) && $this->assign() && - $this->propertyValue($value, $key) && $this->end()) - { - $this->append(array('assign', $key, $value), $s); - return true; - } else { - $this->seek($s); - } - - - // look for special css blocks - if ($this->literal('@', false)) { - $this->count--; - - // media - if ($this->literal('@media')) { - if (($this->mediaQueryList($mediaQueries) || true) - && $this->literal('{')) - { - $media = $this->pushSpecialBlock("media"); - $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; - return true; - } else { - $this->seek($s); - return false; - } - } - - if ($this->literal("@", false) && $this->keyword($dirName)) { - if ($this->isDirective($dirName, $this->blockDirectives)) { - if (($this->openString("{", $dirValue, null, array(";")) || true) && - $this->literal("{")) - { - $dir = $this->pushSpecialBlock("directive"); - $dir->name = $dirName; - if (isset($dirValue)) $dir->value = $dirValue; - return true; - } - } elseif ($this->isDirective($dirName, $this->lineDirectives)) { - if ($this->propertyValue($dirValue) && $this->end()) { - $this->append(array("directive", $dirName, $dirValue)); - return true; - } - } - } - - $this->seek($s); - } - - // setting a variable - if ($this->variable($var) && $this->assign() && - $this->propertyValue($value) && $this->end()) - { - $this->append(array('assign', $var, $value), $s); - return true; - } else { - $this->seek($s); - } - - if ($this->import($importValue)) { - $this->append($importValue, $s); - return true; - } - - // opening parametric mixin - if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && - ($this->guards($guards) || true) && - $this->literal('{')) - { - $block = $this->pushBlock($this->fixTags(array($tag))); - $block->args = $args; - $block->isVararg = $isVararg; - if (!empty($guards)) $block->guards = $guards; - return true; - } else { - $this->seek($s); - } - - // opening a simple block - if ($this->tags($tags) && $this->literal('{', false)) { - $tags = $this->fixTags($tags); - $this->pushBlock($tags); - return true; - } else { - $this->seek($s); - } - - // closing a block - if ($this->literal('}', false)) { - try { - $block = $this->pop(); - } catch (exception $e) { - $this->seek($s); - $this->throwError($e->getMessage()); - } - - $hidden = false; - if (is_null($block->type)) { - $hidden = true; - if (!isset($block->args)) { - foreach ($block->tags as $tag) { - if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { - $hidden = false; - break; - } - } - } - - foreach ($block->tags as $tag) { - if (is_string($tag)) { - $this->env->children[$tag][] = $block; - } - } - } - - if (!$hidden) { - $this->append(array('block', $block), $s); - } - - // this is done here so comments aren't bundled into he block that - // was just closed - $this->whitespace(); - return true; - } - - // mixin - if ($this->mixinTags($tags) && - ($this->argumentDef($argv, $isVararg) || true) && - ($this->keyword($suffix) || true) && $this->end()) - { - $tags = $this->fixTags($tags); - $this->append(array('mixin', $tags, $argv, $suffix), $s); - return true; - } else { - $this->seek($s); - } - - // spare ; - if ($this->literal(';')) return true; - - return false; // got nothing, throw error - } - - protected function isDirective($dirname, $directives) { - // TODO: cache pattern in parser - $pattern = implode("|", - array_map(array("lessc", "preg_quote"), $directives)); - $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; - - return preg_match($pattern, $dirname); - } - - protected function fixTags($tags) { - // move @ tags out of variable namespace - foreach ($tags as &$tag) { - if ($tag{0} == $this->lessc->vPrefix) - $tag[0] = $this->lessc->mPrefix; - } - return $tags; - } - - // a list of expressions - protected function expressionList(&$exps) { - $values = array(); - - while ($this->expression($exp)) { - $values[] = $exp; - } - - if (count($values) == 0) return false; - - $exps = lessc::compressList($values, ' '); - return true; - } - - /** - * Attempt to consume an expression. - * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code - */ - protected function expression(&$out) { - if ($this->value($lhs)) { - $out = $this->expHelper($lhs, 0); - - // look for / shorthand - if (!empty($this->env->supressedDivision)) { - unset($this->env->supressedDivision); - $s = $this->seek(); - if ($this->literal("/") && $this->value($rhs)) { - $out = array("list", "", - array($out, array("keyword", "/"), $rhs)); - } else { - $this->seek($s); - } - } - - return true; - } - return false; - } - - /** - * recursively parse infix equation with $lhs at precedence $minP - */ - protected function expHelper($lhs, $minP) { - $this->inExp = true; - $ss = $this->seek(); - - while (true) { - $whiteBefore = isset($this->buffer[$this->count - 1]) && - ctype_space($this->buffer[$this->count - 1]); - - // If there is whitespace before the operator, then we require - // whitespace after the operator for it to be an expression - $needWhite = $whiteBefore && !$this->inParens; - - if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { - if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { - foreach (self::$supressDivisionProps as $pattern) { - if (preg_match($pattern, $this->env->currentProperty)) { - $this->env->supressedDivision = true; - break 2; - } - } - } - - - $whiteAfter = isset($this->buffer[$this->count - 1]) && - ctype_space($this->buffer[$this->count - 1]); - - if (!$this->value($rhs)) break; - - // peek for next operator to see what to do with rhs - if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { - $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); - } - - $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); - $ss = $this->seek(); - - continue; - } - - break; - } - - $this->seek($ss); - - return $lhs; - } - - // consume a list of values for a property - public function propertyValue(&$value, $keyName = null) { - $values = array(); - - if ($keyName !== null) $this->env->currentProperty = $keyName; - - $s = null; - while ($this->expressionList($v)) { - $values[] = $v; - $s = $this->seek(); - if (!$this->literal(',')) break; - } - - if ($s) $this->seek($s); - - if ($keyName !== null) unset($this->env->currentProperty); - - if (count($values) == 0) return false; - - $value = lessc::compressList($values, ', '); - return true; - } - - protected function parenValue(&$out) { - $s = $this->seek(); - - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { - return false; - } - - $inParens = $this->inParens; - if ($this->literal("(") && - ($this->inParens = true) && $this->expression($exp) && - $this->literal(")")) - { - $out = $exp; - $this->inParens = $inParens; - return true; - } else { - $this->inParens = $inParens; - $this->seek($s); - } - - return false; - } - - // a single value - protected function value(&$value) { - $s = $this->seek(); - - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { - // negation - if ($this->literal("-", false) && - (($this->variable($inner) && $inner = array("variable", $inner)) || - $this->unit($inner) || - $this->parenValue($inner))) - { - $value = array("unary", "-", $inner); - return true; - } else { - $this->seek($s); - } - } - - if ($this->parenValue($value)) return true; - if ($this->unit($value)) return true; - if ($this->color($value)) return true; - if ($this->func($value)) return true; - if ($this->string($value)) return true; - - if ($this->keyword($word)) { - $value = array('keyword', $word); - return true; - } - - // try a variable - if ($this->variable($var)) { - $value = array('variable', $var); - return true; - } - - // unquote string (should this work on any type? - if ($this->literal("~") && $this->string($str)) { - $value = array("escape", $str); - return true; - } else { - $this->seek($s); - } - - // css hack: \0 - if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { - $value = array('keyword', '\\'.$m[1]); - return true; - } else { - $this->seek($s); - } - - return false; - } - - // an import statement - protected function import(&$out) { - if (!$this->literal('@import')) return false; - - // @import "something.css" media; - // @import url("something.css") media; - // @import url(something.css) media; - - if ($this->propertyValue($value)) { - $out = array("import", $value); - return true; - } - } - - protected function mediaQueryList(&$out) { - if ($this->genericList($list, "mediaQuery", ",", false)) { - $out = $list[2]; - return true; - } - return false; - } - - protected function mediaQuery(&$out) { - $s = $this->seek(); - - $expressions = null; - $parts = array(); - - if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { - $prop = array("mediaType"); - if (isset($only)) $prop[] = "only"; - if (isset($not)) $prop[] = "not"; - $prop[] = $mediaType; - $parts[] = $prop; - } else { - $this->seek($s); - } - - - if (!empty($mediaType) && !$this->literal("and")) { - // ~ - } else { - $this->genericList($expressions, "mediaExpression", "and", false); - if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); - } - - if (count($parts) == 0) { - $this->seek($s); - return false; - } - - $out = $parts; - return true; - } - - protected function mediaExpression(&$out) { - $s = $this->seek(); - $value = null; - if ($this->literal("(") && - $this->keyword($feature) && - ($this->literal(":") && $this->expression($value) || true) && - $this->literal(")")) - { - $out = array("mediaExp", $feature); - if ($value) $out[] = $value; - return true; - } elseif ($this->variable($variable)) { - $out = array('variable', $variable); - return true; - } - - $this->seek($s); - return false; - } - - // an unbounded string stopped by $end - protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - $stop = array("'", '"', "@{", $end); - $stop = array_map(array("lessc", "preg_quote"), $stop); - // $stop[] = self::$commentMulti; - - if (!is_null($rejectStrs)) { - $stop = array_merge($stop, $rejectStrs); - } - - $patt = '(.*?)('.implode("|", $stop).')'; - - $nestingLevel = 0; - - $content = array(); - while ($this->match($patt, $m, false)) { - if (!empty($m[1])) { - $content[] = $m[1]; - if ($nestingOpen) { - $nestingLevel += substr_count($m[1], $nestingOpen); - } - } - - $tok = $m[2]; - - $this->count-= strlen($tok); - if ($tok == $end) { - if ($nestingLevel == 0) { - break; - } else { - $nestingLevel--; - } - } - - if (($tok == "'" || $tok == '"') && $this->string($str)) { - $content[] = $str; - continue; - } - - if ($tok == "@{" && $this->interpolation($inter)) { - $content[] = $inter; - continue; - } - - if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { - break; - } - - $content[] = $tok; - $this->count+= strlen($tok); - } - - $this->eatWhiteDefault = $oldWhite; - - if (count($content) == 0) return false; - - // trim the end - if (is_string(end($content))) { - $content[count($content) - 1] = rtrim(end($content)); - } - - $out = array("string", "", $content); - return true; - } - - protected function string(&$out) { - $s = $this->seek(); - if ($this->literal('"', false)) { - $delim = '"'; - } elseif ($this->literal("'", false)) { - $delim = "'"; - } else { - return false; - } - - $content = array(); - - // look for either ending delim , escape, or string interpolation - $patt = '([^\n]*?)(@\{|\\\\|' . - lessc::preg_quote($delim).')'; - - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - while ($this->match($patt, $m, false)) { - $content[] = $m[1]; - if ($m[2] == "@{") { - $this->count -= strlen($m[2]); - if ($this->interpolation($inter, false)) { - $content[] = $inter; - } else { - $this->count += strlen($m[2]); - $content[] = "@{"; // ignore it - } - } elseif ($m[2] == '\\') { - $content[] = $m[2]; - if ($this->literal($delim, false)) { - $content[] = $delim; - } - } else { - $this->count -= strlen($delim); - break; // delim - } - } - - $this->eatWhiteDefault = $oldWhite; - - if ($this->literal($delim)) { - $out = array("string", $delim, $content); - return true; - } - - $this->seek($s); - return false; - } - - protected function interpolation(&$out) { - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = true; - - $s = $this->seek(); - if ($this->literal("@{") && - $this->openString("}", $interp, null, array("'", '"', ";")) && - $this->literal("}", false)) - { - $out = array("interpolate", $interp); - $this->eatWhiteDefault = $oldWhite; - if ($this->eatWhiteDefault) $this->whitespace(); - return true; - } - - $this->eatWhiteDefault = $oldWhite; - $this->seek($s); - return false; - } - - protected function unit(&$unit) { - // speed shortcut - if (isset($this->buffer[$this->count])) { - $char = $this->buffer[$this->count]; - if (!ctype_digit($char) && $char != ".") return false; - } - - if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { - $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); - return true; - } - return false; - } - - // a # color - protected function color(&$out) { - if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { - if (strlen($m[1]) > 7) { - $out = array("string", "", array($m[1])); - } else { - $out = array("raw_color", $m[1]); - } - return true; - } - - return false; - } - - // consume an argument definition list surrounded by () - // each argument is a variable name with optional value - // or at the end a ... or a variable named followed by ... - // arguments are separated by , unless a ; is in the list, then ; is the - // delimiter. - protected function argumentDef(&$args, &$isVararg) { - $s = $this->seek(); - if (!$this->literal('(')) return false; - - $values = array(); - $delim = ","; - $method = "expressionList"; - - $isVararg = false; - while (true) { - if ($this->literal("...")) { - $isVararg = true; - break; - } - - if ($this->$method($value)) { - if ($value[0] == "variable") { - $arg = array("arg", $value[1]); - $ss = $this->seek(); - - if ($this->assign() && $this->$method($rhs)) { - $arg[] = $rhs; - } else { - $this->seek($ss); - if ($this->literal("...")) { - $arg[0] = "rest"; - $isVararg = true; - } - } - - $values[] = $arg; - if ($isVararg) break; - continue; - } else { - $values[] = array("lit", $value); - } - } - - - if (!$this->literal($delim)) { - if ($delim == "," && $this->literal(";")) { - // found new delim, convert existing args - $delim = ";"; - $method = "propertyValue"; - - // transform arg list - if (isset($values[1])) { // 2 items - $newList = array(); - foreach ($values as $i => $arg) { - switch($arg[0]) { - case "arg": - if ($i) { - $this->throwError("Cannot mix ; and , as delimiter types"); - } - $newList[] = $arg[2]; - break; - case "lit": - $newList[] = $arg[1]; - break; - case "rest": - $this->throwError("Unexpected rest before semicolon"); - } - } - - $newList = array("list", ", ", $newList); - - switch ($values[0][0]) { - case "arg": - $newArg = array("arg", $values[0][1], $newList); - break; - case "lit": - $newArg = array("lit", $newList); - break; - } - - } elseif ($values) { // 1 item - $newArg = $values[0]; - } - - if ($newArg) { - $values = array($newArg); - } - } else { - break; - } - } - } - - if (!$this->literal(')')) { - $this->seek($s); - return false; - } - - $args = $values; - - return true; - } - - // consume a list of tags - // this accepts a hanging delimiter - protected function tags(&$tags, $simple = false, $delim = ',') { - $tags = array(); - while ($this->tag($tt, $simple)) { - $tags[] = $tt; - if (!$this->literal($delim)) break; - } - if (count($tags) == 0) return false; - - return true; - } - - // list of tags of specifying mixin path - // optionally separated by > (lazy, accepts extra >) - protected function mixinTags(&$tags) { - $tags = array(); - while ($this->tag($tt, true)) { - $tags[] = $tt; - $this->literal(">"); - } - - if (count($tags) == 0) return false; - - return true; - } - - // a bracketed value (contained within in a tag definition) - protected function tagBracket(&$parts, &$hasExpression) { - // speed shortcut - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { - return false; - } - - $s = $this->seek(); - - $hasInterpolation = false; - - if ($this->literal("[", false)) { - $attrParts = array("["); - // keyword, string, operator - while (true) { - if ($this->literal("]", false)) { - $this->count--; - break; // get out early - } - - if ($this->match('\s+', $m)) { - $attrParts[] = " "; - continue; - } - if ($this->string($str)) { - // escape parent selector, (yuck) - foreach ($str[2] as &$chunk) { - $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); - } - - $attrParts[] = $str; - $hasInterpolation = true; - continue; - } - - if ($this->keyword($word)) { - $attrParts[] = $word; - continue; - } - - if ($this->interpolation($inter, false)) { - $attrParts[] = $inter; - $hasInterpolation = true; - continue; - } - - // operator, handles attr namespace too - if ($this->match('[|-~\$\*\^=]+', $m)) { - $attrParts[] = $m[0]; - continue; - } - - break; - } - - if ($this->literal("]", false)) { - $attrParts[] = "]"; - foreach ($attrParts as $part) { - $parts[] = $part; - } - $hasExpression = $hasExpression || $hasInterpolation; - return true; - } - $this->seek($s); - } - - $this->seek($s); - return false; - } - - // a space separated list of selectors - protected function tag(&$tag, $simple = false) { - if ($simple) - $chars = '^@,:;{}\][>\(\) "\''; - else - $chars = '^@,;{}["\''; - - $s = $this->seek(); - - $hasExpression = false; - $parts = array(); - while ($this->tagBracket($parts, $hasExpression)); - - $oldWhite = $this->eatWhiteDefault; - $this->eatWhiteDefault = false; - - while (true) { - if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { - $parts[] = $m[1]; - if ($simple) break; - - while ($this->tagBracket($parts, $hasExpression)); - continue; - } - - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { - if ($this->interpolation($interp)) { - $hasExpression = true; - $interp[2] = true; // don't unescape - $parts[] = $interp; - continue; - } - - if ($this->literal("@")) { - $parts[] = "@"; - continue; - } - } - - if ($this->unit($unit)) { // for keyframes - $parts[] = $unit[1]; - $parts[] = $unit[2]; - continue; - } - - break; - } - - $this->eatWhiteDefault = $oldWhite; - if (!$parts) { - $this->seek($s); - return false; - } - - if ($hasExpression) { - $tag = array("exp", array("string", "", $parts)); - } else { - $tag = trim(implode($parts)); - } - - $this->whitespace(); - return true; - } - - // a css function - protected function func(&$func) { - $s = $this->seek(); - - if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { - $fname = $m[1]; - - $sPreArgs = $this->seek(); - - $args = array(); - while (true) { - $ss = $this->seek(); - // this ugly nonsense is for ie filter properties - if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { - $args[] = array("string", "", array($name, "=", $value)); - } else { - $this->seek($ss); - if ($this->expressionList($value)) { - $args[] = $value; - } - } - - if (!$this->literal(',')) break; - } - $args = array('list', ',', $args); - - if ($this->literal(')')) { - $func = array('function', $fname, $args); - return true; - } elseif ($fname == 'url') { - // couldn't parse and in url? treat as string - $this->seek($sPreArgs); - if ($this->openString(")", $string) && $this->literal(")")) { - $func = array('function', $fname, $string); - return true; - } - } - } - - $this->seek($s); - return false; - } - - // consume a less variable - protected function variable(&$name) { - $s = $this->seek(); - if ($this->literal($this->lessc->vPrefix, false) && - ($this->variable($sub) || $this->keyword($name))) - { - if (!empty($sub)) { - $name = array('variable', $sub); - } else { - $name = $this->lessc->vPrefix.$name; - } - return true; - } - - $name = null; - $this->seek($s); - return false; - } - - /** - * Consume an assignment operator - * Can optionally take a name that will be set to the current property name - */ - protected function assign($name = null) { - if ($name) $this->currentProperty = $name; - return $this->literal(':') || $this->literal('='); - } - - // consume a keyword - protected function keyword(&$word) { - if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { - $word = $m[1]; - return true; - } - return false; - } - - // consume an end of statement delimiter - protected function end() { - if ($this->literal(';', false)) { - return true; - } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { - // if there is end of file or a closing block next then we don't need a ; - return true; - } - return false; - } - - protected function guards(&$guards) { - $s = $this->seek(); - - if (!$this->literal("when")) { - $this->seek($s); - return false; - } - - $guards = array(); - - while ($this->guardGroup($g)) { - $guards[] = $g; - if (!$this->literal(",")) break; - } - - if (count($guards) == 0) { - $guards = null; - $this->seek($s); - return false; - } - - return true; - } - - // a bunch of guards that are and'd together - // TODO rename to guardGroup - protected function guardGroup(&$guardGroup) { - $s = $this->seek(); - $guardGroup = array(); - while ($this->guard($guard)) { - $guardGroup[] = $guard; - if (!$this->literal("and")) break; - } - - if (count($guardGroup) == 0) { - $guardGroup = null; - $this->seek($s); - return false; - } - - return true; - } - - protected function guard(&$guard) { - $s = $this->seek(); - $negate = $this->literal("not"); - - if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { - $guard = $exp; - if ($negate) $guard = array("negate", $guard); - return true; - } - - $this->seek($s); - return false; - } - - /* raw parsing functions */ - - protected function literal($what, $eatWhitespace = null) { - if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; - - // shortcut on single letter - if (!isset($what[1]) && isset($this->buffer[$this->count])) { - if ($this->buffer[$this->count] == $what) { - if (!$eatWhitespace) { - $this->count++; - return true; - } - // goes below... - } else { - return false; - } - } - - if (!isset(self::$literalCache[$what])) { - self::$literalCache[$what] = lessc::preg_quote($what); - } - - return $this->match(self::$literalCache[$what], $m, $eatWhitespace); - } - - protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { - $s = $this->seek(); - $items = array(); - while ($this->$parseItem($value)) { - $items[] = $value; - if ($delim) { - if (!$this->literal($delim)) break; - } - } - - if (count($items) == 0) { - $this->seek($s); - return false; - } - - if ($flatten && count($items) == 1) { - $out = $items[0]; - } else { - $out = array("list", $delim, $items); - } - - return true; - } - - - // advance counter to next occurrence of $what - // $until - don't include $what in advance - // $allowNewline, if string, will be used as valid char set - protected function to($what, &$out, $until = false, $allowNewline = false) { - if (is_string($allowNewline)) { - $validChars = $allowNewline; - } else { - $validChars = $allowNewline ? "." : "[^\n]"; - } - if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; - if ($until) $this->count -= strlen($what); // give back $what - $out = $m[1]; - return true; - } - - // try to match something on head of buffer - protected function match($regex, &$out, $eatWhitespace = null) { - if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; - - $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; - if (preg_match($r, $this->buffer, $out, null, $this->count)) { - $this->count += strlen($out[0]); - if ($eatWhitespace && $this->writeComments) $this->whitespace(); - return true; - } - return false; - } - - // match some whitespace - protected function whitespace() { - if ($this->writeComments) { - $gotWhite = false; - while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { - if (isset($m[1]) && empty($this->seenComments[$this->count])) { - $this->append(array("comment", $m[1])); - $this->seenComments[$this->count] = true; - } - $this->count += strlen($m[0]); - $gotWhite = true; - } - return $gotWhite; - } else { - $this->match("", $m); - return strlen($m[0]) > 0; - } - } - - // match something without consuming it - protected function peek($regex, &$out = null, $from=null) { - if (is_null($from)) $from = $this->count; - $r = '/'.$regex.'/Ais'; - $result = preg_match($r, $this->buffer, $out, null, $from); - - return $result; - } - - // seek to a spot in the buffer or return where we are on no argument - protected function seek($where = null) { - if ($where === null) return $this->count; - else $this->count = $where; - return true; - } - - /* misc functions */ - - public function throwError($msg = "parse error", $count = null) { - $count = is_null($count) ? $this->count : $count; - - $line = $this->line + - substr_count(substr($this->buffer, 0, $count), "\n"); - - if (!empty($this->sourceName)) { - $loc = "$this->sourceName on line $line"; - } else { - $loc = "line: $line"; - } - - // TODO this depends on $this->count - if ($this->peek("(.*?)(\n|$)", $m, $count)) { - throw new exception("$msg: failed at `$m[1]` $loc"); - } else { - throw new exception("$msg: $loc"); - } - } - - protected function pushBlock($selectors=null, $type=null) { - $b = new stdclass; - $b->parent = $this->env; - - $b->type = $type; - $b->id = self::$nextBlockId++; - - $b->isVararg = false; // TODO: kill me from here - $b->tags = $selectors; - - $b->props = array(); - $b->children = array(); - - $this->env = $b; - return $b; - } - - // push a block that doesn't multiply tags - protected function pushSpecialBlock($type) { - return $this->pushBlock(null, $type); - } - - // append a property to the current block - protected function append($prop, $pos = null) { - if ($pos !== null) $prop[-1] = $pos; - $this->env->props[] = $prop; - } - - // pop something off the stack - protected function pop() { - $old = $this->env; - $this->env = $this->env->parent; - return $old; - } - - // remove comments from $text - // todo: make it work for all functions, not just url - protected function removeComments($text) { - $look = array( - 'url(', '//', '/*', '"', "'" - ); - - $out = ''; - $min = null; - while (true) { - // find the next item - foreach ($look as $token) { - $pos = strpos($text, $token); - if ($pos !== false) { - if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); - } - } - - if (is_null($min)) break; - - $count = $min[1]; - $skip = 0; - $newlines = 0; - switch ($min[0]) { - case 'url(': - if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) - $count += strlen($m[0]) - strlen($min[0]); - break; - case '"': - case "'": - if (preg_match('/'.$min[0].'.*?(? 0, + '>=' => 0, + '=' => 0, + '<' => 0, + '>' => 0, + + '+' => 1, + '-' => 1, + '*' => 2, + '/' => 2, + '%' => 2, + ); + + static protected $whitePattern; + static protected $commentMulti; + + static protected $commentSingle = "//"; + static protected $commentMultiLeft = "/*"; + static protected $commentMultiRight = "*/"; + + // regex string to match any of the operators + static protected $operatorString; + + // these properties will supress division unless it's inside parenthases + static protected $supressDivisionProps = + array('/border-radius$/i', '/^font$/i'); + + protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); + protected $lineDirectives = array("charset"); + + /** + * if we are in parens we can be more liberal with whitespace around + * operators because it must evaluate to a single value and thus is less + * ambiguous. + * + * Consider: + * property1: 10 -5; // is two numbers, 10 and -5 + * property2: (10 -5); // should evaluate to 5 + */ + protected $inParens = false; + + // caches preg escaped literals + static protected $literalCache = array(); + + public function __construct($lessc, $sourceName = null) { + $this->eatWhiteDefault = true; + // reference to less needed for vPrefix, mPrefix, and parentSelector + $this->lessc = $lessc; + + $this->sourceName = $sourceName; // name used for error messages + + $this->writeComments = false; + + if (!self::$operatorString) { + self::$operatorString = + '('.implode('|', array_map(array('lessc', 'preg_quote'), + array_keys(self::$precedence))).')'; + + $commentSingle = lessc::preg_quote(self::$commentSingle); + $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); + $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); + + self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; + self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; + } + } + + public function parse($buffer) { + $this->count = 0; + $this->line = 1; + + $this->env = null; // block stack + $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); + $this->pushSpecialBlock("root"); + $this->eatWhiteDefault = true; + $this->seenComments = array(); + + // trim whitespace on head + // if (preg_match('/^\s+/', $this->buffer, $m)) { + // $this->line += substr_count($m[0], "\n"); + // $this->buffer = ltrim($this->buffer); + // } + $this->whitespace(); + + // parse the entire file + while (false !== $this->parseChunk()); + + if ($this->count != strlen($this->buffer)) + $this->throwError(); + + // TODO report where the block was opened + if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) + throw new exception('parse error: unclosed block'); + + return $this->env; + } + + /** + * Parse a single chunk off the head of the buffer and append it to the + * current parse environment. + * Returns false when the buffer is empty, or when there is an error. + * + * This function is called repeatedly until the entire document is + * parsed. + * + * This parser is most similar to a recursive descent parser. Single + * functions represent discrete grammatical rules for the language, and + * they are able to capture the text that represents those rules. + * + * Consider the function lessc::keyword(). (all parse functions are + * structured the same) + * + * The function takes a single reference argument. When calling the + * function it will attempt to match a keyword on the head of the buffer. + * If it is successful, it will place the keyword in the referenced + * argument, advance the position in the buffer, and return true. If it + * fails then it won't advance the buffer and it will return false. + * + * All of these parse functions are powered by lessc::match(), which behaves + * the same way, but takes a literal regular expression. Sometimes it is + * more convenient to use match instead of creating a new function. + * + * Because of the format of the functions, to parse an entire string of + * grammatical rules, you can chain them together using &&. + * + * But, if some of the rules in the chain succeed before one fails, then + * the buffer position will be left at an invalid state. In order to + * avoid this, lessc::seek() is used to remember and set buffer positions. + * + * Before parsing a chain, use $s = $this->seek() to remember the current + * position into $s. Then if a chain fails, use $this->seek($s) to + * go back where we started. + */ + protected function parseChunk() { + if (empty($this->buffer)) return false; + $s = $this->seek(); + + if ($this->whitespace()) { + return true; + } + + // setting a property + if ($this->keyword($key) && $this->assign() && + $this->propertyValue($value, $key) && $this->end()) + { + $this->append(array('assign', $key, $value), $s); + return true; + } else { + $this->seek($s); + } + + + // look for special css blocks + if ($this->literal('@', false)) { + $this->count--; + + // media + if ($this->literal('@media')) { + if (($this->mediaQueryList($mediaQueries) || true) + && $this->literal('{')) + { + $media = $this->pushSpecialBlock("media"); + $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; + return true; + } else { + $this->seek($s); + return false; + } + } + + if ($this->literal("@", false) && $this->keyword($dirName)) { + if ($this->isDirective($dirName, $this->blockDirectives)) { + if (($this->openString("{", $dirValue, null, array(";")) || true) && + $this->literal("{")) + { + $dir = $this->pushSpecialBlock("directive"); + $dir->name = $dirName; + if (isset($dirValue)) $dir->value = $dirValue; + return true; + } + } elseif ($this->isDirective($dirName, $this->lineDirectives)) { + if ($this->propertyValue($dirValue) && $this->end()) { + $this->append(array("directive", $dirName, $dirValue)); + return true; + } + } + } + + $this->seek($s); + } + + // setting a variable + if ($this->variable($var) && $this->assign() && + $this->propertyValue($value) && $this->end()) + { + $this->append(array('assign', $var, $value), $s); + return true; + } else { + $this->seek($s); + } + + if ($this->import($importValue)) { + $this->append($importValue, $s); + return true; + } + + // opening parametric mixin + if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && + ($this->guards($guards) || true) && + $this->literal('{')) + { + $block = $this->pushBlock($this->fixTags(array($tag))); + $block->args = $args; + $block->isVararg = $isVararg; + if (!empty($guards)) $block->guards = $guards; + return true; + } else { + $this->seek($s); + } + + // opening a simple block + if ($this->tags($tags) && $this->literal('{', false)) { + $tags = $this->fixTags($tags); + $this->pushBlock($tags); + return true; + } else { + $this->seek($s); + } + + // closing a block + if ($this->literal('}', false)) { + try { + $block = $this->pop(); + } catch (exception $e) { + $this->seek($s); + $this->throwError($e->getMessage()); + } + + $hidden = false; + if (is_null($block->type)) { + $hidden = true; + if (!isset($block->args)) { + foreach ($block->tags as $tag) { + if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) { + $hidden = false; + break; + } + } + } + + foreach ($block->tags as $tag) { + if (is_string($tag)) { + $this->env->children[$tag][] = $block; + } + } + } + + if (!$hidden) { + $this->append(array('block', $block), $s); + } + + // this is done here so comments aren't bundled into he block that + // was just closed + $this->whitespace(); + return true; + } + + // mixin + if ($this->mixinTags($tags) && + ($this->argumentDef($argv, $isVararg) || true) && + ($this->keyword($suffix) || true) && $this->end()) + { + $tags = $this->fixTags($tags); + $this->append(array('mixin', $tags, $argv, $suffix), $s); + return true; + } else { + $this->seek($s); + } + + // spare ; + if ($this->literal(';')) return true; + + return false; // got nothing, throw error + } + + protected function isDirective($dirname, $directives) { + // TODO: cache pattern in parser + $pattern = implode("|", + array_map(array("lessc", "preg_quote"), $directives)); + $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; + + return preg_match($pattern, $dirname); + } + + protected function fixTags($tags) { + // move @ tags out of variable namespace + foreach ($tags as &$tag) { + if ($tag{0} == $this->lessc->vPrefix) + $tag[0] = $this->lessc->mPrefix; + } + return $tags; + } + + // a list of expressions + protected function expressionList(&$exps) { + $values = array(); + + while ($this->expression($exp)) { + $values[] = $exp; + } + + if (count($values) == 0) return false; + + $exps = lessc::compressList($values, ' '); + return true; + } + + /** + * Attempt to consume an expression. + * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code + */ + protected function expression(&$out) { + if ($this->value($lhs)) { + $out = $this->expHelper($lhs, 0); + + // look for / shorthand + if (!empty($this->env->supressedDivision)) { + unset($this->env->supressedDivision); + $s = $this->seek(); + if ($this->literal("/") && $this->value($rhs)) { + $out = array("list", "", + array($out, array("keyword", "/"), $rhs)); + } else { + $this->seek($s); + } + } + + return true; + } + return false; + } + + /** + * recursively parse infix equation with $lhs at precedence $minP + */ + protected function expHelper($lhs, $minP) { + $this->inExp = true; + $ss = $this->seek(); + + while (true) { + $whiteBefore = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + // If there is whitespace before the operator, then we require + // whitespace after the operator for it to be an expression + $needWhite = $whiteBefore && !$this->inParens; + + if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { + if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { + foreach (self::$supressDivisionProps as $pattern) { + if (preg_match($pattern, $this->env->currentProperty)) { + $this->env->supressedDivision = true; + break 2; + } + } + } + + + $whiteAfter = isset($this->buffer[$this->count - 1]) && + ctype_space($this->buffer[$this->count - 1]); + + if (!$this->value($rhs)) break; + + // peek for next operator to see what to do with rhs + if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { + $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); + } + + $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); + $ss = $this->seek(); + + continue; + } + + break; + } + + $this->seek($ss); + + return $lhs; + } + + // consume a list of values for a property + public function propertyValue(&$value, $keyName = null) { + $values = array(); + + if ($keyName !== null) $this->env->currentProperty = $keyName; + + $s = null; + while ($this->expressionList($v)) { + $values[] = $v; + $s = $this->seek(); + if (!$this->literal(',')) break; + } + + if ($s) $this->seek($s); + + if ($keyName !== null) unset($this->env->currentProperty); + + if (count($values) == 0) return false; + + $value = lessc::compressList($values, ', '); + return true; + } + + protected function parenValue(&$out) { + $s = $this->seek(); + + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { + return false; + } + + $inParens = $this->inParens; + if ($this->literal("(") && + ($this->inParens = true) && $this->expression($exp) && + $this->literal(")")) + { + $out = $exp; + $this->inParens = $inParens; + return true; + } else { + $this->inParens = $inParens; + $this->seek($s); + } + + return false; + } + + // a single value + protected function value(&$value) { + $s = $this->seek(); + + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { + // negation + if ($this->literal("-", false) && + (($this->variable($inner) && $inner = array("variable", $inner)) || + $this->unit($inner) || + $this->parenValue($inner))) + { + $value = array("unary", "-", $inner); + return true; + } else { + $this->seek($s); + } + } + + if ($this->parenValue($value)) return true; + if ($this->unit($value)) return true; + if ($this->color($value)) return true; + if ($this->func($value)) return true; + if ($this->string($value)) return true; + + if ($this->keyword($word)) { + $value = array('keyword', $word); + return true; + } + + // try a variable + if ($this->variable($var)) { + $value = array('variable', $var); + return true; + } + + // unquote string (should this work on any type? + if ($this->literal("~") && $this->string($str)) { + $value = array("escape", $str); + return true; + } else { + $this->seek($s); + } + + // css hack: \0 + if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { + $value = array('keyword', '\\'.$m[1]); + return true; + } else { + $this->seek($s); + } + + return false; + } + + // an import statement + protected function import(&$out) { + if (!$this->literal('@import')) return false; + + // @import "something.css" media; + // @import url("something.css") media; + // @import url(something.css) media; + + if ($this->propertyValue($value)) { + $out = array("import", $value); + return true; + } + } + + protected function mediaQueryList(&$out) { + if ($this->genericList($list, "mediaQuery", ",", false)) { + $out = $list[2]; + return true; + } + return false; + } + + protected function mediaQuery(&$out) { + $s = $this->seek(); + + $expressions = null; + $parts = array(); + + if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { + $prop = array("mediaType"); + if (isset($only)) $prop[] = "only"; + if (isset($not)) $prop[] = "not"; + $prop[] = $mediaType; + $parts[] = $prop; + } else { + $this->seek($s); + } + + + if (!empty($mediaType) && !$this->literal("and")) { + // ~ + } else { + $this->genericList($expressions, "mediaExpression", "and", false); + if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); + } + + if (count($parts) == 0) { + $this->seek($s); + return false; + } + + $out = $parts; + return true; + } + + protected function mediaExpression(&$out) { + $s = $this->seek(); + $value = null; + if ($this->literal("(") && + $this->keyword($feature) && + ($this->literal(":") && $this->expression($value) || true) && + $this->literal(")")) + { + $out = array("mediaExp", $feature); + if ($value) $out[] = $value; + return true; + } elseif ($this->variable($variable)) { + $out = array('variable', $variable); + return true; + } + + $this->seek($s); + return false; + } + + // an unbounded string stopped by $end + protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + $stop = array("'", '"', "@{", $end); + $stop = array_map(array("lessc", "preg_quote"), $stop); + // $stop[] = self::$commentMulti; + + if (!is_null($rejectStrs)) { + $stop = array_merge($stop, $rejectStrs); + } + + $patt = '(.*?)('.implode("|", $stop).')'; + + $nestingLevel = 0; + + $content = array(); + while ($this->match($patt, $m, false)) { + if (!empty($m[1])) { + $content[] = $m[1]; + if ($nestingOpen) { + $nestingLevel += substr_count($m[1], $nestingOpen); + } + } + + $tok = $m[2]; + + $this->count-= strlen($tok); + if ($tok == $end) { + if ($nestingLevel == 0) { + break; + } else { + $nestingLevel--; + } + } + + if (($tok == "'" || $tok == '"') && $this->string($str)) { + $content[] = $str; + continue; + } + + if ($tok == "@{" && $this->interpolation($inter)) { + $content[] = $inter; + continue; + } + + if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { + break; + } + + $content[] = $tok; + $this->count+= strlen($tok); + } + + $this->eatWhiteDefault = $oldWhite; + + if (count($content) == 0) return false; + + // trim the end + if (is_string(end($content))) { + $content[count($content) - 1] = rtrim(end($content)); + } + + $out = array("string", "", $content); + return true; + } + + protected function string(&$out) { + $s = $this->seek(); + if ($this->literal('"', false)) { + $delim = '"'; + } elseif ($this->literal("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = array(); + + // look for either ending delim , escape, or string interpolation + $patt = '([^\n]*?)(@\{|\\\\|' . + lessc::preg_quote($delim).')'; + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while ($this->match($patt, $m, false)) { + $content[] = $m[1]; + if ($m[2] == "@{") { + $this->count -= strlen($m[2]); + if ($this->interpolation($inter, false)) { + $content[] = $inter; + } else { + $this->count += strlen($m[2]); + $content[] = "@{"; // ignore it + } + } elseif ($m[2] == '\\') { + $content[] = $m[2]; + if ($this->literal($delim, false)) { + $content[] = $delim; + } + } else { + $this->count -= strlen($delim); + break; // delim + } + } + + $this->eatWhiteDefault = $oldWhite; + + if ($this->literal($delim)) { + $out = array("string", $delim, $content); + return true; + } + + $this->seek($s); + return false; + } + + protected function interpolation(&$out) { + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = true; + + $s = $this->seek(); + if ($this->literal("@{") && + $this->openString("}", $interp, null, array("'", '"', ";")) && + $this->literal("}", false)) + { + $out = array("interpolate", $interp); + $this->eatWhiteDefault = $oldWhite; + if ($this->eatWhiteDefault) $this->whitespace(); + return true; + } + + $this->eatWhiteDefault = $oldWhite; + $this->seek($s); + return false; + } + + protected function unit(&$unit) { + // speed shortcut + if (isset($this->buffer[$this->count])) { + $char = $this->buffer[$this->count]; + if (!ctype_digit($char) && $char != ".") return false; + } + + if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { + $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); + return true; + } + return false; + } + + // a # color + protected function color(&$out) { + if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { + if (strlen($m[1]) > 7) { + $out = array("string", "", array($m[1])); + } else { + $out = array("raw_color", $m[1]); + } + return true; + } + + return false; + } + + // consume an argument definition list surrounded by () + // each argument is a variable name with optional value + // or at the end a ... or a variable named followed by ... + // arguments are separated by , unless a ; is in the list, then ; is the + // delimiter. + protected function argumentDef(&$args, &$isVararg) { + $s = $this->seek(); + if (!$this->literal('(')) return false; + + $values = array(); + $delim = ","; + $method = "expressionList"; + + $isVararg = false; + while (true) { + if ($this->literal("...")) { + $isVararg = true; + break; + } + + if ($this->$method($value)) { + if ($value[0] == "variable") { + $arg = array("arg", $value[1]); + $ss = $this->seek(); + + if ($this->assign() && $this->$method($rhs)) { + $arg[] = $rhs; + } else { + $this->seek($ss); + if ($this->literal("...")) { + $arg[0] = "rest"; + $isVararg = true; + } + } + + $values[] = $arg; + if ($isVararg) break; + continue; + } else { + $values[] = array("lit", $value); + } + } + + + if (!$this->literal($delim)) { + if ($delim == "," && $this->literal(";")) { + // found new delim, convert existing args + $delim = ";"; + $method = "propertyValue"; + + // transform arg list + if (isset($values[1])) { // 2 items + $newList = array(); + foreach ($values as $i => $arg) { + switch($arg[0]) { + case "arg": + if ($i) { + $this->throwError("Cannot mix ; and , as delimiter types"); + } + $newList[] = $arg[2]; + break; + case "lit": + $newList[] = $arg[1]; + break; + case "rest": + $this->throwError("Unexpected rest before semicolon"); + } + } + + $newList = array("list", ", ", $newList); + + switch ($values[0][0]) { + case "arg": + $newArg = array("arg", $values[0][1], $newList); + break; + case "lit": + $newArg = array("lit", $newList); + break; + } + + } elseif ($values) { // 1 item + $newArg = $values[0]; + } + + if ($newArg) { + $values = array($newArg); + } + } else { + break; + } + } + } + + if (!$this->literal(')')) { + $this->seek($s); + return false; + } + + $args = $values; + + return true; + } + + // consume a list of tags + // this accepts a hanging delimiter + protected function tags(&$tags, $simple = false, $delim = ',') { + $tags = array(); + while ($this->tag($tt, $simple)) { + $tags[] = $tt; + if (!$this->literal($delim)) break; + } + if (count($tags) == 0) return false; + + return true; + } + + // list of tags of specifying mixin path + // optionally separated by > (lazy, accepts extra >) + protected function mixinTags(&$tags) { + $tags = array(); + while ($this->tag($tt, true)) { + $tags[] = $tt; + $this->literal(">"); + } + + if (count($tags) == 0) return false; + + return true; + } + + // a bracketed value (contained within in a tag definition) + protected function tagBracket(&$parts, &$hasExpression) { + // speed shortcut + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { + return false; + } + + $s = $this->seek(); + + $hasInterpolation = false; + + if ($this->literal("[", false)) { + $attrParts = array("["); + // keyword, string, operator + while (true) { + if ($this->literal("]", false)) { + $this->count--; + break; // get out early + } + + if ($this->match('\s+', $m)) { + $attrParts[] = " "; + continue; + } + if ($this->string($str)) { + // escape parent selector, (yuck) + foreach ($str[2] as &$chunk) { + $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); + } + + $attrParts[] = $str; + $hasInterpolation = true; + continue; + } + + if ($this->keyword($word)) { + $attrParts[] = $word; + continue; + } + + if ($this->interpolation($inter, false)) { + $attrParts[] = $inter; + $hasInterpolation = true; + continue; + } + + // operator, handles attr namespace too + if ($this->match('[|-~\$\*\^=]+', $m)) { + $attrParts[] = $m[0]; + continue; + } + + break; + } + + if ($this->literal("]", false)) { + $attrParts[] = "]"; + foreach ($attrParts as $part) { + $parts[] = $part; + } + $hasExpression = $hasExpression || $hasInterpolation; + return true; + } + $this->seek($s); + } + + $this->seek($s); + return false; + } + + // a space separated list of selectors + protected function tag(&$tag, $simple = false) { + if ($simple) + $chars = '^@,:;{}\][>\(\) "\''; + else + $chars = '^@,;{}["\''; + + $s = $this->seek(); + + $hasExpression = false; + $parts = array(); + while ($this->tagBracket($parts, $hasExpression)); + + $oldWhite = $this->eatWhiteDefault; + $this->eatWhiteDefault = false; + + while (true) { + if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { + $parts[] = $m[1]; + if ($simple) break; + + while ($this->tagBracket($parts, $hasExpression)); + continue; + } + + if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { + if ($this->interpolation($interp)) { + $hasExpression = true; + $interp[2] = true; // don't unescape + $parts[] = $interp; + continue; + } + + if ($this->literal("@")) { + $parts[] = "@"; + continue; + } + } + + if ($this->unit($unit)) { // for keyframes + $parts[] = $unit[1]; + $parts[] = $unit[2]; + continue; + } + + break; + } + + $this->eatWhiteDefault = $oldWhite; + if (!$parts) { + $this->seek($s); + return false; + } + + if ($hasExpression) { + $tag = array("exp", array("string", "", $parts)); + } else { + $tag = trim(implode($parts)); + } + + $this->whitespace(); + return true; + } + + // a css function + protected function func(&$func) { + $s = $this->seek(); + + if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { + $fname = $m[1]; + + $sPreArgs = $this->seek(); + + $args = array(); + while (true) { + $ss = $this->seek(); + // this ugly nonsense is for ie filter properties + if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { + $args[] = array("string", "", array($name, "=", $value)); + } else { + $this->seek($ss); + if ($this->expressionList($value)) { + $args[] = $value; + } + } + + if (!$this->literal(',')) break; + } + $args = array('list', ',', $args); + + if ($this->literal(')')) { + $func = array('function', $fname, $args); + return true; + } elseif ($fname == 'url') { + // couldn't parse and in url? treat as string + $this->seek($sPreArgs); + if ($this->openString(")", $string) && $this->literal(")")) { + $func = array('function', $fname, $string); + return true; + } + } + } + + $this->seek($s); + return false; + } + + // consume a less variable + protected function variable(&$name) { + $s = $this->seek(); + if ($this->literal($this->lessc->vPrefix, false) && + ($this->variable($sub) || $this->keyword($name))) + { + if (!empty($sub)) { + $name = array('variable', $sub); + } else { + $name = $this->lessc->vPrefix.$name; + } + return true; + } + + $name = null; + $this->seek($s); + return false; + } + + /** + * Consume an assignment operator + * Can optionally take a name that will be set to the current property name + */ + protected function assign($name = null) { + if ($name) $this->currentProperty = $name; + return $this->literal(':') || $this->literal('='); + } + + // consume a keyword + protected function keyword(&$word) { + if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { + $word = $m[1]; + return true; + } + return false; + } + + // consume an end of statement delimiter + protected function end() { + if ($this->literal(';', false)) { + return true; + } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { + // if there is end of file or a closing block next then we don't need a ; + return true; + } + return false; + } + + protected function guards(&$guards) { + $s = $this->seek(); + + if (!$this->literal("when")) { + $this->seek($s); + return false; + } + + $guards = array(); + + while ($this->guardGroup($g)) { + $guards[] = $g; + if (!$this->literal(",")) break; + } + + if (count($guards) == 0) { + $guards = null; + $this->seek($s); + return false; + } + + return true; + } + + // a bunch of guards that are and'd together + // TODO rename to guardGroup + protected function guardGroup(&$guardGroup) { + $s = $this->seek(); + $guardGroup = array(); + while ($this->guard($guard)) { + $guardGroup[] = $guard; + if (!$this->literal("and")) break; + } + + if (count($guardGroup) == 0) { + $guardGroup = null; + $this->seek($s); + return false; + } + + return true; + } + + protected function guard(&$guard) { + $s = $this->seek(); + $negate = $this->literal("not"); + + if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { + $guard = $exp; + if ($negate) $guard = array("negate", $guard); + return true; + } + + $this->seek($s); + return false; + } + + /* raw parsing functions */ + + protected function literal($what, $eatWhitespace = null) { + if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; + + // shortcut on single letter + if (!isset($what[1]) && isset($this->buffer[$this->count])) { + if ($this->buffer[$this->count] == $what) { + if (!$eatWhitespace) { + $this->count++; + return true; + } + // goes below... + } else { + return false; + } + } + + if (!isset(self::$literalCache[$what])) { + self::$literalCache[$what] = lessc::preg_quote($what); + } + + return $this->match(self::$literalCache[$what], $m, $eatWhitespace); + } + + protected function genericList(&$out, $parseItem, $delim="", $flatten=true) { + $s = $this->seek(); + $items = array(); + while ($this->$parseItem($value)) { + $items[] = $value; + if ($delim) { + if (!$this->literal($delim)) break; + } + } + + if (count($items) == 0) { + $this->seek($s); + return false; + } + + if ($flatten && count($items) == 1) { + $out = $items[0]; + } else { + $out = array("list", $delim, $items); + } + + return true; + } + + + // advance counter to next occurrence of $what + // $until - don't include $what in advance + // $allowNewline, if string, will be used as valid char set + protected function to($what, &$out, $until = false, $allowNewline = false) { + if (is_string($allowNewline)) { + $validChars = $allowNewline; + } else { + $validChars = $allowNewline ? "." : "[^\n]"; + } + if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; + if ($until) $this->count -= strlen($what); // give back $what + $out = $m[1]; + return true; + } + + // try to match something on head of buffer + protected function match($regex, &$out, $eatWhitespace = null) { + if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; + + $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; + if (preg_match($r, $this->buffer, $out, null, $this->count)) { + $this->count += strlen($out[0]); + if ($eatWhitespace && $this->writeComments) $this->whitespace(); + return true; + } + return false; + } + + // match some whitespace + protected function whitespace() { + if ($this->writeComments) { + $gotWhite = false; + while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { + if (isset($m[1]) && empty($this->seenComments[$this->count])) { + $this->append(array("comment", $m[1])); + $this->seenComments[$this->count] = true; + } + $this->count += strlen($m[0]); + $gotWhite = true; + } + return $gotWhite; + } else { + $this->match("", $m); + return strlen($m[0]) > 0; + } + } + + // match something without consuming it + protected function peek($regex, &$out = null, $from=null) { + if (is_null($from)) $from = $this->count; + $r = '/'.$regex.'/Ais'; + $result = preg_match($r, $this->buffer, $out, null, $from); + + return $result; + } + + // seek to a spot in the buffer or return where we are on no argument + protected function seek($where = null) { + if ($where === null) return $this->count; + else $this->count = $where; + return true; + } + + /* misc functions */ + + public function throwError($msg = "parse error", $count = null) { + $count = is_null($count) ? $this->count : $count; + + $line = $this->line + + substr_count(substr($this->buffer, 0, $count), "\n"); + + if (!empty($this->sourceName)) { + $loc = "$this->sourceName on line $line"; + } else { + $loc = "line: $line"; + } + + // TODO this depends on $this->count + if ($this->peek("(.*?)(\n|$)", $m, $count)) { + throw new exception("$msg: failed at `$m[1]` $loc"); + } else { + throw new exception("$msg: $loc"); + } + } + + protected function pushBlock($selectors=null, $type=null) { + $b = new stdclass; + $b->parent = $this->env; + + $b->type = $type; + $b->id = self::$nextBlockId++; + + $b->isVararg = false; // TODO: kill me from here + $b->tags = $selectors; + + $b->props = array(); + $b->children = array(); + + $this->env = $b; + return $b; + } + + // push a block that doesn't multiply tags + protected function pushSpecialBlock($type) { + return $this->pushBlock(null, $type); + } + + // append a property to the current block + protected function append($prop, $pos = null) { + if ($pos !== null) $prop[-1] = $pos; + $this->env->props[] = $prop; + } + + // pop something off the stack + protected function pop() { + $old = $this->env; + $this->env = $this->env->parent; + return $old; + } + + // remove comments from $text + // todo: make it work for all functions, not just url + protected function removeComments($text) { + $look = array( + 'url(', '//', '/*', '"', "'" + ); + + $out = ''; + $min = null; + while (true) { + // find the next item + foreach ($look as $token) { + $pos = strpos($text, $token); + if ($pos !== false) { + if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); + } + } + + if (is_null($min)) break; + + $count = $min[1]; + $skip = 0; + $newlines = 0; + switch ($min[0]) { + case 'url(': + if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) + $count += strlen($m[0]) - strlen($min[0]); + break; + case '"': + case "'": + if (preg_match('/'.$min[0].'.*?(?indentLevel = 0; - } - - public function indentStr($n = 0) { - return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); - } - - public function property($name, $value) { - return $name . $this->assignSeparator . $value . ";"; - } - - protected function isEmpty($block) { - if (empty($block->lines)) { - foreach ($block->children as $child) { - if (!$this->isEmpty($child)) return false; - } - - return true; - } - return false; - } - - public function block($block) { - if ($this->isEmpty($block)) return; - - $inner = $pre = $this->indentStr(); - - $isSingle = !$this->disableSingle && - is_null($block->type) && count($block->lines) == 1; - - if (!empty($block->selectors)) { - $this->indentLevel++; - - if ($this->breakSelectors) { - $selectorSeparator = $this->selectorSeparator . $this->break . $pre; - } else { - $selectorSeparator = $this->selectorSeparator; - } - - echo $pre . - implode($selectorSeparator, $block->selectors); - if ($isSingle) { - echo $this->openSingle; - $inner = ""; - } else { - echo $this->open . $this->break; - $inner = $this->indentStr(); - } - - } - - if (!empty($block->lines)) { - $glue = $this->break.$inner; - echo $inner . implode($glue, $block->lines); - if (!$isSingle && !empty($block->children)) { - echo $this->break; - } - } - - foreach ($block->children as $child) { - $this->block($child); - } - - if (!empty($block->selectors)) { - if (!$isSingle && empty($block->children)) echo $this->break; - - if ($isSingle) { - echo $this->closeSingle . $this->break; - } else { - echo $pre . $this->close . $this->break; - } - - $this->indentLevel--; - } - } + public $indentChar = " "; + + public $break = "\n"; + public $open = " {"; + public $close = "}"; + public $selectorSeparator = ", "; + public $assignSeparator = ":"; + + public $openSingle = " { "; + public $closeSingle = " }"; + + public $disableSingle = false; + public $breakSelectors = false; + + public $compressColors = false; + + public function __construct() { + $this->indentLevel = 0; + } + + public function indentStr($n = 0) { + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + public function property($name, $value) { + return $name . $this->assignSeparator . $value . ";"; + } + + protected function isEmpty($block) { + if (empty($block->lines)) { + foreach ($block->children as $child) { + if (!$this->isEmpty($child)) return false; + } + + return true; + } + return false; + } + + public function block($block) { + if ($this->isEmpty($block)) return; + + $inner = $pre = $this->indentStr(); + + $isSingle = !$this->disableSingle && + is_null($block->type) && count($block->lines) == 1; + + if (!empty($block->selectors)) { + $this->indentLevel++; + + if ($this->breakSelectors) { + $selectorSeparator = $this->selectorSeparator . $this->break . $pre; + } else { + $selectorSeparator = $this->selectorSeparator; + } + + echo $pre . + implode($selectorSeparator, $block->selectors); + if ($isSingle) { + echo $this->openSingle; + $inner = ""; + } else { + echo $this->open . $this->break; + $inner = $this->indentStr(); + } + + } + + if (!empty($block->lines)) { + $glue = $this->break.$inner; + echo $inner . implode($glue, $block->lines); + if (!$isSingle && !empty($block->children)) { + echo $this->break; + } + } + + foreach ($block->children as $child) { + $this->block($child); + } + + if (!empty($block->selectors)) { + if (!$isSingle && empty($block->children)) echo $this->break; + + if ($isSingle) { + echo $this->closeSingle . $this->break; + } else { + echo $pre . $this->close . $this->break; + } + + $this->indentLevel--; + } + } } class lessc_formatter_compressed extends lessc_formatter_classic { - public $disableSingle = true; - public $open = "{"; - public $selectorSeparator = ","; - public $assignSeparator = ":"; - public $break = ""; - public $compressColors = true; - - public function indentStr($n = 0) { - return ""; - } + public $disableSingle = true; + public $open = "{"; + public $selectorSeparator = ","; + public $assignSeparator = ":"; + public $break = ""; + public $compressColors = true; + + public function indentStr($n = 0) { + return ""; + } } class lessc_formatter_lessjs extends lessc_formatter_classic { - public $disableSingle = true; - public $breakSelectors = true; - public $assignSeparator = ": "; - public $selectorSeparator = ","; + public $disableSingle = true; + public $breakSelectors = true; + public $assignSeparator = ": "; + public $selectorSeparator = ","; } diff --git a/lessify b/lessify index b447e624..b0f4e759 100755 --- a/lessify +++ b/lessify @@ -2,20 +2,20 @@ parse(); +try { + $parser = new lessify($fname); + echo $parser->parse(); } catch (exception $e) { - exit("Fatal error: ".$e->getMessage()."\n"); + exit("Fatal error: ".$e->getMessage()."\n"); } diff --git a/lessify.inc.php b/lessify.inc.php index 91c14423..9b7baa12 100644 --- a/lessify.inc.php +++ b/lessify.inc.php @@ -21,427 +21,427 @@ // class easyparse { - var $buffer; - var $count; - - function __construct($str) { - $this->count = 0; - $this->buffer = trim($str); - } - - function seek($where = null) { - if ($where === null) return $this->count; - else $this->count = $where; - return true; - } - - function preg_quote($what) { - return preg_quote($what, '/'); - } - - function match($regex, &$out, $eatWhitespace = true) { - $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; - if (preg_match($r, $this->buffer, $out, null, $this->count)) { - $this->count += strlen($out[0]); - return true; - } - return false; - } - - function literal($what, $eatWhitespace = true) { - // this is here mainly prevent notice from { } string accessor - if ($this->count >= strlen($this->buffer)) return false; - - // shortcut on single letter - if (!$eatWhitespace and strlen($what) == 1) { - if ($this->buffer{$this->count} == $what) { - $this->count++; - return true; - } - else return false; - } - - return $this->match($this->preg_quote($what), $m, $eatWhitespace); - } + var $buffer; + var $count; + + function __construct($str) { + $this->count = 0; + $this->buffer = trim($str); + } + + function seek($where = null) { + if ($where === null) return $this->count; + else $this->count = $where; + return true; + } + + function preg_quote($what) { + return preg_quote($what, '/'); + } + + function match($regex, &$out, $eatWhitespace = true) { + $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; + if (preg_match($r, $this->buffer, $out, null, $this->count)) { + $this->count += strlen($out[0]); + return true; + } + return false; + } + + function literal($what, $eatWhitespace = true) { + // this is here mainly prevent notice from { } string accessor + if ($this->count >= strlen($this->buffer)) return false; + + // shortcut on single letter + if (!$eatWhitespace and strlen($what) == 1) { + if ($this->buffer{$this->count} == $what) { + $this->count++; + return true; + } + else return false; + } + + return $this->match($this->preg_quote($what), $m, $eatWhitespace); + } } class tagparse extends easyparse { - static private $combinators = null; - static private $match_opts = null; - - function parse() { - if (empty(self::$combinators)) { - self::$combinators = '('.implode('|', array_map(array($this, 'preg_quote'), - array('+', '>', '~'))).')'; - self::$match_opts = '('.implode('|', array_map(array($this, 'preg_quote'), - array('=', '~=', '|=', '$=', '*='))).')'; - } - - // crush whitespace - $this->buffer = preg_replace('/\s+/', ' ', $this->buffer).' '; - - $tags = array(); - while ($this->tag($t)) $tags[] = $t; - - return $tags; - } - - static function compileString($string) { - list(, $delim, $str) = $string; - $str = str_replace($delim, "\\".$delim, $str); - $str = str_replace("\n", "\\\n", $str); - return $delim.$str.$delim; - } - - static function compilePaths($paths) { - return implode(', ', array_map(array('self', 'compilePath'), $paths)); - } - - // array of tags - static function compilePath($path) { - return implode(' ', array_map(array('self', 'compileTag'), $path)); - } - - - static function compileTag($tag) { - ob_start(); - if (isset($tag['comb'])) echo $tag['comb']." "; - if (isset($tag['front'])) echo $tag['front']; - if (isset($tag['attr'])) { - echo '['.$tag['attr']; - if (isset($tag['op'])) { - echo $tag['op'].$tag['op_value']; - } - echo ']'; - } - return ob_get_clean(); - } - - function string(&$out) { - $s = $this->seek(); - - if ($this->literal('"')) { - $delim = '"'; - } elseif ($this->literal("'")) { - $delim = "'"; - } else { - return false; - } - - while (true) { - // step through letters looking for either end or escape - $buff = ""; - $escapeNext = false; - $finished = false; - for ($i = $this->count; $i < strlen($this->buffer); $i++) { - $char = $this->buffer[$i]; - switch ($char) { - case $delim: - if ($escapeNext) { - $buff .= $char; - $escapeNext = false; - break; - } - $finished = true; - break 2; - case "\\": - if ($escapeNext) { - $buff .= $char; - $escapeNext = false; - } else { - $escapeNext = true; - } - break; - case "\n": - if (!$escapeNext) { - break 3; - } - - $buff .= $char; - $escapeNext = false; - break; - default: - if ($escapeNext) { - $buff .= "\\"; - $escapeNext = false; - } - $buff .= $char; - } - } - if (!$finished) break; - $out = array('string', $delim, $buff); - $this->seek($i+1); - return true; - } - - $this->seek($s); - return false; - } - - function tag(&$out) { - $s = $this->seek(); - $tag = array(); - if ($this->combinator($op)) $tag['comb'] = $op; - - if (!$this->match('(.*?)( |$|\[|'.self::$combinators.')', $match)) { - $this->seek($s); - return false; - } - - if (!empty($match[3])) { - // give back combinator - $this->count-=strlen($match[3]); - } - - if (!empty($match[1])) $tag['front'] = $match[1]; - - if ($match[2] == '[') { - if ($this->ident($i)) { - $tag['attr'] = $i; - - if ($this->match(self::$match_opts, $m) && $this->value($v)) { - $tag['op'] = $m[1]; - $tag['op_value'] = $v; - } - - if ($this->literal(']')) { - $out = $tag; - return true; - } - } - } elseif (isset($tag['front'])) { - $out = $tag; - return true; - } - - $this->seek($s); - return false; - } - - function ident(&$out) { - // [-]?{nmstart}{nmchar}* - // nmstart: [_a-z]|{nonascii}|{escape} - // nmchar: [_a-z0-9-]|{nonascii}|{escape} - if ($this->match('(-?[_a-z][_\w]*)', $m)) { - $out = $m[1]; - return true; - } - return false; - } - - function value(&$out) { - if ($this->string($str)) { - $out = $this->compileString($str); - return true; - } elseif ($this->ident($id)) { - $out = $id; - return true; - } - return false; - } - - - function combinator(&$op) { - if ($this->match(self::$combinators, $m)) { - $op = $m[1]; - return true; - } - return false; - } + static private $combinators = null; + static private $match_opts = null; + + function parse() { + if (empty(self::$combinators)) { + self::$combinators = '('.implode('|', array_map(array($this, 'preg_quote'), + array('+', '>', '~'))).')'; + self::$match_opts = '('.implode('|', array_map(array($this, 'preg_quote'), + array('=', '~=', '|=', '$=', '*='))).')'; + } + + // crush whitespace + $this->buffer = preg_replace('/\s+/', ' ', $this->buffer).' '; + + $tags = array(); + while ($this->tag($t)) $tags[] = $t; + + return $tags; + } + + static function compileString($string) { + list(, $delim, $str) = $string; + $str = str_replace($delim, "\\".$delim, $str); + $str = str_replace("\n", "\\\n", $str); + return $delim.$str.$delim; + } + + static function compilePaths($paths) { + return implode(', ', array_map(array('self', 'compilePath'), $paths)); + } + + // array of tags + static function compilePath($path) { + return implode(' ', array_map(array('self', 'compileTag'), $path)); + } + + + static function compileTag($tag) { + ob_start(); + if (isset($tag['comb'])) echo $tag['comb']." "; + if (isset($tag['front'])) echo $tag['front']; + if (isset($tag['attr'])) { + echo '['.$tag['attr']; + if (isset($tag['op'])) { + echo $tag['op'].$tag['op_value']; + } + echo ']'; + } + return ob_get_clean(); + } + + function string(&$out) { + $s = $this->seek(); + + if ($this->literal('"')) { + $delim = '"'; + } elseif ($this->literal("'")) { + $delim = "'"; + } else { + return false; + } + + while (true) { + // step through letters looking for either end or escape + $buff = ""; + $escapeNext = false; + $finished = false; + for ($i = $this->count; $i < strlen($this->buffer); $i++) { + $char = $this->buffer[$i]; + switch ($char) { + case $delim: + if ($escapeNext) { + $buff .= $char; + $escapeNext = false; + break; + } + $finished = true; + break 2; + case "\\": + if ($escapeNext) { + $buff .= $char; + $escapeNext = false; + } else { + $escapeNext = true; + } + break; + case "\n": + if (!$escapeNext) { + break 3; + } + + $buff .= $char; + $escapeNext = false; + break; + default: + if ($escapeNext) { + $buff .= "\\"; + $escapeNext = false; + } + $buff .= $char; + } + } + if (!$finished) break; + $out = array('string', $delim, $buff); + $this->seek($i+1); + return true; + } + + $this->seek($s); + return false; + } + + function tag(&$out) { + $s = $this->seek(); + $tag = array(); + if ($this->combinator($op)) $tag['comb'] = $op; + + if (!$this->match('(.*?)( |$|\[|'.self::$combinators.')', $match)) { + $this->seek($s); + return false; + } + + if (!empty($match[3])) { + // give back combinator + $this->count-=strlen($match[3]); + } + + if (!empty($match[1])) $tag['front'] = $match[1]; + + if ($match[2] == '[') { + if ($this->ident($i)) { + $tag['attr'] = $i; + + if ($this->match(self::$match_opts, $m) && $this->value($v)) { + $tag['op'] = $m[1]; + $tag['op_value'] = $v; + } + + if ($this->literal(']')) { + $out = $tag; + return true; + } + } + } elseif (isset($tag['front'])) { + $out = $tag; + return true; + } + + $this->seek($s); + return false; + } + + function ident(&$out) { + // [-]?{nmstart}{nmchar}* + // nmstart: [_a-z]|{nonascii}|{escape} + // nmchar: [_a-z0-9-]|{nonascii}|{escape} + if ($this->match('(-?[_a-z][_\w]*)', $m)) { + $out = $m[1]; + return true; + } + return false; + } + + function value(&$out) { + if ($this->string($str)) { + $out = $this->compileString($str); + return true; + } elseif ($this->ident($id)) { + $out = $id; + return true; + } + return false; + } + + + function combinator(&$op) { + if ($this->match(self::$combinators, $m)) { + $op = $m[1]; + return true; + } + return false; + } } class nodecounter { - var $count = 0; - var $children = array(); - - var $name; - var $child_blocks; - var $the_block; - - function __construct($name) { - $this->name = $name; - } - - function dump($stack = null) { - if (is_null($stack)) $stack = array(); - $stack[] = $this->getName(); - echo implode(' -> ', $stack)." ($this->count)\n"; - foreach ($this->children as $child) { - $child->dump($stack); - } - } - - static function compileProperties($c, $block) { - foreach($block as $name => $value) { - if ($c->isProperty($name, $value)) { - echo $c->compileProperty($name, $value)."\n"; - } - } - } - - function compile($c, $path = null) { - if (is_null($path)) $path = array(); - $path[] = $this->name; - - $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks); - - if ($isVisible) { - echo $c->indent(implode(' ', $path).' {'); - $c->indentLevel++; - $path = array(); - - if ($this->the_block) { - $this->compileProperties($c, $this->the_block); - } - - if ($this->child_blocks) { - foreach ($this->child_blocks as $block) { - echo $c->indent(tagparse::compilePaths($block['__tags']).' {'); - $c->indentLevel++; - $this->compileProperties($c, $block); - $c->indentLevel--; - echo $c->indent('}'); - } - } - } - - // compile child nodes - foreach($this->children as $node) { - $node->compile($c, $path); - } - - if ($isVisible) { - $c->indentLevel--; - echo $c->indent('}'); - } - - } - - function getName() { - if (is_null($this->name)) return "[root]"; - else return $this->name; - } - - function getNode($name) { - if (!isset($this->children[$name])) { - $this->children[$name] = new nodecounter($name); - } - - return $this->children[$name]; - } - - function findNode($path) { - $current = $this; - for ($i = 0; $i < count($path); $i++) { - $t = tagparse::compileTag($path[$i]); - $current = $current->getNode($t); - } - - return $current; - } - - function addBlock($path, $block) { - $node = $this->findNode($path); - if (!is_null($node->the_block)) throw new exception("can this happen?"); - - unset($block['__tags']); - $node->the_block = $block; - } - - function addToNode($path, $block) { - $node = $this->findNode($path); - $node->child_blocks[] = $block; - } + var $count = 0; + var $children = array(); + + var $name; + var $child_blocks; + var $the_block; + + function __construct($name) { + $this->name = $name; + } + + function dump($stack = null) { + if (is_null($stack)) $stack = array(); + $stack[] = $this->getName(); + echo implode(' -> ', $stack)." ($this->count)\n"; + foreach ($this->children as $child) { + $child->dump($stack); + } + } + + static function compileProperties($c, $block) { + foreach($block as $name => $value) { + if ($c->isProperty($name, $value)) { + echo $c->compileProperty($name, $value)."\n"; + } + } + } + + function compile($c, $path = null) { + if (is_null($path)) $path = array(); + $path[] = $this->name; + + $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks); + + if ($isVisible) { + echo $c->indent(implode(' ', $path).' {'); + $c->indentLevel++; + $path = array(); + + if ($this->the_block) { + $this->compileProperties($c, $this->the_block); + } + + if ($this->child_blocks) { + foreach ($this->child_blocks as $block) { + echo $c->indent(tagparse::compilePaths($block['__tags']).' {'); + $c->indentLevel++; + $this->compileProperties($c, $block); + $c->indentLevel--; + echo $c->indent('}'); + } + } + } + + // compile child nodes + foreach($this->children as $node) { + $node->compile($c, $path); + } + + if ($isVisible) { + $c->indentLevel--; + echo $c->indent('}'); + } + + } + + function getName() { + if (is_null($this->name)) return "[root]"; + else return $this->name; + } + + function getNode($name) { + if (!isset($this->children[$name])) { + $this->children[$name] = new nodecounter($name); + } + + return $this->children[$name]; + } + + function findNode($path) { + $current = $this; + for ($i = 0; $i < count($path); $i++) { + $t = tagparse::compileTag($path[$i]); + $current = $current->getNode($t); + } + + return $current; + } + + function addBlock($path, $block) { + $node = $this->findNode($path); + if (!is_null($node->the_block)) throw new exception("can this happen?"); + + unset($block['__tags']); + $node->the_block = $block; + } + + function addToNode($path, $block) { + $node = $this->findNode($path); + $node->child_blocks[] = $block; + } } /** * create a less file from a css file by combining blocks where appropriate */ class lessify extends lessc { - public function dump() { - print_r($this->env); - } - - public function parse($str = null) { - $this->prepareParser($str ? $str : $this->buffer); - while (false !== $this->parseChunk()); - - $root = new nodecounter(null); - - // attempt to preserve some of the block order - $order = array(); - - $visitedTags = array(); - foreach (end($this->env) as $name => $block) { - if (!$this->isBlock($name, $block)) continue; - if (isset($visitedTags[$name])) continue; - - foreach ($block['__tags'] as $t) { - $visitedTags[$t] = true; - } - - // skip those with more than 1 - if (count($block['__tags']) == 1) { - $p = new tagparse(end($block['__tags'])); - $path = $p->parse(); - $root->addBlock($path, $block); - $order[] = array('compressed', $path, $block); - continue; - } else { - $common = null; - $paths = array(); - foreach ($block['__tags'] as $rawtag) { - $p = new tagparse($rawtag); - $paths[] = $path = $p->parse(); - if (is_null($common)) $common = $path; - else { - $new_common = array(); - foreach ($path as $tag) { - $head = array_shift($common); - if ($tag == $head) { - $new_common[] = $head; - } else break; - } - $common = $new_common; - if (empty($common)) { - // nothing in common - break; - } - } - } - - if (!empty($common)) { - $new_paths = array(); - foreach ($paths as $p) $new_paths[] = array_slice($p, count($common)); - $block['__tags'] = $new_paths; - $root->addToNode($common, $block); - $order[] = array('compressed', $common, $block); - continue; - } - - } - - $order[] = array('none', $block['__tags'], $block); - } - - - $compressed = $root->children; - foreach ($order as $item) { - list($type, $tags, $block) = $item; - if ($type == 'compressed') { - $top = tagparse::compileTag(reset($tags)); - if (isset($compressed[$top])) { - $compressed[$top]->compile($this); - unset($compressed[$top]); - } - } else { - echo $this->indent(implode(', ', $tags).' {'); - $this->indentLevel++; - nodecounter::compileProperties($this, $block); - $this->indentLevel--; - echo $this->indent('}'); - } - } - } + public function dump() { + print_r($this->env); + } + + public function parse($str = null) { + $this->prepareParser($str ? $str : $this->buffer); + while (false !== $this->parseChunk()); + + $root = new nodecounter(null); + + // attempt to preserve some of the block order + $order = array(); + + $visitedTags = array(); + foreach (end($this->env) as $name => $block) { + if (!$this->isBlock($name, $block)) continue; + if (isset($visitedTags[$name])) continue; + + foreach ($block['__tags'] as $t) { + $visitedTags[$t] = true; + } + + // skip those with more than 1 + if (count($block['__tags']) == 1) { + $p = new tagparse(end($block['__tags'])); + $path = $p->parse(); + $root->addBlock($path, $block); + $order[] = array('compressed', $path, $block); + continue; + } else { + $common = null; + $paths = array(); + foreach ($block['__tags'] as $rawtag) { + $p = new tagparse($rawtag); + $paths[] = $path = $p->parse(); + if (is_null($common)) $common = $path; + else { + $new_common = array(); + foreach ($path as $tag) { + $head = array_shift($common); + if ($tag == $head) { + $new_common[] = $head; + } else break; + } + $common = $new_common; + if (empty($common)) { + // nothing in common + break; + } + } + } + + if (!empty($common)) { + $new_paths = array(); + foreach ($paths as $p) $new_paths[] = array_slice($p, count($common)); + $block['__tags'] = $new_paths; + $root->addToNode($common, $block); + $order[] = array('compressed', $common, $block); + continue; + } + + } + + $order[] = array('none', $block['__tags'], $block); + } + + + $compressed = $root->children; + foreach ($order as $item) { + list($type, $tags, $block) = $item; + if ($type == 'compressed') { + $top = tagparse::compileTag(reset($tags)); + if (isset($compressed[$top])) { + $compressed[$top]->compile($this); + unset($compressed[$top]); + } + } else { + echo $this->indent(implode(', ', $tags).' {'); + $this->indentLevel++; + nodecounter::compileProperties($this, $block); + $this->indentLevel--; + echo $this->indent('}'); + } + } + } } diff --git a/tests/ApiTest.php b/tests/ApiTest.php index f8c543b9..f844df49 100644 --- a/tests/ApiTest.php +++ b/tests/ApiTest.php @@ -3,13 +3,13 @@ require_once __DIR__ . "/../lessc.inc.php"; class ApiTest extends PHPUnit\Framework\TestCase { - public function setUp() { - $this->less = new lessc(); - $this->less->importDir = array(__DIR__ . "/inputs/test-imports"); - } + public function setUp() { + $this->less = new lessc(); + $this->less->importDir = array(__DIR__ . "/inputs/test-imports"); + } - public function testPreserveComments() { - $input = <<assertEquals($this->compile($input), trim($outputWithoutComments)); - $this->less->setPreserveComments(true); - $this->assertEquals($this->compile($input), trim($outputWithComments)); - } + $this->assertEquals($this->compile($input), trim($outputWithoutComments)); + $this->less->setPreserveComments(true); + $this->assertEquals($this->compile($input), trim($outputWithComments)); + } - public function testOldInterface() { - $this->less = new lessc(__DIR__ . "/inputs/hi.less"); - $out = $this->less->parse(array("hello" => "10px")); - $this->assertEquals(trim($out), trim(' + public function testOldInterface() { + $this->less = new lessc(__DIR__ . "/inputs/hi.less"); + $out = $this->less->parse(array("hello" => "10px")); + $this->assertEquals(trim($out), trim(' div:before { content: "hi!"; }')); - } + } - public function testInjectVars() { - $out = $this->less->parse(".magic { color: @color; width: @base - 200; }", - array( - 'color' => 'red', - 'base' => '960px' - )); + public function testInjectVars() { + $out = $this->less->parse(".magic { color: @color; width: @base - 200; }", + array( + 'color' => 'red', + 'base' => '960px' + )); - $this->assertEquals(trim($out), trim(" + $this->assertEquals(trim($out), trim(" .magic { color: red; width: 760px; }")); - } + } - public function testDisableImport() { - $this->less->importDisabled = true; - $this->assertEquals( - "/* import disabled */", - $this->compile("@import 'file3';")); - } + public function testDisableImport() { + $this->less->importDisabled = true; + $this->assertEquals( + "/* import disabled */", + $this->compile("@import 'file3';")); + } - public function testUserFunction() { - $this->less->registerFunction("add-two", function($list) { - list($a, $b) = $list[2]; - return $a[1] + $b[1]; - }); + public function testUserFunction() { + $this->less->registerFunction("add-two", function($list) { + list($a, $b) = $list[2]; + return $a[1] + $b[1]; + }); - $this->assertEquals( - $this->compile("result: add-two(10, 20);"), - "result: 30;"); + $this->assertEquals( + $this->compile("result: add-two(10, 20);"), + "result: 30;"); - return $this->less; - } + return $this->less; + } - /** - * @depends testUserFunction - */ - public function testUnregisterFunction($less) { - $less->unregisterFunction("add-two"); + /** + * @depends testUserFunction + */ + public function testUnregisterFunction($less) { + $less->unregisterFunction("add-two"); - $this->assertEquals( - $this->compile("result: add-two(10, 20);"), - "result: add-two(10,20);"); - } + $this->assertEquals( + $this->compile("result: add-two(10, 20);"), + "result: add-two(10,20);"); + } - public function testFormatters() { - $src = " + public function testFormatters() { + $src = " div, pre { color: blue; span, .big, hello.world { @@ -155,14 +155,14 @@ public function testFormatters() { } }"; - $this->less->setFormatter("compressed"); - $this->assertEquals( - $this->compile($src), "div,pre{color:blue;}div span,div .big,div hello.world,pre span,pre .big,pre hello.world{height:20px;color:#fff;}"); + $this->less->setFormatter("compressed"); + $this->assertEquals( + $this->compile($src), "div,pre{color:blue;}div span,div .big,div hello.world,pre span,pre .big,pre hello.world{height:20px;color:#fff;}"); - // TODO: fix the output order of tags - $this->less->setFormatter("lessjs"); - $this->assertEquals( - $this->compile($src), + // TODO: fix the output order of tags + $this->less->setFormatter("lessjs"); + $this->assertEquals( + $this->compile($src), "div, pre { color: blue; @@ -177,9 +177,9 @@ public function testFormatters() { color: #ffffff; }"); - $this->less->setFormatter("classic"); - $this->assertEquals( - $this->compile($src), + $this->less->setFormatter("classic"); + $this->assertEquals( + $this->compile($src), trim("div, pre { color:blue; } div span, div .big, div hello.world, pre span, pre .big, pre hello.world { height:20px; @@ -187,10 +187,10 @@ public function testFormatters() { } ")); - } + } - public function compile($str) { - return trim($this->less->parse($str)); - } + public function compile($str) { + return trim($this->less->parse($str)); + } } diff --git a/tests/ErrorHandlingTest.php b/tests/ErrorHandlingTest.php index 3a81d22e..af6d2b61 100644 --- a/tests/ErrorHandlingTest.php +++ b/tests/ErrorHandlingTest.php @@ -2,80 +2,80 @@ require_once __DIR__ . "/../lessc.inc.php"; class ErrorHandlingTest extends PHPUnit\Framework\TestCase { - public function setUp() { - $this->less = new lessc(); - } + public function setUp() { + $this->less = new lessc(); + } - public function compile() { - $source = join("\n", func_get_args()); - return $this->less->compile($source); - } + public function compile() { + $source = join("\n", func_get_args()); + return $this->less->compile($source); + } - /** - * @expectedException Exception - * @expectedExceptionMessage .parametric-mixin is undefined - */ - public function testRequiredParametersMissing() { - $this->compile( - '.parametric-mixin (@a, @b) { a: @a; b: @b; }', - '.selector { .parametric-mixin(12px); }' - ); - } + /** + * @expectedException Exception + * @expectedExceptionMessage .parametric-mixin is undefined + */ + public function testRequiredParametersMissing() { + $this->compile( + '.parametric-mixin (@a, @b) { a: @a; b: @b; }', + '.selector { .parametric-mixin(12px); }' + ); + } - /** - * @expectedException Exception - * @expectedExceptionMessage .parametric-mixin is undefined - */ - public function testTooManyParameters() { - $this->compile( - '.parametric-mixin (@a, @b) { a: @a; b: @b; }', - '.selector { .parametric-mixin(12px, 13px, 14px); }' - ); - } + /** + * @expectedException Exception + * @expectedExceptionMessage .parametric-mixin is undefined + */ + public function testTooManyParameters() { + $this->compile( + '.parametric-mixin (@a, @b) { a: @a; b: @b; }', + '.selector { .parametric-mixin(12px, 13px, 14px); }' + ); + } - /** - * @expectedException Exception - * @expectedExceptionMessage unrecognised input - */ - public function testRequiredArgumentsMissing() { - $this->compile('.selector { rule: e(); }'); - } + /** + * @expectedException Exception + * @expectedExceptionMessage unrecognised input + */ + public function testRequiredArgumentsMissing() { + $this->compile('.selector { rule: e(); }'); + } - /** - * @expectedException Exception - * @expectedExceptionMessage variable @missing is undefined - */ - public function testVariableMissing() { - $this->compile('.selector { rule: @missing; }'); - } + /** + * @expectedException Exception + * @expectedExceptionMessage variable @missing is undefined + */ + public function testVariableMissing() { + $this->compile('.selector { rule: @missing; }'); + } - /** - * @expectedException Exception - * @expectedExceptionMessage .missing-mixin is undefined - */ - public function testMixinMissing() { - $this->compile('.selector { .missing-mixin; }'); - } + /** + * @expectedException Exception + * @expectedExceptionMessage .missing-mixin is undefined + */ + public function testMixinMissing() { + $this->compile('.selector { .missing-mixin; }'); + } - /** - * @expectedException Exception - * @expectedExceptionMessage .flipped is undefined - */ - public function testGuardUnmatchedValue() { - $this->compile( - '.flipped(@x) when (@x =< 10) { rule: value; }', - '.selector { .flipped(12); }' - ); - } + /** + * @expectedException Exception + * @expectedExceptionMessage .flipped is undefined + */ + public function testGuardUnmatchedValue() { + $this->compile( + '.flipped(@x) when (@x =< 10) { rule: value; }', + '.selector { .flipped(12); }' + ); + } - /** - * @expectedException Exception - * @expectedExceptionMessage .colors-only is undefined - */ - public function testGuardUnmatchedType() { - $this->compile( - '.colors-only(@x) when (iscolor(@x)) { rule: value; }', - '.selector { .colors-only("string value"); }' - ); - } + /** + * @expectedException Exception + * @expectedExceptionMessage .colors-only is undefined + */ + public function testGuardUnmatchedType() { + $this->compile( + '.colors-only(@x) when (iscolor(@x)) { rule: value; }', + '.selector { .colors-only("string value"); }' + ); + } } diff --git a/tests/InputTest.php b/tests/InputTest.php index c0829a11..ae8c494a 100644 --- a/tests/InputTest.php +++ b/tests/InputTest.php @@ -5,85 +5,85 @@ // Runs all the tests in inputs/ and compares their output to ouputs/ function _dump($value) { - fwrite(STDOUT, print_r($value, true)); + fwrite(STDOUT, print_r($value, true)); } function _quote($str) { - return preg_quote($str, "/"); + return preg_quote($str, "/"); } class InputTest extends PHPUnit\Framework\TestCase { - protected static $importDirs = array("inputs/test-imports"); - - protected static $testDirs = array( - "inputs" => "outputs", - "inputs_lessjs" => "outputs_lessjs", - ); - - public function setUp() { - $this->less = new lessc(); - $this->less->importDir = array_map(function($path) { - return __DIR__ . "/" . $path; - }, self::$importDirs); - } - - /** - * @dataProvider fileNameProvider - */ - public function testInputFile($inFname) { - if ($pattern = getenv("BUILD")) { - return $this->buildInput($inFname); - } - - $outFname = self::outputNameFor($inFname); - - if (!is_readable($outFname)) { - $this->fail("$outFname is missing, ". - "consider building tests with BUILD=true"); - } - - $input = file_get_contents($inFname); - $output = file_get_contents($outFname); - - $this->assertEquals($output, $this->less->parse($input)); - } - - public function fileNameProvider() { - return array_map(function($a) { return array($a); }, - self::findInputNames()); - } - - // only run when env is set - public function buildInput($inFname) { - $css = $this->less->parse(file_get_contents($inFname)); - file_put_contents(self::outputNameFor($inFname), $css); - } - - static public function findInputNames($pattern="*.less") { - $files = array(); - foreach (self::$testDirs as $inputDir => $outputDir) { - $files = array_merge($files, glob(__DIR__ . "/" . $inputDir . "/" . $pattern)); - } - - return array_filter($files, "is_file"); - } - - static public function outputNameFor($input) { - $front = _quote(__DIR__ . "/"); - $out = preg_replace("/^$front/", "", $input); - - foreach (self::$testDirs as $inputDir => $outputDir) { - $in = _quote($inputDir . "/"); - $rewritten = preg_replace("/$in/", $outputDir . "/", $out); - if ($rewritten != $out) { - $out = $rewritten; - break; - } - } - - $out = preg_replace("/.less$/", ".css", $out); - - return __DIR__ . "/" . $out; - } + protected static $importDirs = array("inputs/test-imports"); + + protected static $testDirs = array( + "inputs" => "outputs", + "inputs_lessjs" => "outputs_lessjs", + ); + + public function setUp() { + $this->less = new lessc(); + $this->less->importDir = array_map(function($path) { + return __DIR__ . "/" . $path; + }, self::$importDirs); + } + + /** + * @dataProvider fileNameProvider + */ + public function testInputFile($inFname) { + if ($pattern = getenv("BUILD")) { + return $this->buildInput($inFname); + } + + $outFname = self::outputNameFor($inFname); + + if (!is_readable($outFname)) { + $this->fail("$outFname is missing, ". + "consider building tests with BUILD=true"); + } + + $input = file_get_contents($inFname); + $output = file_get_contents($outFname); + + $this->assertEquals($output, $this->less->parse($input)); + } + + public function fileNameProvider() { + return array_map(function($a) { return array($a); }, + self::findInputNames()); + } + + // only run when env is set + public function buildInput($inFname) { + $css = $this->less->parse(file_get_contents($inFname)); + file_put_contents(self::outputNameFor($inFname), $css); + } + + static public function findInputNames($pattern="*.less") { + $files = array(); + foreach (self::$testDirs as $inputDir => $outputDir) { + $files = array_merge($files, glob(__DIR__ . "/" . $inputDir . "/" . $pattern)); + } + + return array_filter($files, "is_file"); + } + + static public function outputNameFor($input) { + $front = _quote(__DIR__ . "/"); + $out = preg_replace("/^$front/", "", $input); + + foreach (self::$testDirs as $inputDir => $outputDir) { + $in = _quote($inputDir . "/"); + $rewritten = preg_replace("/$in/", $outputDir . "/", $out); + if ($rewritten != $out) { + $out = $rewritten; + break; + } + } + + $out = preg_replace("/.less$/", ".css", $out); + + return __DIR__ . "/" . $out; + } } diff --git a/tests/sort.php b/tests/sort.php index 70b907ea..ab88604f 100644 --- a/tests/sort.php +++ b/tests/sort.php @@ -8,46 +8,46 @@ $exe = array_shift($argv); // remove filename if (!$fname = array_shift($argv)) { - $fname = "php://stdin"; + $fname = "php://stdin"; } class lesscNormalized extends lessc { - public $numberPrecision = 3; + public $numberPrecision = 3; - public function compileValue($value) { - if ($value[0] == "raw_color") { - $value = $this->coerceColor($value); - } + public function compileValue($value) { + if ($value[0] == "raw_color") { + $value = $this->coerceColor($value); + } - return parent::compileValue($value); - } + return parent::compileValue($value); + } } class SortingFormatter extends lessc_formatter_lessjs { - function sortKey($block) { - if (!isset($block->sortKey)) { - sort($block->selectors, SORT_STRING); - $block->sortKey = implode(",", $block->selectors); - } - - return $block->sortKey; - } - - function sortBlock($block) { - usort($block->children, function($a, $b) { - $sort = strcmp($this->sortKey($a), $this->sortKey($b)); - if ($sort == 0) { - // TODO - } - return $sort; - }); - - } - - function block($block) { - $this->sortBlock($block); - return parent::block($block); - } + function sortKey($block) { + if (!isset($block->sortKey)) { + sort($block->selectors, SORT_STRING); + $block->sortKey = implode(",", $block->selectors); + } + + return $block->sortKey; + } + + function sortBlock($block) { + usort($block->children, function($a, $b) { + $sort = strcmp($this->sortKey($a), $this->sortKey($b)); + if ($sort == 0) { + // TODO + } + return $sort; + }); + + } + + function block($block) { + $this->sortBlock($block); + return parent::block($block); + } } From e8f652f365e6d475fa20325dc2347cd2ecea5eb1 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 06:11:11 +0100 Subject: [PATCH 26/28] tests: Remove and resolve more PSR2 style overrides Inspired by https://github.com/MarcusSchwarz/lesserphp/pull/6. --- .phpcs.xml | 12 --- lessc.inc.php | 255 +++++++++++++++++++++++++++----------------- lessify.inc.php | 104 +++++++++--------- tests/InputTest.php | 11 +- tests/sort.php | 11 +- 5 files changed, 220 insertions(+), 173 deletions(-) diff --git a/.phpcs.xml b/.phpcs.xml index 8d0ffbd9..802e79c6 100644 --- a/.phpcs.xml +++ b/.phpcs.xml @@ -3,7 +3,6 @@ - @@ -12,36 +11,25 @@ - - - - - - - - - - - diff --git a/lessc.inc.php b/lessc.inc.php index f59e3f9b..37988952 100644 --- a/lessc.inc.php +++ b/lessc.inc.php @@ -81,12 +81,12 @@ protected function fileExists($name) { return is_file($name); } - static public function compressList($items, $delim) { + public static function compressList($items, $delim) { if (!isset($items[1]) && isset($items[0])) return $items[0]; else return array('list', $delim, $items); } - static public function preg_quote($what) { + public static function preg_quote($what) { return preg_quote($what, '/'); } @@ -294,7 +294,7 @@ protected function deduplicate($lines) { $unique = array(); $comments = array(); - foreach($lines as $line) { + foreach ($lines as $line) { if (strpos($line, '/*') === 0) { $comments[] = $line; continue; @@ -390,8 +390,8 @@ protected function compileMediaQuery($queries) { protected function multiplyMedia($env, $childQueries = null) { if (is_null($env) || - !empty($env->block->type) && $env->block->type != "media") - { + !empty($env->block->type) && $env->block->type != "media" + ) { return $childQueries; } @@ -634,7 +634,7 @@ protected function zipSetArgs($args, $orderedValues, $keywordValues) { $assignedValues = array(); $i = 0; - foreach ($args as $a) { + foreach ($args as $a) { if ($a[0] == "arg") { if (isset($keywordValues[$a[1]])) { // has keyword arg @@ -746,8 +746,8 @@ protected function compileProp($prop, $block, $out) { if ($suffix !== null && $subProp[0] == "assign" && is_string($subProp[1]) && - $subProp[1]{0} != $this->vPrefix) - { + $subProp[1]{0} != $this->vPrefix + ) { $subProp[2] = array( 'list', ' ', array($subProp[2], array('keyword', $suffix)) @@ -774,7 +774,7 @@ protected function compileProp($prop, $block, $out) { case "comment": $out->lines[] = $prop[1]; break; - case "import"; + case "import": list(, $importPath, $importId) = $prop; $importPath = $this->reduce($importPath); @@ -971,12 +971,16 @@ protected function lib_isrem($value) { protected function lib_rgbahex($color) { $color = $this->coerceColor($color); - if (is_null($color)) + if (is_null($color)) { $this->throwError("color expected for rgbahex"); + } return sprintf("#%02x%02x%02x%02x", - isset($color[4]) ? $color[4]*255 : 255, - $color[1],$color[2], $color[3]); + isset($color[4]) ? $color[4] * 255 : 255, + $color[1], + $color[2], + $color[3] + ); } protected function lib_argb($color){ @@ -995,20 +999,20 @@ protected function lib_data_uri($value) { $fullpath = $this->findImport($url); - if($fullpath && ($fsize = filesize($fullpath)) !== false) { + if ($fullpath && ($fsize = filesize($fullpath)) !== false) { // IE8 can't handle data uris larger than 32KB - if($fsize/1024 < 32) { - if(is_null($mime)) { - if(class_exists('finfo')) { // php 5.3+ + if ($fsize/1024 < 32) { + if (is_null($mime)) { + if (class_exists('finfo')) { // php 5.3+ $finfo = new finfo(FILEINFO_MIME); $mime = explode('; ', $finfo->file($fullpath)); $mime = $mime[0]; - } elseif(function_exists('mime_content_type')) { // PHP 5.2 + } elseif (function_exists('mime_content_type')) { // PHP 5.2 $mime = mime_content_type($fullpath); } } - if(!is_null($mime)) // fallback if the mime type is still unknown + if (!is_null($mime)) // fallback if the mime type is still unknown $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); } } @@ -1074,7 +1078,7 @@ protected function lib_ceil($arg) { } protected function lib_round($arg) { - if($arg[0] != "list") { + if ($arg[0] != "list") { $value = $this->assertNumber($arg); return array("number", round($value), $arg[2]); } else { @@ -1147,7 +1151,9 @@ protected function lib_spin($args) { $hsl = $this->toHSL($color); $hsl[1] = $hsl[1] + $delta % 360; - if ($hsl[1] < 0) $hsl[1] += 360; + if ($hsl[1] < 0) { + $hsl[1] += 360; + } return $this->toRGB($hsl); } @@ -1365,7 +1371,9 @@ public function assertArgs($value, $expectedArgs, $name="") { } protected function toHSL($color) { - if ($color[0] == 'hsl') return $color; + if ($color[0] === 'hsl') { + return $color; + } $r = $color[1] / 255; $g = $color[2] / 255; @@ -1378,34 +1386,50 @@ protected function toHSL($color) { if ($min == $max) { $S = $H = 0; } else { - if ($L < 0.5) - $S = ($max - $min)/($max + $min); - else - $S = ($max - $min)/(2.0 - $max - $min); - - if ($r == $max) $H = ($g - $b)/($max - $min); - elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); - elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); + if ($L < 0.5) { + $S = ($max - $min) / ($max + $min); + } else { + $S = ($max - $min) / (2.0 - $max - $min); + } + if ($r == $max) { + $H = ($g - $b) / ($max - $min); + } elseif ($g == $max) { + $H = 2.0 + ($b - $r) / ($max - $min); + } elseif ($b == $max) { + $H = 4.0 + ($r - $g) / ($max - $min); + } } $out = array('hsl', ($H < 0 ? $H + 6 : $H)*60, - $S*100, - $L*100, + $S * 100, + $L * 100, ); - if (count($color) > 4) $out[] = $color[4]; // copy alpha + if (count($color) > 4) { + // copy alpha + $out[] = $color[4]; + } return $out; } protected function toRGB_helper($comp, $temp1, $temp2) { - if ($comp < 0) $comp += 1.0; - elseif ($comp > 1) $comp -= 1.0; + if ($comp < 0) { + $comp += 1.0; + } elseif ($comp > 1) { + $comp -= 1.0; + } - if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; - if (2 * $comp < 1) return $temp2; - if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; + if (6 * $comp < 1) { + return $temp1 + ($temp2 - $temp1) * 6 * $comp; + } + if (2 * $comp < 1) { + return $temp2; + } + if (3 * $comp < 2) { + return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; + } return $temp1; } @@ -1415,7 +1439,9 @@ protected function toRGB_helper($comp, $temp1, $temp2) { * Expects H to be in range of 0 to 360, S and L in 0 to 100 */ protected function toRGB($color) { - if ($color[0] == 'color') return $color; + if ($color[0] === 'color') { + return $color; + } $H = $color[1] / 360; $S = $color[2] / 100; @@ -1425,7 +1451,7 @@ protected function toRGB($color) { $r = $g = $b = $L; } else { $temp2 = $L < 0.5 ? - $L*(1.0 + $S) : + $L * (1.0 + $S) : $L + $S - $L * $S; $temp1 = 2.0 * $L - $temp2; @@ -1437,7 +1463,10 @@ protected function toRGB($color) { // $out = array('color', round($r*255), round($g*255), round($b*255)); $out = array('color', $r*255, $g*255, $b*255); - if (count($color) > 4) $out[] = $color[4]; // copy alpha + if (count($color) > 4) { + // copy alpha + $out[] = $color[4]; + } return $out; } @@ -1451,7 +1480,10 @@ protected function clamp($v, $max = 1, $min = 0) { */ protected function funcToColor($func) { $fname = $func[1]; - if ($func[2][0] != 'list') return false; // need a list of arguments + if ($func[2][0] != 'list') { + // need a list of arguments + return false; + } $rawComponents = $func[2][2]; if ($fname == 'hsl' || $fname == 'hsla') { @@ -1461,15 +1493,21 @@ protected function funcToColor($func) { $val = $this->reduce($c); $val = isset($val[1]) ? floatval($val[1]) : 0; - if ($i == 0) $clamp = 360; - elseif ($i < 3) $clamp = 100; - else $clamp = 1; + if ($i == 0) { + $clamp = 360; + } elseif ($i < 3) { + $clamp = 100; + } else { + $clamp = 1; + } $hsl[] = $this->clamp($val, $clamp); $i++; } - while (count($hsl) < 4) $hsl[] = 0; + while (count($hsl) < 4) { + $hsl[] = 0; + } return $this->toRGB($hsl); } elseif ($fname == 'rgb' || $fname == 'rgba') { @@ -1493,7 +1531,9 @@ protected function funcToColor($func) { $i++; } - while (count($components) < 3) $components[] = 0; + while (count($components) < 3) { + $components[] = 0; + } array_unshift($components, 'color'); return $this->fixColor($components); } @@ -1574,8 +1614,11 @@ protected function reduce($value, $forExpression = false) { } // convert to a typed value if the result is a php primitive - if (is_numeric($ret)) $ret = array('number', $ret, ""); - elseif (!is_array($ret)) $ret = array('keyword', $ret); + if (is_numeric($ret)) { + $ret = array('number', $ret, ""); + } elseif (!is_array($ret)) { + $ret = array('keyword', $ret); + } return $ret; } @@ -1617,7 +1660,7 @@ protected function reduce($value, $forExpression = false) { // coerce a value for use in color operation protected function coerceColor($value) { - switch($value[0]) { + switch ($value[0]) { case 'color': return $value; case 'raw_color': $c = array("color", 0, 0, 0); @@ -1638,9 +1681,9 @@ protected function coerceColor($value) { if (isset(self::$cssColors[$name])) { $rgba = explode(',', self::$cssColors[$name]); - if(isset($rgba[3])) + if (isset($rgba[3])) { return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); - + } return array('color', $rgba[0], $rgba[1], $rgba[2]); } return null; @@ -1667,8 +1710,7 @@ protected function flattenList($value) { } public function toBool($a) { - if ($a) return self::$TRUE; - else return self::$FALSE; + return $a ? self::$TRUE : self::$FALSE; } // evaluate an expression @@ -1711,8 +1753,12 @@ protected function evaluate($exp) { // make the expression look it did before being parsed $paddedOp = $op; - if ($whiteBefore) $paddedOp = " " . $paddedOp; - if ($whiteAfter) $paddedOp .= " "; + if ($whiteBefore) { + $paddedOp = " " . $paddedOp; + } + if ($whiteAfter) { + $paddedOp .= " "; + } return array("string", "", array($left, $paddedOp, $right)); } @@ -1776,7 +1822,9 @@ protected function op_color_color($op, $left, $right) { $out[] = $lval % $rval; break; case '/': - if ($rval == 0) $this->throwError("evaluate error: can't divide by zero"); + if ($rval == 0) { + $this->throwError("evaluate error: can't divide by zero"); + } $out[] = $lval / $rval; break; default: @@ -1786,7 +1834,7 @@ protected function op_color_color($op, $left, $right) { return $this->fixColor($out); } - function lib_red($color){ + public function lib_red($color){ $color = $this->coerceColor($color); if (is_null($color)) { $this->throwError('color expected for red()'); @@ -1795,7 +1843,7 @@ function lib_red($color){ return $color[1]; } - function lib_green($color){ + public function lib_green($color){ $color = $this->coerceColor($color); if (is_null($color)) { $this->throwError('color expected for green()'); @@ -1804,7 +1852,7 @@ function lib_green($color){ return $color[2]; } - function lib_blue($color){ + public function lib_blue($color){ $color = $this->coerceColor($color); if (is_null($color)) { $this->throwError('color expected for blue()'); @@ -1898,12 +1946,13 @@ protected function get($name) { return array('list', ' ', $current->arguments); } - if (isset($current->store[$name])) + if (isset($current->store[$name])) { return $current->store[$name]; - else { - $current = isset($current->storeParent) ? - $current->storeParent : $current->parent; } + + $current = isset($current->storeParent) ? + $current->storeParent : + $current->parent; } $this->throwError("variable $name is undefined"); @@ -1914,7 +1963,9 @@ protected function injectVariables($args) { $this->pushEnv(); $parser = new lessc_parser($this, __METHOD__); foreach ($args as $name => $strValue) { - if ($name{0} != '@') $name = '@'.$name; + if ($name{0} !== '@') { + $name = '@' . $name; + } $parser->count = 0; $parser->buffer = (string)$strValue; if (!$parser->propertyValue($value)) { @@ -2022,15 +2073,15 @@ public function cachedCompile($in, $force = false) { if (is_string($in)) { $root = $in; - } elseif (is_array($in) and isset($in['root'])) { - if ($force or ! isset($in['files'])) { + } elseif (is_array($in) && isset($in['root'])) { + if ($force || !isset($in['files'])) { // If we are forcing a recompile or if for some reason the // structure does not contain any file information we should // specify the root to trigger a rebuild. $root = $in['root']; - } elseif (isset($in['files']) and is_array($in['files'])) { - foreach ($in['files'] as $fname => $ftime ) { - if (!file_exists($fname) or filemtime($fname) > $ftime) { + } elseif (isset($in['files']) && is_array($in['files'])) { + foreach ($in['files'] as $fname => $ftime) { + if (!file_exists($fname) || filemtime($fname) > $ftime) { // One of the files we knew about previously has changed // so we should look at our incoming root again. $root = $in['root']; @@ -2473,8 +2524,8 @@ protected function parseChunk() { // setting a property if ($this->keyword($key) && $this->assign() && - $this->propertyValue($value, $key) && $this->end()) - { + $this->propertyValue($value, $key) && $this->end() + ) { $this->append(array('assign', $key, $value), $s); return true; } else { @@ -2489,8 +2540,8 @@ protected function parseChunk() { // media if ($this->literal('@media')) { if (($this->mediaQueryList($mediaQueries) || true) - && $this->literal('{')) - { + && $this->literal('{') + ) { $media = $this->pushSpecialBlock("media"); $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; return true; @@ -2503,8 +2554,8 @@ protected function parseChunk() { if ($this->literal("@", false) && $this->keyword($dirName)) { if ($this->isDirective($dirName, $this->blockDirectives)) { if (($this->openString("{", $dirValue, null, array(";")) || true) && - $this->literal("{")) - { + $this->literal("{") + ) { $dir = $this->pushSpecialBlock("directive"); $dir->name = $dirName; if (isset($dirValue)) $dir->value = $dirValue; @@ -2523,8 +2574,8 @@ protected function parseChunk() { // setting a variable if ($this->variable($var) && $this->assign() && - $this->propertyValue($value) && $this->end()) - { + $this->propertyValue($value) && $this->end() + ) { $this->append(array('assign', $var, $value), $s); return true; } else { @@ -2539,8 +2590,8 @@ protected function parseChunk() { // opening parametric mixin if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && ($this->guards($guards) || true) && - $this->literal('{')) - { + $this->literal('{') + ) { $block = $this->pushBlock($this->fixTags(array($tag))); $block->args = $args; $block->isVararg = $isVararg; @@ -2600,8 +2651,8 @@ protected function parseChunk() { // mixin if ($this->mixinTags($tags) && ($this->argumentDef($argv, $isVararg) || true) && - ($this->keyword($suffix) || true) && $this->end()) - { + ($this->keyword($suffix) || true) && $this->end() + ) { $tags = $this->fixTags($tags); $this->append(array('mixin', $tags, $argv, $suffix), $s); return true; @@ -2756,8 +2807,8 @@ protected function parenValue(&$out) { $inParens = $this->inParens; if ($this->literal("(") && ($this->inParens = true) && $this->expression($exp) && - $this->literal(")")) - { + $this->literal(")") + ) { $out = $exp; $this->inParens = $inParens; return true; @@ -2779,8 +2830,8 @@ protected function value(&$value) { if ($this->literal("-", false) && (($this->variable($inner) && $inner = array("variable", $inner)) || $this->unit($inner) || - $this->parenValue($inner))) - { + $this->parenValue($inner)) + ) { $value = array("unary", "-", $inner); return true; } else { @@ -2885,8 +2936,8 @@ protected function mediaExpression(&$out) { if ($this->literal("(") && $this->keyword($feature) && ($this->literal(":") && $this->expression($value) || true) && - $this->literal(")")) - { + $this->literal(")") + ) { $out = array("mediaExp", $feature); if ($value) $out[] = $value; return true; @@ -3025,8 +3076,8 @@ protected function interpolation(&$out) { $s = $this->seek(); if ($this->literal("@{") && $this->openString("}", $interp, null, array("'", '"', ";")) && - $this->literal("}", false)) - { + $this->literal("}", false) + ) { $out = array("interpolate", $interp); $this->eatWhiteDefault = $oldWhite; if ($this->eatWhiteDefault) $this->whitespace(); @@ -3073,7 +3124,9 @@ protected function color(&$out) { // delimiter. protected function argumentDef(&$args, &$isVararg) { $s = $this->seek(); - if (!$this->literal('(')) return false; + if (!$this->literal('(')) { + return false; + } $values = array(); $delim = ","; @@ -3102,7 +3155,9 @@ protected function argumentDef(&$args, &$isVararg) { } $values[] = $arg; - if ($isVararg) break; + if ($isVararg) { + break; + } continue; } else { $values[] = array("lit", $value); @@ -3120,7 +3175,7 @@ protected function argumentDef(&$args, &$isVararg) { if (isset($values[1])) { // 2 items $newList = array(); foreach ($values as $i => $arg) { - switch($arg[0]) { + switch ($arg[0]) { case "arg": if ($i) { $this->throwError("Cannot mix ; and , as delimiter types"); @@ -3191,7 +3246,9 @@ protected function mixinTags(&$tags) { $this->literal(">"); } - if (count($tags) == 0) return false; + if (!$tags) { + return false; + } return true; } @@ -3268,11 +3325,11 @@ protected function tagBracket(&$parts, &$hasExpression) { // a space separated list of selectors protected function tag(&$tag, $simple = false) { - if ($simple) + if ($simple) { $chars = '^@,:;{}\][>\(\) "\''; - else + } else { $chars = '^@,;{}["\''; - + } $s = $this->seek(); $hasExpression = false; @@ -3377,8 +3434,8 @@ protected function func(&$func) { protected function variable(&$name) { $s = $this->seek(); if ($this->literal($this->lessc->vPrefix, false) && - ($this->variable($sub) || $this->keyword($name))) - { + ($this->variable($sub) || $this->keyword($name)) + ) { if (!empty($sub)) { $name = array('variable', $sub); } else { @@ -3821,5 +3878,3 @@ class lessc_formatter_lessjs extends lessc_formatter_classic { public $assignSeparator = ": "; public $selectorSeparator = ","; } - - diff --git a/lessify.inc.php b/lessify.inc.php index 9b7baa12..c47301ad 100644 --- a/lessify.inc.php +++ b/lessify.inc.php @@ -21,25 +21,27 @@ // class easyparse { - var $buffer; - var $count; + public $buffer; + public $count; - function __construct($str) { + public function __construct($str) { $this->count = 0; $this->buffer = trim($str); } - function seek($where = null) { - if ($where === null) return $this->count; - else $this->count = $where; + public function seek($where = null) { + if ($where === null) { + return $this->count; + } + $this->count = $where; return true; } - function preg_quote($what) { + public function preg_quote($what) { return preg_quote($what, '/'); } - function match($regex, &$out, $eatWhitespace = true) { + public function match($regex, &$out, $eatWhitespace = true) { $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; if (preg_match($r, $this->buffer, $out, null, $this->count)) { $this->count += strlen($out[0]); @@ -48,17 +50,17 @@ function match($regex, &$out, $eatWhitespace = true) { return false; } - function literal($what, $eatWhitespace = true) { + public function literal($what, $eatWhitespace = true) { // this is here mainly prevent notice from { } string accessor if ($this->count >= strlen($this->buffer)) return false; // shortcut on single letter - if (!$eatWhitespace and strlen($what) == 1) { + if (!$eatWhitespace and strlen($what) === 1) { if ($this->buffer{$this->count} == $what) { $this->count++; return true; } - else return false; + return false; } return $this->match($this->preg_quote($what), $m, $eatWhitespace); @@ -70,55 +72,57 @@ class tagparse extends easyparse { static private $combinators = null; static private $match_opts = null; - function parse() { + public function parse() { if (empty(self::$combinators)) { - self::$combinators = '('.implode('|', array_map(array($this, 'preg_quote'), + self::$combinators = '(' . implode('|', array_map(array($this, 'preg_quote'), array('+', '>', '~'))).')'; - self::$match_opts = '('.implode('|', array_map(array($this, 'preg_quote'), - array('=', '~=', '|=', '$=', '*='))).')'; + self::$match_opts = '(' . implode('|', array_map(array($this, 'preg_quote'), + array('=', '~=', '|=', '$=', '*='))) . ')'; } // crush whitespace - $this->buffer = preg_replace('/\s+/', ' ', $this->buffer).' '; + $this->buffer = preg_replace('/\s+/', ' ', $this->buffer) . ' '; $tags = array(); - while ($this->tag($t)) $tags[] = $t; + while ($this->tag($t)) { + $tags[] = $t; + } return $tags; } - static function compileString($string) { + public static function compileString($string) { list(, $delim, $str) = $string; - $str = str_replace($delim, "\\".$delim, $str); + $str = str_replace($delim, "\\" . $delim, $str); $str = str_replace("\n", "\\\n", $str); - return $delim.$str.$delim; + return $delim . $str . $delim; } - static function compilePaths($paths) { + public static function compilePaths($paths) { return implode(', ', array_map(array('self', 'compilePath'), $paths)); } // array of tags - static function compilePath($path) { + public static function compilePath($path) { return implode(' ', array_map(array('self', 'compileTag'), $path)); } - static function compileTag($tag) { + public static function compileTag($tag) { ob_start(); - if (isset($tag['comb'])) echo $tag['comb']." "; + if (isset($tag['comb'])) echo $tag['comb'] . " "; if (isset($tag['front'])) echo $tag['front']; if (isset($tag['attr'])) { - echo '['.$tag['attr']; + echo '[' . $tag['attr']; if (isset($tag['op'])) { - echo $tag['op'].$tag['op_value']; + echo $tag['op'] . $tag['op_value']; } echo ']'; } return ob_get_clean(); } - function string(&$out) { + public function string(&$out) { $s = $this->seek(); if ($this->literal('"')) { @@ -179,7 +183,7 @@ function string(&$out) { return false; } - function tag(&$out) { + public function tag(&$out) { $s = $this->seek(); $tag = array(); if ($this->combinator($op)) $tag['comb'] = $op; @@ -219,7 +223,7 @@ function tag(&$out) { return false; } - function ident(&$out) { + public function ident(&$out) { // [-]?{nmstart}{nmchar}* // nmstart: [_a-z]|{nonascii}|{escape} // nmchar: [_a-z0-9-]|{nonascii}|{escape} @@ -230,7 +234,7 @@ function ident(&$out) { return false; } - function value(&$out) { + public function value(&$out) { if ($this->string($str)) { $out = $this->compileString($str); return true; @@ -242,7 +246,7 @@ function value(&$out) { } - function combinator(&$op) { + public function combinator(&$op) { if ($this->match(self::$combinators, $m)) { $op = $m[1]; return true; @@ -252,42 +256,42 @@ function combinator(&$op) { } class nodecounter { - var $count = 0; - var $children = array(); + public $count = 0; + public $children = array(); - var $name; - var $child_blocks; - var $the_block; + public $name; + public $child_blocks; + public $the_block; - function __construct($name) { + public function __construct($name) { $this->name = $name; } - function dump($stack = null) { + public function dump($stack = null) { if (is_null($stack)) $stack = array(); $stack[] = $this->getName(); - echo implode(' -> ', $stack)." ($this->count)\n"; + echo implode(' -> ', $stack) . " ($this->count)\n"; foreach ($this->children as $child) { $child->dump($stack); } } - static function compileProperties($c, $block) { - foreach($block as $name => $value) { + public static function compileProperties($c, $block) { + foreach ($block as $name => $value) { if ($c->isProperty($name, $value)) { - echo $c->compileProperty($name, $value)."\n"; + echo $c->compileProperty($name, $value) . "\n"; } } } - function compile($c, $path = null) { + public function compile($c, $path = null) { if (is_null($path)) $path = array(); $path[] = $this->name; $isVisible = !is_null($this->the_block) || !is_null($this->child_blocks); if ($isVisible) { - echo $c->indent(implode(' ', $path).' {'); + echo $c->indent(implode(' ', $path) . ' {'); $c->indentLevel++; $path = array(); @@ -307,7 +311,7 @@ function compile($c, $path = null) { } // compile child nodes - foreach($this->children as $node) { + foreach ($this->children as $node) { $node->compile($c, $path); } @@ -318,12 +322,12 @@ function compile($c, $path = null) { } - function getName() { + public function getName() { if (is_null($this->name)) return "[root]"; else return $this->name; } - function getNode($name) { + public function getNode($name) { if (!isset($this->children[$name])) { $this->children[$name] = new nodecounter($name); } @@ -331,7 +335,7 @@ function getNode($name) { return $this->children[$name]; } - function findNode($path) { + public function findNode($path) { $current = $this; for ($i = 0; $i < count($path); $i++) { $t = tagparse::compileTag($path[$i]); @@ -341,7 +345,7 @@ function findNode($path) { return $current; } - function addBlock($path, $block) { + public function addBlock($path, $block) { $node = $this->findNode($path); if (!is_null($node->the_block)) throw new exception("can this happen?"); @@ -349,7 +353,7 @@ function addBlock($path, $block) { $node->the_block = $block; } - function addToNode($path, $block) { + public function addToNode($path, $block) { $node = $this->findNode($path); $node->child_blocks[] = $block; } diff --git a/tests/InputTest.php b/tests/InputTest.php index ae8c494a..bac19917 100644 --- a/tests/InputTest.php +++ b/tests/InputTest.php @@ -49,8 +49,10 @@ public function testInputFile($inFname) { } public function fileNameProvider() { - return array_map(function($a) { return array($a); }, - self::findInputNames()); + return array_map( + function($a) { return array($a); }, + self::findInputNames() + ); } // only run when env is set @@ -59,7 +61,7 @@ public function buildInput($inFname) { file_put_contents(self::outputNameFor($inFname), $css); } - static public function findInputNames($pattern="*.less") { + public static function findInputNames($pattern="*.less") { $files = array(); foreach (self::$testDirs as $inputDir => $outputDir) { $files = array_merge($files, glob(__DIR__ . "/" . $inputDir . "/" . $pattern)); @@ -68,7 +70,7 @@ static public function findInputNames($pattern="*.less") { return array_filter($files, "is_file"); } - static public function outputNameFor($input) { + public static function outputNameFor($input) { $front = _quote(__DIR__ . "/"); $out = preg_replace("/^$front/", "", $input); @@ -86,4 +88,3 @@ static public function outputNameFor($input) { return __DIR__ . "/" . $out; } } - diff --git a/tests/sort.php b/tests/sort.php index ab88604f..8987a90e 100644 --- a/tests/sort.php +++ b/tests/sort.php @@ -1,7 +1,7 @@ coerceColor($value); } @@ -24,7 +24,7 @@ public function compileValue($value) { } class SortingFormatter extends lessc_formatter_lessjs { - function sortKey($block) { + public function sortKey($block) { if (!isset($block->sortKey)) { sort($block->selectors, SORT_STRING); $block->sortKey = implode(",", $block->selectors); @@ -33,7 +33,7 @@ function sortKey($block) { return $block->sortKey; } - function sortBlock($block) { + public function sortBlock($block) { usort($block->children, function($a, $b) { $sort = strcmp($this->sortKey($a), $this->sortKey($b)); if ($sort == 0) { @@ -44,7 +44,7 @@ function sortBlock($block) { } - function block($block) { + public function block($block) { $this->sortBlock($block); return parent::block($block); } @@ -54,4 +54,3 @@ function block($block) { $less = new lesscNormalized(); $less->setFormatter(new SortingFormatter); echo $less->parse(file_get_contents($fname)); - From 18b8f292a57810c3fb6799f8e2ef2aea2a078215 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 06:38:40 +0100 Subject: [PATCH 27/28] tests: Require PHP 5.6 or later (drop PHP 5.4 and PHP 5.5) Per and . Also because PHPUnit 4.x is on longer supported by upstream. Ref https://github.com/MarcusSchwarz/lesserphp/commit/cfdd1e2b27. --- .travis.yml | 2 -- composer.json | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 65310f97..1da8ccb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: php php: - - 5.4 - - 5.5 - 5.6 - hhvm - 7.0 diff --git a/composer.json b/composer.json index bb82cb98..be6d6030 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "classmap": ["lessc.inc.php"] }, "require-dev": { - "phpunit/phpunit": "^4.8.36", + "phpunit/phpunit": "^5.7.27", "squizlabs/php_codesniffer": "3.3.2" }, "scripts": { From 1fdc40e77824a692d7f4811ba86ed89669a66135 Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Tue, 16 Oct 2018 20:24:03 +0100 Subject: [PATCH 28/28] license: Restore space between year and attribution Follows-up d953e57. Sorry! --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0a07ad9f..3b307b5f 100644 --- a/LICENSE +++ b/LICENSE @@ -9,7 +9,7 @@ MIT LICENSE -Copyright (c) 2014Leaf Corcoran, http://leafo.net/lessphp +Copyright (c) 2014 Leaf Corcoran, http://leafo.net/lessphp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the