diff --git a/README.md b/README.md index 6fc4494..36ada70 100755 --- a/README.md +++ b/README.md @@ -87,7 +87,7 @@ This source code includes: * [phpqrcode](https://github.com/t0k4rt/phpqrcode) to generate QR codes * [Ubuntu font for the Web](https://github.com/earaujoassis/ubuntu-fontface) -* [lesserphp](https://github.com/MarcusSchwarz/lesserphp) to compile [Less](http://lesscss.org) +* [less.php](https://github.com/wikimedia/less.php) to compile [Less](http://lesscss.org) ## License diff --git a/README_fr.md b/README_fr.md index 84217b0..199efab 100644 --- a/README_fr.md +++ b/README_fr.md @@ -87,7 +87,7 @@ Ce code source inclus : * [phpqrcode](https://github.com/t0k4rt/phpqrcode) pour générer les codes QR * [La police Ubuntu pour le Web](https://github.com/earaujoassis/ubuntu-fontface) -* [lesserphp](https://github.com/MarcusSchwarz/lesserphp) pour compiler le [Less](http://lesscss.org) +* [less.php](https://github.com/wikimedia/less.php) pour compiler le [Less](http://lesscss.org) ## Licence diff --git a/index.php b/index.php index bcf0314..195b2d4 100755 --- a/index.php +++ b/index.php @@ -101,7 +101,7 @@ if (badQuery()) { // Then delete it unlink("temp/style.min.css"); - require "lesserphp/lessc.inc.php"; + require "less.php/lessc.inc.php"; $less = new lessc; $less->setVariables($variablesTheme); // Make these colors available in style.less $less->setFormatter("compressed"); diff --git a/less.php/CHANGES.md b/less.php/CHANGES.md new file mode 100644 index 0000000..92f82f8 --- /dev/null +++ b/less.php/CHANGES.md @@ -0,0 +1,70 @@ +# 3.1.0 +- [All Changes](https://github.com/wikimedia/less.php/compare/v3.0.0...v3.1.0) +* PHP 8.0 support: Drop use of curly braces for sub-string eval (James D. Forrester) +* Make `Directive::__construct` $rules arg optional (fix PHP 7.4 warning) (Sam Reed) +* ProcessExtends: Improve performance by using a map for selectors and parents (Andrey Legayev) +* build: Run CI tests on PHP 8.0 too (James D. Forrester) +* code: Fix PSR12.Properties.ConstantVisibility.NotFound (Sam Reed) + +# 3.0.0 +- [All Changes](https://github.com/wikimedia/less.php/compare/v2.0.0...v3.0.0) +- Raise PHP requirement from 7.1 to 7.2.9 (James Forrester) +- build: Upgrade phpunit to ^8.5 and make pass (James Forrester) +- build: Install php-parallel-lint (James Forrester) +- build: Install minus-x and make pass (James Forrester) + +# 2.0.0 +- [All Changes](https://github.com/wikimedia/less.php/compare/1.8.2...v2.0.0) +- Relax PHP requirement down to 7.1, from 7.2.9 (Franz Liedke) +- Reflect recent breaking changes properly with the semantic versioning (James Forrester) + +# 1.8.2 +- [All Changes](https://github.com/wikimedia/less.php/compare/1.8.1...1.8.2) +- Require PHP 7.2.9+, up from 5.3+ (James Forrester) +- Release: Update Version.php with the current release ID (COBadger) +- Fix access array offset on value of type null (Michele Locati) +- Fixed test suite on PHP 7.4 (Sergei Morozov) +- docs: Fix 1.8.1 "All changes" link (Timo Tijhof) + +# 1.8.1 +- [All Changes](https://github.com/wikimedia/less.php/compare/v1.8.0...1.8.1) +- Another PHP 7.3 compatibility tweak + +# 1.8.0 +- [All Changes](https://github.com/Asenar/less.php/compare/v1.7.0.13...v1.8.0) +- Wikimedia fork +- Supports up to PHP 7.3 +- No longer tested against PHP 5, though it's still remains allowed in `composer.json` for HHVM compatibility +- Switched to [semantic versioning](https://semver.org/), hence version numbers now use 3 digits + +# 1.7.0.13 + - [All Changes](https://github.com/Asenar/less.php/compare/v1.7.0.12...v1.7.0.13) + - Fix composer.json (PSR-4 was invalid) + +# 1.7.0.12 + - [All Changes](https://github.com/Asenar/less.php/compare/v1.7.0.11...v1.7.0.12) + - set bin/lessc bit executable + - Add 'gettingVariables' method in Less_Parser + +# 1.7.0.11 + - [All Changes](https://github.com/Asenar/less.php/compare/v1.7.0.10...v1.7.0.11) + - Fix realpath issue (windows) + - Set Less_Tree_Call property back to public ( Fix 258 266 267 issues from oyejorge/less.php) + +# 1.7.0.10 + + - [All Changes](https://github.com/oyejorge/less.php/compare/v1.7.0.9...v1.7.10) + - Add indentation option + - Add 'optional' modifier for @import + - fix $color in Exception messages + - don't use set_time_limit when running cli + - take relative-url into account when building the cache filename + - urlArgs should be string no array() + - add bug-report fixtures [#6dc898f](https://github.com/oyejorge/less.php/commit/6dc898f5d75b447464906bdf19d79c2e19d95e33) + - fix #269, missing on NameValue type [#a8dac63](https://github.com/oyejorge/less.php/commit/a8dac63d93fb941c54fb78b12588abf635747c1b) + +# 1.7.0.9 + + - [All Changes](https://github.com/oyejorge/less.php/compare/v1.7.0.8...v1.7.0.9) + - Remove space at beginning of Version.php + - Revert require() paths in test interface diff --git a/less.php/LICENSE b/less.php/LICENSE new file mode 100644 index 0000000..82216a5 --- /dev/null +++ b/less.php/LICENSE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + diff --git a/less.php/README.md b/less.php/README.md new file mode 100644 index 0000000..d10177c --- /dev/null +++ b/less.php/README.md @@ -0,0 +1,315 @@ +[![Continuous Integration](https://github.com/wikimedia/less.php/workflows/PHP%20Test/badge.svg)](https://github.com/wikimedia/less.php/actions) + +[Less.php](http://lessphp.typesettercms.com) +======== + +This is the Wikimedia fork of a PHP port of the official LESS processor . + +* [About](#about) +* [Installation](#installation) +* [Basic Use](#basic-use) +* [Caching](#caching) +* [Source Maps](#source-maps) +* [Command Line](#command-line) +* [Integration with other projects](#integration-with-other-projects) +* [Transitioning from Leafo/lessphp](#transitioning-from-leafolessphp) +* [Credits](#credits) + + + +About +--- +The code structure of less.php mirrors that of the official processor which helps us ensure compatibility and allows for easy maintenance. + +Please note, there are a few unsupported LESS features: + +- Evaluation of JavaScript expressions within back-ticks (for obvious reasons). +- Definition of custom functions. + + +Installation +--- + +You can install the library with Composer or manually. + +#### Composer + +1. [Install Composer](https://getcomposer.org/download/) +2. Run `composer require wikimedia/less.php` + +#### Manually From Release + +Step 1. [Download a release](https://github.com/wikimedia/less.php/releases) and upload the PHP files to your server. + +Step 2. Include the library: + +```php +require_once '[path to less.php]/lib/Less/Autoloader.php'; +Less_Autoloader::register(); +``` + +Basic Use +--- + +#### Parsing Strings + +```php +$parser = new Less_Parser(); +$parser->parse( '@color: #4D926F; #header { color: @color; } h2 { color: @color; }' ); +$css = $parser->getCss(); +``` + + +#### Parsing LESS Files +The parseFile() function takes two arguments: + +1. The absolute path of the .less file to be parsed +2. The url root to prepend to any relative image or @import urls in the .less file. + +```php +$parser = new Less_Parser(); +$parser->parseFile( '/var/www/mysite/bootstrap.less', 'http://example.com/mysite/' ); +$css = $parser->getCss(); +``` + + +#### Handling Invalid LESS +An exception will be thrown if the compiler encounters invalid LESS. + +```php +try{ + $parser = new Less_Parser(); + $parser->parseFile( '/var/www/mysite/bootstrap.less', 'http://example.com/mysite/' ); + $css = $parser->getCss(); +}catch(Exception $e){ + $error_message = $e->getMessage(); +} +``` + + +#### Parsing Multiple Sources +less.php can parse multiple sources to generate a single CSS file. + +```php +$parser = new Less_Parser(); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$parser->parse( '@color: #4D926F; #header { color: @color; } h2 { color: @color; }' ); +$css = $parser->getCss(); +``` + +#### Getting Info About The Parsed Files +less.php can tell you which .less files were imported and parsed. + +```php +$parser = new Less_Parser(); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$css = $parser->getCss(); +$imported_files = $parser->allParsedFiles(); +``` + + +#### Compressing Output +You can tell less.php to remove comments and whitespace to generate minimized CSS files. + +```php +$options = array( 'compress'=>true ); +$parser = new Less_Parser( $options ); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$css = $parser->getCss(); +``` + +#### Getting Variables +You can use the getVariables() method to get an all variables defined and +their value in a php associative array. Note that LESS has to be previously +compiled. +```php +$parser = new Less_Parser; +$parser->parseFile( '/var/www/mysite/bootstrap.less'); +$css = $parser->getCss(); +$variables = $parser->getVariables(); + +``` + + + +#### Setting Variables +You can use the ModifyVars() method to customize your CSS if you have variables stored in PHP associative arrays. + +```php +$parser = new Less_Parser(); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$parser->ModifyVars( array('font-size-base'=>'16px') ); +$css = $parser->getCss(); +``` + + +#### Import Directories +By default, less.php will look for @imports in the directory of the file passed to parseFile(). +If you're using parse() or if @imports reside in different directories, you can tell less.php where to look. + +```php +$directories = array( '/var/www/mysite/bootstrap/' => '/mysite/bootstrap/' ); +$parser = new Less_Parser(); +$parser->SetImportDirs( $directories ); +$parser->parseFile( '/var/www/mysite/theme.less', '/mysite/' ); +$css = $parser->getCss(); +``` + + +Caching +--- +Compiling LESS code into CSS is a time consuming process, caching your results is highly recommended. + + +#### Caching CSS +Use the Less_Cache class to save and reuse the results of compiled LESS files. +This method will check the modified time and size of each LESS file (including imported files) and regenerate a new CSS file when changes are found. +Note: When changes are found, this method will return a different file name for the new cached content. + +```php +$less_files = array( '/var/www/mysite/bootstrap.less' => '/mysite/' ); +$options = array( 'cache_dir' => '/var/www/writable_folder' ); +$css_file_name = Less_Cache::Get( $less_files, $options ); +$compiled = file_get_contents( '/var/www/writable_folder/'.$css_file_name ); +``` + +#### Caching CSS With Variables +Passing options to Less_Cache::Get() + +```php +$less_files = array( '/var/www/mysite/bootstrap.less' => '/mysite/' ); +$options = array( 'cache_dir' => '/var/www/writable_folder' ); +$variables = array( 'width' => '100px' ); +$css_file_name = Less_Cache::Get( $less_files, $options, $variables ); +$compiled = file_get_contents( '/var/www/writable_folder/'.$css_file_name ); +``` + + +#### Parser Caching +less.php will save serialized parser data for each .less file if a writable folder is passed to the SetCacheDir() method. +Note: This feature only caches intermediate parsing results to improve the performance of repeated CSS generation. +Your application should cache any CSS generated by less.php. + +```php +$options = array('cache_dir'=>'/var/www/writable_folder'); +$parser = new Less_Parser( $options ); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$css = $parser->getCss(); +``` + +You can specify the caching technique used by changing the ```cache_method``` option. Supported methods are: +* ```php```: Creates valid PHP files which can be included without any changes (default method). +* ```var_export```: Like "php", but using PHP's ```var_export()``` function without any optimizations. + It's recommended to use "php" instead. +* ```serialize```: Faster, but pretty memory-intense. +* ```callback```: Use custom callback functions to implement your own caching method. Give the "cache_callback_get" and + "cache_callback_set" options with callables (see PHP's ```call_user_func()``` and ```is_callable()``` functions). less.php + will pass the parser object (class ```Less_Parser```), the path to the parsed .less file ("/some/path/to/file.less") and + an identifier that will change every time the .less file is modified. The ```get``` callback must return the ruleset + (an array with ```Less_Tree``` objects) provided as fourth parameter of the ```set``` callback. If something goes wrong, + return ```NULL``` (cache doesn't exist) or ```FALSE```. + + + +Source Maps +--- +Less.php supports v3 sourcemaps + +#### Inline +The sourcemap will be appended to the generated CSS file. + +```php +$options = array( 'sourceMap' => true ); +$parser = new Less_Parser($options); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$css = $parser->getCss(); +``` + +#### Saving to Map File + +```php +$options = array( + 'sourceMap' => true, + 'sourceMapWriteTo' => '/var/www/mysite/writable_folder/filename.map', + 'sourceMapURL' => '/mysite/writable_folder/filename.map', + ); +$parser = new Less_Parser($options); +$parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); +$css = $parser->getCss(); +``` + + +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: + +``` +$ lessc input.less > output.css +``` + +By using the -w flag you can watch a specified input file and have it compile as needed to the output file: + +``` +$ lessc -w input.less output.css +``` + +Errors from watch mode are written to standard out. + +For more help, run `lessc --help` + + +Integration with other projects +--- + +#### Drupal 7 + +This library can be used as drop-in replacement of lessphp to work with [Drupal 7 less module](https://drupal.org/project/less). + +How to install: + +1. [Download the less.php source code](https://github.com/wikimedia/less.php/archive/master.zip) and unzip it so that 'lessc.inc.php' is located at 'sites/all/libraries/lessphp/lessc.inc.php'. +2. Download and install [Drupal 7 less module](https://drupal.org/project/less) as usual. +3. That's it :) + +#### JBST WordPress theme + +JBST has a built-in LESS compiler based on lessphp. Customize your WordPress theme with LESS. + +How to use / install: + +1. [Download the latest release](https://github.com/bassjobsen/jamedo-bootstrap-start-theme) copy the files to your {wordpress/}wp-content/themes folder and activate it. +2. Find the compiler under Appearance > LESS Compiler in your WordPress dashboard +3. Enter your LESS code in the text area and press (re)compile + +Use the built-in compiler to: +- set any [Bootstrap](http://getbootstrap.com/customize/) variable or use Bootstrap's mixins: + -`@navbar-default-color: blue;` + - create a custom button: `.btn-custom { + .button-variant(white; red; blue); +}` +- set any built-in LESS variable: for example `@footer_bg_color: black;` sets the background color of the footer to black +- use built-in mixins: - add a custom font: `.include-custom-font(@family: arial,@font-path, @path: @custom-font-dir, @weight: normal, @style: normal);` + +The compiler can also be downloaded as [plugin](http://wordpress.org/plugins/wp-less-to-css/) + +#### WordPress + +This simple plugin will simply make the library available to other plugins and themes and can be used as a dependency using the [TGM Library](http://tgmpluginactivation.com/) + +How to install: + +1. Install the plugin from your WordPress Dashboard: http://wordpress.org/plugins/lessphp/ +2. That's it :) + + +Transitioning from Leafo/lessphp +--- +Projects looking for an easy transition from leafo/lessphp can use the lessc.inc.php adapter. To use, [Download the less.php source code](https://github.com/wikimedia/less.php/archive/master.zip) and unzip the files into your project so that the new 'lessc.inc.php' replaces the existing 'lessc.inc.php'. + +Note, the 'setPreserveComments' will no longer have any effect on the compiled LESS. + +Credits +--- +less.php was originally ported to PHP by [Matt Agar](https://github.com/agar) and then updated by [Martin Jantošovič](https://github.com/Mordred). This Wikimedia-maintained fork was split off from [Josh Schmidt's version](https://github.com/oyejorge/less.php). + diff --git a/less.php/bin/lessc b/less.php/bin/lessc new file mode 100755 index 0000000..fa1fb95 --- /dev/null +++ b/less.php/bin/lessc @@ -0,0 +1,191 @@ +#!/usr/bin/env php + false, 'relativeUrls' => false); +$silent = false; +$watch = false; +$rootpath = ''; + +// Check for arguments +array_shift($argv); +if (!count($argv)) { + $argv[] = '-h'; +} + +// parse arguments +foreach ($argv as $key => $arg) { + if (preg_match('/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i', $arg, $matches)) { + $option = $matches[1]; + $value = isset($matches[2]) ? $matches[2] : false; + unset($argv[$key]); + + switch ($option) { + case 'h': + case 'help': + echo << 1) { + $output = array_pop($argv); + $inputs = $argv; +} +else { + $inputs = $argv; + $output = false; +} + +if (!count($inputs)) { + echo("lessc: no input files\n"); + exit; +} + +if ($watch) { + if (!$output) { + echo("lessc: you must specify the output file if --watch is given\n"); + exit; + } + + $lastAction = 0; + + echo("lessc: watching input files\n"); + + while (1) { + clearstatcache(); + + $updated = false; + foreach ($inputs as $input) { + if ($input == '-') { + if (count($inputs) == 1) { + echo("lessc: during watching files is not possible to watch stdin\n"); + exit; + } + else { + continue; + } + } + + if (filemtime($input) > $lastAction) { + $updated = true; + break; + } + } + + if ($updated) { + $lastAction = time(); + $parser = new Less_Parser($env); + foreach ($inputs as $input) { + try { + $parser->parseFile($input, $rootpath); + } + catch (Exception $e) { + echo("lessc: " . $e->getMessage() . " \n"); + continue; // Invalid processing + } + } + + file_put_contents($output, $parser->getCss()); + echo("lessc: output file recompiled\n"); + } + + sleep(1); + } +} +else { + $parser = new Less_Parser($env); + foreach ($inputs as $input) { + if ($input == '-') { + $content = file_get_contents('php://stdin'); + $parser->parse($content); + } + else { + try { + $parser->parseFile($input); + } + catch (Exception $e) { + if (!$silent) { + echo("lessc: " . ((string)$e) . " \n"); + } + } + } + } + + if ($output) { + file_put_contents($output, $parser->getCss()); + } + else { + echo $parser->getCss(); + } +} diff --git a/less.php/composer.json b/less.php/composer.json new file mode 100644 index 0000000..970dd27 --- /dev/null +++ b/less.php/composer.json @@ -0,0 +1,49 @@ +{ + "name": "wikimedia/less.php", + "description": "PHP port of the Javascript version of LESS http://lesscss.org (Originally maintained by Josh Schmidt)", + "keywords": [ "less", "css", "php", "stylesheet", "less.js", "lesscss" ], + "license": "Apache-2.0", + "authors": [ + { + "name": "Josh Schmidt", + "homepage": "https://github.com/oyejorge" + }, + { + "name": "Matt Agar", + "homepage": "https://github.com/agar" + }, + { + "name": "Martin Jantošovič", + "homepage": "https://github.com/Mordred" + } + ], + "require": { + "php": ">=7.2.9" + }, + "require-dev": { + "mediawiki/mediawiki-codesniffer": "34.0.0", + "mediawiki/minus-x": "1.0.0", + "php-parallel-lint/php-console-highlighter": "0.5.0", + "php-parallel-lint/php-parallel-lint": "1.2.0", + "phpunit/phpunit": "^8.5" + }, + "scripts": { + "test": [ + "parallel-lint . --exclude vendor", + "phpcs -sp", + "phpunit", + "minus-x check ." + ], + "fix": [ + "minus-x fix .", + "phpcbf" + ] + }, + "autoload": { + "psr-0": { "Less": "lib/" }, + "classmap": ["lessc.inc.php"] + }, + "bin": [ + "bin/lessc" + ] +} diff --git a/less.php/lessc.inc.php b/less.php/lessc.inc.php new file mode 100644 index 0000000..284445f --- /dev/null +++ b/less.php/lessc.inc.php @@ -0,0 +1,274 @@ +importDir = (array)$dirs; + } + + public function addImportDir( $dir ) { + $this->importDir = (array)$this->importDir; + $this->importDir[] = $dir; + } + + public function setFormatter( $name ) { + $this->formatterName = $name; + } + + public function setPreserveComments( $preserve ) { + } + + public function registerFunction( $name, $func ) { + $this->libFunctions[$name] = $func; + } + + public function unregisterFunction( $name ) { + unset( $this->libFunctions[$name] ); + } + + public function setVariables( $variables ) { + foreach ( $variables as $name => $value ) { + $this->setVariable( $name, $value ); + } + } + + public function setVariable( $name, $value ) { + $this->registeredVars[$name] = $value; + } + + public function unsetVariable( $name ) { + unset( $this->registeredVars[$name] ); + } + + public function setOptions( $options ) { + foreach ( $options as $name => $value ) { + $this->setOption( $name, $value ); + } + } + + public function setOption( $name, $value ) { + $this->options[$name] = $value; + } + + public function parse( $buffer, $presets = array() ) { + $this->setVariables( $presets ); + + $parser = new Less_Parser( $this->getOptions() ); + $parser->setImportDirs( $this->getImportDirs() ); + foreach ( $this->libFunctions as $name => $func ) { + $parser->registerFunction( $name, $func ); + } + $parser->parse( $buffer ); + if ( count( $this->registeredVars ) ) { + $parser->ModifyVars( $this->registeredVars ); + } + + return $parser->getCss(); + } + + protected function getOptions() { + $options = array( 'relativeUrls' => false ); + switch ( $this->formatterName ) { + case 'compressed': + $options['compress'] = true; + break; + } + if ( is_array( $this->options ) ) { + $options = array_merge( $options, $this->options ); + } + return $options; + } + + protected function getImportDirs() { + $dirs_ = (array)$this->importDir; + $dirs = array(); + foreach ( $dirs_ as $dir ) { + $dirs[$dir] = ''; + } + return $dirs; + } + + public function compile( $string, $name = null ) { + $oldImport = $this->importDir; + $this->importDir = (array)$this->importDir; + + $this->allParsedFiles = array(); + + $parser = new Less_Parser( $this->getOptions() ); + $parser->SetImportDirs( $this->getImportDirs() ); + if ( count( $this->registeredVars ) ) { + $parser->ModifyVars( $this->registeredVars ); + } + foreach ( $this->libFunctions as $name => $func ) { + $parser->registerFunction( $name, $func ); + } + $parser->parse( $string ); + $out = $parser->getCss(); + + $parsed = Less_Parser::AllParsedFiles(); + foreach ( $parsed as $file ) { + $this->addParsedFile( $file ); + } + + $this->importDir = $oldImport; + + 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[] = Less_Parser::AbsPath( $pi['dirname'] ).'/'; + + $this->allParsedFiles = array(); + $this->addParsedFile( $fname ); + + $parser = new Less_Parser( $this->getOptions() ); + $parser->SetImportDirs( $this->getImportDirs() ); + if ( count( $this->registeredVars ) ) { + $parser->ModifyVars( $this->registeredVars ); + } + foreach ( $this->libFunctions as $name => $func ) { + $parser->registerFunction( $name, $func ); + } + $parser->parseFile( $fname ); + $out = $parser->getCss(); + + $parsed = Less_Parser::AllParsedFiles(); + foreach ( $parsed as $file ) { + $this->addParsedFile( $file ); + } + + $this->importDir = $oldImport; + + if ( $outFname !== null ) { + return file_put_contents( $outFname, $out ); + } + + return $out; + } + + 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; + } + } + + public 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 ); + } + + public function allParsedFiles() { + return $this->allParsedFiles; + } + + protected function addParsedFile( $file ) { + $this->allParsedFiles[Less_Parser::AbsPath( $file )] = filemtime( $file ); + } +} diff --git a/less.php/lib/Less/.easymin/ignore_prefixes b/less.php/lib/Less/.easymin/ignore_prefixes new file mode 100644 index 0000000..ca953b2 --- /dev/null +++ b/less.php/lib/Less/.easymin/ignore_prefixes @@ -0,0 +1,2 @@ +.easymin +Autoloader.php diff --git a/less.php/lib/Less/Autoloader.php b/less.php/lib/Less/Autoloader.php new file mode 100644 index 0000000..00116f7 --- /dev/null +++ b/less.php/lib/Less/Autoloader.php @@ -0,0 +1,77 @@ + '/' ); + } + + // generate name for compiled css file + $hash = md5( json_encode( $less_files ) ); + $list_file = Less_Cache::$cache_dir . Less_Cache::$prefix . $hash . '.list'; + + // check cached content + if ( !isset( $parser_options['use_cache'] ) || $parser_options['use_cache'] === true ) { + if ( file_exists( $list_file ) ) { + + self::ListFiles( $list_file, $list, $cached_name ); + $compiled_name = self::CompiledName( $list, $hash ); + + // if $cached_name is the same as the $compiled name, don't regenerate + if ( !$cached_name || $cached_name === $compiled_name ) { + + $output_file = self::OutputFile( $compiled_name, $parser_options ); + + if ( $output_file && file_exists( $output_file ) ) { + @touch( $list_file ); + return basename( $output_file ); // for backwards compatibility, we just return the name of the file + } + } + } + } + + $compiled = self::Cache( $less_files, $parser_options ); + if ( !$compiled ) { + return false; + } + + $compiled_name = self::CompiledName( $less_files, $hash ); + $output_file = self::OutputFile( $compiled_name, $parser_options ); + + // save the file list + $list = $less_files; + $list[] = $compiled_name; + $cache = implode( "\n", $list ); + file_put_contents( $list_file, $cache ); + + // save the css + file_put_contents( $output_file, $compiled ); + + // clean up + self::CleanCache(); + + return basename( $output_file ); + } + + /** + * Force the compiler to regenerate the cached css file + * + * @param array $less_files Array of .less files to compile + * @param array $parser_options Array of compiler options + * @param array $modify_vars Array of variables + * @return string Name of the css file + */ + public static function Regen( $less_files, $parser_options = array(), $modify_vars = array() ) { + $parser_options['use_cache'] = false; + return self::Get( $less_files, $parser_options, $modify_vars ); + } + + public static function Cache( &$less_files, $parser_options = array() ) { + // get less.php if it exists + $file = dirname( __FILE__ ) . '/Less.php'; + if ( file_exists( $file ) && !class_exists( 'Less_Parser' ) ) { + require_once $file; + } + + $parser_options['cache_dir'] = Less_Cache::$cache_dir; + $parser = new Less_Parser( $parser_options ); + + // combine files + foreach ( $less_files as $file_path => $uri_or_less ) { + + // treat as less markup if there are newline characters + if ( strpos( $uri_or_less, "\n" ) !== false ) { + $parser->Parse( $uri_or_less ); + continue; + } + + $parser->ParseFile( $file_path, $uri_or_less ); + } + + $compiled = $parser->getCss(); + + $less_files = $parser->allParsedFiles(); + + return $compiled; + } + + private static function OutputFile( $compiled_name, $parser_options ) { + // custom output file + if ( !empty( $parser_options['output'] ) ) { + + // relative to cache directory? + if ( preg_match( '#[\\\\/]#', $parser_options['output'] ) ) { + return $parser_options['output']; + } + + return Less_Cache::$cache_dir.$parser_options['output']; + } + + return Less_Cache::$cache_dir.$compiled_name; + } + + private static function CompiledName( $files, $extrahash ) { + // save the file list + $temp = array( Less_Version::cache_version ); + foreach ( $files as $file ) { + $temp[] = filemtime( $file )."\t".filesize( $file )."\t".$file; + } + + return Less_Cache::$prefix.sha1( json_encode( $temp ).$extrahash ).'.css'; + } + + public static function SetCacheDir( $dir ) { + Less_Cache::$cache_dir = $dir; + self::CheckCacheDir(); + } + + public static function CheckCacheDir() { + Less_Cache::$cache_dir = str_replace( '\\', '/', Less_Cache::$cache_dir ); + Less_Cache::$cache_dir = rtrim( Less_Cache::$cache_dir, '/' ).'/'; + + if ( !file_exists( Less_Cache::$cache_dir ) ) { + if ( !mkdir( Less_Cache::$cache_dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: '.Less_Cache::$cache_dir ); + } + + } elseif ( !is_dir( Less_Cache::$cache_dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: '.Less_Cache::$cache_dir ); + + } elseif ( !is_writable( Less_Cache::$cache_dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: '.Less_Cache::$cache_dir ); + + } + + } + + /** + * Delete unused less.php files + * + */ + public static function CleanCache() { + static $clean = false; + + if ( $clean || empty( Less_Cache::$cache_dir ) ) { + return; + } + + $clean = true; + + // only remove files with extensions created by less.php + // css files removed based on the list files + $remove_types = array( 'lesscache' => 1,'list' => 1,'less' => 1,'map' => 1 ); + + $files = scandir( Less_Cache::$cache_dir ); + if ( !$files ) { + return; + } + + $check_time = time() - self::$gc_lifetime; + foreach ( $files as $file ) { + + // don't delete if the file wasn't created with less.php + if ( strpos( $file, Less_Cache::$prefix ) !== 0 ) { + continue; + } + + $parts = explode( '.', $file ); + $type = array_pop( $parts ); + + if ( !isset( $remove_types[$type] ) ) { + continue; + } + + $full_path = Less_Cache::$cache_dir . $file; + $mtime = filemtime( $full_path ); + + // don't delete if it's a relatively new file + if ( $mtime > $check_time ) { + continue; + } + + // delete the list file and associated css file + if ( $type === 'list' ) { + self::ListFiles( $full_path, $list, $css_file_name ); + if ( $css_file_name ) { + $css_file = Less_Cache::$cache_dir . $css_file_name; + if ( file_exists( $css_file ) ) { + unlink( $css_file ); + } + } + } + + unlink( $full_path ); + } + + } + + /** + * Get the list of less files and generated css file from a list file + * + */ + static function ListFiles( $list_file, &$list, &$css_file_name ) { + $list = explode( "\n", file_get_contents( $list_file ) ); + + // pop the cached name that should match $compiled_name + $css_file_name = array_pop( $list ); + + if ( !preg_match( '/^' . Less_Cache::$prefix . '[a-f0-9]+\.css$/', $css_file_name ) ) { + $list[] = $css_file_name; + $css_file_name = false; + } + + } + +} diff --git a/less.php/lib/Less/Colors.php b/less.php/lib/Less/Colors.php new file mode 100644 index 0000000..9be76cb --- /dev/null +++ b/less.php/lib/Less/Colors.php @@ -0,0 +1,169 @@ + '#f0f8ff', + 'antiquewhite' => '#faebd7', + 'aqua' => '#00ffff', + 'aquamarine' => '#7fffd4', + 'azure' => '#f0ffff', + 'beige' => '#f5f5dc', + 'bisque' => '#ffe4c4', + 'black' => '#000000', + 'blanchedalmond' => '#ffebcd', + 'blue' => '#0000ff', + 'blueviolet' => '#8a2be2', + 'brown' => '#a52a2a', + 'burlywood' => '#deb887', + 'cadetblue' => '#5f9ea0', + 'chartreuse' => '#7fff00', + 'chocolate' => '#d2691e', + 'coral' => '#ff7f50', + 'cornflowerblue' => '#6495ed', + 'cornsilk' => '#fff8dc', + 'crimson' => '#dc143c', + 'cyan' => '#00ffff', + 'darkblue' => '#00008b', + 'darkcyan' => '#008b8b', + 'darkgoldenrod' => '#b8860b', + 'darkgray' => '#a9a9a9', + 'darkgrey' => '#a9a9a9', + 'darkgreen' => '#006400', + 'darkkhaki' => '#bdb76b', + 'darkmagenta' => '#8b008b', + 'darkolivegreen' => '#556b2f', + 'darkorange' => '#ff8c00', + 'darkorchid' => '#9932cc', + 'darkred' => '#8b0000', + 'darksalmon' => '#e9967a', + 'darkseagreen' => '#8fbc8f', + 'darkslateblue' => '#483d8b', + 'darkslategray' => '#2f4f4f', + 'darkslategrey' => '#2f4f4f', + 'darkturquoise' => '#00ced1', + 'darkviolet' => '#9400d3', + 'deeppink' => '#ff1493', + 'deepskyblue' => '#00bfff', + 'dimgray' => '#696969', + 'dimgrey' => '#696969', + 'dodgerblue' => '#1e90ff', + 'firebrick' => '#b22222', + 'floralwhite' => '#fffaf0', + 'forestgreen' => '#228b22', + 'fuchsia' => '#ff00ff', + 'gainsboro' => '#dcdcdc', + 'ghostwhite' => '#f8f8ff', + 'gold' => '#ffd700', + 'goldenrod' => '#daa520', + 'gray' => '#808080', + 'grey' => '#808080', + 'green' => '#008000', + 'greenyellow' => '#adff2f', + 'honeydew' => '#f0fff0', + 'hotpink' => '#ff69b4', + 'indianred' => '#cd5c5c', + 'indigo' => '#4b0082', + 'ivory' => '#fffff0', + 'khaki' => '#f0e68c', + 'lavender' => '#e6e6fa', + 'lavenderblush' => '#fff0f5', + 'lawngreen' => '#7cfc00', + 'lemonchiffon' => '#fffacd', + 'lightblue' => '#add8e6', + 'lightcoral' => '#f08080', + 'lightcyan' => '#e0ffff', + 'lightgoldenrodyellow' => '#fafad2', + 'lightgray' => '#d3d3d3', + 'lightgrey' => '#d3d3d3', + 'lightgreen' => '#90ee90', + 'lightpink' => '#ffb6c1', + 'lightsalmon' => '#ffa07a', + 'lightseagreen' => '#20b2aa', + 'lightskyblue' => '#87cefa', + 'lightslategray' => '#778899', + 'lightslategrey' => '#778899', + 'lightsteelblue' => '#b0c4de', + 'lightyellow' => '#ffffe0', + 'lime' => '#00ff00', + 'limegreen' => '#32cd32', + 'linen' => '#faf0e6', + 'magenta' => '#ff00ff', + 'maroon' => '#800000', + 'mediumaquamarine' => '#66cdaa', + 'mediumblue' => '#0000cd', + 'mediumorchid' => '#ba55d3', + 'mediumpurple' => '#9370d8', + 'mediumseagreen' => '#3cb371', + 'mediumslateblue' => '#7b68ee', + 'mediumspringgreen' => '#00fa9a', + 'mediumturquoise' => '#48d1cc', + 'mediumvioletred' => '#c71585', + 'midnightblue' => '#191970', + 'mintcream' => '#f5fffa', + 'mistyrose' => '#ffe4e1', + 'moccasin' => '#ffe4b5', + 'navajowhite' => '#ffdead', + 'navy' => '#000080', + 'oldlace' => '#fdf5e6', + 'olive' => '#808000', + 'olivedrab' => '#6b8e23', + 'orange' => '#ffa500', + 'orangered' => '#ff4500', + 'orchid' => '#da70d6', + 'palegoldenrod' => '#eee8aa', + 'palegreen' => '#98fb98', + 'paleturquoise' => '#afeeee', + 'palevioletred' => '#d87093', + 'papayawhip' => '#ffefd5', + 'peachpuff' => '#ffdab9', + 'peru' => '#cd853f', + 'pink' => '#ffc0cb', + 'plum' => '#dda0dd', + 'powderblue' => '#b0e0e6', + 'purple' => '#800080', + 'red' => '#ff0000', + 'rosybrown' => '#bc8f8f', + 'royalblue' => '#4169e1', + 'saddlebrown' => '#8b4513', + 'salmon' => '#fa8072', + 'sandybrown' => '#f4a460', + 'seagreen' => '#2e8b57', + 'seashell' => '#fff5ee', + 'sienna' => '#a0522d', + 'silver' => '#c0c0c0', + 'skyblue' => '#87ceeb', + 'slateblue' => '#6a5acd', + 'slategray' => '#708090', + 'slategrey' => '#708090', + 'snow' => '#fffafa', + 'springgreen' => '#00ff7f', + 'steelblue' => '#4682b4', + 'tan' => '#d2b48c', + 'teal' => '#008080', + 'thistle' => '#d8bfd8', + 'tomato' => '#ff6347', + 'turquoise' => '#40e0d0', + 'violet' => '#ee82ee', + 'wheat' => '#f5deb3', + 'white' => '#ffffff', + 'whitesmoke' => '#f5f5f5', + 'yellow' => '#ffff00', + 'yellowgreen' => '#9acd32' + ); + + public static function hasOwnProperty( $color ) { + return isset( self::$colors[$color] ); + } + + public static function color( $color ) { + return self::$colors[$color]; + } + +} diff --git a/less.php/lib/Less/Configurable.php b/less.php/lib/Less/Configurable.php new file mode 100644 index 0000000..7f1348b --- /dev/null +++ b/less.php/lib/Less/Configurable.php @@ -0,0 +1,66 @@ +defaultOptions ); + $this->options = array_merge( $this->defaultOptions, $this->options, $options ); + } + + /** + * Get an option value by name + * + * If the option is empty or not set a NULL value will be returned. + * + * @param string $name + * @param mixed $default Default value if confiuration of $name is not present + * @return mixed + */ + public function getOption( $name, $default = null ) { + if ( isset( $this->options[$name] ) ) { + return $this->options[$name]; + } + return $default; + } + + /** + * Set an option + * + * @param string $name + * @param mixed $value + */ + public function setOption( $name, $value ) { + $this->options[$name] = $value; + } + +} diff --git a/less.php/lib/Less/Environment.php b/less.php/lib/Less/Environment.php new file mode 100644 index 0000000..2555d03 --- /dev/null +++ b/less.php/lib/Less/Environment.php @@ -0,0 +1,157 @@ + ',', + ': ' => ':', + '' => '', + ' ' => ' ', + ':' => ' :', + '+' => '+', + '~' => '~', + '>' => '>', + '|' => '|', + '^' => '^', + '^^' => '^^' + ); + + } else { + + Less_Environment::$_outputMap = array( + ',' => ', ', + ': ' => ': ', + '' => '', + ' ' => ' ', + ':' => ' :', + '+' => ' + ', + '~' => ' ~ ', + '>' => ' > ', + '|' => '|', + '^' => ' ^ ', + '^^' => ' ^^ ' + ); + + } + } + + public function copyEvalEnv( $frames = array() ) { + $new_env = new Less_Environment(); + $new_env->frames = $frames; + return $new_env; + } + + public static function isMathOn() { + return !Less_Parser::$options['strictMath'] || Less_Environment::$parensStack; + } + + public static function isPathRelative( $path ) { + return !preg_match( '/^(?:[a-z-]+:|\/)/', $path ); + } + + /** + * Canonicalize a path by resolving references to '/./', '/../' + * Does not remove leading "../" + * @param string path or url + * @return string Canonicalized path + * + */ + public static function normalizePath( $path ) { + $segments = explode( '/', $path ); + $segments = array_reverse( $segments ); + + $path = array(); + $path_len = 0; + + while ( $segments ) { + $segment = array_pop( $segments ); + switch ( $segment ) { + + case '.': + break; + + case '..': + if ( !$path_len || ( $path[$path_len - 1] === '..' ) ) { + $path[] = $segment; + $path_len++; + } else { + array_pop( $path ); + $path_len--; + } + break; + + default: + $path[] = $segment; + $path_len++; + break; + } + } + + return implode( '/', $path ); + } + + public function unshiftFrame( $frame ) { + array_unshift( $this->frames, $frame ); + } + + public function shiftFrame() { + return array_shift( $this->frames ); + } + +} diff --git a/less.php/lib/Less/Exception/Chunk.php b/less.php/lib/Less/Exception/Chunk.php new file mode 100644 index 0000000..1f1dddb --- /dev/null +++ b/less.php/lib/Less/Exception/Chunk.php @@ -0,0 +1,203 @@ +message = 'ParseError: Unexpected input'; // default message + + $this->index = $index; + + $this->currentFile = $currentFile; + + $this->input = $input; + $this->input_len = strlen( $input ); + + $this->Chunks(); + $this->genMessage(); + } + + /** + * See less.js chunks() + * We don't actually need the chunks + * + */ + protected function Chunks() { + $level = 0; + $parenLevel = 0; + $lastMultiCommentEndBrace = null; + $lastOpening = null; + $lastMultiComment = null; + $lastParen = null; + + for ( $this->parserCurrentIndex = 0; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) { + $cc = $this->CharCode( $this->parserCurrentIndex ); + if ( ( ( $cc >= 97 ) && ( $cc <= 122 ) ) || ( $cc < 34 ) ) { + // a-z or whitespace + continue; + } + + switch ( $cc ) { + + // ( + case 40: + $parenLevel++; + $lastParen = $this->parserCurrentIndex; + break; + + // ) + case 41: + $parenLevel--; + if ( $parenLevel < 0 ) { + return $this->fail( "missing opening `(`" ); + } + break; + + // ; + case 59: + // if (!$parenLevel) { $this->emitChunk(); } + break; + + // { + case 123: + $level++; + $lastOpening = $this->parserCurrentIndex; + break; + + // } + case 125: + $level--; + if ( $level < 0 ) { + return $this->fail( "missing opening `{`" ); + + } + // if (!$level && !$parenLevel) { $this->emitChunk(); } + break; + // \ + case 92: + if ( $this->parserCurrentIndex < $this->input_len - 1 ) { $this->parserCurrentIndex++; break; + } + return $this->fail( "unescaped `\\`" ); + + // ", ' and ` + case 34: + case 39: + case 96: + $matched = 0; + $currentChunkStartIndex = $this->parserCurrentIndex; + for ( $this->parserCurrentIndex = $this->parserCurrentIndex + 1; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) { + $cc2 = $this->CharCode( $this->parserCurrentIndex ); + if ( $cc2 > 96 ) { continue; + } + if ( $cc2 == $cc ) { $matched = 1; break; + } + if ( $cc2 == 92 ) { // \ + if ( $this->parserCurrentIndex == $this->input_len - 1 ) { + return $this->fail( "unescaped `\\`" ); + } + $this->parserCurrentIndex++; + } + } + if ( $matched ) { break; + } + return $this->fail( "unmatched `" . chr( $cc ) . "`", $currentChunkStartIndex ); + + // /, check for comment + case 47: + if ( $parenLevel || ( $this->parserCurrentIndex == $this->input_len - 1 ) ) { break; + } + $cc2 = $this->CharCode( $this->parserCurrentIndex + 1 ); + if ( $cc2 == 47 ) { + // //, find lnfeed + for ( $this->parserCurrentIndex = $this->parserCurrentIndex + 2; $this->parserCurrentIndex < $this->input_len; $this->parserCurrentIndex++ ) { + $cc2 = $this->CharCode( $this->parserCurrentIndex ); + if ( ( $cc2 <= 13 ) && ( ( $cc2 == 10 ) || ( $cc2 == 13 ) ) ) { break; + } + } + } else if ( $cc2 == 42 ) { + // /*, find */ + $lastMultiComment = $currentChunkStartIndex = $this->parserCurrentIndex; + for ( $this->parserCurrentIndex = $this->parserCurrentIndex + 2; $this->parserCurrentIndex < $this->input_len - 1; $this->parserCurrentIndex++ ) { + $cc2 = $this->CharCode( $this->parserCurrentIndex ); + if ( $cc2 == 125 ) { $lastMultiCommentEndBrace = $this->parserCurrentIndex; + } + if ( $cc2 != 42 ) { continue; + } + if ( $this->CharCode( $this->parserCurrentIndex + 1 ) == 47 ) { break; + } + } + if ( $this->parserCurrentIndex == $this->input_len - 1 ) { + return $this->fail( "missing closing `*/`", $currentChunkStartIndex ); + } + } + break; + + // *, check for unmatched */ + case 42: + if ( ( $this->parserCurrentIndex < $this->input_len - 1 ) && ( $this->CharCode( $this->parserCurrentIndex + 1 ) == 47 ) ) { + return $this->fail( "unmatched `/*`" ); + } + break; + } + } + + if ( $level !== 0 ) { + if ( ( $lastMultiComment > $lastOpening ) && ( $lastMultiCommentEndBrace > $lastMultiComment ) ) { + return $this->fail( "missing closing `}` or `*/`", $lastOpening ); + } else { + return $this->fail( "missing closing `}`", $lastOpening ); + } + } else if ( $parenLevel !== 0 ) { + return $this->fail( "missing closing `)`", $lastParen ); + } + + // chunk didn't fail + + //$this->emitChunk(true); + } + + public function CharCode( $pos ) { + return ord( $this->input[$pos] ); + } + + public function fail( $msg, $index = null ) { + if ( !$index ) { + $this->index = $this->parserCurrentIndex; + } else { + $this->index = $index; + } + $this->message = 'ParseError: '.$msg; + } + + /* + function emitChunk( $force = false ){ + $len = $this->parserCurrentIndex - $this->emitFrom; + if ((($len < 512) && !$force) || !$len) { + return; + } + $chunks[] = substr($this->input, $this->emitFrom, $this->parserCurrentIndex + 1 - $this->emitFrom ); + $this->emitFrom = $this->parserCurrentIndex + 1; + } + */ + +} diff --git a/less.php/lib/Less/Exception/Compiler.php b/less.php/lib/Less/Exception/Compiler.php new file mode 100644 index 0000000..1c3727a --- /dev/null +++ b/less.php/lib/Less/Exception/Compiler.php @@ -0,0 +1,11 @@ +previous = $previous; + parent::__construct( $message, $code ); + } else { + parent::__construct( $message, $code, $previous ); + } + + $this->currentFile = $currentFile; + $this->index = $index; + + $this->genMessage(); + } + + protected function getInput() { + if ( !$this->input && $this->currentFile && $this->currentFile['filename'] && file_exists( $this->currentFile['filename'] ) ) { + $this->input = file_get_contents( $this->currentFile['filename'] ); + } + } + + /** + * Converts the exception to string + * + * @return string + */ + public function genMessage() { + if ( $this->currentFile && $this->currentFile['filename'] ) { + $this->message .= ' in '.basename( $this->currentFile['filename'] ); + } + + if ( $this->index !== null ) { + $this->getInput(); + if ( $this->input ) { + $line = self::getLineNumber(); + $this->message .= ' on line '.$line.', column '.self::getColumn(); + + $lines = explode( "\n", $this->input ); + + $count = count( $lines ); + $start_line = max( 0, $line - 3 ); + $last_line = min( $count, $start_line + 6 ); + $num_len = strlen( $last_line ); + for ( $i = $start_line; $i < $last_line; $i++ ) { + $this->message .= "\n".str_pad( $i + 1, $num_len, '0', STR_PAD_LEFT ).'| '.$lines[$i]; + } + } + } + + } + + /** + * Returns the line number the error was encountered + * + * @return integer + */ + public function getLineNumber() { + if ( $this->index ) { + // https://bugs.php.net/bug.php?id=49790 + if ( ini_get( "mbstring.func_overload" ) ) { + return substr_count( substr( $this->input, 0, $this->index ), "\n" ) + 1; + } else { + return substr_count( $this->input, "\n", 0, $this->index ) + 1; + } + } + return 1; + } + + /** + * Returns the column the error was encountered + * + * @return integer + */ + public function getColumn() { + $part = substr( $this->input, 0, $this->index ); + $pos = strrpos( $part, "\n" ); + return $this->index - $pos; + } + +} diff --git a/less.php/lib/Less/Functions.php b/less.php/lib/Less/Functions.php new file mode 100644 index 0000000..c9e97bd --- /dev/null +++ b/less.php/lib/Less/Functions.php @@ -0,0 +1,1186 @@ +env = $env; + $this->currentFileInfo = $currentFileInfo; + } + + /** + * @param string $op + */ + public static function operate( $op, $a, $b ) { + switch ( $op ) { + case '+': +return $a + $b; + case '-': +return $a - $b; + case '*': +return $a * $b; + case '/': +return $a / $b; + } + } + + public static function clamp( $val, $max = 1 ) { + return min( max( $val, 0 ), $max ); + } + + public static function fround( $value ) { + if ( $value === 0 ) { + return $value; + } + + if ( Less_Parser::$options['numPrecision'] ) { + $p = pow( 10, Less_Parser::$options['numPrecision'] ); + return round( $value * $p ) / $p; + } + return $value; + } + + public static function number( $n ) { + if ( $n instanceof Less_Tree_Dimension ) { + return floatval( $n->unit->is( '%' ) ? $n->value / 100 : $n->value ); + } else if ( is_numeric( $n ) ) { + return $n; + } else { + throw new Less_Exception_Compiler( "color functions take numbers as parameters" ); + } + } + + public static function scaled( $n, $size = 255 ) { + if ( $n instanceof Less_Tree_Dimension && $n->unit->is( '%' ) ) { + return (float)$n->value * $size / 100; + } else { + return Less_Functions::number( $n ); + } + } + + public function rgb( $r = null, $g = null, $b = null ) { + if ( is_null( $r ) || is_null( $g ) || is_null( $b ) ) { + throw new Less_Exception_Compiler( "rgb expects three parameters" ); + } + return $this->rgba( $r, $g, $b, 1.0 ); + } + + public function rgba( $r = null, $g = null, $b = null, $a = null ) { + $rgb = array( $r, $g, $b ); + $rgb = array_map( array( 'Less_Functions','scaled' ), $rgb ); + + $a = self::number( $a ); + return new Less_Tree_Color( $rgb, $a ); + } + + public function hsl( $h, $s, $l ) { + return $this->hsla( $h, $s, $l, 1.0 ); + } + + public function hsla( $h, $s, $l, $a ) { + $h = fmod( self::number( $h ), 360 ) / 360; // Classic % operator will change float to int + $s = self::clamp( self::number( $s ) ); + $l = self::clamp( self::number( $l ) ); + $a = self::clamp( self::number( $a ) ); + + $m2 = $l <= 0.5 ? $l * ( $s + 1 ) : $l + $s - $l * $s; + + $m1 = $l * 2 - $m2; + + return $this->rgba( self::hsla_hue( $h + 1 / 3, $m1, $m2 ) * 255, + self::hsla_hue( $h, $m1, $m2 ) * 255, + self::hsla_hue( $h - 1 / 3, $m1, $m2 ) * 255, + $a ); + } + + /** + * @param double $h + */ + public function hsla_hue( $h, $m1, $m2 ) { + $h = $h < 0 ? $h + 1 : ( $h > 1 ? $h - 1 : $h ); + if ( $h * 6 < 1 ) return $m1 + ( $m2 - $m1 ) * $h * 6; else if ( $h * 2 < 1 ) return $m2; else if ( $h * 3 < 2 ) return $m1 + ( $m2 - $m1 ) * ( 2 / 3 - $h ) * 6; else return $m1; + } + + public function hsv( $h, $s, $v ) { + return $this->hsva( $h, $s, $v, 1.0 ); + } + + /** + * @param double $a + */ + public function hsva( $h, $s, $v, $a ) { + $h = ( ( Less_Functions::number( $h ) % 360 ) / 360 ) * 360; + $s = Less_Functions::number( $s ); + $v = Less_Functions::number( $v ); + $a = Less_Functions::number( $a ); + + $i = floor( ( $h / 60 ) % 6 ); + $f = ( $h / 60 ) - $i; + + $vs = array( $v, + $v * ( 1 - $s ), + $v * ( 1 - $f * $s ), + $v * ( 1 - ( 1 - $f ) * $s ) ); + + $perm = array( array( 0, 3, 1 ), + array( 2, 0, 1 ), + array( 1, 0, 3 ), + array( 1, 2, 0 ), + array( 3, 1, 0 ), + array( 0, 1, 2 ) ); + + return $this->rgba( $vs[$perm[$i][0]] * 255, + $vs[$perm[$i][1]] * 255, + $vs[$perm[$i][2]] * 255, + $a ); + } + + public function hue( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to hue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $c = $color->toHSL(); + return new Less_Tree_Dimension( Less_Parser::round( $c['h'] ) ); + } + + public function saturation( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to saturation must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $c = $color->toHSL(); + return new Less_Tree_Dimension( Less_Parser::round( $c['s'] * 100 ), '%' ); + } + + public function lightness( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to lightness must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $c = $color->toHSL(); + return new Less_Tree_Dimension( Less_Parser::round( $c['l'] * 100 ), '%' ); + } + + public function hsvhue( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to hsvhue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsv = $color->toHSV(); + return new Less_Tree_Dimension( Less_Parser::round( $hsv['h'] ) ); + } + + public function hsvsaturation( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to hsvsaturation must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsv = $color->toHSV(); + return new Less_Tree_Dimension( Less_Parser::round( $hsv['s'] * 100 ), '%' ); + } + + public function hsvvalue( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to hsvvalue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsv = $color->toHSV(); + return new Less_Tree_Dimension( Less_Parser::round( $hsv['v'] * 100 ), '%' ); + } + + public function red( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to red must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return new Less_Tree_Dimension( $color->rgb[0] ); + } + + public function green( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to green must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return new Less_Tree_Dimension( $color->rgb[1] ); + } + + public function blue( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to blue must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return new Less_Tree_Dimension( $color->rgb[2] ); + } + + public function alpha( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to alpha must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $c = $color->toHSL(); + return new Less_Tree_Dimension( $c['a'] ); + } + + public function luma( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to luma must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return new Less_Tree_Dimension( Less_Parser::round( $color->luma() * $color->alpha * 100 ), '%' ); + } + + public function luminance( $color = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to luminance must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $luminance = + ( 0.2126 * $color->rgb[0] / 255 ) + + ( 0.7152 * $color->rgb[1] / 255 ) + + ( 0.0722 * $color->rgb[2] / 255 ); + + return new Less_Tree_Dimension( Less_Parser::round( $luminance * $color->alpha * 100 ), '%' ); + } + + public function saturate( $color = null, $amount = null ) { + // filter: saturate(3.2); + // should be kept as is, so check for color + if ( $color instanceof Less_Tree_Dimension ) { + return null; + } + + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to saturate must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to saturate must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + + $hsl['s'] += $amount->value / 100; + $hsl['s'] = self::clamp( $hsl['s'] ); + + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + /** + * @param Less_Tree_Dimension $amount + */ + public function desaturate( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to desaturate must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to desaturate must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + + $hsl['s'] -= $amount->value / 100; + $hsl['s'] = self::clamp( $hsl['s'] ); + + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function lighten( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to lighten must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to lighten must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + + $hsl['l'] += $amount->value / 100; + $hsl['l'] = self::clamp( $hsl['l'] ); + + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function darken( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to darken must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to darken must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + $hsl['l'] -= $amount->value / 100; + $hsl['l'] = self::clamp( $hsl['l'] ); + + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function fadein( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to fadein must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to fadein must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + $hsl['a'] += $amount->value / 100; + $hsl['a'] = self::clamp( $hsl['a'] ); + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function fadeout( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to fadeout must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to fadeout must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + $hsl['a'] -= $amount->value / 100; + $hsl['a'] = self::clamp( $hsl['a'] ); + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function fade( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to fade must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to fade must be a percentage' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + + $hsl['a'] = $amount->value / 100; + $hsl['a'] = self::clamp( $hsl['a'] ); + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + public function spin( $color = null, $amount = null ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to spin must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$amount instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The second argument to spin must be a number' . ( $amount instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $hsl = $color->toHSL(); + $hue = fmod( $hsl['h'] + $amount->value, 360 ); + + $hsl['h'] = $hue < 0 ? 360 + $hue : $hue; + + return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); + } + + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + + /** + * @param Less_Tree_Color $color1 + */ + public function mix( $color1 = null, $color2 = null, $weight = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to mix must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to mix must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$weight ) { + $weight = new Less_Tree_Dimension( '50', '%' ); + } + if ( !$weight instanceof Less_Tree_Dimension ) { + throw new Less_Exception_Compiler( 'The third argument to contrast must be a percentage' . ( $weight instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + $p = $weight->value / 100.0; + $w = $p * 2 - 1; + $hsl1 = $color1->toHSL(); + $hsl2 = $color2->toHSL(); + $a = $hsl1['a'] - $hsl2['a']; + + $w1 = ( ( ( ( $w * $a ) == -1 ) ? $w : ( $w + $a ) / ( 1 + $w * $a ) ) + 1 ) / 2; + $w2 = 1 - $w1; + + $rgb = array( $color1->rgb[0] * $w1 + $color2->rgb[0] * $w2, + $color1->rgb[1] * $w1 + $color2->rgb[1] * $w2, + $color1->rgb[2] * $w1 + $color2->rgb[2] * $w2 ); + + $alpha = $color1->alpha * $p + $color2->alpha * ( 1 - $p ); + + return new Less_Tree_Color( $rgb, $alpha ); + } + + public function greyscale( $color ) { + return $this->desaturate( $color, new Less_Tree_Dimension( 100, '%' ) ); + } + + public function contrast( $color, $dark = null, $light = null, $threshold = null ) { + // filter: contrast(3.2); + // should be kept as is, so check for color + if ( !$color instanceof Less_Tree_Color ) { + return null; + } + if ( !$light ) { + $light = $this->rgba( 255, 255, 255, 1.0 ); + } + if ( !$dark ) { + $dark = $this->rgba( 0, 0, 0, 1.0 ); + } + + if ( !$dark instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to contrast must be a color' . ( $dark instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$light instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The third argument to contrast must be a color' . ( $light instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + // Figure out which is actually light and dark! + if ( $dark->luma() > $light->luma() ) { + $t = $light; + $light = $dark; + $dark = $t; + } + if ( !$threshold ) { + $threshold = 0.43; + } else { + $threshold = Less_Functions::number( $threshold ); + } + + if ( $color->luma() < $threshold ) { + return $light; + } else { + return $dark; + } + } + + public function e( $str ) { + if ( is_string( $str ) ) { + return new Less_Tree_Anonymous( $str ); + } + return new Less_Tree_Anonymous( $str instanceof Less_Tree_JavaScript ? $str->expression : $str->value ); + } + + public function escape( $str ) { + $revert = array( '%21' => '!', '%2A' => '*', '%27' => "'",'%3F' => '?','%26' => '&','%2C' => ',','%2F' => '/','%40' => '@','%2B' => '+','%24' => '$' ); + + return new Less_Tree_Anonymous( strtr( rawurlencode( $str->value ), $revert ) ); + } + + /** + * todo: This function will need some additional work to make it work the same as less.js + * + */ + public function replace( $string, $pattern, $replacement, $flags = null ) { + $result = $string->value; + + $expr = '/'.str_replace( '/', '\\/', $pattern->value ).'/'; + if ( $flags && $flags->value ) { + $expr .= self::replace_flags( $flags->value ); + } + + $result = preg_replace( $expr, $replacement->value, $result ); + + if ( property_exists( $string, 'quote' ) ) { + return new Less_Tree_Quoted( $string->quote, $result, $string->escaped ); + } + return new Less_Tree_Quoted( '', $result ); + } + + public static function replace_flags( $flags ) { + $flags = str_split( $flags, 1 ); + $new_flags = ''; + + foreach ( $flags as $flag ) { + switch ( $flag ) { + case 'e': + case 'g': + break; + + default: + $new_flags .= $flag; + break; + } + } + + return $new_flags; + } + + public function _percent() { + $string = func_get_arg( 0 ); + + $args = func_get_args(); + array_shift( $args ); + $result = $string->value; + + foreach ( $args as $arg ) { + if ( preg_match( '/%[sda]/i', $result, $token ) ) { + $token = $token[0]; + $value = stristr( $token, 's' ) ? $arg->value : $arg->toCSS(); + $value = preg_match( '/[A-Z]$/', $token ) ? urlencode( $value ) : $value; + $result = preg_replace( '/%[sda]/i', $value, $result, 1 ); + } + } + $result = str_replace( '%%', '%', $result ); + + return new Less_Tree_Quoted( $string->quote, $result, $string->escaped ); + } + + public function unit( $val, $unit = null ) { + if ( !( $val instanceof Less_Tree_Dimension ) ) { + throw new Less_Exception_Compiler( 'The first argument to unit must be a number' . ( $val instanceof Less_Tree_Operation ? '. Have you forgotten parenthesis?' : '.' ) ); + } + + if ( $unit ) { + if ( $unit instanceof Less_Tree_Keyword ) { + $unit = $unit->value; + } else { + $unit = $unit->toCSS(); + } + } else { + $unit = ""; + } + return new Less_Tree_Dimension( $val->value, $unit ); + } + + public function convert( $val, $unit ) { + return $val->convertTo( $unit->value ); + } + + public function round( $n, $f = false ) { + $fraction = 0; + if ( $f !== false ) { + $fraction = $f->value; + } + + return $this->_math( 'Less_Parser::round', null, $n, $fraction ); + } + + public function pi() { + return new Less_Tree_Dimension( M_PI ); + } + + public function mod( $a, $b ) { + return new Less_Tree_Dimension( $a->value % $b->value, $a->unit ); + } + + public function pow( $x, $y ) { + if ( is_numeric( $x ) && is_numeric( $y ) ) { + $x = new Less_Tree_Dimension( $x ); + $y = new Less_Tree_Dimension( $y ); + } elseif ( !( $x instanceof Less_Tree_Dimension ) || !( $y instanceof Less_Tree_Dimension ) ) { + throw new Less_Exception_Compiler( 'Arguments must be numbers' ); + } + + return new Less_Tree_Dimension( pow( $x->value, $y->value ), $x->unit ); + } + + // var mathFunctions = [{name:"ce ... + public function ceil( $n ) { + return $this->_math( 'ceil', null, $n ); + } + + public function floor( $n ) { + return $this->_math( 'floor', null, $n ); + } + + public function sqrt( $n ) { + return $this->_math( 'sqrt', null, $n ); + } + + public function abs( $n ) { + return $this->_math( 'abs', null, $n ); + } + + public function tan( $n ) { + return $this->_math( 'tan', '', $n ); + } + + public function sin( $n ) { + return $this->_math( 'sin', '', $n ); + } + + public function cos( $n ) { + return $this->_math( 'cos', '', $n ); + } + + public function atan( $n ) { + return $this->_math( 'atan', 'rad', $n ); + } + + public function asin( $n ) { + return $this->_math( 'asin', 'rad', $n ); + } + + public function acos( $n ) { + return $this->_math( 'acos', 'rad', $n ); + } + + private function _math() { + $args = func_get_args(); + $fn = array_shift( $args ); + $unit = array_shift( $args ); + + if ( $args[0] instanceof Less_Tree_Dimension ) { + + if ( $unit === null ) { + $unit = $args[0]->unit; + } else { + $args[0] = $args[0]->unify(); + } + $args[0] = (float)$args[0]->value; + return new Less_Tree_Dimension( call_user_func_array( $fn, $args ), $unit ); + } else if ( is_numeric( $args[0] ) ) { + return call_user_func_array( $fn, $args ); + } else { + throw new Less_Exception_Compiler( "math functions take numbers as parameters" ); + } + } + + /** + * @param boolean $isMin + */ + private function _minmax( $isMin, $args ) { + $arg_count = count( $args ); + + if ( $arg_count < 1 ) { + throw new Less_Exception_Compiler( 'one or more arguments required' ); + } + + $j = null; + $unitClone = null; + $unitStatic = null; + + $order = array(); // elems only contains original argument values. + $values = array(); // key is the unit.toString() for unified tree.Dimension values, + // value is the index into the order array. + + for ( $i = 0; $i < $arg_count; $i++ ) { + $current = $args[$i]; + if ( !( $current instanceof Less_Tree_Dimension ) ) { + if ( is_array( $args[$i]->value ) ) { + $args[] = $args[$i]->value; + } + continue; + } + + if ( $current->unit->toString() === '' && !$unitClone ) { + $temp = new Less_Tree_Dimension( $current->value, $unitClone ); + $currentUnified = $temp->unify(); + } else { + $currentUnified = $current->unify(); + } + + if ( $currentUnified->unit->toString() === "" && !$unitStatic ) { + $unit = $unitStatic; + } else { + $unit = $currentUnified->unit->toString(); + } + + if ( $unit !== '' && !$unitStatic || $unit !== '' && $order[0]->unify()->unit->toString() === "" ) { + $unitStatic = $unit; + } + + if ( $unit != '' && !$unitClone ) { + $unitClone = $current->unit->toString(); + } + + if ( isset( $values[''] ) && $unit !== '' && $unit === $unitStatic ) { + $j = $values['']; + } elseif ( isset( $values[$unit] ) ) { + $j = $values[$unit]; + } else { + + if ( $unitStatic && $unit !== $unitStatic ) { + throw new Less_Exception_Compiler( 'incompatible types' ); + } + $values[$unit] = count( $order ); + $order[] = $current; + continue; + } + + if ( $order[$j]->unit->toString() === "" && $unitClone ) { + $temp = new Less_Tree_Dimension( $order[$j]->value, $unitClone ); + $referenceUnified = $temp->unify(); + } else { + $referenceUnified = $order[$j]->unify(); + } + if ( ( $isMin && $currentUnified->value < $referenceUnified->value ) || ( !$isMin && $currentUnified->value > $referenceUnified->value ) ) { + $order[$j] = $current; + } + } + + if ( count( $order ) == 1 ) { + return $order[0]; + } + $args = array(); + foreach ( $order as $a ) { + $args[] = $a->toCSS( $this->env ); + } + return new Less_Tree_Anonymous( ( $isMin ? 'min(' : 'max(' ) . implode( Less_Environment::$_outputMap[','], $args ).')' ); + } + + public function min() { + $args = func_get_args(); + return $this->_minmax( true, $args ); + } + + public function max() { + $args = func_get_args(); + return $this->_minmax( false, $args ); + } + + public function getunit( $n ) { + return new Less_Tree_Anonymous( $n->unit ); + } + + public function argb( $color ) { + if ( !$color instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to argb must be a color' . ( $color instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return new Less_Tree_Anonymous( $color->toARGB() ); + } + + public function percentage( $n ) { + return new Less_Tree_Dimension( $n->value * 100, '%' ); + } + + public function color( $n ) { + if ( $n instanceof Less_Tree_Quoted ) { + $colorCandidate = $n->value; + $returnColor = Less_Tree_Color::fromKeyword( $colorCandidate ); + if ( $returnColor ) { + return $returnColor; + } + if ( preg_match( '/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/', $colorCandidate ) ) { + return new Less_Tree_Color( substr( $colorCandidate, 1 ) ); + } + throw new Less_Exception_Compiler( "argument must be a color keyword or 3/6 digit hex e.g. #FFF" ); + } else { + throw new Less_Exception_Compiler( "argument must be a string" ); + } + } + + public function iscolor( $n ) { + return $this->_isa( $n, 'Less_Tree_Color' ); + } + + public function isnumber( $n ) { + return $this->_isa( $n, 'Less_Tree_Dimension' ); + } + + public function isstring( $n ) { + return $this->_isa( $n, 'Less_Tree_Quoted' ); + } + + public function iskeyword( $n ) { + return $this->_isa( $n, 'Less_Tree_Keyword' ); + } + + public function isurl( $n ) { + return $this->_isa( $n, 'Less_Tree_Url' ); + } + + public function ispixel( $n ) { + return $this->isunit( $n, 'px' ); + } + + public function ispercentage( $n ) { + return $this->isunit( $n, '%' ); + } + + public function isem( $n ) { + return $this->isunit( $n, 'em' ); + } + + /** + * @param string $unit + */ + public function isunit( $n, $unit ) { + if ( is_object( $unit ) && property_exists( $unit, 'value' ) ) { + $unit = $unit->value; + } + + return ( $n instanceof Less_Tree_Dimension ) && $n->unit->is( $unit ) ? new Less_Tree_Keyword( 'true' ) : new Less_Tree_Keyword( 'false' ); + } + + /** + * @param string $type + */ + private function _isa( $n, $type ) { + return is_a( $n, $type ) ? new Less_Tree_Keyword( 'true' ) : new Less_Tree_Keyword( 'false' ); + } + + public function tint( $color, $amount = null ) { + return $this->mix( $this->rgb( 255, 255, 255 ), $color, $amount ); + } + + public function shade( $color, $amount = null ) { + return $this->mix( $this->rgb( 0, 0, 0 ), $color, $amount ); + } + + public function extract( $values, $index ) { + $index = (int)$index->value - 1; // (1-based index) + // handle non-array values as an array of length 1 + // return 'undefined' if index is invalid + if ( property_exists( $values, 'value' ) && is_array( $values->value ) ) { + if ( isset( $values->value[$index] ) ) { + return $values->value[$index]; + } + return null; + + } elseif ( (int)$index === 0 ) { + return $values; + } + + return null; + } + + public function length( $values ) { + $n = ( property_exists( $values, 'value' ) && is_array( $values->value ) ) ? count( $values->value ) : 1; + return new Less_Tree_Dimension( $n ); + } + + public function datauri( $mimetypeNode, $filePathNode = null ) { + $filePath = ( $filePathNode ? $filePathNode->value : null ); + $mimetype = $mimetypeNode->value; + + $args = 2; + if ( !$filePath ) { + $filePath = $mimetype; + $args = 1; + } + + $filePath = str_replace( '\\', '/', $filePath ); + if ( Less_Environment::isPathRelative( $filePath ) ) { + + if ( Less_Parser::$options['relativeUrls'] ) { + $temp = $this->currentFileInfo['currentDirectory']; + } else { + $temp = $this->currentFileInfo['entryPath']; + } + + if ( !empty( $temp ) ) { + $filePath = Less_Environment::normalizePath( rtrim( $temp, '/' ).'/'.$filePath ); + } + + } + + // detect the mimetype if not given + if ( $args < 2 ) { + + /* incomplete + $mime = require('mime'); + mimetype = mime.lookup(path); + + // use base 64 unless it's an ASCII or UTF-8 format + var charset = mime.charsets.lookup(mimetype); + useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; + if (useBase64) mimetype += ';base64'; + */ + + $mimetype = Less_Mime::lookup( $filePath ); + + $charset = Less_Mime::charsets_lookup( $mimetype ); + $useBase64 = !in_array( $charset, array( 'US-ASCII', 'UTF-8' ) ); + if ( $useBase64 ) { $mimetype .= ';base64'; + } + + } else { + $useBase64 = preg_match( '/;base64$/', $mimetype ); + } + + if ( file_exists( $filePath ) ) { + $buf = @file_get_contents( $filePath ); + } else { + $buf = false; + } + + // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded + // and the --ieCompat flag is enabled, return a normal url() instead. + $DATA_URI_MAX_KB = 32; + $fileSizeInKB = round( strlen( $buf ) / 1024 ); + if ( $fileSizeInKB >= $DATA_URI_MAX_KB ) { + $url = new Less_Tree_Url( ( $filePathNode ? $filePathNode : $mimetypeNode ), $this->currentFileInfo ); + return $url->compile( $this ); + } + + if ( $buf ) { + $buf = $useBase64 ? base64_encode( $buf ) : rawurlencode( $buf ); + $filePath = '"data:' . $mimetype . ',' . $buf . '"'; + } + + return new Less_Tree_Url( new Less_Tree_Anonymous( $filePath ) ); + } + + // svg-gradient + public function svggradient( $direction ) { + $throw_message = 'svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]'; + $arguments = func_get_args(); + + if ( count( $arguments ) < 3 ) { + throw new Less_Exception_Compiler( $throw_message ); + } + + $stops = array_slice( $arguments, 1 ); + $gradientType = 'linear'; + $rectangleDimension = 'x="0" y="0" width="1" height="1"'; + $useBase64 = true; + $directionValue = $direction->toCSS(); + + switch ( $directionValue ) { + case "to bottom": + $gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; + break; + case "to right": + $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; + break; + case "to bottom right": + $gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; + break; + case "to top right": + $gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; + break; + case "ellipse": + case "ellipse at center": + $gradientType = "radial"; + $gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; + $rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; + break; + default: + throw new Less_Exception_Compiler( "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" ); + } + + $returner = '' . + '' . + '<' . $gradientType . 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' . $gradientDirectionSvg . '>'; + + for ( $i = 0; $i < count( $stops ); $i++ ) { + if ( is_object( $stops[$i] ) && property_exists( $stops[$i], 'value' ) ) { + $color = $stops[$i]->value[0]; + $position = $stops[$i]->value[1]; + } else { + $color = $stops[$i]; + $position = null; + } + + if ( !( $color instanceof Less_Tree_Color ) || ( !( ( $i === 0 || $i + 1 === count( $stops ) ) && $position === null ) && !( $position instanceof Less_Tree_Dimension ) ) ) { + throw new Less_Exception_Compiler( $throw_message ); + } + if ( $position ) { + $positionValue = $position->toCSS(); + } elseif ( $i === 0 ) { + $positionValue = '0%'; + } else { + $positionValue = '100%'; + } + $alpha = $color->alpha; + $returner .= ''; + } + + $returner .= ''; + + if ( $useBase64 ) { + $returner = "'data:image/svg+xml;base64,".base64_encode( $returner )."'"; + } else { + $returner = "'data:image/svg+xml,".$returner."'"; + } + + return new Less_Tree_URL( new Less_Tree_Anonymous( $returner ) ); + } + + /** + * Php version of javascript's `encodeURIComponent` function + * + * @param string $string The string to encode + * @return string The encoded string + */ + public static function encodeURIComponent( $string ) { + $revert = array( '%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')' ); + return strtr( rawurlencode( $string ), $revert ); + } + + // Color Blending + // ref: http://www.w3.org/TR/compositing-1 + + public function colorBlend( $mode, $color1, $color2 ) { + $ab = $color1->alpha; // backdrop + $as = $color2->alpha; // source + $r = array(); // result + + $ar = $as + $ab * ( 1 - $as ); + for ( $i = 0; $i < 3; $i++ ) { + $cb = $color1->rgb[$i] / 255; + $cs = $color2->rgb[$i] / 255; + $cr = call_user_func( $mode, $cb, $cs ); + if ( $ar ) { + $cr = ( $as * $cs + $ab * ( $cb - $as * ( $cb + $cs - $cr ) ) ) / $ar; + } + $r[$i] = $cr * 255; + } + + return new Less_Tree_Color( $r, $ar ); + } + + public function multiply( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to multiply must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to multiply must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendMultiply' ), $color1, $color2 ); + } + + private function colorBlendMultiply( $cb, $cs ) { + return $cb * $cs; + } + + public function screen( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to screen must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to screen must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendScreen' ), $color1, $color2 ); + } + + private function colorBlendScreen( $cb, $cs ) { + return $cb + $cs - $cb * $cs; + } + + public function overlay( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to overlay must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to overlay must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendOverlay' ), $color1, $color2 ); + } + + private function colorBlendOverlay( $cb, $cs ) { + $cb *= 2; + return ( $cb <= 1 ) + ? $this->colorBlendMultiply( $cb, $cs ) + : $this->colorBlendScreen( $cb - 1, $cs ); + } + + public function softlight( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to softlight must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to softlight must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendSoftlight' ), $color1, $color2 ); + } + + private function colorBlendSoftlight( $cb, $cs ) { + $d = 1; + $e = $cb; + if ( $cs > 0.5 ) { + $e = 1; + $d = ( $cb > 0.25 ) ? sqrt( $cb ) + : ( ( 16 * $cb - 12 ) * $cb + 4 ) * $cb; + } + return $cb - ( 1 - 2 * $cs ) * $e * ( $d - $cb ); + } + + public function hardlight( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to hardlight must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to hardlight must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendHardlight' ), $color1, $color2 ); + } + + private function colorBlendHardlight( $cb, $cs ) { + return $this->colorBlendOverlay( $cs, $cb ); + } + + public function difference( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to difference must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to difference must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendDifference' ), $color1, $color2 ); + } + + private function colorBlendDifference( $cb, $cs ) { + return abs( $cb - $cs ); + } + + public function exclusion( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to exclusion must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to exclusion must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendExclusion' ), $color1, $color2 ); + } + + private function colorBlendExclusion( $cb, $cs ) { + return $cb + $cs - 2 * $cb * $cs; + } + + public function average( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to average must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to average must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendAverage' ), $color1, $color2 ); + } + + // non-w3c functions: + public function colorBlendAverage( $cb, $cs ) { + return ( $cb + $cs ) / 2; + } + + public function negation( $color1 = null, $color2 = null ) { + if ( !$color1 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The first argument to negation must be a color' . ( $color1 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + if ( !$color2 instanceof Less_Tree_Color ) { + throw new Less_Exception_Compiler( 'The second argument to negation must be a color' . ( $color2 instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) ); + } + + return $this->colorBlend( array( $this,'colorBlendNegation' ), $color1, $color2 ); + } + + public function colorBlendNegation( $cb, $cs ) { + return 1 - abs( $cb + $cs - 1 ); + } + + // ~ End of Color Blending + +} diff --git a/less.php/lib/Less/Less.php.combine b/less.php/lib/Less/Less.php.combine new file mode 100644 index 0000000..d63cc78 --- /dev/null +++ b/less.php/lib/Less/Less.php.combine @@ -0,0 +1,17 @@ + +./Parser.php +./Colors.php +./Environment.php +./Functions.php +./Mime.php +./Tree.php +./Output.php +./Visitor.php +./VisitorReplacing.php +./Configurable.php +./Tree +./Visitor +./Exception/Parser.php +./Exception/ +./Output +./SourceMap diff --git a/less.php/lib/Less/Mime.php b/less.php/lib/Less/Mime.php new file mode 100644 index 0000000..b4723f9 --- /dev/null +++ b/less.php/lib/Less/Mime.php @@ -0,0 +1,41 @@ + 'text/html', + '.html' => 'text/html', + '.gif' => 'image/gif', + '.jpg' => 'image/jpeg', + '.jpeg' => 'image/jpeg', + '.png' => 'image/png', + '.ttf' => 'application/x-font-ttf', + '.otf' => 'application/x-font-otf', + '.eot' => 'application/vnd.ms-fontobject', + '.woff' => 'application/x-font-woff', + '.svg' => 'image/svg+xml', + ); + + public static function lookup( $filepath ) { + $parts = explode( '.', $filepath ); + $ext = '.'.strtolower( array_pop( $parts ) ); + + if ( !isset( self::$_types[$ext] ) ) { + return null; + } + return self::$_types[$ext]; + } + + public static function charsets_lookup( $type = null ) { + // assumes all text types are UTF-8 + return $type && preg_match( '/^text\//', $type ) ? 'UTF-8' : ''; + } +} diff --git a/less.php/lib/Less/Output.php b/less.php/lib/Less/Output.php new file mode 100644 index 0000000..8f7dd61 --- /dev/null +++ b/less.php/lib/Less/Output.php @@ -0,0 +1,48 @@ +strs[] = $chunk; + } + + /** + * Is the output empty? + * + * @return boolean + */ + public function isEmpty() { + return count( $this->strs ) === 0; + } + + /** + * Converts the output to string + * + * @return string + */ + public function toString() { + return implode( '', $this->strs ); + } + +} diff --git a/less.php/lib/Less/Output/Mapped.php b/less.php/lib/Less/Output/Mapped.php new file mode 100644 index 0000000..e486c98 --- /dev/null +++ b/less.php/lib/Less/Output/Mapped.php @@ -0,0 +1,119 @@ +contentsMap = $contentsMap; + $this->generator = $generator; + } + + /** + * Adds a chunk to the stack + * The $index for less.php may be different from less.js since less.php does not chunkify inputs + * + * @param string $chunk + * @param string $fileInfo + * @param integer $index + * @param mixed $mapLines + */ + public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) { + // ignore adding empty strings + if ( $chunk === '' ) { + return; + } + + $sourceLines = array(); + $sourceColumns = ' '; + + if ( $fileInfo ) { + + $url = $fileInfo['currentUri']; + + if ( isset( $this->contentsMap[$url] ) ) { + $inputSource = substr( $this->contentsMap[$url], 0, $index ); + $sourceLines = explode( "\n", $inputSource ); + $sourceColumns = end( $sourceLines ); + } else { + throw new Exception( 'Filename '.$url.' not in contentsMap' ); + } + + } + + $lines = explode( "\n", $chunk ); + $columns = end( $lines ); + + if ( $fileInfo ) { + + if ( !$mapLines ) { + $this->generator->addMapping( + $this->lineNumber + 1, // generated_line + $this->column, // generated_column + count( $sourceLines ), // original_line + strlen( $sourceColumns ), // original_column + $fileInfo + ); + } else { + for ( $i = 0, $count = count( $lines ); $i < $count; $i++ ) { + $this->generator->addMapping( + $this->lineNumber + $i + 1, // generated_line + $i === 0 ? $this->column : 0, // generated_column + count( $sourceLines ) + $i, // original_line + $i === 0 ? strlen( $sourceColumns ) : 0, // original_column + $fileInfo + ); + } + } + } + + if ( count( $lines ) === 1 ) { + $this->column += strlen( $columns ); + } else { + $this->lineNumber += count( $lines ) - 1; + $this->column = strlen( $columns ); + } + + // add only chunk + parent::add( $chunk ); + } + +} diff --git a/less.php/lib/Less/Parser.php b/less.php/lib/Less/Parser.php new file mode 100644 index 0000000..36e0b3e --- /dev/null +++ b/less.php/lib/Less/Parser.php @@ -0,0 +1,2691 @@ + false, // option - whether to compress + 'strictUnits' => false, // whether units need to evaluate correctly + 'strictMath' => false, // whether math has to be within parenthesis + 'relativeUrls' => true, // option - whether to adjust URL's to be relative + 'urlArgs' => '', // whether to add args into url tokens + 'numPrecision' => 8, + + 'import_dirs' => array(), + 'import_callback' => null, + 'cache_dir' => null, + 'cache_method' => 'php', // false, 'serialize', 'php', 'var_export', 'callback'; + 'cache_callback_get' => null, + 'cache_callback_set' => null, + + 'sourceMap' => false, // whether to output a source map + 'sourceMapBasepath' => null, + 'sourceMapWriteTo' => null, + 'sourceMapURL' => null, + + 'indentation' => ' ', + + 'plugins' => array(), + + ); + + public static $options = array(); + + private $input; // Less input string + private $input_len; // input string length + private $pos; // current index in `input` + private $saveStack = array(); // holds state for backtracking + private $furthest; + private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding + + /** + * @var Less_Environment + */ + private $env; + + protected $rules = array(); + + private static $imports = array(); + + public static $has_extends = false; + + public static $next_id = 0; + + /** + * Filename to contents of all parsed the files + * + * @var array + */ + public static $contentsMap = array(); + + /** + * @param Less_Environment|array|null $env + */ + public function __construct( $env = null ) { + // Top parser on an import tree must be sure there is one "env" + // which will then be passed around by reference. + if ( $env instanceof Less_Environment ) { + $this->env = $env; + } else { + $this->SetOptions( Less_Parser::$default_options ); + $this->Reset( $env ); + } + + // mbstring.func_overload > 1 bugfix + // The encoding value must be set for each source file, + // therefore, to conserve resources and improve the speed of this design is taken here + if ( ini_get( 'mbstring.func_overload' ) ) { + $this->mb_internal_encoding = ini_get( 'mbstring.internal_encoding' ); + @ini_set( 'mbstring.internal_encoding', 'ascii' ); + } + + } + + /** + * Reset the parser state completely + * + */ + public function Reset( $options = null ) { + $this->rules = array(); + self::$imports = array(); + self::$has_extends = false; + self::$imports = array(); + self::$contentsMap = array(); + + $this->env = new Less_Environment( $options ); + + // set new options + if ( is_array( $options ) ) { + $this->SetOptions( Less_Parser::$default_options ); + $this->SetOptions( $options ); + } + + $this->env->Init(); + } + + /** + * Set one or more compiler options + * options: import_dirs, cache_dir, cache_method + * + */ + public function SetOptions( $options ) { + foreach ( $options as $option => $value ) { + $this->SetOption( $option, $value ); + } + } + + /** + * Set one compiler option + * + */ + public function SetOption( $option, $value ) { + switch ( $option ) { + + case 'import_dirs': + $this->SetImportDirs( $value ); + return; + + case 'cache_dir': + if ( is_string( $value ) ) { + Less_Cache::SetCacheDir( $value ); + Less_Cache::CheckCacheDir(); + } + return; + } + + Less_Parser::$options[$option] = $value; + } + + /** + * Registers a new custom function + * + * @param string $name function name + * @param callable $callback callback + */ + public function registerFunction( $name, $callback ) { + $this->env->functions[$name] = $callback; + } + + /** + * Removed an already registered function + * + * @param string $name function name + */ + public function unregisterFunction( $name ) { + if ( isset( $this->env->functions[$name] ) ) + unset( $this->env->functions[$name] ); + } + + /** + * Get the current css buffer + * + * @return string + */ + public function getCss() { + $precision = ini_get( 'precision' ); + @ini_set( 'precision', 16 ); + $locale = setlocale( LC_NUMERIC, 0 ); + setlocale( LC_NUMERIC, "C" ); + + try { + + $root = new Less_Tree_Ruleset( array(), $this->rules ); + $root->root = true; + $root->firstRoot = true; + + $this->PreVisitors( $root ); + + self::$has_extends = false; + $evaldRoot = $root->compile( $this->env ); + + $this->PostVisitors( $evaldRoot ); + + if ( Less_Parser::$options['sourceMap'] ) { + $generator = new Less_SourceMap_Generator( $evaldRoot, Less_Parser::$contentsMap, Less_Parser::$options ); + // will also save file + // FIXME: should happen somewhere else? + $css = $generator->generateCSS(); + } else { + $css = $evaldRoot->toCSS(); + } + + if ( Less_Parser::$options['compress'] ) { + $css = preg_replace( '/(^(\s)+)|((\s)+$)/', '', $css ); + } + + } catch ( Exception $exc ) { + // Intentional fall-through so we can reset environment + } + + // reset php settings + @ini_set( 'precision', $precision ); + setlocale( LC_NUMERIC, $locale ); + + // If you previously defined $this->mb_internal_encoding + // is required to return the encoding as it was before + if ( $this->mb_internal_encoding != '' ) { + @ini_set( "mbstring.internal_encoding", $this->mb_internal_encoding ); + $this->mb_internal_encoding = ''; + } + + // Rethrow exception after we handled resetting the environment + if ( !empty( $exc ) ) { + throw $exc; + } + + return $css; + } + + public function findValueOf( $varName ) { + foreach ( $this->rules as $rule ) { + if ( isset( $rule->variable ) && ( $rule->variable == true ) && ( str_replace( "@", "", $rule->name ) == $varName ) ) { + return $this->getVariableValue( $rule ); + } + } + return null; + } + + /** + * + * this function gets the private rules variable and returns an array of the found variables + * it uses a helper method getVariableValue() that contains the logic ot fetch the value from the rule object + * + * @return array + */ + public function getVariables() { + $variables = array(); + + $not_variable_type = array( + 'Comment', // this include less comments ( // ) and css comments (/* */) + 'Import', // do not search variables in included files @import + 'Ruleset', // selectors (.someclass, #someid, …) + 'Operation', // + ); + + // @TODO run compilation if not runned yet + foreach ( $this->rules as $key => $rule ) { + if ( in_array( $rule->type, $not_variable_type ) ) { + continue; + } + + // Note: it seems rule->type is always Rule when variable = true + if ( $rule->type == 'Rule' && $rule->variable ) { + $variables[$rule->name] = $this->getVariableValue( $rule ); + } else { + if ( $rule->type == 'Comment' ) { + $variables[] = $this->getVariableValue( $rule ); + } + } + } + return $variables; + } + + public function findVarByName( $var_name ) { + foreach ( $this->rules as $rule ) { + if ( isset( $rule->variable ) && ( $rule->variable == true ) ) { + if ( $rule->name == $var_name ) { + return $this->getVariableValue( $rule ); + } + } + } + return null; + } + + /** + * + * This method gets the value of the less variable from the rules object. + * Since the objects vary here we add the logic for extracting the css/less value. + * + * @param $var + * + * @return bool|string + */ + private function getVariableValue( $var ) { + if ( !is_a( $var, 'Less_Tree' ) ) { + throw new Exception( 'var is not a Less_Tree object' ); + } + + switch ( $var->type ) { + case 'Color': + return $this->rgb2html( $var->rgb ); + case 'Unit': + return $var->value. $var->unit->numerator[0]; + case 'Variable': + return $this->findVarByName( $var->name ); + case 'Keyword': + return $var->value; + case 'Rule': + return $this->getVariableValue( $var->value ); + case 'Value': + $value = ''; + foreach ( $var->value as $sub_value ) { + $value .= $this->getVariableValue( $sub_value ).' '; + } + return $value; + case 'Quoted': + return $var->quote.$var->value.$var->quote; + case 'Dimension': + $value = $var->value; + if ( $var->unit && $var->unit->numerator ) { + $value .= $var->unit->numerator[0]; + } + return $value; + case 'Expression': + $value = ""; + foreach ( $var->value as $item ) { + $value .= $this->getVariableValue( $item )." "; + } + return $value; + case 'Operation': + throw new Exception( 'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()' ); + case 'Comment': + case 'Import': + case 'Ruleset': + default: + throw new Exception( "type missing in switch/case getVariableValue for ".$var->type ); + } + return false; + } + + private function rgb2html( $r, $g = -1, $b = -1 ) { + if ( is_array( $r ) && sizeof( $r ) == 3 ) + list( $r, $g, $b ) = $r; + + $r = intval( $r ); $g = intval( $g ); + $b = intval( $b ); + + $r = dechex( $r < 0 ? 0 : ( $r > 255 ? 255 : $r ) ); + $g = dechex( $g < 0 ? 0 : ( $g > 255 ? 255 : $g ) ); + $b = dechex( $b < 0 ? 0 : ( $b > 255 ? 255 : $b ) ); + + $color = ( strlen( $r ) < 2 ? '0' : '' ).$r; + $color .= ( strlen( $g ) < 2 ? '0' : '' ).$g; + $color .= ( strlen( $b ) < 2 ? '0' : '' ).$b; + return '#'.$color; + } + + /** + * Run pre-compile visitors + * + */ + private function PreVisitors( $root ) { + if ( Less_Parser::$options['plugins'] ) { + foreach ( Less_Parser::$options['plugins'] as $plugin ) { + if ( !empty( $plugin->isPreEvalVisitor ) ) { + $plugin->run( $root ); + } + } + } + } + + /** + * Run post-compile visitors + * + */ + private function PostVisitors( $evaldRoot ) { + $visitors = array(); + $visitors[] = new Less_Visitor_joinSelector(); + if ( self::$has_extends ) { + $visitors[] = new Less_Visitor_processExtends(); + } + $visitors[] = new Less_Visitor_toCSS(); + + if ( Less_Parser::$options['plugins'] ) { + foreach ( Less_Parser::$options['plugins'] as $plugin ) { + if ( property_exists( $plugin, 'isPreEvalVisitor' ) && $plugin->isPreEvalVisitor ) { + continue; + } + + if ( property_exists( $plugin, 'isPreVisitor' ) && $plugin->isPreVisitor ) { + array_unshift( $visitors, $plugin ); + } else { + $visitors[] = $plugin; + } + } + } + + for ( $i = 0; $i < count( $visitors ); $i++ ) { + $visitors[$i]->run( $evaldRoot ); + } + + } + + /** + * Parse a Less string into css + * + * @param string $str The string to convert + * @param string $uri_root The url of the file + * @return Less_Tree_Ruleset|Less_Parser + */ + public function parse( $str, $file_uri = null ) { + if ( !$file_uri ) { + $uri_root = ''; + $filename = 'anonymous-file-'.Less_Parser::$next_id++.'.less'; + } else { + $file_uri = self::WinPath( $file_uri ); + $filename = $file_uri; + $uri_root = dirname( $file_uri ); + } + + $previousFileInfo = $this->env->currentFileInfo; + $uri_root = self::WinPath( $uri_root ); + $this->SetFileInfo( $filename, $uri_root ); + + $this->input = $str; + $this->_parse(); + + if ( $previousFileInfo ) { + $this->env->currentFileInfo = $previousFileInfo; + } + + return $this; + } + + /** + * Parse a Less string from a given file + * + * @throws Less_Exception_Parser + * @param string $filename The file to parse + * @param string $uri_root The url of the file + * @param bool $returnRoot Indicates whether the return value should be a css string a root node + * @return Less_Tree_Ruleset|Less_Parser + */ + public function parseFile( $filename, $uri_root = '', $returnRoot = false ) { + if ( !file_exists( $filename ) ) { + $this->Error( sprintf( 'File `%s` not found.', $filename ) ); + } + + // fix uri_root? + // Instead of The mixture of file path for the first argument and directory path for the second argument has bee + if ( !$returnRoot && !empty( $uri_root ) && basename( $uri_root ) == basename( $filename ) ) { + $uri_root = dirname( $uri_root ); + } + + $previousFileInfo = $this->env->currentFileInfo; + + if ( $filename ) { + $filename = self::AbsPath( $filename, true ); + } + $uri_root = self::WinPath( $uri_root ); + + $this->SetFileInfo( $filename, $uri_root ); + + self::AddParsedFile( $filename ); + + if ( $returnRoot ) { + $rules = $this->GetRules( $filename ); + $return = new Less_Tree_Ruleset( array(), $rules ); + } else { + $this->_parse( $filename ); + $return = $this; + } + + if ( $previousFileInfo ) { + $this->env->currentFileInfo = $previousFileInfo; + } + + return $return; + } + + /** + * Allows a user to set variables values + * @param array $vars + * @return Less_Parser + */ + public function ModifyVars( $vars ) { + $this->input = Less_Parser::serializeVars( $vars ); + $this->_parse(); + + return $this; + } + + /** + * @param string $filename + */ + public function SetFileInfo( $filename, $uri_root = '' ) { + $filename = Less_Environment::normalizePath( $filename ); + $dirname = preg_replace( '/[^\/\\\\]*$/', '', $filename ); + + if ( !empty( $uri_root ) ) { + $uri_root = rtrim( $uri_root, '/' ).'/'; + } + + $currentFileInfo = array(); + + // entry info + if ( isset( $this->env->currentFileInfo ) ) { + $currentFileInfo['entryPath'] = $this->env->currentFileInfo['entryPath']; + $currentFileInfo['entryUri'] = $this->env->currentFileInfo['entryUri']; + $currentFileInfo['rootpath'] = $this->env->currentFileInfo['rootpath']; + + } else { + $currentFileInfo['entryPath'] = $dirname; + $currentFileInfo['entryUri'] = $uri_root; + $currentFileInfo['rootpath'] = $dirname; + } + + $currentFileInfo['currentDirectory'] = $dirname; + $currentFileInfo['currentUri'] = $uri_root.basename( $filename ); + $currentFileInfo['filename'] = $filename; + $currentFileInfo['uri_root'] = $uri_root; + + // inherit reference + if ( isset( $this->env->currentFileInfo['reference'] ) && $this->env->currentFileInfo['reference'] ) { + $currentFileInfo['reference'] = true; + } + + $this->env->currentFileInfo = $currentFileInfo; + } + + /** + * @deprecated 1.5.1.2 + * + */ + public function SetCacheDir( $dir ) { + if ( !file_exists( $dir ) ) { + if ( mkdir( $dir ) ) { + return true; + } + throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: '.$dir ); + + } elseif ( !is_dir( $dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: '.$dir ); + + } elseif ( !is_writable( $dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: '.$dir ); + + } else { + $dir = self::WinPath( $dir ); + Less_Cache::$cache_dir = rtrim( $dir, '/' ).'/'; + return true; + } + } + + /** + * Set a list of directories or callbacks the parser should use for determining import paths + * + * @param array $dirs + */ + public function SetImportDirs( $dirs ) { + Less_Parser::$options['import_dirs'] = array(); + + foreach ( $dirs as $path => $uri_root ) { + + $path = self::WinPath( $path ); + if ( !empty( $path ) ) { + $path = rtrim( $path, '/' ).'/'; + } + + if ( !is_callable( $uri_root ) ) { + $uri_root = self::WinPath( $uri_root ); + if ( !empty( $uri_root ) ) { + $uri_root = rtrim( $uri_root, '/' ).'/'; + } + } + + Less_Parser::$options['import_dirs'][$path] = $uri_root; + } + } + + /** + * @param string $file_path + */ + private function _parse( $file_path = null ) { + $this->rules = array_merge( $this->rules, $this->GetRules( $file_path ) ); + } + + /** + * Return the results of parsePrimary for $file_path + * Use cache and save cached results if possible + * + * @param string|null $file_path + */ + private function GetRules( $file_path ) { + $this->SetInput( $file_path ); + + $cache_file = $this->CacheFile( $file_path ); + if ( $cache_file ) { + if ( Less_Parser::$options['cache_method'] == 'callback' ) { + if ( is_callable( Less_Parser::$options['cache_callback_get'] ) ) { + $cache = call_user_func_array( + Less_Parser::$options['cache_callback_get'], + array( $this, $file_path, $cache_file ) + ); + + if ( $cache ) { + $this->UnsetInput(); + return $cache; + } + } + + } elseif ( file_exists( $cache_file ) ) { + switch ( Less_Parser::$options['cache_method'] ) { + + // Using serialize + // Faster but uses more memory + case 'serialize': + $cache = unserialize( file_get_contents( $cache_file ) ); + if ( $cache ) { + touch( $cache_file ); + $this->UnsetInput(); + return $cache; + } + break; + + // Using generated php code + case 'var_export': + case 'php': + $this->UnsetInput(); + return include $cache_file; + } + } + } + + $rules = $this->parsePrimary(); + + if ( $this->pos < $this->input_len ) { + throw new Less_Exception_Chunk( $this->input, null, $this->furthest, $this->env->currentFileInfo ); + } + + $this->UnsetInput(); + + // save the cache + if ( $cache_file ) { + if ( Less_Parser::$options['cache_method'] == 'callback' ) { + if ( is_callable( Less_Parser::$options['cache_callback_set'] ) ) { + call_user_func_array( + Less_Parser::$options['cache_callback_set'], + array( $this, $file_path, $cache_file, $rules ) + ); + } + + } else { + // msg('write cache file'); + switch ( Less_Parser::$options['cache_method'] ) { + case 'serialize': + file_put_contents( $cache_file, serialize( $rules ) ); + break; + case 'php': + file_put_contents( $cache_file, '' ); + break; + case 'var_export': + // Requires __set_state() + file_put_contents( $cache_file, '' ); + break; + } + + Less_Cache::CleanCache(); + } + } + + return $rules; + } + + /** + * Set up the input buffer + * + */ + public function SetInput( $file_path ) { + if ( $file_path ) { + $this->input = file_get_contents( $file_path ); + } + + $this->pos = $this->furthest = 0; + + // Remove potential UTF Byte Order Mark + $this->input = preg_replace( '/\\G\xEF\xBB\xBF/', '', $this->input ); + $this->input_len = strlen( $this->input ); + + if ( Less_Parser::$options['sourceMap'] && $this->env->currentFileInfo ) { + $uri = $this->env->currentFileInfo['currentUri']; + Less_Parser::$contentsMap[$uri] = $this->input; + } + + } + + /** + * Free up some memory + * + */ + public function UnsetInput() { + unset( $this->input, $this->pos, $this->input_len, $this->furthest ); + $this->saveStack = array(); + } + + public function CacheFile( $file_path ) { + if ( $file_path && $this->CacheEnabled() ) { + + $env = get_object_vars( $this->env ); + unset( $env['frames'] ); + + $parts = array(); + $parts[] = $file_path; + $parts[] = filesize( $file_path ); + $parts[] = filemtime( $file_path ); + $parts[] = $env; + $parts[] = Less_Version::cache_version; + $parts[] = Less_Parser::$options['cache_method']; + return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache'; + } + } + + static function AddParsedFile( $file ) { + self::$imports[] = $file; + } + + static function AllParsedFiles() { + return self::$imports; + } + + /** + * @param string $file + */ + static function FileParsed( $file ) { + return in_array( $file, self::$imports ); + } + + function save() { + $this->saveStack[] = $this->pos; + } + + private function restore() { + $this->pos = array_pop( $this->saveStack ); + } + + private function forget() { + array_pop( $this->saveStack ); + } + + /** + * Determine if the character at the specified offset from the current position is a white space. + * + * @param int $offset + * + * @return bool + */ + private function isWhitespace( $offset = 0 ) { + return strpos( " \t\n\r\v\f", $this->input[$this->pos + $offset] ) !== false; + } + + /** + * Parse from a token, regexp or string, and move forward if match + * + * @param array $toks + * @return array + */ + private function match( $toks ) { + // The match is confirmed, add the match length to `this::pos`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + + foreach ( $toks as $tok ) { + + $char = $tok[0]; + + if ( $char === '/' ) { + $match = $this->MatchReg( $tok ); + + if ( $match ) { + return count( $match ) === 1 ? $match[0] : $match; + } + + } elseif ( $char === '#' ) { + $match = $this->MatchChar( $tok[1] ); + + } else { + // Non-terminal, match using a function call + $match = $this->$tok(); + + } + + if ( $match ) { + return $match; + } + } + } + + /** + * @param string[] $toks + * + * @return string + */ + private function MatchFuncs( $toks ) { + if ( $this->pos < $this->input_len ) { + foreach ( $toks as $tok ) { + $match = $this->$tok(); + if ( $match ) { + return $match; + } + } + } + + } + + // Match a single character in the input, + private function MatchChar( $tok ) { + if ( ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ) ) { + $this->skipWhitespace( 1 ); + return $tok; + } + } + + // Match a regexp from the current start point + private function MatchReg( $tok ) { + if ( preg_match( $tok, $this->input, $match, 0, $this->pos ) ) { + $this->skipWhitespace( strlen( $match[0] ) ); + return $match; + } + } + + /** + * Same as match(), but don't change the state of the parser, + * just return the match. + * + * @param string $tok + * @return integer + */ + public function PeekReg( $tok ) { + return preg_match( $tok, $this->input, $match, 0, $this->pos ); + } + + /** + * @param string $tok + */ + public function PeekChar( $tok ) { + // return ($this->input[$this->pos] === $tok ); + return ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ); + } + + /** + * @param integer $length + */ + public function skipWhitespace( $length ) { + $this->pos += $length; + + for ( ; $this->pos < $this->input_len; $this->pos++ ) { + $c = $this->input[$this->pos]; + + if ( ( $c !== "\n" ) && ( $c !== "\r" ) && ( $c !== "\t" ) && ( $c !== ' ' ) ) { + break; + } + } + } + + /** + * @param string $tok + * @param string|null $msg + */ + public function expect( $tok, $msg = NULL ) { + $result = $this->match( array( $tok ) ); + if ( !$result ) { + $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); + } else { + return $result; + } + } + + /** + * @param string $tok + */ + public function expectChar( $tok, $msg = null ) { + $result = $this->MatchChar( $tok ); + if ( !$result ) { + $msg = $msg ? $msg : "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'"; + $this->Error( $msg ); + } else { + return $result; + } + } + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + private function parsePrimary() { + $root = array(); + + while ( true ) { + + if ( $this->pos >= $this->input_len ) { + break; + } + + $node = $this->parseExtend( true ); + if ( $node ) { + $root = array_merge( $root, $node ); + continue; + } + + // $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseDirective')); + $node = $this->MatchFuncs( array( 'parseMixinDefinition', 'parseNameValue', 'parseRule', 'parseRuleset', 'parseMixinCall', 'parseComment', 'parseRulesetCall', 'parseDirective' ) ); + + if ( $node ) { + $root[] = $node; + } elseif ( !$this->MatchReg( '/\\G[\s\n;]+/' ) ) { + break; + } + + if ( $this->PeekChar( '}' ) ) { + break; + } + } + + return $root; + } + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + private function parseComment() { + if ( $this->input[$this->pos] !== '/' ) { + return; + } + + if ( $this->input[$this->pos + 1] === '/' ) { + $match = $this->MatchReg( '/\\G\/\/.*/' ); + return $this->NewObj4( 'Less_Tree_Comment', array( $match[0], true, $this->pos, $this->env->currentFileInfo ) ); + } + + // $comment = $this->MatchReg('/\\G\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/'); + $comment = $this->MatchReg( '/\\G\/\*(?s).*?\*+\/\n?/' );// not the same as less.js to prevent fatal errors + if ( $comment ) { + return $this->NewObj4( 'Less_Tree_Comment', array( $comment[0], false, $this->pos, $this->env->currentFileInfo ) ); + } + } + + private function parseComments() { + $comments = array(); + + while ( $this->pos < $this->input_len ) { + $comment = $this->parseComment(); + if ( !$comment ) { + break; + } + + $comments[] = $comment; + } + + return $comments; + } + + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + private function parseEntitiesQuoted() { + $j = $this->pos; + $e = false; + $index = $this->pos; + + if ( $this->input[$this->pos] === '~' ) { + $j++; + $e = true; // Escaped strings + } + + $char = $this->input[$j]; + if ( $char !== '"' && $char !== "'" ) { + return; + } + + if ( $e ) { + $this->MatchChar( '~' ); + } + + $matched = $this->MatchQuoted( $char, $j + 1 ); + if ( $matched === false ) { + return; + } + + $quoted = $char.$matched.$char; + return $this->NewObj5( 'Less_Tree_Quoted', array( $quoted, $matched, $e, $index, $this->env->currentFileInfo ) ); + } + + /** + * When PCRE JIT is enabled in php, regular expressions don't work for matching quoted strings + * + * $regex = '/\\G\'((?:[^\'\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)\'/'; + * $regex = '/\\G"((?:[^"\\\\\r\n]|\\\\.|\\\\\r\n|\\\\[\n\r\f])*)"/'; + * + */ + private function MatchQuoted( $quote_char, $i ) { + $matched = ''; + while ( $i < $this->input_len ) { + $c = $this->input[$i]; + + // escaped character + if ( $c === '\\' ) { + $matched .= $c . $this->input[$i + 1]; + $i += 2; + continue; + } + + if ( $c === $quote_char ) { + $this->pos = $i + 1; + $this->skipWhitespace( 0 ); + return $matched; + } + + if ( $c === "\r" || $c === "\n" ) { + return false; + } + + $i++; + $matched .= $c; + } + + return false; + } + + // + // A catch-all word, such as: + // + // black border-collapse + // + private function parseEntitiesKeyword() { + // $k = $this->MatchReg('/\\G[_A-Za-z-][_A-Za-z0-9-]*/'); + $k = $this->MatchReg( '/\\G%|\\G[_A-Za-z-][_A-Za-z0-9-]*/' ); + if ( $k ) { + $k = $k[0]; + $color = $this->fromKeyword( $k ); + if ( $color ) { + return $color; + } + return $this->NewObj1( 'Less_Tree_Keyword', $k ); + } + } + + // duplicate of Less_Tree_Color::FromKeyword + private function FromKeyword( $keyword ) { + $keyword = strtolower( $keyword ); + + if ( Less_Colors::hasOwnProperty( $keyword ) ) { + // detect named color + return $this->NewObj1( 'Less_Tree_Color', substr( Less_Colors::color( $keyword ), 1 ) ); + } + + if ( $keyword === 'transparent' ) { + return $this->NewObj3( 'Less_Tree_Color', array( array( 0, 0, 0 ), 0, true ) ); + } + } + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + private function parseEntitiesCall() { + $index = $this->pos; + + if ( !preg_match( '/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name, 0, $this->pos ) ) { + return; + } + $name = $name[1]; + $nameLC = strtolower( $name ); + + if ( $nameLC === 'url' ) { + return null; + } + + $this->pos += strlen( $name ); + + if ( $nameLC === 'alpha' ) { + $alpha_ret = $this->parseAlpha(); + if ( $alpha_ret ) { + return $alpha_ret; + } + } + + $this->MatchChar( '(' ); // Parse the '(' and consume whitespace. + + $args = $this->parseEntitiesArguments(); + + if ( !$this->MatchChar( ')' ) ) { + return; + } + + if ( $name ) { + return $this->NewObj4( 'Less_Tree_Call', array( $name, $args, $index, $this->env->currentFileInfo ) ); + } + } + + /** + * Parse a list of arguments + * + * @return array + */ + private function parseEntitiesArguments() { + $args = array(); + while ( true ) { + $arg = $this->MatchFuncs( array( 'parseEntitiesAssignment','parseExpression' ) ); + if ( !$arg ) { + break; + } + + $args[] = $arg; + if ( !$this->MatchChar( ',' ) ) { + break; + } + } + return $args; + } + + private function parseEntitiesLiteral() { + return $this->MatchFuncs( array( 'parseEntitiesDimension','parseEntitiesColor','parseEntitiesQuoted','parseUnicodeDescriptor' ) ); + } + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + private function parseEntitiesAssignment() { + $key = $this->MatchReg( '/\\G\w+(?=\s?=)/' ); + if ( !$key ) { + return; + } + + if ( !$this->MatchChar( '=' ) ) { + return; + } + + $value = $this->parseEntity(); + if ( $value ) { + return $this->NewObj2( 'Less_Tree_Assignment', array( $key[0], $value ) ); + } + } + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + private function parseEntitiesUrl() { + if ( $this->input[$this->pos] !== 'u' || !$this->matchReg( '/\\Gurl\(/' ) ) { + return; + } + + $value = $this->match( array( 'parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ) ); + if ( !$value ) { + $value = ''; + } + + $this->expectChar( ')' ); + + if ( isset( $value->value ) || $value instanceof Less_Tree_Variable ) { + return $this->NewObj2( 'Less_Tree_Url', array( $value, $this->env->currentFileInfo ) ); + } + + return $this->NewObj2( 'Less_Tree_Url', array( $this->NewObj1( 'Less_Tree_Anonymous', $value ), $this->env->currentFileInfo ) ); + } + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + private function parseEntitiesVariable() { + $index = $this->pos; + if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G@@?[\w-]+/' ) ) ) { + return $this->NewObj3( 'Less_Tree_Variable', array( $name[0], $index, $this->env->currentFileInfo ) ); + } + } + + // A variable entity using the protective {} e.g. @{var} + private function parseEntitiesVariableCurly() { + $index = $this->pos; + + if ( $this->input_len > ( $this->pos + 1 ) && $this->input[$this->pos] === '@' && ( $curly = $this->MatchReg( '/\\G@\{([\w-]+)\}/' ) ) ) { + return $this->NewObj3( 'Less_Tree_Variable', array( '@'.$curly[1], $index, $this->env->currentFileInfo ) ); + } + } + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + private function parseEntitiesColor() { + if ( $this->PeekChar( '#' ) && ( $rgb = $this->MatchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' ) ) ) { + return $this->NewObj1( 'Less_Tree_Color', $rgb[1] ); + } + } + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + private function parseEntitiesDimension() { + $c = @ord( $this->input[$this->pos] ); + + // Is the first char of the dimension 0-9, '.', '+' or '-' + if ( ( $c > 57 || $c < 43 ) || $c === 47 || $c == 44 ) { + return; + } + + $value = $this->MatchReg( '/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/' ); + if ( $value ) { + + if ( isset( $value[2] ) ) { + return $this->NewObj2( 'Less_Tree_Dimension', array( $value[1],$value[2] ) ); + } + return $this->NewObj1( 'Less_Tree_Dimension', $value[1] ); + } + } + + // + // A unicode descriptor, as is used in unicode-range + // + // U+0?? or U+00A1-00A9 + // + function parseUnicodeDescriptor() { + $ud = $this->MatchReg( '/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/' ); + if ( $ud ) { + return $this->NewObj1( 'Less_Tree_UnicodeDescriptor', $ud[0] ); + } + } + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + private function parseEntitiesJavascript() { + $e = false; + $j = $this->pos; + if ( $this->input[$j] === '~' ) { + $j++; + $e = true; + } + if ( $this->input[$j] !== '`' ) { + return; + } + if ( $e ) { + $this->MatchChar( '~' ); + } + $str = $this->MatchReg( '/\\G`([^`]*)`/' ); + if ( $str ) { + return $this->NewObj3( 'Less_Tree_Javascript', array( $str[1], $this->pos, $e ) ); + } + } + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + private function parseVariable() { + if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*:/' ) ) ) { + return $name[1]; + } + } + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink(); + // + private function parseRulesetCall() { + if ( $this->input[$this->pos] === '@' && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*\(\s*\)\s*;/' ) ) ) { + return $this->NewObj1( 'Less_Tree_RulesetCall', $name[1] ); + } + } + + // + // extend syntax - used to extend selectors + // + function parseExtend( $isRule = false ) { + $index = $this->pos; + $extendList = array(); + + if ( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ) { return; + } + + do{ + $option = null; + $elements = array(); + while ( true ) { + $option = $this->MatchReg( '/\\G(all)(?=\s*(\)|,))/' ); + if ( $option ) { break; + } + $e = $this->parseElement(); + if ( !$e ) { break; + } + $elements[] = $e; + } + + if ( $option ) { + $option = $option[1]; + } + + $extendList[] = $this->NewObj3( 'Less_Tree_Extend', array( $this->NewObj1( 'Less_Tree_Selector', $elements ), $option, $index ) ); + + }while ( $this->MatchChar( "," ) ); + + $this->expect( '/\\G\)/' ); + + if ( $isRule ) { + $this->expect( '/\\G;/' ); + } + + return $extendList; + } + + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + private function parseMixinCall() { + $char = $this->input[$this->pos]; + if ( $char !== '.' && $char !== '#' ) { + return; + } + + $index = $this->pos; + $this->save(); // stop us absorbing part of an invalid selector + + $elements = $this->parseMixinCallElements(); + + if ( $elements ) { + + if ( $this->MatchChar( '(' ) ) { + $returned = $this->parseMixinArgs( true ); + $args = $returned['args']; + $this->expectChar( ')' ); + } else { + $args = array(); + } + + $important = $this->parseImportant(); + + if ( $this->parseEnd() ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Mixin_Call', array( $elements, $args, $index, $this->env->currentFileInfo, $important ) ); + } + } + + $this->restore(); + } + + private function parseMixinCallElements() { + $elements = array(); + $c = null; + + while ( true ) { + $elemIndex = $this->pos; + $e = $this->MatchReg( '/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' ); + if ( !$e ) { + break; + } + $elements[] = $this->NewObj4( 'Less_Tree_Element', array( $c, $e[0], $elemIndex, $this->env->currentFileInfo ) ); + $c = $this->MatchChar( '>' ); + } + + return $elements; + } + + /** + * @param boolean $isCall + */ + private function parseMixinArgs( $isCall ) { + $expressions = array(); + $argsSemiColon = array(); + $isSemiColonSeperated = null; + $argsComma = array(); + $expressionContainsNamed = null; + $name = null; + $returner = array( 'args' => array(), 'variadic' => false ); + + $this->save(); + + while ( true ) { + if ( $isCall ) { + $arg = $this->MatchFuncs( array( 'parseDetachedRuleset','parseExpression' ) ); + } else { + $this->parseComments(); + if ( $this->input[ $this->pos ] === '.' && $this->MatchReg( '/\\G\.{3}/' ) ) { + $returner['variadic'] = true; + if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { + $isSemiColonSeperated = true; + } + + if ( $isSemiColonSeperated ) { + $argsSemiColon[] = array( 'variadic' => true ); + } else { + $argsComma[] = array( 'variadic' => true ); + } + break; + } + $arg = $this->MatchFuncs( array( 'parseEntitiesVariable','parseEntitiesLiteral','parseEntitiesKeyword' ) ); + } + + if ( !$arg ) { + break; + } + + $nameLoop = null; + if ( $arg instanceof Less_Tree_Expression ) { + $arg->throwAwayComments(); + } + $value = $arg; + $val = null; + + if ( $isCall ) { + // Variable + if ( property_exists( $arg, 'value' ) && count( $arg->value ) == 1 ) { + $val = $arg->value[0]; + } + } else { + $val = $arg; + } + + if ( $val instanceof Less_Tree_Variable ) { + + if ( $this->MatchChar( ':' ) ) { + if ( $expressions ) { + if ( $isSemiColonSeperated ) { + $this->Error( 'Cannot mix ; and , as delimiter types' ); + } + $expressionContainsNamed = true; + } + + // we do not support setting a ruleset as a default variable - it doesn't make sense + // However if we do want to add it, there is nothing blocking it, just don't error + // and remove isCall dependency below + $value = null; + if ( $isCall ) { + $value = $this->parseDetachedRuleset(); + } + if ( !$value ) { + $value = $this->parseExpression(); + } + + if ( !$value ) { + if ( $isCall ) { + $this->Error( 'could not understand value for named argument' ); + } else { + $this->restore(); + $returner['args'] = array(); + return $returner; + } + } + + $nameLoop = ( $name = $val->name ); + } elseif ( !$isCall && $this->MatchReg( '/\\G\.{3}/' ) ) { + $returner['variadic'] = true; + if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { + $isSemiColonSeperated = true; + } + if ( $isSemiColonSeperated ) { + $argsSemiColon[] = array( 'name' => $arg->name, 'variadic' => true ); + } else { + $argsComma[] = array( 'name' => $arg->name, 'variadic' => true ); + } + break; + } elseif ( !$isCall ) { + $name = $nameLoop = $val->name; + $value = null; + } + } + + if ( $value ) { + $expressions[] = $value; + } + + $argsComma[] = array( 'name' => $nameLoop, 'value' => $value ); + + if ( $this->MatchChar( ',' ) ) { + continue; + } + + if ( $this->MatchChar( ';' ) || $isSemiColonSeperated ) { + + if ( $expressionContainsNamed ) { + $this->Error( 'Cannot mix ; and , as delimiter types' ); + } + + $isSemiColonSeperated = true; + + if ( count( $expressions ) > 1 ) { + $value = $this->NewObj1( 'Less_Tree_Value', $expressions ); + } + $argsSemiColon[] = array( 'name' => $name, 'value' => $value ); + + $name = null; + $expressions = array(); + $expressionContainsNamed = false; + } + } + + $this->forget(); + $returner['args'] = ( $isSemiColonSeperated ? $argsSemiColon : $argsComma ); + return $returner; + } + + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + private function parseMixinDefinition() { + $cond = null; + + $char = $this->input[$this->pos]; + if ( ( $char !== '.' && $char !== '#' ) || ( $char === '{' && $this->PeekReg( '/\\G[^{]*\}/' ) ) ) { + return; + } + + $this->save(); + + $match = $this->MatchReg( '/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/' ); + if ( $match ) { + $name = $match[1]; + + $argInfo = $this->parseMixinArgs( false ); + $params = $argInfo['args']; + $variadic = $argInfo['variadic']; + + // .mixincall("@{a}"); + // looks a bit like a mixin definition.. + // also + // .mixincall(@a: {rule: set;}); + // so we have to be nice and restore + if ( !$this->MatchChar( ')' ) ) { + $this->furthest = $this->pos; + $this->restore(); + return; + } + + $this->parseComments(); + + if ( $this->MatchReg( '/\\Gwhen/' ) ) { // Guard + $cond = $this->expect( 'parseConditions', 'Expected conditions' ); + } + + $ruleset = $this->parseBlock(); + + if ( is_array( $ruleset ) ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Mixin_Definition', array( $name, $params, $ruleset, $cond, $variadic ) ); + } + + $this->restore(); + } else { + $this->forget(); + } + } + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + private function parseEntity() { + return $this->MatchFuncs( array( 'parseEntitiesLiteral','parseEntitiesVariable','parseEntitiesUrl','parseEntitiesCall','parseEntitiesKeyword','parseEntitiesJavascript','parseComment' ) ); + } + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was omitted. + // + private function parseEnd() { + return $this->MatchChar( ';' ) || $this->PeekChar( '}' ); + } + + // + // IE's alpha function + // + // alpha(opacity=88) + // + private function parseAlpha() { + if ( !$this->MatchReg( '/\\G\(opacity=/i' ) ) { + return; + } + + $value = $this->MatchReg( '/\\G[0-9]+/' ); + if ( $value ) { + $value = $value[0]; + } else { + $value = $this->parseEntitiesVariable(); + if ( !$value ) { + return; + } + } + + $this->expectChar( ')' ); + return $this->NewObj1( 'Less_Tree_Alpha', $value ); + } + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + private function parseElement() { + $c = $this->parseCombinator(); + $index = $this->pos; + + $e = $this->match( array( '/\\G(?:\d+\.\d+|\d+)%/', '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', + '#*', '#&', 'parseAttribute', '/\\G\([^()@]+\)/', '/\\G[\.#](?=@)/', 'parseEntitiesVariableCurly' ) ); + + if ( is_null( $e ) ) { + $this->save(); + if ( $this->MatchChar( '(' ) ) { + if ( ( $v = $this->parseSelector() ) && $this->MatchChar( ')' ) ) { + $e = $this->NewObj1( 'Less_Tree_Paren', $v ); + $this->forget(); + } else { + $this->restore(); + } + } else { + $this->forget(); + } + } + + if ( !is_null( $e ) ) { + return $this->NewObj4( 'Less_Tree_Element', array( $c, $e, $index, $this->env->currentFileInfo ) ); + } + } + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. + // + private function parseCombinator() { + if ( $this->pos < $this->input_len ) { + $c = $this->input[$this->pos]; + if ( $c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ) { + + $this->pos++; + if ( $this->input[$this->pos] === '^' ) { + $c = '^^'; + $this->pos++; + } + + $this->skipWhitespace( 0 ); + + return $c; + } + + if ( $this->pos > 0 && $this->isWhitespace( -1 ) ) { + return ' '; + } + } + } + + // + // A CSS selector (see selector below) + // with less extensions e.g. the ability to extend and guard + // + private function parseLessSelector() { + return $this->parseSelector( true ); + } + + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + private function parseSelector( $isLess = false ) { + $elements = array(); + $extendList = array(); + $condition = null; + $when = false; + $extend = false; + $e = null; + $c = null; + $index = $this->pos; + + while ( ( $isLess && ( $extend = $this->parseExtend() ) ) || ( $isLess && ( $when = $this->MatchReg( '/\\Gwhen/' ) ) ) || ( $e = $this->parseElement() ) ) { + if ( $when ) { + $condition = $this->expect( 'parseConditions', 'expected condition' ); + } elseif ( $condition ) { + // error("CSS guard can only be used at the end of selector"); + } elseif ( $extend ) { + $extendList = array_merge( $extendList, $extend ); + } else { + // if( count($extendList) ){ + //error("Extend can only be used at the end of selector"); + //} + if ( $this->pos < $this->input_len ) { + $c = $this->input[ $this->pos ]; + } + $elements[] = $e; + $e = null; + } + + if ( $c === '{' || $c === '}' || $c === ';' || $c === ',' || $c === ')' ) { break; + } + } + + if ( $elements ) { + return $this->NewObj5( 'Less_Tree_Selector', array( $elements, $extendList, $condition, $index, $this->env->currentFileInfo ) ); + } + if ( $extendList ) { + $this->Error( 'Extend must be used to extend a selector, it cannot be used on its own' ); + } + } + + private function parseTag() { + return ( $tag = $this->MatchReg( '/\\G[A-Za-z][A-Za-z-]*[0-9]?/' ) ) ? $tag : $this->MatchChar( '*' ); + } + + private function parseAttribute() { + $val = null; + + if ( !$this->MatchChar( '[' ) ) { + return; + } + + $key = $this->parseEntitiesVariableCurly(); + if ( !$key ) { + $key = $this->expect( '/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/' ); + } + + $op = $this->MatchReg( '/\\G[|~*$^]?=/' ); + if ( $op ) { + $val = $this->match( array( 'parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly' ) ); + } + + $this->expectChar( ']' ); + + return $this->NewObj3( 'Less_Tree_Attribute', array( $key, $op === null ? null : $op[0], $val ) ); + } + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + private function parseBlock() { + if ( $this->MatchChar( '{' ) ) { + $content = $this->parsePrimary(); + if ( $this->MatchChar( '}' ) ) { + return $content; + } + } + } + + private function parseBlockRuleset() { + $block = $this->parseBlock(); + + if ( $block ) { + $block = $this->NewObj2( 'Less_Tree_Ruleset', array( null, $block ) ); + } + + return $block; + } + + private function parseDetachedRuleset() { + $blockRuleset = $this->parseBlockRuleset(); + if ( $blockRuleset ) { + return $this->NewObj1( 'Less_Tree_DetachedRuleset', $blockRuleset ); + } + } + + // + // div, .class, body > p {...} + // + private function parseRuleset() { + $selectors = array(); + + $this->save(); + + while ( true ) { + $s = $this->parseLessSelector(); + if ( !$s ) { + break; + } + $selectors[] = $s; + $this->parseComments(); + + if ( $s->condition && count( $selectors ) > 1 ) { + $this->Error( 'Guards are only currently allowed on a single selector.' ); + } + + if ( !$this->MatchChar( ',' ) ) { + break; + } + if ( $s->condition ) { + $this->Error( 'Guards are only currently allowed on a single selector.' ); + } + $this->parseComments(); + } + + if ( $selectors ) { + $rules = $this->parseBlock(); + if ( is_array( $rules ) ) { + $this->forget(); + return $this->NewObj2( 'Less_Tree_Ruleset', array( $selectors, $rules ) ); // Less_Environment::$strictImports + } + } + + // Backtrack + $this->furthest = $this->pos; + $this->restore(); + } + + /** + * Custom less.php parse function for finding simple name-value css pairs + * ex: width:100px; + * + */ + private function parseNameValue() { + $index = $this->pos; + $this->save(); + + // $match = $this->MatchReg('/\\G([a-zA-Z\-]+)\s*:\s*((?:\'")?[a-zA-Z0-9\-% \.,!]+?(?:\'")?)\s*([;}])/'); + $match = $this->MatchReg( '/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/' ); + if ( $match ) { + + if ( $match[4] == '}' ) { + $this->pos = $index + strlen( $match[0] ) - 1; + } + + if ( $match[3] ) { + $match[2] .= ' !important'; + } + + return $this->NewObj4( 'Less_Tree_NameValue', array( $match[1], $match[2], $index, $this->env->currentFileInfo ) ); + } + + $this->restore(); + } + + private function parseRule( $tryAnonymous = null ) { + $merge = false; + $startOfRule = $this->pos; + + $c = $this->input[$this->pos]; + if ( $c === '.' || $c === '#' || $c === '&' ) { + return; + } + + $this->save(); + $name = $this->MatchFuncs( array( 'parseVariable','parseRuleProperty' ) ); + + if ( $name ) { + + $isVariable = is_string( $name ); + + $value = null; + if ( $isVariable ) { + $value = $this->parseDetachedRuleset(); + } + + $important = null; + if ( !$value ) { + + // prefer to try to parse first if its a variable or we are compressing + // but always fallback on the other one + //if( !$tryAnonymous && is_string($name) && $name[0] === '@' ){ + if ( !$tryAnonymous && ( Less_Parser::$options['compress'] || $isVariable ) ) { + $value = $this->MatchFuncs( array( 'parseValue','parseAnonymousValue' ) ); + } else { + $value = $this->MatchFuncs( array( 'parseAnonymousValue','parseValue' ) ); + } + + $important = $this->parseImportant(); + + // a name returned by this.ruleProperty() is always an array of the form: + // [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"] + // where each item is a tree.Keyword or tree.Variable + if ( !$isVariable && is_array( $name ) ) { + $nm = array_pop( $name ); + if ( $nm->value ) { + $merge = $nm->value; + } + } + } + + if ( $value && $this->parseEnd() ) { + $this->forget(); + return $this->NewObj6( 'Less_Tree_Rule', array( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo ) ); + } else { + $this->furthest = $this->pos; + $this->restore(); + if ( $value && !$tryAnonymous ) { + return $this->parseRule( true ); + } + } + } else { + $this->forget(); + } + } + + function parseAnonymousValue() { + if ( preg_match( '/\\G([^@+\/\'"*`(;{}-]*);/', $this->input, $match, 0, $this->pos ) ) { + $this->pos += strlen( $match[1] ); + return $this->NewObj1( 'Less_Tree_Anonymous', $match[1] ); + } + } + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environment, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + private function parseImport() { + $this->save(); + + $dir = $this->MatchReg( '/\\G@import?\s+/' ); + + if ( $dir ) { + $options = $this->parseImportOptions(); + $path = $this->MatchFuncs( array( 'parseEntitiesQuoted','parseEntitiesUrl' ) ); + + if ( $path ) { + $features = $this->parseMediaFeatures(); + if ( $this->MatchChar( ';' ) ) { + if ( $features ) { + $features = $this->NewObj1( 'Less_Tree_Value', $features ); + } + + $this->forget(); + return $this->NewObj5( 'Less_Tree_Import', array( $path, $features, $options, $this->pos, $this->env->currentFileInfo ) ); + } + } + } + + $this->restore(); + } + + private function parseImportOptions() { + $options = array(); + + // list of options, surrounded by parens + if ( !$this->MatchChar( '(' ) ) { + return $options; + } + do{ + $optionName = $this->parseImportOption(); + if ( $optionName ) { + $value = true; + switch ( $optionName ) { + case "css": + $optionName = "less"; + $value = false; + break; + case "once": + $optionName = "multiple"; + $value = false; + break; + } + $options[$optionName] = $value; + if ( !$this->MatchChar( ',' ) ) { break; + } + } + }while ( $optionName ); + $this->expectChar( ')' ); + return $options; + } + + private function parseImportOption() { + $opt = $this->MatchReg( '/\\G(less|css|multiple|once|inline|reference|optional)/' ); + if ( $opt ) { + return $opt[1]; + } + } + + private function parseMediaFeature() { + $nodes = array(); + + do{ + $e = $this->MatchFuncs( array( 'parseEntitiesKeyword','parseEntitiesVariable' ) ); + if ( $e ) { + $nodes[] = $e; + } elseif ( $this->MatchChar( '(' ) ) { + $p = $this->parseProperty(); + $e = $this->parseValue(); + if ( $this->MatchChar( ')' ) ) { + if ( $p && $e ) { + $r = $this->NewObj7( 'Less_Tree_Rule', array( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true ) ); + $nodes[] = $this->NewObj1( 'Less_Tree_Paren', $r ); + } elseif ( $e ) { + $nodes[] = $this->NewObj1( 'Less_Tree_Paren', $e ); + } else { + return null; + } + } else return null; + } + } while ( $e ); + + if ( $nodes ) { + return $this->NewObj1( 'Less_Tree_Expression', $nodes ); + } + } + + private function parseMediaFeatures() { + $features = array(); + + do{ + $e = $this->parseMediaFeature(); + if ( $e ) { + $features[] = $e; + if ( !$this->MatchChar( ',' ) ) break; + } else { + $e = $this->parseEntitiesVariable(); + if ( $e ) { + $features[] = $e; + if ( !$this->MatchChar( ',' ) ) break; + } + } + } while ( $e ); + + return $features ? $features : null; + } + + private function parseMedia() { + if ( $this->MatchReg( '/\\G@media/' ) ) { + $features = $this->parseMediaFeatures(); + $rules = $this->parseBlock(); + + if ( is_array( $rules ) ) { + return $this->NewObj4( 'Less_Tree_Media', array( $rules, $features, $this->pos, $this->env->currentFileInfo ) ); + } + } + } + + // + // A CSS Directive + // + // @charset "utf-8"; + // + private function parseDirective() { + if ( !$this->PeekChar( '@' ) ) { + return; + } + + $rules = null; + $index = $this->pos; + $hasBlock = true; + $hasIdentifier = false; + $hasExpression = false; + $hasUnknown = false; + + $value = $this->MatchFuncs( array( 'parseImport','parseMedia' ) ); + if ( $value ) { + return $value; + } + + $this->save(); + + $name = $this->MatchReg( '/\\G@[a-z-]+/' ); + + if ( !$name ) return; + $name = $name[0]; + + $nonVendorSpecificName = $name; + $pos = strpos( $name, '-', 2 ); + if ( $name[1] == '-' && $pos > 0 ) { + $nonVendorSpecificName = "@" . substr( $name, $pos + 1 ); + } + + switch ( $nonVendorSpecificName ) { + /* + case "@font-face": + case "@viewport": + case "@top-left": + case "@top-left-corner": + case "@top-center": + case "@top-right": + case "@top-right-corner": + case "@bottom-left": + case "@bottom-left-corner": + case "@bottom-center": + case "@bottom-right": + case "@bottom-right-corner": + case "@left-top": + case "@left-middle": + case "@left-bottom": + case "@right-top": + case "@right-middle": + case "@right-bottom": + hasBlock = true; + break; + */ + case "@charset": + $hasIdentifier = true; + $hasBlock = false; + break; + case "@namespace": + $hasExpression = true; + $hasBlock = false; + break; + case "@keyframes": + $hasIdentifier = true; + break; + case "@host": + case "@page": + case "@document": + case "@supports": + $hasUnknown = true; + break; + } + + if ( $hasIdentifier ) { + $value = $this->parseEntity(); + if ( !$value ) { + $this->error( "expected " . $name . " identifier" ); + } + } else if ( $hasExpression ) { + $value = $this->parseExpression(); + if ( !$value ) { + $this->error( "expected " . $name. " expression" ); + } + } else if ( $hasUnknown ) { + + $value = $this->MatchReg( '/\\G[^{;]+/' ); + if ( $value ) { + $value = $this->NewObj1( 'Less_Tree_Anonymous', trim( $value[0] ) ); + } + } + + if ( $hasBlock ) { + $rules = $this->parseBlockRuleset(); + } + + if ( $rules || ( !$hasBlock && $value && $this->MatchChar( ';' ) ) ) { + $this->forget(); + return $this->NewObj5( 'Less_Tree_Directive', array( $name, $value, $rules, $index, $this->env->currentFileInfo ) ); + } + + $this->restore(); + } + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + private function parseValue() { + $expressions = array(); + + do{ + $e = $this->parseExpression(); + if ( $e ) { + $expressions[] = $e; + if ( !$this->MatchChar( ',' ) ) { + break; + } + } + }while ( $e ); + + if ( $expressions ) { + return $this->NewObj1( 'Less_Tree_Value', $expressions ); + } + } + + private function parseImportant() { + if ( $this->PeekChar( '!' ) && $this->MatchReg( '/\\G! *important/' ) ) { + return ' !important'; + } + } + + private function parseSub() { + if ( $this->MatchChar( '(' ) ) { + $a = $this->parseAddition(); + if ( $a ) { + $this->expectChar( ')' ); + return $this->NewObj2( 'Less_Tree_Expression', array( array( $a ), true ) ); // instead of $e->parens = true so the value is cached + } + } + } + + /** + * Parses multiplication operation + * + * @return Less_Tree_Operation|null + */ + function parseMultiplication() { + $return = $m = $this->parseOperand(); + if ( $return ) { + while ( true ) { + + $isSpaced = $this->isWhitespace( -1 ); + + if ( $this->PeekReg( '/\\G\/[*\/]/' ) ) { + break; + } + + $op = $this->MatchChar( '/' ); + if ( !$op ) { + $op = $this->MatchChar( '*' ); + if ( !$op ) { + break; + } + } + + $a = $this->parseOperand(); + + if ( !$a ) { break; + } + + $m->parensInOp = true; + $a->parensInOp = true; + $return = $this->NewObj3( 'Less_Tree_Operation', array( $op, array( $return, $a ), $isSpaced ) ); + } + } + return $return; + + } + + /** + * Parses an addition operation + * + * @return Less_Tree_Operation|null + */ + private function parseAddition() { + $return = $m = $this->parseMultiplication(); + if ( $return ) { + while ( true ) { + + $isSpaced = $this->isWhitespace( -1 ); + + $op = $this->MatchReg( '/\\G[-+]\s+/' ); + if ( $op ) { + $op = $op[0]; + } else { + if ( !$isSpaced ) { + $op = $this->match( array( '#+','#-' ) ); + } + if ( !$op ) { + break; + } + } + + $a = $this->parseMultiplication(); + if ( !$a ) { + break; + } + + $m->parensInOp = true; + $a->parensInOp = true; + $return = $this->NewObj3( 'Less_Tree_Operation', array( $op, array( $return, $a ), $isSpaced ) ); + } + } + + return $return; + } + + /** + * Parses the conditions + * + * @return Less_Tree_Condition|null + */ + private function parseConditions() { + $index = $this->pos; + $return = $a = $this->parseCondition(); + if ( $a ) { + while ( true ) { + if ( !$this->PeekReg( '/\\G,\s*(not\s*)?\(/' ) || !$this->MatchChar( ',' ) ) { + break; + } + $b = $this->parseCondition(); + if ( !$b ) { + break; + } + + $return = $this->NewObj4( 'Less_Tree_Condition', array( 'or', $return, $b, $index ) ); + } + return $return; + } + } + + private function parseCondition() { + $index = $this->pos; + $negate = false; + $c = null; + + if ( $this->MatchReg( '/\\Gnot/' ) ) $negate = true; + $this->expectChar( '(' ); + $a = $this->MatchFuncs( array( 'parseAddition','parseEntitiesKeyword','parseEntitiesQuoted' ) ); + + if ( $a ) { + $op = $this->MatchReg( '/\\G(?:>=|<=|=<|[<=>])/' ); + if ( $op ) { + $b = $this->MatchFuncs( array( 'parseAddition','parseEntitiesKeyword','parseEntitiesQuoted' ) ); + if ( $b ) { + $c = $this->NewObj5( 'Less_Tree_Condition', array( $op[0], $a, $b, $index, $negate ) ); + } else { + $this->Error( 'Unexpected expression' ); + } + } else { + $k = $this->NewObj1( 'Less_Tree_Keyword', 'true' ); + $c = $this->NewObj5( 'Less_Tree_Condition', array( '=', $a, $k, $index, $negate ) ); + } + $this->expectChar( ')' ); + return $this->MatchReg( '/\\Gand/' ) ? $this->NewObj3( 'Less_Tree_Condition', array( 'and', $c, $this->parseCondition() ) ) : $c; + } + } + + /** + * An operand is anything that can be part of an operation, + * such as a Color, or a Variable + * + */ + private function parseOperand() { + $negate = false; + $offset = $this->pos + 1; + if ( $offset >= $this->input_len ) { + return; + } + $char = $this->input[$offset]; + if ( $char === '@' || $char === '(' ) { + $negate = $this->MatchChar( '-' ); + } + + $o = $this->MatchFuncs( array( 'parseSub','parseEntitiesDimension','parseEntitiesColor','parseEntitiesVariable','parseEntitiesCall' ) ); + + if ( $negate ) { + $o->parensInOp = true; + $o = $this->NewObj1( 'Less_Tree_Negative', $o ); + } + + return $o; + } + + /** + * Expressions either represent mathematical operations, + * or white-space delimited Entities. + * + * 1px solid black + * @var * 2 + * + * @return Less_Tree_Expression|null + */ + private function parseExpression() { + $entities = array(); + + do{ + $e = $this->MatchFuncs( array( 'parseAddition','parseEntity' ) ); + if ( $e ) { + $entities[] = $e; + // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here + if ( !$this->PeekReg( '/\\G\/[\/*]/' ) ) { + $delim = $this->MatchChar( '/' ); + if ( $delim ) { + $entities[] = $this->NewObj1( 'Less_Tree_Anonymous', $delim ); + } + } + } + }while ( $e ); + + if ( $entities ) { + return $this->NewObj1( 'Less_Tree_Expression', $entities ); + } + } + + /** + * Parse a property + * eg: 'min-width', 'orientation', etc + * + * @return string + */ + private function parseProperty() { + $name = $this->MatchReg( '/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/' ); + if ( $name ) { + return $name[1]; + } + } + + /** + * Parse a rule property + * eg: 'color', 'width', 'height', etc + * + * @return string + */ + private function parseRuleProperty() { + $offset = $this->pos; + $name = array(); + $index = array(); + $length = 0; + + $this->rulePropertyMatch( '/\\G(\*?)/', $offset, $length, $index, $name ); + while ( $this->rulePropertyMatch( '/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $offset, $length, $index, $name ) ); // ! + + if ( ( count( $name ) > 1 ) && $this->rulePropertyMatch( '/\\G\s*((?:\+_|\+)?)\s*:/', $offset, $length, $index, $name ) ) { + // at last, we have the complete match now. move forward, + // convert name particles to tree objects and return: + $this->skipWhitespace( $length ); + + if ( $name[0] === '' ) { + array_shift( $name ); + array_shift( $index ); + } + foreach ( $name as $k => $s ) { + if ( !$s || $s[0] !== '@' ) { + $name[$k] = $this->NewObj1( 'Less_Tree_Keyword', $s ); + } else { + $name[$k] = $this->NewObj3( 'Less_Tree_Variable', array( '@' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo ) ); + } + } + return $name; + } + + } + + private function rulePropertyMatch( $re, &$offset, &$length, &$index, &$name ) { + preg_match( $re, $this->input, $a, 0, $offset ); + if ( $a ) { + $index[] = $this->pos + $length; + $length += strlen( $a[0] ); + $offset += strlen( $a[0] ); + $name[] = $a[1]; + return true; + } + } + + public static function serializeVars( $vars ) { + $s = ''; + + foreach ( $vars as $name => $value ) { + $s .= ( ( $name[0] === '@' ) ? '' : '@' ) . $name .': '. $value . ( ( substr( $value, -1 ) === ';' ) ? '' : ';' ); + } + + return $s; + } + + /** + * Some versions of php have trouble with method_exists($a,$b) if $a is not an object + * + * @param string $b + */ + public static function is_method( $a, $b ) { + return is_object( $a ) && method_exists( $a, $b ); + } + + /** + * Round numbers similarly to javascript + * eg: 1.499999 to 1 instead of 2 + * + */ + public static function round( $i, $precision = 0 ) { + $precision = pow( 10, $precision ); + $i = $i * $precision; + + $ceil = ceil( $i ); + $floor = floor( $i ); + if ( ( $ceil - $i ) <= ( $i - $floor ) ) { + return $ceil / $precision; + } else { + return $floor / $precision; + } + } + + /** + * Create Less_Tree_* objects and optionally generate a cache string + * + * @return mixed + */ + public function NewObj0( $class ) { + $obj = new $class(); + if ( $this->CacheEnabled() ) { + $obj->cache_string = ' new '.$class.'()'; + } + return $obj; + } + + public function NewObj1( $class, $arg ) { + $obj = new $class( $arg ); + if ( $this->CacheEnabled() ) { + $obj->cache_string = ' new '.$class.'('.Less_Parser::ArgString( $arg ).')'; + } + return $obj; + } + + public function NewObj2( $class, $args ) { + $obj = new $class( $args[0], $args[1] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj3( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj4( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj5( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj6( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + public function NewObj7( $class, $args ) { + $obj = new $class( $args[0], $args[1], $args[2], $args[3], $args[4], $args[5], $args[6] ); + if ( $this->CacheEnabled() ) { + $this->ObjCache( $obj, $class, $args ); + } + return $obj; + } + + // caching + public function ObjCache( $obj, $class, $args = array() ) { + $obj->cache_string = ' new '.$class.'('. self::ArgCache( $args ).')'; + } + + public function ArgCache( $args ) { + return implode( ',', array_map( array( 'Less_Parser','ArgString' ), $args ) ); + } + + /** + * Convert an argument to a string for use in the parser cache + * + * @return string + */ + public static function ArgString( $arg ) { + $type = gettype( $arg ); + + if ( $type === 'object' ) { + $string = $arg->cache_string; + unset( $arg->cache_string ); + return $string; + + } elseif ( $type === 'array' ) { + $string = ' Array('; + foreach ( $arg as $k => $a ) { + $string .= var_export( $k, true ).' => '.self::ArgString( $a ).','; + } + return $string . ')'; + } + + return var_export( $arg, true ); + } + + public function Error( $msg ) { + throw new Less_Exception_Parser( $msg, null, $this->furthest, $this->env->currentFileInfo ); + } + + public static function WinPath( $path ) { + return str_replace( '\\', '/', $path ); + } + + public static function AbsPath( $path, $winPath = false ) { + if ( strpos( $path, '//' ) !== false && preg_match( '_^(https?:)?//\\w+(\\.\\w+)+/\\w+_i', $path ) ) { + return $winPath ? '' : false; + } else { + $path = realpath( $path ); + if ( $winPath ) { + $path = self::WinPath( $path ); + } + return $path; + } + } + + public function CacheEnabled() { + return ( Less_Parser::$options['cache_method'] && ( Less_Cache::$cache_dir || ( Less_Parser::$options['cache_method'] == 'callback' ) ) ); + } + +} diff --git a/less.php/lib/Less/SourceMap/Base64VLQ.php b/less.php/lib/Less/SourceMap/Base64VLQ.php new file mode 100644 index 0000000..989a4ce --- /dev/null +++ b/less.php/lib/Less/SourceMap/Base64VLQ.php @@ -0,0 +1,187 @@ + 0, 'B' => 1, 'C' => 2, 'D' => 3, 'E' => 4, 'F' => 5, 'G' => 6, + 'H' => 7,'I' => 8, 'J' => 9, 'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, + 'O' => 14, 'P' => 15, 'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, + 'V' => 21, 'W' => 22, 'X' => 23, 'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27, + 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31, 'g' => 32, 'h' => 33, 'i' => 34, + 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39, 'o' => 40, 'p' => 41, + 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47, 'w' => 48, + 'x' => 49, 'y' => 50, 'z' => 51, 0 => 52, 1 => 53, 2 => 54, 3 => 55, 4 => 56, + 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63, + ); + + /** + * Integer to char map + * + * @var array + */ + private $intToCharMap = array( + 0 => 'A', 1 => 'B', 2 => 'C', 3 => 'D', 4 => 'E', 5 => 'F', 6 => 'G', + 7 => 'H', 8 => 'I', 9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', + 14 => 'O', 15 => 'P', 16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', + 21 => 'V', 22 => 'W', 23 => 'X', 24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b', + 28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f', 32 => 'g', 33 => 'h', 34 => 'i', + 35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n', 40 => 'o', 41 => 'p', + 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v', 48 => 'w', + 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3', + 56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', + 63 => '/', + ); + + /** + * Constructor + */ + public function __construct() { + // I leave it here for future reference + // foreach(str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char) + // { + // $this->charToIntMap[$char] = $i; + // $this->intToCharMap[$i] = $char; + // } + } + + /** + * Convert from a two-complement value to a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 1 becomes 2 (10 binary), -1 becomes 3 (11 binary) + * 2 becomes 4 (100 binary), -2 becomes 5 (101 binary) + * We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297, + * even on a 64 bit machine. + * @param string $aValue + */ + public function toVLQSigned( $aValue ) { + return 0xffffffff & ( $aValue < 0 ? ( ( -$aValue ) << 1 ) + 1 : ( $aValue << 1 ) + 0 ); + } + + /** + * Convert to a two-complement value from a value where the sign bit is + * is placed in the least significant bit. For example, as decimals: + * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1 + * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2 + * We assume that the value was generated with a 32 bit machine in mind. + * Hence + * 1 becomes -2147483648 + * even on a 64 bit machine. + * @param integer $aValue + */ + public function fromVLQSigned( $aValue ) { + return $aValue & 1 ? $this->zeroFill( ~$aValue + 2, 1 ) | ( -1 - 0x7fffffff ) : $this->zeroFill( $aValue, 1 ); + } + + /** + * Return the base 64 VLQ encoded value. + * + * @param string $aValue The value to encode + * @return string The encoded value + */ + public function encode( $aValue ) { + $encoded = ''; + $vlq = $this->toVLQSigned( $aValue ); + do + { + $digit = $vlq & $this->mask; + $vlq = $this->zeroFill( $vlq, $this->shift ); + if ( $vlq > 0 ) { + $digit |= $this->continuationBit; + } + $encoded .= $this->base64Encode( $digit ); + } while ( $vlq > 0 ); + + return $encoded; + } + + /** + * Return the value decoded from base 64 VLQ. + * + * @param string $encoded The encoded value to decode + * @return integer The decoded value + */ + public function decode( $encoded ) { + $vlq = 0; + $i = 0; + do + { + $digit = $this->base64Decode( $encoded[$i] ); + $vlq |= ( $digit & $this->mask ) << ( $i * $this->shift ); + $i++; + } while ( $digit & $this->continuationBit ); + + return $this->fromVLQSigned( $vlq ); + } + + /** + * Right shift with zero fill. + * + * @param integer $a number to shift + * @param integer $b number of bits to shift + * @return integer + */ + public function zeroFill( $a, $b ) { + return ( $a >= 0 ) ? ( $a >> $b ) : ( $a >> $b ) & ( PHP_INT_MAX >> ( $b - 1 ) ); + } + + /** + * Encode single 6-bit digit as base64. + * + * @param integer $number + * @return string + * @throws Exception If the number is invalid + */ + public function base64Encode( $number ) { + if ( $number < 0 || $number > 63 ) { + throw new Exception( sprintf( 'Invalid number "%s" given. Must be between 0 and 63.', $number ) ); + } + return $this->intToCharMap[$number]; + } + + /** + * Decode single 6-bit digit from base64 + * + * @param string $char + * @return number + * @throws Exception If the number is invalid + */ + public function base64Decode( $char ) { + if ( !array_key_exists( $char, $this->charToIntMap ) ) { + throw new Exception( sprintf( 'Invalid base 64 digit "%s" given.', $char ) ); + } + return $this->charToIntMap[$char]; + } + +} diff --git a/less.php/lib/Less/SourceMap/Generator.php b/less.php/lib/Less/SourceMap/Generator.php new file mode 100644 index 0000000..eded501 --- /dev/null +++ b/less.php/lib/Less/SourceMap/Generator.php @@ -0,0 +1,354 @@ + '', + + // an optional name of the generated code that this source map is associated with. + 'sourceMapFilename' => null, + + // url of the map + 'sourceMapURL' => null, + + // absolute path to a file to write the map to + 'sourceMapWriteTo' => null, + + // output source contents? + 'outputSourceFiles' => false, + + // base path for filename normalization + 'sourceMapRootpath' => '', + + // base path for filename normalization + 'sourceMapBasepath' => '' + ); + + /** + * The base64 VLQ encoder + * + * @var Less_SourceMap_Base64VLQ + */ + protected $encoder; + + /** + * Array of mappings + * + * @var array + */ + protected $mappings = array(); + + /** + * The root node + * + * @var Less_Tree_Ruleset + */ + protected $root; + + /** + * Array of contents map + * + * @var array + */ + protected $contentsMap = array(); + + /** + * File to content map + * + * @var array + */ + protected $sources = array(); + protected $source_keys = array(); + + /** + * Constructor + * + * @param Less_Tree_Ruleset $root The root node + * @param array $options Array of options + */ + public function __construct( Less_Tree_Ruleset $root, $contentsMap, $options = array() ) { + $this->root = $root; + $this->contentsMap = $contentsMap; + $this->encoder = new Less_SourceMap_Base64VLQ(); + + $this->SetOptions( $options ); + + $this->options['sourceMapRootpath'] = $this->fixWindowsPath( $this->options['sourceMapRootpath'], true ); + $this->options['sourceMapBasepath'] = $this->fixWindowsPath( $this->options['sourceMapBasepath'], true ); + } + + /** + * Generates the CSS + * + * @return string + */ + public function generateCSS() { + $output = new Less_Output_Mapped( $this->contentsMap, $this ); + + // catch the output + $this->root->genCSS( $output ); + + $sourceMapUrl = $this->getOption( 'sourceMapURL' ); + $sourceMapFilename = $this->getOption( 'sourceMapFilename' ); + $sourceMapContent = $this->generateJson(); + $sourceMapWriteTo = $this->getOption( 'sourceMapWriteTo' ); + + if ( !$sourceMapUrl && $sourceMapFilename ) { + $sourceMapUrl = $this->normalizeFilename( $sourceMapFilename ); + } + + // write map to a file + if ( $sourceMapWriteTo ) { + $this->saveMap( $sourceMapWriteTo, $sourceMapContent ); + } + + // inline the map + if ( !$sourceMapUrl ) { + $sourceMapUrl = sprintf( 'data:application/json,%s', Less_Functions::encodeURIComponent( $sourceMapContent ) ); + } + + if ( $sourceMapUrl ) { + $output->add( sprintf( '/*# sourceMappingURL=%s */', $sourceMapUrl ) ); + } + + return $output->toString(); + } + + /** + * Saves the source map to a file + * + * @param string $file The absolute path to a file + * @param string $content The content to write + * @throws Exception If the file could not be saved + */ + protected function saveMap( $file, $content ) { + $dir = dirname( $file ); + // directory does not exist + if ( !is_dir( $dir ) ) { + // FIXME: create the dir automatically? + throw new Exception( sprintf( 'The directory "%s" does not exist. Cannot save the source map.', $dir ) ); + } + // FIXME: proper saving, with dir write check! + if ( file_put_contents( $file, $content ) === false ) { + throw new Exception( sprintf( 'Cannot save the source map to "%s"', $file ) ); + } + return true; + } + + /** + * Normalizes the filename + * + * @param string $filename + * @return string + */ + protected function normalizeFilename( $filename ) { + $filename = $this->fixWindowsPath( $filename ); + + $rootpath = $this->getOption( 'sourceMapRootpath' ); + $basePath = $this->getOption( 'sourceMapBasepath' ); + + // "Trim" the 'sourceMapBasepath' from the output filename. + if ( is_string( $basePath ) && strpos( $filename, $basePath ) === 0 ) { + $filename = substr( $filename, strlen( $basePath ) ); + } + + // Remove extra leading path separators. + if ( strpos( $filename, '\\' ) === 0 || strpos( $filename, '/' ) === 0 ) { + $filename = substr( $filename, 1 ); + } + + return $rootpath . $filename; + } + + /** + * Adds a mapping + * + * @param integer $generatedLine The line number in generated file + * @param integer $generatedColumn The column number in generated file + * @param integer $originalLine The line number in original file + * @param integer $originalColumn The column number in original file + * @param string $sourceFile The original source file + */ + public function addMapping( $generatedLine, $generatedColumn, $originalLine, $originalColumn, $fileInfo ) { + $this->mappings[] = array( + 'generated_line' => $generatedLine, + 'generated_column' => $generatedColumn, + 'original_line' => $originalLine, + 'original_column' => $originalColumn, + 'source_file' => $fileInfo['currentUri'] + ); + + $this->sources[$fileInfo['currentUri']] = $fileInfo['filename']; + } + + /** + * Generates the JSON source map + * + * @return string + * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# + */ + protected function generateJson() { + $sourceMap = array(); + $mappings = $this->generateMappings(); + + // File version (always the first entry in the object) and must be a positive integer. + $sourceMap['version'] = self::VERSION; + + // An optional name of the generated code that this source map is associated with. + $file = $this->getOption( 'sourceMapFilename' ); + if ( $file ) { + $sourceMap['file'] = $file; + } + + // An optional source root, useful for relocating source files on a server or removing repeated values in the 'sources' entry. This value is prepended to the individual entries in the 'source' field. + $root = $this->getOption( 'sourceRoot' ); + if ( $root ) { + $sourceMap['sourceRoot'] = $root; + } + + // A list of original sources used by the 'mappings' entry. + $sourceMap['sources'] = array(); + foreach ( $this->sources as $source_uri => $source_filename ) { + $sourceMap['sources'][] = $this->normalizeFilename( $source_filename ); + } + + // A list of symbol names used by the 'mappings' entry. + $sourceMap['names'] = array(); + + // A string with the encoded mapping data. + $sourceMap['mappings'] = $mappings; + + if ( $this->getOption( 'outputSourceFiles' ) ) { + // An optional list of source content, useful when the 'source' can't be hosted. + // The contents are listed in the same order as the sources above. + // 'null' may be used if some original sources should be retrieved by name. + $sourceMap['sourcesContent'] = $this->getSourcesContent(); + } + + // less.js compat fixes + if ( count( $sourceMap['sources'] ) && empty( $sourceMap['sourceRoot'] ) ) { + unset( $sourceMap['sourceRoot'] ); + } + + return json_encode( $sourceMap ); + } + + /** + * Returns the sources contents + * + * @return array|null + */ + protected function getSourcesContent() { + if ( empty( $this->sources ) ) { + return; + } + $content = array(); + foreach ( $this->sources as $sourceFile ) { + $content[] = file_get_contents( $sourceFile ); + } + return $content; + } + + /** + * Generates the mappings string + * + * @return string + */ + public function generateMappings() { + if ( !count( $this->mappings ) ) { + return ''; + } + + $this->source_keys = array_flip( array_keys( $this->sources ) ); + + // group mappings by generated line number. + $groupedMap = $groupedMapEncoded = array(); + foreach ( $this->mappings as $m ) { + $groupedMap[$m['generated_line']][] = $m; + } + ksort( $groupedMap ); + + $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0; + + foreach ( $groupedMap as $lineNumber => $line_map ) { + while ( ++$lastGeneratedLine < $lineNumber ) { + $groupedMapEncoded[] = ';'; + } + + $lineMapEncoded = array(); + $lastGeneratedColumn = 0; + + foreach ( $line_map as $m ) { + $mapEncoded = $this->encoder->encode( $m['generated_column'] - $lastGeneratedColumn ); + $lastGeneratedColumn = $m['generated_column']; + + // find the index + if ( $m['source_file'] ) { + $index = $this->findFileIndex( $m['source_file'] ); + if ( $index !== false ) { + $mapEncoded .= $this->encoder->encode( $index - $lastOriginalIndex ); + $lastOriginalIndex = $index; + + // lines are stored 0-based in SourceMap spec version 3 + $mapEncoded .= $this->encoder->encode( $m['original_line'] - 1 - $lastOriginalLine ); + $lastOriginalLine = $m['original_line'] - 1; + + $mapEncoded .= $this->encoder->encode( $m['original_column'] - $lastOriginalColumn ); + $lastOriginalColumn = $m['original_column']; + } + } + + $lineMapEncoded[] = $mapEncoded; + } + + $groupedMapEncoded[] = implode( ',', $lineMapEncoded ) . ';'; + } + + return rtrim( implode( $groupedMapEncoded ), ';' ); + } + + /** + * Finds the index for the filename + * + * @param string $filename + * @return integer|false + */ + protected function findFileIndex( $filename ) { + return $this->source_keys[$filename]; + } + + /** + * fix windows paths + * @param string $path + * @return string + */ + public function fixWindowsPath( $path, $addEndSlash = false ) { + $slash = ( $addEndSlash ) ? '/' : ''; + if ( !empty( $path ) ) { + $path = str_replace( '\\', '/', $path ); + $path = rtrim( $path, '/' ) . $slash; + } + + return $path; + } + +} diff --git a/less.php/lib/Less/Tree.php b/less.php/lib/Less/Tree.php new file mode 100644 index 0000000..7b48e9a --- /dev/null +++ b/less.php/lib/Less/Tree.php @@ -0,0 +1,84 @@ +genCSS( $output ); + return $output->toString(); + } + + /** + * Generate CSS by adding it to the output object + * + * @param Less_Output $output The output + * @return void + */ + public function genCSS( $output ) { + } + + /** + * @param Less_Tree_Ruleset[] $rules + */ + public static function outputRuleset( $output, $rules ) { + $ruleCnt = count( $rules ); + Less_Environment::$tabLevel++; + + // Compressed + if ( Less_Parser::$options['compress'] ) { + $output->add( '{' ); + for ( $i = 0; $i < $ruleCnt; $i++ ) { + $rules[$i]->genCSS( $output ); + } + + $output->add( '}' ); + Less_Environment::$tabLevel--; + return; + } + + // Non-compressed + $tabSetStr = "\n".str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 ); + $tabRuleStr = $tabSetStr.Less_Parser::$options['indentation']; + + $output->add( " {" ); + for ( $i = 0; $i < $ruleCnt; $i++ ) { + $output->add( $tabRuleStr ); + $rules[$i]->genCSS( $output ); + } + Less_Environment::$tabLevel--; + $output->add( $tabSetStr.'}' ); + + } + + public function accept( $visitor ) { + } + + public static function ReferencedArray( $rules ) { + foreach ( $rules as $rule ) { + if ( method_exists( $rule, 'markReferenced' ) ) { + $rule->markReferenced(); + } + } + } + + /** + * Requires php 5.3+ + */ + public static function __set_state( $args ) { + $class = get_called_class(); + $obj = new $class( null, null, null, null ); + foreach ( $args as $key => $val ) { + $obj->$key = $val; + } + return $obj; + } + +} diff --git a/less.php/lib/Less/Tree/Alpha.php b/less.php/lib/Less/Tree/Alpha.php new file mode 100644 index 0000000..cd38c91 --- /dev/null +++ b/less.php/lib/Less/Tree/Alpha.php @@ -0,0 +1,48 @@ +value = $val; + } + + // function accept( $visitor ){ + // $this->value = $visitor->visit( $this->value ); + //} + + public function compile( $env ) { + if ( is_object( $this->value ) ) { + $this->value = $this->value->compile( $env ); + } + + return $this; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( "alpha(opacity=" ); + + if ( is_string( $this->value ) ) { + $output->add( $this->value ); + } else { + $this->value->genCSS( $output ); + } + + $output->add( ')' ); + } + + public function toCSS() { + return "alpha(opacity=" . ( is_string( $this->value ) ? $this->value : $this->value->toCSS() ) . ")"; + } + +} diff --git a/less.php/lib/Less/Tree/Anonymous.php b/less.php/lib/Less/Tree/Anonymous.php new file mode 100644 index 0000000..1dc9bab --- /dev/null +++ b/less.php/lib/Less/Tree/Anonymous.php @@ -0,0 +1,58 @@ +value = $value; + $this->index = $index; + $this->mapLines = $mapLines; + $this->currentFileInfo = $currentFileInfo; + } + + public function compile() { + return new Less_Tree_Anonymous( $this->value, $this->index, $this->currentFileInfo, $this->mapLines ); + } + + public function compare( $x ) { + if ( !is_object( $x ) ) { + return -1; + } + + $left = $this->toCSS(); + $right = $x->toCSS(); + + if ( $left === $right ) { + return 0; + } + + return $left < $right ? -1 : 1; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->value, $this->currentFileInfo, $this->index, $this->mapLines ); + } + + public function toCSS() { + return $this->value; + } + +} diff --git a/less.php/lib/Less/Tree/Assignment.php b/less.php/lib/Less/Tree/Assignment.php new file mode 100644 index 0000000..69c7a4e --- /dev/null +++ b/less.php/lib/Less/Tree/Assignment.php @@ -0,0 +1,39 @@ +key = $key; + $this->value = $val; + } + + public function accept( $visitor ) { + $this->value = $visitor->visitObj( $this->value ); + } + + public function compile( $env ) { + return new Less_Tree_Assignment( $this->key, $this->value->compile( $env ) ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->key . '=' ); + $this->value->genCSS( $output ); + } + + public function toCss() { + return $this->key . '=' . $this->value->toCSS(); + } +} diff --git a/less.php/lib/Less/Tree/Attribute.php b/less.php/lib/Less/Tree/Attribute.php new file mode 100644 index 0000000..c47abb9 --- /dev/null +++ b/less.php/lib/Less/Tree/Attribute.php @@ -0,0 +1,53 @@ +key = $key; + $this->op = $op; + $this->value = $value; + } + + public function compile( $env ) { + $key_obj = is_object( $this->key ); + $val_obj = is_object( $this->value ); + + if ( !$key_obj && !$val_obj ) { + return $this; + } + + return new Less_Tree_Attribute( + $key_obj ? $this->key->compile( $env ) : $this->key, + $this->op, + $val_obj ? $this->value->compile( $env ) : $this->value ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->toCSS() ); + } + + public function toCSS() { + $value = $this->key; + + if ( $this->op ) { + $value .= $this->op; + $value .= ( is_object( $this->value ) ? $this->value->toCSS() : $this->value ); + } + + return '[' . $value . ']'; + } +} diff --git a/less.php/lib/Less/Tree/Call.php b/less.php/lib/Less/Tree/Call.php new file mode 100644 index 0000000..5341b74 --- /dev/null +++ b/less.php/lib/Less/Tree/Call.php @@ -0,0 +1,117 @@ +name = $name; + $this->args = $args; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function accept( $visitor ) { + $this->args = $visitor->visitArray( $this->args ); + } + + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // or we simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + public function compile( $env = null ) { + $args = array(); + foreach ( $this->args as $a ) { + $args[] = $a->compile( $env ); + } + + $nameLC = strtolower( $this->name ); + switch ( $nameLC ) { + case '%': + $nameLC = '_percent'; + break; + + case 'get-unit': + $nameLC = 'getunit'; + break; + + case 'data-uri': + $nameLC = 'datauri'; + break; + + case 'svg-gradient': + $nameLC = 'svggradient'; + break; + } + + $result = null; + if ( $nameLC === 'default' ) { + $result = Less_Tree_DefaultFunc::compile(); + + } else { + + if ( method_exists( 'Less_Functions', $nameLC ) ) { // 1. + try { + + $func = new Less_Functions( $env, $this->currentFileInfo ); + $result = call_user_func_array( array( $func,$nameLC ), $args ); + + } catch ( Exception $e ) { + throw new Less_Exception_Compiler( 'error evaluating function `' . $this->name . '` '.$e->getMessage().' index: '. $this->index ); + } + } elseif ( isset( $env->functions[$nameLC] ) && is_callable( $env->functions[$nameLC] ) ) { + try { + $result = call_user_func_array( $env->functions[$nameLC], $args ); + } catch ( Exception $e ) { + throw new Less_Exception_Compiler( 'error evaluating function `' . $this->name . '` '.$e->getMessage().' index: '. $this->index ); + } + } + } + + if ( $result !== null ) { + return $result; + } + + return new Less_Tree_Call( $this->name, $args, $this->index, $this->currentFileInfo ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->name . '(', $this->currentFileInfo, $this->index ); + $args_len = count( $this->args ); + for ( $i = 0; $i < $args_len; $i++ ) { + $this->args[$i]->genCSS( $output ); + if ( $i + 1 < $args_len ) { + $output->add( ', ' ); + } + } + + $output->add( ')' ); + } + + // public function toCSS(){ + // return $this->compile()->toCSS(); + //} + +} diff --git a/less.php/lib/Less/Tree/Color.php b/less.php/lib/Less/Tree/Color.php new file mode 100644 index 0000000..b0f79db --- /dev/null +++ b/less.php/lib/Less/Tree/Color.php @@ -0,0 +1,230 @@ +rgb = $rgb; + $this->alpha = $a; + $this->isTransparentKeyword = true; + return; + } + + $this->rgb = array(); + if ( is_array( $rgb ) ) { + $this->rgb = $rgb; + } else if ( strlen( $rgb ) == 6 ) { + foreach ( str_split( $rgb, 2 ) as $c ) { + $this->rgb[] = hexdec( $c ); + } + } else { + foreach ( str_split( $rgb, 1 ) as $c ) { + $this->rgb[] = hexdec( $c.$c ); + } + } + $this->alpha = is_numeric( $a ) ? $a : 1; + } + + public function compile() { + return $this; + } + + public function luma() { + $r = $this->rgb[0] / 255; + $g = $this->rgb[1] / 255; + $b = $this->rgb[2] / 255; + + $r = ( $r <= 0.03928 ) ? $r / 12.92 : pow( ( ( $r + 0.055 ) / 1.055 ), 2.4 ); + $g = ( $g <= 0.03928 ) ? $g / 12.92 : pow( ( ( $g + 0.055 ) / 1.055 ), 2.4 ); + $b = ( $b <= 0.03928 ) ? $b / 12.92 : pow( ( ( $b + 0.055 ) / 1.055 ), 2.4 ); + + return 0.2126 * $r + 0.7152 * $g + 0.0722 * $b; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->toCSS() ); + } + + public function toCSS( $doNotCompress = false ) { + $compress = Less_Parser::$options['compress'] && !$doNotCompress; + $alpha = Less_Functions::fround( $this->alpha ); + + // + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. + // + if ( $alpha < 1 ) { + if ( ( $alpha === 0 || $alpha === 0.0 ) && isset( $this->isTransparentKeyword ) && $this->isTransparentKeyword ) { + return 'transparent'; + } + + $values = array(); + foreach ( $this->rgb as $c ) { + $values[] = Less_Functions::clamp( round( $c ), 255 ); + } + $values[] = $alpha; + + $glue = ( $compress ? ',' : ', ' ); + return "rgba(" . implode( $glue, $values ) . ")"; + } else { + + $color = $this->toRGB(); + + if ( $compress ) { + + // Convert color to short format + if ( $color[1] === $color[2] && $color[3] === $color[4] && $color[5] === $color[6] ) { + $color = '#'.$color[1] . $color[3] . $color[5]; + } + } + + return $color; + } + } + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + + /** + * @param string $op + */ + public function operate( $op, $other ) { + $rgb = array(); + $alpha = $this->alpha * ( 1 - $other->alpha ) + $other->alpha; + for ( $c = 0; $c < 3; $c++ ) { + $rgb[$c] = Less_Functions::operate( $op, $this->rgb[$c], $other->rgb[$c] ); + } + return new Less_Tree_Color( $rgb, $alpha ); + } + + public function toRGB() { + return $this->toHex( $this->rgb ); + } + + public function toHSL() { + $r = $this->rgb[0] / 255; + $g = $this->rgb[1] / 255; + $b = $this->rgb[2] / 255; + $a = $this->alpha; + + $max = max( $r, $g, $b ); + $min = min( $r, $g, $b ); + $l = ( $max + $min ) / 2; + $d = $max - $min; + + $h = $s = 0; + if ( $max !== $min ) { + $s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min ); + + switch ( $max ) { + case $r: $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 ); +break; + case $g: $h = ( $b - $r ) / $d + 2; +break; + case $b: $h = ( $r - $g ) / $d + 4; +break; + } + $h /= 6; + } + return array( 'h' => $h * 360, 's' => $s, 'l' => $l, 'a' => $a ); + } + + // Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript + public function toHSV() { + $r = $this->rgb[0] / 255; + $g = $this->rgb[1] / 255; + $b = $this->rgb[2] / 255; + $a = $this->alpha; + + $max = max( $r, $g, $b ); + $min = min( $r, $g, $b ); + + $v = $max; + + $d = $max - $min; + if ( $max === 0 ) { + $s = 0; + } else { + $s = $d / $max; + } + + $h = 0; + if ( $max !== $min ) { + switch ( $max ) { + case $r: $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 ); +break; + case $g: $h = ( $b - $r ) / $d + 2; +break; + case $b: $h = ( $r - $g ) / $d + 4; +break; + } + $h /= 6; + } + return array( 'h' => $h * 360, 's' => $s, 'v' => $v, 'a' => $a ); + } + + public function toARGB() { + $argb = array_merge( (array)Less_Parser::round( $this->alpha * 255 ), $this->rgb ); + return $this->toHex( $argb ); + } + + public function compare( $x ) { + if ( !property_exists( $x, 'rgb' ) ) { + return -1; + } + + return ( $x->rgb[0] === $this->rgb[0] && + $x->rgb[1] === $this->rgb[1] && + $x->rgb[2] === $this->rgb[2] && + $x->alpha === $this->alpha ) ? 0 : -1; + } + + public function toHex( $v ) { + $ret = '#'; + foreach ( $v as $c ) { + $c = Less_Functions::clamp( Less_Parser::round( $c ), 255 ); + if ( $c < 16 ) { + $ret .= '0'; + } + $ret .= dechex( $c ); + } + + return $ret; + } + + /** + * @param string $keyword + */ + public static function fromKeyword( $keyword ) { + $keyword = strtolower( $keyword ); + + if ( Less_Colors::hasOwnProperty( $keyword ) ) { + // detect named color + return new Less_Tree_Color( substr( Less_Colors::color( $keyword ), 1 ) ); + } + + if ( $keyword === 'transparent' ) { + return new Less_Tree_Color( array( 0, 0, 0 ), 0, true ); + } + } + +} diff --git a/less.php/lib/Less/Tree/Comment.php b/less.php/lib/Less/Tree/Comment.php new file mode 100644 index 0000000..c7e6c0f --- /dev/null +++ b/less.php/lib/Less/Tree/Comment.php @@ -0,0 +1,51 @@ +value = $value; + $this->silent = !!$silent; + $this->currentFileInfo = $currentFileInfo; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + // if( $this->debugInfo ){ + //$output->add( tree.debugInfo($env, $this), $this->currentFileInfo, $this->index); + //} + $output->add( trim( $this->value ) );// TODO shouldn't need to trim, we shouldn't grab the \n + } + + public function toCSS() { + return Less_Parser::$options['compress'] ? '' : $this->value; + } + + public function isSilent() { + $isReference = ( $this->currentFileInfo && isset( $this->currentFileInfo['reference'] ) && ( !isset( $this->isReferenced ) || !$this->isReferenced ) ); + $isCompressed = Less_Parser::$options['compress'] && !preg_match( '/^\/\*!/', $this->value ); + return $this->silent || $isReference || $isCompressed; + } + + public function compile() { + return $this; + } + + public function markReferenced() { + $this->isReferenced = true; + } + +} diff --git a/less.php/lib/Less/Tree/Condition.php b/less.php/lib/Less/Tree/Condition.php new file mode 100644 index 0000000..355de8c --- /dev/null +++ b/less.php/lib/Less/Tree/Condition.php @@ -0,0 +1,72 @@ +op = trim( $op ); + $this->lvalue = $l; + $this->rvalue = $r; + $this->index = $i; + $this->negate = $negate; + } + + public function accept( $visitor ) { + $this->lvalue = $visitor->visitObj( $this->lvalue ); + $this->rvalue = $visitor->visitObj( $this->rvalue ); + } + + public function compile( $env ) { + $a = $this->lvalue->compile( $env ); + $b = $this->rvalue->compile( $env ); + + switch ( $this->op ) { + case 'and': + $result = $a && $b; + break; + + case 'or': + $result = $a || $b; + break; + + default: + if ( Less_Parser::is_method( $a, 'compare' ) ) { + $result = $a->compare( $b ); + } elseif ( Less_Parser::is_method( $b, 'compare' ) ) { + $result = $b->compare( $a ); + } else { + throw new Less_Exception_Compiler( 'Unable to perform comparison', null, $this->index ); + } + + switch ( $result ) { + case -1: + $result = $this->op === '<' || $this->op === '=<' || $this->op === '<='; + break; + + case 0: + $result = $this->op === '=' || $this->op === '>=' || $this->op === '=<' || $this->op === '<='; + break; + + case 1: + $result = $this->op === '>' || $this->op === '>='; + break; + } + break; + } + + return $this->negate ? !$result : $result; + } + +} diff --git a/less.php/lib/Less/Tree/DefaultFunc.php b/less.php/lib/Less/Tree/DefaultFunc.php new file mode 100644 index 0000000..41b78b6 --- /dev/null +++ b/less.php/lib/Less/Tree/DefaultFunc.php @@ -0,0 +1,34 @@ +ruleset = $ruleset; + $this->frames = $frames; + } + + public function accept( $visitor ) { + $this->ruleset = $visitor->visitObj( $this->ruleset ); + } + + public function compile( $env ) { + if ( $this->frames ) { + $frames = $this->frames; + } else { + $frames = $env->frames; + } + return new Less_Tree_DetachedRuleset( $this->ruleset, $frames ); + } + + public function callEval( $env ) { + if ( $this->frames ) { + return $this->ruleset->compile( $env->copyEvalEnv( array_merge( $this->frames, $env->frames ) ) ); + } + return $this->ruleset->compile( $env ); + } +} diff --git a/less.php/lib/Less/Tree/Dimension.php b/less.php/lib/Less/Tree/Dimension.php new file mode 100644 index 0000000..70c71b4 --- /dev/null +++ b/less.php/lib/Less/Tree/Dimension.php @@ -0,0 +1,198 @@ +value = floatval( $value ); + + if ( $unit && ( $unit instanceof Less_Tree_Unit ) ) { + $this->unit = $unit; + } elseif ( $unit ) { + $this->unit = new Less_Tree_Unit( array( $unit ) ); + } else { + $this->unit = new Less_Tree_Unit(); + } + } + + public function accept( $visitor ) { + $this->unit = $visitor->visitObj( $this->unit ); + } + + public function compile() { + return $this; + } + + public function toColor() { + return new Less_Tree_Color( array( $this->value, $this->value, $this->value ) ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( Less_Parser::$options['strictUnits'] && !$this->unit->isSingular() ) { + throw new Less_Exception_Compiler( "Multiple units in dimension. Correct the units or use the unit function. Bad unit: ".$this->unit->toString() ); + } + + $value = Less_Functions::fround( $this->value ); + $strValue = (string)$value; + + if ( $value !== 0 && $value < 0.000001 && $value > -0.000001 ) { + // would be output 1e-6 etc. + $strValue = number_format( $strValue, 10 ); + $strValue = preg_replace( '/\.?0+$/', '', $strValue ); + } + + if ( Less_Parser::$options['compress'] ) { + // Zero values doesn't need a unit + if ( $value === 0 && $this->unit->isLength() ) { + $output->add( $strValue ); + return $strValue; + } + + // Float values doesn't need a leading zero + if ( $value > 0 && $value < 1 && $strValue[0] === '0' ) { + $strValue = substr( $strValue, 1 ); + } + } + + $output->add( $strValue ); + $this->unit->genCSS( $output ); + } + + public function __toString() { + return $this->toCSS(); + } + + // In an operation between two Dimensions, + // we default to the first Dimension's unit, + // so `1px + 2em` will yield `3px`. + + /** + * @param string $op + */ + public function operate( $op, $other ) { + $value = Less_Functions::operate( $op, $this->value, $other->value ); + $unit = clone $this->unit; + + if ( $op === '+' || $op === '-' ) { + + if ( !$unit->numerator && !$unit->denominator ) { + $unit->numerator = $other->unit->numerator; + $unit->denominator = $other->unit->denominator; + } elseif ( !$other->unit->numerator && !$other->unit->denominator ) { + // do nothing + } else { + $other = $other->convertTo( $this->unit->usedUnits() ); + + if ( Less_Parser::$options['strictUnits'] && $other->unit->toString() !== $unit->toCSS() ) { + throw new Less_Exception_Compiler( "Incompatible units. Change the units or use the unit function. Bad units: '" . $unit->toString() . "' and " . $other->unit->toString() . "'." ); + } + + $value = Less_Functions::operate( $op, $this->value, $other->value ); + } + } elseif ( $op === '*' ) { + $unit->numerator = array_merge( $unit->numerator, $other->unit->numerator ); + $unit->denominator = array_merge( $unit->denominator, $other->unit->denominator ); + sort( $unit->numerator ); + sort( $unit->denominator ); + $unit->cancel(); + } elseif ( $op === '/' ) { + $unit->numerator = array_merge( $unit->numerator, $other->unit->denominator ); + $unit->denominator = array_merge( $unit->denominator, $other->unit->numerator ); + sort( $unit->numerator ); + sort( $unit->denominator ); + $unit->cancel(); + } + return new Less_Tree_Dimension( $value, $unit ); + } + + public function compare( $other ) { + if ( $other instanceof Less_Tree_Dimension ) { + + if ( $this->unit->isEmpty() || $other->unit->isEmpty() ) { + $a = $this; + $b = $other; + } else { + $a = $this->unify(); + $b = $other->unify(); + if ( $a->unit->compare( $b->unit ) !== 0 ) { + return -1; + } + } + $aValue = $a->value; + $bValue = $b->value; + + if ( $bValue > $aValue ) { + return -1; + } elseif ( $bValue < $aValue ) { + return 1; + } else { + return 0; + } + } else { + return -1; + } + } + + public function unify() { + return $this->convertTo( array( 'length' => 'px', 'duration' => 's', 'angle' => 'rad' ) ); + } + + public function convertTo( $conversions ) { + $value = $this->value; + $unit = clone $this->unit; + + if ( is_string( $conversions ) ) { + $derivedConversions = array(); + foreach ( Less_Tree_UnitConversions::$groups as $i ) { + if ( isset( Less_Tree_UnitConversions::${$i}[$conversions] ) ) { + $derivedConversions = array( $i => $conversions ); + } + } + $conversions = $derivedConversions; + } + + foreach ( $conversions as $groupName => $targetUnit ) { + $group = Less_Tree_UnitConversions::${$groupName}; + + // numerator + foreach ( $unit->numerator as $i => $atomicUnit ) { + $atomicUnit = $unit->numerator[$i]; + if ( !isset( $group[$atomicUnit] ) ) { + continue; + } + + $value = $value * ( $group[$atomicUnit] / $group[$targetUnit] ); + + $unit->numerator[$i] = $targetUnit; + } + + // denominator + foreach ( $unit->denominator as $i => $atomicUnit ) { + $atomicUnit = $unit->denominator[$i]; + if ( !isset( $group[$atomicUnit] ) ) { + continue; + } + + $value = $value / ( $group[$atomicUnit] / $group[$targetUnit] ); + + $unit->denominator[$i] = $targetUnit; + } + } + + $unit->cancel(); + + return new Less_Tree_Dimension( $value, $unit ); + } +} diff --git a/less.php/lib/Less/Tree/Directive.php b/less.php/lib/Less/Tree/Directive.php new file mode 100644 index 0000000..6c6a94f --- /dev/null +++ b/less.php/lib/Less/Tree/Directive.php @@ -0,0 +1,96 @@ +name = $name; + $this->value = $value; + if ( $rules ) { + $this->rules = $rules; + $this->rules->allowImports = true; + } + + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + $this->debugInfo = $debugInfo; + } + + public function accept( $visitor ) { + if ( $this->rules ) { + $this->rules = $visitor->visitObj( $this->rules ); + } + if ( $this->value ) { + $this->value = $visitor->visitObj( $this->value ); + } + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $value = $this->value; + $rules = $this->rules; + $output->add( $this->name, $this->currentFileInfo, $this->index ); + if ( $this->value ) { + $output->add( ' ' ); + $this->value->genCSS( $output ); + } + if ( $this->rules ) { + Less_Tree::outputRuleset( $output, array( $this->rules ) ); + } else { + $output->add( ';' ); + } + } + + public function compile( $env ) { + $value = $this->value; + $rules = $this->rules; + if ( $value ) { + $value = $value->compile( $env ); + } + + if ( $rules ) { + $rules = $rules->compile( $env ); + $rules->root = true; + } + + return new Less_Tree_Directive( $this->name, $value, $rules, $this->index, $this->currentFileInfo, $this->debugInfo ); + } + + public function variable( $name ) { + if ( $this->rules ) { + return $this->rules->variable( $name ); + } + } + + public function find( $selector ) { + if ( $this->rules ) { + return $this->rules->find( $selector, $this ); + } + } + + // rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); }, + + public function markReferenced() { + $this->isReferenced = true; + if ( $this->rules ) { + Less_Tree::ReferencedArray( $this->rules->rules ); + } + } + +} diff --git a/less.php/lib/Less/Tree/Element.php b/less.php/lib/Less/Tree/Element.php new file mode 100644 index 0000000..5e25bc0 --- /dev/null +++ b/less.php/lib/Less/Tree/Element.php @@ -0,0 +1,70 @@ +value = $value; + $this->value_is_object = is_object( $value ); + + if ( $combinator ) { + $this->combinator = $combinator; + } + + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function accept( $visitor ) { + if ( $this->value_is_object ) { // object or string + $this->value = $visitor->visitObj( $this->value ); + } + } + + public function compile( $env ) { + if ( Less_Environment::$mixin_stack ) { + return new Less_Tree_Element( $this->combinator, ( $this->value_is_object ? $this->value->compile( $env ) : $this->value ), $this->index, $this->currentFileInfo ); + } + + if ( $this->value_is_object ) { + $this->value = $this->value->compile( $env ); + } + + return $this; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->toCSS(), $this->currentFileInfo, $this->index ); + } + + public function toCSS() { + if ( $this->value_is_object ) { + $value = $this->value->toCSS(); + } else { + $value = $this->value; + } + + if ( $value === '' && $this->combinator && $this->combinator === '&' ) { + return ''; + } + + return Less_Environment::$_outputMap[$this->combinator] . $value; + } + +} diff --git a/less.php/lib/Less/Tree/Expression.php b/less.php/lib/Less/Tree/Expression.php new file mode 100644 index 0000000..cf06315 --- /dev/null +++ b/less.php/lib/Less/Tree/Expression.php @@ -0,0 +1,95 @@ +value = $value; + $this->parens = $parens; + } + + public function accept( $visitor ) { + $this->value = $visitor->visitArray( $this->value ); + } + + public function compile( $env ) { + $doubleParen = false; + + if ( $this->parens && !$this->parensInOp ) { + Less_Environment::$parensStack++; + } + + $returnValue = null; + if ( $this->value ) { + + $count = count( $this->value ); + + if ( $count > 1 ) { + + $ret = array(); + foreach ( $this->value as $e ) { + $ret[] = $e->compile( $env ); + } + $returnValue = new Less_Tree_Expression( $ret ); + + } else { + + if ( ( $this->value[0] instanceof Less_Tree_Expression ) && $this->value[0]->parens && !$this->value[0]->parensInOp ) { + $doubleParen = true; + } + + $returnValue = $this->value[0]->compile( $env ); + } + + } else { + $returnValue = $this; + } + + if ( $this->parens ) { + if ( !$this->parensInOp ) { + Less_Environment::$parensStack--; + + } elseif ( !Less_Environment::isMathOn() && !$doubleParen ) { + $returnValue = new Less_Tree_Paren( $returnValue ); + + } + } + return $returnValue; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $val_len = count( $this->value ); + for ( $i = 0; $i < $val_len; $i++ ) { + $this->value[$i]->genCSS( $output ); + if ( $i + 1 < $val_len ) { + $output->add( ' ' ); + } + } + } + + public function throwAwayComments() { + if ( is_array( $this->value ) ) { + $new_value = array(); + foreach ( $this->value as $v ) { + if ( $v instanceof Less_Tree_Comment ) { + continue; + } + $new_value[] = $v; + } + $this->value = $new_value; + } + } +} diff --git a/less.php/lib/Less/Tree/Extend.php b/less.php/lib/Less/Tree/Extend.php new file mode 100644 index 0000000..4dd3665 --- /dev/null +++ b/less.php/lib/Less/Tree/Extend.php @@ -0,0 +1,80 @@ +selector = $selector; + $this->option = $option; + $this->index = $index; + + switch ( $option ) { + case "all": + $this->allowBefore = true; + $this->allowAfter = true; + break; + default: + $this->allowBefore = false; + $this->allowAfter = false; + break; + } + + // This must use a string (instead of int) so that array_merge() + // preserves keys on arrays that use IDs in their keys. + $this->object_id = 'id_' . $i++; + + $this->parent_ids = array( + $this->object_id => true + ); + } + + public function accept( $visitor ) { + $this->selector = $visitor->visitObj( $this->selector ); + } + + public function compile( $env ) { + Less_Parser::$has_extends = true; + $this->selector = $this->selector->compile( $env ); + return $this; + // return new Less_Tree_Extend( $this->selector->compile($env), $this->option, $this->index); + } + + public function findSelfSelectors( $selectors ) { + $selfElements = array(); + + for ( $i = 0, $selectors_len = count( $selectors ); $i < $selectors_len; $i++ ) { + $selectorElements = $selectors[$i]->elements; + // duplicate the logic in genCSS function inside the selector node. + // future TODO - move both logics into the selector joiner visitor + if ( $i && $selectorElements && $selectorElements[0]->combinator === "" ) { + $selectorElements[0]->combinator = ' '; + } + $selfElements = array_merge( $selfElements, $selectors[$i]->elements ); + } + + $this->selfSelectors = array( new Less_Tree_Selector( $selfElements ) ); + } + +} diff --git a/less.php/lib/Less/Tree/Import.php b/less.php/lib/Less/Tree/Import.php new file mode 100644 index 0000000..ffdac64 --- /dev/null +++ b/less.php/lib/Less/Tree/Import.php @@ -0,0 +1,291 @@ +options = $options; + $this->index = $index; + $this->path = $path; + $this->features = $features; + $this->currentFileInfo = $currentFileInfo; + + if ( is_array( $options ) ) { + $this->options += array( 'inline' => false ); + + if ( isset( $this->options['less'] ) || $this->options['inline'] ) { + $this->css = !isset( $this->options['less'] ) || !$this->options['less'] || $this->options['inline']; + } else { + $pathValue = $this->getPath(); + if ( $pathValue && preg_match( '/css([\?;].*)?$/', $pathValue ) ) { + $this->css = true; + } + } + } + } + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// + + public function accept( $visitor ) { + if ( $this->features ) { + $this->features = $visitor->visitObj( $this->features ); + } + $this->path = $visitor->visitObj( $this->path ); + + if ( !$this->options['inline'] && $this->root ) { + $this->root = $visitor->visit( $this->root ); + } + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( $this->css ) { + + $output->add( '@import ', $this->currentFileInfo, $this->index ); + + $this->path->genCSS( $output ); + if ( $this->features ) { + $output->add( ' ' ); + $this->features->genCSS( $output ); + } + $output->add( ';' ); + } + } + + public function toCSS() { + $features = $this->features ? ' ' . $this->features->toCSS() : ''; + + if ( $this->css ) { + return "@import " . $this->path->toCSS() . $features . ";\n"; + } else { + return ""; + } + } + + /** + * @return string + */ + public function getPath() { + if ( $this->path instanceof Less_Tree_Quoted ) { + $path = $this->path->value; + $path = ( isset( $this->css ) || preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ) ? $path : $path . '.less'; + } else if ( $this->path instanceof Less_Tree_URL ) { + $path = $this->path->value->value; + } else { + return null; + } + + // remove query string and fragment + return preg_replace( '/[\?#][^\?]*$/', '', $path ); + } + + public function compileForImport( $env ) { + return new Less_Tree_Import( $this->path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo ); + } + + public function compilePath( $env ) { + $path = $this->path->compile( $env ); + $rootpath = ''; + if ( $this->currentFileInfo && $this->currentFileInfo['rootpath'] ) { + $rootpath = $this->currentFileInfo['rootpath']; + } + + if ( !( $path instanceof Less_Tree_URL ) ) { + if ( $rootpath ) { + $pathValue = $path->value; + // Add the base path if the import is relative + if ( $pathValue && Less_Environment::isPathRelative( $pathValue ) ) { + $path->value = $this->currentFileInfo['uri_root'].$pathValue; + } + } + $path->value = Less_Environment::normalizePath( $path->value ); + } + + return $path; + } + + public function compile( $env ) { + $evald = $this->compileForImport( $env ); + + // get path & uri + $path_and_uri = null; + if ( is_callable( Less_Parser::$options['import_callback'] ) ) { + $path_and_uri = call_user_func( Less_Parser::$options['import_callback'], $evald ); + } + + if ( !$path_and_uri ) { + $path_and_uri = $evald->PathAndUri(); + } + + if ( $path_and_uri ) { + list( $full_path, $uri ) = $path_and_uri; + } else { + $full_path = $uri = $evald->getPath(); + } + + // import once + if ( $evald->skip( $full_path, $env ) ) { + return array(); + } + + if ( $this->options['inline'] ) { + // todo needs to reference css file not import + //$contents = new Less_Tree_Anonymous($this->root, 0, array('filename'=>$this->importedFilename), true ); + + Less_Parser::AddParsedFile( $full_path ); + $contents = new Less_Tree_Anonymous( file_get_contents( $full_path ), 0, array(), true ); + + if ( $this->features ) { + return new Less_Tree_Media( array( $contents ), $this->features->value ); + } + + return array( $contents ); + } + + // optional (need to be before "CSS" to support optional CSS imports. CSS should be checked only if empty($this->currentFileInfo)) + if ( isset( $this->options['optional'] ) && $this->options['optional'] && !file_exists( $full_path ) && ( !$evald->css || !empty( $this->currentFileInfo ) ) ) { + return array(); + } + + // css ? + if ( $evald->css ) { + $features = ( $evald->features ? $evald->features->compile( $env ) : null ); + return new Less_Tree_Import( $this->compilePath( $env ), $features, $this->options, $this->index ); + } + + return $this->ParseImport( $full_path, $uri, $env ); + } + + /** + * Using the import directories, get the full absolute path and uri of the import + * + * @param Less_Tree_Import $evald + */ + public function PathAndUri() { + $evald_path = $this->getPath(); + + if ( $evald_path ) { + + $import_dirs = array(); + + if ( Less_Environment::isPathRelative( $evald_path ) ) { + // if the path is relative, the file should be in the current directory + if ( $this->currentFileInfo ) { + $import_dirs[ $this->currentFileInfo['currentDirectory'] ] = $this->currentFileInfo['uri_root']; + } + + } else { + // otherwise, the file should be relative to the server root + if ( $this->currentFileInfo ) { + $import_dirs[ $this->currentFileInfo['entryPath'] ] = $this->currentFileInfo['entryUri']; + } + // if the user supplied entryPath isn't the actual root + $import_dirs[ $_SERVER['DOCUMENT_ROOT'] ] = ''; + + } + + // always look in user supplied import directories + $import_dirs = array_merge( $import_dirs, Less_Parser::$options['import_dirs'] ); + + foreach ( $import_dirs as $rootpath => $rooturi ) { + if ( is_callable( $rooturi ) ) { + list( $path, $uri ) = call_user_func( $rooturi, $evald_path ); + if ( is_string( $path ) ) { + $full_path = $path; + return array( $full_path, $uri ); + } + } elseif ( !empty( $rootpath ) ) { + + $path = rtrim( $rootpath, '/\\' ).'/'.ltrim( $evald_path, '/\\' ); + + if ( file_exists( $path ) ) { + $full_path = Less_Environment::normalizePath( $path ); + $uri = Less_Environment::normalizePath( dirname( $rooturi.$evald_path ) ); + return array( $full_path, $uri ); + } elseif ( file_exists( $path.'.less' ) ) { + $full_path = Less_Environment::normalizePath( $path.'.less' ); + $uri = Less_Environment::normalizePath( dirname( $rooturi.$evald_path.'.less' ) ); + return array( $full_path, $uri ); + } + } + } + } + } + + /** + * Parse the import url and return the rules + * + * @return Less_Tree_Media|array + */ + public function ParseImport( $full_path, $uri, $env ) { + $import_env = clone $env; + if ( ( isset( $this->options['reference'] ) && $this->options['reference'] ) || isset( $this->currentFileInfo['reference'] ) ) { + $import_env->currentFileInfo['reference'] = true; + } + + if ( ( isset( $this->options['multiple'] ) && $this->options['multiple'] ) ) { + $import_env->importMultiple = true; + } + + $parser = new Less_Parser( $import_env ); + $root = $parser->parseFile( $full_path, $uri, true ); + + $ruleset = new Less_Tree_Ruleset( array(), $root->rules ); + $ruleset->evalImports( $import_env ); + + return $this->features ? new Less_Tree_Media( $ruleset->rules, $this->features->value ) : $ruleset->rules; + } + + /** + * Should the import be skipped? + * + * @return boolean|null + */ + private function Skip( $path, $env ) { + $path = Less_Parser::AbsPath( $path, true ); + + if ( $path && Less_Parser::FileParsed( $path ) ) { + + if ( isset( $this->currentFileInfo['reference'] ) ) { + return true; + } + + return !isset( $this->options['multiple'] ) && !$env->importMultiple; + } + + } +} diff --git a/less.php/lib/Less/Tree/Javascript.php b/less.php/lib/Less/Tree/Javascript.php new file mode 100644 index 0000000..179781a --- /dev/null +++ b/less.php/lib/Less/Tree/Javascript.php @@ -0,0 +1,30 @@ +escaped = $escaped; + $this->expression = $string; + $this->index = $index; + } + + public function compile() { + return new Less_Tree_Anonymous( '/* Sorry, can not do JavaScript evaluation in PHP... :( */' ); + } + +} diff --git a/less.php/lib/Less/Tree/Keyword.php b/less.php/lib/Less/Tree/Keyword.php new file mode 100644 index 0000000..ed217cf --- /dev/null +++ b/less.php/lib/Less/Tree/Keyword.php @@ -0,0 +1,43 @@ +value = $value; + } + + public function compile() { + return $this; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( $this->value === '%' ) { + throw new Less_Exception_Compiler( "Invalid % without number" ); + } + + $output->add( $this->value ); + } + + public function compare( $other ) { + if ( $other instanceof Less_Tree_Keyword ) { + return $other->value === $this->value ? 0 : 1; + } else { + return -1; + } + } +} diff --git a/less.php/lib/Less/Tree/Media.php b/less.php/lib/Less/Tree/Media.php new file mode 100644 index 0000000..ba25a67 --- /dev/null +++ b/less.php/lib/Less/Tree/Media.php @@ -0,0 +1,173 @@ +index = $index; + $this->currentFileInfo = $currentFileInfo; + + $selectors = $this->emptySelectors(); + + $this->features = new Less_Tree_Value( $features ); + + $this->rules = array( new Less_Tree_Ruleset( $selectors, $value ) ); + $this->rules[0]->allowImports = true; + } + + public function accept( $visitor ) { + $this->features = $visitor->visitObj( $this->features ); + $this->rules = $visitor->visitArray( $this->rules ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( '@media ', $this->currentFileInfo, $this->index ); + $this->features->genCSS( $output ); + Less_Tree::outputRuleset( $output, $this->rules ); + + } + + public function compile( $env ) { + $media = new Less_Tree_Media( array(), array(), $this->index, $this->currentFileInfo ); + + $strictMathBypass = false; + if ( Less_Parser::$options['strictMath'] === false ) { + $strictMathBypass = true; + Less_Parser::$options['strictMath'] = true; + } + + $media->features = $this->features->compile( $env ); + + if ( $strictMathBypass ) { + Less_Parser::$options['strictMath'] = false; + } + + $env->mediaPath[] = $media; + $env->mediaBlocks[] = $media; + + array_unshift( $env->frames, $this->rules[0] ); + $media->rules = array( $this->rules[0]->compile( $env ) ); + array_shift( $env->frames ); + + array_pop( $env->mediaPath ); + + return !$env->mediaPath ? $media->compileTop( $env ) : $media->compileNested( $env ); + } + + public function variable( $name ) { + return $this->rules[0]->variable( $name ); + } + + public function find( $selector ) { + return $this->rules[0]->find( $selector, $this ); + } + + public function emptySelectors() { + $el = new Less_Tree_Element( '', '&', $this->index, $this->currentFileInfo ); + $sels = array( new Less_Tree_Selector( array( $el ), array(), null, $this->index, $this->currentFileInfo ) ); + $sels[0]->mediaEmpty = true; + return $sels; + } + + public function markReferenced() { + $this->rules[0]->markReferenced(); + $this->isReferenced = true; + Less_Tree::ReferencedArray( $this->rules[0]->rules ); + } + + // evaltop + public function compileTop( $env ) { + $result = $this; + + if ( count( $env->mediaBlocks ) > 1 ) { + $selectors = $this->emptySelectors(); + $result = new Less_Tree_Ruleset( $selectors, $env->mediaBlocks ); + $result->multiMedia = true; + } + + $env->mediaBlocks = array(); + $env->mediaPath = array(); + + return $result; + } + + public function compileNested( $env ) { + $path = array_merge( $env->mediaPath, array( $this ) ); + + // Extract the media-query conditions separated with `,` (OR). + foreach ( $path as $key => $p ) { + $value = $p->features instanceof Less_Tree_Value ? $p->features->value : $p->features; + $path[$key] = is_array( $value ) ? $value : array( $value ); + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + + $permuted = $this->permute( $path ); + $expressions = array(); + foreach ( $permuted as $path ) { + + for ( $i = 0, $len = count( $path ); $i < $len; $i++ ) { + $path[$i] = Less_Parser::is_method( $path[$i], 'toCSS' ) ? $path[$i] : new Less_Tree_Anonymous( $path[$i] ); + } + + for ( $i = count( $path ) - 1; $i > 0; $i-- ) { + array_splice( $path, $i, 0, array( new Less_Tree_Anonymous( 'and' ) ) ); + } + + $expressions[] = new Less_Tree_Expression( $path ); + } + $this->features = new Less_Tree_Value( $expressions ); + + // Fake a tree-node that doesn't output anything. + return new Less_Tree_Ruleset( array(), array() ); + } + + public function permute( $arr ) { + if ( !$arr ) + return array(); + + if ( count( $arr ) == 1 ) + return $arr[0]; + + $result = array(); + $rest = $this->permute( array_slice( $arr, 1 ) ); + foreach ( $rest as $r ) { + foreach ( $arr[0] as $a ) { + $result[] = array_merge( + is_array( $a ) ? $a : array( $a ), + is_array( $r ) ? $r : array( $r ) + ); + } + } + + return $result; + } + + public function bubbleSelectors( $selectors ) { + if ( !$selectors ) return; + + $this->rules = array( new Less_Tree_Ruleset( $selectors, array( $this->rules[0] ) ) ); + } + +} diff --git a/less.php/lib/Less/Tree/Mixin/Call.php b/less.php/lib/Less/Tree/Mixin/Call.php new file mode 100644 index 0000000..8e1eece --- /dev/null +++ b/less.php/lib/Less/Tree/Mixin/Call.php @@ -0,0 +1,193 @@ +selector = new Less_Tree_Selector( $elements ); + $this->arguments = $args; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + $this->important = $important; + } + + // function accept($visitor){ + // $this->selector = $visitor->visit($this->selector); + // $this->arguments = $visitor->visit($this->arguments); + //} + + public function compile( $env ) { + $rules = array(); + $match = false; + $isOneFound = false; + $candidates = array(); + $defaultUsed = false; + $conditionResult = array(); + + $args = array(); + foreach ( $this->arguments as $a ) { + $args[] = array( 'name' => $a['name'], 'value' => $a['value']->compile( $env ) ); + } + + foreach ( $env->frames as $frame ) { + + $mixins = $frame->find( $this->selector ); + + if ( !$mixins ) { + continue; + } + + $isOneFound = true; + $defNone = 0; + $defTrue = 1; + $defFalse = 2; + + // To make `default()` function independent of definition order we have two "subpasses" here. + // At first we evaluate each guard *twice* (with `default() == true` and `default() == false`), + // and build candidate list with corresponding flags. Then, when we know all possible matches, + // we make a final decision. + + $mixins_len = count( $mixins ); + for ( $m = 0; $m < $mixins_len; $m++ ) { + $mixin = $mixins[$m]; + + if ( $this->IsRecursive( $env, $mixin ) ) { + continue; + } + + if ( $mixin->matchArgs( $args, $env ) ) { + + $candidate = array( 'mixin' => $mixin, 'group' => $defNone ); + + if ( $mixin instanceof Less_Tree_Ruleset ) { + + for ( $f = 0; $f < 2; $f++ ) { + Less_Tree_DefaultFunc::value( $f ); + $conditionResult[$f] = $mixin->matchCondition( $args, $env ); + } + if ( $conditionResult[0] || $conditionResult[1] ) { + if ( $conditionResult[0] != $conditionResult[1] ) { + $candidate['group'] = $conditionResult[1] ? $defTrue : $defFalse; + } + + $candidates[] = $candidate; + } + } else { + $candidates[] = $candidate; + } + + $match = true; + } + } + + Less_Tree_DefaultFunc::reset(); + + $count = array( 0, 0, 0 ); + for ( $m = 0; $m < count( $candidates ); $m++ ) { + $count[ $candidates[$m]['group'] ]++; + } + + if ( $count[$defNone] > 0 ) { + $defaultResult = $defFalse; + } else { + $defaultResult = $defTrue; + if ( ( $count[$defTrue] + $count[$defFalse] ) > 1 ) { + throw new Exception( 'Ambiguous use of `default()` found when matching for `' . $this->format( $args ) . '`' ); + } + } + + $candidates_length = count( $candidates ); + $length_1 = ( $candidates_length == 1 ); + + for ( $m = 0; $m < $candidates_length; $m++ ) { + $candidate = $candidates[$m]['group']; + if ( ( $candidate === $defNone ) || ( $candidate === $defaultResult ) ) { + try{ + $mixin = $candidates[$m]['mixin']; + if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) { + $mixin = new Less_Tree_Mixin_Definition( '', array(), $mixin->rules, null, false ); + $mixin->originalRuleset = $mixins[$m]->originalRuleset; + } + $rules = array_merge( $rules, $mixin->evalCall( $env, $args, $this->important )->rules ); + } catch ( Exception $e ) { + // throw new Less_Exception_Compiler($e->getMessage(), $e->index, null, $this->currentFileInfo['filename']); + throw new Less_Exception_Compiler( $e->getMessage(), null, null, $this->currentFileInfo ); + } + } + } + + if ( $match ) { + if ( !$this->currentFileInfo || !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] ) { + Less_Tree::ReferencedArray( $rules ); + } + + return $rules; + } + } + + if ( $isOneFound ) { + throw new Less_Exception_Compiler( 'No matching definition was found for `'.$this->Format( $args ).'`', null, $this->index, $this->currentFileInfo ); + + } else { + throw new Less_Exception_Compiler( trim( $this->selector->toCSS() ) . " is undefined in ".$this->currentFileInfo['filename'], null, $this->index ); + } + + } + + /** + * Format the args for use in exception messages + * + */ + private function Format( $args ) { + $message = array(); + if ( $args ) { + foreach ( $args as $a ) { + $argValue = ''; + if ( $a['name'] ) { + $argValue .= $a['name'] . ':'; + } + if ( is_object( $a['value'] ) ) { + $argValue .= $a['value']->toCSS(); + } else { + $argValue .= '???'; + } + $message[] = $argValue; + } + } + return implode( ', ', $message ); + } + + /** + * Are we in a recursive mixin call? + * + * @return bool + */ + private function IsRecursive( $env, $mixin ) { + foreach ( $env->frames as $recur_frame ) { + if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) { + + if ( $mixin === $recur_frame ) { + return true; + } + + if ( isset( $recur_frame->originalRuleset ) && $mixin->ruleset_id === $recur_frame->originalRuleset ) { + return true; + } + } + } + + return false; + } + +} diff --git a/less.php/lib/Less/Tree/Mixin/Definition.php b/less.php/lib/Less/Tree/Mixin/Definition.php new file mode 100644 index 0000000..f9f2eb4 --- /dev/null +++ b/less.php/lib/Less/Tree/Mixin/Definition.php @@ -0,0 +1,233 @@ +name = $name; + $this->selectors = array( new Less_Tree_Selector( array( new Less_Tree_Element( null, $name ) ) ) ); + + $this->params = $params; + $this->condition = $condition; + $this->variadic = $variadic; + $this->rules = $rules; + + if ( $params ) { + $this->arity = count( $params ); + foreach ( $params as $p ) { + if ( !isset( $p['name'] ) || ( $p['name'] && !isset( $p['value'] ) ) ) { + $this->required++; + } + } + } + + $this->frames = $frames; + $this->SetRulesetIndex(); + } + + // function accept( $visitor ){ + // $this->params = $visitor->visit($this->params); + // $this->rules = $visitor->visit($this->rules); + // $this->condition = $visitor->visit($this->condition); + //} + + public function toCSS() { + return ''; + } + + // less.js : /lib/less/tree/mixin.js : tree.mixin.Definition.evalParams + public function compileParams( $env, $mixinFrames, $args = array(), &$evaldArguments = array() ) { + $frame = new Less_Tree_Ruleset( null, array() ); + $params = $this->params; + $mixinEnv = null; + $argsLength = 0; + + if ( $args ) { + $argsLength = count( $args ); + for ( $i = 0; $i < $argsLength; $i++ ) { + $arg = $args[$i]; + + if ( $arg && $arg['name'] ) { + $isNamedFound = false; + + foreach ( $params as $j => $param ) { + if ( !isset( $evaldArguments[$j] ) && $arg['name'] === $params[$j]['name'] ) { + $evaldArguments[$j] = $arg['value']->compile( $env ); + array_unshift( $frame->rules, new Less_Tree_Rule( $arg['name'], $arg['value']->compile( $env ) ) ); + $isNamedFound = true; + break; + } + } + if ( $isNamedFound ) { + array_splice( $args, $i, 1 ); + $i--; + $argsLength--; + continue; + } else { + throw new Less_Exception_Compiler( "Named argument for " . $this->name .' '.$args[$i]['name'] . ' not found' ); + } + } + } + } + + $argIndex = 0; + foreach ( $params as $i => $param ) { + + if ( isset( $evaldArguments[$i] ) ) { continue; + } + + $arg = null; + if ( isset( $args[$argIndex] ) ) { + $arg = $args[$argIndex]; + } + + if ( isset( $param['name'] ) && $param['name'] ) { + + if ( isset( $param['variadic'] ) ) { + $varargs = array(); + for ( $j = $argIndex; $j < $argsLength; $j++ ) { + $varargs[] = $args[$j]['value']->compile( $env ); + } + $expression = new Less_Tree_Expression( $varargs ); + array_unshift( $frame->rules, new Less_Tree_Rule( $param['name'], $expression->compile( $env ) ) ); + } else { + $val = ( $arg && $arg['value'] ) ? $arg['value'] : false; + + if ( $val ) { + $val = $val->compile( $env ); + } else if ( isset( $param['value'] ) ) { + + if ( !$mixinEnv ) { + $mixinEnv = new Less_Environment(); + $mixinEnv->frames = array_merge( array( $frame ), $mixinFrames ); + } + + $val = $param['value']->compile( $mixinEnv ); + $frame->resetCache(); + } else { + throw new Less_Exception_Compiler( "Wrong number of arguments for " . $this->name . " (" . $argsLength . ' for ' . $this->arity . ")" ); + } + + array_unshift( $frame->rules, new Less_Tree_Rule( $param['name'], $val ) ); + $evaldArguments[$i] = $val; + } + } + + if ( isset( $param['variadic'] ) && $args ) { + for ( $j = $argIndex; $j < $argsLength; $j++ ) { + $evaldArguments[$j] = $args[$j]['value']->compile( $env ); + } + } + $argIndex++; + } + + ksort( $evaldArguments ); + $evaldArguments = array_values( $evaldArguments ); + + return $frame; + } + + public function compile( $env ) { + if ( $this->frames ) { + return new Less_Tree_Mixin_Definition( $this->name, $this->params, $this->rules, $this->condition, $this->variadic, $this->frames ); + } + return new Less_Tree_Mixin_Definition( $this->name, $this->params, $this->rules, $this->condition, $this->variadic, $env->frames ); + } + + public function evalCall( $env, $args = NULL, $important = NULL ) { + Less_Environment::$mixin_stack++; + + $_arguments = array(); + + if ( $this->frames ) { + $mixinFrames = array_merge( $this->frames, $env->frames ); + } else { + $mixinFrames = $env->frames; + } + + $frame = $this->compileParams( $env, $mixinFrames, $args, $_arguments ); + + $ex = new Less_Tree_Expression( $_arguments ); + array_unshift( $frame->rules, new Less_Tree_Rule( '@arguments', $ex->compile( $env ) ) ); + + $ruleset = new Less_Tree_Ruleset( null, $this->rules ); + $ruleset->originalRuleset = $this->ruleset_id; + + $ruleSetEnv = new Less_Environment(); + $ruleSetEnv->frames = array_merge( array( $this, $frame ), $mixinFrames ); + $ruleset = $ruleset->compile( $ruleSetEnv ); + + if ( $important ) { + $ruleset = $ruleset->makeImportant(); + } + + Less_Environment::$mixin_stack--; + + return $ruleset; + } + + public function matchCondition( $args, $env ) { + if ( !$this->condition ) { + return true; + } + + // set array to prevent error on array_merge + if ( !is_array( $this->frames ) ) { + $this->frames = array(); + } + + $frame = $this->compileParams( $env, array_merge( $this->frames, $env->frames ), $args ); + + $compile_env = new Less_Environment(); + $compile_env->frames = array_merge( + array( $frame ), // the parameter variables + $this->frames, // the parent namespace/mixin frames + $env->frames // the current environment frames + ); + + $compile_env->functions = $env->functions; + + return (bool)$this->condition->compile( $compile_env ); + } + + public function matchArgs( $args, $env = NULL ) { + $argsLength = count( $args ); + + if ( !$this->variadic ) { + if ( $argsLength < $this->required ) { + return false; + } + if ( $argsLength > count( $this->params ) ) { + return false; + } + } else { + if ( $argsLength < ( $this->required - 1 ) ) { + return false; + } + } + + $len = min( $argsLength, $this->arity ); + + for ( $i = 0; $i < $len; $i++ ) { + if ( !isset( $this->params[$i]['name'] ) && !isset( $this->params[$i]['variadic'] ) ) { + if ( $args[$i]['value']->compile( $env )->toCSS() != $this->params[$i]['value']->compile( $env )->toCSS() ) { + return false; + } + } + } + + return true; + } + +} diff --git a/less.php/lib/Less/Tree/NameValue.php b/less.php/lib/Less/Tree/NameValue.php new file mode 100644 index 0000000..4f9aa97 --- /dev/null +++ b/less.php/lib/Less/Tree/NameValue.php @@ -0,0 +1,49 @@ + color:#FF0000; + * + * @package Less + * @subpackage tree + */ +class Less_Tree_NameValue extends Less_Tree { + + public $name; + public $value; + public $index; + public $currentFileInfo; + public $type = 'NameValue'; + public $important = ''; + + public function __construct( $name, $value = null, $index = null, $currentFileInfo = null ) { + $this->name = $name; + $this->value = $value; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function genCSS( $output ) { + $output->add( + $this->name + . Less_Environment::$_outputMap[': '] + . $this->value + . $this->important + . ( ( ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), + $this->currentFileInfo, $this->index ); + } + + public function compile( $env ) { + return $this; + } + + public function makeImportant() { + $new = new Less_Tree_NameValue( $this->name, $this->value, $this->index, $this->currentFileInfo ); + $new->important = ' !important'; + return $new; + } + +} diff --git a/less.php/lib/Less/Tree/Negative.php b/less.php/lib/Less/Tree/Negative.php new file mode 100644 index 0000000..f0f36c8 --- /dev/null +++ b/less.php/lib/Less/Tree/Negative.php @@ -0,0 +1,37 @@ +value = $node; + } + + // function accept($visitor) { + // $this->value = $visitor->visit($this->value); + //} + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( '-' ); + $this->value->genCSS( $output ); + } + + public function compile( $env ) { + if ( Less_Environment::isMathOn() ) { + $ret = new Less_Tree_Operation( '*', array( new Less_Tree_Dimension( -1 ), $this->value ) ); + return $ret->compile( $env ); + } + return new Less_Tree_Negative( $this->value->compile( $env ) ); + } +} diff --git a/less.php/lib/Less/Tree/Operation.php b/less.php/lib/Less/Tree/Operation.php new file mode 100644 index 0000000..d4eb9ac --- /dev/null +++ b/less.php/lib/Less/Tree/Operation.php @@ -0,0 +1,68 @@ +op = trim( $op ); + $this->operands = $operands; + $this->isSpaced = $isSpaced; + } + + public function accept( $visitor ) { + $this->operands = $visitor->visitArray( $this->operands ); + } + + public function compile( $env ) { + $a = $this->operands[0]->compile( $env ); + $b = $this->operands[1]->compile( $env ); + + if ( Less_Environment::isMathOn() ) { + + if ( $a instanceof Less_Tree_Dimension && $b instanceof Less_Tree_Color ) { + $a = $a->toColor(); + + } elseif ( $b instanceof Less_Tree_Dimension && $a instanceof Less_Tree_Color ) { + $b = $b->toColor(); + + } + + if ( !method_exists( $a, 'operate' ) ) { + throw new Less_Exception_Compiler( "Operation on an invalid type" ); + } + + return $a->operate( $this->op, $b ); + } + + return new Less_Tree_Operation( $this->op, array( $a, $b ), $this->isSpaced ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $this->operands[0]->genCSS( $output ); + if ( $this->isSpaced ) { + $output->add( " " ); + } + $output->add( $this->op ); + if ( $this->isSpaced ) { + $output->add( ' ' ); + } + $this->operands[1]->genCSS( $output ); + } + +} diff --git a/less.php/lib/Less/Tree/Paren.php b/less.php/lib/Less/Tree/Paren.php new file mode 100644 index 0000000..77ee48c --- /dev/null +++ b/less.php/lib/Less/Tree/Paren.php @@ -0,0 +1,35 @@ +value = $value; + } + + public function accept( $visitor ) { + $this->value = $visitor->visitObj( $this->value ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( '(' ); + $this->value->genCSS( $output ); + $output->add( ')' ); + } + + public function compile( $env ) { + return new Less_Tree_Paren( $this->value->compile( $env ) ); + } + +} diff --git a/less.php/lib/Less/Tree/Quoted.php b/less.php/lib/Less/Tree/Quoted.php new file mode 100644 index 0000000..d01598b --- /dev/null +++ b/less.php/lib/Less/Tree/Quoted.php @@ -0,0 +1,79 @@ +escaped = $escaped; + $this->value = $content; + if ( $str ) { + $this->quote = $str[0]; + } + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( !$this->escaped ) { + $output->add( $this->quote, $this->currentFileInfo, $this->index ); + } + $output->add( $this->value ); + if ( !$this->escaped ) { + $output->add( $this->quote ); + } + } + + public function compile( $env ) { + $value = $this->value; + if ( preg_match_all( '/`([^`]+)`/', $this->value, $matches ) ) { + foreach ( $matches as $i => $match ) { + $js = new Less_Tree_JavaScript( $matches[1], $this->index, true ); + $js = $js->compile()->value; + $value = str_replace( $matches[0][$i], $js, $value ); + } + } + + if ( preg_match_all( '/@\{([\w-]+)\}/', $value, $matches ) ) { + foreach ( $matches[1] as $i => $match ) { + $v = new Less_Tree_Variable( '@' . $match, $this->index, $this->currentFileInfo ); + $v = $v->compile( $env ); + $v = ( $v instanceof Less_Tree_Quoted ) ? $v->value : $v->toCSS(); + $value = str_replace( $matches[0][$i], $v, $value ); + } + } + + return new Less_Tree_Quoted( $this->quote . $value . $this->quote, $value, $this->escaped, $this->index, $this->currentFileInfo ); + } + + public function compare( $x ) { + if ( !Less_Parser::is_method( $x, 'toCSS' ) ) { + return -1; + } + + $left = $this->toCSS(); + $right = $x->toCSS(); + + if ( $left === $right ) { + return 0; + } + + return $left < $right ? -1 : 1; + } +} diff --git a/less.php/lib/Less/Tree/Rule.php b/less.php/lib/Less/Tree/Rule.php new file mode 100644 index 0000000..5f393eb --- /dev/null +++ b/less.php/lib/Less/Tree/Rule.php @@ -0,0 +1,112 @@ +name = $name; + $this->value = ( $value instanceof Less_Tree_Value || $value instanceof Less_Tree_Ruleset ) ? $value : new Less_Tree_Value( array( $value ) ); + $this->important = $important ? ' ' . trim( $important ) : ''; + $this->merge = $merge; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + $this->inline = $inline; + $this->variable = ( is_string( $name ) && $name[0] === '@' ); + } + + public function accept( $visitor ) { + $this->value = $visitor->visitObj( $this->value ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->name . Less_Environment::$_outputMap[': '], $this->currentFileInfo, $this->index ); + try{ + $this->value->genCSS( $output ); + + }catch ( Less_Exception_Parser $e ) { + $e->index = $this->index; + $e->currentFile = $this->currentFileInfo; + throw $e; + } + $output->add( $this->important . ( ( $this->inline || ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), $this->currentFileInfo, $this->index ); + } + + public function compile( $env ) { + $name = $this->name; + if ( is_array( $name ) ) { + // expand 'primitive' name directly to get + // things faster (~10% for benchmark.less): + if ( count( $name ) === 1 && $name[0] instanceof Less_Tree_Keyword ) { + $name = $name[0]->value; + } else { + $name = $this->CompileName( $env, $name ); + } + } + + $strictMathBypass = Less_Parser::$options['strictMath']; + if ( $name === "font" && !Less_Parser::$options['strictMath'] ) { + Less_Parser::$options['strictMath'] = true; + } + + try { + $evaldValue = $this->value->compile( $env ); + + if ( !$this->variable && $evaldValue->type === "DetachedRuleset" ) { + throw new Less_Exception_Compiler( "Rulesets cannot be evaluated on a property.", null, $this->index, $this->currentFileInfo ); + } + + if ( Less_Environment::$mixin_stack ) { + $return = new Less_Tree_Rule( $name, $evaldValue, $this->important, $this->merge, $this->index, $this->currentFileInfo, $this->inline ); + } else { + $this->name = $name; + $this->value = $evaldValue; + $return = $this; + } + + }catch ( Less_Exception_Parser $e ) { + if ( !is_numeric( $e->index ) ) { + $e->index = $this->index; + $e->currentFile = $this->currentFileInfo; + } + throw $e; + } + + Less_Parser::$options['strictMath'] = $strictMathBypass; + + return $return; + } + + public function CompileName( $env, $name ) { + $output = new Less_Output(); + foreach ( $name as $n ) { + $n->compile( $env )->genCSS( $output ); + } + return $output->toString(); + } + + public function makeImportant() { + return new Less_Tree_Rule( $this->name, $this->value, '!important', $this->merge, $this->index, $this->currentFileInfo, $this->inline ); + } + +} diff --git a/less.php/lib/Less/Tree/Ruleset.php b/less.php/lib/Less/Tree/Ruleset.php new file mode 100644 index 0000000..9bc8243 --- /dev/null +++ b/less.php/lib/Less/Tree/Ruleset.php @@ -0,0 +1,621 @@ +ruleset_id = Less_Parser::$next_id++; + $this->originalRuleset = $this->ruleset_id; + + if ( $this->selectors ) { + foreach ( $this->selectors as $sel ) { + if ( $sel->_oelements ) { + $this->first_oelements[$sel->_oelements[0]] = true; + } + } + } + } + + public function __construct( $selectors, $rules, $strictImports = null ) { + $this->selectors = $selectors; + $this->rules = $rules; + $this->lookups = array(); + $this->strictImports = $strictImports; + $this->SetRulesetIndex(); + } + + public function accept( $visitor ) { + if ( $this->paths ) { + $paths_len = count( $this->paths ); + for ( $i = 0,$paths_len; $i < $paths_len; $i++ ) { + $this->paths[$i] = $visitor->visitArray( $this->paths[$i] ); + } + } elseif ( $this->selectors ) { + $this->selectors = $visitor->visitArray( $this->selectors ); + } + + if ( $this->rules ) { + $this->rules = $visitor->visitArray( $this->rules ); + } + } + + public function compile( $env ) { + $ruleset = $this->PrepareRuleset( $env ); + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + $rsRuleCnt = count( $ruleset->rules ); + for ( $i = 0; $i < $rsRuleCnt; $i++ ) { + if ( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) { + $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env ); + } + } + + $mediaBlockCount = 0; + if ( $env instanceof Less_Environment ) { + $mediaBlockCount = count( $env->mediaBlocks ); + } + + // Evaluate mixin calls. + $this->EvalMixinCalls( $ruleset, $env, $rsRuleCnt ); + + // Evaluate everything else + for ( $i = 0; $i < $rsRuleCnt; $i++ ) { + if ( !( $ruleset->rules[$i] instanceof Less_Tree_Mixin_Definition || $ruleset->rules[$i] instanceof Less_Tree_DetachedRuleset ) ) { + $ruleset->rules[$i] = $ruleset->rules[$i]->compile( $env ); + } + } + + // Evaluate everything else + for ( $i = 0; $i < $rsRuleCnt; $i++ ) { + $rule = $ruleset->rules[$i]; + + // for rulesets, check if it is a css guard and can be removed + if ( $rule instanceof Less_Tree_Ruleset && $rule->selectors && count( $rule->selectors ) === 1 ) { + + // check if it can be folded in (e.g. & where) + if ( $rule->selectors[0]->isJustParentSelector() ) { + array_splice( $ruleset->rules, $i--, 1 ); + $rsRuleCnt--; + + for ( $j = 0; $j < count( $rule->rules ); $j++ ) { + $subRule = $rule->rules[$j]; + if ( !( $subRule instanceof Less_Tree_Rule ) || !$subRule->variable ) { + array_splice( $ruleset->rules, ++$i, 0, array( $subRule ) ); + $rsRuleCnt++; + } + } + + } + } + } + + // Pop the stack + $env->shiftFrame(); + + if ( $mediaBlockCount ) { + $len = count( $env->mediaBlocks ); + for ( $i = $mediaBlockCount; $i < $len; $i++ ) { + $env->mediaBlocks[$i]->bubbleSelectors( $ruleset->selectors ); + } + } + + return $ruleset; + } + + /** + * Compile Less_Tree_Mixin_Call objects + * + * @param Less_Tree_Ruleset $ruleset + * @param integer $rsRuleCnt + */ + private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) { + for ( $i = 0; $i < $rsRuleCnt; $i++ ) { + $rule = $ruleset->rules[$i]; + + if ( $rule instanceof Less_Tree_Mixin_Call ) { + $rule = $rule->compile( $env ); + + $temp = array(); + foreach ( $rule as $r ) { + if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) { + // do not pollute the scope if the variable is + // already there. consider returning false here + // but we need a way to "return" variable from mixins + if ( !$ruleset->variable( $r->name ) ) { + $temp[] = $r; + } + } else { + $temp[] = $r; + } + } + $temp_count = count( $temp ) - 1; + array_splice( $ruleset->rules, $i, 1, $temp ); + $rsRuleCnt += $temp_count; + $i += $temp_count; + $ruleset->resetCache(); + + } elseif ( $rule instanceof Less_Tree_RulesetCall ) { + + $rule = $rule->compile( $env ); + $rules = array(); + foreach ( $rule->rules as $r ) { + if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) { + continue; + } + $rules[] = $r; + } + + array_splice( $ruleset->rules, $i, 1, $rules ); + $temp_count = count( $rules ); + $rsRuleCnt += $temp_count - 1; + $i += $temp_count - 1; + $ruleset->resetCache(); + } + + } + } + + /** + * Compile the selectors and create a new ruleset object for the compile() method + * + */ + private function PrepareRuleset( $env ) { + $hasOnePassingSelector = false; + $selectors = array(); + if ( $this->selectors ) { + Less_Tree_DefaultFunc::error( "it is currently only allowed in parametric mixin guards," ); + + foreach ( $this->selectors as $s ) { + $selector = $s->compile( $env ); + $selectors[] = $selector; + if ( $selector->evaldCondition ) { + $hasOnePassingSelector = true; + } + } + + Less_Tree_DefaultFunc::reset(); + } else { + $hasOnePassingSelector = true; + } + + if ( $this->rules && $hasOnePassingSelector ) { + $rules = $this->rules; + } else { + $rules = array(); + } + + $ruleset = new Less_Tree_Ruleset( $selectors, $rules, $this->strictImports ); + + $ruleset->originalRuleset = $this->ruleset_id; + + $ruleset->root = $this->root; + $ruleset->firstRoot = $this->firstRoot; + $ruleset->allowImports = $this->allowImports; + + // push the current ruleset to the frames stack + $env->unshiftFrame( $ruleset ); + + // Evaluate imports + if ( $ruleset->root || $ruleset->allowImports || !$ruleset->strictImports ) { + $ruleset->evalImports( $env ); + } + + return $ruleset; + } + + function evalImports( $env ) { + $rules_len = count( $this->rules ); + for ( $i = 0; $i < $rules_len; $i++ ) { + $rule = $this->rules[$i]; + + if ( $rule instanceof Less_Tree_Import ) { + $rules = $rule->compile( $env ); + if ( is_array( $rules ) ) { + array_splice( $this->rules, $i, 1, $rules ); + $temp_count = count( $rules ) - 1; + $i += $temp_count; + $rules_len += $temp_count; + } else { + array_splice( $this->rules, $i, 1, array( $rules ) ); + } + + $this->resetCache(); + } + } + } + + function makeImportant() { + $important_rules = array(); + foreach ( $this->rules as $rule ) { + if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_Ruleset || $rule instanceof Less_Tree_NameValue ) { + $important_rules[] = $rule->makeImportant(); + } else { + $important_rules[] = $rule; + } + } + + return new Less_Tree_Ruleset( $this->selectors, $important_rules, $this->strictImports ); + } + + public function matchArgs( $args ) { + return !$args; + } + + // lets you call a css selector with a guard + public function matchCondition( $args, $env ) { + $lastSelector = end( $this->selectors ); + + if ( !$lastSelector->evaldCondition ) { + return false; + } + if ( $lastSelector->condition && !$lastSelector->condition->compile( $env->copyEvalEnv( $env->frames ) ) ) { + return false; + } + return true; + } + + function resetCache() { + $this->_rulesets = null; + $this->_variables = null; + $this->lookups = array(); + } + + public function variables() { + $this->_variables = array(); + foreach ( $this->rules as $r ) { + if ( $r instanceof Less_Tree_Rule && $r->variable === true ) { + $this->_variables[$r->name] = $r; + } + } + } + + public function variable( $name ) { + if ( is_null( $this->_variables ) ) { + $this->variables(); + } + return isset( $this->_variables[$name] ) ? $this->_variables[$name] : null; + } + + public function find( $selector, $self = null ) { + $key = implode( ' ', $selector->_oelements ); + + if ( !isset( $this->lookups[$key] ) ) { + + if ( !$self ) { + $self = $this->ruleset_id; + } + + $this->lookups[$key] = array(); + + $first_oelement = $selector->_oelements[0]; + + foreach ( $this->rules as $rule ) { + if ( $rule instanceof Less_Tree_Ruleset && $rule->ruleset_id != $self ) { + + if ( isset( $rule->first_oelements[$first_oelement] ) ) { + + foreach ( $rule->selectors as $ruleSelector ) { + $match = $selector->match( $ruleSelector ); + if ( $match ) { + if ( $selector->elements_len > $match ) { + $this->lookups[$key] = array_merge( $this->lookups[$key], $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self ) ); + } else { + $this->lookups[$key][] = $rule; + } + break; + } + } + } + } + } + } + + return $this->lookups[$key]; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( !$this->root ) { + Less_Environment::$tabLevel++; + } + + $tabRuleStr = $tabSetStr = ''; + if ( !Less_Parser::$options['compress'] ) { + if ( Less_Environment::$tabLevel ) { + $tabRuleStr = "\n".str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel ); + $tabSetStr = "\n".str_repeat( Less_Parser::$options['indentation'], Less_Environment::$tabLevel - 1 ); + } else { + $tabSetStr = $tabRuleStr = "\n"; + } + } + + $ruleNodes = array(); + $rulesetNodes = array(); + foreach ( $this->rules as $rule ) { + + $class = get_class( $rule ); + if ( ( $class === 'Less_Tree_Media' ) || ( $class === 'Less_Tree_Directive' ) || ( $this->root && $class === 'Less_Tree_Comment' ) || ( $class === 'Less_Tree_Ruleset' && $rule->rules ) ) { + $rulesetNodes[] = $rule; + } else { + $ruleNodes[] = $rule; + } + } + + // If this is the root node, we don't render + // a selector, or {}. + if ( !$this->root ) { + + /* + debugInfo = tree.debugInfo(env, this, tabSetStr); + + if (debugInfo) { + output.add(debugInfo); + output.add(tabSetStr); + } + */ + + $paths_len = count( $this->paths ); + for ( $i = 0; $i < $paths_len; $i++ ) { + $path = $this->paths[$i]; + $firstSelector = true; + + foreach ( $path as $p ) { + $p->genCSS( $output, $firstSelector ); + $firstSelector = false; + } + + if ( $i + 1 < $paths_len ) { + $output->add( ',' . $tabSetStr ); + } + } + + $output->add( ( Less_Parser::$options['compress'] ? '{' : " {" ) . $tabRuleStr ); + } + + // Compile rules and rulesets + $ruleNodes_len = count( $ruleNodes ); + $rulesetNodes_len = count( $rulesetNodes ); + for ( $i = 0; $i < $ruleNodes_len; $i++ ) { + $rule = $ruleNodes[$i]; + + // @page{ directive ends up with root elements inside it, a mix of rules and rulesets + // In this instance we do not know whether it is the last property + if ( $i + 1 === $ruleNodes_len && ( !$this->root || $rulesetNodes_len === 0 || $this->firstRoot ) ) { + Less_Environment::$lastRule = true; + } + + $rule->genCSS( $output ); + + if ( !Less_Environment::$lastRule ) { + $output->add( $tabRuleStr ); + } else { + Less_Environment::$lastRule = false; + } + } + + if ( !$this->root ) { + $output->add( $tabSetStr . '}' ); + Less_Environment::$tabLevel--; + } + + $firstRuleset = true; + $space = ( $this->root ? $tabRuleStr : $tabSetStr ); + for ( $i = 0; $i < $rulesetNodes_len; $i++ ) { + + if ( $ruleNodes_len && $firstRuleset ) { + $output->add( $space ); + } elseif ( !$firstRuleset ) { + $output->add( $space ); + } + $firstRuleset = false; + $rulesetNodes[$i]->genCSS( $output ); + } + + if ( !Less_Parser::$options['compress'] && $this->firstRoot ) { + $output->add( "\n" ); + } + + } + + function markReferenced() { + if ( !$this->selectors ) { + return; + } + foreach ( $this->selectors as $selector ) { + $selector->markReferenced(); + } + } + + public function joinSelectors( $context, $selectors ) { + $paths = array(); + if ( is_array( $selectors ) ) { + foreach ( $selectors as $selector ) { + $this->joinSelector( $paths, $context, $selector ); + } + } + return $paths; + } + + public function joinSelector( &$paths, $context, $selector ) { + $hasParentSelector = false; + + foreach ( $selector->elements as $el ) { + if ( $el->value === '&' ) { + $hasParentSelector = true; + } + } + + if ( !$hasParentSelector ) { + if ( $context ) { + foreach ( $context as $context_el ) { + $paths[] = array_merge( $context_el, array( $selector ) ); + } + } else { + $paths[] = array( $selector ); + } + return; + } + + // The paths are [[Selector]] + // The first list is a list of comma separated selectors + // The inner list is a list of inheritance separated selectors + // e.g. + // .a, .b { + // .c { + // } + // } + // == [[.a] [.c]] [[.b] [.c]] + // + + // the elements from the current selector so far + $currentElements = array(); + // the current list of new selectors to add to the path. + // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors + // by the parents + $newSelectors = array( array() ); + + foreach ( $selector->elements as $el ) { + + // non parent reference elements just get added + if ( $el->value !== '&' ) { + $currentElements[] = $el; + } else { + // the new list of selectors to add + $selectorsMultiplied = array(); + + // merge the current list of non parent selector elements + // on to the current list of selectors to add + if ( $currentElements ) { + $this->mergeElementsOnToSelectors( $currentElements, $newSelectors ); + } + + // loop through our current selectors + foreach ( $newSelectors as $sel ) { + + // if we don't have any parent paths, the & might be in a mixin so that it can be used + // whether there are parents or not + if ( !$context ) { + // the combinator used on el should now be applied to the next element instead so that + // it is not lost + if ( $sel ) { + $sel[0]->elements = array_slice( $sel[0]->elements, 0 ); + $sel[0]->elements[] = new Less_Tree_Element( $el->combinator, '', $el->index, $el->currentFileInfo ); + } + $selectorsMultiplied[] = $sel; + } else { + + // and the parent selectors + foreach ( $context as $parentSel ) { + // We need to put the current selectors + // then join the last selector's elements on to the parents selectors + + // our new selector path + $newSelectorPath = array(); + // selectors from the parent after the join + $afterParentJoin = array(); + $newJoinedSelectorEmpty = true; + + // construct the joined selector - if & is the first thing this will be empty, + // if not newJoinedSelector will be the last set of elements in the selector + if ( $sel ) { + $newSelectorPath = $sel; + $lastSelector = array_pop( $newSelectorPath ); + $newJoinedSelector = $selector->createDerived( array_slice( $lastSelector->elements, 0 ) ); + $newJoinedSelectorEmpty = false; + } else { + $newJoinedSelector = $selector->createDerived( array() ); + } + + // put together the parent selectors after the join + if ( count( $parentSel ) > 1 ) { + $afterParentJoin = array_merge( $afterParentJoin, array_slice( $parentSel, 1 ) ); + } + + if ( $parentSel ) { + $newJoinedSelectorEmpty = false; + + // join the elements so far with the first part of the parent + $newJoinedSelector->elements[] = new Less_Tree_Element( $el->combinator, $parentSel[0]->elements[0]->value, $el->index, $el->currentFileInfo ); + + $newJoinedSelector->elements = array_merge( $newJoinedSelector->elements, array_slice( $parentSel[0]->elements, 1 ) ); + } + + if ( !$newJoinedSelectorEmpty ) { + // now add the joined selector + $newSelectorPath[] = $newJoinedSelector; + } + + // and the rest of the parent + $newSelectorPath = array_merge( $newSelectorPath, $afterParentJoin ); + + // add that to our new set of selectors + $selectorsMultiplied[] = $newSelectorPath; + } + } + } + + // our new selectors has been multiplied, so reset the state + $newSelectors = $selectorsMultiplied; + $currentElements = array(); + } + } + + // if we have any elements left over (e.g. .a& .b == .b) + // add them on to all the current selectors + if ( $currentElements ) { + $this->mergeElementsOnToSelectors( $currentElements, $newSelectors ); + } + foreach ( $newSelectors as $new_sel ) { + if ( $new_sel ) { + $paths[] = $new_sel; + } + } + } + + function mergeElementsOnToSelectors( $elements, &$selectors ) { + if ( !$selectors ) { + $selectors[] = array( new Less_Tree_Selector( $elements ) ); + return; + } + + foreach ( $selectors as &$sel ) { + + // if the previous thing in sel is a parent this needs to join on to it + if ( $sel ) { + $last = count( $sel ) - 1; + $sel[$last] = $sel[$last]->createDerived( array_merge( $sel[$last]->elements, $elements ) ); + } else { + $sel[] = new Less_Tree_Selector( $elements ); + } + } + } +} diff --git a/less.php/lib/Less/Tree/RulesetCall.php b/less.php/lib/Less/Tree/RulesetCall.php new file mode 100644 index 0000000..079ff4f --- /dev/null +++ b/less.php/lib/Less/Tree/RulesetCall.php @@ -0,0 +1,26 @@ +variable = $variable; + } + + public function accept( $visitor ) { + } + + public function compile( $env ) { + $variable = new Less_Tree_Variable( $this->variable ); + $detachedRuleset = $variable->compile( $env ); + return $detachedRuleset->callEval( $env ); + } +} diff --git a/less.php/lib/Less/Tree/Selector.php b/less.php/lib/Less/Tree/Selector.php new file mode 100644 index 0000000..7ecfc6a --- /dev/null +++ b/less.php/lib/Less/Tree/Selector.php @@ -0,0 +1,165 @@ +elements = $elements; + $this->elements_len = count( $elements ); + $this->extendList = $extendList; + $this->condition = $condition; + if ( $currentFileInfo ) { + $this->currentFileInfo = $currentFileInfo; + } + $this->isReferenced = $isReferenced; + if ( !$condition ) { + $this->evaldCondition = true; + } + + $this->CacheElements(); + } + + public function accept( $visitor ) { + $this->elements = $visitor->visitArray( $this->elements ); + $this->extendList = $visitor->visitArray( $this->extendList ); + if ( $this->condition ) { + $this->condition = $visitor->visitObj( $this->condition ); + } + + if ( $visitor instanceof Less_Visitor_extendFinder ) { + $this->CacheElements(); + } + } + + public function createDerived( $elements, $extendList = null, $evaldCondition = null ) { + $newSelector = new Less_Tree_Selector( $elements, ( $extendList ? $extendList : $this->extendList ), null, $this->index, $this->currentFileInfo, $this->isReferenced ); + $newSelector->evaldCondition = $evaldCondition ? $evaldCondition : $this->evaldCondition; + return $newSelector; + } + + public function match( $other ) { + if ( !$other->_oelements || ( $this->elements_len < $other->_oelements_len ) ) { + return 0; + } + + for ( $i = 0; $i < $other->_oelements_len; $i++ ) { + if ( $this->elements[$i]->value !== $other->_oelements[$i] ) { + return 0; + } + } + + return $other->_oelements_len; // return number of matched elements + } + + public function CacheElements() { + $this->_oelements = array(); + $this->_oelements_assoc = array(); + + $css = ''; + + foreach ( $this->elements as $v ) { + + $css .= $v->combinator; + if ( !$v->value_is_object ) { + $css .= $v->value; + continue; + } + + if ( !property_exists( $v->value, 'value' ) || !is_string( $v->value->value ) ) { + $this->cacheable = false; + return; + } + $css .= $v->value->value; + } + + $this->_oelements_len = preg_match_all( '/[,&#\.\w-](?:[\w-]|(?:\\\\.))*/', $css, $matches ); + if ( $this->_oelements_len ) { + $this->_oelements = $matches[0]; + + if ( $this->_oelements[0] === '&' ) { + array_shift( $this->_oelements ); + $this->_oelements_len--; + } + + $this->_oelements_assoc = array_fill_keys( $this->_oelements, true ); + } + } + + public function isJustParentSelector() { + return !$this->mediaEmpty && + count( $this->elements ) === 1 && + $this->elements[0]->value === '&' && + ( $this->elements[0]->combinator === ' ' || $this->elements[0]->combinator === '' ); + } + + public function compile( $env ) { + $elements = array(); + foreach ( $this->elements as $el ) { + $elements[] = $el->compile( $env ); + } + + $extendList = array(); + foreach ( $this->extendList as $el ) { + $extendList[] = $el->compile( $el ); + } + + $evaldCondition = false; + if ( $this->condition ) { + $evaldCondition = $this->condition->compile( $env ); + } + + return $this->createDerived( $elements, $extendList, $evaldCondition ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output, $firstSelector = true ) { + if ( !$firstSelector && $this->elements[0]->combinator === "" ) { + $output->add( ' ', $this->currentFileInfo, $this->index ); + } + + foreach ( $this->elements as $element ) { + $element->genCSS( $output ); + } + } + + public function markReferenced() { + $this->isReferenced = true; + } + + public function getIsReferenced() { + return !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] || $this->isReferenced; + } + + public function getIsOutput() { + return $this->evaldCondition; + } + +} diff --git a/less.php/lib/Less/Tree/UnicodeDescriptor.php b/less.php/lib/Less/Tree/UnicodeDescriptor.php new file mode 100644 index 0000000..38e0526 --- /dev/null +++ b/less.php/lib/Less/Tree/UnicodeDescriptor.php @@ -0,0 +1,28 @@ +value = $value; + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( $this->value ); + } + + public function compile() { + return $this; + } +} diff --git a/less.php/lib/Less/Tree/Unit.php b/less.php/lib/Less/Tree/Unit.php new file mode 100644 index 0000000..29230df --- /dev/null +++ b/less.php/lib/Less/Tree/Unit.php @@ -0,0 +1,142 @@ +numerator = $numerator; + $this->denominator = $denominator; + $this->backupUnit = $backupUnit; + } + + public function __clone() { + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + if ( $this->numerator ) { + $output->add( $this->numerator[0] ); + } elseif ( $this->denominator ) { + $output->add( $this->denominator[0] ); + } elseif ( !Less_Parser::$options['strictUnits'] && $this->backupUnit ) { + $output->add( $this->backupUnit ); + return; + } + } + + public function toString() { + $returnStr = implode( '*', $this->numerator ); + foreach ( $this->denominator as $d ) { + $returnStr .= '/'.$d; + } + return $returnStr; + } + + public function __toString() { + return $this->toString(); + } + + /** + * @param Less_Tree_Unit $other + */ + public function compare( $other ) { + return $this->is( $other->toString() ) ? 0 : -1; + } + + public function is( $unitString ) { + return $this->toString() === $unitString; + } + + public function isLength() { + $css = $this->toCSS(); + return !!preg_match( '/px|em|%|in|cm|mm|pc|pt|ex/', $css ); + } + + public function isAngle() { + return isset( Less_Tree_UnitConversions::$angle[$this->toCSS()] ); + } + + public function isEmpty() { + return !$this->numerator && !$this->denominator; + } + + public function isSingular() { + return count( $this->numerator ) <= 1 && !$this->denominator; + } + + public function usedUnits() { + $result = array(); + + foreach ( Less_Tree_UnitConversions::$groups as $groupName ) { + $group = Less_Tree_UnitConversions::${$groupName}; + + foreach ( $this->numerator as $atomicUnit ) { + if ( isset( $group[$atomicUnit] ) && !isset( $result[$groupName] ) ) { + $result[$groupName] = $atomicUnit; + } + } + + foreach ( $this->denominator as $atomicUnit ) { + if ( isset( $group[$atomicUnit] ) && !isset( $result[$groupName] ) ) { + $result[$groupName] = $atomicUnit; + } + } + } + + return $result; + } + + public function cancel() { + $counter = array(); + $backup = null; + + foreach ( $this->numerator as $atomicUnit ) { + if ( !$backup ) { + $backup = $atomicUnit; + } + $counter[$atomicUnit] = ( isset( $counter[$atomicUnit] ) ? $counter[$atomicUnit] : 0 ) + 1; + } + + foreach ( $this->denominator as $atomicUnit ) { + if ( !$backup ) { + $backup = $atomicUnit; + } + $counter[$atomicUnit] = ( isset( $counter[$atomicUnit] ) ? $counter[$atomicUnit] : 0 ) - 1; + } + + $this->numerator = array(); + $this->denominator = array(); + + foreach ( $counter as $atomicUnit => $count ) { + if ( $count > 0 ) { + for ( $i = 0; $i < $count; $i++ ) { + $this->numerator[] = $atomicUnit; + } + } elseif ( $count < 0 ) { + for ( $i = 0; $i < -$count; $i++ ) { + $this->denominator[] = $atomicUnit; + } + } + } + + if ( !$this->numerator && !$this->denominator && $backup ) { + $this->backupUnit = $backup; + } + + sort( $this->numerator ); + sort( $this->denominator ); + } + +} diff --git a/less.php/lib/Less/Tree/UnitConversions.php b/less.php/lib/Less/Tree/UnitConversions.php new file mode 100644 index 0000000..7f857fb --- /dev/null +++ b/less.php/lib/Less/Tree/UnitConversions.php @@ -0,0 +1,35 @@ + 1, + 'cm' => 0.01, + 'mm' => 0.001, + 'in' => 0.0254, + 'px' => 0.000264583, // 0.0254 / 96, + 'pt' => 0.000352778, // 0.0254 / 72, + 'pc' => 0.004233333, // 0.0254 / 72 * 12 + ); + + public static $duration = array( + 's' => 1, + 'ms' => 0.001 + ); + + public static $angle = array( + 'rad' => 0.1591549430919, // 1/(2*M_PI), + 'deg' => 0.002777778, // 1/360, + 'grad' => 0.0025, // 1/400, + 'turn' => 1 + ); + +} diff --git a/less.php/lib/Less/Tree/Url.php b/less.php/lib/Less/Tree/Url.php new file mode 100644 index 0000000..4e7f114 --- /dev/null +++ b/less.php/lib/Less/Tree/Url.php @@ -0,0 +1,76 @@ +value = $value; + $this->currentFileInfo = $currentFileInfo; + $this->isEvald = $isEvald; + } + + public function accept( $visitor ) { + $this->value = $visitor->visitObj( $this->value ); + } + + /** + * @see Less_Tree::genCSS + */ + public function genCSS( $output ) { + $output->add( 'url(' ); + $this->value->genCSS( $output ); + $output->add( ')' ); + } + + /** + * @param Less_Functions $ctx + */ + public function compile( $ctx ) { + $val = $this->value->compile( $ctx ); + + if ( !$this->isEvald ) { + // Add the base path if the URL is relative + if ( Less_Parser::$options['relativeUrls'] + && $this->currentFileInfo + && is_string( $val->value ) + && Less_Environment::isPathRelative( $val->value ) + ) { + $rootpath = $this->currentFileInfo['uri_root']; + if ( !$val->quote ) { + $rootpath = preg_replace( '/[\(\)\'"\s]/', '\\$1', $rootpath ); + } + $val->value = $rootpath . $val->value; + } + + $val->value = Less_Environment::normalizePath( $val->value ); + } + + // Add cache buster if enabled + if ( Less_Parser::$options['urlArgs'] ) { + if ( !preg_match( '/^\s*data:/', $val->value ) ) { + $delimiter = strpos( $val->value, '?' ) === false ? '?' : '&'; + $urlArgs = $delimiter . Less_Parser::$options['urlArgs']; + $hash_pos = strpos( $val->value, '#' ); + if ( $hash_pos !== false ) { + $val->value = substr_replace( $val->value, $urlArgs, $hash_pos, 0 ); + } else { + $val->value .= $urlArgs; + } + } + } + + return new Less_Tree_URL( $val, $this->currentFileInfo, true ); + } + +} diff --git a/less.php/lib/Less/Tree/Value.php b/less.php/lib/Less/Tree/Value.php new file mode 100644 index 0000000..a4c0d85 --- /dev/null +++ b/less.php/lib/Less/Tree/Value.php @@ -0,0 +1,47 @@ +value = $value; + } + + public function accept( $visitor ) { + $this->value = $visitor->visitArray( $this->value ); + } + + public function compile( $env ) { + $ret = array(); + $i = 0; + foreach ( $this->value as $i => $v ) { + $ret[] = $v->compile( $env ); + } + if ( $i > 0 ) { + return new Less_Tree_Value( $ret ); + } + return $ret[0]; + } + + /** + * @see Less_Tree::genCSS + */ + function genCSS( $output ) { + $len = count( $this->value ); + for ( $i = 0; $i < $len; $i++ ) { + $this->value[$i]->genCSS( $output ); + if ( $i + 1 < $len ) { + $output->add( Less_Environment::$_outputMap[','] ); + } + } + } + +} diff --git a/less.php/lib/Less/Tree/Variable.php b/less.php/lib/Less/Tree/Variable.php new file mode 100644 index 0000000..e36d3d4 --- /dev/null +++ b/less.php/lib/Less/Tree/Variable.php @@ -0,0 +1,51 @@ +name = $name; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function compile( $env ) { + if ( $this->name[1] === '@' ) { + $v = new Less_Tree_Variable( substr( $this->name, 1 ), $this->index + 1, $this->currentFileInfo ); + $name = '@' . $v->compile( $env )->value; + } else { + $name = $this->name; + } + + if ( $this->evaluating ) { + throw new Less_Exception_Compiler( "Recursive variable definition for " . $name, null, $this->index, $this->currentFileInfo ); + } + + $this->evaluating = true; + + foreach ( $env->frames as $frame ) { + if ( $v = $frame->variable( $name ) ) { + $r = $v->value->compile( $env ); + $this->evaluating = false; + return $r; + } + } + + throw new Less_Exception_Compiler( "variable " . $name . " is undefined in file ".$this->currentFileInfo["filename"], null, $this->index, $this->currentFileInfo ); + } + +} diff --git a/less.php/lib/Less/Version.php b/less.php/lib/Less/Version.php new file mode 100644 index 0000000..2d05c38 --- /dev/null +++ b/less.php/lib/Less/Version.php @@ -0,0 +1,15 @@ +_visitFnCache = get_class_methods( get_class( $this ) ); + $this->_visitFnCache = array_flip( $this->_visitFnCache ); + } + + public function visitObj( $node ) { + $funcName = 'visit'.$node->type; + if ( isset( $this->_visitFnCache[$funcName] ) ) { + + $visitDeeper = true; + $this->$funcName( $node, $visitDeeper ); + + if ( $visitDeeper ) { + $node->accept( $this ); + } + + $funcName = $funcName . "Out"; + if ( isset( $this->_visitFnCache[$funcName] ) ) { + $this->$funcName( $node ); + } + + } else { + $node->accept( $this ); + } + + return $node; + } + + public function visitArray( $nodes ) { + array_map( array( $this,'visitObj' ), $nodes ); + return $nodes; + } +} diff --git a/less.php/lib/Less/Visitor/extendFinder.php b/less.php/lib/Less/Visitor/extendFinder.php new file mode 100644 index 0000000..7d5d3fd --- /dev/null +++ b/less.php/lib/Less/Visitor/extendFinder.php @@ -0,0 +1,109 @@ +contexts = array(); + $this->allExtendsStack = array( array() ); + parent::__construct(); + } + + /** + * @param Less_Tree_Ruleset $root + */ + public function run( $root ) { + $root = $this->visitObj( $root ); + $root->allExtends =& $this->allExtendsStack[0]; + return $root; + } + + public function visitRule( $ruleNode, &$visitDeeper ) { + $visitDeeper = false; + } + + public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) { + $visitDeeper = false; + } + + public function visitRuleset( $rulesetNode ) { + if ( $rulesetNode->root ) { + return; + } + + $allSelectorsExtendList = array(); + + // get &:extend(.a); rules which apply to all selectors in this ruleset + if ( $rulesetNode->rules ) { + foreach ( $rulesetNode->rules as $rule ) { + if ( $rule instanceof Less_Tree_Extend ) { + $allSelectorsExtendList[] = $rule; + $rulesetNode->extendOnEveryPath = true; + } + } + } + + // now find every selector and apply the extends that apply to all extends + // and the ones which apply to an individual extend + foreach ( $rulesetNode->paths as $selectorPath ) { + $selector = end( $selectorPath ); // $selectorPath[ count($selectorPath)-1]; + + $j = 0; + foreach ( $selector->extendList as $extend ) { + $this->allExtendsStackPush( $rulesetNode, $selectorPath, $extend, $j ); + } + foreach ( $allSelectorsExtendList as $extend ) { + $this->allExtendsStackPush( $rulesetNode, $selectorPath, $extend, $j ); + } + } + + $this->contexts[] = $rulesetNode->selectors; + } + + public function allExtendsStackPush( $rulesetNode, $selectorPath, $extend, &$j ) { + $this->foundExtends = true; + $extend = clone $extend; + $extend->findSelfSelectors( $selectorPath ); + $extend->ruleset = $rulesetNode; + if ( $j === 0 ) { + $extend->firstExtendOnThisSelectorPath = true; + } + + $end_key = count( $this->allExtendsStack ) - 1; + $this->allExtendsStack[$end_key][] = $extend; + $j++; + } + + public function visitRulesetOut( $rulesetNode ) { + if ( !is_object( $rulesetNode ) || !$rulesetNode->root ) { + array_pop( $this->contexts ); + } + } + + public function visitMedia( $mediaNode ) { + $mediaNode->allExtends = array(); + $this->allExtendsStack[] =& $mediaNode->allExtends; + } + + public function visitMediaOut() { + array_pop( $this->allExtendsStack ); + } + + public function visitDirective( $directiveNode ) { + $directiveNode->allExtends = array(); + $this->allExtendsStack[] =& $directiveNode->allExtends; + } + + public function visitDirectiveOut() { + array_pop( $this->allExtendsStack ); + } +} diff --git a/less.php/lib/Less/Visitor/import.php b/less.php/lib/Less/Visitor/import.php new file mode 100644 index 0000000..7af96eb --- /dev/null +++ b/less.php/lib/Less/Visitor/import.php @@ -0,0 +1,137 @@ +env = $evalEnv; + $this->importCount = 0; + parent::__construct(); + } + + + function run( $root ){ + $root = $this->visitObj($root); + $this->isFinished = true; + + //if( $this->importCount === 0) { + // $this->_finish(); + //} + } + + function visitImport($importNode, &$visitDeeper ){ + $importVisitor = $this; + $inlineCSS = $importNode->options['inline']; + + if( !$importNode->css || $inlineCSS ){ + $evaldImportNode = $importNode->compileForImport($this->env); + + if( $evaldImportNode && (!$evaldImportNode->css || $inlineCSS) ){ + $importNode = $evaldImportNode; + $this->importCount++; + $env = clone $this->env; + + if( (isset($importNode->options['multiple']) && $importNode->options['multiple']) ){ + $env->importMultiple = true; + } + + //get path & uri + $path_and_uri = null; + if( is_callable(Less_Parser::$options['import_callback']) ){ + $path_and_uri = call_user_func(Less_Parser::$options['import_callback'],$importNode); + } + + if( !$path_and_uri ){ + $path_and_uri = $importNode->PathAndUri(); + } + + if( $path_and_uri ){ + list($full_path, $uri) = $path_and_uri; + }else{ + $full_path = $uri = $importNode->getPath(); + } + + + //import once + if( $importNode->skip( $full_path, $env) ){ + return array(); + } + + if( $importNode->options['inline'] ){ + //todo needs to reference css file not import + //$contents = new Less_Tree_Anonymous($importNode->root, 0, array('filename'=>$importNode->importedFilename), true ); + + Less_Parser::AddParsedFile($full_path); + $contents = new Less_Tree_Anonymous( file_get_contents($full_path), 0, array(), true ); + + if( $importNode->features ){ + return new Less_Tree_Media( array($contents), $importNode->features->value ); + } + + return array( $contents ); + } + + + // css ? + if( $importNode->css ){ + $features = ( $importNode->features ? $importNode->features->compile($env) : null ); + return new Less_Tree_Import( $importNode->compilePath( $env), $features, $importNode->options, $this->index); + } + + return $importNode->ParseImport( $full_path, $uri, $env ); + } + + } + + $visitDeeper = false; + return $importNode; + } + + + function visitRule( $ruleNode, &$visitDeeper ){ + $visitDeeper = false; + return $ruleNode; + } + + function visitDirective($directiveNode, $visitArgs){ + array_unshift($this->env->frames,$directiveNode); + return $directiveNode; + } + + function visitDirectiveOut($directiveNode) { + array_shift($this->env->frames); + } + + function visitMixinDefinition($mixinDefinitionNode, $visitArgs) { + array_unshift($this->env->frames,$mixinDefinitionNode); + return $mixinDefinitionNode; + } + + function visitMixinDefinitionOut($mixinDefinitionNode) { + array_shift($this->env->frames); + } + + function visitRuleset($rulesetNode, $visitArgs) { + array_unshift($this->env->frames,$rulesetNode); + return $rulesetNode; + } + + function visitRulesetOut($rulesetNode) { + array_shift($this->env->frames); + } + + function visitMedia($mediaNode, $visitArgs) { + array_unshift($this->env->frames, $mediaNode->ruleset); + return $mediaNode; + } + + function visitMediaOut($mediaNode) { + array_shift($this->env->frames); + } + +} +*/ diff --git a/less.php/lib/Less/Visitor/joinSelector.php b/less.php/lib/Less/Visitor/joinSelector.php new file mode 100644 index 0000000..bb08ece --- /dev/null +++ b/less.php/lib/Less/Visitor/joinSelector.php @@ -0,0 +1,68 @@ +visitObj( $root ); + } + + public function visitRule( $ruleNode, &$visitDeeper ) { + $visitDeeper = false; + } + + public function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) { + $visitDeeper = false; + } + + public function visitRuleset( $rulesetNode ) { + $paths = array(); + + if ( !$rulesetNode->root ) { + $selectors = array(); + + if ( $rulesetNode->selectors && $rulesetNode->selectors ) { + foreach ( $rulesetNode->selectors as $selector ) { + if ( $selector->getIsOutput() ) { + $selectors[] = $selector; + } + } + } + + if ( !$selectors ) { + $rulesetNode->selectors = null; + $rulesetNode->rules = null; + } else { + $context = end( $this->contexts ); // $context = $this->contexts[ count($this->contexts) - 1]; + $paths = $rulesetNode->joinSelectors( $context, $selectors ); + } + + $rulesetNode->paths = $paths; + } + + $this->contexts[] = $paths; // different from less.js. Placed after joinSelectors() so that $this->contexts will get correct $paths + } + + public function visitRulesetOut() { + array_pop( $this->contexts ); + } + + public function visitMedia( $mediaNode ) { + $context = end( $this->contexts ); // $context = $this->contexts[ count($this->contexts) - 1]; + + if ( !count( $context ) || ( is_object( $context[0] ) && $context[0]->multiMedia ) ) { + $mediaNode->rules[0]->root = true; + } + } + +} diff --git a/less.php/lib/Less/Visitor/processExtends.php b/less.php/lib/Less/Visitor/processExtends.php new file mode 100644 index 0000000..65082a4 --- /dev/null +++ b/less.php/lib/Less/Visitor/processExtends.php @@ -0,0 +1,441 @@ +run( $root ); + if ( !$extendFinder->foundExtends ) { + return $root; + } + + $root->allExtends = $this->doExtendChaining( $root->allExtends, $root->allExtends ); + + $this->allExtendsStack = array(); + $this->allExtendsStack[] = &$root->allExtends; + + return $this->visitObj( $root ); + } + + private function doExtendChaining( $extendsList, $extendsListTarget, $iterationCount = 0 ) { + // + // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting + // the selector we would do normally, but we are also adding an extend with the same target selector + // this means this new extend can then go and alter other extends + // + // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors + // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if + // we look at each selector at a time, as is done in visitRuleset + + $extendsToAdd = array(); + + // loop through comparing every extend with every target extend. + // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place + // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one + // and the second is the target. + // the separation into two lists allows us to process a subset of chains with a bigger set, as is the + // case when processing media queries + for ( $extendIndex = 0, $extendsList_len = count( $extendsList ); $extendIndex < $extendsList_len; $extendIndex++ ) { + for ( $targetExtendIndex = 0; $targetExtendIndex < count( $extendsListTarget ); $targetExtendIndex++ ) { + + $extend = $extendsList[$extendIndex]; + $targetExtend = $extendsListTarget[$targetExtendIndex]; + + // Optimisation: Explicit reference,  + if ( \array_key_exists( $targetExtend->object_id, $extend->parent_ids ) ) { + // ignore circular references + continue; + } + + // find a match in the target extends self selector (the bit before :extend) + $selectorPath = array( $targetExtend->selfSelectors[0] ); + $matches = $this->findMatch( $extend, $selectorPath ); + + if ( $matches ) { + + // we found a match, so for each self selector.. + foreach ( $extend->selfSelectors as $selfSelector ) { + + // process the extend as usual + $newSelector = $this->extendSelector( $matches, $selectorPath, $selfSelector ); + + // but now we create a new extend from it + $newExtend = new Less_Tree_Extend( $targetExtend->selector, $targetExtend->option, 0 ); + $newExtend->selfSelectors = $newSelector; + + // add the extend onto the list of extends for that selector + end( $newSelector )->extendList = array( $newExtend ); + // $newSelector[ count($newSelector)-1]->extendList = array($newExtend); + + // record that we need to add it. + $extendsToAdd[] = $newExtend; + $newExtend->ruleset = $targetExtend->ruleset; + + // remember its parents for circular references + $newExtend->parent_ids = array_merge( $newExtend->parent_ids, $targetExtend->parent_ids, $extend->parent_ids ); + + // only process the selector once.. if we have :extend(.a,.b) then multiple + // extends will look at the same selector path, so when extending + // we know that any others will be duplicates in terms of what is added to the css + if ( $targetExtend->firstExtendOnThisSelectorPath ) { + $newExtend->firstExtendOnThisSelectorPath = true; + $targetExtend->ruleset->paths[] = $newSelector; + } + } + } + } + } + + if ( $extendsToAdd ) { + // try to detect circular references to stop a stack overflow. + // may no longer be needed. $this->extendChainCount++; + if ( $iterationCount > 100 ) { + + try{ + $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS(); + $selectorTwo = $extendsToAdd[0]->selector->toCSS(); + }catch ( Exception $e ) { + $selectorOne = "{unable to calculate}"; + $selectorTwo = "{unable to calculate}"; + } + + throw new Less_Exception_Parser( "extend circular reference detected. One of the circular extends is currently:" . $selectorOne . ":extend(" . $selectorTwo . ")" ); + } + + // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... + $extendsToAdd = $this->doExtendChaining( $extendsToAdd, $extendsListTarget, $iterationCount + 1 ); + } + + return array_merge( $extendsList, $extendsToAdd ); + } + + protected function visitRule( $ruleNode, &$visitDeeper ) { + $visitDeeper = false; + } + + protected function visitMixinDefinition( $mixinDefinitionNode, &$visitDeeper ) { + $visitDeeper = false; + } + + protected function visitSelector( $selectorNode, &$visitDeeper ) { + $visitDeeper = false; + } + + protected function visitRuleset( $rulesetNode ) { + if ( $rulesetNode->root ) { + return; + } + + $allExtends = end( $this->allExtendsStack ); + $paths_len = count( $rulesetNode->paths ); + + // look at each selector path in the ruleset, find any extend matches and then copy, find and replace + foreach ( $allExtends as $allExtend ) { + for ( $pathIndex = 0; $pathIndex < $paths_len; $pathIndex++ ) { + + // extending extends happens initially, before the main pass + if ( isset( $rulesetNode->extendOnEveryPath ) && $rulesetNode->extendOnEveryPath ) { + continue; + } + + $selectorPath = $rulesetNode->paths[$pathIndex]; + + if ( end( $selectorPath )->extendList ) { + continue; + } + + $this->ExtendMatch( $rulesetNode, $allExtend, $selectorPath ); + + } + } + } + + private function ExtendMatch( $rulesetNode, $extend, $selectorPath ) { + $matches = $this->findMatch( $extend, $selectorPath ); + + if ( $matches ) { + foreach ( $extend->selfSelectors as $selfSelector ) { + $rulesetNode->paths[] = $this->extendSelector( $matches, $selectorPath, $selfSelector ); + } + } + } + + private function findMatch( $extend, $haystackSelectorPath ) { + if ( !$this->HasMatches( $extend, $haystackSelectorPath ) ) { + return false; + } + + // + // look through the haystack selector path to try and find the needle - extend.selector + // returns an array of selector matches that can then be replaced + // + $needleElements = $extend->selector->elements; + $potentialMatches = array(); + $potentialMatches_len = 0; + $potentialMatch = null; + $matches = array(); + + // loop through the haystack elements + $haystack_path_len = count( $haystackSelectorPath ); + for ( $haystackSelectorIndex = 0; $haystackSelectorIndex < $haystack_path_len; $haystackSelectorIndex++ ) { + $hackstackSelector = $haystackSelectorPath[$haystackSelectorIndex]; + + $haystack_elements_len = count( $hackstackSelector->elements ); + for ( $hackstackElementIndex = 0; $hackstackElementIndex < $haystack_elements_len; $hackstackElementIndex++ ) { + + $haystackElement = $hackstackSelector->elements[$hackstackElementIndex]; + + // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. + if ( $extend->allowBefore || ( $haystackSelectorIndex === 0 && $hackstackElementIndex === 0 ) ) { + $potentialMatches[] = array( 'pathIndex' => $haystackSelectorIndex, 'index' => $hackstackElementIndex, 'matched' => 0, 'initialCombinator' => $haystackElement->combinator ); + $potentialMatches_len++; + } + + for ( $i = 0; $i < $potentialMatches_len; $i++ ) { + + $potentialMatch = &$potentialMatches[$i]; + $potentialMatch = $this->PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ); + + // if we are still valid and have finished, test whether we have elements after and whether these are allowed + if ( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ) { + $potentialMatch['finished'] = true; + + if ( !$extend->allowAfter && ( $hackstackElementIndex + 1 < $haystack_elements_len || $haystackSelectorIndex + 1 < $haystack_path_len ) ) { + $potentialMatch = null; + } + } + + // if null we remove, if not, we are still valid, so either push as a valid match or continue + if ( $potentialMatch ) { + if ( $potentialMatch['finished'] ) { + $potentialMatch['length'] = $extend->selector->elements_len; + $potentialMatch['endPathIndex'] = $haystackSelectorIndex; + $potentialMatch['endPathElementIndex'] = $hackstackElementIndex + 1; // index after end of match + $potentialMatches = array(); // we don't allow matches to overlap, so start matching again + $potentialMatches_len = 0; + $matches[] = $potentialMatch; + } + continue; + } + + array_splice( $potentialMatches, $i, 1 ); + $potentialMatches_len--; + $i--; + } + } + } + + return $matches; + } + + // Before going through all the nested loops, lets check to see if a match is possible + // Reduces Bootstrap 3.1 compile time from ~6.5s to ~5.6s + private function HasMatches( $extend, $haystackSelectorPath ) { + if ( !$extend->selector->cacheable ) { + return true; + } + + $first_el = $extend->selector->_oelements[0]; + + foreach ( $haystackSelectorPath as $hackstackSelector ) { + if ( !$hackstackSelector->cacheable ) { + return true; + } + + // Optimisation: Explicit reference,  + if ( \array_key_exists( $first_el, $hackstackSelector->_oelements_assoc ) ) { + return true; + } + } + + return false; + } + + /** + * @param integer $hackstackElementIndex + */ + private function PotentialMatch( $potentialMatch, $needleElements, $haystackElement, $hackstackElementIndex ) { + if ( $potentialMatch['matched'] > 0 ) { + + // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't + // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out + // what the resulting combinator will be + $targetCombinator = $haystackElement->combinator; + if ( $targetCombinator === '' && $hackstackElementIndex === 0 ) { + $targetCombinator = ' '; + } + + if ( $needleElements[ $potentialMatch['matched'] ]->combinator !== $targetCombinator ) { + return null; + } + } + + // if we don't match, null our match to indicate failure + if ( !$this->isElementValuesEqual( $needleElements[$potentialMatch['matched'] ]->value, $haystackElement->value ) ) { + return null; + } + + $potentialMatch['finished'] = false; + $potentialMatch['matched']++; + + return $potentialMatch; + } + + private function isElementValuesEqual( $elementValue1, $elementValue2 ) { + if ( $elementValue1 === $elementValue2 ) { + return true; + } + + if ( is_string( $elementValue1 ) || is_string( $elementValue2 ) ) { + return false; + } + + if ( $elementValue1 instanceof Less_Tree_Attribute ) { + return $this->isAttributeValuesEqual( $elementValue1, $elementValue2 ); + } + + $elementValue1 = $elementValue1->value; + if ( $elementValue1 instanceof Less_Tree_Selector ) { + return $this->isSelectorValuesEqual( $elementValue1, $elementValue2 ); + } + + return false; + } + + /** + * @param Less_Tree_Selector $elementValue1 + */ + private function isSelectorValuesEqual( $elementValue1, $elementValue2 ) { + $elementValue2 = $elementValue2->value; + if ( !( $elementValue2 instanceof Less_Tree_Selector ) || $elementValue1->elements_len !== $elementValue2->elements_len ) { + return false; + } + + for ( $i = 0; $i < $elementValue1->elements_len; $i++ ) { + + if ( $elementValue1->elements[$i]->combinator !== $elementValue2->elements[$i]->combinator ) { + if ( $i !== 0 || ( $elementValue1->elements[$i]->combinator || ' ' ) !== ( $elementValue2->elements[$i]->combinator || ' ' ) ) { + return false; + } + } + + if ( !$this->isElementValuesEqual( $elementValue1->elements[$i]->value, $elementValue2->elements[$i]->value ) ) { + return false; + } + } + + return true; + } + + /** + * @param Less_Tree_Attribute $elementValue1 + */ + private function isAttributeValuesEqual( $elementValue1, $elementValue2 ) { + if ( $elementValue1->op !== $elementValue2->op || $elementValue1->key !== $elementValue2->key ) { + return false; + } + + if ( !$elementValue1->value || !$elementValue2->value ) { + if ( $elementValue1->value || $elementValue2->value ) { + return false; + } + return true; + } + + $elementValue1 = ( $elementValue1->value->value ? $elementValue1->value->value : $elementValue1->value ); + $elementValue2 = ( $elementValue2->value->value ? $elementValue2->value->value : $elementValue2->value ); + + return $elementValue1 === $elementValue2; + } + + private function extendSelector( $matches, $selectorPath, $replacementSelector ) { + // for a set of matches, replace each match with the replacement selector + + $currentSelectorPathIndex = 0; + $currentSelectorPathElementIndex = 0; + $path = array(); + $selectorPath_len = count( $selectorPath ); + + for ( $matchIndex = 0, $matches_len = count( $matches ); $matchIndex < $matches_len; $matchIndex++ ) { + + $match = $matches[$matchIndex]; + $selector = $selectorPath[ $match['pathIndex'] ]; + + $firstElement = new Less_Tree_Element( + $match['initialCombinator'], + $replacementSelector->elements[0]->value, + $replacementSelector->elements[0]->index, + $replacementSelector->elements[0]->currentFileInfo + ); + + if ( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ) { + $last_path = end( $path ); + $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) ); + $currentSelectorPathElementIndex = 0; + $currentSelectorPathIndex++; + } + + $newElements = array_merge( + array_slice( $selector->elements, $currentSelectorPathElementIndex, ( $match['index'] - $currentSelectorPathElementIndex ) ), // last parameter of array_slice is different than the last parameter of javascript's slice + array( $firstElement ), + array_slice( $replacementSelector->elements, 1 ) + ); + + if ( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ) { + $last_key = count( $path ) - 1; + $path[$last_key]->elements = array_merge( $path[$last_key]->elements, $newElements ); + } else { + $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $match['pathIndex'] ) ); + $path[] = new Less_Tree_Selector( $newElements ); + } + + $currentSelectorPathIndex = $match['endPathIndex']; + $currentSelectorPathElementIndex = $match['endPathElementIndex']; + if ( $currentSelectorPathElementIndex >= count( $selectorPath[$currentSelectorPathIndex]->elements ) ) { + $currentSelectorPathElementIndex = 0; + $currentSelectorPathIndex++; + } + } + + if ( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ) { + $last_path = end( $path ); + $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) ); + $currentSelectorPathIndex++; + } + + $slice_len = $selectorPath_len - $currentSelectorPathIndex; + $path = array_merge( $path, array_slice( $selectorPath, $currentSelectorPathIndex, $slice_len ) ); + + return $path; + } + + protected function visitMedia( $mediaNode ) { + $newAllExtends = array_merge( $mediaNode->allExtends, end( $this->allExtendsStack ) ); + $this->allExtendsStack[] = $this->doExtendChaining( $newAllExtends, $mediaNode->allExtends ); + } + + protected function visitMediaOut() { + array_pop( $this->allExtendsStack ); + } + + protected function visitDirective( $directiveNode ) { + $newAllExtends = array_merge( $directiveNode->allExtends, end( $this->allExtendsStack ) ); + $this->allExtendsStack[] = $this->doExtendChaining( $newAllExtends, $directiveNode->allExtends ); + } + + protected function visitDirectiveOut() { + array_pop( $this->allExtendsStack ); + } + +} diff --git a/less.php/lib/Less/Visitor/toCSS.php b/less.php/lib/Less/Visitor/toCSS.php new file mode 100644 index 0000000..e90f211 --- /dev/null +++ b/less.php/lib/Less/Visitor/toCSS.php @@ -0,0 +1,280 @@ +visitObj( $root ); + } + + public function visitRule( $ruleNode ) { + if ( $ruleNode->variable ) { + return array(); + } + return $ruleNode; + } + + public function visitMixinDefinition( $mixinNode ) { + // mixin definitions do not get eval'd - this means they keep state + // so we have to clear that state here so it isn't used if toCSS is called twice + $mixinNode->frames = array(); + return array(); + } + + public function visitExtend() { + return array(); + } + + public function visitComment( $commentNode ) { + if ( $commentNode->isSilent() ) { + return array(); + } + return $commentNode; + } + + public function visitMedia( $mediaNode, &$visitDeeper ) { + $mediaNode->accept( $this ); + $visitDeeper = false; + + if ( !$mediaNode->rules ) { + return array(); + } + return $mediaNode; + } + + public function visitDirective( $directiveNode ) { + if ( isset( $directiveNode->currentFileInfo['reference'] ) && ( !property_exists( $directiveNode, 'isReferenced' ) || !$directiveNode->isReferenced ) ) { + return array(); + } + if ( $directiveNode->name === '@charset' ) { + // Only output the debug info together with subsequent @charset definitions + // a comment (or @media statement) before the actual @charset directive would + // be considered illegal css as it has to be on the first line + if ( isset( $this->charset ) && $this->charset ) { + + // if( $directiveNode->debugInfo ){ + // $comment = new Less_Tree_Comment('/* ' . str_replace("\n",'',$directiveNode->toCSS())." */\n"); + // $comment->debugInfo = $directiveNode->debugInfo; + // return $this->visit($comment); + //} + + return array(); + } + $this->charset = true; + } + return $directiveNode; + } + + public function checkPropertiesInRoot( $rulesetNode ) { + if ( !$rulesetNode->firstRoot ) { + return; + } + + foreach ( $rulesetNode->rules as $ruleNode ) { + if ( $ruleNode instanceof Less_Tree_Rule && !$ruleNode->variable ) { + $msg = "properties must be inside selector blocks, they cannot be in the root. Index ".$ruleNode->index.( $ruleNode->currentFileInfo ? ( ' Filename: '.$ruleNode->currentFileInfo['filename'] ) : null ); + throw new Less_Exception_Compiler( $msg ); + } + } + } + + public function visitRuleset( $rulesetNode, &$visitDeeper ) { + $visitDeeper = false; + + $this->checkPropertiesInRoot( $rulesetNode ); + + if ( $rulesetNode->root ) { + return $this->visitRulesetRoot( $rulesetNode ); + } + + $rulesets = array(); + $rulesetNode->paths = $this->visitRulesetPaths( $rulesetNode ); + + // Compile rules and rulesets + $nodeRuleCnt = $rulesetNode->rules ? count( $rulesetNode->rules ) : 0; + for ( $i = 0; $i < $nodeRuleCnt; ) { + $rule = $rulesetNode->rules[$i]; + + if ( property_exists( $rule, 'rules' ) ) { + // visit because we are moving them out from being a child + $rulesets[] = $this->visitObj( $rule ); + array_splice( $rulesetNode->rules, $i, 1 ); + $nodeRuleCnt--; + continue; + } + $i++; + } + + // accept the visitor to remove rules and refactor itself + // then we can decide now whether we want it or not + if ( $nodeRuleCnt > 0 ) { + $rulesetNode->accept( $this ); + + if ( $rulesetNode->rules ) { + + if ( count( $rulesetNode->rules ) > 1 ) { + $this->_mergeRules( $rulesetNode->rules ); + $this->_removeDuplicateRules( $rulesetNode->rules ); + } + + // now decide whether we keep the ruleset + if ( $rulesetNode->paths ) { + // array_unshift($rulesets, $rulesetNode); + array_splice( $rulesets, 0, 0, array( $rulesetNode ) ); + } + } + + } + + if ( count( $rulesets ) === 1 ) { + return $rulesets[0]; + } + return $rulesets; + } + + /** + * Helper function for visitiRuleset + * + * return array|Less_Tree_Ruleset + */ + private function visitRulesetRoot( $rulesetNode ) { + $rulesetNode->accept( $this ); + if ( $rulesetNode->firstRoot || $rulesetNode->rules ) { + return $rulesetNode; + } + return array(); + } + + /** + * Helper function for visitRuleset() + * + * @return array + */ + private function visitRulesetPaths( $rulesetNode ) { + $paths = array(); + foreach ( $rulesetNode->paths as $p ) { + if ( $p[0]->elements[0]->combinator === ' ' ) { + $p[0]->elements[0]->combinator = ''; + } + + foreach ( $p as $pi ) { + if ( $pi->getIsReferenced() && $pi->getIsOutput() ) { + $paths[] = $p; + break; + } + } + } + + return $paths; + } + + protected function _removeDuplicateRules( &$rules ) { + // remove duplicates + $ruleCache = array(); + for ( $i = count( $rules ) - 1; $i >= 0; $i-- ) { + $rule = $rules[$i]; + if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_NameValue ) { + + if ( !isset( $ruleCache[$rule->name] ) ) { + $ruleCache[$rule->name] = $rule; + } else { + $ruleList =& $ruleCache[$rule->name]; + + if ( $ruleList instanceof Less_Tree_Rule || $ruleList instanceof Less_Tree_NameValue ) { + $ruleList = $ruleCache[$rule->name] = array( $ruleCache[$rule->name]->toCSS() ); + } + + $ruleCSS = $rule->toCSS(); + if ( array_search( $ruleCSS, $ruleList ) !== false ) { + array_splice( $rules, $i, 1 ); + } else { + $ruleList[] = $ruleCSS; + } + } + } + } + } + + protected function _mergeRules( &$rules ) { + $groups = array(); + + // obj($rules); + + $rules_len = count( $rules ); + for ( $i = 0; $i < $rules_len; $i++ ) { + $rule = $rules[$i]; + + if ( ( $rule instanceof Less_Tree_Rule ) && $rule->merge ) { + + $key = $rule->name; + if ( $rule->important ) { + $key .= ',!'; + } + + if ( !isset( $groups[$key] ) ) { + $groups[$key] = array(); + } else { + array_splice( $rules, $i--, 1 ); + $rules_len--; + } + + $groups[$key][] = $rule; + } + } + + foreach ( $groups as $parts ) { + + if ( count( $parts ) > 1 ) { + $rule = $parts[0]; + $spacedGroups = array(); + $lastSpacedGroup = array(); + $parts_mapped = array(); + foreach ( $parts as $p ) { + if ( $p->merge === '+' ) { + if ( $lastSpacedGroup ) { + $spacedGroups[] = self::toExpression( $lastSpacedGroup ); + } + $lastSpacedGroup = array(); + } + $lastSpacedGroup[] = $p; + } + + $spacedGroups[] = self::toExpression( $lastSpacedGroup ); + $rule->value = self::toValue( $spacedGroups ); + } + } + + } + + public static function toExpression( $values ) { + $mapped = array(); + foreach ( $values as $p ) { + $mapped[] = $p->value; + } + return new Less_Tree_Expression( $mapped ); + } + + public static function toValue( $values ) { + // return new Less_Tree_Value($values); ?? + + $mapped = array(); + foreach ( $values as $p ) { + $mapped[] = $p; + } + return new Less_Tree_Value( $mapped ); + } +} diff --git a/less.php/lib/Less/VisitorReplacing.php b/less.php/lib/Less/VisitorReplacing.php new file mode 100644 index 0000000..cd401ad --- /dev/null +++ b/less.php/lib/Less/VisitorReplacing.php @@ -0,0 +1,70 @@ +type; + if ( isset( $this->_visitFnCache[$funcName] ) ) { + + $visitDeeper = true; + $node = $this->$funcName( $node, $visitDeeper ); + + if ( $node ) { + if ( $visitDeeper && is_object( $node ) ) { + $node->accept( $this ); + } + + $funcName = $funcName . "Out"; + if ( isset( $this->_visitFnCache[$funcName] ) ) { + $this->$funcName( $node ); + } + } + + } else { + $node->accept( $this ); + } + + return $node; + } + + public function visitArray( $nodes ) { + $newNodes = array(); + foreach ( $nodes as $node ) { + $evald = $this->visitObj( $node ); + if ( $evald ) { + if ( is_array( $evald ) ) { + self::flatten( $evald, $newNodes ); + } else { + $newNodes[] = $evald; + } + } + } + return $newNodes; + } + + public function flatten( $arr, &$out ) { + foreach ( $arr as $item ) { + if ( !is_array( $item ) ) { + $out[] = $item; + continue; + } + + foreach ( $item as $nestedItem ) { + if ( is_array( $nestedItem ) ) { + self::flatten( $nestedItem, $out ); + } else { + $out[] = $nestedItem; + } + } + } + + return $out; + } + +} diff --git a/lesserphp/LICENSE b/lesserphp/LICENSE deleted file mode 100644 index 5290202..0000000 --- a/lesserphp/LICENSE +++ /dev/null @@ -1,661 +0,0 @@ -For ease of distribution, lessphp 0.5.1 is under a dual license. -You are free to pick which one suits your needs. - - - - -MIT LICENSE - - - - -Copyright (c) 2013 - 2015 Leaf Corcoran, http://leafo.net/lessphp -Copyright (c) 2016 - Marcus Schwarz, https://www.maswaba.de - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - - -GPL VERSION 3 - - - - - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - diff --git a/lesserphp/README.md b/lesserphp/README.md deleted file mode 100644 index 8c26ab1..0000000 --- a/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.5.4 -### - -`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/lesserphp/docs/docs.md b/lesserphp/docs/docs.md deleted file mode 100644 index e25c877..0000000 --- a/lesserphp/docs/docs.md +++ /dev/null @@ -1,1400 +0,0 @@ - title: v0.5.1 documentation - link_to_home: true --- - -

Documentation v0.5.1

- -
$index
- -**lesserphp** is a compiler that generates CSS from a superset language which -adds a collection of convenient features often seen in other languages. All CSS -is compatible with LESS, so you can start using new features with your existing CSS. - -It is designed to be compatible with [less.js](http://lesscss.org), and suitable -as a drop in replacement for PHP projects. - -## Getting Started - -The homepage for **lesserphp** can be found at [http://leafo.net/lessphp/][1]. - -You can follow development at the project's [GitHub][2]. - -Including **lesserphp** in your project is as simple as dropping the single -include file into your code base and running the appropriate compile method as -described in the [PHP Interface](#php_interface). - - [1]: http://leafo.net/lessphp "lessphp homepage" - [2]: https://github.com/MarcusSchwarz/lesserphp "lesserphp GitHub page" - -## Installation - -**lesserphp** is distributed entirely in a single stand-alone file. Download the -latest version from [GitHub][2]. - -Development versions can also be downloading from GitHub. - -Place `lessphp.inc.php` in a location available to your PHP scripts, and -include it. That's it! you're ready to begin. - -## The Language - -**lesserphp** is very easy to learn because it generally functions how you would -expect it to. If you feel something is challenging or missing, feel free to -open an issue on the [bug tracker](https://github.com/MarcusSchwarz/lesserphp/issues). - -It is also easy to learn because any standards-compliant CSS code is valid LESS -code. You are free to gradually enhance your existing CSS code base with LESS -features without having to worry about rewriting anything. - -The following is a description of the new languages features provided by LESS. - -### Line Comments - -Simple but very useful; line comments are started with `//`: - - ```less - // this is a comment - body { - color: red; // as is this - /* block comments still work also */ - } - ``` - -### Variables - -Variables are identified with a name that starts with `@`. To declare a -variable, you create an appropriately named CSS property and assign it a value: - - ```less - @family: "verdana"; - @color: red; - body { - @mycolor: red; - font-family: @family; - color: @color; - border-bottom: 1px solid @color; - } - ``` - -Variable declarations will not appear in the output. Variables can be declared -in the outer most scope of the file, or anywhere else a CSS property may -appear. They can hold any CSS property value. - -Variables are only visible for use from their current scope, or any enclosed -scopes. - -If you have a string or keyword in a variable, you can reference another -variable by that name by repeating the `@`: - - ```less - @value: 20px; - @value_name: "value"; - - width: @@value_name; - ``` - -### Expressions - -Expressions let you combine values and variables in meaningful ways. For -example you can add to a color to make it a different shade. Or divide up the -width of your layout logically. You can even concatenate strings. - -Use the mathematical operators to evaluate an expression: - - ```less - @width: 960px; - .nav { - width: @width / 3; - color: #001 + #abc; - } - .body { - width: 2 * @width / 3; - font-family: "hel" + "vetica"; - } - ``` - -Parentheses can be used to control the order of evaluation. They can also be -used to force an evaluation for cases where CSS's syntax makes the expression -ambiguous. - -The following property will produce two numbers, instead of doing the -subtraction: - - ```less - margin: 10px -5px; - ``` - -To force the subtraction: - - ```less - margin: (10px -5px); - ``` - -It is also safe to surround mathematical operators by spaces to ensure that -they are evaluated: - - ```less - margin: 10px - 5px; - ``` - -Division has a special quirk. There are certain CSS properties that use the `/` -operator as part of their value's syntax. Namely, the [font][4] shorthand and -[border-radius][3]. - - [3]: https://developer.mozilla.org/en/CSS/border-radius - [4]: https://developer.mozilla.org/en/CSS/font - - -Thus, **lessphp** will ignore any division in these properties unless it is -wrapped in parentheses. For example, no division will take place here: - - ```less - .font { - font: 20px/80px "Times New Roman"; - } - ``` - -In order to force division we must wrap the expression in parentheses: - - ```less - .font { - font: (20px/80px) "Times New Roman"; - } - ``` - -If you want to write a literal `/` expression without dividing in another -property (or a variable), you can use [string unquoting](#string_unquoting): - - ```less - .var { - @size: ~"20px/80px"; - font: @size sans-serif; - } - ``` - -### Nested Blocks - -By nesting blocks we can build up a chain of CSS selectors through scope -instead of repeating them. In addition to reducing repetition, this also helps -logically organize the structure of our CSS. - - ```less - ol.list { - li.special { - border: 1px solid red; - } - - li.plain { - font-weight: bold; - } - } - ``` - - -This will produce two blocks, a `ol.list li.special` and `ol.list li.plain`. - -Blocks can be nested as deep as required in order to build a hierarchy of -relationships. - -The `&` operator can be used in a selector to represent its parent's selector. -If the `&` operator is used, then the default action of appending the parent to -the front of the child selector separated by space is not performed. - - ```less - b { - a & { - color: red; - } - - // the following have the same effect - - & i { - color: blue; - } - - i { - color: blue; - } - } - ``` - - -Because the `&` operator respects the whitespace around it, we can use it to -control how the child blocks are joined. Consider the differences between the -following: - - ```less - div { - .child-class { color: purple; } - - &.isa-class { color: green; } - - #child-id { height: 200px; } - - &#div-id { height: 400px; } - - &:hover { color: red; } - - :link { color: blue; } - } - ``` - -The `&` operator also works with [mixins](#mixins), which produces interesting results: - - ```less - .within_box_style() { - .box & { - color: blue; - } - } - - #menu { - .within_box_style; - } - ``` - -### Mixins - -Any block can be mixed in just by naming it: - - ```less - .mymixin { - color: blue; - border: 1px solid red; - - .special { - font-weight: bold; - } - } - - - h1 { - font-size: 200px; - .mymixin; - } - ``` - -All properties and child blocks are mixed in. - -Mixins can be made parametric, meaning they can take arguments, in order to -enhance their utility. A parametric mixin all by itself is not outputted when -compiled. Its properties will only appear when mixed into another block. - -The canonical example is to create a rounded corners mixin that works across -browsers: - - ```less - .rounded-corners(@radius: 5px) { - border-radius: @radius; - -webkit-border-radius: @radius; - -moz-border-radius: @radius; - } - - .header { - .rounded-corners(); - } - - .info { - background: red; - .rounded-corners(14px); - } - ``` - -If you have a mixin that doesn't have any arguments, but you don't want it to -show up in the output, give it a blank argument list: - - ```less - .secret() { - font-size: 6000px; - } - - .div { - .secret; - } - ``` - -If the mixin doesn't need any arguments, you can leave off the parentheses when -mixing it in, as seen above. - -You can also mixin a block that is nested inside other blocks. You can think of -the outer block as a way of making a scope for your mixins. You just list the -names of the mixins separated by spaces, which describes the path to the mixin -you want to include. Optionally you can separate them by `>`. - - ```less - .my_scope { - .some_color { - color: red; - .inner_block { - text-decoration: underline; - } - } - .bold { - font-weight: bold; - color: blue; - } - } - - .a_block { - .my_scope .some_color; - .my_scope .some_color .inner_block; - } - - .another_block { - // the alternative syntax - .my_scope > .bold; - } - ``` - - -#### Mixin Arguments - -When declaring a mixin you can specify default values for each argument. Any -argument left out will be given the default value specified. Here's the -syntax: - -```less -.mix(@color: red, @height: 20px, @pad: 12px) { - border: 1px solid @color; - height: @height - @pad; - padding: @pad; -} - -.default1 { - .mix(); -} - -.default2 { - .mix(blue); -} - -.default3 { - .mix(blue, 40px, 5px); -} -``` - -Additionally, you can also call a mixin using the argument names, this is -useful if you want to replace a specific argument while having all the others -take the default regardless of what position the argument appears in. The -syntax looks something like this: - - -```lessbasic -div { - .my_mixin(@paddding: 4px); // @color and @height get default values - .my_mixin(@paddding: 4px, @height: 50px); // you can specify them in any order -} -``` - -You can also combine the ordered arguments with the named ones: - -```lessbasic -div { - // @color is blue, @padding is 4px, @height is default - .my_mixin(blue, @padding: 4px); -} -``` - -Mixin arguments can be delimited with either a `,` or `;`, but only one can be -active at once. This means that each argument is separated by either `,` or -`;`. By default `,` is the delimiter, in all the above examples we used a `,`. - -A problem arises though, sometimes CSS value lists are made up with commas. In -order to be able to pass a comma separated list literal we need to use `;` as -the delimiter. (You don't need to worry about this if your list is stored in a -variable) - -If a `;` appears anywhere in the argument list, then it will be used as the -argument delimiter, and all commas we be used as part of the argument values. - -Here's a basic example: - -```less -.fancy_mixin(@box_shadow, @color: blue) { - border: 1px solid @color; - box-shadow: @box_shadow; -} - - -div { - // two arguments passed separated by ; - .fancy_mixin(2px 2px, -2px -2px; red); -} - -pre { - // one argument passed, ends in ; - .fancy_mixin(inset 4px 4px, -2px 2px;); -} - -``` - -If we only want to pass a single comma separated value we still need to use -`;`, to do this we stick it on the end as demonstrated above. - - -#### `@arguments` Variable - -Within an mixin there is a special variable named `@arguments` that contains -all the arguments passed to the mixin along with any remaining arguments that -have default values. The value of the variable has all the values separated by -spaces. - -This useful for quickly assigning all the arguments: - - ```less - .box-shadow(@x, @y, @blur, @color) { - box-shadow: @arguments; - -webkit-box-shadow: @arguments; - -moz-box-shadow: @arguments; - } - .menu { - .box-shadow(1px, 1px, 5px, #aaa); - } - ``` - -In addition to the arguments passed to the mixin, `@arguments` will also include -remaining default values assigned by the mixin: - - - ```less - .border-mixin(@width, @style: solid, @color: black) { - border: @arguments; - } - - pre { - .border-mixin(4px, dotted); - } - - ``` - - -#### Pattern Matching - -When you *mix in* a mixin, all the available mixins of that name in the current -scope are checked to see if they match based on what was passed to the mixin -and how it was declared. - -The simplest case is matching by number of arguments. Only the mixins that -match the number of arguments passed in are used. - - ```less - .simple() { // matches no arguments - height: 10px; - } - - .simple(@a, @b) { // matches two arguments - color: red; - } - - .simple(@a) { // matches one argument - color: blue; - } - - div { - .simple(10); - } - - span { - .simple(10, 20); - } - ``` - -Whether an argument has default values is also taken into account when matching -based on number of arguments: - - ```less - // matches one or two arguments - .hello(@a, @b: blue) { - height: @a; - color: @b; - } - - .hello(@a, @b) { // matches only two - width: @a; - border-color: @b; - } - - .hello(@a) { // matches only one - padding: 1em; - } - - div { - .hello(10px); - } - - pre { - .hello(10px, yellow); - } - ``` - -Additionally, a *vararg* value can be used to further control how things are -matched. A mixin's argument list can optionally end in the special argument -named `...`. The `...` may match any number of arguments, including 0. - - ```less - // this will match any number of arguments - .first(...) { - color: blue; - } - - // matches at least 1 argument - .second(@arg, ...) { - height: 200px + @arg; - } - - div { .first("some", "args"); } - pre { .second(10px); } - ``` - -If you want to capture the values that get captured by the *vararg* you can -give it a variable name by putting it directly before the `...`. This variable -must be the last argument defined. It's value is just like the special -[`@arguments` variable](#arguments_variable), a space separated list. - - - ```less - .hello(@first, @rest...) { - color: @first; - text-shadow: @rest; - } - - span { - .hello(red, 1px, 1px, 0px, white); - } - - ``` - -Another way of controlling whether a mixin matches is by specifying a value in -place of an argument name when declaring the mixin: - - ```less - .style(old, @size) { - font: @size serif; - } - - .style(new, @size) { - font: @size sans-serif; - } - - .style(@_, @size) { - letter-spacing: floor(@size / 6px); - } - - em { - @switch: old; - .style(@switch, 15px); - } - ``` - -Notice that two of the three mixins were matched. The mixin with a matching -first argument, and the generic mixin that matches two arguments. It's common -to use `@_` as the name of a variable we intend to not use. It has no special -meaning to LESS, just to the reader of the code. - -#### Guards - -Another way of restricting when a mixin is mixed in is by using guards. A guard -is a special expression that is associated with a mixin declaration that is -evaluated during the mixin process. It must evaluate to true before the mixin -can be used. - -We use the `when` keyword to begin describing a list of guard expressions. - -Here's a simple example: - - ```less - .guarded(@arg) when (@arg = hello) { - color: blue; - } - - div { - .guarded(hello); // match - } - - span { - .guarded(world); // no match - } - ``` -Only the `div`'s mixin will match in this case, because the guard expression -requires that `@arg` is equal to `hello`. - -We can include many different guard expressions by separating them by commas. -Only one of them needs to match to trigger the mixin: - - ```less - .x(@a, @b) when (@a = hello), (@b = world) { - width: 960px; - } - - div { - .x(hello, bar); // match - } - - span { - .x(foo, world); // match - } - - pre { - .x(foo, bar); // no match - } - ``` - -Instead of a comma, we can use `and` keyword to make it so all of the guards -must match in order to trigger the mixin. `and` has higher precedence than the -comma. - - ```less - .y(@a, @b) when (@a = hello) and (@b = world) { - height: 600px; - } - - div { - .y(hello, world); // match - } - - span { - .y(hello, bar); // no match - } - ``` - -Commas and `and`s can be mixed and matched. - -You can also negate a guard expression by using `not` in from of the parentheses: - - ```less - .x(@a) when not (@a = hello) { - color: blue; - } - - div { - .x(hello); // no match - } - ``` - -The `=` operator is used to check equality between any two values. For numbers -the following comparison operators are also defined: - -`<`, `>`, `=<`, `>=` - -There is also a collection of predicate functions that can be used to test the -type of a value. - -These are `isnumber`, `iscolor`, `iskeyword`, `isstring`, `ispixel`, -`ispercentage` and `isem`. - - ```less - .mix(@a) when (ispercentage(@a)) { - height: 500px * @a; - } - .mix(@a) when (ispixel(@a)) { - height: @a; - } - - div.a { - .mix(50%); - } - - div.a { - .mix(350px); - } - ``` - -#### !important - -If you want to apply the `!important` suffix to every property when mixing in a -mixin, just append `!important` to the end of the call to the mixin: - - ```less - .make_bright { - color: red; - font-weight: bold; - } - - .color { - color: green; - } - - body { - .make_bright() !important; - .color(); - } - - ``` - -### Selector Expressions - -Sometimes we want to dynamically generate the selector of a block based on some -variable or expression. We can do this by using *selector expressions*. Selector -expressions are CSS selectors that are evaluated in the current scope before -being written out. - -A simple example is a mixin that dynamically creates a selector named after the -mixin's argument: - - ```less - .create-selector(@name) { - @{name} { - color: red; - } - } - - .create-selector(hello); - .create-selector(world); - ``` - -The string interpolation syntax works inside of selectors, letting you insert varaibles. - -Here's an interesting example adapted from Twitter Bootstrap. A couple advanced -things are going on. We are using [Guards](#guards) along with a recursive -mixin to work like a loop to generate a series of CSS blocks. - - - ```less - // create our recursive mixin: - .spanX (@index) when (@index > 0) { - .span@{index} { - width: @index * 100px; - } - .spanX(@index - 1); - } - .spanX (0) {} - - // mix it into the global scopee: - .spanX(4); - ``` - -### Import - -Multiple LESS files can be compiled into a single CSS file by using the -`@import` statement. Be careful, the LESS import statement shares syntax with -the CSS import statement. If the file being imported ends in a `.less` -extension, or no extension, then it is treated as a LESS import. Otherwise it -is left alone and outputted directly: - - ```lessbasic - // my_file.less - .some-mixin(@height) { - height: @height; - } - - // main.less - @import "main.less" // will import the file if it can be found - @import "main.css" // will be left alone - - body { - .some-mixin(400px); - } - ``` - -All of the following lines are valid ways to import the same file: - - ```lessbasic - @import "file"; - @import 'file.less'; - @import url("file"); - @import url('file'); - @import url(file); - ``` - -When importing, the `importDir` is searched for files. This can be configured, -see [PHP Interface](#php_interface). - -A file is only imported once. If you try to include the same file multiple -times all the import statements after the first produce no output. - -### String Interpolation - -String interpolation is a convenient way to insert the value of a variable -right into a string literal. Given some variable named `@var_name`, you just -need to write it as `@{var_name}` from within the string to have its value -inserted: - - ```less - @symbol: ">"; - h1:before { - content: "@{symbol}: "; - } - - h2:before { - content: "@{symbol}@{symbol}: "; - } - ``` - -There are two kinds of strings, implicit and explicit strings. Explicit strings -are wrapped by double quotes, `"hello I am a string"`, or single quotes `'I am -another string'`. Implicit strings only appear when using `url()`. The text -between the parentheses is considered a string and thus string interpolation is -possible: - - ```less - @path: "files/"; - body { - background: url(@{path}my_background.png); - } - ``` - -### String Format Function - -The `%` function can be used to insert values into strings using a *format -string*. It works similar to `printf` seen in other languages. It has the -same purpose as string interpolation above, but gives explicit control over -the output format. - - ```less - @symbol: ">"; - h1:before { - content: %("%s: ", @symbol); - } - ``` - -The `%` function takes as its first argument the format string, following any -number of addition arguments that are inserted in place of the format -directives. - -A format directive starts with a `%` and is followed by a single character that -is either `a`, `d`, or `s`: - - ```less - strings: %("%a %d %s %a", hi, 1, 'ok', 'cool'); - ``` - -`%a` and `%d` format the value the same way: they compile the argument to its -CSS value and insert it directly. When used with a string, the quotes are -included in the output. This typically isn't what we want, so we have the `%s` -format directive which strips quotes from strings before inserting them. - -The `%d` directive functions the same as `%a`, but is typically used for numbers -assuming the output format of numbers might change in the future. - -### String Unquoting - -Sometimes you will need to write proprietary CSS syntax that is unable to be -parsed. As a workaround you can place the code into a string and unquote it. -Unquoting is the process of outputting a string without its surrounding quotes. -There are two ways to unquote a string. - -The `~` operator in front of a string will unquote that string: - - ```less - .class { - // a made up, but problematic vendor specific CSS - filter: ~"Microsoft.AlphaImage(src='image.png')"; - } - ``` - -If you are working with other types, such as variables, there is a built in -function that let's you unquote any value. It is called `e`. - - ```less - @color: "red"; - .class { - color: e(@color); - } - ``` - -### Built In Functions - -**lesserphp** has a collection of built in functions: - -* `e(str)` -- returns a string without the surrounding quotes. - See [String Unquoting](#string_unquoting) - -* `floor(number)` -- returns the floor of a numerical input -* `round(number, [precision])` -- returns the rounded value of numerical input with optional precision - -* `lighten(color, percent)` -- lightens `color` by `percent` and returns it -* `darken(color, percent)` -- darkens `color` by `percent` and returns it - -* `saturate(color, percent)` -- saturates `color` by `percent` and returns it -* `desaturate(color, percent)` -- desaturates `color` by `percent` and returns it - -* `fadein(color, percent)` -- makes `color` less transparent by `percent` and returns it -* `fadeout(color, percent)` -- makes `color` more transparent by `percent` and returns it - -* `spin(color, amount)` -- returns a color with `amount` degrees added to hue - -* `fade(color, amount)` -- returns a color with the alpha set to `amount` - -* `hue(color)` -- returns the hue of `color` - -* `saturation(color)` -- returns the saturation of `color` - -* `lightness(color)` -- returns the lightness of `color` - -* `alpha(color)` -- returns the alpha value of `color` or 1.0 if it doesn't have an alpha - -* `percentage(number)` -- converts a floating point number to a percentage, e.g. `0.65` -> `65%` - -* `mix(color1, color1, percent)` -- mixes two colors by percentage where 100% - keeps all of `color1`, and 0% keeps all of `color2`. Will take into account - the alpha of the colors if it exists. See - . - -* `contrast(color, dark, light)` -- if `color` has a lightness value greater - than 50% then `dark` is returned, otherwise return `light`. - -* `extract(list, index)` -- returns the `index`th item from `list`. The list is - `1` indexed, meaning the first item's index is 1, the second is 2, and etc. - -* `pow(base, exp)` -- returns `base` raised to the power of `exp` - -* `pi()` -- returns pi - -* `mod(a,b)` -- returns `a` modulus `b` - -* `tan(a)` -- returns tangent of `a` where `a` is in radians - -* `cos(a)` -- returns cosine of `a` where `a` is in radians - -* `sin(a)` -- returns sine of `a` where `a` is in radians - -* `atan(a)` -- returns arc tangent of `a` - -* `acos(a)` -- returns arc cosine of `a` - -* `asin(a)` -- returns arc sine of `a` - -* `sqrt(a)` -- returns square root of `a` - -* `rgbahex(color)` -- returns a string containing 4 part hex color. - - This is used to convert a CSS color into the hex format that IE's filter - method expects when working with an alpha component. - - ```less - .class { - @start: rgbahex(rgba(25, 34, 23, .5)); - @end: rgbahex(rgba(85, 74, 103, .6)); - // abridged example - -ms-filter: - e("gradient(start=@{start},end=@{end})"); - } - ``` - -## PHP Interface - -When working with **lesserphp** from PHP, the typical flow 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. - -Methods: - -* [`compile($string)`](#compiling[) -- Compile a string - -* [`compileFile($inFile, [$outFile])`](#compiling) -- Compile a file to another or return it - -* [`checkedCompile($inFile, $outFile)`](#compiling) -- Compile a file only if it's newer - -* [`cachedCompile($cacheOrFile, [$force])`](#compiling_automatically) -- Conditionally compile while tracking imports - -* [`setFormatter($formatterName)`](#output_formatting) -- Change how CSS output looks - -* [`setPreserveComments($keepComments)`](#preserving_comments) -- Change if comments are kept in output - -* [`registerFunction($name, $callable)`](#custom_functions) -- Add a custom function - -* [`unregisterFunction($name)`](#custom_functions) -- Remove a registered function - -* [`setVariables($vars)`](#setting_variables_from_php) -- Set a variable from PHP - -* [`unsetVariable($name)`](#setting_variables_from_php) -- Remove a PHP variable - -* [`setImportDir($dirs)`](#import_directory) -- Set the search path for imports - -* [`addImportDir($dir)`](#import_directory) -- Append directory to search path for imports - - -### Compiling - -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 - echo $less->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 - $less->checkedCompile("input.less", "output.css"); - ``` - -See [Compiling Automatically](#compiling_automatically) for a description of -the more advanced `cachedCompile` method. - -### Output Formatting - -Output formatting controls the indentation of the output CSS. Besides the -default formatter, two additional ones are included and it's also easy to make -your own. - -To use a formatter, the method `setFormatter` is used. Just -pass the name of the formatter: - - ```php - $less = new lessc; - - $less->setFormatter("compressed"); - echo $less->compile("div { color: lighten(blue, 10%) }"); - ``` - -In this example, the `compressed` formatter is used. The formatters are: - - * `lessjs` *(default)* -- Same style used in LESS for JavaScript - - * `compressed` -- Compresses all the unrequired whitespace - - * `classic` -- **lessphp**'s original formatter - -To revert to the default formatter, call `setFormatter` with a value of `null`. - -#### Custom Formatter - -The easiest way to customize the formatter is to create your own instance of an -existing formatter and alter its public properties before passing it off to -**lessphp**. The `setFormatter` method can also take an instance of a -formatter. - -Each of the formatter names corresponds to a class with `lessc_formatter_` -prepended in front of it. Here the classic formatter is customized to use tabs -instead of spaces: - - - ```php - $formatter = new lessc_formatter_classic; - $formatter->indentChar = "\t"; - - $less = new lessc; - $less->setFormatter($formatter); - echo $less->compileFile("myfile.less"); - ``` - -For more information about what can be configured with the formatter consult -the source code. - -### Preserving Comments - -By default, all comments in the source LESS file are stripped when compiling. -You might want to keep the `/* */` comments in the output though. For -example, bundling a license in the file. - -Enable or disable comment preservation by calling `setPreserveComments`: - - ```php - $less = new lessc; - $less->setPreserveComments(true); - echo $less->compile("/* hello! */"); - ``` - -Comments are disabled by default because there is additional overhead, and more -often than not they aren't needed. - - -### Compiling Automatically - -Often, you want to only compile a LESS file only if it has been modified since -last compile. This is very important because compiling is performance intensive -and you should avoid a recompile if it possible. - -The `checkedCompile` compile method will do just that. It will check if the -input file is newer than the output file, or if the output file doesn't exist -yet, and compile only then. - - ```php - $less->checkedCompile("input.less", "output.css"); - ``` - -There's a problem though. `checkedCompile` is very basic, it only checks the -input file's modification time. It is unaware of any files from `@import`. - - -For this reason we also have `cachedCompile`. It's slightly more complex, but -gives us the ability to check changes to all files including those imported. It -takes one argument, either the name of the file we want to compile, or an -existing *cache object*. Its return value is an updated cache object. - -If we don't have a cache object, then we call the function with the name of the -file to get the initial cache object. If we do have a cache object, then we -call the function with it. In both cases, an updated cache object is returned. - -The cache object keeps track of all the files that must be checked in order to -determine if a rebuild is required. - -The cache object is a plain PHP `array`. It stores the last time it compiled in -`$cache["updated"]` and output of the compile in `$cache["compiled"]`. - -Here we demonstrate creating an new cache object, then using it to see if we -have a recompiled version available to be written: - - - ```php - $inputFile = "myfile.less"; - $outputFile = "myfile.css"; - - $less = new lessc; - - // create a new cache object, and compile - $cache = $less->cachedCompile($inputFile); - - file_put_contents($outputFile, $cache["compiled"]); - - // the next time we run, write only if it has updated - $last_updated = $cache["updated"]; - $cache = $less->cachedCompile($cache); - if ($cache["updated"] > $last_updated) { - file_put_contents($outputFile, $cache["compiled"]); - } - - ``` - -In order for the system to fully work, we must save cache object between -requests. Because it's a plain PHP `array`, it's sufficient to -[`serialize`](http://php.net/serialize) it and save it the string somewhere -like a file or in persistent memory. - -An example with saving cache object to a file: - - ```php - function autoCompileLess($inputFile, $outputFile) { - // load the cache - $cacheFile = $inputFile.".cache"; - - if (file_exists($cacheFile)) { - $cache = unserialize(file_get_contents($cacheFile)); - } else { - $cache = $inputFile; - } - - $less = new lessc; - $newCache = $less->cachedCompile($cache); - - if (!is_array($cache) || $newCache["updated"] > $cache["updated"]) { - file_put_contents($cacheFile, serialize($newCache)); - file_put_contents($outputFile, $newCache['compiled']); - } - } - - autoCompileLess('myfile.less', 'myfile.css'); - ``` - -`cachedCompile` method takes an optional second argument, `$force`. Passing in -true will cause the input to always be recompiled. - -### Error Handling - -All of the compile methods will throw an `Exception` if the parsing fails or -there is a compile time error. Compile time errors include things like passing -incorrectly typed values for functions that expect specific things, like the -color manipulation functions. - - ```php - $less = new lessc; - try { - $less->compile("} invalid LESS }}}"); - } catch (Exception $ex) { - echo "lessphp fatal error: ".$ex->getMessage(); - } - ``` -### Setting Variables From PHP - -Before compiling any code you can set initial LESS variables from PHP. The -`setVariables` method lets us do this. It takes an associative array of names -to values. The values must be strings, and will be parsed into correct CSS -values. - - - ```php - $less = new lessc; - - $less->setVariables(array( - "color" => "red", - "base" => "960px" - )); - - echo $less->compile(".magic { color: @color; width: @base - 200; }"); - ``` - -If you need to unset a variable, the `unsetVariable` method is available. It -takes the name of the variable to unset. - - ```php - $less->unsetVariable("color"); - ``` - -Be aware that the value of the variable is a string containing a CSS value. So -if you want to pass a LESS string in, you're going to need two sets of quotes. -One for PHP and one for LESS. - - - ```php - $less->setVariables(array( - "url" => "'http://example.com'" - )); - - echo $less->compile("body { background: url("@{url}/bg.png"); }"); - ``` - -### Import Directory - -When running the `@import` directive, an array of directories called the import -search path is searched through to find the file being asked for. - -By default, when using `compile`, the import search path just contains `""`, -which is equivalent to the current directory of the script. If `compileFile` is -used, then the directory of the file being compiled is used as the starting -import search path. - -Two methods are available for configuring the search path. - -`setImportDir` will overwrite the search path with its argument. If the value -isn't an array it will be converted to one. - - -In this example, `@import "colors";` will look for either -`assets/less/colors.less` or `assets/bootstrap/colors.less` in that order: - - ```php - $less->setImportDir(array("assets/less/", "assets/bootstrap")); - - echo $less->compile('@import "colors";'); - ``` - -`addImportDir` will append a single path to the import search path instead of -overwriting the whole thing. - - ```php - $less->addImportDir("public/stylesheets"); - ``` - -### Custom Functions - -**lesserphp** has a simple extension interface where you can implement user -functions that will be exposed in LESS code during the compile. They can be a -little tricky though because you need to work with the **lesserphp** type system. - -The two methods we are interested in are `registerFunction` and -`unregisterFunction`. `registerFunction` takes two arguments, a name and a -callable value. `unregisterFunction` just takes the name of an existing -function to remove. - -Here's an example that adds a function called `double` that doubles any numeric -argument: - - ```php - registerFunction("double", "lessphp_double"); - - // gives us a width of 800px - echo $less->compile("div { width: double(400px); }"); - ``` - -The second argument to `registerFunction` is any *callable value* that is -understood by [`call_user_func`](http://php.net/call_user_func). - -If we are using PHP 5.3 or above then we are free to pass a function literal -like so: - - ```php - $less->registerFunction("double", function($arg) { - list($type, $value, $unit) = $arg; - return array($type, $value*2, $unit); - }); - ``` - -Now let's talk about the `double` function itself. - -Although a little verbose, the implementation gives us some insight on the type -system. All values in **lesserphp** are stored in an array where the 0th element -is a string representing the type, and the other elements make up the -associated data for that value. - -The best way to get an understanding of the system is to register is dummy -function which does a `var_dump` on the argument. Try passing the function -different values from LESS and see what the results are. - -The return value of the registered function must also be a **lesserphp** type, -but if it is a string or numeric value, it will automatically be coerced into -an appropriate typed value. In our example, we reconstruct the value with our -modifications while making sure that we preserve the original type. - -The instance of **lesserphp** itself is sent to the registered function as the -second argument in addition to the arguments array. - -## Command Line Interface - -**lesserphp** comes with a command line script written in PHP that can be used to -invoke the compiler from the terminal. On Linux and OSX, all you need to do is -place `plessc` and `lessc.inc.php` somewhere in your PATH (or you can run it in -the current directory as well). On windows you'll need a copy of `php.exe` to -run the file. To compile a file, `input.less` to CSS, run: - - ```bash - $ plessc input.less - ``` - -To write to a file, redirect standard out: - - ```bash - $ plessc input.less > output.css - ``` - -To compile code directly on the command line: - - ```bash - $ plessc -r "@color: red; body { color: @color; }" - ``` - -To watch a file for changes, and compile it as needed, use the `-w` flag: - - ```bash - $ plessc -w input-file output-file - ``` - -Errors from watch mode are written to standard out. - - -## License - -Copyright (c) 2012 Leaf Corcoran, - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -*Also under GPL3 if required, see `LICENSE` file* - diff --git a/lesserphp/lessc.inc.php b/lesserphp/lessc.inc.php deleted file mode 100644 index 4764b28..0000000 --- a/lesserphp/lessc.inc.php +++ /dev/null @@ -1,4019 +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.5.2"; - - 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"]; - - list($top, $bottom) = $this->sortProps($root->props, true); - $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); - - return array(true, $bottom, $parser, $dir); - } - - protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { - $oldSourceParser = $this->sourceParser; - - $oldImport = $this->importDir; - - // TODO: this is because the importDir api is stupid - $this->importDir = (array)$this->importDir; - array_unshift($this->importDir, $importDir); - - foreach ($props as $prop) { - $this->compileProp($prop, $block, $out); - } - - $this->importDir = $oldImport; - $this->sourceParser = $oldSourceParser; - } - - /** - * Recursively compiles a block. - * - * A block is analogous to a CSS block in most cases. A single LESS document - * is encapsulated in a block when parsed, but it does not have parent tags - * so all of it's children appear on the root level when compiled. - * - * Blocks are made up of props and children. - * - * Props are property instructions, array tuples which describe an action - * to be taken, eg. write a property, set a variable, mixin a block. - * - * The children of a block are just all the blocks that are defined within. - * This is used to look up mixins when performing a mixin. - * - * Compiling the block involves pushing a fresh environment on the stack, - * and iterating through the props, compiling each one. - * - * See lessc::compileProp() - * - */ - protected function compileBlock($block) { - switch ($block->type) { - case "root": - $this->compileRoot($block); - break; - case null: - $this->compileCSSBlock($block); - break; - case "media": - $this->compileMedia($block); - break; - case "directive": - $name = "@" . $block->name; - if (!empty($block->value)) { - $name .= " " . $this->compileValue($this->reduce($block->value)); - } - - $this->compileNestedBlock($block, array($name)); - break; - default: - $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)) { - list(, $value) = $s; - $out[] = trim($this->compileValue($this->reduce($value))); - } else { - $out[] = $s; - } - } - - return $out; - } - - protected function eq($left, $right) { - return $left == $right; - } - - protected function patternMatch($block, $orderedArgs, $keywordArgs) { - // match the guards if it has them - // any one of the groups must have all its guards pass for a match - if (!empty($block->guards)) { - $groupPassed = false; - foreach ($block->guards as $guardGroup) { - foreach ($guardGroup as $guard) { - $this->pushEnv(); - $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); - - $negate = false; - if ($guard[0] == "negate") { - $guard = $guard[1]; - $negate = true; - } - - $passed = $this->reduce($guard) == self::$TRUE; - if ($negate) $passed = !$passed; - - $this->popEnv(); - - if ($passed) { - $groupPassed = true; - } else { - $groupPassed = false; - break; - } - } - - if ($groupPassed) break; - } - - if (!$groupPassed) { - return false; - } - } - - if (empty($block->args)) { - return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); - } - - $remainingArgs = $block->args; - if ($keywordArgs) { - $remainingArgs = array(); - foreach ($block->args as $arg) { - if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { - continue; - } - - $remainingArgs[] = $arg; - } - } - - $i = -1; // no args - // try to match by arity or by argument literal - foreach ($remainingArgs as $i => $arg) { - switch ($arg[0]) { - case "lit": - if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { - return false; - } - break; - case "arg": - // no arg and no default value - if (!isset($orderedArgs[$i]) && !isset($arg[2])) { - return false; - } - break; - case "rest": - $i--; // rest can be empty - break 2; - } - } - - if ($block->isVararg) { - return true; // not having enough is handled above - } else { - $numMatched = $i + 1; - // greater than because default values always match - return $numMatched >= count($orderedArgs); - } - } - - protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) { - $matches = null; - foreach ($blocks as $block) { - // skip seen blocks that don't have arguments - if (isset($skip[$block->id]) && !isset($block->args)) { - continue; - } - - if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { - $matches[] = $block; - } - } - - return $matches; - } - - // attempt to find blocks matched by path and args - protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) { - if ($searchIn == null) return null; - if (isset($seen[$searchIn->id])) return null; - $seen[$searchIn->id] = true; - - $name = $path[0]; - - if (isset($searchIn->children[$name])) { - $blocks = $searchIn->children[$name]; - if (count($path) == 1) { - $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); - if (!empty($matches)) { - // This will return all blocks that match in the closest - // scope that has any matching block, like lessjs - return $matches; - } - } else { - $matches = array(); - foreach ($blocks as $subBlock) { - $subMatches = $this->findBlocks($subBlock, - array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); - - if (!is_null($subMatches)) { - foreach ($subMatches as $sm) { - $matches[] = $sm; - } - } - } - - return count($matches) > 0 ? $matches : null; - } - } - if ($searchIn->parent === $searchIn) return null; - return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); - } - - // sets all argument names in $args to either the default value - // or the one passed in through $values - protected function zipSetArgs($args, $orderedValues, $keywordValues) { - $assignedValues = array(); - - $i = 0; - foreach ($args as $a) { - if ($a[0] == "arg") { - if (isset($keywordValues[$a[1]])) { - // has keyword arg - $value = $keywordValues[$a[1]]; - } elseif (isset($orderedValues[$i])) { - // has ordered arg - $value = $orderedValues[$i]; - $i++; - } elseif (isset($a[2])) { - // has default value - $value = $a[2]; - } else { - $this->throwError("Failed to assign arg " . $a[1]); - $value = null; // :( - } - - $value = $this->reduce($value); - $this->set($a[1], $value); - $assignedValues[] = $value; - } else { - // a lit - $i++; - } - } - - // check for a rest - $last = end($args); - if ($last !== 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': - list(, $name, $value) = $prop; - if ($name[0] == $this->vPrefix) { - $this->set($name, $value); - } else { - $out->lines[] = $this->formatter->property($name, - $this->compileValue($this->reduce($value))); - } - break; - case 'block': - list(, $child) = $prop; - $this->compileBlock($child); - break; - case 'ruleset': - case 'mixin': - list(, $path, $args, $suffix) = $prop; - - $orderedArgs = array(); - $keywordArgs = array(); - foreach ((array)$args as $arg) { - $argval = null; - switch ($arg[0]) { - case "arg": - if (!isset($arg[2])) { - $orderedArgs[] = $this->reduce(array("variable", $arg[1])); - } else { - $keywordArgs[$arg[1]] = $this->reduce($arg[2]); - } - break; - - case "lit": - $orderedArgs[] = $this->reduce($arg[1]); - break; - default: - $this->throwError("Unknown arg type: " . $arg[0]); - } - } - - $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); - - if ($mixins === null) { - $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": - list(, $name, $value) = $prop; - $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; - break; - case "comment": - $out->lines[] = $prop[1]; - break; - case "import"; - list(, $importPath, $importId) = $prop; - $importPath = $this->reduce($importPath); - - if (!isset($this->env->imports)) { - $this->env->imports = array(); - } - - $result = $this->tryImport($importPath, $block, $out); - - $this->env->imports[$importId] = $result === false ? - array(false, "@import " . $this->compileValue($importPath).";") : - $result; - - break; - case "import_mixin": - list(,$importId) = $prop; - $import = $this->env->imports[$importId]; - if ($import[0] === false) { - if (isset($import[1])) { - $out->lines[] = $import[1]; - } - } else { - list(, $bottom, $parser, $importDir) = $import; - $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); - } - - break; - default: - $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': - list(, $num, $unit) = $value; - // [1] - the number - // [2] - the unit - if ($this->numberPrecision !== null) { - $num = round($num, $this->numberPrecision); - } - return $num . $unit; - case 'string': - // [1] - contents of string (includes quotes) - list(, $delim, $content) = $value; - foreach ($content as &$part) { - if (is_array($part)) { - $part = $this->compileValue($part); - } - } - return $delim . implode($content) . $delim; - case 'color': - // [1] - red component (either number or a %) - // [2] - green component - // [3] - blue component - // [4] - optional alpha component - list(, $r, $g, $b) = $value; - $r = round($r); - $g = round($g); - $b = round($b); - - if (count($value) == 5 && $value[4] != 1) { // rgba - return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; - } - - $h = sprintf("#%02x%02x%02x", $r, $g, $b); - - if (!empty($this->formatter->compressColors)) { - // Converting hex color to short notation (e.g. #003399 to #039) - if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { - $h = '#' . $h[1] . $h[3] . $h[5]; - } - } - - return $h; - - case 'function': - list(, $name, $args) = $value; - return $name.'('.$this->compileValue($args).')'; - default: // assumed to be unit - $this->throwError("unknown value type: $value[0]"); - } - } - - protected function lib_pow($args) { - list($base, $exp) = $this->assertArgs($args, 2, "pow"); - return array( "number", pow($this->assertNumber($base), $this->assertNumber($exp)), $args[2][0][2] ); - } - - protected function lib_pi() { - return pi(); - } - - protected function lib_mod($args) { - list($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) { - list($value, $to) = $this->assertArgs($args, 2, "convert"); - - // If it's a keyword, grab the string version instead - if( is_array( $to ) && $to[0] == "keyword" ) - $to = $to[1]; - - return $this->convert( $value, $to ); - } - - protected function lib_abs($num) { - return array( "number", abs($this->assertNumber($num)), $num[2] ); - } - - protected function lib_min($args) { - $values = $this->assertMinArgs($args, 1, "min"); - - $first_format = $values[0][2]; - - $min_index = 0; - $min_value = $values[0][1]; - - for( $a = 0; $a < sizeof( $values ); $a++ ) - { - $converted = $this->convert( $values[$a], $first_format ); - - if( $converted[1] < $min_value ) - { - $min_index = $a; - $min_value = $values[$a][1]; - } - } - - return $values[ $min_index ]; - } - - protected function lib_max($args) { - $values = $this->assertMinArgs($args, 1, "max"); - - $first_format = $values[0][2]; - - $max_index = 0; - $max_value = $values[0][1]; - - for( $a = 0; $a < sizeof( $values ); $a++ ) - { - $converted = $this->convert( $values[$a], $first_format ); - - if( $converted[1] > $max_value ) - { - $max_index = $a; - $max_value = $values[$a][1]; - } - } - - return $values[ $max_index ]; - } - - protected function lib_tan($num) { - return tan($this->assertNumber($num)); - } - - protected function lib_sin($num) { - return sin($this->assertNumber($num)); - } - - protected function lib_cos($num) { - return cos($this->assertNumber($num)); - } - - protected function lib_atan($num) { - $num = atan($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_asin($num) { - $num = asin($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_acos($num) { - $num = acos($this->assertNumber($num)); - return array("number", $num, "rad"); - } - - protected function lib_sqrt($num) { - return sqrt($this->assertNumber($num)); - } - - protected function lib_extract($value) { - list($list, $idx) = $this->assertArgs($value, 2, "extract"); - $idx = $this->assertNumber($idx); - // 1 indexed - if ($list[0] == "list" && isset($list[2][$idx - 1])) { - return $list[2][$idx - 1]; - } - } - - protected function lib_isnumber($value) { - return $this->toBool($value[0] == "number"); - } - - protected function lib_isstring($value) { - return $this->toBool($value[0] == "string"); - } - - protected function lib_iscolor($value) { - return $this->toBool($this->coerceColor($value)); - } - - protected function lib_iskeyword($value) { - return $this->toBool($value[0] == "keyword"); - } - - protected function lib_ispixel($value) { - return $this->toBool($value[0] == "number" && $value[2] == "px"); - } - - protected function lib_ispercentage($value) { - return $this->toBool($value[0] == "number" && $value[2] == "%"); - } - - protected function lib_isem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "em"); - } - - protected function lib_isrem($value) { - return $this->toBool($value[0] == "number" && $value[2] == "rem"); - } - - protected function lib_rgbahex($color) { - $color = $this->coerceColor($color); - if (is_null($color)) - $this->throwError("color expected for rgbahex"); - - return sprintf("#%02x%02x%02x%02x", - isset($color[4]) ? $color[4]*255 : 255, - $color[1],$color[2], $color[3]); - } - - protected function lib_argb($color){ - return $this->lib_rgbahex($color); - } - - /** - * Given an url, decide whether to output a regular link or the base64-encoded contents of the file - * - * @param array $value either an argument list (two strings) or a single string - * @return string formatted url(), either as a link or base64-encoded - */ - protected function lib_data_uri($value) { - $mime = ($value[0] === 'list') ? $value[2][0][2] : null; - $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; - - $fullpath = $this->findImport($url); - - if($fullpath && ($fsize = filesize($fullpath)) !== false) { - // IE8 can't handle data uris larger than 32KB - if($fsize/1024 < 32) { - if(is_null($mime)) { - if(class_exists('finfo')) { // php 5.3+ - $finfo = new finfo(FILEINFO_MIME); - $mime = explode('; ', $finfo->file($fullpath)); - $mime = $mime[0]; - } elseif(function_exists('mime_content_type')) { // PHP 5.2 - $mime = mime_content_type($fullpath); - } - } - - if(!is_null($mime)) // fallback if the mime type is still unknown - $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); - } - } - - return 'url("'.$url.'")'; - } - - // utility func to unquote a string - protected function lib_e($arg) { - switch ($arg[0]) { - case "list": - $items = $arg[2]; - if (isset($items[0])) { - return $this->lib_e($items[0]); - } - $this->throwError("unrecognised input"); - case "string": - $arg[1] = ""; - return $arg; - case "keyword": - return $arg; - default: - return array("keyword", $this->compileValue($arg)); - } - } - - protected function lib__sprintf($args) { - if ($args[0] != "list") return $args; - $values = $args[2]; - $string = array_shift($values); - $template = $this->compileValue($this->lib_e($string)); - - $i = 0; - if (preg_match_all('/%[dsa]/', $template, $m)) { - foreach ($m[0] as $match) { - $val = isset($values[$i]) ? - $this->reduce($values[$i]) : array('keyword', ''); - - // lessjs compat, renders fully expanded color, not raw color - if ($color = $this->coerceColor($val)) { - $val = $color; - } - - $i++; - $rep = $this->compileValue($this->lib_e($val)); - $template = preg_replace('/'.self::preg_quote($match).'/', - $rep, $template, 1); - } - } - - $d = $string[0] == "string" ? $string[1] : '"'; - return array("string", $d, array($template)); - } - - protected function lib_floor($arg) { - $value = $this->assertNumber($arg); - return array("number", floor($value), $arg[2]); - } - - protected function lib_ceil($arg) { - $value = $this->assertNumber($arg); - return array("number", ceil($value), $arg[2]); - } - - protected function lib_round($arg) { - if($arg[0] != "list") { - $value = $this->assertNumber($arg); - return array("number", round($value), $arg[2]); - } else { - $value = $this->assertNumber($arg[2][0]); - $precision = $this->assertNumber($arg[2][1]); - return array("number", round($value, $precision), $arg[2][0][2]); - } - } - - protected function lib_unit($arg) { - if ($arg[0] == "list") { - list($number, $newUnit) = $arg[2]; - return array("number", $this->assertNumber($number), - $this->compileValue($this->lib_e($newUnit))); - } else { - return array("number", $this->assertNumber($arg), ""); - } - } - - /** - * Helper function to get arguments for color manipulation functions. - * takes a list that contains a color like thing and a percentage - */ - public function colorArgs($args) { - if ($args[0] != 'list' || count($args[2]) < 2) { - return array(array('color', 0, 0, 0), 0); - } - list($color, $delta) = $args[2]; - $color = $this->assertColor($color); - $delta = floatval($delta[1]); - - return array($color, $delta); - } - - protected function lib_darken($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_lighten($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[3] = $this->clamp($hsl[3] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_saturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] + $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_desaturate($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - $hsl[2] = $this->clamp($hsl[2] - $delta, 100); - return $this->toRGB($hsl); - } - - protected function lib_spin($args) { - list($color, $delta) = $this->colorArgs($args); - - $hsl = $this->toHSL($color); - - $hsl[1] = $hsl[1] + $delta % 360; - if ($hsl[1] < 0) $hsl[1] += 360; - - return $this->toRGB($hsl); - } - - protected function lib_fadeout($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); - return $color; - } - - protected function lib_fadein($args) { - list($color, $delta) = $this->colorArgs($args); - $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); - return $color; - } - - protected function lib_hue($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[1]); - } - - protected function lib_saturation($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[2]); - } - - protected function lib_lightness($color) { - $hsl = $this->toHSL($this->assertColor($color)); - return round($hsl[3]); - } - - // get the alpha of a color - // defaults to 1 for non-colors or colors without an alpha - protected function lib_alpha($value) { - if (!is_null($color = $this->coerceColor($value))) { - return isset($color[4]) ? $color[4] : 1; - } - } - - // set the alpha of the color - protected function lib_fade($args) { - list($color, $alpha) = $this->colorArgs($args); - $color[4] = $this->clamp($alpha / 100.0); - return $color; - } - - protected function lib_percentage($arg) { - $num = $this->assertNumber($arg); - return array("number", $num*100, "%"); - } - - // mixes two colors by weight - // mix(@color1, @color2, [@weight: 50%]); - // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method - protected function lib_mix($args) { - if ($args[0] != "list" || count($args[2]) < 2) - $this->throwError("mix expects (color1, color2, weight)"); - - list($first, $second) = $args[2]; - $first = $this->assertColor($first); - $second = $this->assertColor($second); - - $first_a = $this->lib_alpha($first); - $second_a = $this->lib_alpha($second); - - if (isset($args[2][2])) { - $weight = $args[2][2][1] / 100.0; - } else { - $weight = 0.5; - } - - $w = $weight * 2 - 1; - $a = $first_a - $second_a; - - $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; - $w2 = 1.0 - $w1; - - $new = array('color', - $w1 * $first[1] + $w2 * $second[1], - $w1 * $first[2] + $w2 * $second[2], - $w1 * $first[3] + $w2 * $second[3], - ); - - if ($first_a != 1.0 || $second_a != 1.0) { - $new[] = $first_a * $weight + $second_a * ($weight - 1); - } - - return $this->fixColor($new); - } - - protected function lib_contrast($args) { - $darkColor = array('color', 0, 0, 0); - $lightColor = array('color', 255, 255, 255); - $threshold = 0.43; - - if ( $args[0] == 'list' ) { - $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; - $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; - $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; - 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": - list(,$inner) = $value; - return $this->lib_e($this->reduce($inner)); - case "function": - $color = $this->funcToColor($value); - if ($color) return $color; - - list(, $name, $args) = $value; - if ($name == "%") $name = "_sprintf"; - - $f = isset($this->libFunctions[$name]) ? - $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); - - if (is_callable($f)) { - if ($args[0] == 'list') - $args = self::compressList($args[2], $args[1]); - - $ret = call_user_func($f, $this->reduce($args, true), $this); - - if (is_null($ret)) { - return array("string", "", array( - $name, "(", $args, ")" - )); - } - - // convert to a typed value if the result is a php primitive - if (is_numeric($ret)) $ret = array('number', $ret, ""); - elseif (!is_array($ret)) $ret = array('keyword', $ret); - - return $ret; - } - - // plain function, reduce args - $value[2] = $this->reduce($value[2]); - return $value; - case "unary": - list(, $op, $exp) = $value; - $exp = $this->reduce($exp); - - if ($exp[0] == "number") { - switch ($op) { - case "+": - return $exp; - case "-": - $exp[1] *= -1; - return $exp; - } - } - return array("string", "", array($op, $exp)); - } - - if ($forExpression) { - switch ($value[0]) { - case "keyword": - if ($color = $this->coerceColor($value)) { - return $color; - } - break; - case "raw_color": - return $this->coerceColor($value); - } - } - - return $value; - } - - - // coerce a value for use in color operation - protected function coerceColor($value) { - switch($value[0]) { - case 'color': return $value; - case 'raw_color': - $c = array("color", 0, 0, 0); - $colorStr = substr($value[1], 1); - $num = hexdec($colorStr); - $width = strlen($colorStr) == 3 ? 16 : 256; - - for ($i = 3; $i > 0; $i--) { // 3 2 1 - $t = $num % $width; - $num /= $width; - - $c[$i] = $t * (256/$width) + $t * floor(16/$width); - } - - return $c; - case 'keyword': - $name = $value[1]; - if (isset(self::$cssColors[$name])) { - $rgba = explode(',', self::$cssColors[$name]); - - if(isset($rgba[3])) - return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); - - return array('color', $rgba[0], $rgba[1], $rgba[2]); - } - return null; - } - } - - // make something string like into a string - protected function coerceString($value) { - switch ($value[0]) { - case "string": - return $value; - case "keyword": - return array("string", "", array($value[1])); - } - return null; - } - - // turn list of length 1 into value type - protected function flattenList($value) { - if ($value[0] == "list" && count($value[2]) == 1) { - return $this->flattenList($value[2][0]); - } - return $value; - } - - public function toBool($a) { - if ($a) return self::$TRUE; - else return self::$FALSE; - } - - // evaluate an expression - protected function evaluate($exp) { - list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; - - $left = $this->reduce($left, true); - $right = $this->reduce($right, true); - - if ($leftColor = $this->coerceColor($left)) { - $left = $leftColor; - } - - if ($rightColor = $this->coerceColor($right)) { - $right = $rightColor; - } - - $ltype = $left[0]; - $rtype = $right[0]; - - // operators that work on all types - if ($op == "and") { - return $this->toBool($left == self::$TRUE && $right == self::$TRUE); - } - - if ($op == "=") { - return $this->toBool($this->eq($left, $right) ); - } - - if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { - return $str; - } - - // type based operators - $fname = "op_${ltype}_${rtype}"; - if (is_callable(array($this, $fname))) { - $out = $this->$fname($op, $left, $right); - if (!is_null($out)) return $out; - } - - // make the expression look it did before being parsed - $paddedOp = $op; - if ($whiteBefore) $paddedOp = " " . $paddedOp; - if ($whiteAfter) $paddedOp .= " "; - - return array("string", "", array($left, $paddedOp, $right)); - } - - protected function stringConcatenate($left, $right) { - if ($strLeft = $this->coerceString($left)) { - if ($right[0] == "string") { - $right[1] = ""; - } - $strLeft[2][] = $right; - return $strLeft; - } - - if ($strRight = $this->coerceString($right)) { - array_unshift($strRight[2], $left); - return $strRight; - } - } - - 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) { - $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 = ","; -} - -