From e6380ba37d6b3f7dd03146b3c03030ccc8c1b297 Mon Sep 17 00:00:00 2001 From: Andreas Gohr Date: Fri, 2 Feb 2024 15:01:14 +0100 Subject: [PATCH] replace LESS library. fixes #4088 This replaces the abandoned fork with my own fork at splitbrain/lesserphp That fork has been cleaned up somewhat and the issues in #4088 should be fixed. --- .gitignore | 4 - composer.json | 2 +- composer.lock | 117 +- lib/exe/css.php | 6 +- vendor/composer/autoload_classmap.php | 5 - vendor/composer/autoload_psr4.php | 2 + vendor/composer/autoload_static.php | 18 +- vendor/composer/installed.json | 121 +- vendor/composer/installed.php | 22 +- vendor/marcusschwarz/lesserphp/README.md | 97 - vendor/marcusschwarz/lesserphp/composer.json | 36 - vendor/marcusschwarz/lesserphp/lessc.inc.php | 4021 -------------------- .../lesserphp/.gitignore | 0 vendor/splitbrain/lesserphp/.phpcs.xml | 13 + .../lesserphp/HISTORY.md | 6 +- .../lesserphp/LICENSE | 0 vendor/splitbrain/lesserphp/README.md | 64 + vendor/splitbrain/lesserphp/composer.json | 49 + vendor/splitbrain/lesserphp/rector.php | 17 + vendor/splitbrain/lesserphp/src/Constants.php | 181 + .../splitbrain/lesserphp/src/FormatterClassic.php | 114 + .../lesserphp/src/FormatterCompressed.php | 28 + .../splitbrain/lesserphp/src/FormatterLessJs.php | 21 + .../src/Functions/AbstractFunctionCollection.php | 25 + .../lesserphp/src/Functions/ColorChannels.php | 132 + .../lesserphp/src/Functions/ColorDefinition.php | 71 + .../lesserphp/src/Functions/ColorOperation.php | 315 ++ .../splitbrain/lesserphp/src/Functions/Lists.php | 50 + vendor/splitbrain/lesserphp/src/Functions/Math.php | 274 ++ vendor/splitbrain/lesserphp/src/Functions/Misc.php | 115 + .../splitbrain/lesserphp/src/Functions/Strings.php | 85 + vendor/splitbrain/lesserphp/src/Functions/Type.php | 120 + vendor/splitbrain/lesserphp/src/Lessc.php | 1746 +++++++++ vendor/splitbrain/lesserphp/src/Parser.php | 1519 ++++++++ .../splitbrain/lesserphp/src/ParserException.php | 77 + vendor/splitbrain/lesserphp/src/Utils/Asserts.php | 81 + vendor/splitbrain/lesserphp/src/Utils/Color.php | 187 + vendor/splitbrain/lesserphp/src/Utils/Util.php | 128 + 38 files changed, 5573 insertions(+), 4296 deletions(-) delete mode 100644 vendor/marcusschwarz/lesserphp/README.md delete mode 100644 vendor/marcusschwarz/lesserphp/composer.json delete mode 100644 vendor/marcusschwarz/lesserphp/lessc.inc.php rename vendor/{marcusschwarz => splitbrain}/lesserphp/.gitignore (100%) create mode 100644 vendor/splitbrain/lesserphp/.phpcs.xml rename vendor/{marcusschwarz => splitbrain}/lesserphp/HISTORY.md (90%) rename vendor/{marcusschwarz => splitbrain}/lesserphp/LICENSE (100%) create mode 100644 vendor/splitbrain/lesserphp/README.md create mode 100644 vendor/splitbrain/lesserphp/composer.json create mode 100644 vendor/splitbrain/lesserphp/rector.php create mode 100644 vendor/splitbrain/lesserphp/src/Constants.php create mode 100644 vendor/splitbrain/lesserphp/src/FormatterClassic.php create mode 100644 vendor/splitbrain/lesserphp/src/FormatterCompressed.php create mode 100644 vendor/splitbrain/lesserphp/src/FormatterLessJs.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/AbstractFunctionCollection.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/ColorChannels.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/ColorDefinition.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/ColorOperation.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/Lists.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/Math.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/Misc.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/Strings.php create mode 100644 vendor/splitbrain/lesserphp/src/Functions/Type.php create mode 100644 vendor/splitbrain/lesserphp/src/Lessc.php create mode 100644 vendor/splitbrain/lesserphp/src/Parser.php create mode 100644 vendor/splitbrain/lesserphp/src/ParserException.php create mode 100644 vendor/splitbrain/lesserphp/src/Utils/Asserts.php create mode 100644 vendor/splitbrain/lesserphp/src/Utils/Color.php create mode 100644 vendor/splitbrain/lesserphp/src/Utils/Util.php diff --git a/.gitignore b/.gitignore index 7edadd465..279efea35 100644 --- a/.gitignore +++ b/.gitignore @@ -83,10 +83,6 @@ vendor/paragonie/random_compat/build-phar.sh vendor/paragonie/random_compat/dist/* vendor/paragonie/random_compat/other/* vendor/simplepie/simplepie/db.sql -vendor/marcusschwarz/lesserphp/package.sh -vendor/marcusschwarz/lesserphp/lessify* -vendor/marcusschwarz/lesserphp/Makefile -vendor/marcusschwarz/lesserphp/plessc vendor/splitbrain/php-cli/examples/* vendor/splitbrain/php-cli/screenshot* vendor/splitbrain/php-cli/generate-api.sh diff --git a/composer.json b/composer.json index 9828fafda..21ea0c597 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "geshi/geshi": "dev-master as 1.0.x-dev", "openpsa/universalfeedcreator": "^1.8", "aziraphale/email-address-validator": "^2", - "marcusschwarz/lesserphp": "^0.6", + "splitbrain/lesserphp": "^0.10", "splitbrain/php-cli": "^1.1", "splitbrain/slika": "^1.0", "kissifrot/php-ixr": "^1.8", diff --git a/composer.lock b/composer.lock index 012d299a0..fe8065c09 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9fdb13c8abdf5a5c4124687e6f8d3b16", + "content-hash": "34c61e4640c708005e450f6b5caa5cf8", "packages": [ { "name": "aziraphale/email-address-validator", @@ -151,60 +151,6 @@ "time": "2023-04-15T08:50:43+00:00" }, { - "name": "marcusschwarz/lesserphp", - "version": "v0.6.0", - "source": { - "type": "git", - "url": "https://github.com/MarcusSchwarz/lesserphp.git", - "reference": "64ece57ad81ab1fe4d2a1894729e0d293fce09ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarcusSchwarz/lesserphp/zipball/64ece57ad81ab1fe4d2a1894729e0d293fce09ef", - "reference": "64ece57ad81ab1fe4d2a1894729e0d293fce09ef", - "shasum": "" - }, - "require": { - "php": "^7.2|^7.3|^7.4|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^8.0|^9.0" - }, - "bin": [ - "plessc" - ], - "type": "library", - "autoload": { - "classmap": [ - "lessc.inc.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT", - "GPL-3.0" - ], - "authors": [ - { - "name": "Leaf Corcoran", - "email": "leafot@gmail.com", - "homepage": "http://leafo.net" - }, - { - "name": "Marcus Schwarz", - "email": "github@maswaba.de", - "homepage": "https://www.maswaba.de" - } - ], - "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", - "homepage": "https://www.maswaba.de/lesserphpdocs/", - "support": { - "issues": "https://github.com/MarcusSchwarz/lesserphp/issues", - "source": "https://github.com/MarcusSchwarz/lesserphp/tree/v0.6.0" - }, - "time": "2021-03-10T19:14:23+00:00" - }, - { "name": "openpsa/universalfeedcreator", "version": "v1.8.6", "source": { @@ -563,6 +509,67 @@ "time": "2023-01-20T08:37:35+00:00" }, { + "name": "splitbrain/lesserphp", + "version": "v0.10.0", + "source": { + "type": "git", + "url": "https://github.com/splitbrain/lesserphp.git", + "reference": "2e5d20c4ce9186a34554a137a4a1784fc5c29505" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/splitbrain/lesserphp/zipball/2e5d20c4ce9186a34554a137a4a1784fc5c29505", + "reference": "2e5d20c4ce9186a34554a137a4a1784fc5c29505", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^8.5", + "rector/rector": "^0.19", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-fileinfo": "For mime type guessing of embedded files" + }, + "type": "library", + "autoload": { + "psr-4": { + "LesserPHP\\": "src", + "LesserPHP\\tests\\": "tests" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + }, + { + "name": "Marcus Schwarz", + "email": "github@maswaba.de", + "homepage": "https://www.maswaba.de" + }, + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org", + "homepage": "https://www.splitbrain.org" + } + ], + "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", + "support": { + "issues": "https://github.com/splitbrain/lesserphp/issues", + "source": "https://github.com/splitbrain/lesserphp/tree/v0.10.0" + }, + "time": "2024-02-02T11:47:26+00:00" + }, + { "name": "splitbrain/php-archive", "version": "1.3.1", "source": { diff --git a/lib/exe/css.php b/lib/exe/css.php index cf54e521b..f8bb9749f 100644 --- a/lib/exe/css.php +++ b/lib/exe/css.php @@ -208,12 +208,12 @@ function css_parseless($css) { global $conf; - $less = new lessc(); - $less->importDir = [DOKU_INC]; + $less = new LesserPHP\Lessc(); + $less->setImportDir([DOKU_INC]); $less->setPreserveComments(!$conf['compress']); if (defined('DOKU_UNITTEST')) { - $less->importDir[] = TMP_DIR; + $less->addImportDir(TMP_DIR); } try { diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 235c072b7..2c7e80332 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -28,9 +28,4 @@ return array( 'RSSCreator10' => $vendorDir . '/openpsa/universalfeedcreator/lib/Creator/RSSCreator10.php', 'RSSCreator20' => $vendorDir . '/openpsa/universalfeedcreator/lib/Creator/RSSCreator20.php', 'UniversalFeedCreator' => $vendorDir . '/openpsa/universalfeedcreator/lib/UniversalFeedCreator.php', - 'lessc' => $vendorDir . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_classic' => $vendorDir . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_compressed' => $vendorDir . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_lessjs' => $vendorDir . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_parser' => $vendorDir . '/marcusschwarz/lesserphp/lessc.inc.php', ); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index da0aedd69..3331a32bb 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -15,6 +15,8 @@ return array( 'phpseclib3\\' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'), 'SimplePie\\' => array($vendorDir . '/simplepie/simplepie/src'), 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'), + 'LesserPHP\\tests\\' => array($vendorDir . '/splitbrain/lesserphp/tests'), + 'LesserPHP\\' => array($vendorDir . '/splitbrain/lesserphp/src'), 'IXR\\tests\\' => array($vendorDir . '/kissifrot/php-ixr/tests'), 'IXR\\' => array($vendorDir . '/kissifrot/php-ixr/src'), ); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 84c9ceff0..ea7a7c731 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -33,6 +33,11 @@ class ComposerStaticInita19a915ee98347a0c787119619d2ff9b array ( 'ParagonIE\\ConstantTime\\' => 23, ), + 'L' => + array ( + 'LesserPHP\\tests\\' => 16, + 'LesserPHP\\' => 10, + ), 'I' => array ( 'IXR\\tests\\' => 10, @@ -77,6 +82,14 @@ class ComposerStaticInita19a915ee98347a0c787119619d2ff9b array ( 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src', ), + 'LesserPHP\\tests\\' => + array ( + 0 => __DIR__ . '/..' . '/splitbrain/lesserphp/tests', + ), + 'LesserPHP\\' => + array ( + 0 => __DIR__ . '/..' . '/splitbrain/lesserphp/src', + ), 'IXR\\tests\\' => array ( 0 => __DIR__ . '/..' . '/kissifrot/php-ixr/tests', @@ -127,11 +140,6 @@ class ComposerStaticInita19a915ee98347a0c787119619d2ff9b 'RSSCreator10' => __DIR__ . '/..' . '/openpsa/universalfeedcreator/lib/Creator/RSSCreator10.php', 'RSSCreator20' => __DIR__ . '/..' . '/openpsa/universalfeedcreator/lib/Creator/RSSCreator20.php', 'UniversalFeedCreator' => __DIR__ . '/..' . '/openpsa/universalfeedcreator/lib/UniversalFeedCreator.php', - 'lessc' => __DIR__ . '/..' . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_classic' => __DIR__ . '/..' . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_compressed' => __DIR__ . '/..' . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_formatter_lessjs' => __DIR__ . '/..' . '/marcusschwarz/lesserphp/lessc.inc.php', - 'lessc_parser' => __DIR__ . '/..' . '/marcusschwarz/lesserphp/lessc.inc.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 74cdf2e7a..ef5ea9170 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -150,63 +150,6 @@ "install-path": "../kissifrot/php-ixr" }, { - "name": "marcusschwarz/lesserphp", - "version": "v0.6.0", - "version_normalized": "0.6.0.0", - "source": { - "type": "git", - "url": "https://github.com/MarcusSchwarz/lesserphp.git", - "reference": "64ece57ad81ab1fe4d2a1894729e0d293fce09ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/MarcusSchwarz/lesserphp/zipball/64ece57ad81ab1fe4d2a1894729e0d293fce09ef", - "reference": "64ece57ad81ab1fe4d2a1894729e0d293fce09ef", - "shasum": "" - }, - "require": { - "php": "^7.2|^7.3|^7.4|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^8.0|^9.0" - }, - "time": "2021-03-10T19:14:23+00:00", - "bin": [ - "plessc" - ], - "type": "library", - "installation-source": "dist", - "autoload": { - "classmap": [ - "lessc.inc.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT", - "GPL-3.0" - ], - "authors": [ - { - "name": "Leaf Corcoran", - "email": "leafot@gmail.com", - "homepage": "http://leafo.net" - }, - { - "name": "Marcus Schwarz", - "email": "github@maswaba.de", - "homepage": "https://www.maswaba.de" - } - ], - "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", - "homepage": "https://www.maswaba.de/lesserphpdocs/", - "support": { - "issues": "https://github.com/MarcusSchwarz/lesserphp/issues", - "source": "https://github.com/MarcusSchwarz/lesserphp/tree/v0.6.0" - }, - "install-path": "../marcusschwarz/lesserphp" - }, - { "name": "openpsa/universalfeedcreator", "version": "v1.8.6", "version_normalized": "1.8.6.0", @@ -580,6 +523,70 @@ "install-path": "../simplepie/simplepie" }, { + "name": "splitbrain/lesserphp", + "version": "v0.10.0", + "version_normalized": "0.10.0.0", + "source": { + "type": "git", + "url": "https://github.com/splitbrain/lesserphp.git", + "reference": "2e5d20c4ce9186a34554a137a4a1784fc5c29505" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/splitbrain/lesserphp/zipball/2e5d20c4ce9186a34554a137a4a1784fc5c29505", + "reference": "2e5d20c4ce9186a34554a137a4a1784fc5c29505", + "shasum": "" + }, + "require": { + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "^8.5", + "rector/rector": "^0.19", + "squizlabs/php_codesniffer": "^3.8" + }, + "suggest": { + "ext-fileinfo": "For mime type guessing of embedded files" + }, + "time": "2024-02-02T11:47:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "LesserPHP\\": "src", + "LesserPHP\\tests\\": "tests" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + }, + { + "name": "Marcus Schwarz", + "email": "github@maswaba.de", + "homepage": "https://www.maswaba.de" + }, + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org", + "homepage": "https://www.splitbrain.org" + } + ], + "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", + "support": { + "issues": "https://github.com/splitbrain/lesserphp/issues", + "source": "https://github.com/splitbrain/lesserphp/tree/v0.10.0" + }, + "install-path": "../splitbrain/lesserphp" + }, + { "name": "splitbrain/php-archive", "version": "1.3.1", "version_normalized": "1.3.1.0", diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 7a2b0d400..0766ed655 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'dokuwiki/dokuwiki', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'e860a4fbf1abc8ba96c4cd6c00e8f6efd6510d50', + 'reference' => '87f6c7185502767dd23d1039195eed23f0048dd1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -22,7 +22,7 @@ 'dokuwiki/dokuwiki' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'e860a4fbf1abc8ba96c4cd6c00e8f6efd6510d50', + 'reference' => '87f6c7185502767dd23d1039195eed23f0048dd1', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -49,15 +49,6 @@ 'aliases' => array(), 'dev_requirement' => false, ), - 'marcusschwarz/lesserphp' => array( - 'pretty_version' => 'v0.6.0', - 'version' => '0.6.0.0', - 'reference' => '64ece57ad81ab1fe4d2a1894729e0d293fce09ef', - 'type' => 'library', - 'install_path' => __DIR__ . '/../marcusschwarz/lesserphp', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'openpsa/universalfeedcreator' => array( 'pretty_version' => 'v1.8.6', 'version' => '1.8.6.0', @@ -103,6 +94,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'splitbrain/lesserphp' => array( + 'pretty_version' => 'v0.10.0', + 'version' => '0.10.0.0', + 'reference' => '2e5d20c4ce9186a34554a137a4a1784fc5c29505', + 'type' => 'library', + 'install_path' => __DIR__ . '/../splitbrain/lesserphp', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'splitbrain/php-archive' => array( 'pretty_version' => '1.3.1', 'version' => '1.3.1.0', diff --git a/vendor/marcusschwarz/lesserphp/README.md b/vendor/marcusschwarz/lesserphp/README.md deleted file mode 100644 index d58f9132d..000000000 --- a/vendor/marcusschwarz/lesserphp/README.md +++ /dev/null @@ -1,97 +0,0 @@ -[![Build Status](https://travis-ci.org/MarcusSchwarz/lesserphp.svg)](https://travis-ci.org/MarcusSchwarz/lesserphp) - -# lesserphp v0.6.0 -### - -`lesserphp` is a compiler for LESS written in PHP. It is based on lessphp bei leafo. -The documentation is great, -so check it out: . - -Here's a quick tutorial: - -### How to use in your PHP project - -The only file required is `lessc.inc.php`, so copy that to your include directory. - -The typical flow of **lesserphp** is to create a new instance of `lessc`, -configure it how you like, then tell it to compile something using one built in -compile methods. - -The `compile` method compiles a string of LESS code to CSS. - -```php -compile(".block { padding: 3 + 4px }"); -``` - -The `compileFile` method reads and compiles a file. It will either return the -result or write it to the path specified by an optional second argument. - -```php -compileFile("input.less"); -``` - -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 -checkedCompile("input.less", "output.css"); -``` - -If there any problem compiling your code, an exception is thrown with a helpful message: - -```php -compile("invalid LESS } {"); -} catch (\Exception $e) { - echo "fatal error: " . $e->getMessage(); -} -``` - -The `lessc` object can be configured through an assortment of instance methods. -Some possible configuration options include [changing the output format][1], -[setting variables from PHP][2], and [controlling the preservation of -comments][3], writing [custom functions][4] and much more. It's all described -in [the documentation][0]. - - - [0]: http://leafo.net/lessphp/docs/ - [1]: http://leafo.net/lessphp/docs/#output_formatting - [2]: http://leafo.net/lessphp/docs/#setting_variables_from_php - [3]: http://leafo.net/lessphp/docs/#preserving_comments - [4]: http://leafo.net/lessphp/docs/#custom_functions - - -### How to use from the command line - -An additional script has been included to use the compiler from the command -line. In the simplest invocation, you specify an input file and the compiled -css is written to standard out: - - $ plessc input.less > output.css - -Using the -r flag, you can specify LESS code directly as an argument or, if -the argument is left off, from standard in: - - $ plessc -r "my less code here" - -Finally, by using the -w flag you can watch a specified input file and have it -compile as needed to the output file: - - $ plessc -w input-file output-file - -Errors from watch mode are written to standard out. - -The -f flag sets the [output formatter][1]. For example, to compress the -output run this: - - $ plessc -f=compressed myfile.less - -For more help, run `plessc --help` - diff --git a/vendor/marcusschwarz/lesserphp/composer.json b/vendor/marcusschwarz/lesserphp/composer.json deleted file mode 100644 index d1246cb70..000000000 --- a/vendor/marcusschwarz/lesserphp/composer.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "marcusschwarz/lesserphp", - "type": "library", - "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", - "homepage": "https://www.maswaba.de/lesserphpdocs/", - "license": [ - "MIT", - "GPL-3.0" - ], - "authors": [ - { - "name": "Leaf Corcoran", - "email": "leafot@gmail.com", - "homepage": "http://leafo.net" - }, - { - "name": "Marcus Schwarz", - "email": "github@maswaba.de", - "homepage": "https://www.maswaba.de" - } - - ], - "bin": ["plessc"], - "autoload": { - "classmap": ["lessc.inc.php"] - }, - "require": { - "php": "^7.2|^7.3|^7.4|^8.0" - }, - "require-dev": { - "phpunit/phpunit": "^8.0|^9.0" - }, - "scripts": { - "test": "phpunit" - } -} diff --git a/vendor/marcusschwarz/lesserphp/lessc.inc.php b/vendor/marcusschwarz/lesserphp/lessc.inc.php deleted file mode 100644 index 304af35fc..000000000 --- a/vendor/marcusschwarz/lesserphp/lessc.inc.php +++ /dev/null @@ -1,4021 +0,0 @@ - - * Copyright 2016, Marcus Schwarz - * Licensed under MIT or GPLv3, see LICENSE - */ - - -/** - * The LESS compiler and parser. - * - * Converting LESS to CSS is a three stage process. The incoming file is parsed - * by `lessc_parser` into a syntax tree, then it is compiled into another tree - * representing the CSS structure by `lessc`. The CSS tree is fed into a - * formatter, like `lessc_formatter` which then outputs CSS as a string. - * - * During the first compile, all values are *reduced*, which means that their - * types are brought to the lowest form before being dump as strings. This - * handles math equations, variable dereferences, and the like. - * - * The `parse` function of `lessc` is the entry point. - * - * In summary: - * - * The `lessc` class creates an instance of the parser, feeds it LESS code, - * then transforms the resulting tree to a CSS tree. This class also holds the - * evaluation context, such as all available mixins and variables at any given - * time. - * - * The `lessc_parser` class is only concerned with parsing its input. - * - * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, - * handling things like indentation. - */ -class lessc { - static public $VERSION = "v0.6.0"; - - static public $TRUE = array("keyword", "true"); - static public $FALSE = array("keyword", "false"); - - protected $libFunctions = array(); - protected $registeredVars = array(); - protected $preserveComments = false; - - public $vPrefix = '@'; // prefix of abstract properties - public $mPrefix = '$'; // prefix of abstract blocks - public $parentSelector = '&'; - - static public $lengths = array( "px", "m", "cm", "mm", "in", "pt", "pc" ); - static public $times = array( "s", "ms" ); - static public $angles = array( "rad", "deg", "grad", "turn" ); - - static public $lengths_to_base = array( 1, 3779.52755906, 37.79527559, 3.77952756, 96, 1.33333333, 16 ); - public $importDisabled = false; - public $importDir = array(); - - protected $numberPrecision = null; - - 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; - - 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"]; - - [$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: - $block->parser->throwError("unknown block type: $block->type\n", $block->count); - } - } - - 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($vars, $imports, $vars), $other); - } else { - return array_merge($vars, $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)) { - [, $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 !== false && $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': - [, $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': - [, $child] = $prop; - $this->compileBlock($child); - break; - case 'ruleset': - case 'mixin': - [, $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) { - $block->parser->throwError("{$prop[1][0]} is undefined", $block->count); - } - - if(strpos($prop[1][0], "$") === 0) { - //Use Ruleset Logic - Only last element - $mixins = array(array_pop($mixins)); - } - - 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": - [, $name, $value] = $prop; - $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; - break; - case "comment": - $out->lines[] = $prop[1]; - break; - case "import"; - [, $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": - [,$importId] = $prop; - $import = $this->env->imports[$importId]; - if ($import[0] === false) { - if (isset($import[1])) { - $out->lines[] = $import[1]; - } - } else { - [, $bottom, $parser, $importDir] = $import; - $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); - } - - break; - default: - $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count); - } - } - - - /** - * 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': - [, $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) - [, $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 - [, $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': - [, $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) { - [$base, $exp] = $this->assertArgs($args, 2, "pow"); - return array( "number", pow($this->assertNumber($base), $this->assertNumber($exp)), $args[2][0][2] ); - } - - protected function lib_pi() { - return pi(); - } - - protected function lib_mod($args) { - [$a, $b] = $this->assertArgs($args, 2, "mod"); - return array( "number", $this->assertNumber($a) % $this->assertNumber($b), $args[2][0][2] ); - } - - protected function lib_convert($args) { - [$value, $to] = $this->assertArgs($args, 2, "convert"); - - // If it's a keyword, grab the string version instead - if( is_array( $to ) && $to[0] == "keyword" ) - $to = $to[1]; - - return $this->convert( $value, $to ); - } - - protected function lib_abs($num) { - return array( "number", abs($this->assertNumber($num)), $num[2] ); - } - - protected function lib_min($args) { - $values = $this->assertMinArgs($args, 1, "min"); - - $first_format = $values[0][2]; - - $min_index = 0; - $min_value = $values[0][1]; - - for( $a = 0; $a < sizeof( $values ); $a++ ) - { - $converted = $this->convert( $values[$a], $first_format ); - - if( $converted[1] < $min_value ) - { - $min_index = $a; - $min_value = $values[$a][1]; - } - } - - return $values[ $min_index ]; - } - - protected function lib_max($args) { - $values = $this->assertMinArgs($args, 1, "max"); - - $first_format = $values[0][2]; - - $max_index = 0; - $max_value = $values[0][1]; - - for( $a = 0; $a < sizeof( $values ); $a++ ) - { - $converted = $this->convert( $values[$a], $first_format ); - - if( $converted[1] > $max_value ) - { - $max_index = $a; - $max_value = $values[$a][1]; - } - } - - return $values[ $max_index ]; - } - - protected function lib_tan($num) { - return tan($this->assertNumber($num)); - } - - 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, $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") { - [$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); - } - [$color, $delta] = $args[2]; - $color = $this->assertColor($color); - $delta = floatval($delta[1]); - - return array($color, $delta); - } - - protected function lib_darken($args) { - [$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) { - [$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) { - [$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) { - [$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) { - [$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) { - [$color, $delta] = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); - return $color; - } - - protected function lib_fadein($args) { - [$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) { - [$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, "%"); - } - - // 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)"); - - [$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; - if( isset($args[2][3]) ) { - if( isset($args[2][3][2]) && $args[2][3][2] == '%' ) { - $args[2][3][1] /= 100; - unset($args[2][3][2]); - } - $threshold = $this->assertNumber($args[2][3]); - } - } - 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->lib_luma($darkColor) > $this->lib_luma($lightColor) ) { - $t = $lightColor; - $lightColor = $darkColor; - $darkColor = $t; - } - - $inputColor_alpha = $this->lib_alpha($inputColor); - if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) { - return $lightColor; - } - return $darkColor; - } - - protected function lib_luma($color) { - $color = $this->coerceColor($color); - return (0.2126 * $color[1] / 255) + (0.7152 * $color[2] / 255) + (0.0722 * $color[3] / 255); - } - - - 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; - } - } - - public function assertMinArgs($value, $expectedMinArgs, $name="") { - if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); - $values = $value[2]; - $numValues = count($values); - if ($expectedMinArgs > $numValues) { - if ($name) { - $name = $name . ": "; - } - - $this->throwError("${name}expecting at least $expectedMinArgs arguments, got $numValues"); - } - - return $values; -} - - protected function toHSL($color) { - if ($color[0] == 'hsl') return $color; - - $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": - [,$inner] = $value; - return $this->lib_e($this->reduce($inner)); - case "function": - $color = $this->funcToColor($value); - if ($color) return $color; - - [, $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": - [, $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) { - [, $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; - } - } - - protected function convert( $number, $to ) - { - $value = $this->assertNumber( $number ); - $from = $number[2]; - - // easy out - if( $from == $to ) - return $number; - - // check if the from value is a length - if( ( $from_index = array_search( $from, self::$lengths ) ) !== false ) { - // make sure to value is too - if( in_array( $to, self::$lengths ) ) { - // do the actual conversion - $to_index = array_search( $to, self::$lengths ); - $px = $value * self::$lengths_to_base[ $from_index ]; - $result = $px * ( 1 / self::$lengths_to_base[ $to_index ] ); - - $result = round( $result, 8 ); - return array( "number", $result, $to ); - } - } - - // do the same check for times - if( in_array( $from, self::$times ) ) { - if( in_array( $to, self::$times ) ) { - // currently only ms and s are valid - if( $to == "ms" ) - $result = $value * 1000; - else - $result = $value / 1000; - - $result = round( $result, 8 ); - return array( "number", $result, $to ); - } - } - - // lastly check for an angle - if( in_array( $from, self::$angles ) ) { - // convert whatever angle it is into degrees - if( $from == "rad" ) - $deg = rad2deg( $value ); - - else if( $from == "turn" ) - $deg = $value * 360; - - else if( $from == "grad" ) - $deg = $value / (400 / 360); - - else - $deg = $value; - - // Then convert it from degrees into desired unit - if( $to == "deg" ) - $result = $deg; - - if( $to == "rad" ) - $result = deg2rad( $deg ); - - if( $to == "turn" ) - $result = $value / 360; - - if( $to == "grad" ) - $result = $value * (400 / 360); - - $result = round( $result, 8 ); - return array( "number", $result, $to ); - } - - // we don't know how to convert these - $this->throwError( "Cannot convert {$from} to {$to}" ); - } - - // make sure a color's components don't go out of bounds - protected function fixColor($c) { - 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; - - // track scope to evaluate - $scope_secondary = array(); - - $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]; - // has secondary scope? - if (isset($current->storeParent)) - $scope_secondary[] = $current->storeParent; - - if (isset($current->parent)) - $current = $current->parent; - else - $current = null; - } - - while (count($scope_secondary)) { - // pop one off - $current = array_shift($scope_secondary); - while ($current) { - if ($isArguments && isset($current->arguments)) { - return array('list', ' ', $current->arguments); - } - - if (isset($current->store[$name])) { - return $current->store[$name]; - } - - // has secondary scope? - if (isset($current->storeParent)) { - $scope_secondary[] = $current->storeParent; - } - - if (isset($current->parent)) { - $current = $current->parent; - } else { - $current = null; - } - } - } - - $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->allParsedFiles = array(); - - $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; - } - - /** - * Based on explicit input/output files does a full change check on cache before compiling. - * - * @param string $in - * @param string $out - * @param boolean $force - * @return string Compiled CSS results - * @throws Exception - */ - public function checkedCachedCompile($in, $out, $force = false) { - if (!is_file($in) || !is_readable($in)) { - throw new Exception('Invalid or unreadable input file specified.'); - } - if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) { - throw new Exception('Invalid or unwritable output file specified.'); - } - - $outMeta = $out . '.meta'; - $metadata = null; - if (!$force && is_file($outMeta)) { - $metadata = unserialize(file_get_contents($outMeta)); - } - - $output = $this->cachedCompile($metadata ? $metadata : $in); - - if (!$metadata || $metadata['updated'] != $output['updated']) { - $css = $output['compiled']; - unset($output['compiled']); - file_put_contents($out, $css); - file_put_contents($outMeta, serialize($output)); - } else { - $css = file_get_contents($out); - } - - return $css; - } - - // 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; - } - } elseif ($this->literal(":", true)) { - //Ruleset Definition - if (($this->openString("{", $dirValue, null, array(";")) || true) && - $this->literal("{")) - { - $dir = $this->pushBlock($this->fixTags(array("@".$dirName))); - $dir->name = $dirName; - if (isset($dirValue)) $dir->value = $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->stringValue($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->stringValue($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->stringValue($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 stringValue(&$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->stringValue($str)) { - // escape parent selector, (yuck) - foreach ($str[2] as &$chunk) { - if (is_string($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(); - - // add a reference to the parser so - // we can access the parser to throw errors - // or retrieve the sourceName of this block. - $b->parser = $this; - - // so we know the position of this block - $b->count = $this->count; - - $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--; - } - } -} - -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 ""; - } -} - -class lessc_formatter_lessjs extends lessc_formatter_classic { - public $disableSingle = true; - public $breakSelectors = true; - public $assignSeparator = ": "; - public $selectorSeparator = ","; -} - - diff --git a/vendor/marcusschwarz/lesserphp/.gitignore b/vendor/splitbrain/lesserphp/.gitignore similarity index 100% rename from vendor/marcusschwarz/lesserphp/.gitignore rename to vendor/splitbrain/lesserphp/.gitignore diff --git a/vendor/splitbrain/lesserphp/.phpcs.xml b/vendor/splitbrain/lesserphp/.phpcs.xml new file mode 100644 index 000000000..cafd3c5d3 --- /dev/null +++ b/vendor/splitbrain/lesserphp/.phpcs.xml @@ -0,0 +1,13 @@ + + + + + + + + + . + + + vendor + diff --git a/vendor/marcusschwarz/lesserphp/HISTORY.md b/vendor/splitbrain/lesserphp/HISTORY.md similarity index 90% rename from vendor/marcusschwarz/lesserphp/HISTORY.md rename to vendor/splitbrain/lesserphp/HISTORY.md index aa2f8820e..e77961ff0 100644 --- a/vendor/marcusschwarz/lesserphp/HISTORY.md +++ b/vendor/splitbrain/lesserphp/HISTORY.md @@ -1,12 +1,16 @@ -# lesserphp v0.6.0 +# lesserphp (reloaded) Originally written by Leaf Corcoran, obviously abandoned circa 2014 https://github.com/leafo/lessphp Last version provided by Leaf was 0.5.0 +### No Version yet +* 2024-01-27: Refactoring based on 0.6-dev branch and some cherry-picks from other repos + ### v.0.6.0 * 2021-03-10: adds php8-support and drops php5.x-support, pushing min requirement to php7.2 + (with work from @stefanvandekaa) ### v.0.5.5 * 2021-03-10: More PHP 7.4 support for 0.5-dev (@phy25 and @ArtemGoutsoul) diff --git a/vendor/marcusschwarz/lesserphp/LICENSE b/vendor/splitbrain/lesserphp/LICENSE similarity index 100% rename from vendor/marcusschwarz/lesserphp/LICENSE rename to vendor/splitbrain/lesserphp/LICENSE diff --git a/vendor/splitbrain/lesserphp/README.md b/vendor/splitbrain/lesserphp/README.md new file mode 100644 index 000000000..03c978b1c --- /dev/null +++ b/vendor/splitbrain/lesserphp/README.md @@ -0,0 +1,64 @@ +# LesserPHP (reloaded) + +LesserPHP is a compiler for LESS written in PHP. It is based on `lessphp` by [@leafo](https://github.com/leafo/lessphp). The original has been abandoned in 2014. The fork by [@MarcusSchwarz](https://github.com/MarcusSchwarz/lesserphp) has been mostly abandoned in 2021. There are other forks with dubious status. + +This is an opinionated fork with the goal to modernize the code base enough to be somewhat easier to maintain without completely rewriting it. It is meant to be used as a stable base for [DokuWiki](https://www.dokuwiki.org). This means features not needed for this goal are removed. + +Please note that this fork is based on the `0.6.0-dev` branch of `MarcusSchwarz/lesserphp`, not the much modernized `master` branch. This has two reasons: + +1. The `master` was not up-to-date with all the bug fixes in the `0.6.0-dev` branch (some of which had been contributed by DokuWiki developers) +2. I simply only noticed the considerable refactoring Marcus had done in the `master` branch after I had already started my own refactoring. I did not want to start over again. His approach is much more radical than mine and probably took more than the long weekend I had available for this. + +## Contributing and Bugs + +Please report bugs to the [issue tracker](https://github.com/splitbrain/lesserphp/issues). Fixes are only likely when DokuWiki needs them, or you provide a pull request. + +Feature Requests will be ignored unless accompanied by a pull request. + +## How to use in your PHP project + +Don't. You really wouldn't want to start a new project using LESS. It simply seems that SASS has won the battle. Or maybe even skip the whole CSS preprocessor thing - modern CSS is quite powerful on its own. + +If you are already using `lessphp` in one of it's many forks, using this one isn't too different. + +You can still look at the [original documentation](https://leafo.net/lessphp/docs/) for the most part. The API is mostly the same. Refer to the [upstream documentation](https://lesscss.org/features/) the [bundled Documentation](docs/docs.md) for the LESS syntax itself. Keep in mind that some more modern features are not supported by LesserPHP. + +To install it, use composer: + +```bash +composer require splitbrain/lesserphp +``` + +The typical flow of LesserPHP is to create a new instance of `Lessc`, +configure it how you like, then tell it to compile something using one built in +compile methods. + +The `compile` method compiles a string of LESS code to CSS. + +```php +compile(".block { padding: 3 + 4px }"); +``` + +The `compileFile` method reads and compiles a file. It will either return the +result or write it to the path specified by an optional second argument. + +```php +compileFile("input.less"); +``` + + +If there's any problem compiling your code, an exception is thrown with a helpful message: + +```php +compile("invalid LESS } {"); +} catch (LesserPHP\ParserException $e) { + echo $e->getMessage(); +} +``` diff --git a/vendor/splitbrain/lesserphp/composer.json b/vendor/splitbrain/lesserphp/composer.json new file mode 100644 index 000000000..27acc6ca0 --- /dev/null +++ b/vendor/splitbrain/lesserphp/composer.json @@ -0,0 +1,49 @@ +{ + "name": "splitbrain/lesserphp", + "type": "library", + "description": "lesserphp is a compiler for LESS written in PHP based on leafo's lessphp.", + "license": [ + "MIT", + "GPL-3.0" + ], + "authors": [ + { + "name": "Leaf Corcoran", + "email": "leafot@gmail.com", + "homepage": "http://leafo.net" + }, + { + "name": "Marcus Schwarz", + "email": "github@maswaba.de", + "homepage": "https://www.maswaba.de" + }, + { + "name": "Andreas Gohr", + "email": "andi@splitbrain.org", + "homepage": "https://www.splitbrain.org" + } + ], + "autoload": { + "psr-4": { + "LesserPHP\\": "src", + "LesserPHP\\tests\\": "tests" + } + }, + "require": { + "php": ">=7.4" + }, + "suggest": { + "ext-fileinfo": "For mime type guessing of embedded files" + }, + "require-dev": { + "phpunit/phpunit": "^8.5", + "squizlabs/php_codesniffer": "^3.8", + "rector/rector": "^0.19" + }, + "scripts": { + "test": "phpunit --verbose", + "check": "phpcs -p -s", + "fix": "phpcbf", + "rector": "rector process" + } +} diff --git a/vendor/splitbrain/lesserphp/rector.php b/vendor/splitbrain/lesserphp/rector.php new file mode 100644 index 000000000..c4a07e25e --- /dev/null +++ b/vendor/splitbrain/lesserphp/rector.php @@ -0,0 +1,17 @@ +paths([ + //__DIR__ . '/tests', + __DIR__ . '/src', + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_74 + ]); +}; diff --git a/vendor/splitbrain/lesserphp/src/Constants.php b/vendor/splitbrain/lesserphp/src/Constants.php new file mode 100644 index 000000000..1633c48ee --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Constants.php @@ -0,0 +1,181 @@ + "r,g,b"] */ + public const CSS_COLORS = [ + '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' + ]; +} diff --git a/vendor/splitbrain/lesserphp/src/FormatterClassic.php b/vendor/splitbrain/lesserphp/src/FormatterClassic.php new file mode 100644 index 000000000..84bb212e0 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/FormatterClassic.php @@ -0,0 +1,114 @@ + + * Copyright 2016, Marcus Schwarz + * Licensed under MIT or GPLv3, see LICENSE + */ + + +namespace LesserPHP; + +class FormatterClassic +{ + 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; + protected int $indentLevel; + + 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--; + } + } +} diff --git a/vendor/splitbrain/lesserphp/src/FormatterCompressed.php b/vendor/splitbrain/lesserphp/src/FormatterCompressed.php new file mode 100644 index 000000000..acb6de81b --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/FormatterCompressed.php @@ -0,0 +1,28 @@ + + * Copyright 2016, Marcus Schwarz + * Licensed under MIT or GPLv3, see LICENSE + */ + + +namespace LesserPHP; + +class FormatterCompressed extends FormatterClassic +{ + public $disableSingle = true; + public $open = '{'; + public $selectorSeparator = ','; + public $assignSeparator = ':'; + public $break = ''; + public $compressColors = true; + + public function indentStr($n = 0) + { + return ''; + } +} diff --git a/vendor/splitbrain/lesserphp/src/FormatterLessJs.php b/vendor/splitbrain/lesserphp/src/FormatterLessJs.php new file mode 100644 index 000000000..7afa9086d --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/FormatterLessJs.php @@ -0,0 +1,21 @@ + + * Copyright 2016, Marcus Schwarz + * Licensed under MIT or GPLv3, see LICENSE + */ + + +namespace LesserPHP; + +class FormatterLessJs extends FormatterClassic +{ + public $disableSingle = true; + public $breakSelectors = true; + public $assignSeparator = ': '; + public $selectorSeparator = ','; +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/AbstractFunctionCollection.php b/vendor/splitbrain/lesserphp/src/Functions/AbstractFunctionCollection.php new file mode 100644 index 000000000..87cc5c320 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/AbstractFunctionCollection.php @@ -0,0 +1,25 @@ +lessc = $lessc; + } + + /** + * Get the functions provided by this collection + * + * @return array [name => callable] + */ + abstract public function getFunctions(): array; +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/ColorChannels.php b/vendor/splitbrain/lesserphp/src/Functions/ColorChannels.php new file mode 100644 index 000000000..40af7e059 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/ColorChannels.php @@ -0,0 +1,132 @@ + [$this, 'hue'], + 'saturation' => [$this, 'saturation'], + 'lightness' => [$this, 'lightness'], + //'hsvhue' => [$this, 'hsvhue'], + //'hsvsaturation' => [$this, 'hsvsaturation'], + //'hsvvalue' => [$this, 'hsvvalue'], + 'red' => [$this, 'red'], + 'green' => [$this, 'green'], + 'blue' => [$this, 'blue'], + 'alpha' => [$this, 'alpha'], + 'luma' => [$this, 'luma'], + //'luminance' => [$this, 'luminance'], + ]; + } + + /** + * Extracts the hue channel of a color object in the HSL color space + * + * @link https://lesscss.org/functions/#color-channel-hue + * @throws Exception + */ + public function hue(array $color): int + { + $hsl = Color::toHSL(Asserts::assertColor($color)); + return round($hsl[1]); + } + + /** + * Extracts the saturation channel of a color object in the HSL color space + * + * @link https://lesscss.org/functions/#color-channel-saturation + * @throws Exception + */ + public function saturation(array $color): int + { + $hsl = Color::toHSL(Asserts::assertColor($color)); + return round($hsl[2]); + } + + /** + * Extracts the lightness channel of a color object in the HSL color space + * + * @link https://lesscss.org/functions/#color-channel-lightness + * @throws Exception + */ + public function lightness(array $color): int + { + $hsl = Color::toHSL(Asserts::assertColor($color)); + return round($hsl[3]); + } + + // hsvhue is missing + + // hsvsaturation is missing + + // hsvvalue is missing + + /** + * @throws Exception + */ + public function red($color) + { + $color = Asserts::assertColor($color); + return $color[1]; + } + + /** + * @throws Exception + */ + public function green($color) + { + $color = Asserts::assertColor($color); + return $color[2]; + } + + /** + * @throws Exception + */ + public function blue($color) + { + $color = Asserts::assertColor($color); + return $color[3]; + } + + /** + * Extracts the alpha channel of a color object + * + * defaults to 1 for colors without an alpha + * @fixme non-colors return null - should they? + * @link https://lesscss.org/functions/#color-channel-alpha + */ + public function alpha(array $value): ?float + { + if (!is_null($color = Color::coerceColor($value))) { + return $color[4] ?? 1; + } + return null; + } + + /** + * Calculates the luma (perceptual brightness) of a color object + * + * @link https://lesscss.org/functions/#color-channel-luma + * @throws Exception + */ + public function luma(array $color): array + { + $color = Asserts::assertColor($color); + return ['number', round(Color::toLuma($color) * 100, 8), '%']; + } + + // luminance is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/ColorDefinition.php b/vendor/splitbrain/lesserphp/src/Functions/ColorDefinition.php new file mode 100644 index 000000000..c22f50ba8 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/ColorDefinition.php @@ -0,0 +1,71 @@ + [$this, 'rgb'], + //'rgba' => [$this, 'rgba'], + 'rgbahex' => [$this, 'rgbahex'], + 'argb' => [$this, 'argb'], + //'hsl' => [$this, 'hsl'], + //'hsla' => [$this, 'hsla'], + //'hsv' => [$this, 'hsv'], + //'hsva' => [$this, 'hsva'], + ]; + } + + // rgb is missing + // rgba is missing + + /** + * Creates a hex representation of a color in #AARRGGBB format (NOT #RRGGBBAA!) + * + * This method does not exist in the official less.js implementation + * @see lib_argb + * @throws Exception + */ + public function rgbahex(array $color): string + { + $color = Asserts::assertColor($color); + + return sprintf( + '#%02x%02x%02x%02x', + isset($color[4]) ? $color[4] * 255 : 255, + $color[1], + $color[2], + $color[3] + ); + } + + /** + * Creates a hex representation of a color in #AARRGGBB format (NOT #RRGGBBAA!) + * + * @https://lesscss.org/functions/#color-definition-argb + * @throws Exception + */ + public function argb(array $color): string + { + return $this->rgbahex($color); + } + + // hsl is missing + + // hsla is missing + + // hsv is missing + + // hsva is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/ColorOperation.php b/vendor/splitbrain/lesserphp/src/Functions/ColorOperation.php new file mode 100644 index 000000000..908eddc90 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/ColorOperation.php @@ -0,0 +1,315 @@ + [$this, 'saturate'], + 'desaturate' => [$this, 'desaturate'], + 'lighten' => [$this, 'lighten'], + 'darken' => [$this, 'darken'], + 'fadein' => [$this, 'fadein'], + 'fadeout' => [$this, 'fadeout'], + 'fade' => [$this, 'fade'], + 'spin' => [$this, 'spin'], + 'mix' => [$this, 'mix'], + 'tint' => [$this, 'tint'], + 'shade' => [$this, 'shade'], + //'greyscale' => [$this, 'greyscale'], + 'contrast' => [$this, 'contrast'], + ]; + } + + + /** + * Increase the saturation of a color in the HSL color space by an absolute amount + * + * @link https://lesscss.org/functions/#color-operations-saturate + * @throws Exception + */ + public function saturate(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + + $hsl = Color::toHSL($color); + $hsl[2] = Util::clamp($hsl[2] + $delta, 100); + return Color::toRGB($hsl); + } + + /** + * Decrease the saturation of a color in the HSL color space by an absolute amount + * + * @link https://lesscss.org/functions/#color-operations-desaturate + * @throws Exception + */ + public function desaturate(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + + $hsl = Color::toHSL($color); + $hsl[2] = Util::clamp($hsl[2] - $delta, 100); + return Color::toRGB($hsl); + } + + /** + * Increase the lightness of a color in the HSL color space by an absolute amount + * + * @link https://lesscss.org/functions/#color-operations-lighten + * @throws Exception + */ + public function lighten(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + + $hsl = Color::toHSL($color); + $hsl[3] = Util::clamp($hsl[3] + $delta, 100); + return Color::toRGB($hsl); + } + + /** + * Decrease the lightness of a color in the HSL color space by an absolute amount + * + * @link https://lesscss.org/functions/#color-operations-darken + * @throws Exception + */ + public function darken(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + + $hsl = Color::toHSL($color); + $hsl[3] = Util::clamp($hsl[3] - $delta, 100); + return Color::toRGB($hsl); + } + + /** + * Decrease the transparency (or increase the opacity) of a color, making it more opaque + * + * @link https://lesscss.org/functions/#color-operations-fadein + * @throws Exception + */ + public function fadein(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + $color[4] = Util::clamp(($color[4] ?? 1) + $delta / 100); + return $color; + } + + /** + * Increase the transparency (or decrease the opacity) of a color, making it less opaque + * + * @link https://lesscss.org/functions/#color-operations-fadeout + * @throws Exception + */ + public function fadeout(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + $color[4] = Util::clamp(($color[4] ?? 1) - $delta / 100); + return $color; + } + + /** + * Set the absolute opacity of a color. + * Can be applied to colors whether they already have an opacity value or not. + * + * @link https://lesscss.org/functions/#color-operations-fade + * @throws Exception + */ + public function fade(array $args): array + { + [$color, $alpha] = $this->colorArgs($args); + $color[4] = Util::clamp($alpha / 100.0); + return $color; + } + + /** + * Rotate the hue angle of a color in either direction + * + * @link https://lesscss.org/functions/#color-operations-spin + * @throws Exception + */ + public function spin(array $args): array + { + [$color, $delta] = $this->colorArgs($args); + + $hsl = Color::toHSL($color); + + $hsl[1] = $hsl[1] + $delta % 360; + if ($hsl[1] < 0) $hsl[1] += 360; + + return Color::toRGB($hsl); + } + + /** + * mixes two colors by weight + * mix(@color1, @color2, [@weight: 50%]); + * + * @link https://lesscss.org/functions/#color-operations-mix + * @throws Exception + */ + public function mix(array $args): array + { + if ($args[0] != 'list' || count($args[2]) < 2) { + throw new Exception('mix expects (color1, color2, weight)'); + } + + [$first, $second] = $args[2]; + $first = Asserts::assertColor($first); + $second = Asserts::assertColor($second); + + $first_a = $this->alpha($first); + $second_a = $this->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 = [ + '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 Color::fixColor($new); + } + + /** + * Mix color with white in variable proportion. + * + * It is the same as calling `mix(#ffffff, @color, @weight)`. + * + * tint(@color, [@weight: 50%]); + * + * @link https://lesscss.org/functions/#color-operations-tint + * @throws Exception + * @return array Color + */ + public function tint(array $args): array + { + $white = ['color', 255, 255, 255]; + if ($args[0] == 'color') { + return $this->mix(['list', ',', [$white, $args]]); + } elseif ($args[0] == 'list' && count($args[2]) == 2) { + return $this->mix([$args[0], $args[1], [$white, $args[2][0], $args[2][1]]]); + } else { + throw new Exception('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%]); + * + * @link http://lesscss.org/functions/#color-operations-shade + * @return array Color + * @throws Exception + */ + public function shade(array $args): array + { + $black = ['color', 0, 0, 0]; + if ($args[0] == 'color') { + return $this->mix(['list', ',', [$black, $args]]); + } elseif ($args[0] == 'list' && count($args[2]) == 2) { + return $this->mix([$args[0], $args[1], [$black, $args[2][0], $args[2][1]]]); + } else { + throw new Exception('shade expects (color, weight)'); + } + } + + // greyscale is missing + + /** + * Choose which of two colors provides the greatest contrast with another + * + * @link https://lesscss.org/functions/#color-operations-contrast + * @throws Exception + */ + public function contrast(array $args): array + { + $darkColor = ['color', 0, 0, 0]; + $lightColor = ['color', 255, 255, 255]; + $threshold = 0.43; + + if ($args[0] == 'list') { + $inputColor = (isset($args[2][0])) ? Asserts::assertColor($args[2][0]) : $lightColor; + $darkColor = (isset($args[2][1])) ? Asserts::assertColor($args[2][1]) : $darkColor; + $lightColor = (isset($args[2][2])) ? Asserts::assertColor($args[2][2]) : $lightColor; + if (isset($args[2][3])) { + if (isset($args[2][3][2]) && $args[2][3][2] == '%') { + $args[2][3][1] /= 100; + unset($args[2][3][2]); + } + $threshold = Asserts::assertNumber($args[2][3]); + } + } else { + $inputColor = Asserts::assertColor($args); + } + + $inputColor = Color::coerceColor($inputColor); + $darkColor = Color::coerceColor($darkColor); + $lightColor = Color::coerceColor($lightColor); + + //Figure out which is actually light and dark! + if (Color::toLuma($darkColor) > Color::toLuma($lightColor)) { + $t = $lightColor; + $lightColor = $darkColor; + $darkColor = $t; + } + + $inputColor_alpha = $this->alpha($inputColor); + if ((Color::toLuma($inputColor) * $inputColor_alpha) < $threshold) { + return $lightColor; + } + return $darkColor; + } + + + /** + * Helper function to get arguments for color manipulation functions. + * takes a list that contains a color like thing and a percentage + * + * @fixme explanation needs to be improved + * @throws Exception + */ + protected function colorArgs(array $args): array + { + if ($args[0] != 'list' || count($args[2]) < 2) { + return [['color', 0, 0, 0], 0]; + } + [$color, $delta] = $args[2]; + $color = Asserts::assertColor($color); + $delta = floatval($delta[1]); + + return [$color, $delta]; + } +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/Lists.php b/vendor/splitbrain/lesserphp/src/Functions/Lists.php new file mode 100644 index 000000000..e302de995 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/Lists.php @@ -0,0 +1,50 @@ + [$this, 'length'], + 'extract' => [$this, 'extract'], + //'range' => [$this, 'range'], + //'each' => [$this, 'each'], + ]; + } + + // length is missing + + /** + * Returns the value at a specified position in a list + * + * @link https://lesscss.org/functions/#list-functions-extract + * @throws Exception + */ + public function extract(array $value) + { + [$list, $idx] = Asserts::assertArgs($value, 2, 'extract'); + $idx = Asserts::assertNumber($idx); + // 1 indexed + if ($list[0] == 'list' && isset($list[2][$idx - 1])) { + return $list[2][$idx - 1]; + } + + // FIXME what is the expected behavior here? Apparently it's not an error? + } + + // range is missing + + // each is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/Math.php b/vendor/splitbrain/lesserphp/src/Functions/Math.php new file mode 100644 index 000000000..d156e9956 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/Math.php @@ -0,0 +1,274 @@ + [$this, 'ceil'], + 'floor' => [$this, 'floor'], + 'percentage' => [$this, 'percentage'], + 'round' => [$this, 'round'], + 'sqrt' => [$this, 'sqrt'], + 'abs' => [$this, 'abs'], + 'sin' => [$this, 'sin'], + 'asin' => [$this, 'asin'], + 'cos' => [$this, 'cos'], + 'acos' => [$this, 'acos'], + 'tan' => [$this, 'tan'], + 'atan' => [$this, 'atan'], + 'pi' => [$this, 'pi'], + 'pow' => [$this, 'pow'], + 'mod' => [$this, 'mod'], + 'min' => [$this, 'min'], + 'max' => [$this, 'max'], + ]; + } + + + /** + * Rounds up to the next highest integer + * + * @link https://lesscss.org/functions/#math-functions-ceil + * @throws Exception + */ + public function ceil(array $arg): array + { + $value = Asserts::assertNumber($arg); + return ['number', ceil($value), $arg[2]]; + } + + /** + * Rounds down to the next lowest integer + * + * @link https://lesscss.org/functions/#math-functions-floor + * @throws Exception + */ + public function floor(array $arg): array + { + $value = Asserts::assertNumber($arg); + return ['number', floor($value), $arg[2]]; + } + + /** + * Converts a floating point number into a percentage string + * + * @link https://lesscss.org/functions/#math-functions-percentage + * @throws Exception + */ + public function percentage(array $arg): array + { + $num = Asserts::assertNumber($arg); + return ['number', $num * 100, '%']; + } + + /** + * Applies rounding + * + * @link https://lesscss.org/functions/#math-functions-round + * @throws Exception + */ + public function round(array $arg): array + { + if ($arg[0] != 'list') { + $value = Asserts::assertNumber($arg); + return ['number', round($value), $arg[2]]; + } else { + $value = Asserts::assertNumber($arg[2][0]); + $precision = Asserts::assertNumber($arg[2][1]); + return ['number', round($value, $precision), $arg[2][0][2]]; + } + } + + /** + * Calculates square root of a number + * + * @link https://lesscss.org/functions/#math-functions-sqrt + * @throws Exception + */ + public function sqrt(array $num): float + { + return sqrt(Asserts::assertNumber($num)); + } + + /** + * Calculates absolute value of a number. Keeps units as they are. + * + * @link https://lesscss.org/functions/#math-functions-abs + * @throws Exception + */ + public function abs(array $num): array + { + return ['number', abs(Asserts::assertNumber($num)), $num[2]]; + } + + /** + * Calculates sine function + * + * @link https://lesscss.org/functions/#math-functions-sin + * @throws Exception + */ + public function sin(array $num): float + { + return sin(Asserts::assertNumber($num)); + } + + /** + * Calculates arcsine function + * + * @link https://lesscss.org/functions/#math-functions-asin + * @throws Exception + */ + public function asin(array $num): array + { + $num = asin(Asserts::assertNumber($num)); + return ['number', $num, 'rad']; + } + + /** + * Calculates cosine function + * + * @link https://lesscss.org/functions/#math-functions-cos + * @throws Exception + */ + public function cos(array $num): float + { + return cos(Asserts::assertNumber($num)); + } + + /** + * Calculates arccosine function + * + * @link https://lesscss.org/functions/#math-functions-acos + * @throws Exception + */ + public function acos(array $num): array + { + $num = acos(Asserts::assertNumber($num)); + return ['number', $num, 'rad']; + } + + /** + * Calculates tangent function + * + * @link https://lesscss.org/functions/#math-functions-tan + * @throws Exception + */ + public function tan(array $num): float + { + return tan(Asserts::assertNumber($num)); + } + + /** + * Calculates arctangent function + * + * @link https://lesscss.org/functions/#math-functions-atan + * @throws Exception + */ + public function atan(array $num): array + { + $num = atan(Asserts::assertNumber($num)); + return ['number', $num, 'rad']; + } + + /** + * Return the value of pi + * + * @link https://lesscss.org/functions/#math-functions-pi + */ + public function pi(): float + { + return pi(); + } + + /** + * Returns the value of the first argument raised to the power of the second argument. + * + * @link https://lesscss.org/functions/#math-functions-pow + * @throws Exception + */ + public function pow(array $args): array + { + [$base, $exp] = Asserts::assertArgs($args, 2, 'pow'); + return ['number', Asserts::assertNumber($base) ** Asserts::assertNumber($exp), $args[2][0][2]]; + } + + /** + * Returns the value of the first argument modulus second argument. + * + * @link https://lesscss.org/functions/#math-functions-mod + * @throws Exception + */ + public function mod(array $args): array + { + [$a, $b] = Asserts::assertArgs($args, 2, 'mod'); + return ['number', Asserts::assertNumber($a) % Asserts::assertNumber($b), $args[2][0][2]]; + } + + /** + * Returns the lowest of one or more values + * + * @link https://lesscss.org/functions/#math-functions-min + * @throws Exception + */ + public function min(array $args): array + { + $values = Asserts::assertMinArgs($args, 1, 'min'); + + $first_format = $values[0][2]; + + $min_index = 0; + $min_value = $values[0][1]; + + for ($a = 0; $a < sizeof($values); $a++) { + $converted = Util::convert($values[$a], $first_format); + + if ($converted[1] < $min_value) { + $min_index = $a; + $min_value = $values[$a][1]; + } + } + + return $values[$min_index]; + } + + /** + * Returns the highest of one or more values + * + * @link https://lesscss.org/functions/#math-functions-max + * @throws Exception + */ + public function max(array $args): array + { + $values = Asserts::assertMinArgs($args, 1, 'max'); + + $first_format = $values[0][2]; + + $max_index = 0; + $max_value = $values[0][1]; + + for ($a = 0; $a < sizeof($values); $a++) { + $converted = Util::convert($values[$a], $first_format); + + if ($converted[1] > $max_value) { + $max_index = $a; + $max_value = $values[$a][1]; + } + } + + return $values[$max_index]; + } +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/Misc.php b/vendor/splitbrain/lesserphp/src/Functions/Misc.php new file mode 100644 index 000000000..9cf2e3ac2 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/Misc.php @@ -0,0 +1,115 @@ + [$this, 'color'], + //'image-size' => [$this, 'imageSize'], + //'image-width' => [$this, 'imageWidth'], + //'image-height' => [$this, 'imageHeight'], + 'convert' => [$this, 'convert'], + 'data-uri' => [$this, 'dataUri'], + //'default' => [$this, 'default'], + 'unit' => [$this, 'unit'], + //'get-unit' => [$this, 'getUnit'], + //'svg-gradient' => [$this, 'svgGradient'], + ]; + } + + // color is missing + // image-size is missing + // image-width is missing + // image-height is missing + + /** + * Convert a number from one unit into another + * + * @link https://lesscss.org/functions/#misc-functions-convert + * @throws Exception + */ + public function convert(array $args): array + { + [$value, $to] = Asserts::assertArgs($args, 2, 'convert'); + + // If it's a keyword, grab the string version instead + if (is_array($to) && $to[0] == 'keyword') { + $to = $to[1]; + } + + return Util::convert($value, $to); + } + + /** + * 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 + */ + public function dataUri(array $value): string + { + $mime = ($value[0] === 'list') ? $value[2][0][2] : null; + $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; + + $fullpath = $this->lessc->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 . '")'; + } + + // default is missing + + + /** + * Remove or change the unit of a dimension + * + * @link https://lesscss.org/functions/#misc-functions-unit + * @throws Exception + */ + public function unit(array $arg): array + { + if ($arg[0] == 'list') { + [$number, $newUnit] = $arg[2]; + return [ + 'number', + Asserts::assertNumber($number), + $this->lessc->compileValue($this->lessc->unwrap($newUnit)) + ]; + } else { + return ['number', Asserts::assertNumber($arg), '']; + } + } + + // get-unit is missing + // svg-gradient is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/Strings.php b/vendor/splitbrain/lesserphp/src/Functions/Strings.php new file mode 100644 index 000000000..a91160d90 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/Strings.php @@ -0,0 +1,85 @@ + [$this, 'escape'], + 'e' => [$this, 'e'], + '%' => [$this, 'format'], + //'replace' => [$this, 'replace'], + ]; + } + + + // escape is missing + + /** + * String escaping. + * + * It expects string as a parameter and return its content as is, but without quotes. It can be used + * to output CSS value which is either not valid CSS syntax, or uses proprietary syntax which + * Less doesn't recognize. + * + * @link https://lesscss.org/functions/#string-functions-e + * @throws Exception + */ + public function e(array $arg): array + { + return $this->lessc->unwrap($arg); + } + + /** + * Formats a string + * + * @link https://lesscss.org/functions/#string-functions--format + * @throws Exception + */ + public function format(array $args) : array + { + if ($args[0] != 'list') return $args; + $values = $args[2]; + $string = array_shift($values); + $template = $this->lessc->compileValue($this->lessc->unwrap($string)); + + $i = 0; + if (preg_match_all('/%[dsa]/', $template, $m)) { + foreach ($m[0] as $match) { + $val = isset($values[$i]) ? + $this->lessc->reduce($values[$i]) : ['keyword', '']; + + // lessjs compat, renders fully expanded color, not raw color + if ($color = Color::coerceColor($val)) { + $val = $color; + } + + $i++; + $rep = $this->lessc->compileValue($this->lessc->unwrap($val)); + $template = preg_replace( + '/' . Util::pregQuote($match) . '/', + $rep, + $template, + 1 + ); + } + } + + $d = $string[0] == 'string' ? $string[1] : '"'; + return ['string', $d, [$template]]; + } + + // replace is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Functions/Type.php b/vendor/splitbrain/lesserphp/src/Functions/Type.php new file mode 100644 index 000000000..2e229cecd --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Functions/Type.php @@ -0,0 +1,120 @@ + [$this, 'isnumber'], + 'isstring' => [$this, 'isstring'], + 'iscolor' => [$this, 'iscolor'], + 'iskeyword' => [$this, 'iskeyword'], + //'isurl' => [$this, 'isurl'], + 'ispixel' => [$this, 'ispixel'], + 'isem' => [$this, 'isem'], + 'isrem' => [$this, 'isrem'], + 'ispercentage' => [$this, 'ispercentage'], + //'isunit' => [$this, 'isunit'], + //'isruleset' => [$this, 'isruleset'], + //'isdefined' => [$this, 'isdefined'], + ]; + } + + + /** + * Returns true if a value is a number, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-isnumber + */ + public function isnumber(array $value): array + { + return Util::toBool($value[0] == 'number'); + } + + /** + * Returns true if a value is a string, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-isstring + */ + public function isstring(array $value): array + { + return Util::toBool($value[0] == 'string'); + } + + /** + * Returns true if a value is a color, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-iscolor + */ + public function iscolor(array $value): array + { + return Util::toBool(Color::coerceColor($value)); + } + + /** + * Returns true if a value is a keyword, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-iskeyword + */ + public function iskeyword(array $value): array + { + return Util::toBool($value[0] == 'keyword'); + } + + // isurl is missing + + /** + * Returns true if a value is a number in pixels, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-ispixel + */ + public function ispixel(array $value): array + { + return Util::toBool($value[0] == 'number' && $value[2] == 'px'); + } + + /** + * Returns true if a value is an em value, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-isem + */ + public function isem(array $value): array + { + return Util::toBool($value[0] == 'number' && $value[2] == 'em'); + } + + /** + * Returns true if a value is an rem value, false otherwise + * + * This method does not exist in the official less.js implementation + */ + public function isrem(array $value): array + { + return Util::toBool($value[0] == 'number' && $value[2] == 'rem'); + } + + /** + * Returns true if a value is a percentage, false otherwise + * + * @link https://lesscss.org/functions/#type-functions-ispercentage + */ + public function ispercentage(array $value): array + { + return Util::toBool($value[0] == 'number' && $value[2] == '%'); + } + + // isunit is missing + // isruleset is missing + // isdefined is missing +} diff --git a/vendor/splitbrain/lesserphp/src/Lessc.php b/vendor/splitbrain/lesserphp/src/Lessc.php new file mode 100644 index 000000000..9780dfb2e --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Lessc.php @@ -0,0 +1,1746 @@ + + * Copyright 2016, Marcus Schwarz + * Licensed under MIT or GPLv3, see LICENSE + */ + +namespace LesserPHP; + +use Exception; +use LesserPHP\Functions\AbstractFunctionCollection; +use LesserPHP\Utils\Color; +use LesserPHP\Utils\Util; +use stdClass; + +/** + * The LESS compiler and parser. + * + * Converting LESS to CSS is a three stage process. The incoming file is parsed + * by `Parser` into a syntax tree, then it is compiled into another tree + * representing the CSS structure by `Lessc`. The CSS tree is fed into a + * formatter, which then outputs CSS as a string. + * + * During the first compile, all values are *reduced*, which means that their + * types are brought to the lowest form before being dumped as strings. This + * handles math equations, variable dereferences, and the like. + * + * The `compile` function of `Lessc` is the entry point. + * + * In summary: + * + * The `Lessc` class creates an instance of the parser, feeds it LESS code, + * then transforms the resulting tree to a CSS tree. This class also holds the + * evaluation context, such as all available mixins and variables at any given + * time. + * + * The `Parser` class is only concerned with parsing its input. + * + * The `Formatter` takes a CSS tree, and dumps it to a formatted string, + * handling things like indentation. + */ +class Lessc +{ + /** @var string A list of directories used to search for imported files */ + protected $importDir = []; + + /** @var bool Should imports be disabled? */ + protected bool $importDisabled = false; + + /** @var null|int Round numbers to this precision FIXME currently not settable */ + protected ?int $numberPrecision = null; + + /** @var bool Should comments be preserved in the output? */ + protected bool $preserveComments = false; + + /** @var array List of all functions registered to be available in LESS */ + protected array $libFunctions = []; + + /** @var array List of all registered variables */ + protected array $registeredVars = []; + + /** @var FormatterClassic|null The formatter for the output */ + protected ?FormatterClassic $formatter = null; + + /** @var stdClass|null Environment FIXME should probably be its own proper class */ + protected ?stdClass $env = null; + + /** @var stdClass|null The currently parsed block FIXME should probably be its own proper class */ + protected ?stdClass $scope; + + /** @var array [file => mtime] list of all files that have been parsed, to avoid circular imports */ + protected array $allParsedFiles = []; + + /** @var Parser|null The currently used Parser instance. Used when creating error messages */ + protected ?Parser $sourceParser = null; + + /** @var int The position in the current parsing step (in $sourceParser) */ + protected int $sourceLoc = -1; + + /** @var int counter to uniquely identify imports */ + protected static int $nextImportId = 0; + + // region public API + + /** + * Initialize the LESS Parser + */ + public function __construct() + { + $this->registerLibraryFunctions(); + } + + /** + * Compile the given LESS string into CSS + * + * @param string $string LESS code + * @param string|null $name optional filename to show in errors + * @throws Exception + * @throws ParserException + */ + public function compile(string $string, ?string $name = null): string + { + $locale = setlocale(LC_NUMERIC, 0); + setlocale(LC_NUMERIC, 'C'); + + $parser = $this->makeParser($name); + $root = $parser->parse($string); + + $this->env = null; + $this->scope = null; + $this->allParsedFiles = []; + + if ($this->formatter === null) $this->setFormatter(); + + if (!empty($this->registeredVars)) { + $this->injectVariables($this->registeredVars); + } + + $this->sourceParser = $parser; // used for error messages + + try { + $this->compileBlock($root); + } catch (Exception $e) { + setlocale(LC_NUMERIC, $locale); + $position = $this->sourceLoc !== -1 ? $this->sourceLoc : $root->count; + $this->sourceParser->throwError($e->getMessage(), $position, $e); + } + + ob_start(); + $this->formatter->block($this->scope); + $out = ob_get_clean(); + setlocale(LC_NUMERIC, $locale); + return $out; + } + + /** + * Parse the given File and return the compiled CSS. + * + * If an output file is specified, the compiled CSS will be written to that file. + * + * @param string $fname LESS file + * @param string|null $outFname optional output file + * @return int|string number of bytes written to file, or CSS if no output file + * @throws Exception + * @throws ParserException + */ + public function compileFile(string $fname, ?string $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; + } + + // endregion + + // region configuration API + + /** + * Should comments be preserved in the output? + * + * Default is false + * + * @param bool $preserve + */ + public function setPreserveComments(bool $preserve): void + { + $this->preserveComments = $preserve; + } + + /** + * Register a custom function to be available in LESS + * + * @param string $name name of function + * @param callable $func callback + */ + public function registerFunction(string $name, callable $func): void + { + $this->libFunctions[$name] = $func; + } + + /** + * Remove a function from being available in LESS + * + * @param string $name The name of the function to unregister + */ + public function unregisterFunction(string $name): void + { + if (isset($this->libFunctions[$name])) { + unset($this->libFunctions[$name]); + } + } + + /** + * Add additional variables to the parser + * + * Given variables are merged with any already set variables + * + * @param array $variables [name => value, ...] + */ + public function setVariables($variables): void + { + $this->registeredVars = array_merge($this->registeredVars, $variables); + } + + /** + * Get the currently set variables + * + * @return array [name => value, ...] + */ + public function getVariables(): array + { + return $this->registeredVars; + } + + /** + * Remove a currently set variable + * + * @param string $name + */ + public function unsetVariable(string $name): void + { + if (isset($this->registeredVars[$name])) { + unset($this->registeredVars[$name]); + } + } + + /** + * Set the directories to search for imports + * + * Overwrites any previously set directories + * + * @param string|string[] $dirs + */ + public function setImportDir($dirs): void + { + $this->importDir = (array)$dirs; + } + + /** + * Add an additional directory to search for imports + */ + public function addImportDir(string $dir): void + { + $this->importDir = (array)$this->importDir; + $this->importDir[] = $dir; + } + + /** + * Enable or disable import statements + * + * There is usually no need to disable imports + * + * @param bool $enable + * @return void + */ + public function enableImports(bool $enable): void + { + $this->importDisabled = !$enable; + } + + /** + * Set the formatter to use for output + * + * @param FormatterClassic|null $formatter Null for the default LessJs formatter + * @return void + */ + public function setFormatter(?FormatterClassic $formatter = null) + { + if ($formatter === null) { + $formatter = new FormatterLessJs(); + } + + $this->formatter = $formatter; + } + + // endregion + + + /** + * Register all the default functions + */ + protected function registerLibraryFunctions() + { + $files = glob(__DIR__ . '/Functions/*.php'); + foreach ($files as $file) { + $name = basename($file, '.php'); + if (substr($name, 0, 8) == 'Abstract') continue; + $class = '\\LesserPHP\\Functions\\' . $name; + $funcObj = new $class($this); + if ($funcObj instanceof AbstractFunctionCollection) { + foreach ($funcObj->getFunctions() as $name => $callback) { + $this->registerFunction($name, $callback); + } + } + } + } + + /** + * attempts to find the path of an import url, returns null for css files + * + * @internal parser internal method + */ + public function findImport(string $url): ?string + { + 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; + } + + /** + * Check if a given file exists and is actually a file + * + * @param string $name file path + * @return bool + */ + protected function fileExists(string $name): bool + { + return is_file($name); + } + + /** + * @internal parser internal method + */ + public static function compressList($items, $delim) + { + if (!isset($items[1]) && isset($items[0])) return $items[0]; + else return ['list', $delim, $items]; + } + + + /** + * @throws Exception + */ + 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->unwrap($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 [false, '/* import disabled */']; + } + + if (isset($this->allParsedFiles[realpath($realPath)])) { + return [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']; + + [$top, $bottom] = $this->sortProps($root->props, true); + $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); + + return [true, $bottom, $parser, $dir]; + } + + /** + * @throws Exception + */ + 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. + * + * @throws Exception + * @see 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, [$name]); + break; + default: + throw new Exception("unknown block type: $block->type\n"); + } + } + + /** + * @throws Exception + */ + 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(); + } + + /** + * @throws Exception + */ + 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, [$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 = []; + } + } + + $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; + } + + /** + * @throws Exception + */ + 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(); + } + + /** + * @throws Exception + */ + protected function compileRoot($root) + { + $this->pushEnv(); + $this->scope = $this->makeOutputBlock($root->type); + $this->compileProps($root, $this->scope); + $this->popEnv(); + } + + /** + * @throws Exception + */ + 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 + * occurrence are consolidated. + */ + protected function deduplicate($lines) + { + $unique = []; + $comments = []; + + 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 = []; + } + return array_merge($unique, $comments); + } + + protected function sortProps($props, $split = false) + { + $vars = []; + $imports = []; + $other = []; + $stack = []; + + foreach ($props as $prop) { + switch ($prop[0]) { + case 'comment': + $stack[] = $prop; + break; + case 'assign': + $stack[] = $prop; + if (isset($prop[1][0]) && $prop[1][0] == Constants::VPREFIX) { + $vars = array_merge($vars, $stack); + } else { + $other = array_merge($other, $stack); + } + $stack = []; + break; + case 'import': + $id = self::$nextImportId++; + $prop[] = $id; + $stack[] = $prop; + $imports = array_merge($imports, $stack); + $other[] = ['import_mixin', $id]; + $stack = []; + break; + default: + $stack[] = $prop; + $other = array_merge($other, $stack); + $stack = []; + break; + } + } + $other = array_merge($other, $stack); + + if ($split) { + return [array_merge($vars, $imports, $vars), $other]; + } else { + return array_merge($vars, $imports, $vars, $other); + } + } + + /** + * @throws Exception + */ + protected function compileMediaQuery($queries) + { + $compiledQueries = []; + foreach ($queries as $query) { + $parts = []; + 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 = []; + $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): int + { + $parts = explode("$&$", $tag); + $count = 0; + foreach ($parts as &$part) { + $part = str_replace(Constants::PARENT_SELECTOR, $replace, $part, $c); + $count += $c; + } + $tag = implode(Constants::PARENT_SELECTOR, $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 = []; + 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 + * @throws Exception + */ + protected function compileSelectors($selectors) + { + $out = []; + + foreach ($selectors as $s) { + if (is_array($s)) { + [, $value] = $s; + $out[] = trim($this->compileValue($this->reduce($value))); + } else { + $out[] = $s; + } + } + + return $out; + } + + protected function eq($left, $right) + { + return $left == $right; + } + + /** + * @return bool + * @throws Exception + */ + 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) == Constants::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 = []; + 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); + } + } + + /** + * @throws Exception + */ + protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip = []) + { + $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 + * @throws Exception + */ + protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen = []) + { + 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 = []; + 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 + * + * @throws Exception + */ + protected function zipSetArgs($args, $orderedValues, $keywordValues) + { + $assignedValues = []; + + $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 { + throw new Exception('Failed to assign arg ' . $a[1]); + } + + $value = $this->reduce($value); + $this->set($a[1], $value); + $assignedValues[] = $value; + } else { + // a lit + $i++; + } + } + + // check for a rest + $last = end($args); + if ($last !== false && $last[0] === 'rest') { + $rest = array_slice($orderedValues, count($args) - 1); + $this->set($last[1], $this->reduce(['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 + * @throws Exception + */ + protected function compileProp($prop, $block, $out) + { + // set error position context + $this->sourceLoc = $prop[-1] ?? -1; + + switch ($prop[0]) { + case 'assign': + [, $name, $value] = $prop; + if ($name[0] == Constants::VPREFIX) { + $this->set($name, $value); + } else { + $out->lines[] = $this->formatter->property( + $name, + $this->compileValue($this->reduce($value)) + ); + } + break; + case 'block': + [, $child] = $prop; + $this->compileBlock($child); + break; + case 'ruleset': + case 'mixin': + [, $path, $args, $suffix] = $prop; + + $orderedArgs = []; + $keywordArgs = []; + foreach ((array)$args as $arg) { + switch ($arg[0]) { + case 'arg': + if (!isset($arg[2])) { + $orderedArgs[] = $this->reduce(['variable', $arg[1]]); + } else { + $keywordArgs[$arg[1]] = $this->reduce($arg[2]); + } + break; + + case 'lit': + $orderedArgs[] = $this->reduce($arg[1]); + break; + default: + throw new Exception('Unknown arg type: ' . $arg[0]); + } + } + + $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); + + if ($mixins === null) { + $block->parser->throwError("{$prop[1][0]} is undefined", $block->count); + } + + if (strpos($prop[1][0], "$") === 0) { + //Use Ruleset Logic - Only last element + $mixins = [array_pop($mixins)]; + } + + 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] != Constants::VPREFIX) { + $subProp[2] = ['list', ' ', [$subProp[2], ['keyword', $suffix]]]; + } + + $this->compileProp($subProp, $mixin, $out); + } + + $mixin->parent = $oldParent; + + if ($haveArgs) $this->popEnv(); + if ($haveScope) $this->popEnv(); + } + + break; + case 'raw': + case 'comment': + $out->lines[] = $prop[1]; + break; + case 'directive': + [, $name, $value] = $prop; + $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';'; + break; + case 'import': + [, $importPath, $importId] = $prop; + $importPath = $this->reduce($importPath); + + if (!isset($this->env->imports)) { + $this->env->imports = []; + } + + $result = $this->tryImport($importPath, $block, $out); + + $this->env->imports[$importId] = $result === false ? + [false, '@import ' . $this->compileValue($importPath) . ';'] : + $result; + + break; + case 'import_mixin': + [, $importId] = $prop; + $import = $this->env->imports[$importId]; + if ($import[0] === false) { + if (isset($import[1])) { + $out->lines[] = $import[1]; + } + } else { + [, $bottom, $parser, $importDir] = $import; + $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); + } + + break; + default: + $block->parser->throwError("unknown op: $prop[0]\n", $block->count); + } + } + + + /** + * 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. + * @throws Exception + * @internal parser internal method + */ + public function compileValue($value) + { + switch ($value[0]) { + case 'list': + // [1] - delimiter + // [2] - array of values + return implode($value[1], array_map([$this, 'compileValue'], $value[2])); + case 'raw_color': + if (!empty($this->formatter->compressColors)) { + return $this->compileValue(Color::coerceColor($value)); + } + return $value[1]; + case 'keyword': + // [1] - the keyword + return $value[1]; + case 'number': + [, $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) + [, $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 + [, $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': + [, $name, $args] = $value; + return $name . '(' . $this->compileValue($args) . ')'; + default: // assumed to be unit + throw new Exception("unknown value type: $value[0]"); + } + } + + /** + * Utility func to unquote a string + * + * @todo this not really a good name for this function + * @internal parser internal method + */ + public function unwrap(array $arg): array + { + switch ($arg[0]) { + case 'list': + $items = $arg[2]; + if (isset($items[0])) { + return self::unwrap($items[0]); + } + throw new Exception('unrecognised input'); + case 'string': + $arg[1] = ''; + return $arg; + case 'keyword': + return $arg; + default: + return ['keyword', $this->compileValue($arg)]; + } + } + + /** + * Convert the rgb, rgba, hsl color literals of function type + * as returned by the parser into values of color type. + * + * @throws Exception + */ + 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 = ['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[] = Util::clamp($val, $clamp); + $i++; + } + + while (count($hsl) < 4) $hsl[] = 0; + return Color::toRGB($hsl); + } elseif ($fname == 'rgb' || $fname == 'rgba') { + $components = []; + $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 Color::fixColor($components); + } + + return false; + } + + /** + * @throws Exception + * @internal parser internal method + */ + public function reduce($value, $forExpression = false) + { + switch ($value[0]) { + case 'interpolate': + $reduced = $this->reduce($value[1]); + $var = $this->compileValue($reduced); + $res = $this->reduce(['variable', Constants::VPREFIX . $var]); + + if ($res[0] == 'raw_color') { + $res = Color::coerceColor($res); + } + + if (empty($value[2])) $res = $this->unwrap($res); + + return $res; + case 'variable': + $key = $value[1]; + if (is_array($key)) { + $key = $this->reduce($key); + $key = Constants::VPREFIX . $this->compileValue($this->unwrap($key)); + } + + $seen =& $this->env->seenNames; + + if (!empty($seen[$key])) { + throw new Exception("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->unwrap($part); + } + } + return $value; + case 'escape': + [, $inner] = $value; + return $this->unwrap($this->reduce($inner)); + case 'function': + $color = $this->funcToColor($value); + if ($color) return $color; + + [, $name, $args] = $value; + + $f = $this->libFunctions[$name] ?? null; + + 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 ['string', '', [$name, '(', $args, ')']]; + } + + // convert to a typed value if the result is a php primitive + if (is_numeric($ret)) $ret = ['number', $ret, '']; + elseif (!is_array($ret)) $ret = ['keyword', $ret]; + + return $ret; + } + + // plain function, reduce args + $value[2] = $this->reduce($value[2]); + return $value; + case 'unary': + [, $op, $exp] = $value; + $exp = $this->reduce($exp); + + if ($exp[0] == 'number') { + switch ($op) { + case '+': + return $exp; + case '-': + $exp[1] *= -1; + return $exp; + } + } + return ['string', '', [$op, $exp]]; + } + + if ($forExpression) { + switch ($value[0]) { + case 'keyword': + if ($color = Color::coerceColor($value)) { + return $color; + } + break; + case 'raw_color': + return Color::coerceColor($value); + } + } + + return $value; + } + + + // make something string like into a string + protected function coerceString($value) + { + switch ($value[0]) { + case 'string': + return $value; + case 'keyword': + return ['string', '', [$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; + } + + /** + * evaluate an expression + * @throws Exception + */ + protected function evaluate($exp) + { + [, $op, $left, $right, $whiteBefore, $whiteAfter] = $exp; + + $left = $this->reduce($left, true); + $right = $this->reduce($right, true); + + if ($leftColor = Color::coerceColor($left)) { + $left = $leftColor; + } + + if ($rightColor = Color::coerceColor($right)) { + $right = $rightColor; + } + + $ltype = $left[0]; + $rtype = $right[0]; + + // operators that work on all types + if ($op == 'and') { + return Util::toBool($left == Constants::TRUE && $right == Constants::TRUE); + } + + if ($op == '=') { + return Util::toBool($this->eq($left, $right)); + } + + if ($op == '+' && !is_null($str = $this->stringConcatenate($left, $right))) { + return $str; + } + + // type based operators + $fname = sprintf('op_%s_%s', $ltype, $rtype); + if (is_callable([$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 ['string', '', [$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; + } + } + + + /** + * @throws Exception + */ + protected function op_number_color($op, $lft, $rgt) + { + if ($op == '+' || $op == '*') { + return $this->op_color_number($op, $rgt, $lft); + } + } + + /** + * @throws Exception + */ + 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]) + ); + } + + /** + * @throws Exception + */ + protected function op_color_color($op, $left, $right) + { + $out = ['color']; + $max = count($left) > count($right) ? count($left) : count($right); + foreach (range(1, $max - 1) as $i) { + $lval = $left[$i] ?? 0; + $rval = $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) throw new Exception("evaluate error: can't divide by zero"); + $out[] = $lval / $rval; + break; + default: + throw new Exception('evaluate error: color op number failed on op ' . $op); + } + } + return Color::fixColor($out); + } + + + /** + * operator on two numbers + * @throws Exception + */ + 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) throw new Exception('parse error: divide by zero'); + $value = $left[1] / $right[1]; + break; + case '<': + return Util::toBool($left[1] < $right[1]); + case '>': + return Util::toBool($left[1] > $right[1]); + case '>=': + return Util::toBool($left[1] >= $right[1]); + case '=<': + return Util::toBool($left[1] <= $right[1]); + default: + throw new Exception('parse error: unknown number operator: ' . $op); + } + + return ['number', $value, $unit]; + } + + + /* environment functions */ + + protected function makeOutputBlock($type, $selectors = null) + { + $b = new stdclass; + $b->lines = []; + $b->children = []; + $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 = []; + $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 + * @throws Exception + */ + protected function get($name) + { + $current = $this->env; + + // track scope to evaluate + $scope_secondary = []; + + $isArguments = $name == Constants::VPREFIX . 'arguments'; + while ($current) { + if ($isArguments && isset($current->arguments)) { + return ['list', ' ', $current->arguments]; + } + + if (isset($current->store[$name])) + return $current->store[$name]; + // has secondary scope? + if (isset($current->storeParent)) + $scope_secondary[] = $current->storeParent; + + $current = $current->parent ?? null; + } + + while (count($scope_secondary)) { + // pop one off + $current = array_shift($scope_secondary); + while ($current) { + if ($isArguments && isset($current->arguments)) { + return ['list', ' ', $current->arguments]; + } + + if (isset($current->store[$name])) { + return $current->store[$name]; + } + + // has secondary scope? + if (isset($current->storeParent)) { + $scope_secondary[] = $current->storeParent; + } + + $current = $current->parent ?? null; + } + } + + throw new Exception("variable $name is undefined"); + } + + /** + * inject array of unparsed strings into environment as variables + * @throws Exception + */ + protected function injectVariables($args) + { + $this->pushEnv(); + $parser = new Parser(__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); + } + } + + /** + * Create a new parser instance + * + * @param string|null $name A name to identify the parser in error messages + */ + protected function makeParser(?string $name): Parser + { + $parser = new Parser($name); + $parser->writeComments = $this->preserveComments; + + return $parser; + } + + /** + * Add the given file to the list of parsed files + * + * @param $file + */ + protected function addParsedFile($file): void + { + $this->allParsedFiles[realpath($file)] = filemtime($file); + } +} diff --git a/vendor/splitbrain/lesserphp/src/Parser.php b/vendor/splitbrain/lesserphp/src/Parser.php new file mode 100644 index 000000000..420e38356 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Parser.php @@ -0,0 +1,1519 @@ + + * Copyright 2016, Marcus Schwarz + * Licensed under MIT or GPLv3, see LICENSE + */ + +namespace LesserPHP; + +use Exception; +use LesserPHP\Utils\Util; +use stdClass; + +/** + * responsible for taking a string of LESS code and converting it into a syntax tree + */ +class Parser +{ + + public $eatWhiteDefault; + public $sourceName; + public $writeComments; + public $count; + public $line; + public $env; + public $buffer; + public $seenComments; + public $inExp; + + protected static $nextBlockId = 0; // used to uniquely identify blocks + + protected static $precedence = [ + '=<' => 0, + '>=' => 0, + '=' => 0, + '<' => 0, + '>' => 0, + + '+' => 1, + '-' => 1, + '*' => 2, + '/' => 2, + '%' => 2, + ]; + + protected static $whitePattern; + protected static $commentMulti; + + protected static $commentSingle = '//'; + protected static $commentMultiLeft = '/*'; + protected static $commentMultiRight = '*/'; + + // regex string to match any of the operators + protected static $operatorString; + + // these properties will supress division unless it's inside parenthases + protected static $supressDivisionProps = + ['/border-radius$/i', '/^font$/i']; + + protected $blockDirectives = [ + 'font-face', + 'keyframes', + 'page', + '-moz-document', + 'viewport', + '-moz-viewport', + '-o-viewport', + '-ms-viewport' + ]; + protected $lineDirectives = ['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 + protected static $literalCache = []; + + protected $currentProperty; + + /** + * @param string|null $sourceName name used for error messages + */ + public function __construct(?string $sourceName = null) + { + $this->eatWhiteDefault = true; + $this->sourceName = $sourceName; // name used for error messages + + $this->writeComments = false; + + if (!self::$operatorString) { + self::$operatorString = + '(' . implode('|', array_map( + [Util::class, 'pregQuote'], + array_keys(self::$precedence) + )) . ')'; + + $commentSingle = Util::pregQuote(self::$commentSingle); + $commentMultiLeft = Util::pregQuote(self::$commentMultiLeft); + $commentMultiRight = Util::pregQuote(self::$commentMultiRight); + + self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight; + self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais'; + } + } + + /** + * @throws Exception + */ + 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 = []; + + // 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()) { + // no-op + } + + 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)) { + $this->throwError('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. + * + * @throws Exception + */ + 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(['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) ? [] : $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, [';']) || 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(['directive', $dirName, $dirValue]); + return true; + } + } elseif ($this->literal(':', true)) { + //Ruleset Definition + if (($this->openString('{', $dirValue, null, [';']) || true) && + $this->literal('{')) { + $dir = $this->pushBlock($this->fixTags(['@' . $dirName])); + $dir->name = $dirName; + if (isset($dirValue)) $dir->value = $dirValue; + return true; + } + } + } + + $this->seek($s); + } + + // setting a variable + if ($this->variable($var) && $this->assign() && + $this->propertyValue($value) && $this->end()) { + $this->append(['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([$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] != Constants::MPREFIX) { + $hidden = false; + break; + } + } + } + + foreach ($block->tags as $tag) { + if (is_string($tag)) { + $this->env->children[$tag][] = $block; + } + } + } + + if (!$hidden) { + $this->append(['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(['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([Util::class, 'pregQuote'], $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] == Constants::VPREFIX) + $tag[0] = Constants::MPREFIX; + } + return $tags; + } + + // a list of expressions + protected function expressionList(&$exps) + { + $values = []; + + 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 = ['list', '', [$out, ['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 = ['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 = []; + + 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 = ['variable', $inner]) || + $this->unit($inner) || + $this->parenValue($inner))) { + $value = ['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->stringValue($value)) return true; + + if ($this->keyword($word)) { + $value = ['keyword', $word]; + return true; + } + + // try a variable + if ($this->variable($var)) { + $value = ['variable', $var]; + return true; + } + + // unquote string (should this work on any type? + if ($this->literal('~') && $this->stringValue($str)) { + $value = ['escape', $str]; + return true; + } else { + $this->seek($s); + } + + // css hack: \0 + if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { + $value = ['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 = ['import', $value]; + return true; + } + return false; + } + + 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 = []; + + if ( + ( + $this->literal('only') && ($only = true) || + $this->literal('not') && ($not = true) || + true + ) && + $this->keyword($mediaType) + ) { + $prop = ['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 = ['mediaExp', $feature]; + if ($value) $out[] = $value; + return true; + } elseif ($this->variable($variable)) { + $out = ['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 = ["'", '"', '@{', $end]; + $stop = array_map([Util::class, 'pregQuote'], $stop); + // $stop[] = self::$commentMulti; + + if (!is_null($rejectStrs)) { + $stop = array_merge($stop, $rejectStrs); + } + + $patt = '(.*?)(' . implode('|', $stop) . ')'; + + $nestingLevel = 0; + + $content = []; + 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->stringValue($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 = ['string', '', $content]; + return true; + } + + protected function stringValue(&$out) + { + $s = $this->seek(); + if ($this->literal('"', false)) { + $delim = '"'; + } elseif ($this->literal("'", false)) { + $delim = "'"; + } else { + return false; + } + + $content = []; + + // look for either ending delim , escape, or string interpolation + $patt = '([^\n]*?)(@\{|\\\\|' . Util::pregQuote($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)) { + $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 = ['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, ["'", '"', ';']) && + $this->literal('}', false)) { + $out = ['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 = ['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 = ['string', '', [$m[1]]]; + } else { + $out = ['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. + * + * @throws Exception + */ + protected function argumentDef(&$args, &$isVararg) + { + $s = $this->seek(); + if (!$this->literal('(')) return false; + + $values = []; + $delim = ','; + $method = 'expressionList'; + + $value = null; + $rhs = null; + + $isVararg = false; + while (true) { + if ($this->literal('...')) { + $isVararg = true; + break; + } + + if ($this->$method($value)) { + if ($value[0] == 'variable') { + $arg = ['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[] = ['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 = []; + 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 = ['list', ', ', $newList]; + + switch ($values[0][0]) { + case 'arg': + $newArg = ['arg', $values[0][1], $newList]; + break; + case 'lit': + $newArg = ['lit', $newList]; + break; + } + } elseif ($values) { // 1 item + $newArg = $values[0]; + } + + if ($newArg) { + $values = [$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 = []; + 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 = []; + 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 = ['[']; + // keyword, string, operator + while (true) { + if ($this->literal(']', false)) { + $this->count--; + break; // get out early + } + + if ($this->match('\s+', $m)) { + $attrParts[] = ' '; + continue; + } + if ($this->stringValue($str)) { + // escape parent selector, (yuck) + foreach ($str[2] as &$chunk) { + if (is_string($chunk)) { + $chunk = str_replace(Constants::PARENT_SELECTOR, "$&$", $chunk); + } + } + + $attrParts[] = $str; + $hasInterpolation = true; + continue; + } + + if ($this->keyword($word)) { + $attrParts[] = $word; + continue; + } + + if ($this->interpolation($inter)) { + $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 = []; + while ($this->tagBracket($parts, $hasExpression)) { + // no-op + } + + $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)) { + // no-op + } + 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 = ['exp', ['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 = []; + while (true) { + $ss = $this->seek(); + // this ugly nonsense is for ie filter properties + if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { + $args[] = ['string', '', [$name, '=', $value]]; + } else { + $this->seek($ss); + if ($this->expressionList($value)) { + $args[] = $value; + } + } + + if (!$this->literal(',')) break; + } + $args = ['list', ',', $args]; + + if ($this->literal(')')) { + $func = ['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 = ['function', $fname, $string]; + return true; + } + } + } + + $this->seek($s); + return false; + } + + // consume a less variable + protected function variable(&$name) + { + $s = $this->seek(); + if ($this->literal(Constants::VPREFIX, false) && + ($this->variable($sub) || $this->keyword($name))) { + if (!empty($sub)) { + $name = ['variable', $sub]; + } else { + $name = Constants::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 = []; + + 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 = []; + 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 = ['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] = Util::pregQuote($what); + } + + return $this->match(self::$literalCache[$what], $m, $eatWhitespace); + } + + protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) + { + $s = $this->seek(); + $items = []; + $value = null; + 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 = ['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, 0, $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, 0, $this->count)) { + if (isset($m[1]) && empty($this->seenComments[$this->count])) { + $this->append(['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'; + return preg_match($r, $this->buffer, $out, 0, $from); + } + + // 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 */ + + /** + * Throw a parser exception + * + * This function tries to use the current parsing context to provide + * additional info on where/why the error occurred. + * + * @param string $msg The error message to throw + * @param int|null $count A line number counter to use instead of the current count + * @param \Throwable|null $previous A previous exception to chain + * @throws ParserException + */ + public function throwError(string $msg = 'parse error', ?int $count = null, \Throwable $previous = null) + { + $count = is_null($count) ? $this->count : $count; + + $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n"); + + if ($this->peek("(.*?)(\n|$)", $m, $count)) { + $culprit = $m[1]; + } + + throw new ParserException( + $msg, + $culprit, + $this->sourceName, + $line, + $previous + ); + } + + 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 = []; + $b->children = []; + + // add a reference to the parser so + // we can access the parser to throw errors + // or retrieve the sourceName of this block. + $b->parser = $this; + + // so we know the position of this block + $b->count = $this->count; + + $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 = ['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 = [$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] . '.*?(?error = $message; + + if ($culprit) { + $this->culprit = $culprit; + $message .= " `$culprit`"; + } + if ($sourceFile) { + $this->sourceFile = $sourceFile; + $message .= " in $sourceFile"; + } + + if ($sourceLine !== null && $sourceLine > -1) { + $this->sourceLine = $sourceLine; + $message .= " line: $sourceLine"; + } + + parent::__construct($message, 0, $previous); + } + + /** + * This is the error message without any additional context + */ + public function getError(): string + { + return $this->error; + } + + /** + * The LESS code that triggered the error + * + * This is the line the parser choked on. Not always available. + */ + public function getCulprit(): string + { + return $this->culprit; + } + + /** + * The LESS source file where the error was triggered + * + * This is the file the parser was parsing, will usually only be available when + * parsing an import or when compileFile() was used. + */ + public function getSourceFile(): string + { + return $this->sourceFile; + } + + /** + * The line number where the error was triggered + */ + public function getSourceLine(): int + { + return $this->sourceLine; + } +} diff --git a/vendor/splitbrain/lesserphp/src/Utils/Asserts.php b/vendor/splitbrain/lesserphp/src/Utils/Asserts.php new file mode 100644 index 000000000..2932483ac --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Utils/Asserts.php @@ -0,0 +1,81 @@ + $numValues) { + if ($name) { + $name = $name . ': '; + } + + throw new Exception("${name}expecting at least $expectedMinArgs arguments, got $numValues"); + } + + return $values; + } + + /** + * Checks that the value is a number and returns it as float + * + * @param array $value The parsed value triplet + * @param string $error The error message to throw + * @throws Exception + */ + public static function assertNumber(array $value, string $error = 'expecting number'): float + { + if ($value[0] == 'number') return (float)$value[1]; + throw new Exception($error); + } + + /** + * @throws Exception + */ + public static function assertColor(array $value, $error = 'expected color value'): array + { + $color = Color::coerceColor($value); + if (is_null($color)) throw new Exception($error); + return $color; + } +} diff --git a/vendor/splitbrain/lesserphp/src/Utils/Color.php b/vendor/splitbrain/lesserphp/src/Utils/Color.php new file mode 100644 index 000000000..dee380907 --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Utils/Color.php @@ -0,0 +1,187 @@ + 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(Constants::CSS_COLORS[$name])) { + $rgba = explode(',', Constants::CSS_COLORS[$name]); + + if (isset($rgba[3])) + return ['color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]]; + + return ['color', $rgba[0], $rgba[1], $rgba[2]]; + } + return null; + } + return null; + } + + /** + * Calculate the perceptual brightness of a color object + */ + public static function toLuma(array $color): float + { + [, $r, $g, $b] = Color::coerceColor($color); + + $r = $r / 255; + $g = $g / 255; + $b = $b / 255; + + $r = ($r <= 0.03928) ? $r / 12.92 : (($r + 0.055) / 1.055) ** 2.4; + $g = ($g <= 0.03928) ? $g / 12.92 : (($g + 0.055) / 1.055) ** 2.4; + $b = ($b <= 0.03928) ? $b / 12.92 : (($b + 0.055) / 1.055) ** 2.4; + + return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b); + } + + /** + * Convert a color to HSL color space + */ + public static function toHSL(array $color): array + { + 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); + } else { + $H = 0; + } + } + + $out = [ + 'hsl', + ($H < 0 ? $H + 6 : $H) * 60, + $S * 100, + $L * 100, + ]; + + if (count($color) > 4) $out[] = $color[4]; // copy alpha + return $out; + } + + + /** + * 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 + */ + public static function toRGB(array $color): array + { + 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 = self::calculateRGBComponent($H + 1 / 3, $temp1, $temp2); + $g = self::calculateRGBComponent($H, $temp1, $temp2); + $b = self::calculateRGBComponent($H - 1 / 3, $temp1, $temp2); + } + + // $out = array('color', round($r*255), round($g*255), round($b*255)); + $out = ['color', $r * 255, $g * 255, $b * 255]; + if (count($color) > 4) $out[] = $color[4]; // copy alpha + return $out; + } + + + /** + * make sure a color's components don't go out of bounds + */ + public static function fixColor(array $c): array + { + foreach (range(1, 3) as $i) { + if ($c[$i] < 0) $c[$i] = 0; + if ($c[$i] > 255) $c[$i] = 255; + } + + return $c; + } + + /** + * Helper function for the HSL to RGB conversion process. + * + * This function normalizes the input component of the HSL color and determines the RGB + * value based on the HSL values. + * + * @param float $comp The component of the HSL color to be normalized and converted. + * @param float $temp1 The first temporary variable used in the conversion process + * @param float $temp2 The second temporary variable used in the conversion process + * + * @return float The calculated RGB value as percentage of the maximum value (255) + */ + protected static function calculateRGBComponent(float $comp, float $temp1, float $temp2): float + { + // Normalize the component value to be within the range [0, 1] + if ($comp < 0) $comp += 1.0; + elseif ($comp > 1) $comp -= 1.0; + + // Determine the return value based on the value of the component + 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; + + // Fallback return value, represents the case where the saturation of the color is zero + return $temp1; + } +} diff --git a/vendor/splitbrain/lesserphp/src/Utils/Util.php b/vendor/splitbrain/lesserphp/src/Utils/Util.php new file mode 100644 index 000000000..c638d801f --- /dev/null +++ b/vendor/splitbrain/lesserphp/src/Utils/Util.php @@ -0,0 +1,128 @@ +