diff --git a/Less.module.php b/Less.module.php index 720e138..433a766 100755 --- a/Less.module.php +++ b/Less.module.php @@ -27,7 +27,7 @@ class Less extends WireData implements Module, ConfigurableModule { public static function getModuleInfo() { return array( 'title' => 'Less', - 'version' => 5, + 'version' => 6, 'summary' => 'Less CSS preprocessor for ProcessWire using Wikimedia Less.', 'author' => 'Bernhard Baumrock, Ryan Cramer', 'icon' => 'css3', diff --git a/wikimedia/less.php/API.md b/wikimedia/less.php/API.md index 010c90f..cec44bb 100644 --- a/wikimedia/less.php/API.md +++ b/wikimedia/less.php/API.md @@ -59,7 +59,7 @@ file(s) and any direct and indirect imports. $parser = new Less_Parser(); $parser->parseFile( '/var/www/mysite/bootstrap.less', '/mysite/' ); $css = $parser->getCss(); -$files = $parser->AllParsedFiles(); +$files = $parser->getParsedFiles(); ``` #### Compress output @@ -147,7 +147,23 @@ $css = file_get_contents( '/var/www/writable_folder/' . $cssOutputFile ); #### Incremental caching -In addition to the whole-output caching described above, Less.php also has the ability to keep an internal cache which allows re-parses to be faster by effectively only re-compiling portions that have changed. +**Warning:** The incremental cache in Less_Parser is significantly slower and more memory-intense than `Less_Cache`! If you call Less.php during web requests, or otherwise call it repeatedly with files that might be unchanged, use `Less_Cache` instead. + +You can turn off the incremental cache to save memory and disk space ([ref](https://github.com/wikimedia/less.php/issues/104)). Without incremental cache, cache misses may be slower. + +```php +// Disable incremental cache +$lessFiles = [ __DIR__ . '/assets/bootstrap.less' => '/mysite/assets/' ]; +$options = [ + 'cache_dir' => '/var/www/writable_folder', + 'cache_incremental' => false, +]; +$css = Less_Cache::Get( $lessFiles, $options ); +``` + +When you instantiate Less_Parser, and call `parseFile()` or `getCss()`, it is assumed that at least one input file has changed (i.e. the whole-output cache from Less_Cache was a cache miss). The incremental cache exists to speed up cache misses on very large code bases, and is enabled by default when you call Less_Cache. + +It is inherent to the Less language that later imports may change variables or extend mixins used in earlier files, and that imports may reference variables defined by earlier imports. Thus one can't actually cache the CSS output of an import. All inputs needs to be re-compiled together to produce the correct CSS output. The incremental cache merely allows the `parseFile()` method to skip parsing for unchanged files (i.e. interpreting Less syntax into an object structure). The `getCss()` method will still traverse and compile the representation of all input files. ## Source maps diff --git a/wikimedia/less.php/CHANGES.md b/wikimedia/less.php/CHANGES.md index e24cb22..8e80a7a 100644 --- a/wikimedia/less.php/CHANGES.md +++ b/wikimedia/less.php/CHANGES.md @@ -1,6 +1,181 @@ # Changelog -## 4.1.1 +## v5.4.0 + +Added: +* Add support for Logical Functions `if()` and `boolean()`. (Hannah Okwelum) [T393383](https://phabricator.wikimedia.org/T393383) + +Changed: +* Remove support for PHP 7.4 and 8.0. Raise requirement to PHP 8.1+. (James D. Forrester) + +## v5.3.1 + +Fixed: +* Fix `PHP Warning: Undefined property $value` in `extract()`. (Timo Tijhof) [T391735](https://phabricator.wikimedia.org/T391735) + +## v5.3.0 + +Added: +* Less_Parser: Add `cache_incremental` option. Set this to false via `Less_Cache` to use the fast whole-output cache without the memory-intensive incremental cache. (Timo Tijhof) + +Deprecated: +* Deprecate `Less_Cache::CheckCacheDir()` as public method. This is called automatically. +* Deprecate `Less_Cache::CleanCache()` as public method. This is called automatically. +* Deprecate `Less_Parser::SetCacheDir()`. Set the `cache_dir` option, or use `Less_Cache::SetCacheDir()` instead. + +## v5.2.2 + +Fixed: +* Fix ParseError on CSS variable when there is no trailing semicolon (Hannah Okwelum) [T386077](https://phabricator.wikimedia.org/T386077) +* Support functions calls in CSS variable value after first comma (Hannah Okwelum) [T386079](https://phabricator.wikimedia.org/T386079) + +## v5.2.1 + +Fixed: +* Fix un-parenthesized nested operation via a variable (Hannah Okwelum) [T386074](https://phabricator.wikimedia.org/T386074) +* Faster Less_Visitor_joinSelector by skipping Declaration blocks (ubermanu) + +## v5.2.0 + +Added: +* Add support for BrianHenryIE/strauss codemod in Less_Visitor (Stefan Warnat) + +Fixed: +* Fix "PHP Warning: Undefined array key currentUri" when using `@import (inline)` (tck) [T380641](https://phabricator.wikimedia.org/T380641) +* Fix "Implicit conversion from float to int" PHP 8.1 warning when using `hsv()` (Peter Knut) +* Less_Visitor: Faster class mapping in `visitObj` by using inline cache (Thiemo Kreuz) + +## v5.1.2 + +Fixed: +* Less_Functions: Fix "Implicitly nullable parameter" PHP 8.4 warning (Reedy) [T376276](https://phabricator.wikimedia.org/T376276) + +## v5.1.1 + +Fixed: +* Fix compiling of PHP-injected variables with false, null or empty string (Hannah Okwelum) + +## v5.1.0 + +Added: +* Add support for property acessors (Piotr Miazga) [T368408](https://phabricator.wikimedia.org/T368408) +* Increase parsing flexibility around at-rule blocks and custom properties (Piotr Miazga) [T368408](https://phabricator.wikimedia.org/T368408) +* Add support for Namespaces and Accessors (Piotr Miazga) [T368409](https://phabricator.wikimedia.org/T368409) + +Fixed: +* Fix parse error when opacity is set to zero in `alpha()` function (Hannah Okwelum) [T371606](https://phabricator.wikimedia.org/T371606) + +## v5.0.0 + +Added: +* Add support for Lessjs 3.5.0 `calc()` exception (Piotr Miazga) [T367186](https://phabricator.wikimedia.org/T367186) +* Add support for CSS Grid syntax (Dringsim) [T288498](https://phabricator.wikimedia.org/T288498) +* Add support for `\9` escapes in CSS keyword (Dringsim) [T288498](https://phabricator.wikimedia.org/T288498) +* Add Less_Parser "math" option, renamed from strictMath (Hannah Okwelum) [T366445](https://phabricator.wikimedia.org/T366445) + +Changed: +* Change Less_Parser "math" default from "always" to "parens-division" (Hannah Okwelum) [T366445](https://phabricator.wikimedia.org/T366445) +* Change `Less_Version::less_version` to "3.13.3". This end compatibility support of Less.js 2.5.3. + Less.php 5.0 and later will target Less.js 3.13.1 behaviour instead. (Piotr Miazga) + +Removed: +* Remove `import_callback` Less_Parser option (Hannah Okwelum) +* Remove backtick evaluation inside quoted strings (Bartosz Dziewoński) +* Remove `Less_Parser::AllParsedFiles()` (Hannah Okwelum) +* Remove Less_Parser->SetInput() public method, now private (Hannah Okwelum) +* Remove Less_Parser->CacheFile() public method, now private (Hannah Okwelum) +* Remove Less_Parser->UnsetInput() public method, now private (Hannah Okwelum) +* Remove Less_Parser->save() public method, now private (Hannah Okwelum) + +## v4.4.1 + +Fixed: +* Update `Less_Version::version` and bump `Less_Version::cache_version` (Timo Tijhof) + +## v4.4.0 + +Added: +* Add `image-size()` function, disable base64 for SVG `data-uri()` (Hannah Okwelum) [T353147](https://phabricator.wikimedia.org/T353147) +* Improve support for preserving `!important` via variables (Piotr Miazga) [T362341](https://phabricator.wikimedia.org/T362341) +* Add support for include path inside `data-uri()` (Hannah Okwelum) [T364871](https://phabricator.wikimedia.org/T364871) + +Changed, to match Less.js 2.5.3: +* Fix multiplication of mixed units to preserve the first unit (Piotr Miazga) [T362341](https://phabricator.wikimedia.org/T362341) + +Fixed: +* Fix checking of guard conditions in nested mixins (Hannah Okwelum) [T352867](https://phabricator.wikimedia.org/T352867) +* Less_Functions: Avoid clobbering `clamp()` with internal helper (Timo Tijhof) [T363728](https://phabricator.wikimedia.org/T363728) + +## v4.3.0 + +Added: +* Support interpolated variable imports, via ImportVisitor (Hannah Okwelum) [T353133](https://phabricator.wikimedia.org/T353133) +* Support rulesets as default values of a mixin parameter (Hannah Okwelum) [T353143](https://phabricator.wikimedia.org/T353143) +* Support `...` expand operator in mixin calls (Piotr Miazga) [T352897](https://phabricator.wikimedia.org/T352897) +* Improve support for `@import (reference)` matching Less.js 2.x (Hannah Okwelum) [T362647](https://phabricator.wikimedia.org/T362647) + +Changed: +* Improve `mix()` argument exception message to mention given arg type (Timo Tijhof) +* The `Less_Tree_Import->getPath()` method now reflects the path as written in the source code, + without auto-appended `.less` suffix, matching upstream Less.js 2.5.3 behaviour. + This internal detail is exposed via the deprecated `import_callback` parser option. + It is recommended to migrate to `Less_Parser->SetImportDirs`, which doesn't expose internals, + and is unaffected by this change. + +Deprecated: +* Deprecate `import_callback` Less_Parser option. Use `Less_Parser->SetImportDirs` with callback instead. +* Deprecate `Less_Parser->SetInput()` as public method. Use `Less_Parser->parseFile()` instead. +* Deprecate `Less_Parser->CacheFile()` as public method. Use `Less_Cache` API instead. +* Deprecate `Less_Parser::AllParsedFiles()` as static method. Use `Less_Parser->getParsedFiles()` instead. +* Deprecate `Less_Parser->UnsetInput()` as public method, considered internal. +* Deprecate `Less_Parser->save()` as public method, considered internal. + +Fixed: +* Fix `replace()` when passed multiple replacements (Roan Kattouw) [T358631](https://phabricator.wikimedia.org/T358631) +* Fix unexpected duplicating of uncalled mixin rules (Hannah Okwelum) [T363076](https://phabricator.wikimedia.org/T363076) +* Fix ParseError for comments after rule name or in `@keyframes` (Piotr Miazga) [T353131](https://phabricator.wikimedia.org/T353131) +* Fix ParseError for comments in more places and preserve them (Piotr Miazga) [T353132](https://phabricator.wikimedia.org/T353132) +* Fix ParseError effecting pseudo classes with `when` guards (Piotr Miazga) [T353144](https://phabricator.wikimedia.org/T353144) +* Fix preservation of units in some cases (Timo Tijhof) [T360065](https://phabricator.wikimedia.org/T360065) +* Less_Parser: Faster matching by inlining `matcher()` chains (Timo Tijhof) +* Less_Parser: Faster matching with `matchStr()` method (Timo Tijhof) + +## v4.2.1 + +Added: +* Add support for `/deep/` selectors (Hannah Okwelum) [T352862](https://phabricator.wikimedia.org/T352862) + +Fixed: +* Fix ParseError in some division expressions (Hannah Okwelum) [T358256](https://phabricator.wikimedia.org/T358256) +* Fix `when()` matching between string and non-string (Timo Tijhof) [T358159](https://phabricator.wikimedia.org/T358159) +* Preserve whitespace before `;` or `!` in simple rules (Hannah Okwelum) [T352911](https://phabricator.wikimedia.org/T352911) + +## v4.2.0 + +Added: +* Add `isruleset()` function (Hannah Okwelum) [T354895](https://phabricator.wikimedia.org/T354895) +* Add source details to "Operation on an invalid type" error (Hannah Okwelum) [T344197](https://phabricator.wikimedia.org/T344197) +* Add support for `method=relative` parameter in color functions (Hannah Okwelum) [T354895](https://phabricator.wikimedia.org/T354895) +* Add support for comments in variables and function parameters (Hannah Okwelum) [T354895](https://phabricator.wikimedia.org/T354895) +* Less_Parser: Add `functions` parser option API (Hannah Okwelum) + +Changed, to match Less.js 2.5.3: +* Preserve original color keywords and shorthand hex (Hannah Okwelum) [T352866](https://phabricator.wikimedia.org/T352866) + +Fixed: +* Fix PHP Warning when using a dynamic variable name like `@@name` (Hannah Okwelum) [T352830](https://phabricator.wikimedia.org/T352830) +* Fix PHP Warning when `@extend` path contains non-quoted attribute (Gr8b) [T349433](https://phabricator.wikimedia.org/T349433) +* Less_Parser: Faster `skipWhitespace` by using native `strspn` (Umherirrender) +* Less_Parser: Fix Less_Tree_JavaScript references to consistently be in camel-case (Stefan Fröhlich) +* Fix `!important` in nested mixins (Hannah Okwelum) [T353141](https://phabricator.wikimedia.org/T353141) +* Fix crash when using recursive mixins (Timo Tijhof) [T352829](https://phabricator.wikimedia.org/T352829) +* Fix disappearing selectors in certain nested blocks (Hannah Okwelum) [T352859](https://phabricator.wikimedia.org/T352859) +* Fix Less_Exception_Compiler when passing unquoted value to `color()` (Hannah Okwelum) [T353289](https://phabricator.wikimedia.org/T353289) +* Fix order of comments in `@font-face` blocks (Timo Tijhof) [T356706](https://phabricator.wikimedia.org/T356706) +* Fix string comparison to ignore quote type (Timo Tijhof) [T357160](https://phabricator.wikimedia.org/T357160) +* Fix string interpolation in selectors (Hannah Okwelum) [T353142](https://phabricator.wikimedia.org/T353142) + +## v4.1.1 * Less_Parser: Faster `MatchQuoted` by using native `strcspn`. (Thiemo Kreuz) * Less_Parser: Faster `parseEntitiesQuoted` by inlining `MatchQuoted`. (Thiemo Kreuz) @@ -8,12 +183,12 @@ * Less_Tree_Mixin_Call: Include mixin name in error message (Jeremy P) * Fix mismatched casing in class names to fix autoloading on case-sensitive filesystems (Jeremy P) -## 4.1.0 +## v4.1.0 * Add support for `@supports` blocks. (Anne Tomasevich) [T332923](http://phabricator.wikimedia.org/T332923) * Less_Parser: Returning a URI from `SetImportDirs()` callbacks is now optional. (Timo Tijhof) -## 4.0.0 +## v4.0.0 * Remove support for PHP 7.2 and 7.3. Raise requirement to PHP 7.4+. * Remove support for `cache_method=php` and `cache_method=var_export`, only the faster and more secure `cache_method=serialize` is now available. The built-in cache remains disabled by default. @@ -21,14 +196,14 @@ * Fix "Undefined property" PHP 8.1 warning when `calc()` is used with CSS `var()`. [T331688](https://phabricator.wikimedia.org/T331688) * Less_Parser: Improve performance by removing MatchFuncs and NewObj overhead. (Timo Tijhof) -## 3.2.1 +## v3.2.1 * Tree_Ruleset: Fix support for nested parent selectors (Timo Tijhof) [T204816](https://phabricator.wikimedia.org/T204816) * Fix ParseError when interpolating variable after colon in selector (Timo Tijhof) [T327163](https://phabricator.wikimedia.org/T327163) * Functions: Fix "Undefined property" warning on bad minmax arg * Tree_Call: Include previous exception when catching functions (Robert Frunzke) -## 3.2.0 +## v3.2.0 * Fix "Implicit conversion" PHP 8.1 warnings (Ayokunle Odusan) * Fix "Creation of dynamic property" PHP 8.2 warnings (Bas Couwenberg) @@ -36,33 +211,33 @@ * Tree_Url: Add support for "Url" type to `Parser::getVariables()` (ciroarcadio) [#51](https://github.com/wikimedia/less.php/pull/51) * Tree_Import: Add support for importing URLs without file extension (Timo Tijhof) [#27](https://github.com/wikimedia/less.php/issues/27) -## 3.1.0 +## v3.1.0 * Add 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) -## 3.0.0 +## v3.0.0 * Raise PHP requirement from 7.1 to 7.2.9 (James Forrester) -## 2.0.0 +## 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 +## v1.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) * Fix test suite on PHP 7.4 (Sergei Morozov) -## 1.8.1 +## v1.8.1 * Another PHP 7.3 compatibility tweak -## 1.8.0 +## v1.8.0 Library forked by Wikimedia, from [oyejorge/less.php](https://github.com/oyejorge/less.php). @@ -70,21 +245,21 @@ Library forked by Wikimedia, from [oyejorge/less.php](https://github.com/oyejorg * 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 +## v1.7.0.13 * Fix composer.json (PSR-4 was invalid) -## 1.7.0.12 +## v1.7.0.12 * set bin/lessc bit executable * Add `gettingVariables` method to `Less_Parser` -## 1.7.0.11 +## 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 +## v1.7.0.10 * Add indentation option * Add `optional` modifier for `@import` @@ -93,7 +268,7 @@ Library forked by Wikimedia, from [oyejorge/less.php](https://github.com/oyejorg * urlArgs should be string no array() * fix missing on NameValue type [#269](https://github.com/oyejorge/less.php/issues/269) -## 1.7.0.9 +## v1.7.0.9 * Remove space at beginning of Version.php * Revert require() paths in test interface diff --git a/wikimedia/less.php/CODE_OF_CONDUCT.md b/wikimedia/less.php/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..498acf7 --- /dev/null +++ b/wikimedia/less.php/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct). diff --git a/wikimedia/less.php/CONTRIBUTING.md b/wikimedia/less.php/CONTRIBUTING.md new file mode 100644 index 0000000..8c588ab --- /dev/null +++ b/wikimedia/less.php/CONTRIBUTING.md @@ -0,0 +1,79 @@ +# Maintainers guide + +## Release process + +1. **Changelog.** Add a new section to the top of `CHANGES.md` with the output from `composer changelog`. + + Edit your new section by following the [Keep a changelog](https://keepachangelog.com/en/1.0.0/) conventions, where by bullet points are under one of the "Added", "Changed", "Fixed", "Deprecated", or "Removed" labels. + + Review each point and make sure it is phrased in a way that explains the impact on end-users of the library. If the change does not affect the public API or CSS output, remove the bullet point. + +2. **Version bump.** Update `/lib/Less/Version.php` and set `version` to the version that you're about to release. Also increase `cache_version` to increment the last number. + +3. **Commit.** Stage and commit your changes with the message `Tag vX.Y.Z`, and then push the commit for review. + +4. **Tag.** After the above release commit is merged, checkout the master branch and pull down the latest changes. Then create a `vX.Y.Z` tag and push the tag. + + Remember to, after the commit is merged, first checkout the master branch and pull down the latest changes. This is to make sure you have the merged version and not the draft commit that you pushed for review. + +## Internal overview + +This is an overview of the high-level steps during the transformation +from Less to CSS, and how they compare between Less.js and Less.php. + +Less.js: + +* `less.render(input, { paths: … })` + * `Parser.parse` normalizes input + * `Parser.parse` parses input into rules via `parsers.primary` + * `Parser.parse` creates the "root" ruleset object + * `Parser.parse` applies ImportVisitor + * `ImportVisitor` applies these steps to each `Import` node: + * `ImportVisitor#processImportNode` + * `Import#evalForImport` + * `ImportVisitor` ends with `ImporVisitor#tryRun` loop (async, after last call to `ImportVisitor#onImported`. +* `less.render` callback + * `ParseTree.prototype.toCSS` + * `transformTree` applies pre-visitors, compiles all rules, and applies post-visitors. + * `ParseTree.prototype.toCSS` runs toCSS transform on the "root" ruleset. +* CSS result ready! + +Less.php + +* `Less_Parser->parseFile` + * `Less_Parser->_parse` + * `Less_Parser->GetRules` normalizes input (via `Less_Parser->SetInput`) + * `Less_Parser->GetRules` parses input into rules via `Less_Parser->parsePrimary` +* `Less_Parser->getCss` + * `Less_Parser->getCss` creates the "root" ruleset object + * `Less_Parser->getCss` applies Less_ImportVisitor + * `Less_ImportVisitor` applies these steps to each `Import` node: + * `ImportVisitor->processImportNode` + * `Less_Tree_Import->compileForImport` + * `ImportVisitor` ends with `ImporVisitor#tryRun` loop (all sync, no async needed). + * `Less_Parser->getCss` applies pre-visitors, compiles all rules, and applies post-visitors. + * `Less_Parser->getCss` runs toCSS transform on the "root" ruleset. +* CSS result ready! + +## Compatibility + +The `wikimedia/less.php` package inherits a long history of loosely compatible +and interchangable Less compilers written in PHP. + +Starting with less.php v3.2.1 (released in 2023), the public API is more clearly +documented, and internal code is now consistently marked `@private`. + +The public API includes the `Less_Parser` class and several of its public methods. +For legacy reasons, some of its internal methods remain public. Maintainers must +take care to search the following downstream applications when changing or +removing public methods. If a method has one or more references in the below +codebases, treat it as a breaking change and document a migration path in the +commit message (and later in CHANGES.md), even if the method was undocumented +or feels like it is for internal use only. + +* [MediaWiki (source code)](https://codesearch.wmcloud.org/core/?q=Less_Parser&files=php%24) +* [Matomo (source code)](https://github.com/matomo-org/matomo/blob/5.0.2/core/AssetManager/UIAssetMerger/StylesheetUIAssetMerger.php) +* [Adobe Magento (source code)](https://github.com/magento/magento2/blob/2.4.6/lib/internal/Magento/Framework/Css/PreProcessor/Adapter/Less/Processor.php) +* [Shopware 5 (source code)](https://github.com/shopware5/shopware/blob/5.7/engine/Shopware/Components/Theme/LessCompiler/Oyejorge.php) +* [Winter CMS Assetic (source code)](https://github.com/assetic-php/assetic/tree/v3.1.0/src/Assetic/Filter) +* [Flarum Framework (source code)](https://github.com/flarum/framework) diff --git a/wikimedia/less.php/README.md b/wikimedia/less.php/README.md index 3d76ec7..c0a1dd8 100644 --- a/wikimedia/less.php/README.md +++ b/wikimedia/less.php/README.md @@ -7,7 +7,7 @@ This is a PHP port of the [official LESS processor](https://lesscss.org). ## About -The code structure of Less.php mirrors that of upstream Less.js to ensure compatibility and help reduce maintenance. The port is currently compatible with Less.js 2.5.3. Please note that "inline JavaScript expressions" (via eval or backticks) are not supported. +The code structure of Less.php mirrors that of upstream Less.js to ensure compatibility and help reduce maintenance. The port aims to be compatible with Less.js 3.13.1. Please note that "inline JavaScript expressions" (via eval or backticks) are not supported. * [API § Caching](./API.md#caching), Less.php includes a file-based cache. * [API § Source maps](./API.md#source-maps), Less.php supports v3 sourcemaps. @@ -43,10 +43,12 @@ _See also [SECURITY](./SECURITY.md)._ ## Who uses Less.php? * **[Wikipedia](https://en.wikipedia.org/wiki/MediaWiki)** and the MediaWiki platform ([docs](https://www.mediawiki.org/wiki/ResourceLoader/Architecture#Resource:_Styles)). -* **[Matomo](https://en.wikipedia.org/wiki/Matomo_(software))** ([docs](https://devdocs.magento.com/guides/v2.4/frontend-dev-guide/css-topics/custom_preprocess.html)). -* **[Magento](https://en.wikipedia.org/wiki/Magento)** as part of Adobe Commerce ([docs](https://developer.matomo.org/guides/asset-pipeline#vanilla-javascript-css-and-less-files)). +* **[Matomo](https://en.wikipedia.org/wiki/Matomo_(software))** ([docs](https://developer.matomo.org/guides/asset-pipeline#vanilla-javascript-css-and-less-files)). +* **[Magento](https://en.wikipedia.org/wiki/Magento)** as part of Adobe Commerce ([docs](https://developer.adobe.com/commerce/frontend-core/guide/css/preprocess/)). * **[Icinga](https://en.wikipedia.org/wiki/Icinga)** in Icinga Web ([docs](https://github.com/Icinga/icingaweb2)). * **[Shopware](https://de.wikipedia.org/wiki/Shopware)** ([docs](https://developers.shopware.com/designers-guide/less/)). +* **[Winter CMS](https://wintercms.com/)** ([docs](https://wintercms.com/docs/v1.2/docs/themes/development)) +* **[Flarum](https://en.wikipedia.org/wiki/Flarum)** ([docs](https://docs.flarum.org/themes/)) ## Integrations diff --git a/wikimedia/less.php/lessc.inc.php b/wikimedia/less.php/lessc.inc.php index 419398e..fb731a8 100644 --- a/wikimedia/less.php/lessc.inc.php +++ b/wikimedia/less.php/lessc.inc.php @@ -14,13 +14,20 @@ class lessc { + /** @var string */ public static $VERSION = Less_Version::less_version; + /** @var string|string[] */ public $importDir = ''; + /** @var array */ protected $allParsedFiles = []; + /** @var array */ protected $libFunctions = []; + /** @var array */ protected $registeredVars = []; + /** @var string */ private $formatterName; + /** @var array */ private $options = []; public function __construct( $lessc = null, $sourceName = null ) { @@ -129,8 +136,7 @@ public function compile( $string, $name = null ) { $parser->parse( $string ); $out = $parser->getCss(); - $parsed = Less_Parser::AllParsedFiles(); - foreach ( $parsed as $file ) { + foreach ( $parser->getParsedFiles() as $file ) { $this->addParsedFile( $file ); } @@ -165,8 +171,7 @@ public function compileFile( $fname, $outFname = null ) { $parser->parseFile( $fname ); $out = $parser->getCss(); - $parsed = Less_Parser::AllParsedFiles(); - foreach ( $parsed as $file ) { + foreach ( $parser->getParsedFiles() as $file ) { $this->addParsedFile( $file ); } diff --git a/wikimedia/less.php/lib/Less/.easymin/ignore_prefixes b/wikimedia/less.php/lib/Less/.easymin/ignore_prefixes deleted file mode 100644 index ca953b2..0000000 --- a/wikimedia/less.php/lib/Less/.easymin/ignore_prefixes +++ /dev/null @@ -1,2 +0,0 @@ -.easymin -Autoloader.php diff --git a/wikimedia/less.php/lib/Less/Autoloader.php b/wikimedia/less.php/lib/Less/Autoloader.php index 4309c21..ef5fee0 100644 --- a/wikimedia/less.php/lib/Less/Autoloader.php +++ b/wikimedia/less.php/lib/Less/Autoloader.php @@ -5,16 +5,14 @@ */ class Less_Autoloader { - /** @var bool */ - protected static $registered = false; + protected static bool $registered = false; /** * Register the autoloader in the SPL autoloader * - * @return void * @throws Exception If there was an error in registration */ - public static function register() { + public static function register(): void { if ( self::$registered ) { return; } @@ -28,10 +26,8 @@ public static function register() { /** * Unregister the autoloader - * - * @return void */ - public static function unregister() { + public static function unregister(): void { spl_autoload_unregister( [ __CLASS__, 'loadClass' ] ); self::$registered = false; } @@ -41,9 +37,9 @@ public static function unregister() { * * @param string $className The class to load */ - public static function loadClass( $className ) { + public static function loadClass( string $className ): void { // handle only package classes - if ( strpos( $className, 'Less_' ) !== 0 ) { + if ( !str_starts_with( $className, 'Less_' ) ) { return; } @@ -51,7 +47,6 @@ public static function loadClass( $className ) { $fileName = __DIR__ . DIRECTORY_SEPARATOR . str_replace( '_', DIRECTORY_SEPARATOR, $className ) . '.php'; require $fileName; - return true; } } diff --git a/wikimedia/less.php/lib/Less/Cache.php b/wikimedia/less.php/lib/Less/Cache.php index a142d19..cc25beb 100644 --- a/wikimedia/less.php/lib/Less/Cache.php +++ b/wikimedia/less.php/lib/Less/Cache.php @@ -20,6 +20,9 @@ class Less_Cache { */ public static $gc_lifetime = 604800; + /** @var bool */ + private static $gc_done = false; + /** * Save and reuse the results of compiled less files. * The first call to Get() will generate css and save it. @@ -56,7 +59,7 @@ public static function Get( $less_files, $parser_options = [], $modify_vars = [] throw new Exception( 'prefix_vars not set' ); } - self::CheckCacheDir(); + self::$cache_dir = self::CheckCacheDir(); $less_files = (array)$less_files; // create a file for variables @@ -113,7 +116,12 @@ public static function Get( $less_files, $parser_options = [], $modify_vars = [] file_put_contents( $output_file, $compiled ); // clean up - self::CleanCache(); + // Garbage collection can be slow, so run it only on cache misses, + // and at most once per process. + if ( !self::$gc_done ) { + self::$gc_done = true; + self::CleanCache(); + } return basename( $output_file ); } @@ -139,7 +147,7 @@ public static function Cache( &$less_files, $parser_options = [] ) { 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 ) { + if ( str_contains( $uri_or_less, "\n" ) ) { $parser->Parse( $uri_or_less ); continue; } @@ -149,7 +157,7 @@ public static function Cache( &$less_files, $parser_options = [] ) { $compiled = $parser->getCss(); - $less_files = $parser->AllParsedFiles(); + $less_files = $parser->getParsedFiles(); return $compiled; } @@ -180,45 +188,46 @@ private static function CompiledName( $files, $extrahash ) { } public static function SetCacheDir( $dir ) { - self::$cache_dir = $dir; - self::CheckCacheDir(); + self::$cache_dir = self::CheckCacheDir( $dir ); } - public static function CheckCacheDir() { - self::$cache_dir = str_replace( '\\', '/', self::$cache_dir ); - self::$cache_dir = rtrim( self::$cache_dir, '/' ) . '/'; - - if ( !file_exists( self::$cache_dir ) ) { - if ( !mkdir( self::$cache_dir ) ) { - throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: ' . self::$cache_dir ); + /** + * @deprecated since 5.3.0 Internal for use by Less_Cache and Less_Parser only. + */ + public static function CheckCacheDir( $dir = null ) { + $dir ??= self::$cache_dir; + $dir = Less_Parser::WinPath( $dir ); + $dir = rtrim( $dir, '/' ) . '/'; + + if ( !file_exists( $dir ) ) { + if ( !mkdir( $dir ) ) { + throw new Less_Exception_Parser( 'Less.php cache directory couldn\'t be created: ' . $dir ); } - } elseif ( !is_dir( self::$cache_dir ) ) { - throw new Less_Exception_Parser( 'Less.php cache directory doesn\'t exist: ' . self::$cache_dir ); - - } elseif ( !is_writable( self::$cache_dir ) ) { - throw new Less_Exception_Parser( 'Less.php cache directory isn\'t writable: ' . self::$cache_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 ); } + + return $dir; } /** - * Delete unused less.php files + * @deprecated since 5.3.0 Called automatically. Internal for use by Less_Cache and Less_Parser only. */ - public static function CleanCache() { - static $clean = false; - - if ( $clean || empty( self::$cache_dir ) ) { + public static function CleanCache( $dir = null ) { + $dir ??= self::$cache_dir; + if ( !$dir ) { return; } - $clean = true; - // only remove files with extensions created by less.php // css files removed based on the list files - $remove_types = [ 'lesscache' => 1,'list' => 1,'less' => 1,'map' => 1 ]; + $remove_types = [ 'lesscache' => 1, 'list' => 1, 'less' => 1, 'map' => 1 ]; - $files = scandir( self::$cache_dir ); + $files = scandir( $dir ); if ( !$files ) { return; } @@ -227,7 +236,7 @@ public static function CleanCache() { foreach ( $files as $file ) { // don't delete if the file wasn't created with less.php - if ( strpos( $file, self::$prefix ) !== 0 ) { + if ( !str_starts_with( $file, self::$prefix ) ) { continue; } @@ -238,8 +247,8 @@ public static function CleanCache() { continue; } - $full_path = self::$cache_dir . $file; - $mtime = filemtime( $full_path ); + $fullPath = $dir . $file; + $mtime = filemtime( $fullPath ); // don't delete if it's a relatively new file if ( $mtime > $check_time ) { @@ -248,16 +257,16 @@ public static function CleanCache() { // delete the list file and associated css file if ( $type === 'list' ) { - self::ListFiles( $full_path, $list, $css_file_name ); + self::ListFiles( $fullPath, $list, $css_file_name ); if ( $css_file_name ) { - $css_file = self::$cache_dir . $css_file_name; + $css_file = $dir . $css_file_name; if ( file_exists( $css_file ) ) { unlink( $css_file ); } } } - unlink( $full_path ); + unlink( $fullPath ); } } diff --git a/wikimedia/less.php/lib/Less/Environment.php b/wikimedia/less.php/lib/Less/Environment.php index f419585..4f3fe8e 100644 --- a/wikimedia/less.php/lib/Less/Environment.php +++ b/wikimedia/less.php/lib/Less/Environment.php @@ -20,23 +20,54 @@ class Less_Environment { * @var array */ public $frames = []; + /** @var array */ + public $importantScope = []; + /** @var bool */ + public $inCalc = false; + /** @var bool */ + public $mathOn = true; + + /** @var true[] */ + private $calcStack = []; /** @var Less_Tree_Media[] */ public $mediaBlocks = []; /** @var Less_Tree_Media[] */ public $mediaPath = []; - public static $parensStack = 0; + /** @var string[] */ + public $imports = []; + + /** + * This is the equivalent of `importVisitor.onceFileDetectionMap` + * as used by the dynamic `importNode.skip` function. + * + * @see less-2.5.3.js#ImportVisitor.prototype.onImported + * @var array + */ + public $importVisitorOnceMap = []; + /** @var int */ public static $tabLevel = 0; + /** @var bool */ public static $lastRule = false; - public static $_outputMap; + /** @var array */ + public static $_noSpaceCombinators; + /** @var int */ public static $mixin_stack = 0; - public static $mathOn = true; + /** @var int */ + public $math = self::MATH_PARENS_DIVISION; + + /** @var true[] */ + public $parensStack = []; + + public const MATH_ALWAYS = 0; + public const MATH_PARENS_DIVISION = 1; + public const MATH_PARENS = 2; /** * @var array @@ -44,61 +75,80 @@ class Less_Environment { public $functions = []; public function Init() { - self::$parensStack = 0; self::$tabLevel = 0; self::$lastRule = false; self::$mixin_stack = 0; - if ( Less_Parser::$options['compress'] ) { - - self::$_outputMap = [ - ',' => ',', - ': ' => ':', - '' => '', - ' ' => ' ', - ':' => ' :', - '+' => '+', - '~' => '~', - '>' => '>', - '|' => '|', - '^' => '^', - '^^' => '^^' - ]; - - } else { - - self::$_outputMap = [ - ',' => ', ', - ': ' => ': ', - '' => '', - ' ' => ' ', - ':' => ' :', - '+' => ' + ', - '~' => ' ~ ', - '>' => ' > ', - '|' => '|', - '^' => ' ^ ', - '^^' => ' ^^ ' - ]; + self::$_noSpaceCombinators = [ + '' => true, + ' ' => true, + '|' => true + ]; + } - } + /** + * @param string $file + * @return void + */ + public function addParsedFile( $file ) { + $this->imports[] = $file; + } + + public function clone() { + $new_env = clone $this; + // NOTE: Match JavaScript by-ref behaviour for arrays + $new_env->imports =& $this->imports; + $new_env->importVisitorOnceMap =& $this->importVisitorOnceMap; + return $new_env; + } + + /** + * @param string $file + * @return bool + */ + public function isFileParsed( $file ) { + return in_array( $file, $this->imports ); } public function copyEvalEnv( $frames = [] ) { $new_env = new self(); $new_env->frames = $frames; + $new_env->importantScope = $this->importantScope; + $new_env->math = $this->math; return $new_env; } /** * @return bool - * @see Eval.prototype.isMathOn in less.js 3.0.0 https://github.com/less/less.js/blob/v3.0.0/dist/less.js#L1007 + * @see less-3.13.1.js#Eval.prototype.isMathOn */ - public static function isMathOn() { - if ( !self::$mathOn ) { + public function isMathOn( $op = "" ) { + if ( !$this->mathOn ) { + return false; + } + if ( $op === '/' && $this->math !== $this::MATH_ALWAYS && !$this->parensStack ) { return false; } - return !Less_Parser::$options['strictMath'] || self::$parensStack; + + if ( $this->math > $this::MATH_PARENS_DIVISION ) { + return (bool)$this->parensStack; + } + return true; + } + + /** + * @see less-3.13.1.js#Eval.prototype.inParenthesis + */ + public function inParenthesis() { + // Optimization: We don't need undefined/null, always have an array + $this->parensStack[] = true; + } + + /** + * @see less-3.13.1.js#Eval.prototype.inParenthesis + */ + public function outOfParenthesis() { + array_pop( $this->parensStack ); } /** @@ -110,6 +160,18 @@ public static function isPathRelative( $path ) { return !preg_match( '/^(?:[a-z-]+:|\/|#)/', $path ); } + public function enterCalc() { + $this->calcStack[] = true; + $this->inCalc = true; + } + + public function exitCalc() { + array_pop( $this->calcStack ); + if ( !$this->calcStack ) { + $this->inCalc = false; + } + } + /** * Canonicalize a path by resolving references to '/./', '/../' * Does not remove leading "../" diff --git a/wikimedia/less.php/lib/Less/Exception/Chunk.php b/wikimedia/less.php/lib/Less/Exception/Chunk.php index 5697cd6..5575b47 100644 --- a/wikimedia/less.php/lib/Less/Exception/Chunk.php +++ b/wikimedia/less.php/lib/Less/Exception/Chunk.php @@ -4,10 +4,13 @@ */ class Less_Exception_Chunk extends Less_Exception_Parser { + /** @var int */ protected $parserCurrentIndex = 0; + /** @var int */ protected $emitFrom = 0; + /** @var int */ protected $input_len; /** @@ -17,7 +20,7 @@ class Less_Exception_Chunk extends Less_Exception_Parser { * @param array|null $currentFile The file * @param int $code The exception code */ - public function __construct( $input, Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) { + public function __construct( $input, ?Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) { $this->message = 'ParseError: Unexpected input'; // default message $this->index = $index; @@ -29,6 +32,7 @@ public function __construct( $input, Exception $previous = null, $index = null, $this->Chunks(); $this->genMessage(); + $this->getFinalMessage(); } /** diff --git a/wikimedia/less.php/lib/Less/Exception/Parser.php b/wikimedia/less.php/lib/Less/Exception/Parser.php index 22d9d19..1c379d3 100644 --- a/wikimedia/less.php/lib/Less/Exception/Parser.php +++ b/wikimedia/less.php/lib/Less/Exception/Parser.php @@ -19,9 +19,11 @@ class Less_Exception_Parser extends Exception { */ public $index; - protected $input; + /** @var string */ + public $finalMessage = ''; - protected $details = []; + /** @var string|null */ + protected $input; /** * @param string|null $message @@ -30,7 +32,7 @@ class Less_Exception_Parser extends Exception { * @param array|null $currentFile The file * @param int $code The exception code */ - public function __construct( $message = null, Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) { + public function __construct( $message = null, ?Exception $previous = null, $index = null, $currentFile = null, $code = 0 ) { parent::__construct( $message, $code, $previous ); $this->currentFile = $currentFile; @@ -50,14 +52,14 @@ protected function getInput() { */ public function genMessage() { if ( $this->currentFile && $this->currentFile['filename'] ) { - $this->message .= ' in ' . basename( $this->currentFile['filename'] ); + $this->finalMessage .= ' 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(); + $this->finalMessage .= ' on line ' . $line . ', column ' . self::getColumn(); $lines = explode( "\n", $this->input ); @@ -66,7 +68,7 @@ public function genMessage() { $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( (string)( $i + 1 ), $num_len, '0', STR_PAD_LEFT ) . '| ' . $lines[$i]; + $this->finalMessage .= "\n" . str_pad( (string)( $i + 1 ), $num_len, '0', STR_PAD_LEFT ) . '| ' . $lines[$i]; } } } @@ -79,12 +81,7 @@ public function genMessage() { */ 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 substr_count( $this->input, "\n", 0, $this->index ) + 1; } return 1; } @@ -100,4 +97,7 @@ public function getColumn() { return $this->index - $pos; } + public function getFinalMessage() { + $this->message .= $this->finalMessage; + } } diff --git a/wikimedia/less.php/lib/Less/FileManager.php b/wikimedia/less.php/lib/Less/FileManager.php new file mode 100644 index 0000000..7b3d005 --- /dev/null +++ b/wikimedia/less.php/lib/Less/FileManager.php @@ -0,0 +1,65 @@ + $rooturi ) { + if ( is_callable( $rooturi ) ) { + $res = $rooturi( $filename ); + if ( $res && is_string( $res[0] ) ) { + return [ + Less_Environment::normalizePath( $res[0] ), + Less_Environment::normalizePath( $res[1] ?? dirname( $filename ) ) + ]; + } + } elseif ( !empty( $rootpath ) ) { + $path = rtrim( $rootpath, '/\\' ) . '/' . ltrim( $filename, '/\\' ); + if ( file_exists( $path ) ) { + return [ + Less_Environment::normalizePath( $path ), + Less_Environment::normalizePath( dirname( $rooturi . $filename ) ) + ]; + } + if ( file_exists( $path . '.less' ) ) { + return [ + Less_Environment::normalizePath( $path . '.less' ), + Less_Environment::normalizePath( dirname( $rooturi . $filename . '.less' ) ) + ]; + } + } + } + } +} diff --git a/wikimedia/less.php/lib/Less/Functions.php b/wikimedia/less.php/lib/Less/Functions.php index 73a44a6..6f03cfd 100644 --- a/wikimedia/less.php/lib/Less/Functions.php +++ b/wikimedia/less.php/lib/Less/Functions.php @@ -6,49 +6,21 @@ */ class Less_Functions { + /** @var Less_Environment */ public $env; + /** @var array|null */ public $currentFileInfo; - public function __construct( $env, array $currentFileInfo = null ) { + public function __construct( $env, ?array $currentFileInfo = null ) { $this->env = $env; $this->currentFileInfo = $currentFileInfo; } - /** - * @param string $op - * @param float $a - * @param float $b - */ - 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 ) { + private 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 ) { + private static function _number( $n ) { if ( $n instanceof Less_Tree_Dimension ) { return floatval( $n->unit->is( '%' ) ? $n->value / 100 : $n->value ); } elseif ( is_numeric( $n ) ) { @@ -58,11 +30,11 @@ public static function number( $n ) { } } - public static function scaled( $n, $size = 255 ) { + private static function _scaled( $n, $size = 255 ) { if ( $n instanceof Less_Tree_Dimension && $n->unit->is( '%' ) ) { return (float)$n->value * $size / 100; } else { - return self::number( $n ); + return self::_number( $n ); } } @@ -74,10 +46,13 @@ public function rgb( $r = null, $g = null, $b = null ) { } public function rgba( $r = null, $g = null, $b = null, $a = null ) { - $rgb = [ $r, $g, $b ]; - $rgb = array_map( [ __CLASS__, 'scaled' ], $rgb ); + $rgb = [ + self::_scaled( $r ), + self::_scaled( $g ), + self::_scaled( $b ) + ]; - $a = self::number( $a ); + $a = self::_number( $a ); return new Less_Tree_Color( $rgb, $a ); } @@ -86,10 +61,10 @@ public function hsl( $h, $s, $l ) { } 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 ) ); + $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; @@ -132,10 +107,10 @@ public function hsv( $h, $s, $v ) { * @param float $a */ public function hsva( $h, $s, $v, $a ) { - $h = ( ( self::number( $h ) % 360 ) / 360 ) * 360; - $s = self::number( $s ); - $v = self::number( $v ); - $a = self::number( $a ); + $h = ( ( (int)self::_number( $h ) % 360 ) / 360 ) * 360; + $s = self::_number( $s ); + $v = self::_number( $v ); + $a = self::_number( $a ); $i = (int)floor( (int)( $h / 60 ) % 6 ); $f = ( $h / 60 ) - $i; @@ -166,61 +141,75 @@ public function hsva( $h, $s, $v, $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?)' : '' ) ); + 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'] ) ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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 ), '%' ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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 ), '%' ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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'] ) ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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 ), '%' ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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 ), '%' ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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] ); @@ -228,7 +217,9 @@ public function red( $color = null ) { 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?)' : '' ) ); + 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] ); @@ -236,7 +227,9 @@ public function green( $color = null ) { 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?)' : '' ) ); + 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] ); @@ -244,7 +237,9 @@ public function blue( $color = null ) { 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?)' : '' ) ); + 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(); @@ -253,26 +248,30 @@ public function alpha( $color = null ) { 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?)' : '' ) ); + 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 ), '%' ); + return new Less_Tree_Dimension( $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?)' : '' ) ); + 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 ); + + ( 0.7152 * $color->rgb[1] / 255 ) + + ( 0.0722 * $color->rgb[2] / 255 ); - return new Less_Tree_Dimension( Less_Parser::round( $luminance * $color->alpha * 100 ), '%' ); + return new Less_Tree_Dimension( $luminance * $color->alpha * 100, '%' ); } - public function saturate( $color = null, $amount = null ) { + public function saturate( $color = null, $amount = null, $method = null ) { // filter: saturate(3.2); // should be kept as is, so check for color if ( $color instanceof Less_Tree_Dimension ) { @@ -280,16 +279,23 @@ public function saturate( $color = null, $amount = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['s'] += $hsl['s'] * $amount->value / 100; + } else { + $hsl['s'] += $amount->value / 100; + } $hsl['s'] = self::_clamp( $hsl['s'] ); return $this->hsla( $hsl['h'], $hsl['s'], $hsl['l'], $hsl['a'] ); } @@ -297,102 +303,158 @@ public function saturate( $color = null, $amount = null ) { /** * @param Less_Tree_Color|null $color * @param Less_Tree_Dimension|null $amount + * @param Less_Tree_Quoted|Less_Tree_Color|Less_Tree_Keyword|null $method */ - public function desaturate( $color = null, $amount = null ) { + public function desaturate( $color = null, $amount = null, $method = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['s'] -= $hsl['s'] * $amount->value / 100; + } else { + $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 ) { + public function lighten( $color = null, $amount = null, $method = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['l'] += $hsl['l'] * $amount->value / 100; + } else { + $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 ) { + public function darken( $color = null, $amount = null, $method = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['l'] -= $hsl['l'] * $amount->value / 100; + } else { + $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 ) { + public function fadein( $color = null, $amount = null, $method = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['a'] += $hsl['a'] * $amount->value / 100; + } else { + $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 ) { + public function fadeout( $color = null, $amount = null, $method = 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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + + if ( isset( $method ) && $method->value === "relative" ) { + $hsl['a'] -= $hsl['a'] * $amount->value / 100; + } else { + $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?)' : '' ) ); + 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?)' : '' ) ); + 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'] ); + $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?)' : '' ) ); + 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?)' : '' ) ); + 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(); @@ -415,16 +477,25 @@ public function spin( $color = null, $amount = null ) { */ 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?)' : '' ) ); + $type = is_object( $color1 ) ? get_class( $color1 ) : gettype( $color1 ); + throw new Less_Exception_Compiler( + "The first argument must be a color, $type given" . ( $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?)' : '' ) ); + $type = is_object( $color2 ) ? get_class( $color2 ) : gettype( $color2 ); + throw new Less_Exception_Compiler( + "The second argument must be a color, $type given" . ( $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?)' : '' ) ); + $type = is_object( $weight ) ? get_class( $weight ) : gettype( $weight ); + throw new Less_Exception_Compiler( + "The third argument must be a percentage, $type given" . ( $weight instanceof Less_Tree_Expression ? ' (did you forgot commas?)' : '' ) + ); } $p = $weight->value / 100.0; @@ -465,10 +536,14 @@ public function contrast( $color, $dark = null, $light = null, $threshold = null } 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?)' : '' ) ); + 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?)' : '' ) ); + 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! @@ -480,7 +555,7 @@ public function contrast( $color, $dark = null, $light = null, $threshold = null if ( !$threshold ) { $threshold = 0.43; } else { - $threshold = self::number( $threshold ); + $threshold = self::_number( $threshold ); } if ( $color->luma() < $threshold ) { @@ -498,7 +573,18 @@ public function e( $str ) { } public function escape( $str ) { - $revert = [ '%21' => '!', '%2A' => '*', '%27' => "'",'%3F' => '?','%26' => '&','%2C' => ',','%2F' => '/','%40' => '@','%2B' => '+','%24' => '$' ]; + $revert = [ + '%21' => '!', + '%2A' => '*', + '%27' => "'", + '%3F' => '?', + '%26' => '&', + '%2C' => ',', + '%2F' => '/', + '%40' => '@', + '%2B' => '+', + '%24' => '$' + ]; return new Less_Tree_Anonymous( strtr( rawurlencode( $str->value ), $revert ) ); } @@ -514,16 +600,22 @@ public function replace( $string, $pattern, $replacement, $flags = null ) { if ( $flags && $flags->value ) { $expr .= self::replace_flags( $flags->value ); } + $replacement = ( $replacement instanceof Less_Tree_Quoted ) ? + $replacement->value : $replacement->toCSS(); - $result = preg_replace( $expr, $replacement->value, $result ); + if ( $flags && $flags->value && preg_match( '/g/', $flags->value ) ) { + $result = preg_replace( $expr, $replacement, $result ); + } else { + $result = preg_replace( $expr, $replacement, $result, 1 ); + } - if ( property_exists( $string, 'quote' ) ) { + if ( $string instanceof Less_Tree_Quoted ) { return new Less_Tree_Quoted( $string->quote, $result, $string->escaped ); } return new Less_Tree_Quoted( '', $result ); } - public static function replace_flags( $flags ) { + private static function replace_flags( $flags ) { return str_replace( [ 'e', 'g' ], '', $flags ); } @@ -533,19 +625,26 @@ public function _percent( $string, ...$args ) { foreach ( $args as $arg ) { if ( preg_match( '/%[sda]/i', $result, $token ) ) { $token = $token[0]; - $value = stristr( $token, 's' ) ? $arg->value : $arg->toCSS(); + $value = ( ( $arg instanceof Less_Tree_Quoted ) && + 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 ); + if ( $string instanceof Less_Tree_Quoted ) { + return new Less_Tree_Quoted( $string->quote, $result, $string->escaped ); + } + return new Less_Tree_Quoted( '', $result ); } 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?' : '.' ) ); + 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 ) { @@ -598,7 +697,7 @@ public function ceil( $n ) { } public function floor( $n ) { - return $this->_math( 'floor', null, $n ); + return $this->_math( 'floor', null, $n ); } public function sqrt( $n ) { @@ -652,8 +751,9 @@ private function _math( $fn, $unit, ...$args ) { /** * @param bool $isMin * @param array $args + * @see less-2.5.3.js#minMax */ - private function _minmax( $isMin, $args ) { + private function _minMax( $isMin, $args ) { $arg_count = count( $args ); if ( $arg_count < 1 ) { @@ -694,7 +794,7 @@ private function _minmax( $isMin, $args ) { $unit = $currentUnified->unit->toString(); } - if ( $unit !== '' && !$unitStatic || $unit !== '' && $order[0]->unify()->unit->toString() === "" ) { + if ( ( $unit !== '' && !$unitStatic ) || ( $unit !== '' && $order[0]->unify()->unit->toString() === "" ) ) { $unitStatic = $unit; } @@ -734,15 +834,15 @@ private function _minmax( $isMin, $args ) { foreach ( $order as $a ) { $args[] = $a->toCSS(); } - return new Less_Tree_Anonymous( ( $isMin ? 'min(' : 'max(' ) . implode( Less_Environment::$_outputMap[','], $args ) . ')' ); + return new Less_Tree_Anonymous( ( $isMin ? 'min(' : 'max(' ) . implode( ( Less_Parser::$options['compress'] ? ',' : ', ' ), $args ) . ')' ); } public function min( ...$args ) { - return $this->_minmax( true, $args ); + return $this->_minMax( true, $args ); } public function max( ...$args ) { - return $this->_minmax( false, $args ); + return $this->_minMax( false, $args ); } public function getunit( $n ) { @@ -751,7 +851,9 @@ public function getunit( $n ) { 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?)' : '' ) ); + 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() ); @@ -761,20 +863,29 @@ 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" ); + /** + * @see less-2.5.3.js#colorFunctions.color + * @param Less_Tree_Quoted|Less_Tree_Color|Less_Tree_Keyword $c + * @return Less_Tree_Color + */ + public function color( $c ) { + if ( ( $c instanceof Less_Tree_Quoted ) && + preg_match( '/^#([a-f0-9]{6}|[a-f0-9]{3})/', $c->value ) + ) { + return new Less_Tree_Color( substr( $c->value, 1 ) ); + } + + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition + if ( ( $c instanceof Less_Tree_Color ) || ( $c = Less_Tree_Color::fromKeyword( $c->value ) ) ) { + $c->value = null; + return $c; } + + throw new Less_Exception_Compiler( "argument must be a color keyword or 3/6 digit hex e.g. #FFF" ); + } + + public function isruleset( $n ) { + return new Less_Tree_Keyword( $n instanceof Less_Tree_DetachedRuleset ? 'true' : 'false' ); } public function iscolor( $n ) { @@ -829,72 +940,77 @@ 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) + /** + * @see less-3.13.1.js#getItemsFromNode + */ + private function getItemsFromNode( Less_Tree $node ) { // handle non-array values as an array of length 1 // return 'undefined' if index is invalid - if ( !( $values instanceof Less_Tree_Color ) && is_array( $values->value ) ) { - if ( isset( $values->value[$index] ) ) { - return $values->value[$index]; - } - return null; + // + // NOTE: Less.js uses duck-typing `isArray(node.value)`, which would cause warnings in PHP, + // and potentially bugs for Less_Tree classes with a $value that is only sometimes an array. + // Instead, check for Less_Tree classes that always implement an array $value. + return ( $node instanceof Less_Tree_Expression || $node instanceof Less_Tree_Value ) + ? $node->value + : [ $node ]; + } - } elseif ( (int)$index === 0 ) { - return $values; - } + /** + * @see less-3.13.1.js#_SELF + */ + public function _self( $args ) { + return $args; + } - return null; + /** + * @see less-3.13.1.js#extract + */ + public function extract( $values, $index ) { + // (1-based index) + $index = (int)$index->value - 1; + return $this->getItemsFromNode( $values )[ $index ] ?? null; } + /** + * @see less-3.13.1.js#length + */ public function length( $values ) { - $n = ( $values instanceof Less_Tree_Expression || $values instanceof Less_Tree_Value ) ? - count( $values->value ) : 1; - return new Less_Tree_Dimension( $n ); + return new Less_Tree_Dimension( count( $this->getItemsFromNode( $values ) ) ); } + /** + * @see less-2.5.3.js#data-uri + */ public function datauri( $mimetypeNode, $filePathNode = null ) { - $filePath = ( $filePathNode ? $filePathNode->value : null ); - $mimetype = $mimetypeNode->value; - - $args = 2; - if ( !$filePath ) { - $filePath = $mimetype; - $args = 1; + if ( !$filePathNode ) { + $filePathNode = $mimetypeNode; + $mimetypeNode = null; } - $filePath = str_replace( '\\', '/', $filePath ); - if ( Less_Environment::isPathRelative( $filePath ) ) { - $currentFileInfo = $this->currentFileInfo; - '@phan-var array $currentFileInfo'; - if ( Less_Parser::$options['relativeUrls'] ) { - $temp = $currentFileInfo['currentDirectory']; - } else { - $temp = $currentFileInfo['entryPath']; - } - - if ( !empty( $temp ) ) { - $filePath = Less_Environment::normalizePath( rtrim( $temp, '/' ) . '/' . $filePath ); - } + $filePath = $filePathNode->value; + $mimetype = ( $mimetypeNode ? $mimetypeNode->value : null ); + $filePath = str_replace( '\\', '/', $filePath ); + $fragmentStart = strpos( $filePath, '#' ); + $fragment = ''; + if ( $fragmentStart !== false ) { + $fragment = substr( $filePath, $fragmentStart ); + $filePath = substr( $filePath, 0, $fragmentStart ); } - // detect the mimetype if not given - if ( $args < 2 ) { - - /* incomplete - $mime = require('mime'); - mimetype = mime.lookup(path); + [ $filePath ] = Less_FileManager::getFilePath( $filePath, $this->currentFileInfo ); - // 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'; - */ + // detect the mimetype if not given + if ( !$mimetype ) { $mimetype = Less_Mime::lookup( $filePath ); - $charset = Less_Mime::charsets_lookup( $mimetype ); - $useBase64 = !in_array( $charset, [ 'US-ASCII', 'UTF-8' ] ); + if ( $mimetype === "image/svg+xml" ) { + $useBase64 = false; + } else { + $charset = Less_Mime::charsets_lookup( $mimetype ); + $useBase64 = !in_array( $charset, [ 'US-ASCII', 'UTF-8' ] ); + } if ( $useBase64 ) { $mimetype .= ';base64'; } @@ -903,27 +1019,26 @@ public function datauri( $mimetypeNode, $filePathNode = null ) { $useBase64 = preg_match( '/;base64$/', $mimetype ); } - if ( file_exists( $filePath ) ) { - $buf = @file_get_contents( $filePath ); - } else { - $buf = false; + if ( !file_exists( $filePath ) ) { + $fallback = new Less_Tree_Url( ( $filePathNode ?: $mimetypeNode ), $this->currentFileInfo ); + return $fallback->compile( $this->env ); } + $buf = @file_get_contents( $filePath ); + + $buf = $useBase64 ? base64_encode( $buf ) : rawurlencode( $buf ); + $url = "data:" . $mimetype . ',' . $buf . $fragment; // 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 ?: $mimetypeNode ), $this->currentFileInfo ); - return $url->compile( $this->env ); - } - - if ( $buf ) { - $buf = $useBase64 ? base64_encode( $buf ) : rawurlencode( $buf ); - $filePath = '"data:' . $mimetype . ',' . $buf . '"'; + $DATA_URI_MAX_KB = 32768; + if ( strlen( $buf ) >= $DATA_URI_MAX_KB ) { + // NOTE: Less.js checks for ieCompat here (true by default). + // For Less.php, ieCompat is not configurable, and always true. + $fallback = new Less_Tree_Url( ( $filePathNode ?: $mimetypeNode ), $this->currentFileInfo ); + return $fallback->compile( $this->env ); } - return new Less_Tree_Url( new Less_Tree_Anonymous( $filePath ) ); + return new Less_Tree_Url( new Less_Tree_Quoted( '"' . $url . '"', $url, false ) ); } // svg-gradient @@ -936,7 +1051,6 @@ public function svggradient( $direction, ...$stops ) { $gradientType = 'linear'; $rectangleDimension = 'x="0" y="0" width="1" height="1"'; - $useBase64 = true; $directionValue = $direction->toCSS(); switch ( $directionValue ) { @@ -959,7 +1073,9 @@ public function svggradient( $direction, ...$stops ) { $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'" ); + 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 = '' . @@ -976,7 +1092,9 @@ public function svggradient( $direction, ...$stops ) { $position = null; } - if ( !( $color instanceof Less_Tree_Color ) || ( !( ( $i === 0 || $i + 1 === count( $stops ) ) && $position === null ) && !( $position instanceof Less_Tree_Dimension ) ) ) { + 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 ) { @@ -987,29 +1105,80 @@ public function svggradient( $direction, ...$stops ) { $positionValue = '100%'; } $alpha = $color->alpha; - $returner .= ''; + $returner .= ''; } $returner .= ''; - if ( $useBase64 ) { - $returner = "'data:image/svg+xml;base64," . base64_encode( $returner ) . "'"; - } else { - $returner = "'data:image/svg+xml," . $returner . "'"; + $revert = [ + '%21' => '!', + '%2A' => '*', + '%27' => "'", + '%26' => '&', + '%2C' => ',', + '%40' => '@', + '%2B' => '+', + '%24' => '$', + '%28' => '(', + '%29' => ')' + ]; + $returner = strtr( rawurlencode( $returner ), $revert ); + + $returner = "data:image/svg+xml," . $returner; + + return new Less_Tree_Url( new Less_Tree_Quoted( "'" . $returner . "'", $returner, false ) ); + } + + /** + * @see https://github.com/less/less.js/blob/v2.5.3/lib/less-node/image-size.js + */ + private function getImageSize( $filePathNode ) { + $filePath = $filePathNode->value; + + $filePath = str_replace( '\\', '/', $filePath ); + + [ $filePath ] = Less_FileManager::getFilePath( $filePath, $this->currentFileInfo ); + + $mimetype = Less_Mime::lookup( $filePath ); + + if ( $mimetype === "image/svg+xml" ) { + return $this->getSvgSize( $filePath ); } - return new Less_Tree_Url( new Less_Tree_Anonymous( $returner ) ); + [ $imagewidth, $imageheight ] = getimagesize( $filePath ); + + return [ "width" => $imagewidth, "height" => $imageheight ]; } /** - * Php version of javascript's `encodeURIComponent` function - * - * @param string $string The string to encode - * @return string The encoded string + * @see https://github.com/image-size/image-size/blob/main/lib/types/svg.ts */ - public static function encodeURIComponent( $string ) { - $revert = [ '%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')' ]; - return strtr( rawurlencode( $string ), $revert ); + private function getSvgSize( string $filePathNode ) { + $xml = simplexml_load_string( file_get_contents( $filePathNode ) ); + $attributes = $xml->attributes(); + $width = (string)$attributes->width; + $height = (string)$attributes->height; + + return [ "width" => $width, "height" => $height ]; + } + + public function imagesize( $filePathNode = null ) { + $imagesize = $this->getImageSize( $filePathNode ); + return new Less_Tree_Expression( [ + new Less_Tree_Dimension( $imagesize["width"], "px" ), + new Less_Tree_Dimension( $imagesize["height"], "px" ) + ] ); + } + + public function imagewidth( $filePathNode = null ) { + $imagesize = $this->getImageSize( $filePathNode ); + return new Less_Tree_Dimension( $imagesize["width"], "px" ); + } + + public function imageheight( $filePathNode = null ) { + $imagesize = $this->getImageSize( $filePathNode ); + return new Less_Tree_Dimension( $imagesize["height"], "px" ); } // Color Blending @@ -1037,13 +1206,17 @@ public function colorBlend( $mode, $color1, $color2 ) { 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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendMultiply' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendMultiply' ], $color1, $color2 ); } private function colorBlendMultiply( $cb, $cs ) { @@ -1052,13 +1225,17 @@ private function colorBlendMultiply( $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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendScreen' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendScreen' ], $color1, $color2 ); } private function colorBlendScreen( $cb, $cs ) { @@ -1067,13 +1244,17 @@ private function colorBlendScreen( $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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendOverlay' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendOverlay' ], $color1, $color2 ); } private function colorBlendOverlay( $cb, $cs ) { @@ -1085,13 +1266,17 @@ private function colorBlendOverlay( $cb, $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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendSoftlight' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendSoftlight' ], $color1, $color2 ); } private function colorBlendSoftlight( $cb, $cs ) { @@ -1107,13 +1292,17 @@ private function colorBlendSoftlight( $cb, $cs ) { 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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendHardlight' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendHardlight' ], $color1, $color2 ); } private function colorBlendHardlight( $cb, $cs ) { @@ -1122,13 +1311,17 @@ private function colorBlendHardlight( $cb, $cs ) { 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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendDifference' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendDifference' ], $color1, $color2 ); } private function colorBlendDifference( $cb, $cs ) { @@ -1137,13 +1330,17 @@ private function colorBlendDifference( $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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendExclusion' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendExclusion' ], $color1, $color2 ); } private function colorBlendExclusion( $cb, $cs ) { @@ -1152,35 +1349,52 @@ private function colorBlendExclusion( $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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendAverage' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendAverage' ], $color1, $color2 ); } // non-w3c functions: - public function colorBlendAverage( $cb, $cs ) { + private 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?)' : '' ) ); + 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?)' : '' ) ); + 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( [ $this,'colorBlendNegation' ], $color1, $color2 ); + return $this->colorBlend( [ $this, 'colorBlendNegation' ], $color1, $color2 ); } - public function colorBlendNegation( $cb, $cs ) { + private function colorBlendNegation( $cb, $cs ) { return 1 - abs( $cb + $cs - 1 ); } // ~ End of Color Blending + public function if( $condition, $trueValue, $falseValue = null ) { + return $condition->compile( $this->env ) ? $trueValue->compile( $this->env ) + : ( $falseValue ? $falseValue->compile( $this->env ) : new Less_Tree_Anonymous( '' ) ); + } + + public function boolean( $condition ) { + return $condition ? new Less_Tree_Keyword( 'true' ) : new Less_Tree_Keyword( 'false' ); + } + } diff --git a/wikimedia/less.php/lib/Less/ImportVisitor.php b/wikimedia/less.php/lib/Less/ImportVisitor.php new file mode 100644 index 0000000..4eab0aa --- /dev/null +++ b/wikimedia/less.php/lib/Less/ImportVisitor.php @@ -0,0 +1,266 @@ + */ + public $variableImports = []; + /** @var array */ + public $recursionDetector = []; + + /** @var int */ + public $_currentDepth = 0; + /** @var mixed */ + public $importItem; + + public function __construct( $env ) { + parent::__construct(); + // NOTE: Upstream creates a new environment/context here. We re-use the main one instead. + // This makes Less_Environment->addParsedFile() easier to support (which is custom to Less.php) + $this->env = $env; + // NOTE: Upstream `importCount` is not here, appears unused. + // NOTE: Upstream `isFinished` is not here, we simply call tryRun() once at the end. + // NOTE: Upstream `onceFileDetectionMap` is instead Less_Environment->isFileParsed. + // NOTE: Upstream `ImportSequencer` logic is directly inside ImportVisitor for simplicity. + } + + public function run( $root ) { + $this->visitObj( $root ); + $this->tryRun(); + } + + public function visitImport( $importNode, &$visitDeeper ) { + $inlineCSS = $importNode->options['inline']; + + if ( !$importNode->css || $inlineCSS ) { + + $env = $this->env->clone(); + $importParent = $env->frames[0]; + if ( $importNode->isVariableImport() ) { + $this->addVariableImport( [ + 'function' => 'processImportNode', + 'args' => [ $importNode, $env, $importParent ] + ] ); + } else { + $this->processImportNode( $importNode, $env, $importParent ); + } + } + $visitDeeper = false; + } + + public function processImportNode( $importNode, $env, &$importParent ) { + $evaldImportNode = $inlineCSS = $importNode->options['inline']; + + try { + $evaldImportNode = $importNode->compileForImport( $env ); + } catch ( Exception $e ) { + $importNode->css = true; + } + + if ( $evaldImportNode && ( !$evaldImportNode->css || $inlineCSS ) ) { + + if ( $importNode->options['multiple'] ) { + $env->importMultiple = true; + } + + $tryAppendLessExtension = $evaldImportNode->css === null; + + for ( $i = 0; $i < count( $importParent->rules ); $i++ ) { + if ( $importParent->rules[$i] === $importNode ) { + $importParent->rules[$i] = $evaldImportNode; + break; + } + } + + // Rename $evaldImportNode to $importNode here so that we avoid avoid mistaken use + // of not-yet-compiled $importNode after this point, which upstream's code doesn't + // have access to after this point, either. + $importNode = $evaldImportNode; + unset( $evaldImportNode ); + + // NOTE: Upstream Less.js's ImportVisitor puts the rest of the processImportNode logic + // into a separate ImportVisitor.prototype.onImported function, because file loading + // is async there. They implement and call: + // + // - ImportSequencer.prototype.addImport: + // remembers what processImportNode() was doing, and will call onImported + // once the async file load is finished. + // - ImportManager.prototype.push: + // resolves the import path to full path and uri, + // then parses the file content into a root Ruleset for that file. + // - ImportVisitor.prototype.onImported: + // marks the file as parsed (for skipping duplicates, to avoid recursion), + // and calls tryRun() if this is the last remaining import. + // + // In PHP we load files synchronously, so we can put a simpler version of this + // logic directly here. + + // @see less-2.5.3.js#ImportManager.prototype.push + + // NOTE: This is the equivalent to upstream `newFileInfo` and `fileManager.getPath()` + + $path = $importNode->getPath(); + + if ( $tryAppendLessExtension ) { + $path = preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ? $path : $path . '.less'; + } + + [ $fullPath, $uri ] = + Less_FileManager::getFilePath( $path, $importNode->currentFileInfo ) ?? [ $path, $path ]; + + // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback + + // NOTE: Upstream creates the next `currentFileInfo` here as `newFileInfo` + // We instead let Less_Parser::SetFileInfo() do that later via Less_Parser::parseFile(). + // This means that instead of setting `newFileInfo.reference` we modify the $env, + // and Less_Parser::SetFileInfo will inherit that. + if ( $importNode->options['reference'] ?? false ) { + $env->currentFileInfo['reference'] = true; + } + + $e = null; + try { + if ( $importNode->options['inline'] ) { + if ( !file_exists( $fullPath ) ) { + throw new Less_Exception_Parser( + sprintf( 'File `%s` not found.', $fullPath ), + null, + $importNode->index, + $importNode->currentFileInfo + ); + } + $root = file_get_contents( $fullPath ); + } else { + $parser = new Less_Parser( $env ); + // NOTE: Upstream sets `env->processImports = false` here to avoid + // running ImportVisitor again (infinite loop). We instead separate + // Less_Parser->parseFile() from Less_Parser->getCss(), + // and only getCss() runs ImportVisitor. + $root = $parser->parseFile( $fullPath, $uri, true ); + } + } catch ( Less_Exception_Parser $err ) { + $e = $err; + } + + // @see less-2.5.3.js#ImportManager.prototype.push/fileParsedFunc + + if ( $importNode->options['optional'] && $e ) { + $e = null; + $root = new Less_Tree_Ruleset( null, [] ); + $fullPath = null; + } + + // @see less-2.5.3.js#ImportVisitor.prototype.onImported + + if ( $e instanceof Less_Exception_Parser ) { + if ( !is_numeric( $e->index ) ) { + $e->index = $importNode->index; + $e->currentFile = $importNode->currentFileInfo; + $e->genMessage(); + } + throw $e; + } + + $duplicateImport = isset( $this->recursionDetector[$fullPath] ); + + if ( !$env->importMultiple ) { + if ( $duplicateImport ) { + $importNode->doSkip = true; + } else { + // NOTE: Upstream implements skip() as dynamic function. + // We instead have a regular Less_Tree_Import::skip() method, + // and in cases where skip() would be re-defined here we set doSkip=null. + $importNode->doSkip = null; + } + } + + if ( !$fullPath && $importNode->options['optional'] ) { + $importNode->doSkip = true; + } + + if ( $root ) { + $importNode->root = $root; + $importNode->importedFilename = $fullPath; + + if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $fullPath ) { + $this->recursionDetector[$fullPath] = true; + $oldContext = $this->env; + $this->env = $env; + $this->visitObj( $root ); + $this->env = $oldContext; + } + } + } else { + $this->tryRun(); + } + } + + public function addVariableImport( $callback ) { + $this->variableImports[] = $callback; + } + + public function tryRun() { + while ( true ) { + // NOTE: Upstream keeps a `this.imports` queue here that resumes + // processImportNode() logic by calling onImported() after a file + // is finished loading. We don't need that since we load and parse + // synchronously within processImportNode() instead. + + if ( count( $this->variableImports ) === 0 ) { + break; + } + $variableImport = $this->variableImports[0]; + + $this->variableImports = array_slice( $this->variableImports, 1 ); + $function = $variableImport['function']; + + $this->$function( ...$variableImport["args"] ); + } + } + + public function visitDeclaration( $declNode, $visitDeeper ) { + // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }` + $visitDeeper = false; + } + + // TODO: Implement less-3.13.1.js#ImportVisitor.prototype.visitDeclarationOut + // if (… DetachedRuleset) { this.context.frames.shift(); } + + public function visitAtRule( $atRuleNode, $visitArgs ) { + array_unshift( $this->env->frames, $atRuleNode ); + } + + public function visitAtRuleOut( $atRuleNode ) { + array_shift( $this->env->frames ); + } + + public function visitMixinDefinition( $mixinDefinitionNode, $visitArgs ) { + array_unshift( $this->env->frames, $mixinDefinitionNode ); + } + + public function visitMixinDefinitionOut( $mixinDefinitionNode ) { + array_shift( $this->env->frames ); + } + + public function visitRuleset( $rulesetNode, $visitArgs ) { + array_unshift( $this->env->frames, $rulesetNode ); + } + + public function visitRulesetOut( $rulesetNode ) { + array_shift( $this->env->frames ); + } + + public function visitMedia( $mediaNode, $visitArgs ) { + // TODO: Upsteam does not modify $mediaNode here. Why do we? + $mediaNode->allExtends = []; + array_unshift( $this->env->frames, $mediaNode->allExtends ); + } + + public function visitMediaOut( $mediaNode ) { + array_shift( $this->env->frames ); + } + +} diff --git a/wikimedia/less.php/lib/Less/Less.php.combine b/wikimedia/less.php/lib/Less/Less.php.combine deleted file mode 100644 index d63cc78..0000000 --- a/wikimedia/less.php/lib/Less/Less.php.combine +++ /dev/null @@ -1,17 +0,0 @@ - -./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/wikimedia/less.php/lib/Less/Output.php b/wikimedia/less.php/lib/Less/Output.php index d531bce..67ae47b 100644 --- a/wikimedia/less.php/lib/Less/Output.php +++ b/wikimedia/less.php/lib/Less/Output.php @@ -25,15 +25,6 @@ public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) { $this->strs[] = $chunk; } - /** - * Is the output empty? - * - * @return bool - */ - public function isEmpty() { - return count( $this->strs ) === 0; - } - /** * Converts the output to string * diff --git a/wikimedia/less.php/lib/Less/Output/Mapped.php b/wikimedia/less.php/lib/Less/Output/Mapped.php index 8fa5311..1fe8e87 100644 --- a/wikimedia/less.php/lib/Less/Output/Mapped.php +++ b/wikimedia/less.php/lib/Less/Output/Mapped.php @@ -63,8 +63,7 @@ public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) { $sourceLines = []; $sourceColumns = ' '; - if ( $fileInfo ) { - + if ( isset( $fileInfo['currentUri'] ) ) { $url = $fileInfo['currentUri']; if ( isset( $this->contentsMap[$url] ) ) { @@ -84,19 +83,19 @@ public function add( $chunk, $fileInfo = null, $index = 0, $mapLines = null ) { if ( !$mapLines ) { $this->generator->addMapping( - $this->lineNumber + 1, // generated_line - $this->column, // generated_column - count( $sourceLines ), // original_line - strlen( $sourceColumns ), // original_column + $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 + $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 ); } diff --git a/wikimedia/less.php/lib/Less/Parser.php b/wikimedia/less.php/lib/Less/Parser.php index 4662180..f7d3ee1 100644 --- a/wikimedia/less.php/lib/Less/Parser.php +++ b/wikimedia/less.php/lib/Less/Parser.php @@ -10,52 +10,93 @@ class Less_Parser { * @var array */ public static $default_options = [ - 'compress' => 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' => [], - 'import_callback' => null, - 'cache_dir' => null, - 'cache_method' => 'serialize', // false, 'serialize', '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' => [], + // whether to compress + 'compress' => false, + // whether units need to evaluate correctly + 'strictUnits' => false, + // How to process math + // + // always - eagerly try to solve all operations + // parens-division - require parens for division "/" + // parens | strict - require parens for all operations + // + // NOTE: We use the default of Less.js 4.0 (parens-division) + // instead of Less.js 3.13 (always). + 'math' => 'parens-division', + // whether to adjust URL's to be relative + 'relativeUrls' => true, + // whether to add args into url tokens + 'urlArgs' => '', + 'numPrecision' => 8, + + 'import_dirs' => [], + + /** + * Set this to a directory to enable the incremental cache. + * + * It is recommended to use Less_Cache::Get() instead, which is much faster, + * as it can skip compilation alltogether. Refer to API.md#incremental-cache + * for more information. + */ + 'cache_dir' => null, + 'cache_incremental' => true, + // one of false, 'serialize', or 'callback' + 'cache_method' => 'serialize', + 'cache_callback_get' => null, + 'cache_callback_set' => null, + + // whether to output a source map + 'sourceMap' => false, + 'sourceMapBasepath' => null, + 'sourceMapWriteTo' => null, + 'sourceMapURL' => null, + + 'indentation' => ' ', + + 'plugins' => [], + 'functions' => [], ]; - /** @var array{compress:bool,strictUnits:bool,strictMath:bool,numPrecision:int,import_dirs:array,import_callback:null|callable,indentation:string} */ + /** @var array{compress:bool,strictUnits:bool,relativeUrls:bool,urlArgs:string,numPrecision:int,import_dirs:array,cache_dir:?string,cache_incremental:bool,indentation:string} */ public static $options = []; - private $input; // Less input string - private $input_len; // input string length - private $pos; // current index in `input` - private $saveStack = []; // holds state for backtracking + /** @var string Less input string */ + private $input; + /** @var int input string length */ + private $input_len; + /** @var int current index in `input` */ + private $pos; + /** @var int[] holds state for backtracking */ + private $saveStack = []; + /** @var int */ private $furthest; - private $mb_internal_encoding = ''; // for remember exists value of mbstring.internal_encoding + + /** @var bool */ + private $autoCommentAbsorb = true; + /** + * @var array + */ + private $commentStore = []; /** * @var Less_Environment */ private $env; + /** @var Less_Tree[] */ protected $rules = []; - private static $imports = []; + /** + * Evaluated ruleset created by `getCss()`. Stored for potential use in `getVariables()` + * @var Less_Tree[]|null + */ + private $cachedEvaldRules; + /** @var bool */ public static $has_extends = false; + /** @var int */ public static $next_id = 0; /** @@ -74,17 +115,10 @@ public function __construct( $env = null ) { if ( $env instanceof Less_Environment ) { $this->env = $env; } else { - $this->SetOptions( self::$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' ); - } + Less_Tree::$parse = $this; } /** @@ -92,16 +126,15 @@ public function __construct( $env = null ) { */ public function Reset( $options = null ) { $this->rules = []; - self::$imports = []; + $this->cachedEvaldRules = null; self::$has_extends = false; - self::$imports = []; self::$contentsMap = []; $this->env = new Less_Environment(); // set new options + $this->SetOptions( self::$default_options ); if ( is_array( $options ) ) { - $this->SetOptions( self::$default_options ); $this->SetOptions( $options ); } @@ -110,7 +143,6 @@ public function Reset( $options = null ) { /** * Set one or more compiler options - * options: import_dirs, cache_dir, cache_method */ public function SetOptions( $options ) { foreach ( $options as $option => $value ) { @@ -123,6 +155,24 @@ public function SetOptions( $options ) { */ public function SetOption( $option, $value ) { switch ( $option ) { + case 'strictMath': + if ( $value ) { + $this->env->math = Less_Environment::MATH_PARENS; + } else { + $this->env->math = Less_Environment::MATH_ALWAYS; + } + break; + + case 'math': + $value = strtolower( $value ); + if ( $value === 'always' ) { + $this->env->math = Less_Environment::MATH_ALWAYS; + } elseif ( $value === 'parens-division' ) { + $this->env->math = Less_Environment::MATH_PARENS_DIVISION; + } elseif ( $value === 'parens' || $value === 'strict' ) { + $this->env->math = Less_Environment::MATH_PARENS; + } + return; case 'import_dirs': $this->SetImportDirs( $value ); @@ -130,8 +180,13 @@ public function SetOption( $option, $value ) { case 'cache_dir': if ( is_string( $value ) ) { - Less_Cache::SetCacheDir( $value ); - Less_Cache::CheckCacheDir(); + $value = Less_Cache::CheckCacheDir( $value ); + } + break; + + case 'functions': + foreach ( $value as $key => $function ) { + $this->registerFunction( $key, $function ); } return; } @@ -176,11 +231,16 @@ public function getCss() { $root->root = true; $root->firstRoot = true; + $importVisitor = new Less_ImportVisitor( $this->env ); + $importVisitor->run( $root ); + $this->PreVisitors( $root ); self::$has_extends = false; $evaldRoot = $root->compile( $this->env ); + $this->cachedEvaldRules = $evaldRoot->rules; + $this->PostVisitors( $evaldRoot ); if ( self::$options['sourceMap'] ) { @@ -204,15 +264,11 @@ public function getCss() { @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 ) ) { + if ( $exc instanceof Less_Exception_Parser ) { + $exc->getFinalMessage(); + } throw $exc; } @@ -220,7 +276,10 @@ public function getCss() { } public function findValueOf( $varName ) { - foreach ( $this->rules as $rule ) { + $rules = $this->cachedEvaldRules ?? $this->rules; + + foreach ( $rules as $rule ) { + // @phan-suppress-next-line PhanUndeclaredProperty if ( isset( $rule->variable ) && ( $rule->variable == true ) && ( str_replace( "@", "", $rule->name ) == $varName ) ) { return $this->getVariableValue( $rule ); } @@ -229,43 +288,30 @@ public function findValueOf( $varName ) { } /** - * 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 + * Get an array of the found variables in the parsed input. * * @return array + * @phan-return array */ public function getVariables() { $variables = []; - $not_variable_type = [ - Less_Tree_Comment::class, // this include less comments ( // ) and css comments (/* */) - Less_Tree_Import::class, // do not search variables in included files @import - Less_Tree_Ruleset::class, // selectors (.someclass, #someid, …) - Less_Tree_Operation::class, - ]; - - // @TODO run compilation if not runned yet - foreach ( $this->rules as $key => $rule ) { - if ( in_array( get_class( $rule ), $not_variable_type ) ) { - continue; - } - - // Note: it seems $rule is always Less_Tree_Rule when variable = true - if ( $rule instanceof Less_Tree_Rule && $rule->variable ) { + $rules = $this->cachedEvaldRules ?? $this->rules; + foreach ( $rules as $key => $rule ) { + if ( $rule instanceof Less_Tree_Declaration && $rule->variable ) { $variables[$rule->name] = $this->getVariableValue( $rule ); - } else { - if ( $rule instanceof Less_Tree_Comment ) { - $variables[] = $this->getVariableValue( $rule ); - } } } return $variables; } public function findVarByName( $var_name ) { - foreach ( $this->rules as $rule ) { + $rules = $this->cachedEvaldRules ?? $this->rules; + + foreach ( $rules as $rule ) { + // @phan-suppress-next-line PhanUndeclaredProperty if ( isset( $rule->variable ) && ( $rule->variable == true ) ) { + // @phan-suppress-next-line PhanUndeclaredProperty if ( $rule->name == $var_name ) { return $this->getVariableValue( $rule ); } @@ -279,7 +325,8 @@ public function findVarByName( $var_name ) { * Since the objects vary here we add the logic for extracting the css/less value. * * @param Less_Tree $var - * @return string + * @return mixed + * @phan-return string|float|array */ private function getVariableValue( Less_Tree $var ) { switch ( get_class( $var ) ) { @@ -289,18 +336,33 @@ private function getVariableValue( Less_Tree $var ) { return $this->findVarByName( $var->name ); case Less_Tree_Keyword::class: return $var->value; + case Less_Tree_Anonymous::class: + $return = []; + if ( is_array( $var->value ) ) { + // in compilation phase, Less_Tree_Anonymous::$val can be a Less_Tree[] + // @phan-suppress-next-line PhanTypeMismatchForeach + foreach ( $var->value as $value ) { + /** @var Less_Tree $value */ + $return[ $value->name ] = $this->getVariableValue( $value ); + } + } + return count( $return ) === 1 ? $return[0] : $return; case Less_Tree_Url::class: // Based on Less_Tree_Url::genCSS() // Recurse to serialize the Less_Tree_Quoted value return 'url(' . $this->getVariableValue( $var->value ) . ')'; - case Less_Tree_Rule::class: + case Less_Tree_Declaration::class: + if ( $var->value instanceof Less_Tree_Anonymous ) { + $nodes = $this->parseNode( $var->value->value, [ 'value', 'important' ], 0, [] ); + return $this->getVariableValue( $nodes[1][0] ); + } return $this->getVariableValue( $var->value ); case Less_Tree_Value::class: - $value = ''; + $values = []; foreach ( $var->value as $sub_value ) { - $value .= $this->getVariableValue( $sub_value ) . ' '; + $values[] = $this->getVariableValue( $sub_value ); } - return $value; + return count( $values ) === 1 ? $values[0] : $values; case Less_Tree_Quoted::class: return $var->quote . $var->value . $var->quote; case Less_Tree_Dimension::class: @@ -310,11 +372,11 @@ private function getVariableValue( Less_Tree $var ) { } return $value; case Less_Tree_Expression::class: - $value = ''; + $values = []; foreach ( $var->value as $item ) { - $value .= $this->getVariableValue( $item ) . " "; + $values[] = $this->getVariableValue( $item ); } - return $value; + return implode( ' ', $values ); case Less_Tree_Operation::class: throw new Exception( 'getVariables() require Less to be compiled. please use $parser->getCss() before calling getVariables()' ); case Less_Tree_Unit::class: @@ -328,7 +390,7 @@ private function getVariableValue( Less_Tree $var ) { private function rgb2html( $r, $g = -1, $b = -1 ) { if ( is_array( $r ) && count( $r ) == 3 ) { - list( $r, $g, $b ) = $r; + [ $r, $g, $b ] = $r; } return sprintf( '#%02x%02x%02x', $r, $g, $b ); @@ -438,7 +500,7 @@ public function parseFile( $filename, $uri_root = '', $returnRoot = false ) { $this->SetFileInfo( $filename, $uri_root ); - self::AddParsedFile( $filename ); + $this->env->addParsedFile( $filename ); if ( $returnRoot ) { $rules = $this->GetRules( $filename ); @@ -507,26 +569,11 @@ public function SetFileInfo( $filename, $uri_root = '' ) { } /** - * @deprecated 1.5.1.2 + * @deprecated 1.5.1.2 Use Less_Cache::SetCacheDir instead. */ 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; - } + trigger_error( 'Less_Parser::SetCacheDir is deprecated, use Less_Cache::SetCacheDir instead', E_USER_DEPRECATED ); + Less_Cache::SetCacheDir( $dir ); } /** @@ -548,11 +595,11 @@ public function SetCacheDir( $dir ) { * } * } * - * - * @param array $dirs The key should be a server directory from which LESS + * @param array $dirs The key should be a server directory from which LESS * files may be imported. The value is an optional public URL or URL base path that corresponds to * the same directory (use empty string otherwise). The value may also be a closure, in * which case the key is ignored. + * @phan-param array $dirs */ public function SetImportDirs( $dirs ) { self::$options['import_dirs'] = []; @@ -589,60 +636,48 @@ private function _parse( $file_path = null ) { * @param string|null $file_path */ private function GetRules( $file_path ) { - $this->SetInput( $file_path ); + $this->setInput( $file_path ); - $cache_file = $this->CacheFile( $file_path ); + $cache_file = $this->cacheFile( $file_path ); if ( $cache_file ) { - if ( self::$options['cache_method'] == 'callback' ) { + if ( self::$options['cache_method'] === 'callback' ) { $callback = self::$options['cache_callback_get']; if ( is_callable( $callback ) ) { $cache = $callback( $this, $file_path, $cache_file ); - if ( $cache ) { - $this->UnsetInput(); + $this->unsetInput(); return $cache; } } - } elseif ( file_exists( $cache_file ) ) { - switch ( self::$options['cache_method'] ) { - - // Using serialize - case 'serialize': - $cache = unserialize( file_get_contents( $cache_file ) ); - if ( $cache ) { - touch( $cache_file ); - $this->UnsetInput(); - return $cache; - } - break; + } elseif ( self::$options['cache_method'] === 'serialize' && file_exists( $cache_file ) ) { + $cache = unserialize( file_get_contents( $cache_file ) ); + if ( $cache ) { + touch( $cache_file ); + $this->unsetInput(); + return $cache; } } } - + $this->skipWhitespace( 0 ); $rules = $this->parsePrimary(); if ( $this->pos < $this->input_len ) { throw new Less_Exception_Chunk( $this->input, null, $this->furthest, $this->env->currentFileInfo ); } - $this->UnsetInput(); + $this->unsetInput(); // save the cache if ( $cache_file ) { - if ( self::$options['cache_method'] == 'callback' ) { + if ( self::$options['cache_method'] === 'callback' ) { $callback = self::$options['cache_callback_set']; if ( is_callable( $callback ) ) { $callback( $this, $file_path, $cache_file, $rules ); } - } else { - switch ( self::$options['cache_method'] ) { - case 'serialize': - file_put_contents( $cache_file, serialize( $rules ) ); - break; - } - - Less_Cache::CleanCache(); + } elseif ( self::$options['cache_method'] === 'serialize' ) { + file_put_contents( $cache_file, serialize( $rules ) ); + Less_Cache::CleanCache( self::$options['cache_dir'] ); } } @@ -650,9 +685,10 @@ private function GetRules( $file_path ) { } /** - * Set up the input buffer + * @internal since 4.3.0 No longer a public API. */ - public function SetInput( $file_path ) { + private function setInput( $file_path ) { + // Set up the input buffer if ( $file_path ) { $this->input = file_get_contents( $file_path ); } @@ -670,14 +706,16 @@ public function SetInput( $file_path ) { } /** - * Free up some memory + * @internal since 4.3.0 No longer a public API. */ - public function UnsetInput() { - $this->input = $this->pos = $this->input_len = $this->furthest = null; + private function unsetInput() { + // Free up some memory + $this->input = ''; + $this->pos = $this->input_len = $this->furthest = 0; $this->saveStack = []; } - public function CacheFile( $file_path ) { + private function cacheFile( $file_path ) { if ( $file_path && $this->CacheEnabled() ) { $env = get_object_vars( $this->env ); @@ -690,26 +728,22 @@ public function CacheFile( $file_path ) { $parts[] = $env; $parts[] = Less_Version::cache_version; $parts[] = self::$options['cache_method']; - return Less_Cache::$cache_dir . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache'; + return self::$options['cache_dir'] . Less_Cache::$prefix . base_convert( sha1( json_encode( $parts ) ), 16, 36 ) . '.lesscache'; } } - public static function AddParsedFile( $file ) { - self::$imports[] = $file; - } - - public static function AllParsedFiles() { - return self::$imports; - } - /** - * @param string $file + * @since 4.3.0 + * @return string[] */ - public static function FileParsed( $file ) { - return in_array( $file, self::$imports ); + public function getParsedFiles() { + return $this->env->imports; } - public function save() { + /** + * @internal since 4.3.0 No longer a public API. + */ + private function save() { $this->saveStack[] = $this->pos; } @@ -735,45 +769,6 @@ 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 null|string|array|Less_Tree - */ - private function matcher( $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; - } - } - } - /** * Match a single character in the input. * @@ -781,7 +776,7 @@ private function matcher( $toks ) { * @return string|null * @see less-2.5.3.js#parserInput.$char */ - private function MatchChar( $tok ) { + private function matchChar( $tok ) { if ( ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ) ) { $this->skipWhitespace( 1 ); return $tok; @@ -791,13 +786,179 @@ private function MatchChar( $tok ) { /** * Match a regexp from the current start point * - * @return array|null + * @return string|array|null + * @see less-2.5.3.js#parserInput.$re */ - private function MatchReg( $tok ) { + private function matchReg( $tok ) { if ( preg_match( $tok, $this->input, $match, 0, $this->pos ) ) { $this->skipWhitespace( strlen( $match[0] ) ); - return $match; + return count( $match ) === 1 ? $match[0] : $match; + } + } + + /** + * Match an exact string of characters. + * + * @param string $tok + * @return string|null + * @see less-2.5.3.js#parserInput.$str + */ + private function matchStr( $tok ) { + $tokLength = strlen( $tok ); + if ( + ( $this->pos < $this->input_len ) && + substr( $this->input, $this->pos, $tokLength ) === $tok + ) { + $this->skipWhitespace( $tokLength ); + return $tok; + } + } + + /** + * @param int|null $loc + * @return array|string|void|null + * @see less-3.13.1.js#parserInput.$quoted + */ + private function parseQuoted( $loc = null ) { + $pos = $loc ?? $this->pos; + $startChar = $this->input[ $pos ] ?? ''; + if ( $startChar !== '\'' && $startChar !== '"' ) { + return; + } + $currentPos = $pos; + $i = 1; + while ( $currentPos + $i < $this->input_len ) { + // Optimization: Skip over irrelevant chars without slow loop + $i += strcspn( $this->input, "\n\r$startChar\\", $currentPos + $i ); + switch ( $this->input[$currentPos + $i++] ) { + case "\\": + $i++; + break; + case "\r": + case "\n": + break; + case $startChar: + // NOTE: Our optimization means we look ahead instead of behind, + // so no +1s here. + $str = substr( $this->input, $currentPos, $i ); + if ( !$loc && $loc !== 0 ) { + $this->skipWhitespace( $i ); + return $str; + } + return [ $startChar, $str ]; + } } + return null; + } + + /** + * Permissive parsing. Ignores everything except matching {} [] () and quotes + * until matching token (outside of blocks) + * @see less-3.13.1.js#parserInput.$parseUntil + */ + private function parseUntil( $tok ) { + $quote = ''; + $returnVal = null; + $inComment = false; + $blockDepth = 0; + $blockStack = []; + $parseGroups = []; + $startPos = $this->pos; + $lastPos = $this->pos; + $i = $this->pos; + $loop = true; + if ( is_string( $tok ) ) { + $testChar = static function ( $char ) use ( $tok ) { + return $tok === $char; + }; + } else { + $testChar = static function ( $char ) use ( $tok ) { + return in_array( $char, $tok ); + }; + } + do { + $nextChar = $this->input[$i]; + if ( $blockDepth === 0 && $testChar( $nextChar ) ) { + $returnVal = substr( $this->input, $lastPos, $i - $lastPos ); + if ( $returnVal ) { + $parseGroups[] = $returnVal; + } else { + $parseGroups[] = ' '; + } + $returnVal = $parseGroups; + $this->skipWhitespace( $i - $startPos ); + $loop = false; + } else { + if ( $inComment ) { + if ( $nextChar === '*' && ( $this->input[$i + 1] ?? '' ) === '/' ) { + $i++; + $blockDepth--; + $inComment = false; + } + $i++; + continue; + } + switch ( $nextChar ) { + case '\\': + $i++; + $nextChar = $this->input[$i] ?? ''; + $parseGroups[] = substr( $this->input, $lastPos, $i - $lastPos + 1 ); + $lastPos = $i + 1; + break; + case '/': + if ( ( $this->input[$i + 1] ?? '' ) === '*' ) { + $i++; + $inComment = true; + $blockDepth++; + } + break; + case '\'': + case '"': + $quote = $this->parseQuoted( $i ); + if ( $quote ) { + $parseGroups[] = substr( $this->input, $lastPos, $i - $lastPos ); + $parseGroups[] = $quote; + $i += strlen( $quote[1] ) - 1; + $lastPos = $i + 1; + } else { + $this->skipWhitespace( $i - $startPos ); + $returnVal = $nextChar; + $loop = false; + } + break; + case '{': + $blockStack[] = '}'; + $blockDepth++; + break; + case '(': + $blockStack[] = ')'; + $blockDepth++; + break; + case '[': + $blockStack[] = ']'; + $blockDepth++; + break; + case '}': + case ')': + case ']': + $expected = array_pop( $blockStack ); + if ( $nextChar === $expected ) { + $blockDepth--; + } else { + // move the parser to the error and return expected; + $this->skipWhitespace( $i - $startPos ); + $returnVal = $expected; + $loop = false; + } + } + $i++; + if ( $i > $this->input_len ) { + $loop = false; + } + } + } while ( $loop ); + + return $returnVal ?: null; } /** @@ -807,14 +968,14 @@ private function MatchReg( $tok ) { * @param string $tok * @return int|false */ - public function PeekReg( $tok ) { + private function peekReg( $tok ) { return preg_match( $tok, $this->input, $match, 0, $this->pos ); } /** * @param string $tok */ - public function PeekChar( $tok ) { + private function peekChar( $tok ) { return ( $this->pos < $this->input_len ) && ( $this->input[$this->pos] === $tok ); } @@ -822,31 +983,73 @@ public function PeekChar( $tok ) { * @param int $length * @see less-2.5.3.js#skipWhitespace */ - public function skipWhitespace( $length ) { + private function skipWhitespace( $length ) { $this->pos += $length; - // Optimization: Skip over irrelevant chars without slow loop - $this->pos += strspn( $this->input, "\n\r\t ", $this->pos ); + + for ( ; $this->pos < $this->input_len; $this->pos++ ) { + $currentChar = $this->input[$this->pos]; + + if ( $this->autoCommentAbsorb && $currentChar === '/' ) { + $nextChar = $this->input[$this->pos + 1] ?? ''; + if ( $nextChar === '/' ) { + $comment = [ 'index' => $this->pos, 'isLineComment' => true ]; + $nextNewLine = strpos( $this->input, "\n", $this->pos + 2 ); + if ( $nextNewLine === false ) { + $nextNewLine = $this->input_len ?? 0; + } + $this->pos = $nextNewLine; + $comment['text'] = substr( $this->input, $this->pos, $nextNewLine - $this->pos ); + $this->commentStore[] = $comment; + continue; + } elseif ( $nextChar === '*' ) { + $nextStarSlash = strpos( $this->input, "*/", $this->pos + 2 ); + if ( $nextStarSlash !== false ) { + $comment = [ + 'index' => $this->pos, + 'text' => substr( $this->input, $this->pos, $nextStarSlash + 2 - $this->pos ), + 'isLineComment' => false, + ]; + $this->pos += strlen( $comment['text'] ) - 1; + $this->commentStore[] = $comment; + continue; + } + } + break; + } + + // Optimization: Skip over irrelevant chars without slow loop + $skip = strspn( $this->input, " \n\t\r", $this->pos ); + if ( $skip ) { + $this->pos += $skip - 1; + } + if ( !$skip && $this->pos < $this->input_len ) { + break; + } + } } /** + * Parse a token from a regexp or method name string + * * @param string $tok * @param string|null $msg + * @return string|array|never + * @see less-3.13.1.js#Parser.expect */ - public function expect( $tok, $msg = null ) { - $result = $this->matcher( [ $tok ] ); - if ( !$result ) { - $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); - } else { + private function expect( $tok, $msg = null ) { + $result = $this->matchReg( $tok ); + if ( $result ) { return $result; } + $this->Error( $msg ?? "expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" ); } /** * @param string $tok * @param string|null $msg */ - public function expectChar( $tok, $msg = null ) { - $result = $this->MatchChar( $tok ); + private function expectChar( $tok, $msg = null ) { + $result = $this->matchChar( $tok ); if ( !$result ) { $msg = $msg ?: "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'"; $this->Error( $msg ); @@ -855,12 +1058,67 @@ public function expectChar( $tok, $msg = null ) { } } + /** + * @param string $str + * @see less-3.13.1.js#ParserInput.start + */ + private function parserInputStart( $str ) { + $this->pos = $this->furthest = 0; + $this->input = $str; + $this->input_len = strlen( $str ); + $this->skipWhitespace( 0 ); + } + + /** + * Used after initial parsing to create nodes on the fly + * + * @param string $str string to parse + * @param string[] $parseList array of parsers to run input through e.g. ["value", "important"] + * @param int $currentIndex start number to begin indexing + * @param array $fileInfo fileInfo to attach to created nodes + * @return array + * @see less-3.13.1.js#Parser.parseNode + */ + public function parseNode( $str, array $parseList, $currentIndex, $fileInfo ) { + $returnNodes = []; + try { + $this->parserInputStart( $str ); + foreach ( $parseList as $p ) { + $i = $this->pos; + $method = 'parse' . ucfirst( $p ); + if ( !method_exists( $this, $method ) ) { + throw new CompileError( 'Unknown parser ' . $p ); + } + $result = $this->$method(); + if ( $result ) { + $result->index = $i + $currentIndex; + $result->currentFileInfo = $fileInfo; + $returnNodes[] = $result; + } else { + $returnNodes[] = null; + } + } + if ( $this->pos >= $this->input_len ) { + return [ null, $returnNodes ]; + } else { + return [ true, null ]; + } + } catch ( Less_Exception_Parser $e ) { + throw new Less_Exception_Parser( + $e->getMessage(), + $e, + ( $e->index ?? 0 ) + $currentIndex, + $fileInfo + ); + } + } + // // Here in, the parsing rules/functions // // The basic structure of the syntax tree generated is as follows: // - // Ruleset -> Rule -> Value -> Expression -> Entity + // Ruleset -> Declaration -> Value -> Expression -> Entity // // Here's some LESS code: // @@ -874,9 +1132,9 @@ public function expectChar( $tok, $msg = null ) { // 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]]])) + // Declaration ("color", Value ([Expression [Color #fff]])) + // Declaration ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Declaration ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) // Ruleset (Selector [Element '>', '.child'], [...]) // ]) // @@ -893,7 +1151,7 @@ public function expectChar( $tok, $msg = null ) { // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, // as represented by this simplified grammar: // - // primary → (ruleset | rule)+ + // primary → (ruleset | declaration )+ // ruleset → selector+ block // block → '{' primary '}' // @@ -906,10 +1164,23 @@ private function parsePrimary() { while ( true ) { + while ( true ) { + $node = $this->parseComment(); + if ( !$node ) { + break; + } + $root[] = $node; + } + + // always process comments before deciding if finished if ( $this->pos >= $this->input_len ) { break; } + if ( $this->peekChar( '}' ) ) { + break; + } + $node = $this->parseExtend( true ); if ( $node ) { $root = array_merge( $root, $node ); @@ -918,63 +1189,58 @@ private function parsePrimary() { $node = $this->parseMixinDefinition() // Optimisation: NameValue is specific to less.php - ?? $this->parseNameValue() - ?? $this->parseRule() + /** + * TODO enabling $this->parseNameValue causes property-accessors to fail with + * + * 'error evaluating function `lighten` The first argument to lighten must be a + * color index: 146 in property-accessors.less on line 9, + * + * note: the Less_Tree_NameValue specifies that it may break color keyword + * interpretation + */ + // ?? $this->parseNameValue() + ?? $this->parseDeclaration() ?? $this->parseRuleset() - ?? $this->parseMixinCall() - ?? $this->parseComment() - ?? $this->parseRulesetCall() - ?? $this->parseDirective(); + ?? $this->parseMixinCall( false, false ) + ?? $this->parseVariableCall() + ?? $this->parseEntitiesCall() + ?? $this->parseAtRule(); if ( $node ) { $root[] = $node; - } elseif ( !$this->MatchReg( '/\\G[\s\n;]+/' ) ) { + } 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. + /** + * comments are collected by the main parsing mechanism and then assigned to nodes + * where the current structure allows it + * + * @return Less_Tree_Comment|void + * @see less-2.5.3.js#parsers.comment + */ private function parseComment() { - $char = $this->input[$this->pos] ?? null; - if ( $char !== '/' ) { - return; - } - - $nextChar = $this->input[$this->pos + 1] ?? null; - if ( $nextChar === '/' ) { - $match = $this->MatchReg( '/\\G\/\/.*/' ); - return new Less_Tree_Comment( $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 + $comment = array_shift( $this->commentStore ); if ( $comment ) { - return new Less_Tree_Comment( $comment[0], false, $this->pos, $this->env->currentFileInfo ); + return new Less_Tree_Comment( + $comment['text'], + $comment['isLineComment'], + $comment['index'], + $this->env->currentFileInfo + ); } } - private function parseComments() { - $comments = []; - - while ( $this->pos < $this->input_len ) { - $comment = $this->parseComment(); - if ( !$comment ) { - break; - } - - $comments[] = $comment; - } - - return $comments; + /** + * @see less-3.13.1.js#parsers.entities.mixinLookup + */ + private function parseEntitiesMixinLookup() { + return $this->parseMixinCall( true, true ); } /** @@ -983,52 +1249,35 @@ private function parseComments() { * "milky way" 'he\'s the one!' * * @return Less_Tree_Quoted|null - * @see less-2.5.3.js#entities.quoted - */ - private function parseEntitiesQuoted() { - // Optimization: Determine match potential without save()/restore() overhead - // Optimization: Inline MatchChar() here, with its skipWhitespace(1) call below - $startChar = $this->input[$this->pos] ?? null; - $isEscaped = $startChar === '~'; - if ( !$isEscaped && $startChar !== "'" && $startChar !== '"' ) { + * @see less-3.13.1.js#entities.quoted + */ + private function parseEntitiesQuoted( $forceEscaped = false ) { + // Optimization: Inline matchChar() here, with its skipWhitespace(1) call below + $isEscaped = ( $this->input[ $this->pos ] ?? null ) === '~'; + $index = $this->pos; + if ( $forceEscaped && !$isEscaped ) { return; } - - $index = $this->pos; + // Optimization: Move save() down to avoid save()+restore() + // overhead during the early return above which is a hot code path. $this->save(); - if ( $isEscaped ) { $this->skipWhitespace( 1 ); - $startChar = $this->input[$this->pos] ?? null; - if ( $startChar !== "'" && $startChar !== '"' ) { - $this->restore(); - return; - } } - // Optimization: Inline matching of quotes for 8% overall speed up - // on large LESS files. https://gerrit.wikimedia.org/r/939727 - // @see less-2.5.3.js#parserInput.$quoted - $i = 1; - while ( $this->pos + $i < $this->input_len ) { - // Optimization: Skip over irrelevant chars without slow loop - $i += strcspn( $this->input, "\n\r$startChar\\", $this->pos + $i ); - switch ( $this->input[$this->pos + $i++] ) { - case "\\": - $i++; - break; - case "\r": - case "\n": - break 2; - case $startChar: - $str = substr( $this->input, $this->pos, $i ); - $this->skipWhitespace( $i ); - $this->forget(); - return new Less_Tree_Quoted( $str[0], substr( $str, 1, -1 ), $isEscaped, $index, $this->env->currentFileInfo ); - } + $str = $this->parseQuoted(); + if ( !$str ) { + $this->restore(); + return; } - - $this->restore(); + $this->forget(); + return new Less_Tree_Quoted( + $str[0], + substr( $str, 1, -1 ), + $isEscaped, + $index, + $this->env->currentFileInfo + ); } /** @@ -1037,36 +1286,13 @@ private function parseEntitiesQuoted() { * black border-collapse * * @return Less_Tree_Keyword|Less_Tree_Color|null + * @see less-3.13.1.js#parsers.entities.keyword */ 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-]*/' ); + $k = $this->matchChar( '%' ) + ?? $this->matchReg( '/\\G\\[?(?:[\\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+\\]?/' ); if ( $k ) { - $k = $k[0]; - $color = $this->fromKeyword( $k ); - if ( $color ) { - return $color; - } - return new Less_Tree_Keyword( $k ); - } - } - - // duplicate of Less_Tree_Color::FromKeyword - private function FromKeyword( $keyword ) { - $c = $keyword = strtolower( $keyword ); - - if ( Less_Colors::hasOwnProperty( $keyword ) ) { - // detect named color - $c = new Less_Tree_Color( substr( Less_Colors::color( $keyword ), 1 ) ); - } - - if ( $keyword === 'transparent' ) { - $c = new Less_Tree_Color( [ 0, 0, 0 ], 0, true ); - } - - if ( isset( $c ) && is_object( $c ) ) { - $c->value = $keyword; - return $c; + return Less_Tree_Color::fromKeyword( $k ) ?? new Less_Tree_Keyword( $k ); } } @@ -1080,60 +1306,94 @@ private function FromKeyword( $keyword ) { // // The arguments are parsed with the `entities.arguments` parser. // + // @see less-3.13.1.js#parsers.entities.call private function parseEntitiesCall() { $index = $this->pos; - if ( !preg_match( '/\\G([\w-]+|%|progid:[\w\.]+)\(/', $this->input, $name, 0, $this->pos ) ) { + if ( $this->peekReg( '/\\Gurl\(/i' ) ) { return; } - $name = $name[1]; - $nameLC = strtolower( $name ); - if ( $nameLC === 'url' ) { - return null; + $this->save(); + + $name = $this->matchReg( '/\\G([\w-]+|%|progid:[\w\.]+)\(/' ); + if ( !$name ) { + $this->forget(); + return; } - $this->pos += strlen( $name ); + $args = null; + $name = $name[1]; + + // NOTE: Inline equivalent of less-3.13.1.js#customFuncCall + $nameLC = strtolower( $name ); if ( $nameLC === 'alpha' ) { - $alpha_ret = $this->parseAlpha(); - if ( $alpha_ret ) { - return $alpha_ret; + $args = $this->parseAlpha(); + // NOTE: Equivalent of stop=true for parseAlpha in customFuncCall() + if ( $args ) { + $this->forget(); + return $args; } } + if ( $nameLC === 'boolean' || $nameLC === 'if' ) { + $args = [ $this->parseCondition() ?? $this->Error( 'expected condition' ) ]; + } - $this->MatchChar( '(' ); // Parse the '(' and consume whitespace. - - $args = $this->parseEntitiesArguments(); + $args = $this->parseEntitiesArguments( $args ); - if ( !$this->MatchChar( ')' ) ) { + if ( !$this->matchChar( ')' ) ) { + $this->restore(); return; } - if ( $name ) { - return new Less_Tree_Call( $name, $args, $index, $this->env->currentFileInfo ); - } + $this->forget(); + return new Less_Tree_Call( $name, $args, $index, $this->env->currentFileInfo ); } /** * Parse a list of arguments * * @return array + * @see less-3.13.1.js#parsers.entities.arguments */ - private function parseEntitiesArguments() { - $args = []; + private function parseEntitiesArguments( $prevArgs = null ) { + // NOTE: In Less.js, prevArgs can be undefined (no args parsed) or false (set below). + // We treat both of those as null in PHP so that we can use ?? to easily distinguish + // these, without treating empty array the same as false. + $argsComma = $prevArgs ?? []; + $argsSemiColon = []; + $isSemiColonSeparated = false; + $this->save(); while ( true ) { - $arg = $this->parseEntitiesAssignment() ?? $this->parseExpression(); - if ( !$arg ) { - break; + if ( $prevArgs !== null ) { + $prevArgs = null; + } else { + $value = $this->parseDetachedRuleset() ?? $this->parseEntitiesAssignment() ?? $this->parseExpression(); + if ( !$value ) { + break; + } + + if ( $value instanceof Less_Tree_Expression && count( $value->value ) == 1 ) { + $value = $value->value[0]; + } + $argsComma[] = $value; } - $args[] = $arg; - if ( !$this->MatchChar( ',' ) ) { - break; + if ( $this->matchChar( ',' ) ) { + continue; + } + if ( $this->matchChar( ';' ) || $isSemiColonSeparated ) { + $isSemiColonSeparated = true; + // NOTE: Avoid apparent Less.js bug, accessing undefined argsComma[0] + $value = !$argsComma ? null : new Less_Tree_Value( $argsComma ); + $argsSemiColon[] = $value; + $argsComma = []; } + } - return $args; + $this->forget(); + return $isSemiColonSeparated ? $argsSemiColon : $argsComma; } /** @return Less_Tree_Dimension|Less_Tree_Color|Less_Tree_Quoted|Less_Tree_UnicodeDescriptor|null */ @@ -1149,20 +1409,21 @@ private function parseEntitiesLiteral() { * filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) * * @return Less_Tree_Assignment|null + * @see less-2.5.3.js#parsers.entities.assignment */ private function parseEntitiesAssignment() { - $key = $this->MatchReg( '/\\G\w+(?=\s?=)/' ); + $key = $this->matchReg( '/\\G\w+(?=\s?=)/' ); if ( !$key ) { return; } - if ( !$this->MatchChar( '=' ) ) { + if ( !$this->matchChar( '=' ) ) { return; } $value = $this->parseEntity(); if ( $value ) { - return new Less_Tree_Assignment( $key[0], $value ); + return new Less_Tree_Assignment( $key, $value ); } } @@ -1175,19 +1436,30 @@ private function parseEntitiesAssignment() { // private function parseEntitiesUrl() { $char = $this->input[$this->pos] ?? null; + + $this->autoCommentAbsorb = false; // Optimisation: 'u' check is specific to less.php if ( $char !== 'u' || !$this->matchReg( '/\\Gurl\(/' ) ) { + $this->autoCommentAbsorb = true; return; } - $value = $this->matcher( [ 'parseEntitiesQuoted','parseEntitiesVariable','/\\Gdata\:.*?[^\)]+/','/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ] ); + $value = $this->parseEntitiesQuoted() + ?? $this->parseEntitiesVariable() + ?? $this->parseEntitiesProperty() + ?? $this->matchReg( '/\\Gdata\:.*?[^\)]+/' ) // TODO less doesn't handle this + ?? $this->matchReg( '/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ) + ?? null; + if ( !$value ) { $value = ''; } - + $this->autoCommentAbsorb = true; $this->expectChar( ')' ); - if ( $value instanceof Less_Tree_Quoted || $value instanceof Less_Tree_Variable ) { + if ( $value instanceof Less_Tree_Quoted + || $value instanceof Less_Tree_Variable + || $value instanceof Less_Tree_Property ) { return new Less_Tree_Url( $value, $this->env->currentFileInfo ); } @@ -1202,13 +1474,32 @@ private function parseEntitiesUrl() { * We use a different parser for variable definitions, * see `parsers.variable`. * - * @return Less_Tree_Variable|null + * @return Less_Tree_Variable|Less_Tree_VariableCall|Less_Tree_NamespaceValue|null + * @see less-3.13.1.js#parsers.entities.variable */ private function parseEntitiesVariable() { $index = $this->pos; - if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G@@?[\w-]+/' ) ) ) { - return new Less_Tree_Variable( $name[0], $index, $this->env->currentFileInfo ); + $this->save(); + + if ( $this->peekChar( '@' ) ) { + $name = $this->matchReg( '/\\G@@?[\w-]+/' ); + if ( $name ) { + $ch = $this->input[ $this->pos ] ?? ''; + $prevChar = $this->input[ $this->pos - 1 ] ?? ''; + if ( $ch === '(' || ( $ch === '[' && !preg_match( '/\s/', $prevChar, $match ) ) ) { + // this may be a VariableCall lookup + $result = $this->parseVariableCall( $name ); + if ( $result ) { + $this->forget(); + return $result; + } + } + $this->forget(); + return new Less_Tree_Variable( $name, $index, $this->env->currentFileInfo ); + } } + + $this->restore(); } /** @@ -1219,8 +1510,39 @@ private function parseEntitiesVariable() { private function parseEntitiesVariableCurly() { $index = $this->pos; - if ( $this->input_len > ( $this->pos + 1 ) && $this->input[$this->pos] === '@' && ( $curly = $this->MatchReg( '/\\G@\{([\w-]+)\}/' ) ) ) { - return new Less_Tree_Variable( '@' . $curly[1], $index, $this->env->currentFileInfo ); + if ( $this->input_len > ( $this->pos + 1 ) && $this->input[$this->pos] === '@' ) { + $curly = $this->matchReg( '/\\G@\{([\w-]+)\}/' ); + if ( $curly ) { + return new Less_Tree_Variable( '@' . $curly[1], $index, $this->env->currentFileInfo ); + } + } + } + + /** + * A Property accessor, such as `$color`, in + * + * background-color: $color + */ + private function parseEntitiesProperty() { + $index = $this->pos; + + if ( ( $this->input[$this->pos] ?? '' ) === '$' ) { + $name = $this->matchReg( '/\\G\$[\w-]+/' ); + if ( $name ) { + return new Less_Tree_Property( $name, $index, $this->env->currentFileInfo ); + } + } + } + + // A property entity useing the protective {} e.g. @{prop} + private function parseEntitiesPropertyCurly() { + $index = $this->pos; + + if ( $this->input[$this->pos] === '$' ) { + $curly = $this->matchReg( '/\\G@\{([\w-]+)\}/' ); + if ( $curly ) { + return new Less_Tree_Property( "$" . $curly[1], $index, $this->env->currentFileInfo ); + } } } @@ -1234,8 +1556,11 @@ private function parseEntitiesVariableCurly() { * @return Less_Tree_Color|null */ private function parseEntitiesColor() { - if ( $this->PeekChar( '#' ) && ( $rgb = $this->MatchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' ) ) ) { - return new Less_Tree_Color( $rgb[1] ); + if ( $this->peekChar( '#' ) ) { + $rgb = $this->matchReg( '/\\G#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/' ); + if ( $rgb ) { + return new Less_Tree_Color( $rgb[1], 1, $rgb[0] ); + } } } @@ -1247,14 +1572,14 @@ private function parseEntitiesColor() { * @return Less_Tree_Dimension|null */ private function parseEntitiesDimension() { - $c = @ord( $this->input[$this->pos] ); + $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]+)?/' ); + $value = $this->matchReg( '/\\G([+-]?\d*\.?\d+)(%|[a-z]+)?/i' ); if ( $value ) { if ( isset( $value[2] ) ) { return new Less_Tree_Dimension( $value[1], $value[2] ); @@ -1270,14 +1595,14 @@ private function parseEntitiesDimension() { * * @return Less_Tree_UnicodeDescriptor|null */ - public function parseUnicodeDescriptor() { - // Optimization: Hardcode first char, to avoid MatchReg() cost for common case + private function parseUnicodeDescriptor() { + // Optimization: Hardcode first char, to avoid matchReg() cost for common case $char = $this->input[$this->pos] ?? null; if ( $char !== 'U' ) { return; } - $ud = $this->MatchReg( '/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/' ); + $ud = $this->matchReg( '/\\G(U\+[0-9a-fA-F?]+)(\-[0-9a-fA-F?]+)?/' ); if ( $ud ) { return new Less_Tree_UnicodeDescriptor( $ud[0] ); } @@ -1289,11 +1614,11 @@ public function parseUnicodeDescriptor() { * `window.location.href` * * @return Less_Tree_JavaScript|null - * @see less-2.5.3.js#parsers.entities.javascript + * @see less-3.13.1.js#parsers.entities.javascript */ private function parseEntitiesJavascript() { // Optimization: Hardcode first char, to avoid save()/restore() overhead - // Optimization: Inline MatchChar(), with skipWhitespace(1) below + // Optimization: Inline matchChar(), with skipWhitespace(1) below $char = $this->input[$this->pos] ?? null; $isEscaped = $char === '~'; if ( !$isEscaped && $char !== '`' ) { @@ -1313,46 +1638,80 @@ private function parseEntitiesJavascript() { } $this->skipWhitespace( 1 ); - $js = $this->MatchReg( '/\\G[^`]*`/' ); + $js = $this->matchReg( '/\\G[^`]*`/' ); if ( $js ) { $this->forget(); - return new Less_Tree_JavaScript( substr( $js[0], 0, -1 ), $index, $isEscaped ); + return new Less_Tree_JavaScript( substr( $js, 0, -1 ), $isEscaped, $index ); } $this->restore(); } - // // The variable part of a variable definition. Used in the `rule` parser // - // @fink: + // @fink: // - // @see less-2.5.3.js#parsers.variable + // @see less-3.13.1.js#parsers.variable private function parseVariable() { - if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*:/' ) ) ) { - return $name[1]; + if ( $this->peekChar( '@' ) ) { + $name = $this->matchReg( '/\\G(@[\w-]+)\s*:/' ); + if ( $name ) { + return $name[1]; + } } } + // Call a variable value to retrieve a detached ruleset + // or a value from a detached ruleset's rules. // - // The variable part of a variable definition. Used in the `rule` parser - // - // @fink(); + // @fink(); + // @fink; + // color: @fink[@color]; // - // @see less-2.5.3.js#parsers.rulesetCall - private function parseRulesetCall() { - if ( $this->PeekChar( '@' ) && ( $name = $this->MatchReg( '/\\G(@[\w-]+)\s*\(\s*\)\s*;/' ) ) ) { - return new Less_Tree_RulesetCall( $name[1] ); + // @see less-3.13.1.js#parsers.variableCall + private function parseVariableCall( $parsedName = null ) { + $i = $this->pos; + $inValue = (bool)$parsedName; + + if ( $parsedName === null && !$this->peekChar( '@' ) ) { + return; + } + $this->save(); + $name = $parsedName ?? $this->matchReg( '/\\G(@[\w-]+)(\(\s*\))?/' ); + if ( $name === null ) { + $this->restore(); + return; + } + + $lookups = $this->parseMixinRuleLookups(); + if ( !$lookups && ( + ( $inValue && $this->matchStr( '()' ) !== '()' ) || ( ( $name[2] ?? '' ) !== '()' ) ) ) { + // Restore error mesage: 'Missing \'[...]\' lookup in variable call' + $this->restore(); + return; + } + if ( !$inValue ) { + $name = $name[1]; + } + + $call = new Less_Tree_VariableCall( $name, $i, $this->env->currentFileInfo ); + if ( !$inValue && $this->parseEnd() ) { + $this->forget(); + return $call; + } else { + $this->forget(); + return new Less_Tree_NamespaceValue( $call, $lookups, $i, $this->env->currentFileInfo ); } } // // extend syntax - used to extend selectors // - public function parseExtend( $isRule = false ) { + // @see less-2.5.3.js#parsers.extend + private function parseExtend( $isRule = false ) { $index = $this->pos; $extendList = []; - if ( !$this->MatchReg( $isRule ? '/\\G&:extend\(/' : '/\\G:extend\(/' ) ) { + if ( !$this->matchStr( $isRule ? '&:extend(' : ':extend(' ) ) { return; } @@ -1360,7 +1719,7 @@ public function parseExtend( $isRule = false ) { $option = null; $elements = []; while ( true ) { - $option = $this->MatchReg( '/\\G(all)(?=\s*(\)|,))/' ); + $option = $this->matchReg( '/\\G(all)(?=\s*(\)|,))/' ); if ( $option ) { break; } @@ -1377,7 +1736,7 @@ public function parseExtend( $isRule = false ) { $extendList[] = new Less_Tree_Extend( new Less_Tree_Selector( $elements ), $option, $index ); - } while ( $this->MatchChar( "," ) ); + } while ( $this->matchChar( "," ) ); $this->expect( '/\\G\)/' ); @@ -1392,64 +1751,102 @@ public function parseExtend( $isRule = false ) { // A Mixin call, with an optional argument list // // #mixins > .square(#fff); + // #mixins.square(#fff); // .rounded(4px, black); // .button; // + // We can lookup / return a value using the lookup syntax: + // + // color: #mixin.square(#fff)[@color]; + // // 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] ?? null; - if ( $char !== '.' && $char !== '#' ) { + // @see less-3.13.1.js#parsers.mixin.call + // + private function parseMixinCall( $inValue, $getLookup = null ) { + $s = $this->input[$this->pos] ?? null; + $important = false; + $lookups = null; + $index = $this->pos; + $args = []; + $hasParens = false; + if ( $s !== '.' && $s !== '#' ) { 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']; + if ( $this->matchChar( '(' ) ) { + $args = ( $this->parseMixinArgs( true ) )['args']; $this->expectChar( ')' ); - } else { - $args = []; + $hasParens = true; + } + if ( $getLookup !== false ) { + $lookups = $this->parseMixinRuleLookups(); + } + if ( $getLookup === true && $lookups === null ) { + $this->restore(); + return; + } + if ( $inValue && !$lookups && !$hasParens ) { + // This isn't a valid in-value mixin call + $this->restore(); + return; } - $important = $this->parseImportant(); + if ( !$inValue && $this->parseImportant() ) { + $important = true; + } - if ( $this->parseEnd() ) { + if ( $inValue || $this->parseEnd() ) { $this->forget(); - return new Less_Tree_Mixin_Call( $elements, $args, $index, $this->env->currentFileInfo, $important ); + $mixin = new Less_Tree_Mixin_Call( + $elements, + $args, + $index, + $this->env->currentFileInfo, + !$lookups && $important + ); + if ( $lookups ) { + return new Less_Tree_NamespaceValue( $mixin, $lookups ); + } else { + return $mixin; + } } } $this->restore(); } + /** + * Matching elements for mixins + * (Start with . or # and can have > ) + * @see less-3.13.1.js#parsers.mixin.elements + */ private function parseMixinCallElements() { $elements = []; $c = null; while ( true ) { $elemIndex = $this->pos; - $e = $this->MatchReg( '/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' ); + $e = $this->matchReg( '/\\G[#.](?:[\w-]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' ); if ( !$e ) { break; } - $elements[] = new Less_Tree_Element( $c, $e[0], $elemIndex, $this->env->currentFileInfo ); - $c = $this->MatchChar( '>' ); + $elements[] = new Less_Tree_Element( $c, $e, $elemIndex, $this->env->currentFileInfo ); + $c = $this->matchChar( '>' ); } - return $elements; + return $elements ?: null; } /** * @param bool $isCall + * @see less-2.5.3.js#parsers.mixin.args */ private function parseMixinArgs( $isCall ) { $expressions = []; @@ -1459,6 +1856,7 @@ private function parseMixinArgs( $isCall ) { $expressionContainsNamed = null; $name = null; $returner = [ 'args' => [], 'variadic' => false ]; + $expand = false; $this->save(); @@ -1466,10 +1864,10 @@ private function parseMixinArgs( $isCall ) { if ( $isCall ) { $arg = $this->parseDetachedRuleset() ?? $this->parseExpression(); } else { - $this->parseComments(); - if ( $this->input[ $this->pos ] === '.' && $this->MatchReg( '/\\G\.{3}/' ) ) { + $this->commentStore = []; + if ( $this->input[ $this->pos ] === '.' && $this->matchStr( '...' ) ) { $returner['variadic'] = true; - if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { + if ( $this->matchChar( ";" ) && !$isSemiColonSeperated ) { $isSemiColonSeperated = true; } @@ -1480,7 +1878,10 @@ private function parseMixinArgs( $isCall ) { } break; } - $arg = $this->parseEntitiesVariable() ?? $this->parseEntitiesLiteral() ?? $this->parseEntitiesKeyword(); + $arg = $this->parseEntitiesVariable() + ?? $this->parseEntitiesProperty() + ?? $this->parseEntitiesLiteral() + ?? $this->parseEntitiesKeyword(); } if ( !$arg ) { @@ -1503,9 +1904,9 @@ private function parseMixinArgs( $isCall ) { $val = $arg; } - if ( $val instanceof Less_Tree_Variable ) { + if ( $val instanceof Less_Tree_Variable || $val instanceof Less_Tree_Property ) { - if ( $this->MatchChar( ':' ) ) { + if ( $this->matchChar( ':' ) ) { if ( $expressions ) { if ( $isSemiColonSeperated ) { $this->Error( 'Cannot mix ; and , as delimiter types' ); @@ -1516,13 +1917,7 @@ private function parseMixinArgs( $isCall ) { // 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(); - } + $value = $this->parseDetachedRuleset() ?? $this->parseExpression(); if ( !$value ) { if ( $isCall ) { @@ -1535,17 +1930,21 @@ private function parseMixinArgs( $isCall ) { } $nameLoop = ( $name = $val->name ); - } elseif ( !$isCall && $this->MatchReg( '/\\G\.{3}/' ) ) { - $returner['variadic'] = true; - if ( $this->MatchChar( ";" ) && !$isSemiColonSeperated ) { - $isSemiColonSeperated = true; - } - if ( $isSemiColonSeperated ) { - $argsSemiColon[] = [ 'name' => $arg->name, 'variadic' => true ]; + } elseif ( $this->matchStr( '...' ) ) { + if ( !$isCall ) { + $returner['variadic'] = true; + if ( $this->matchChar( ";" ) && !$isSemiColonSeperated ) { + $isSemiColonSeperated = true; + } + if ( $isSemiColonSeperated ) { + $argsSemiColon[] = [ 'name' => $arg->name, 'variadic' => true ]; + } else { + $argsComma[] = [ 'name' => $arg->name, 'variadic' => true ]; + } + break; } else { - $argsComma[] = [ 'name' => $arg->name, 'variadic' => true ]; + $expand = true; } - break; } elseif ( !$isCall ) { $name = $nameLoop = $val->name; $value = null; @@ -1556,13 +1955,13 @@ private function parseMixinArgs( $isCall ) { $expressions[] = $value; } - $argsComma[] = [ 'name' => $nameLoop, 'value' => $value ]; + $argsComma[] = [ 'name' => $nameLoop, 'value' => $value, 'expand' => $expand ]; - if ( $this->MatchChar( ',' ) ) { + if ( $this->matchChar( ',' ) ) { continue; } - if ( $this->MatchChar( ';' ) || $isSemiColonSeperated ) { + if ( $this->matchChar( ';' ) || $isSemiColonSeperated ) { if ( $expressionContainsNamed ) { $this->Error( 'Cannot mix ; and , as delimiter types' ); @@ -1573,7 +1972,7 @@ private function parseMixinArgs( $isCall ) { if ( count( $expressions ) > 1 ) { $value = new Less_Tree_Value( $expressions ); } - $argsSemiColon[] = [ 'name' => $name, 'value' => $value ]; + $argsSemiColon[] = [ 'name' => $name, 'value' => $value, 'expand' => $expand ]; $name = null; $expressions = []; @@ -1586,6 +1985,54 @@ private function parseMixinArgs( $isCall ) { return $returner; } + /** + * @see less-3.13.1.js#parsers.mixin.ruleLookups + */ + private function parseMixinRuleLookups() { + $lookups = []; + + if ( !$this->peekChar( '[' ) ) { + return; + } + + while ( true ) { + $this->save(); + $rule = $this->parseLookupValue(); + if ( !$rule && $rule !== '' ) { + $this->restore(); + break; + } + $lookups[] = $rule; + $this->forget(); + } + if ( $lookups ) { + return $lookups; + } + } + + /** + * @see less-3.13.1.js#parsers.mixin.lookupValue + */ + private function parseLookupValue() { + $this->save(); + + if ( !$this->matchChar( '[' ) ) { + $this->restore(); + return; + } + $name = $this->matchReg( "/\\G(?:[@\$]{0,2})[_a-zA-Z0-9-]*/" ); + + if ( !$this->matchChar( ']' ) ) { + $this->restore(); + return; + } + if ( $name || $name === '' ) { + $this->forget(); + return $name; + } + $this->restore(); + } + // // A Mixin definition, with a list of parameters // @@ -1605,18 +2052,19 @@ private function parseMixinArgs( $isCall ) { // Once we've got our params list, and a closing `)`, we parse // the `{...}` block. // + // @see less-2.5.3.js#parsers.mixin.definition private function parseMixinDefinition() { $cond = null; $char = $this->input[$this->pos] ?? null; // TODO: Less.js doesn't limit this to $char == '{'. - if ( ( $char !== '.' && $char !== '#' ) || ( $char === '{' && $this->PeekReg( '/\\G[^{]*\}/' ) ) ) { + 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*\(/' ); + $match = $this->matchReg( '/\\G([#.](?:[\w-]|\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/' ); if ( $match ) { $name = $match[1]; @@ -1629,15 +2077,15 @@ private function parseMixinDefinition() { // also // .mixincall(@a: {rule: set;}); // so we have to be nice and restore - if ( !$this->MatchChar( ')' ) ) { + if ( !$this->matchChar( ')' ) ) { $this->restore(); return; } - $this->parseComments(); + $this->commentStore = []; - if ( $this->MatchReg( '/\\Gwhen/' ) ) { // Guard - $cond = $this->expect( 'parseConditions', 'Expected conditions' ); + if ( $this->matchStr( 'when' ) ) { // Guard + $cond = $this->parseConditions() ?? $this->Error( 'Expected conditions' ); } $ruleset = $this->parseBlock(); @@ -1658,22 +2106,24 @@ private function parseMixinDefinition() { // and can be found inside a rule's value. // private function parseEntity() { - return $this->parseEntitiesLiteral() ?? + return $this->parseComment() ?? + $this->parseEntitiesLiteral() ?? $this->parseEntitiesVariable() ?? $this->parseEntitiesUrl() ?? + $this->parseEntitiesProperty() ?? $this->parseEntitiesCall() ?? $this->parseEntitiesKeyword() ?? - $this->parseEntitiesJavascript() ?? - $this->parseComment(); + $this->parseMixinCall( true ) ?? + $this->parseEntitiesJavascript(); } // - // A Rule terminator. Note that we use `peek()` to check for '}', + // A Declaration 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( '}' ); + return $this->matchChar( ';' ) || $this->peekChar( '}' ); } // @@ -1681,23 +2131,20 @@ private function parseEnd() { // // alpha(opacity=88) // + // @see less-3.13.1.js#parsers.ieAlpha private function parseAlpha() { - if ( !$this->MatchReg( '/\\G\(opacity=/i' ) ) { + if ( !$this->matchReg( '/\\Gopacity=/i' ) ) { return; } - $value = $this->MatchReg( '/\\G[0-9]+/' ); - if ( $value ) { - $value = $value[0]; - } else { - $value = $this->parseEntitiesVariable(); - if ( !$value ) { - return; - } + $value = $this->matchReg( '/\\G[0-9]+/' ); + if ( $value === null ) { + $value = $this->parseEntitiesVariable() ?? $this->Error( 'Could not parse alpha' ); + $value = "@{" . substr( $value->name, 1 ) . "}"; } $this->expectChar( ')' ); - return new Less_Tree_Alpha( $value ); + return new Less_Tree_Quoted( '', "alpha(opacity=" . $value . ")" ); } /** @@ -1719,22 +2166,20 @@ private function parseElement() { $c = $this->parseCombinator(); $index = $this->pos; - // TODO: Speed up by calling MatchChar directly, like less.js does - $e = $this->matcher( [ - '/\\G(?:\d+\.\d+|\d+)%/', - '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/', - '#*', - '#&', - 'parseAttribute', - '/\\G\([^&()@]+\)/', - '/\\G[\.#:](?=@)/', - 'parseEntitiesVariableCurly' - ] ); + $e = $this->matchReg( '/\\G(?:\d+\.\d+|\d+)%/' ) + ?? $this->matchReg( '/\\G(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/' ) + ?? $this->matchChar( '*' ) + ?? $this->matchChar( '&' ) + ?? $this->parseAttribute() + ?? $this->matchReg( '/\\G\([^&()@]+\)/' ) + ?? $this->matchReg( '/\\G[\.#:](?=@)/' ) + ?? $this->parseEntitiesVariableCurly(); if ( $e === null ) { $this->save(); - if ( $this->MatchChar( '(' ) ) { - if ( ( $v = $this->parseSelector() ) && $this->MatchChar( ')' ) ) { + if ( $this->matchChar( '(' ) ) { + $v = $this->parseSelector(); + if ( $v && $this->matchChar( ')' ) ) { $e = new Less_Tree_Paren( $v ); $this->forget(); } else { @@ -1762,11 +2207,21 @@ private function parseElement() { private function parseCombinator() { if ( $this->pos < $this->input_len ) { $c = $this->input[$this->pos]; + if ( $c === '/' ) { + $this->save(); + $slashedCombinator = $this->matchReg( '/\\G\/[a-z]+\//i' ); + if ( $slashedCombinator ) { + $this->forget(); + return $slashedCombinator; + } + $this->restore(); + } + // TODO: Figure out why less.js also handles '/' here, and implement with regression test. if ( $c === '>' || $c === '+' || $c === '~' || $c === '|' || $c === '^' ) { $this->pos++; - if ( $this->input[$this->pos] === '^' ) { + if ( $c === '^' && $this->input[$this->pos] === '^' ) { $c = '^^'; $this->pos++; } @@ -1814,9 +2269,10 @@ private function parseSelector( $isLess = false ) { $c = null; $index = $this->pos; - while ( ( $isLess && ( $extend = $this->parseExtend() ) ) || ( $isLess && ( $when = $this->MatchReg( '/\\Gwhen/' ) ) ) || ( $e = $this->parseElement() ) ) { + // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition + while ( ( $isLess && ( $extend = $this->parseExtend() ) ) || ( $isLess && ( $when = $this->matchStr( 'when' ) ) ) || ( $e = $this->parseElement() ) ) { if ( $when ) { - $condition = $this->expect( 'parseConditions', 'expected condition' ); + $condition = $this->parseConditions() ?? $this->Error( 'Expected condition' ); } elseif ( $condition ) { // error("CSS guard can only be used at the end of selector"); } elseif ( $extend ) { @@ -1828,7 +2284,9 @@ private function parseSelector( $isLess = false ) { if ( $this->pos < $this->input_len ) { $c = $this->input[ $this->pos ]; } - $elements[] = $e; + if ( $e !== null ) { + $elements[] = $e; + } $e = null; } @@ -1845,14 +2303,14 @@ private function parseSelector( $isLess = false ) { } } - private function parseTag() { - return $this->MatchReg( '/\\G[A-Za-z][A-Za-z-]*[0-9]?/' ) ?: $this->MatchChar( '*' ); - } - + /** + * @return Less_Tree_Attribute|null + * @see less-2.5.3.js#parsers.attribute + */ private function parseAttribute() { $val = null; - if ( !$this->MatchChar( '[' ) ) { + if ( !$this->matchChar( '[' ) ) { return; } @@ -1861,14 +2319,14 @@ private function parseAttribute() { $key = $this->expect( '/\\G(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\\\.)+/' ); } - $op = $this->MatchReg( '/\\G[|~*$^]?=/' ); + $op = $this->matchReg( '/\\G[|~*$^]?=/' ); if ( $op ) { - $val = $this->matcher( [ 'parseEntitiesQuoted','/\\G[0-9]+%/','/\\G[\w-]+/','parseEntitiesVariableCurly' ] ); + $val = $this->parseEntitiesQuoted() ?? $this->matchReg( '/\\G[0-9]+%/' ) ?? $this->matchReg( '/\\G[\w-]+/' ) ?? $this->parseEntitiesVariableCurly(); } $this->expectChar( ']' ); - return new Less_Tree_Attribute( $key, $op === null ? null : $op[0], $val ); + return new Less_Tree_Attribute( $key, $op, $val ); } /** @@ -1879,9 +2337,9 @@ private function parseAttribute() { * @see less-2.5.3.js#parsers.block */ private function parseBlock() { - if ( $this->MatchChar( '{' ) ) { + if ( $this->matchChar( '{' ) ) { $content = $this->parsePrimary(); - if ( $this->MatchChar( '}' ) ) { + if ( $this->matchChar( '}' ) ) { return $content; } } @@ -1889,12 +2347,9 @@ private function parseBlock() { private function parseBlockRuleset() { $block = $this->parseBlock(); - - if ( $block ) { - $block = new Less_Tree_Ruleset( null, $block ); + if ( $block !== null ) { + return new Less_Tree_Ruleset( null, $block ); } - - return $block; } /** @return Less_Tree_DetachedRuleset|null */ @@ -1918,26 +2373,31 @@ private function parseRuleset() { $selectors = []; $this->save(); - + // TODO: missing + // https://github.com/less/less.js/commit/b8140d4baad18ba732e2b322d8891a9b0ff065d5#diff-cad419f131cbecb0799ee17eba9319d3ff51de09eb3876efb9e4c068c1f6025f + // the commit above updated the `permissive-parse.less` fixture worked on Id36e0f142d7f430603da3f0d6825aa6a0bc9b7f1 + // and it required to add an override for permisive-parse.css. + // When working on parse interpolation, please make sure to remove the permissive-parse + // override while ( true ) { $s = $this->parseLessSelector(); if ( !$s ) { break; } $selectors[] = $s; - $this->parseComments(); + $this->commentStore = []; if ( $s->condition && count( $selectors ) > 1 ) { $this->Error( 'Guards are only currently allowed on a single selector.' ); } - if ( !$this->MatchChar( ',' ) ) { + if ( !$this->matchChar( ',' ) ) { break; } if ( $s->condition ) { $this->Error( 'Guards are only currently allowed on a single selector.' ); } - $this->parseComments(); + $this->commentStore = []; } if ( $selectors ) { @@ -1962,31 +2422,35 @@ private function parseNameValue() { $index = $this->pos; $this->save(); - $match = $this->MatchReg( '/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?) *(! *important)?\s*([;}])/' ); + $match = $this->matchReg( '/\\G([a-zA-Z\-]+)\s*:\s*([\'"]?[#a-zA-Z0-9\-%\.,]+?[\'"]?\s*) *(! *important)?\s*([;}])/' ); if ( $match ) { if ( $match[4] == '}' ) { + // because we will parse all comments after closing }, we need to reset the store as + // we're going to reset the position to closing } + $this->commentStore = []; $this->pos = $index + strlen( $match[0] ) - 1; + $match[2] = rtrim( $match[2] ); } if ( $match[3] ) { - $match[2] .= ' !important'; + $match[2] .= $match[3]; } - + $this->forget(); return new Less_Tree_NameValue( $match[1], $match[2], $index, $this->env->currentFileInfo ); } $this->restore(); } - // @see less-2.5.3.js#parsers.rule - private function parseRule( $tryAnonymous = null ) { + // @see less-3.13.1.js#parsers.declaration + private function parseDeclaration() { $value = null; - $startOfRule = $this->pos; + $index = $this->pos; + $hasDR = false; $c = $this->input[$this->pos] ?? null; $important = null; $merge = false; - // TODO: Figure out why less.js also handles ':' here, and implement with regression test. if ( $c === '.' || $c === '#' || $c === '&' ) { return; @@ -2000,60 +2464,180 @@ private function parseRule( $tryAnonymous = null ) { if ( $isVariable ) { $value = $this->parseDetachedRuleset(); + if ( $value ) { + $hasDR = true; + } } - + $this->commentStore = []; if ( !$value ) { // 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 && count( $name ) > 1 ) { + if ( !$isVariable && is_array( $name ) && count( $name ) > 1 ) { $merge = array_pop( $name )->value; } + // Custom property values get permissive parsing + if ( is_array( $name ) && array_key_exists( 0, $name ) // to satisfy phan + && $name[0] instanceof Less_Tree_Keyword + && $name[0]->value && str_starts_with( $name[0]->value, '--' ) ) { + $value = $this->parsePermissiveValue( [ ';', '}' ] ); + } else { + // Try to store values as anonymous + // If we need the value later we'll re-parse it in ruleset.parseValue + $value = $this->parseAnonymousValue(); + } - // prefer to try to parse first if its a variable or we are compressing - // but always fallback on the other one - $tryValueFirst = ( !$tryAnonymous && ( self::$options['compress'] || $isVariable ) ); - if ( $tryValueFirst ) { - $value = $this->parseValue(); + if ( $value ) { + $this->forget(); + // anonymous values absorb the end ';' which is required for them to work + return new Less_Tree_Declaration( + $name, + $value, + false, + $merge, + $index, + $this->env->currentFileInfo + ); } if ( !$value ) { - $value = $this->parseAnonymousValue(); - if ( $value ) { - $this->forget(); - // anonymous values absorb the end ';' which is required for them to work - return new Less_Tree_Rule( $name, $value, false, $merge, $startOfRule, $this->env->currentFileInfo ); - } - } - if ( !$tryValueFirst && !$value ) { $value = $this->parseValue(); } - - $important = $this->parseImportant(); + if ( $value ) { + $important = $this->parseImportant(); + } elseif ( $isVariable ) { + $value = $this->parsePermissiveValue(); + } } - - if ( $value && $this->parseEnd() ) { + if ( $value && ( $this->parseEnd() || $hasDR ) ) { $this->forget(); - return new Less_Tree_Rule( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo ); + return new Less_Tree_Declaration( $name, $value, $important, $merge, $index, $this->env->currentFileInfo ); } else { $this->restore(); - if ( $value && !$tryAnonymous ) { - return $this->parseRule( true ); - } } } else { - $this->forget(); + $this->restore(); } } - public function parseAnonymousValue() { - $match = $this->MatchReg( '/\\G([^@+\/\'"*`(;{}-]*);/' ); + /** + * @see less-3.13.1.js#parsers.anonymousValue + */ + private function parseAnonymousValue() { + $index = $this->pos; + $match = $this->matchReg( '/\\G([^.#@\$+\/\'"*`(;{}-]*);/' ); if ( $match ) { - return new Less_Tree_Anonymous( $match[1] ); + return new Less_Tree_Anonymous( $match[1], $index ); + } + } + + /** + * Used for custom properties, at-rules, and variables (as fallback) + * Parses almost anything inside of {} [] () "" blocks + * until it reaches outer-most tokens. + * + * First, it will try to parse comments and entities to reach + * the end. This is mostly like the Expression parser except no + * math is allowed. + * + * @see less-3.13.1.js#parsers.permissiveValue + * @param null|string|array $untilTokens + */ + private function parsePermissiveValue( $untilTokens = null ) { + $tok = $untilTokens ?? ';'; + $index = $this->pos; + $result = []; + + if ( is_array( $tok ) ) { + $testCurrentChar = static function ( $currentChar ) use ( $tok ) { + return in_array( $currentChar, $tok ); + }; + } else { + $testCurrentChar = static function ( $currentChar ) use ( $tok ) { + return $tok === $currentChar; + }; + } + + if ( $testCurrentChar( $this->input[$this->pos] ) ) { + return; + } + + $value = []; + do { + $e = $this->parseComment(); + if ( $e ) { + $value[] = $e; + continue; + } + $e = $this->parseEntity(); + if ( $e ) { + $value[] = $e; + } + // NOTE: Comma handling backported from Less.js 4.2.1 (T386077) + if ( $this->peekChar( ',' ) ) { + $value[] = new Less_Tree_Anonymous( ',' ); + $this->matchChar( ',' ); + } + } while ( $e ); + $done = $testCurrentChar( $this->input[$this->pos] ); + if ( $value ) { + $value = new Less_Tree_Expression( $value ); + if ( $done ) { + return $value; + } else { + $result[] = $value; + } + // Preserve space before $parseUntil as it will not + if ( $this->input[$this->pos - 1] === ' ' ) { + $result[] = new Less_Tree_Anonymous( ' ', $index ); + } + } + $this->save(); + $value = $this->parseUntil( $tok ); + + if ( $value ) { + if ( is_string( $value ) ) { + $this->Error( "expected '" . $value . "'" ); + } + if ( count( $value ) === 1 && $value[0] === ' ' ) { + $this->forget(); + return new Less_Tree_Anonymous( '', $index ); + } + $valueLength = count( $value ); + for ( $i = 0; $i < $valueLength; $i++ ) { + $item = $value[$i]; + if ( is_array( $item ) ) { + $result[] = new Less_Tree_Quoted( + $item[0], + $item[1], + true, + $index, + $this->env->currentFileInfo + ); + } else { + if ( $i === $valueLength - 1 ) { + $item = trim( $item ); + } + // Treat like quoted values, but replace vars like unquoted expressions + $quote = new Less_Tree_Quoted( + '\'', + $item, + true, + $index, + $this->env->currentFileInfo + ); + $quote->variableRegex = '/@([\w-]+)/'; + $quote->propRegex = '/\$([\w-]+)/'; + $result[] = $quote; + } + } + $this->forget(); + return new Less_Tree_Expression( $result, true ); } + $this->restore(); } // - // An @import directive + // An @import atrule // // @import "lib"; // @@ -2065,7 +2649,7 @@ public function parseAnonymousValue() { private function parseImport() { $this->save(); - $dir = $this->MatchReg( '/\\G@import?\s+/' ); + $dir = $this->matchReg( '/\\G@import?\s+/' ); if ( $dir ) { $options = $this->parseImportOptions(); @@ -2073,7 +2657,7 @@ private function parseImport() { if ( $path ) { $features = $this->parseMediaFeatures(); - if ( $this->MatchChar( ';' ) ) { + if ( $this->matchChar( ';' ) ) { if ( $features ) { $features = new Less_Tree_Value( $features ); } @@ -2091,10 +2675,10 @@ private function parseImportOptions() { $options = []; // list of options, surrounded by parens - if ( !$this->MatchChar( '(' ) ) { + if ( !$this->matchChar( '(' ) ) { return $options; } - do{ + do { $optionName = $this->parseImportOption(); if ( $optionName ) { $value = true; @@ -2109,17 +2693,17 @@ private function parseImportOptions() { break; } $options[$optionName] = $value; - if ( !$this->MatchChar( ',' ) ) { + if ( !$this->matchChar( ',' ) ) { break; } } - }while ( $optionName ); + } while ( $optionName ); $this->expectChar( ')' ); return $options; } private function parseImportOption() { - $opt = $this->MatchReg( '/\\G(less|css|multiple|once|inline|reference|optional)/' ); + $opt = $this->matchReg( '/\\G(less|css|multiple|once|inline|reference|optional)/' ); if ( $opt ) { return $opt[1]; } @@ -2128,16 +2712,16 @@ private function parseImportOption() { private function parseMediaFeature() { $nodes = []; - do{ + do { $e = $this->parseEntitiesKeyword() ?? $this->parseEntitiesVariable(); if ( $e ) { $nodes[] = $e; - } elseif ( $this->MatchChar( '(' ) ) { + } elseif ( $this->matchChar( '(' ) ) { $p = $this->parseProperty(); $e = $this->parseValue(); - if ( $this->MatchChar( ')' ) ) { + if ( $this->matchChar( ')' ) ) { if ( $p && $e ) { - $r = new Less_Tree_Rule( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true ); + $r = new Less_Tree_Declaration( $p, $e, null, null, $this->pos, $this->env->currentFileInfo, true ); $nodes[] = new Less_Tree_Paren( $r ); } elseif ( $e ) { $nodes[] = new Less_Tree_Paren( $e ); @@ -2162,14 +2746,14 @@ private function parseMediaFeatures() { $e = $this->parseMediaFeature(); if ( $e ) { $features[] = $e; - if ( !$this->MatchChar( ',' ) ) { + if ( !$this->matchChar( ',' ) ) { break; } } else { $e = $this->parseEntitiesVariable(); if ( $e ) { $features[] = $e; - if ( !$this->MatchChar( ',' ) ) { + if ( !$this->matchChar( ',' ) ) { break; } } @@ -2179,8 +2763,11 @@ private function parseMediaFeatures() { return $features ?: null; } + /** + * @see less-2.5.3.js#parsers.media + */ private function parseMedia() { - if ( $this->MatchReg( '/\\G@media/' ) ) { + if ( $this->matchStr( '@media' ) ) { $this->save(); $features = $this->parseMediaFeatures(); @@ -2196,13 +2783,15 @@ private function parseMedia() { } } - // - // A CSS Directive - // - // @charset "utf-8"; - // - private function parseDirective() { - if ( !$this->PeekChar( '@' ) ) { + /** + * A CSS AtRule like `@charset "utf-8";` + * + * @return Less_Tree_Import|Less_Tree_Media|Less_Tree_AtRule|null + * @see less-3.13.1.js#parsers.atrule + * @todo check feature parity with 3.13.1 + */ + private function parseAtRule() { + if ( !$this->peekChar( '@' ) ) { return; } @@ -2221,12 +2810,11 @@ private function parseDirective() { $this->save(); - $name = $this->MatchReg( '/\\G@[a-z-]+/' ); + $name = $this->matchReg( '/\\G@[a-z-]+/' ); if ( !$name ) { return; } - $name = $name[0]; $nonVendorSpecificName = $name; $pos = strpos( $name, '-', 2 ); @@ -2258,6 +2846,9 @@ private function parseDirective() { isRooted = true; break; */ + case "@counter-style": + $hasIdentifier = true; + break; case "@charset": $hasIdentifier = true; $hasBlock = false; @@ -2278,23 +2869,33 @@ private function parseDirective() { $hasUnknown = true; $isRooted = false; break; + default: + // TODO: port other parts of https://github.com/less/less.js/commit/e3c13121dfdca48ba8fe26335cc12dd3f7948676 + $hasUnknown = true; + break; } + $this->commentStore = []; + if ( $hasIdentifier ) { $value = $this->parseEntity(); if ( !$value ) { - $this->error( "expected " . $name . " identifier" ); + $this->Error( "expected " . $name . " identifier" ); } } elseif ( $hasExpression ) { $value = $this->parseExpression(); if ( !$value ) { - $this->error( "expected " . $name . " expression" ); + $this->Error( "expected " . $name . " expression" ); } } elseif ( $hasUnknown ) { - - $value = $this->MatchReg( '/\\G[^{;]+/' ); - if ( $value ) { - $value = new Less_Tree_Anonymous( trim( $value[0] ) ); + $value = $this->parsePermissiveValue( [ '{', ';' ] ); + $hasBlock = $this->input[$this->pos] === '{'; + if ( !$value ) { + if ( !$hasBlock && $this->input[$this->pos] !== ';' ) { + $this->Error( $name . " rule is missing block or ending semi-colon" ); + } + } elseif ( !$value->value ) { + $value = null; } } @@ -2302,9 +2903,9 @@ private function parseDirective() { $rules = $this->parseBlockRuleset(); } - if ( $rules || ( !$hasBlock && $value && $this->MatchChar( ';' ) ) ) { + if ( $rules || ( !$hasBlock && $value && $this->matchChar( ';' ) ) ) { $this->forget(); - return new Less_Tree_Directive( $name, $value, $rules, $index, $isRooted, $this->env->currentFileInfo ); + return new Less_Tree_AtRule( $name, $value, $rules, $index, $isRooted, $this->env->currentFileInfo ); } $this->restore(); @@ -2320,67 +2921,73 @@ private function parseDirective() { // private function parseValue() { $expressions = []; + $index = $this->pos; - do{ + do { $e = $this->parseExpression(); if ( $e ) { $expressions[] = $e; - if ( !$this->MatchChar( ',' ) ) { + if ( !$this->matchChar( ',' ) ) { break; } } } while ( $e ); if ( $expressions ) { - return new Less_Tree_Value( $expressions ); + return new Less_Tree_Value( $expressions, $index ); } } private function parseImportant() { - if ( $this->PeekChar( '!' ) && $this->MatchReg( '/\\G! *important/' ) ) { + if ( $this->peekChar( '!' ) && $this->matchReg( '/\\G! *important/' ) ) { return ' !important'; } } private function parseSub() { - if ( $this->MatchChar( '(' ) ) { + $this->save(); + if ( $this->matchChar( '(' ) ) { $a = $this->parseAddition(); - if ( $a ) { - $this->expectChar( ')' ); - return new Less_Tree_Expression( [ $a ], true ); // instead of $e->parens = true so the value is cached + if ( $a && $this->matchChar( ')' ) ) { + $this->forget(); + $e = new Less_Tree_Expression( [ $a ] ); + $e->parens = true; + return $e; } } + $this->restore(); } /** * Parses multiplication operation * * @return Less_Tree_Operation|null + * @see less-3.13.1.js#parsers.multiplication */ - public function parseMultiplication() { + private function parseMultiplication() { $return = $m = $this->parseOperand(); if ( $return ) { while ( true ) { - $isSpaced = $this->isWhitespace( -1 ); - if ( $this->PeekReg( '/\\G\/[*\/]/' ) ) { + if ( $this->peekReg( '/\\G\/[*\/]/' ) ) { break; } + $this->save(); - $op = $this->MatchChar( '/' ); + $op = $this->matchChar( '/' ) ?? $this->matchChar( '*' ) ?? $this->matchStr( './' ); if ( !$op ) { - $op = $this->MatchChar( '*' ); - if ( !$op ) { - break; - } + $this->forget(); + break; } $a = $this->parseOperand(); if ( !$a ) { + $this->restore(); break; } + $this->forget(); $m->parensInOp = true; $a->parensInOp = true; @@ -2402,12 +3009,10 @@ private function parseAddition() { $isSpaced = $this->isWhitespace( -1 ); - $op = $this->MatchReg( '/\\G[-+]\s+/' ); - if ( $op ) { - $op = $op[0]; - } else { + $op = $this->matchReg( '/\\G[-+]\s+/' ); + if ( !$op ) { if ( !$isSpaced ) { - $op = $this->matcher( [ '#+','#-' ] ); + $op = $this->matchChar( '+' ) ?? $this->matchChar( '-' ); } if ( !$op ) { break; @@ -2435,13 +3040,13 @@ private function parseAddition() { */ private function parseConditions() { $index = $this->pos; - $return = $a = $this->parseCondition(); + $return = $a = $this->parseCondition( true ); if ( $a ) { while ( true ) { - if ( !$this->PeekReg( '/\\G,\s*(not\s*)?\(/' ) || !$this->MatchChar( ',' ) ) { + if ( !$this->peekReg( '/\\G,\s*(not\s*)?\(/' ) || !$this->matchChar( ',' ) ) { break; } - $b = $this->parseCondition(); + $b = $this->parseCondition( true ); if ( !$b ) { break; } @@ -2452,39 +3057,137 @@ private function parseConditions() { } } - private function parseCondition() { - $index = $this->pos; - $negate = false; - $c = null; + /** + * @see less-3.13.1.js#parsers.condition + */ + private function parseCondition( $needsParens = false ) { + $result = $this->parseConditionAnd( $needsParens ); + if ( !$result ) { + return null; + } + + if ( $this->matchStr( 'or' ) ) { + $next = $this->parseCondition( $needsParens ); + if ( $next ) { + $result = new Less_Tree_Condition( 'or', $result, $next ); + } else { + return null; + } + } + return $result; + } + + /** + * @see less-3.13.1.js#Parser.conditionAnd + */ + public function parseConditionAnd( $needsParens ) { + // NOTE: Simplified inline equivalent of insideCondition() + $cond = $this->negatedCondition( $needsParens ) ?? $this->parenthesisCondition( $needsParens ); + if ( !$cond && !$needsParens ) { + $cond = $this->atomicCondition(); + } + + if ( $this->matchStr( 'and' ) ) { + $next = $this->parseConditionAnd( $needsParens ); + if ( $next ) { + $cond = new Less_Tree_Condition( 'and', $cond, $next ); + } else { + return; + } + } + return $cond; + } + + /** + * @see less-3.13.1.js#Parser.negatedCondition + */ + public function negatedCondition( $needsParens ) { + if ( $this->matchStr( 'not' ) ) { + $result = $this->parenthesisCondition( $needsParens ); + if ( $result ) { + $result->negate = !$result->negate; + } + return $result; + } + } - if ( $this->MatchReg( '/\\Gnot/' ) ) { - $negate = true; + /** + * @see less-3.13.1.js#Parser.parenthesisCondition + */ + public function parenthesisCondition( $needsParens ) { + $tryConditionFollowedByParenthesis = function () use ( $needsParens ) { + $this->save(); + $body = $this->parseCondition( $needsParens ); + if ( !$body ) { + $this->restore(); + return; + } + if ( !$this->matchChar( ')' ) ) { + $this->restore(); + return; + } + $this->forget(); + return $body; + }; + + $this->save(); + if ( !$this->matchChar( '(' ) ) { + $this->restore(); + return; + } + $body = $tryConditionFollowedByParenthesis(); + if ( $body ) { + $this->forget(); + return $body; } - $this->expectChar( '(' ); - $a = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted(); + $body = $this->atomicCondition(); + if ( !$body ) { + $this->restore(); + return; + } + if ( !$this->matchChar( ')' ) ) { + $this->restore(); + } + + $this->forget(); + return $body; + } + + /** + * @see less-3.13.1.js#Parser.atomicCondition + */ + public function atomicCondition() { + $index = $this->pos; + $a = $this->parseAddition() + ?? $this->parseEntitiesKeyword() + ?? $this->parseEntitiesQuoted() + ?? $this->parseEntitiesMixinLookup(); if ( $a ) { - $op = $this->MatchReg( '/\\G(?:>=|<=|=<|[<=>])/' ); + $op = $this->matchReg( '/\\G(?:>=|<=|=<|[<=>])/' ); if ( $op ) { - $b = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted(); + $b = $this->parseAddition() + ?? $this->parseEntitiesKeyword() + ?? $this->parseEntitiesQuoted() + ?? $this->parseEntitiesMixinLookup(); if ( $b ) { - $c = new Less_Tree_Condition( $op[0], $a, $b, $index, $negate ); + $c = new Less_Tree_Condition( $op, $a, $b, $index, false ); } else { $this->Error( 'Unexpected expression' ); } } else { $k = new Less_Tree_Keyword( 'true' ); - $c = new Less_Tree_Condition( '=', $a, $k, $index, $negate ); + $c = new Less_Tree_Condition( '=', $a, $k, $index, false ); } - $this->expectChar( ')' ); - // @phan-suppress-next-line PhanPossiblyInfiniteRecursionSameParams - return $this->MatchReg( '/\\Gand/' ) ? new Less_Tree_Condition( 'and', $c, $this->parseCondition() ) : $c; + return $c; } } /** * An operand is anything that can be part of an operation, * such as a Color, or a Variable + * + * @see less-3.13.1.js#parsers.operand */ private function parseOperand() { $negate = false; @@ -2493,11 +3196,20 @@ private function parseOperand() { return; } $char = $this->input[$offset]; - if ( $char === '@' || $char === '(' ) { - $negate = $this->MatchChar( '-' ); + + if ( $char === '@' || $char === '(' || $char === '$' ) { + $negate = $this->matchChar( '-' ); } - $o = $this->parseSub() ?? $this->parseEntitiesDimension() ?? $this->parseEntitiesColor() ?? $this->parseEntitiesVariable() ?? $this->parseEntitiesCall(); + $o = $this->parseSub() + ?? $this->parseEntitiesDimension() + ?? $this->parseEntitiesColor() + ?? $this->parseEntitiesVariable() + ?? $this->parseEntitiesProperty() + ?? $this->parseEntitiesCall() + ?? $this->parseEntitiesQuoted( true ) + // TODO: from less-3.13.1.js missing entities.colorKeyword() + ?? $this->parseEntitiesMixinLookup(); if ( $negate ) { $o->parensInOp = true; @@ -2512,19 +3224,29 @@ private function parseOperand() { * or white-space delimited Entities. * * @return Less_Tree_Expression|null + * @see less-3.13.1.js#parsers.expression */ private function parseExpression() { $entities = []; + $index = $this->pos; do { + $e = $this->parseComment(); + if ( $e ) { + $entities[] = $e; + continue; + } $e = $this->parseAddition() ?? $this->parseEntity(); + if ( $e instanceof Less_Tree_Comment ) { + $e = null; + } 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 ( !$this->peekReg( '/\\G\/[\/*]/' ) ) { + $delim = $this->matchChar( '/' ); if ( $delim ) { - $entities[] = new Less_Tree_Anonymous( $delim ); + $entities[] = new Less_Tree_Anonymous( $delim, $index ); } } } @@ -2542,7 +3264,7 @@ private function parseExpression() { * @return string */ private function parseProperty() { - $name = $this->MatchReg( '/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/' ); + $name = $this->matchReg( '/\\G(\*?-?[_a-zA-Z0-9-]+)\s*:/' ); if ( $name ) { return $name[1]; } @@ -2553,6 +3275,7 @@ private function parseProperty() { * eg: 'color', 'width', 'height', etc * * @return array + * @see less-3.13.1.js#parsers.ruleProperty */ private function parseRuleProperty() { $name = []; @@ -2560,7 +3283,7 @@ private function parseRuleProperty() { $this->save(); - $simpleProperty = $this->MatchReg( '/\\G([_a-zA-Z0-9-]+)\s*:/' ); + $simpleProperty = $this->matchReg( '/\\G([_a-zA-Z0-9-]+)\s*:/' ); if ( $simpleProperty ) { $name[] = new Less_Tree_Keyword( $simpleProperty[1] ); $this->forget(); @@ -2571,9 +3294,10 @@ private function parseRuleProperty() { // Consume! // @phan-suppress-next-line PhanPluginEmptyStatementWhileLoop - while ( $this->rulePropertyMatch( '/\\G((?:[\w-]+)|(?:@\{[\w-]+\}))/', $index, $name ) ); + while ( $this->rulePropertyMatch( '/\\G((?:[\w-]+)|(?:[@\$]\{[\w-]+\}))/', $index, $name + ) ); - if ( ( count( $name ) > 1 ) && $this->rulePropertyMatch( '/\\G\s*((?:\+_|\+)?)\s*:/', $index, $name ) ) { + if ( ( count( $name ) > 1 ) && $this->rulePropertyMatch( '/\\G((?:\+_|\+)?)\s*:/', $index, $name ) ) { $this->forget(); // at last, we have the complete match now. move forward, @@ -2583,11 +3307,13 @@ private function parseRuleProperty() { array_shift( $index ); } foreach ( $name as $k => $s ) { - if ( !$s || $s[0] !== '@' ) { - $name[$k] = new Less_Tree_Keyword( $s ); - } else { - $name[$k] = new Less_Tree_Variable( '@' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo ); - } + $firstChar = $s[0] ?? ''; + $name[$k] = ( $firstChar !== '@' && $firstChar !== '$' ) ? + new Less_Tree_Keyword( $s ) : + ( $s[0] === '@' + ? new Less_Tree_Variable( '@' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo ) + : new Less_Tree_Property( '$' . substr( $s, 2, -1 ), $index[$k], $this->env->currentFileInfo ) + ); } return $name; } else { @@ -2597,7 +3323,7 @@ private function parseRuleProperty() { private function rulePropertyMatch( $re, &$index, &$name ) { $i = $this->pos; - $chunk = $this->MatchReg( $re ); + $chunk = $this->matchReg( $re ); if ( $chunk ) { $index[] = $i; $name[] = $chunk[1]; @@ -2609,25 +3335,20 @@ public static function serializeVars( $vars ) { $s = ''; foreach ( $vars as $name => $value ) { - $s .= ( ( $name[0] === '@' ) ? '' : '@' ) . $name . ': ' . $value . ( ( substr( $value, -1 ) === ';' ) ? '' : ';' ); + if ( strval( $value ) === "" ) { + $value = '~""'; + } + $s .= ( str_starts_with( $name, '@' ) ? '' : '@' ) . $name . ': ' . $value . ( str_ends_with( $value, ';' ) ? '' : ';' ); } return $s; } - /** - * Some versions of PHP have trouble with method_exists($a,$b) if $a is not an object - * - * @param mixed $a - * @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 + * + * @internal For internal use only */ public static function round( $input, $precision = 0 ) { $precision = pow( 10, $precision ); @@ -2652,7 +3373,7 @@ public static function WinPath( $path ) { } public static function AbsPath( $path, $winPath = false ) { - if ( strpos( $path, '//' ) !== false && preg_match( '/^(https?:)?\/\//i', $path ) ) { + if ( str_contains( $path, '//' ) && preg_match( '/^(https?:)?\/\//i', $path ) ) { return $winPath ? '' : false; } else { $path = realpath( $path ); @@ -2664,7 +3385,6 @@ public static function AbsPath( $path, $winPath = false ) { } public function CacheEnabled() { - return ( self::$options['cache_method'] && ( Less_Cache::$cache_dir || ( self::$options['cache_method'] == 'callback' ) ) ); + return ( self::$options['cache_incremental'] && self::$options['cache_method'] && self::$options['cache_dir'] ); } - } diff --git a/wikimedia/less.php/lib/Less/SourceMap/Base64VLQ.php b/wikimedia/less.php/lib/Less/SourceMap/Base64VLQ.php index fa88158..0ceb0e3 100644 --- a/wikimedia/less.php/lib/Less/SourceMap/Base64VLQ.php +++ b/wikimedia/less.php/lib/Less/SourceMap/Base64VLQ.php @@ -34,14 +34,14 @@ class Less_SourceMap_Base64VLQ { */ private $charToIntMap = [ 'A' => 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, + '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, + 5 => 57, 6 => 58, 7 => 59, 8 => 60, 9 => 61, '+' => 62, '/' => 63, ]; /** @@ -111,8 +111,7 @@ public function fromVLQSigned( $aValue ) { public function encode( $aValue ) { $encoded = ''; $vlq = $this->toVLQSigned( $aValue ); - do - { + do { $digit = $vlq & $this->mask; $vlq = $this->zeroFill( $vlq, $this->shift ); if ( $vlq > 0 ) { @@ -133,8 +132,7 @@ public function encode( $aValue ) { public function decode( $encoded ) { $vlq = 0; $i = 0; - do - { + do { $digit = $this->base64Decode( $encoded[$i] ); $vlq |= ( $digit & $this->mask ) << ( $i * $this->shift ); $i++; diff --git a/wikimedia/less.php/lib/Less/SourceMap/Generator.php b/wikimedia/less.php/lib/Less/SourceMap/Generator.php index 7da9609..fb64c28 100644 --- a/wikimedia/less.php/lib/Less/SourceMap/Generator.php +++ b/wikimedia/less.php/lib/Less/SourceMap/Generator.php @@ -20,25 +20,25 @@ class Less_SourceMap_Generator extends Less_Configurable { // 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. - 'sourceRoot' => '', + 'sourceRoot' => '', // an optional name of the generated code that this source map is associated with. - 'sourceMapFilename' => null, + 'sourceMapFilename' => null, // url of the map - 'sourceMapURL' => null, + 'sourceMapURL' => null, // absolute path to a file to write the map to - 'sourceMapWriteTo' => null, + 'sourceMapWriteTo' => null, // output source contents? - 'outputSourceFiles' => false, + 'outputSourceFiles' => false, // base path for filename normalization - 'sourceMapRootpath' => '', + 'sourceMapRootpath' => '', // base path for filename normalization - 'sourceMapBasepath' => '' + 'sourceMapBasepath' => '' ]; /** @@ -72,9 +72,10 @@ class Less_SourceMap_Generator extends Less_Configurable { /** * File to content map * - * @var array + * @var array */ protected $sources = []; + /** @var array */ protected $source_keys = []; /** @@ -95,6 +96,17 @@ public function __construct( Less_Tree_Ruleset $root, $contentsMap, $options = [ $this->options['sourceMapBasepath'] = $this->fixWindowsPath( $this->options['sourceMapBasepath'], true ); } + /** + * PHP version of JavaScript's `encodeURIComponent` function + * + * @param string $string The string to encode + * @return string The encoded string + */ + private static function encodeURIComponent( $string ) { + $revert = [ '%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')' ]; + return strtr( rawurlencode( $string ), $revert ); + } + /** * Generates the CSS * @@ -106,10 +118,10 @@ public function generateCSS() { // catch the output $this->root->genCSS( $output ); - $sourceMapUrl = $this->getOption( 'sourceMapURL' ); - $sourceMapFilename = $this->getOption( 'sourceMapFilename' ); - $sourceMapContent = $this->generateJson(); - $sourceMapWriteTo = $this->getOption( 'sourceMapWriteTo' ); + $sourceMapUrl = $this->getOption( 'sourceMapURL' ); + $sourceMapFilename = $this->getOption( 'sourceMapFilename' ); + $sourceMapContent = $this->generateJson(); + $sourceMapWriteTo = $this->getOption( 'sourceMapWriteTo' ); if ( !$sourceMapUrl && $sourceMapFilename ) { $sourceMapUrl = $this->normalizeFilename( $sourceMapFilename ); @@ -122,7 +134,7 @@ public function generateCSS() { // inline the map if ( !$sourceMapUrl ) { - $sourceMapUrl = sprintf( 'data:application/json,%s', Less_Functions::encodeURIComponent( $sourceMapContent ) ); + $sourceMapUrl = sprintf( 'data:application/json,%s', self::encodeURIComponent( $sourceMapContent ) ); } if ( $sourceMapUrl ) { @@ -166,12 +178,12 @@ protected function normalizeFilename( $filename ) { $basePath = $this->getOption( 'sourceMapBasepath' ); // "Trim" the 'sourceMapBasepath' from the output filename. - if ( is_string( $basePath ) && strpos( $filename, $basePath ) === 0 ) { + if ( is_string( $basePath ) && str_starts_with( $filename, $basePath ) ) { $filename = substr( $filename, strlen( $basePath ) ); } // Remove extra leading path separators. - if ( strpos( $filename, '\\' ) === 0 || strpos( $filename, '/' ) === 0 ) { + if ( str_starts_with( $filename, '\\' ) || str_starts_with( $filename, '/' ) ) { $filename = substr( $filename, 1 ); } @@ -193,10 +205,12 @@ public function addMapping( $generatedLine, $generatedColumn, $originalLine, $or 'generated_column' => $generatedColumn, 'original_line' => $originalLine, 'original_column' => $originalColumn, - 'source_file' => $fileInfo['currentUri'] + 'source_file' => $fileInfo['currentUri'] ?? null ]; - $this->sources[$fileInfo['currentUri']] = $fileInfo['filename']; + if ( isset( $fileInfo['currentUri'] ) ) { + $this->sources[$fileInfo['currentUri']] = $fileInfo['filename']; + } } /** @@ -218,7 +232,8 @@ protected function generateJson() { $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. + // 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; @@ -332,7 +347,7 @@ public function generateMappings() { * @return int|false */ protected function findFileIndex( $filename ) { - return $this->source_keys[$filename]; + return $this->source_keys[$filename] ?? false; } /** diff --git a/wikimedia/less.php/lib/Less/Tree.php b/wikimedia/less.php/lib/Less/Tree.php index f5ec816..3ad3abf 100644 --- a/wikimedia/less.php/lib/Less/Tree.php +++ b/wikimedia/less.php/lib/Less/Tree.php @@ -5,10 +5,30 @@ */ class Less_Tree { + /** @var bool */ public $parensInOp = false; + /** @var true|null */ public $extendOnEveryPath; + /** @var Less_Tree_Extend[] */ public $allExtends; + /** + * This is set to true to ensure visibility + * for all except Less_Tree_Anonymous where we decide + * if the the node should be visible or not + * + * @var bool + */ + public $nodeVisible = true; + + /** + * @var Less_Parser + * @see less-3.13.1.js#Node.prototype.parse + */ + public static $parse; + /** + * @see less-2.5.3.js#Node.prototype.toCSS + */ public function toCSS() { $output = new Less_Output(); $this->genCSS( $output ); @@ -28,6 +48,41 @@ public function compile( $env ) { return $this; } + /** + * @param string $op + * @param float $a + * @param float $b + * @see less-2.5.3.js#Node.prototype._operate + */ + protected function _operate( $op, $a, $b ) { + switch ( $op ) { + case '+': + return $a + $b; + case '-': + return $a - $b; + case '*': + return $a * $b; + case '/': + return $a / $b; + } + } + + /** + * @see less-2.5.3.js#Node.prototype.fround + */ + protected function fround( $value ) { + if ( $value === 0 ) { + return $value; + } + + // TODO: Migrate to passing $env. + if ( Less_Parser::$options['numPrecision'] ) { + $p = pow( 10, Less_Parser::$options['numPrecision'] ); + return round( $value * $p ) / $p; + } + return $value; + } + /** * @param Less_Output $output * @param Less_Tree_Ruleset[] $rules @@ -64,10 +119,79 @@ public static function outputRuleset( $output, $rules ) { public function accept( $visitor ) { } + /** + * @param Less_Tree $a + * @param Less_Tree $b + * @return int|null + * @see less-2.5.3.js#Node.compare + */ + public static function nodeCompare( $a, $b ) { + // Less_Tree subclasses that implement compare() are: + // Anonymous, Color, Dimension, Quoted, Unit + $aHasCompare = ( $a instanceof Less_Tree_Anonymous || $a instanceof Less_Tree_Color + || $a instanceof Less_Tree_Dimension || $a instanceof Less_Tree_Quoted || $a instanceof Less_Tree_Unit + ); + $bHasCompare = ( $b instanceof Less_Tree_Anonymous || $b instanceof Less_Tree_Color + || $b instanceof Less_Tree_Dimension || $b instanceof Less_Tree_Quoted || $b instanceof Less_Tree_Unit + ); + + if ( $aHasCompare && + !( $b instanceof Less_Tree_Quoted || $b instanceof Less_Tree_Anonymous ) + ) { + // for "symmetric results" force toCSS-based comparison via b.compare() + // of Quoted or Anonymous if either value is one of those + // @phan-suppress-next-line PhanUndeclaredMethod + return $a->compare( $b ); + } elseif ( $bHasCompare ) { + $res = $b->compare( $a ); + // In JS, `-undefined` produces NAN, which, just like undefined + // will enter the the default/false branch of Less_Tree_Condition#compile. + // In PHP, `-null` is 0. To ensure parity, preserve the null. + return $res !== null ? -$res : null; + } elseif ( get_class( $a ) !== get_class( $b ) ) { + return null; + } + + // Less_Tree subclasses that have an array value: Less_Tree_Expression, Less_Tree_Value + // @phan-suppress-next-line PhanUndeclaredProperty + $aval = $a->value ?? []; + $bval = $b->value ?? []; + if ( !( $a instanceof Less_Tree_Expression || $a instanceof Less_Tree_Value ) ) { + return $aval === $bval ? 0 : null; + } + '@phan-var Less_Tree[] $aval'; + '@phan-var Less_Tree[] $bval'; + if ( count( $aval ) !== count( $bval ) ) { + return null; + } + foreach ( $aval as $i => $item ) { + if ( self::nodeCompare( $item, $bval[$i] ) !== 0 ) { + return null; + } + } + return 0; + } + + /** + * @param string|float|int $a + * @param string|float|int $b + * @return int|null + * @see less-2.5.3.js#Node.numericCompare + */ + public static function numericCompare( $a, $b ) { + return $a < $b ? -1 + : ( $a === $b ? 0 + : ( $a > $b ? 1 + // NAN is not greater, less, or equal + : null + ) + ); + } + public static function ReferencedArray( $rules ) { foreach ( $rules as $rule ) { if ( method_exists( $rule, 'markReferenced' ) ) { - // @phan-suppress-next-line PhanUndeclaredMethod + // @phan-suppress-next-line PhanUndeclaredMethod False positive $rule->markReferenced(); } } @@ -85,4 +209,11 @@ public static function __set_state( $args ) { return $obj; } + /** + * @see less-3.13.1.js#Node.prototype.isVisible + */ + public function isVisible() { + return $this->nodeVisible; + } + } diff --git a/wikimedia/less.php/lib/Less/Tree/Alpha.php b/wikimedia/less.php/lib/Less/Tree/Alpha.php index 50a7a9a..822542c 100644 --- a/wikimedia/less.php/lib/Less/Tree/Alpha.php +++ b/wikimedia/less.php/lib/Less/Tree/Alpha.php @@ -1,43 +1,44 @@ value = $val; } - // function accept( $visitor ){ - // $this->value = $visitor->visit( $this->value ); - //} + public function accept( $visitor ) { + if ( $this->value instanceof Less_Tree ) { + $this->value = $visitor->visitObj( $this->value ); + } + } public function compile( $env ) { - if ( is_object( $this->value ) ) { - $this->value = $this->value->compile( $env ); + if ( $this->value instanceof Less_Tree ) { + return new self( $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 { + if ( $this->value instanceof Less_Tree ) { $this->value->genCSS( $output ); + } else { + $output->add( $this->value ); } $output->add( ')' ); } - - public function toCSS() { - return "alpha(opacity=" . ( is_string( $this->value ) ? $this->value : $this->value->toCSS() ) . ")"; - } - } diff --git a/wikimedia/less.php/lib/Less/Tree/Anonymous.php b/wikimedia/less.php/lib/Less/Tree/Anonymous.php index 21dc971..55edd96 100644 --- a/wikimedia/less.php/lib/Less/Tree/Anonymous.php +++ b/wikimedia/less.php/lib/Less/Tree/Anonymous.php @@ -1,55 +1,75 @@ value = $value; $this->index = $index; $this->mapLines = $mapLines; $this->currentFileInfo = $currentFileInfo; + $this->rulesetLike = $rulesetLike; + // TODO: remove isReferenced and implement $visibilityInfo + // https://github.com/less/less.js/commit/ead3e29f7b79390ad3ac798bf42195b24919107d + $this->isReferenced = $referenced; } public function compile( $env ) { - return new self( $this->value, $this->index, $this->currentFileInfo, $this->mapLines ); + return new self( $this->value, $this->index, $this->currentFileInfo, $this->mapLines, $this->rulesetLike, $this->isReferenced ); } + /** + * @param Less_Tree|mixed $x + * @return int|null + * @see less-3.13.1.js#Anonymous.prototype.compare + */ public function compare( $x ) { - if ( !is_object( $x ) ) { - return -1; - } - - $left = $this->toCSS(); - $right = $x->toCSS(); - - if ( $left === $right ) { - return 0; - } + return ( $x instanceof Less_Tree && $this->toCSS() === $x->toCSS() ) ? 0 : null; + } - return $left < $right ? -1 : 1; + public function isRulesetLike() { + return $this->rulesetLike; } /** - * @see Less_Tree::genCSS + * @see less-3.13.1.js#Anonymous.prototype.genCSS */ public function genCSS( $output ) { - $output->add( $this->value, $this->currentFileInfo, $this->index, $this->mapLines ); + $this->nodeVisible = $this->value !== "" && $this->value !== 0; + if ( $this->nodeVisible ) { + $output->add( $this->value, $this->currentFileInfo, $this->index, $this->mapLines ); + } } - public function toCSS() { - return $this->value; + public function markReferenced() { + $this->isReferenced = true; } + public function getIsReferenced() { + return !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] || $this->isReferenced; + } } diff --git a/wikimedia/less.php/lib/Less/Tree/Assignment.php b/wikimedia/less.php/lib/Less/Tree/Assignment.php index 238eb51..025e6f3 100644 --- a/wikimedia/less.php/lib/Less/Tree/Assignment.php +++ b/wikimedia/less.php/lib/Less/Tree/Assignment.php @@ -1,13 +1,15 @@ key = $key; $this->value = $val; } @@ -17,18 +19,13 @@ public function accept( $visitor ) { } public function compile( $env ) { + // NOTE: Less.js has a conditional for $this->value, + // but this appears unreachable ($val is not optional). return new self( $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/wikimedia/less.php/lib/Less/Tree/Directive.php b/wikimedia/less.php/lib/Less/Tree/AtRule.php similarity index 65% rename from wikimedia/less.php/lib/Less/Tree/Directive.php rename to wikimedia/less.php/lib/Less/Tree/AtRule.php index 10d82cd..e0c75c1 100644 --- a/wikimedia/less.php/lib/Less/Tree/Directive.php +++ b/wikimedia/less.php/lib/Less/Tree/AtRule.php @@ -1,23 +1,41 @@ name = $name; + // TODO: Less.js 3.13 handles `$value instanceof Less_Tree` and creates Anonymous here. $this->value = $value; - if ( $rules ) { + if ( $rules !== null ) { if ( is_array( $rules ) ) { $this->rules = $rules; } else { @@ -28,12 +46,14 @@ public function __construct( $name, $value = null, $rules = null, $index = null, foreach ( $this->rules as $rule ) { $rule->allowImports = true; } + // TODO: Less.js 3.13 handles setParent() here } $this->index = $index; $this->isRooted = $isRooted; $this->currentFileInfo = $currentFileInfo; $this->debugInfo = $debugInfo; + $this->isReferenced = $isReferenced; } public function accept( $visitor ) { @@ -45,18 +65,24 @@ public function accept( $visitor ) { } } + public function isRulesetLike() { + return $this->rules || !$this->isCharset(); + } + + public function isCharset() { + return $this->name === "@charset"; + } + /** * @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 ) { + if ( $this->rules !== null ) { Less_Tree::outputRuleset( $output, $this->rules ); } else { $output->add( ';' ); @@ -89,7 +115,7 @@ public function compile( $env ) { $env->mediaPath = $mediaPathBackup; $env->mediaBlocks = $mediaPBlocksBackup; - return new self( $this->name, $value, $rules, $this->index, $this->isRooted, $this->currentFileInfo, $this->debugInfo ); + return new self( $this->name, $value, $rules, $this->index, $this->isRooted, $this->currentFileInfo, $this->debugInfo, $this->isReferenced ); } public function variable( $name ) { @@ -99,12 +125,17 @@ public function variable( $name ) { } public function find( $selector ) { + // TODO: Less.js 3.13.1 adds multiple variadic arguments here if ( $this->rules ) { return $this->rules[0]->find( $selector, $this ); } } - // rulesets: function () { if (this.rules) return tree.Ruleset.prototype.rulesets.apply(this.rules); }, + // TODO: Implement less-3.13.1.js#AtRule.prototype.rulesets + // Unused? + + // TODO: Implement less-3.13.1.js#AtRule.prototype.outputRuleset + // We have ours in Less_Tree::outputRuleset instead. public function markReferenced() { $this->isReferenced = true; @@ -113,6 +144,10 @@ public function markReferenced() { } } + public function getIsReferenced() { + return !isset( $this->currentFileInfo['reference'] ) || !$this->currentFileInfo['reference'] || $this->isReferenced; + } + public function emptySelectors() { $el = new Less_Tree_Element( '', '&', $this->index, $this->currentFileInfo ); $sels = [ new Less_Tree_Selector( [ $el ], [], null, $this->index, $this->currentFileInfo ) ]; diff --git a/wikimedia/less.php/lib/Less/Tree/Attribute.php b/wikimedia/less.php/lib/Less/Tree/Attribute.php index 017daa8..6eec785 100644 --- a/wikimedia/less.php/lib/Less/Tree/Attribute.php +++ b/wikimedia/less.php/lib/Less/Tree/Attribute.php @@ -1,13 +1,21 @@ key = $key; $this->op = $op; @@ -15,22 +23,18 @@ public function __construct( $key, $op, $value ) { } public function compile( $env ) { - $key_obj = is_object( $this->key ); - $val_obj = is_object( $this->value ); - - if ( !$key_obj && !$val_obj ) { + // Optimization: Avoid object churn for the common case. + // Attributes are very common in CSS/LESS input, but rarely involve dynamic values. + if ( !$this->key instanceof Less_Tree && !$this->value instanceof Less_Tree ) { return $this; } return new self( - $key_obj ? $this->key->compile( $env ) : $this->key, + $this->key instanceof Less_Tree ? $this->key->compile( $env ) : $this->key, $this->op, - $val_obj ? $this->value->compile( $env ) : $this->value ); + $this->value instanceof Less_Tree ? $this->value->compile( $env ) : $this->value ); } - /** - * @see Less_Tree::genCSS - */ public function genCSS( $output ) { $output->add( $this->toCSS() ); } @@ -40,7 +44,7 @@ public function toCSS() { if ( $this->op ) { $value .= $this->op; - $value .= ( is_object( $this->value ) ? $this->value->toCSS() : $this->value ); + $value .= ( $this->value instanceof Less_Tree ? $this->value->toCSS() : $this->value ); } return '[' . $value . ']'; diff --git a/wikimedia/less.php/lib/Less/Tree/Call.php b/wikimedia/less.php/lib/Less/Tree/Call.php index ab3d3a7..03deccc 100644 --- a/wikimedia/less.php/lib/Less/Tree/Call.php +++ b/wikimedia/less.php/lib/Less/Tree/Call.php @@ -1,22 +1,24 @@ name = $name; $this->args = $args; - $this->mathOn = ( $name !== 'calc' ); + $this->calc = $name === 'calc'; $this->index = $index; $this->currentFileInfo = $currentFileInfo; } @@ -25,6 +27,48 @@ public function accept( $visitor ) { $this->args = $visitor->visitArray( $this->args ); } + /** + * @see less-3.13.1.js#functionCaller.prototype.call + */ + private function functionCaller( $function, array $arguments, $env, $evalArgs ) { + // This code is terrible and should be replaced as per this issue... + // https://github.com/less/less.js/issues/2477 + $filtered = []; + + if ( $evalArgs !== false ) { + foreach ( $arguments as $a ) { + $filtered[] = $a->compile( $env ); + } + $arguments = $filtered; + $filtered = []; + } + + foreach ( $arguments as $argument ) { + if ( $argument instanceof Less_Tree_Comment ) { + continue; + } + $filtered[] = $argument; + } + foreach ( $filtered as $index => $argument ) { + if ( $argument instanceof Less_Tree_Expression ) { + $filtered[$index] = $argument->mapToFunctionCallArgument(); + } + } + + return $function( ...$filtered ); + } + + /** + * @param Less_Environment $env + * @return void + */ + private function exitCalc( $env, $currentMathContext ) { + if ( $this->calc || $env->inCalc ) { + $env->exitCalc(); + } + $env->mathOn = $currentMathContext; + } + // // When evaluating a function call, // we either find the function in Less_Functions, @@ -35,35 +79,46 @@ public function accept( $visitor ) { // of them is a LESS variable that only PHP knows the value of, // like: `saturate(@mycolor)`. // The function should receive the value, not the variable. - // - public function compile( $env = null ) { - // Turn off math for calc(). https://phabricator.wikimedia.org/T331688 - $currentMathContext = Less_Environment::$mathOn; - Less_Environment::$mathOn = $this->mathOn; - - $args = []; - foreach ( $this->args as $a ) { - $args[] = $a->compile( $env ); + // TODO less.js#3.13.1 provide better parity with upstream. + public function compile( $env ) { + /** + * Turn off math for calc(), and switch back on for evaluating nested functions + */ + $currentMathContext = $env->mathOn; + $env->mathOn = !$this->calc; + $evalArgs = null; + + if ( $this->calc || $env->inCalc ) { + $env->enterCalc(); } + $nameLC = strtolower( $this->name ); - Less_Environment::$mathOn = $currentMathContext; + $args = []; - $nameLC = strtolower( $this->name ); switch ( $nameLC ) { case '%': - $nameLC = '_percent'; + $nameLC = '_percent'; break; - case 'get-unit': - $nameLC = 'getunit'; + $nameLC = 'getunit'; break; - case 'data-uri': - $nameLC = 'datauri'; + $nameLC = 'datauri'; break; - case 'svg-gradient': - $nameLC = 'svggradient'; + $nameLC = 'svggradient'; + break; + case 'image-size': + $nameLC = 'imagesize'; + break; + case 'image-width': + $nameLC = 'imagewidth'; + break; + case 'image-height': + $nameLC = 'imageheight'; + break; + case 'if': + $evalArgs = false; break; } @@ -72,24 +127,36 @@ public function compile( $env = null ) { $result = Less_Tree_DefaultFunc::compile(); } else { $func = null; - if ( method_exists( Less_Functions::class, $nameLC ) ) { - $functions = new Less_Functions( $env, $this->currentFileInfo ); - $func = [ $functions, $nameLC ]; + $functions = new Less_Functions( $env, $this->currentFileInfo ); + $funcBuiltin = [ $functions, $nameLC ]; + // Avoid method_exists() as that considers private utility functions too + if ( is_callable( $funcBuiltin ) ) { + $func = $funcBuiltin; } elseif ( isset( $env->functions[$nameLC] ) && is_callable( $env->functions[$nameLC] ) ) { $func = $env->functions[$nameLC]; } // If the function name isn't known to LESS, output it unchanged as CSS. if ( $func ) { try { - $result = $func( ...$args ); + $result = $this->functionCaller( $func, $this->args, $env, $evalArgs ); + $this->exitCalc( $env, $currentMathContext ); + } catch ( Exception $e ) { // Preserve original trace, especially from custom functions. // https://github.com/wikimedia/less.php/issues/38 + + // Check if 'error evaluating function' is the start of the error message + // less.js does this by checking if line and column already set + if ( str_starts_with( $e->getMessage(), 'error evaluating function' ) ) { + throw $e; + } + throw new Less_Exception_Compiler( - 'error evaluating function `' . $this->name . '` ' . $e->getMessage() - . ' index: ' . $this->index, - $e - ); + 'error evaluating function `' . $this->name . '`' . ( $e->getMessage() ? ': ' . $e->getMessage() : '' ), + $e, + $this->index, + $this->currentFileInfo + ); } } } @@ -97,6 +164,10 @@ public function compile( $env = null ) { if ( $result !== null ) { return $result; } + foreach ( $this->args as $a ) { + $args[] = $a->compile( $env ); + } + $this->exitCalc( $env, $currentMathContext ); return new self( $this->name, $args, $this->index, $this->currentFileInfo ); } @@ -117,8 +188,4 @@ public function genCSS( $output ) { $output->add( ')' ); } - // public function toCSS(){ - // return $this->compile()->toCSS(); - //} - } diff --git a/wikimedia/less.php/lib/Less/Tree/Color.php b/wikimedia/less.php/lib/Less/Tree/Color.php index 5152895..c41ed18 100644 --- a/wikimedia/less.php/lib/Less/Tree/Color.php +++ b/wikimedia/less.php/lib/Less/Tree/Color.php @@ -1,34 +1,38 @@ */ public $rgb; + /** @var int */ public $alpha; - public $isTransparentKeyword; + /** @var null|string */ public $value; - public function __construct( $rgb, $a = 1, $isTransparentKeyword = null ) { - if ( $isTransparentKeyword ) { - $this->rgb = $rgb; - $this->alpha = $a; - $this->isTransparentKeyword = true; - return; - } - - $this->rgb = []; + public function __construct( $rgb, $a = null, ?string $originalForm = null ) { if ( is_array( $rgb ) ) { $this->rgb = $rgb; } elseif ( strlen( $rgb ) == 6 ) { + // TODO: Less.js 3.13 supports 8-digit rgba as #RRGGBBAA + $this->rgb = []; foreach ( str_split( $rgb, 2 ) as $c ) { $this->rgb[] = hexdec( $c ); } } else { + $this->rgb = []; + // TODO: Less.js 3.13 supports 4-digit short rgba as #RGBA foreach ( str_split( $rgb, 1 ) as $c ) { $this->rgb[] = hexdec( $c . $c ); } } + $this->alpha = is_numeric( $a ) ? $a : 1; + + if ( $originalForm !== null ) { + $this->value = $originalForm; + } } public function luma() { @@ -52,51 +56,48 @@ public function genCSS( $output ) { public function toCSS( $doNotCompress = false ) { $compress = Less_Parser::$options['compress'] && !$doNotCompress; - $alpha = Less_Functions::fround( $this->alpha ); + $alpha = $this->fround( $this->alpha ); - // - // If we have some transparency, the only way to represent it - // is via `rgba`. Otherwise, we use the hex representation, + // `value` is set if this color was originally + // converted from a named color string so we need + // to respect this and try to output named color too. + if ( $this->value ) { + return $this->value; + } + + // If we have alpha transparency other than 1.0, 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. // + // TODO: Less.js 3.13 supports hsla() and hsl() as well if ( $alpha < 1 ) { - if ( ( $alpha === 0 || $alpha === 0.0 ) && isset( $this->isTransparentKeyword ) && $this->isTransparentKeyword ) { - return 'transparent'; - } - $values = []; foreach ( $this->rgb as $c ) { - $values[] = Less_Functions::clamp( round( $c ), 255 ); + $values[] = $this->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]; - } + $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; } + 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. - // - /** + * 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 * @param self $other */ @@ -104,7 +105,7 @@ public function operate( $op, $other ) { $rgb = []; $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] ); + $rgb[$c] = $this->_operate( $op, $this->rgb[$c], $other->rgb[$c] ); } return new self( $rgb, $alpha ); } @@ -124,8 +125,9 @@ public function toHSL() { $l = ( $max + $min ) / 2; $d = $max - $min; - $h = $s = 0; - if ( $max !== $min ) { + if ( $max === $min ) { + $h = $s = 0; + } else { $s = $l > 0.5 ? $d / ( 2 - $max - $min ) : $d / ( $max + $min ); switch ( $max ) { @@ -163,8 +165,9 @@ public function toHSV() { $s = $d / $max; } - $h = 0; - if ( $max !== $min ) { + if ( $max === $min ) { + $h = 0; + } else { switch ( $max ) { case $r: $h = ( $g - $b ) / $d + ( $g < $b ? 6 : 0 ); @@ -186,27 +189,38 @@ public function toARGB() { return $this->toHex( $argb ); } + /** + * @param mixed $x + * @return int|null + * @see less-3.13.1.js#Color.prototype.compare + */ public function compare( $x ) { - if ( !property_exists( $x, 'rgb' ) ) { - return -1; - } - - return ( $x->rgb[0] === $this->rgb[0] && + return ( $x instanceof self && + $x->rgb[0] === $this->rgb[0] && $x->rgb[1] === $this->rgb[1] && $x->rgb[2] === $this->rgb[2] && - $x->alpha === $this->alpha ) ? 0 : -1; + $x->alpha === $this->alpha ) ? 0 : null; + } + + /** + * @param int|float $val + * @param int $max + * @return int|float + * @see less-3.13.1.js#Color.prototype + */ + private function clamp( $val, $max ) { + return min( max( $val, 0 ), $max ); } public function toHex( $v ) { $ret = '#'; foreach ( $v as $c ) { - $c = Less_Functions::clamp( Less_Parser::round( $c ), 255 ); + $c = $this->clamp( Less_Parser::round( $c ), 255 ); if ( $c < 16 ) { $ret .= '0'; } $ret .= dechex( $c ); } - return $ret; } @@ -214,21 +228,19 @@ public function toHex( $v ) { * @param string $keyword */ public static function fromKeyword( $keyword ) { - $c = $keyword = strtolower( $keyword ); + $c = null; + $key = strtolower( $keyword ); - if ( Less_Colors::hasOwnProperty( $keyword ) ) { + if ( Less_Colors::hasOwnProperty( $key ) ) { // detect named color - $c = new self( substr( Less_Colors::color( $keyword ), 1 ) ); + $c = new self( substr( Less_Colors::color( $key ), 1 ) ); + } elseif ( $key === 'transparent' ) { + $c = new self( [ 0, 0, 0 ], 0 ); } - if ( $keyword === 'transparent' ) { - $c = new self( [ 0, 0, 0 ], 0, true ); - } - - if ( isset( $c ) && is_object( $c ) ) { + if ( $c instanceof self ) { $c->value = $keyword; return $c; } } - } diff --git a/wikimedia/less.php/lib/Less/Tree/Comment.php b/wikimedia/less.php/lib/Less/Tree/Comment.php index 98a5436..a5937a2 100644 --- a/wikimedia/less.php/lib/Less/Tree/Comment.php +++ b/wikimedia/less.php/lib/Less/Tree/Comment.php @@ -1,38 +1,34 @@ value = $value; - $this->silent = (bool)$silent; + $this->isLineComment = (bool)$isLineComment; $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 - } + // NOTE: Skip debugInfo handling (not implemented) - public function toCSS() { - return Less_Parser::$options['compress'] ? '' : $this->value; + $output->add( $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; + $isCompressed = Less_Parser::$options['compress'] && ( $this->value[2] ?? '' ) !== "!"; + return $this->isLineComment || $isReference || $isCompressed; } public function markReferenced() { diff --git a/wikimedia/less.php/lib/Less/Tree/Condition.php b/wikimedia/less.php/lib/Less/Tree/Condition.php index f0fbafb..e10282d 100644 --- a/wikimedia/less.php/lib/Less/Tree/Condition.php +++ b/wikimedia/less.php/lib/Less/Tree/Condition.php @@ -4,10 +4,15 @@ */ class Less_Tree_Condition extends Less_Tree { + /** @var string */ public $op; + /** @var Less_Tree */ public $lvalue; + /** @var Less_Tree */ public $rvalue; + /** @var int */ public $index; + /** @var bool */ public $negate; public function __construct( $op, $l, $r, $i = 0, $negate = false ) { @@ -23,6 +28,11 @@ public function accept( $visitor ) { $this->rvalue = $visitor->visitObj( $this->rvalue ); } + /** + * @param Less_Environment $env + * @return bool + * @see less-2.5.3.js#Condition.prototype.eval + */ public function compile( $env ) { $a = $this->lvalue->compile( $env ); $b = $this->rvalue->compile( $env ); @@ -37,28 +47,19 @@ public function compile( $env ) { 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: + $res = Less_Tree::nodeCompare( $a, $b ); + // In JS, switch(undefined) with -1,0,-1,defaults goes to `default`. + // In PHP, switch(null) would go to case 0. Use if/else instead. + if ( $res === -1 ) { $result = $this->op === '<' || $this->op === '=<' || $this->op === '<='; - break; - - case 0: + } elseif ( $res === 0 ) { $result = $this->op === '=' || $this->op === '>=' || $this->op === '=<' || $this->op === '<='; - break; - - case 1: + } elseif ( $res === 1 ) { $result = $this->op === '>' || $this->op === '>='; - break; + } else { + // null, NAN + $result = false; } - break; } return $this->negate ? !$result : $result; diff --git a/wikimedia/less.php/lib/Less/Tree/Declaration.php b/wikimedia/less.php/lib/Less/Tree/Declaration.php new file mode 100644 index 0000000..5c0e907 --- /dev/null +++ b/wikimedia/less.php/lib/Less/Tree/Declaration.php @@ -0,0 +1,189 @@ + */ + public $name; + /** @var Less_Tree[]|Less_Tree_Anonymous */ + public $value; + /** @var string */ + public $important; + /** @var null|false|string */ + public $merge; + /** @var int|null */ + public $index; + /** @var bool */ + public $inline; + /** @var bool */ + public $variable; + /** @var array|null */ + public $currentFileInfo; + + /** + * In the upstream `parsed` is stored in `Node`, but Less_Tree_Declaration is the only place + * that make use of it. + * @see less-3.13.1.js#Node.parsed + * @var bool + */ + public $parsed = false; + + /** + * @param string|array $name + * @param Less_Tree|string|null $value + * @param null|false|string $important + * @param null|false|string $merge + * @param int|null $index + * @param array|null $currentFileInfo + * @param bool $inline + * @param bool|null $variable + */ + public function __construct( + $name, + $value = null, + $important = null, + $merge = null, + $index = null, + $currentFileInfo = null, + $inline = false, + $variable = null + ) { + $this->name = $name; + $this->value = ( $value instanceof Less_Tree ) + ? $value + : new Less_Tree_Value( [ $value ? new Less_Tree_Anonymous( $value ) : null ] ); + $this->important = $important ? ' ' . trim( $important ) : ''; + $this->merge = $merge; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + $this->inline = $inline; + $this->variable = $variable ?? ( is_string( $name ) && $name[0] === '@' ); + } + + public function accept( $visitor ) { + $this->value = $visitor->visitObj( $this->value ); + } + + /** + * @see less-2.5.3.js#Rule.prototype.genCSS + */ + public function genCSS( $output ) { + $output->add( $this->name . ( Less_Parser::$options['compress'] ? ':' : ': ' ), $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 + ); + } + + /** + * @see less-2.5.3.js#Rule.prototype.eval + * @param Less_Environment $env + * @return self + */ + public function compile( $env ) { + $variable = $this->variable; + $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 ); + } + $variable = false; // never treat expanded interpolation as new variable name + } + + $mathBypass = false; + $prevMath = $env->math; + if ( $name === "font" && $env->math === Less_Environment::MATH_ALWAYS ) { + $mathBypass = true; + $env->math = Less_Environment::MATH_PARENS_DIVISION; + } + + try { + $env->importantScope[] = []; + $evaldValue = $this->value->compile( $env ); + + if ( !$this->variable && $evaldValue instanceof Less_Tree_DetachedRuleset ) { + throw new Less_Exception_Compiler( "Rulesets cannot be evaluated on a property.", null, $this->index, $this->currentFileInfo ); + } + + $important = $this->important; + $importantResult = array_pop( $env->importantScope ); + + if ( !$important && isset( $importantResult['important'] ) && $importantResult['important'] ) { + $important = $importantResult['important']; + } + + $return = new Less_Tree_Declaration( + $name, + $evaldValue, + $important, + $this->merge, + $this->index, + $this->currentFileInfo, + $this->inline, + $variable, + ); + + } catch ( Less_Exception_Parser $e ) { + if ( !is_numeric( $e->index ) ) { + $e->index = $this->index; + $e->currentFile = $this->currentFileInfo; + $e->genMessage(); + } + throw $e; + } + + if ( $mathBypass ) { + $env->math = $prevMath; + } + + 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 self( $this->name, $this->value, '!important', $this->merge, $this->index, $this->currentFileInfo, $this->inline ); + } + + public function mark( $value ) { + if ( !is_array( $this->value ) ) { + + if ( method_exists( $value, 'markReferenced' ) ) { + $value->markReferenced(); + } + } else { + foreach ( $this->value as $v ) { + $this->mark( $v ); + } + } + } + + public function markReferenced() { + if ( $this->value ) { + $this->mark( $this->value ); + } + } + +} diff --git a/wikimedia/less.php/lib/Less/Tree/DefaultFunc.php b/wikimedia/less.php/lib/Less/Tree/DefaultFunc.php index 0c07f38..e4bdf95 100644 --- a/wikimedia/less.php/lib/Less/Tree/DefaultFunc.php +++ b/wikimedia/less.php/lib/Less/Tree/DefaultFunc.php @@ -4,7 +4,9 @@ */ class Less_Tree_DefaultFunc { + /** @var string|null */ private static $error_; + /** @var int|null */ private static $value_; public static function compile() { diff --git a/wikimedia/less.php/lib/Less/Tree/DetachedRuleset.php b/wikimedia/less.php/lib/Less/Tree/DetachedRuleset.php index 53b25fb..632ac6e 100644 --- a/wikimedia/less.php/lib/Less/Tree/DetachedRuleset.php +++ b/wikimedia/less.php/lib/Less/Tree/DetachedRuleset.php @@ -4,7 +4,9 @@ */ class Less_Tree_DetachedRuleset extends Less_Tree { + /** @var Less_Tree_Ruleset */ public $ruleset; + /** @var array|null */ public $frames; public function __construct( $ruleset, $frames = null ) { diff --git a/wikimedia/less.php/lib/Less/Tree/Dimension.php b/wikimedia/less.php/lib/Less/Tree/Dimension.php index f4e87a5..1e7fd5b 100644 --- a/wikimedia/less.php/lib/Less/Tree/Dimension.php +++ b/wikimedia/less.php/lib/Less/Tree/Dimension.php @@ -1,11 +1,13 @@ unit->isSingular() ) { - throw new Less_Exception_Compiler( "Multiple units in dimension. Correct the units or use the unit function. Bad unit: " . $this->unit->toString() ); + 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 ); + $value = $this->fround( $this->value ); $strValue = (string)$value; if ( $value !== 0 && $value < 0.000001 && $value > -0.000001 ) { @@ -75,24 +79,28 @@ public function __toString() { * @param self $other */ public function operate( $op, $other ) { - $value = Less_Functions::operate( $op, $this->value, $other->value ); - $unit = clone $this->unit; + $value = $this->_operate( $op, $this->value, $other->value ); + $unit = $this->unit->clone(); if ( $op === '+' || $op === '-' ) { - if ( !$unit->numerator && !$unit->denominator ) { - $unit->numerator = $other->unit->numerator; - $unit->denominator = $other->unit->denominator; + $unit = $other->unit->clone(); + if ( $this->unit->backupUnit ) { + $unit->backupUnit = $this->unit->backupUnit; + } } 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() . "'." ); + 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 ); + $value = $this->_operate( $op, $this->value, $other->value ); } } elseif ( $op === '*' ) { $unit->numerator = array_merge( $unit->numerator, $other->unit->numerator ); @@ -110,32 +118,28 @@ public function operate( $op, $other ) { return new self( $value, $unit ); } + /** + * @param Less_Tree $other + * @return int|null + * @see less-2.5.3.js#Dimension.prototype.compare + */ public function compare( $other ) { - if ( $other instanceof self ) { - - 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 ( !$other instanceof self ) { + return null; + } - if ( $bValue > $aValue ) { - return -1; - } elseif ( $bValue < $aValue ) { - return 1; - } else { - return 0; - } + if ( $this->unit->isEmpty() || $other->unit->isEmpty() ) { + $a = $this; + $b = $other; } else { - return -1; + $a = $this->unify(); + $b = $other->unify(); + if ( $a->unit->compare( $b->unit ) !== 0 ) { + return null; + } } + + return Less_Tree::numericCompare( $a->value, $b->value ); } public function unify() { @@ -144,7 +148,7 @@ public function unify() { public function convertTo( $conversions ) { $value = $this->value; - $unit = clone $this->unit; + $unit = $this->unit->clone(); if ( is_string( $conversions ) ) { $derivedConversions = []; diff --git a/wikimedia/less.php/lib/Less/Tree/Element.php b/wikimedia/less.php/lib/Less/Tree/Element.php index 6079cca..30ac2ec 100644 --- a/wikimedia/less.php/lib/Less/Tree/Element.php +++ b/wikimedia/less.php/lib/Less/Tree/Element.php @@ -10,11 +10,11 @@ class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty public $combinatorIsEmptyOrWhitespace; /** @var string|Less_Tree */ public $value; + /** @var int|null */ public $index; + /** @var array|null */ public $currentFileInfo; - public $value_is_object = false; - /** * @param null|string $combinator * @param string|Less_Tree $value @@ -23,7 +23,6 @@ class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty */ public function __construct( $combinator, $value, $index = null, $currentFileInfo = null ) { $this->value = $value; - $this->value_is_object = is_object( $value ); // see less-2.5.3.js#Combinator $this->combinator = $combinator ?? ''; @@ -34,7 +33,7 @@ public function __construct( $combinator, $value, $index = null, $currentFileInf } public function accept( $visitor ) { - if ( $this->value_is_object ) { // object or string + if ( $this->value instanceof Less_Tree ) { $this->value = $visitor->visitObj( $this->value ); } } @@ -42,7 +41,7 @@ public function accept( $visitor ) { public function compile( $env ) { return new self( $this->combinator, - ( $this->value_is_object ? $this->value->compile( $env ) : $this->value ), + ( $this->value instanceof Less_Tree ? $this->value->compile( $env ) : $this->value ), $this->index, $this->currentFileInfo ); @@ -56,17 +55,20 @@ public function genCSS( $output ) { } public function toCSS() { - if ( $this->value_is_object ) { + if ( $this->value instanceof Less_Tree ) { $value = $this->value->toCSS(); } else { $value = $this->value; } - if ( $value === '' && $this->combinator === '&' ) { - return ''; + $spaceOrEmpty = ' '; + if ( Less_Parser::$options['compress'] || + ( isset( Less_Environment::$_noSpaceCombinators[$this->combinator] ) && Less_Environment::$_noSpaceCombinators[$this->combinator] ) + ) { + $spaceOrEmpty = ''; } - return Less_Environment::$_outputMap[$this->combinator] . $value; + return $spaceOrEmpty . $this->combinator . $spaceOrEmpty . $value; } } diff --git a/wikimedia/less.php/lib/Less/Tree/Expression.php b/wikimedia/less.php/lib/Less/Tree/Expression.php index 192764e..986b7e0 100644 --- a/wikimedia/less.php/lib/Less/Tree/Expression.php +++ b/wikimedia/less.php/lib/Less/Tree/Expression.php @@ -1,16 +1,20 @@ value = $value; - $this->parens = $parens; + $this->noSpacing = $noSpacing; } public function accept( $visitor ) { @@ -18,46 +22,42 @@ public function accept( $visitor ) { } public function compile( $env ) { + $mathOn = $env->isMathOn(); + // NOTE: We don't support STRICT_LEGACY (Less.js 3.13) + $inParenthesis = $this->parens && ( true || !$this->parensInOp ); $doubleParen = false; - - if ( $this->parens && !$this->parensInOp ) { - Less_Environment::$parensStack++; + if ( $inParenthesis ) { + $env->inParenthesis(); } - $returnValue = null; if ( $this->value ) { - $count = count( $this->value ); - - if ( $count > 1 ) { - + if ( count( $this->value ) > 1 ) { $ret = []; foreach ( $this->value as $e ) { $ret[] = $e->compile( $env ); } - $returnValue = new self( $ret ); + $returnValue = new self( $ret, $this->noSpacing ); } else { - - if ( ( $this->value[0] instanceof self ) && $this->value[0]->parens && !$this->value[0]->parensInOp ) { + // Implied `if ( count() === 1 )` + if ( ( $this->value[0] instanceof self ) && $this->value[0]->parens && !$this->value[0]->parensInOp && !$env->inCalc ) { $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 ); + if ( $inParenthesis ) { + $env->outOfParenthesis(); + } - } + if ( $this->parens && $this->parensInOp && !$mathOn && !$doubleParen && + ( !( $returnValue instanceof Less_Tree_Dimension ) ) + ) { + $returnValue = new Less_Tree_Paren( $returnValue ); } return $returnValue; } @@ -69,8 +69,13 @@ 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( ' ' ); + if ( !$this->noSpacing && ( $i + 1 < $val_len ) ) { + // NOTE: Comma handling backported from Less.js 4.2.1 (T386077) + if ( !( $this->value[$i + 1] instanceof Less_Tree_Anonymous ) + || ( $this->value[$i + 1] instanceof Less_Tree_Anonymous && $this->value[$i + 1]->value !== ',' ) + ) { + $output->add( ' ' ); + } } } } @@ -87,4 +92,34 @@ public function throwAwayComments() { $this->value = $new_value; } } + + public function markReferenced() { + if ( is_array( $this->value ) ) { + foreach ( $this->value as $v ) { + if ( method_exists( $v, 'markReferenced' ) ) { + $v->markReferenced(); + } + } + } + } + + /** + * Should be used only in Less_Tree_Call::functionCaller() + * to retrieve expression without comments + * @internal + */ + public function mapToFunctionCallArgument() { + if ( is_array( $this->value ) ) { + $subNodes = []; + foreach ( $this->value as $subNode ) { + if ( !( $subNode instanceof Less_Tree_Comment ) ) { + $subNodes[] = $subNode; + } + } + return count( $subNodes ) === 1 + ? $subNodes[0] + : new Less_Tree_Expression( $subNodes ); + } + return $this; + } } diff --git a/wikimedia/less.php/lib/Less/Tree/Extend.php b/wikimedia/less.php/lib/Less/Tree/Extend.php index e0a9b2a..e121202 100644 --- a/wikimedia/less.php/lib/Less/Tree/Extend.php +++ b/wikimedia/less.php/lib/Less/Tree/Extend.php @@ -4,16 +4,25 @@ */ class Less_Tree_Extend extends Less_Tree { + /** @var Less_Tree_Selector */ public $selector; + /** @var string */ public $option; + /** @var int */ public $index; + /** @var Less_Tree_Selector[] */ public $selfSelectors = []; + /** @var bool */ public $allowBefore; + /** @var bool */ public $allowAfter; + /** @var bool */ public $firstExtendOnThisSelectorPath; + /** @var Less_Tree_Ruleset|null */ public $ruleset; - + /** @var string */ public $object_id; + /** @var array */ public $parent_ids = []; /** diff --git a/wikimedia/less.php/lib/Less/Tree/Import.php b/wikimedia/less.php/lib/Less/Tree/Import.php index 4c6b7f3..fcad61f 100644 --- a/wikimedia/less.php/lib/Less/Tree/Import.php +++ b/wikimedia/less.php/lib/Less/Tree/Import.php @@ -12,38 +12,54 @@ * the file has been fetched, and parsed. * * @private + * @see less-2.5.3.js#Import.prototype */ class Less_Tree_Import extends Less_Tree { + /** @var array */ public $options; + /** @var int */ public $index; + /** @var Less_Tree_Quoted|Less_Tree_Url */ public $path; + /** @var Less_Tree_Value */ public $features; + /** @var array|null */ public $currentFileInfo; + /** @var bool|null */ public $css; - public $skip; + /** @var bool|null This is populated by Less_ImportVisitor */ + public $doSkip = false; + /** @var string|null This is populated by Less_ImportVisitor */ + public $importedFilename; + /** + * This is populated by Less_ImportVisitor. + * + * For imports that use "inline", this holds a raw string. + * + * @var string|Less_Tree_Ruleset|null + */ public $root; - public function __construct( $path, $features, $options, $index, $currentFileInfo = null ) { - $this->options = $options; + public function __construct( $path, $features, array $options, $index, $currentFileInfo = null ) { + $this->options = $options + [ 'inline' => false, 'optional' => false, 'multiple' => false ]; $this->index = $index; $this->path = $path; $this->features = $features; $this->currentFileInfo = $currentFileInfo; - if ( is_array( $options ) ) { - $this->options += [ '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(); - // Leave any ".css" file imports as literals for the browser. - // Also leave any remote HTTP resources as literals regardless of whether - // they contain ".css" in their filename. - if ( $pathValue && preg_match( '/^(https?:)?\/\/|\.css$/i', $pathValue ) ) { - $this->css = true; - } + if ( isset( $this->options['less'] ) || $this->options['inline'] ) { + $this->css = !isset( $this->options['less'] ) || !$this->options['less'] || $this->options['inline']; + } else { + $pathValue = $this->getPath(); + // Leave any ".css" file imports as literals for the browser. + // Also leave any remote HTTP resources as literals regardless of whether + // they contain ".css" in their filename. + if ( $pathValue && ( + preg_match( '/[#\.\&\?\/]css([\?;].*)?$/', $pathValue ) + || preg_match( '/^(https?:)?\/\//i', $pathValue ) + ) ) { + $this->css = true; } } } @@ -69,14 +85,9 @@ public function accept( $visitor ) { } } - /** - * @see Less_Tree::genCSS - */ public function genCSS( $output ) { - if ( $this->css ) { - + if ( $this->css && !isset( $this->path->currentFileInfo["reference"] ) ) { $output->add( '@import ', $this->currentFileInfo, $this->index ); - $this->path->genCSS( $output ); if ( $this->features ) { $output->add( ' ' ); @@ -86,47 +97,47 @@ public function genCSS( $output ) { } } - public function toCSS() { - $features = $this->features ? ' ' . $this->features->toCSS() : ''; - - if ( $this->css ) { - return "@import " . $this->path->toCSS() . $features . ";\n"; - } else { - return ""; - } - } - /** * @return string|null */ 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'; - // During the first pass, Less_Tree_Url may contain a Less_Tree_Variable (not yet expanded), // and thus has no value property defined yet. Return null until we reach the next phase. // https://github.com/wikimedia/less.php/issues/29 - } elseif ( $this->path instanceof Less_Tree_Url && !( $this->path->value instanceof Less_Tree_Variable ) ) { - $path = $this->path->value->value; - } else { - return null; + // TODO: Upstream doesn't need a check against Less_Tree_Variable. Why do we? + $path = ( $this->path instanceof Less_Tree_Url && !( $this->path->value instanceof Less_Tree_Variable ) ) + ? $this->path->value->value + // e.g. Less_Tree_Quoted + : $this->path->value; + + if ( is_string( $path ) ) { + // remove query string and fragment + return preg_replace( '/[\?#][^\?]*$/', '', $path ); } + } - // remove query string and fragment - return preg_replace( '/[\?#][^\?]*$/', '', $path ); + public function isVariableImport() { + $path = $this->path; + if ( $path instanceof Less_Tree_Url ) { + $path = $path->value; + } + if ( $path instanceof Less_Tree_Quoted ) { + return $path->containsVariables(); + } + return true; } public function compileForImport( $env ) { - return new self( $this->path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo ); + $path = $this->path; + if ( $path instanceof Less_Tree_Url ) { + $path = $path->value; + } + return new self( $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']; - } + $rootpath = $this->currentFileInfo['rootpath'] ?? null; if ( !( $path instanceof Less_Tree_Url ) ) { if ( $rootpath ) { @@ -142,158 +153,72 @@ public function compilePath( $env ) { return $path; } + /** + * @param Less_Environment $env + * @see less-2.5.3.js#Import.prototype.eval + */ public function compile( $env ) { - $evald = $this->compileForImport( $env ); - - // get path & uri - $callback = Less_Parser::$options['import_callback']; - $path_and_uri = is_callable( $callback ) ? $callback( $evald ) : null; - - 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(); - } + $features = ( $this->features ? $this->features->compile( $env ) : null ); // import once - if ( $evald->skip( $full_path, $env ) ) { + if ( $this->skip( $env ) ) { return []; } - '@phan-var string $full_path'; 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, [], true ); - - if ( $this->features ) { - return new Less_Tree_Media( [ $contents ], $this->features->value ); - } - - return [ $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 []; - } - - // css ? - if ( $evald->css ) { - $features = ( $evald->features ? $evald->features->compile( $env ) : null ); - return new self( $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 - */ - public function PathAndUri() { - $evald_path = $this->getPath(); - - if ( $evald_path ) { - - $import_dirs = []; - - 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'] ] = ''; - - } + $contents = new Less_Tree_Anonymous( + $this->root, + 0, + [ + 'filename' => $this->importedFilename, + 'reference' => $this->currentFileInfo['reference'] ?? null, + ], + true, + true, + false + ); + return $this->features + ? new Less_Tree_Media( [ $contents ], $this->features->value ) + : [ $contents ]; + } elseif ( $this->css ) { + $newImport = new self( $this->compilePath( $env ), $features, $this->options, $this->index ); + // TODO: We might need upstream's `if (!newImport.css && this.error) { throw this.error;` + return $newImport; + } else { + $ruleset = new Less_Tree_Ruleset( null, $this->root->rules ); - // always look in user supplied import directories - $import_dirs = array_merge( $import_dirs, Less_Parser::$options['import_dirs'] ); + $ruleset->evalImports( $env ); - foreach ( $import_dirs as $rootpath => $rooturi ) { - if ( is_callable( $rooturi ) ) { - $res = $rooturi( $evald_path ); - if ( $res && is_string( $res[0] ) ) { - return [ - Less_Environment::normalizePath( $res[0] ), - Less_Environment::normalizePath( $res[1] ?? dirname( $evald_path ) ) - ]; - } - } elseif ( !empty( $rootpath ) ) { - $path = rtrim( $rootpath, '/\\' ) . '/' . ltrim( $evald_path, '/\\' ); - if ( file_exists( $path ) ) { - return [ - Less_Environment::normalizePath( $path ), - Less_Environment::normalizePath( dirname( $rooturi . $evald_path ) ) - ]; - } - if ( file_exists( $path . '.less' ) ) { - return [ - Less_Environment::normalizePath( $path . '.less' ), - Less_Environment::normalizePath( dirname( $rooturi . $evald_path . '.less' ) ) - ]; - } - } - } - } - } - - /** - * Parse the import url and return the rules - * - * @param string $full_path - * @param string|null $uri - * @param mixed $env - * @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; - } + return $this->features + ? new Less_Tree_Media( $ruleset->rules, $this->features->value ) + : $ruleset->rules; - 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( null, $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? * - * @param string|null $path * @param Less_Environment $env * @return bool|null */ - private function skip( $path, $env ) { - $path = Less_Parser::AbsPath( $path, true ); + public function skip( $env ) { + $path = $this->getPath(); + // TODO: Since our Import->getPath() varies from upstream Less.js (ours can return null). + // we therefore need an empty string fallback here. Remove this fallback once getPath() + // is in sync with upstream. + $fullPath = Less_FileManager::getFilePath( $path, $this->currentFileInfo )[0] ?? $path ?? ''; - if ( $path && Less_Parser::FileParsed( $path ) ) { - - if ( isset( $this->currentFileInfo['reference'] ) ) { - return true; - } + if ( $this->doSkip !== null ) { + return $this->doSkip; + } - return !isset( $this->options['multiple'] ) && !$env->importMultiple; + // @see less-2.5.3.js#ImportVisitor.prototype.onImported + if ( isset( $env->importVisitorOnceMap[$fullPath] ) ) { + return true; } + + $env->importVisitorOnceMap[$fullPath] = true; + return false; } } diff --git a/wikimedia/less.php/lib/Less/Tree/Javascript.php b/wikimedia/less.php/lib/Less/Tree/JavaScript.php similarity index 73% rename from wikimedia/less.php/lib/Less/Tree/Javascript.php rename to wikimedia/less.php/lib/Less/Tree/JavaScript.php index ff432c1..50be0a1 100644 --- a/wikimedia/less.php/lib/Less/Tree/Javascript.php +++ b/wikimedia/less.php/lib/Less/Tree/JavaScript.php @@ -1,19 +1,23 @@ escaped = $escaped; $this->expression = $string; $this->index = $index; diff --git a/wikimedia/less.php/lib/Less/Tree/Keyword.php b/wikimedia/less.php/lib/Less/Tree/Keyword.php index 8da4ce6..be5b2a4 100644 --- a/wikimedia/less.php/lib/Less/Tree/Keyword.php +++ b/wikimedia/less.php/lib/Less/Tree/Keyword.php @@ -24,12 +24,4 @@ public function genCSS( $output ) { $output->add( $this->value ); } - - public function compare( $other ) { - if ( $other instanceof self ) { - return $other->value === $this->value ? 0 : 1; - } else { - return -1; - } - } } diff --git a/wikimedia/less.php/lib/Less/Tree/Media.php b/wikimedia/less.php/lib/Less/Tree/Media.php index f13cc55..f52e49e 100644 --- a/wikimedia/less.php/lib/Less/Tree/Media.php +++ b/wikimedia/less.php/lib/Less/Tree/Media.php @@ -4,10 +4,15 @@ */ class Less_Tree_Media extends Less_Tree { + /** @var Less_Tree_Value */ public $features; + /** @var Less_Tree_Ruleset[] */ public $rules; + /** @var int|null */ public $index; + /** @var array|null */ public $currentFileInfo; + /** @var bool|null */ public $isReferenced; public function __construct( $value = [], $features = [], $index = null, $currentFileInfo = null ) { @@ -44,16 +49,16 @@ public function genCSS( $output ) { public function compile( $env ) { $media = new self( [], [], $this->index, $this->currentFileInfo ); - $strictMathBypass = false; - if ( Less_Parser::$options['strictMath'] === false ) { - $strictMathBypass = true; - Less_Parser::$options['strictMath'] = true; + $mathBypass = false; + if ( !$env->mathOn ) { + $mathBypass = true; + $env->mathOn = true; } $media->features = $this->features->compile( $env ); - if ( $strictMathBypass ) { - Less_Parser::$options['strictMath'] = false; + if ( $mathBypass ) { + $env->mathOn = false; } $env->mediaPath[] = $media; @@ -134,7 +139,7 @@ public function compileNested( $env ) { 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] ); + $path[$i] = $path[$i] instanceof Less_Tree ? $path[$i] : new Less_Tree_Anonymous( $path[$i] ); } for ( $i = count( $path ) - 1; $i > 0; $i-- ) { diff --git a/wikimedia/less.php/lib/Less/Tree/Mixin/Call.php b/wikimedia/less.php/lib/Less/Tree/Mixin/Call.php index d0363e0..df759dc 100644 --- a/wikimedia/less.php/lib/Less/Tree/Mixin/Call.php +++ b/wikimedia/less.php/lib/Less/Tree/Mixin/Call.php @@ -4,17 +4,18 @@ */ class Less_Tree_Mixin_Call extends Less_Tree { + /** @var Less_Tree_Selector */ public $selector; + /** @var array[] */ public $arguments; + /** @var int */ public $index; + /** @var array */ public $currentFileInfo; + /** @var bool */ public $important; - /** - * less.js: tree.mixin.Call - * - */ public function __construct( $elements, $args, $index, $currentFileInfo, $important = false ) { $this->selector = new Less_Tree_Selector( $elements ); $this->arguments = $args; @@ -23,36 +24,41 @@ public function __construct( $elements, $args, $index, $currentFileInfo, $import $this->important = $important; } - // function accept($visitor){ - // $this->selector = $visitor->visit($this->selector); - // $this->arguments = $visitor->visit($this->arguments); - //} - + /** + * @see less-2.5.3.js#MixinCall.prototype.eval + */ public function compile( $env ) { $rules = []; $match = false; $isOneFound = false; $candidates = []; - $defaultUsed = false; $conditionResult = []; - + $this->selector = $this->selector->compile( $env ); $args = []; foreach ( $this->arguments as $a ) { - $args[] = [ 'name' => $a['name'], 'value' => $a['value']->compile( $env ) ]; + $argValue = $a['value']->compile( $env ); + if ( !empty( $a['expand'] ) && is_array( $argValue->value ) ) { + foreach ( $argValue->value as $value ) { + $args[] = [ 'name' => null, 'value' => $value ]; + } + } else { + $args[] = [ 'name' => $a['name'], 'value' => $argValue ]; + } } + $defNone = 0; + $defTrue = 1; + $defFalse = 2; foreach ( $env->frames as $frame ) { - - $mixins = $frame->find( $this->selector ); - + $noArgumentsFilter = static function ( $rule ) use ( $env ) { + return $rule->matchArgs( [], $env ); + }; + $mixins = $frame->find( $this->selector, null, $noArgumentsFilter ); 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`), @@ -61,7 +67,8 @@ public function compile( $env ) { $mixins_len = count( $mixins ); for ( $m = 0; $m < $mixins_len; $m++ ) { - $mixin = $mixins[$m]; + $mixin = $mixins[$m]["rule"]; + $mixinPath = $mixins[$m]["path"]; if ( $this->IsRecursive( $env, $mixin ) ) { continue; @@ -69,25 +76,36 @@ public function compile( $env ) { if ( $mixin->matchArgs( $args, $env ) ) { - $candidate = [ '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 ); + // less-2.5.3.js#MixinCall calcDefGroup() + $group = -1; + for ( $f = 0; $f < 2; $f++ ) { + $conditionResult[$f] = true; + Less_Tree_DefaultFunc::value( $f ); + for ( $p = 0; $p < count( $mixinPath ) && $conditionResult[$f]; $p++ ) { + $namespace = $mixinPath[$p] ?? null; + if ( isset( $namespace ) && method_exists( $namespace, "matchCondition" ) ) { + $conditionResult[$f] = $conditionResult[$f] && $namespace->matchCondition( null, $env ); + } + } + if ( method_exists( $mixin, "matchCondition" ) ) { + $conditionResult[$f] = $conditionResult[$f] && $mixin->matchCondition( $args, $env ); + } + } + // PhanTypeInvalidDimOffset -- False positive + '@phan-var array{0:bool,1:bool} $conditionResult'; + if ( $conditionResult[0] || $conditionResult[1] ) { + if ( $conditionResult[0] != $conditionResult[1] ) { + $group = $conditionResult[1] ? + $defTrue : $defFalse; + } else { + $group = $defNone; } - // PhanTypeInvalidDimOffset -- False positive - '@phan-var array{0:bool,1:bool} $conditionResult'; + } - if ( $conditionResult[0] || $conditionResult[1] ) { - if ( $conditionResult[0] != $conditionResult[1] ) { - $candidate['group'] = $conditionResult[1] ? $defTrue : $defFalse; - } + $candidate = [ 'mixin' => $mixin, 'group' => $group ]; - $candidates[] = $candidate; - } - } else { + if ( $candidate["group"] !== -1 ) { $candidates[] = $candidate; } @@ -112,20 +130,18 @@ public function compile( $env ) { } $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{ + try { $mixin = $candidates[$m]['mixin']; if ( !( $mixin instanceof Less_Tree_Mixin_Definition ) ) { + $originalRuleset = $mixin instanceof Less_Tree_Ruleset ? $mixin->originalRuleset : $mixin; $mixin = new Less_Tree_Mixin_Definition( '', [], $mixin->rules, null, false ); - $mixin->originalRuleset = $mixins[$m]->originalRuleset; + $mixin->originalRuleset = $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 ); } } @@ -142,16 +158,24 @@ public function compile( $env ) { if ( $isOneFound ) { $selectorName = $this->selector->toCSS(); - throw new Less_Exception_Compiler( 'No matching definition was found for ' . $selectorName . ' with args `' . $this->Format( $args ) . '`', null, $this->index, $this->currentFileInfo ); + throw new Less_Exception_Compiler( + 'No matching definition was found for ' . $selectorName . ' with args `' . $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 ); + 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 = []; @@ -161,7 +185,7 @@ private function Format( $args ) { if ( $a['name'] ) { $argValue .= $a['name'] . ':'; } - if ( is_object( $a['value'] ) ) { + if ( $a['value'] instanceof Less_Tree ) { $argValue .= $a['value']->toCSS(); } else { $argValue .= '???'; diff --git a/wikimedia/less.php/lib/Less/Tree/Mixin/Definition.php b/wikimedia/less.php/lib/Less/Tree/Mixin/Definition.php index 9a4d266..13f210c 100644 --- a/wikimedia/less.php/lib/Less/Tree/Mixin/Definition.php +++ b/wikimedia/less.php/lib/Less/Tree/Mixin/Definition.php @@ -3,18 +3,29 @@ * @private */ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { + /** @var string */ public $name; + /** @var Less_Tree_Selector[] */ public $selectors; + /** @var array[] */ public $params; + /** @var int */ public $arity = 0; + /** @var Less_Tree[] */ public $rules; - public $lookups = []; - public $required = 0; - public $frames = []; + /** @var array[][] */ + public $lookups = []; + /** @var int */ + public $required = 0; + /** @var array */ + public $frames = []; + /** @var Less_Tree_Condition|null */ public $condition; + /** @var bool */ public $variadic; + /** @var array */ + public $optionalParameters = []; - // less.js : /lib/less/tree/mixin.js : tree.mixin.Definition public function __construct( $name, $params, $rules, $condition, $variadic = false, $frames = [] ) { $this->name = $name; $this->selectors = [ new Less_Tree_Selector( [ new Less_Tree_Element( null, $name ) ] ) ]; @@ -29,6 +40,8 @@ public function __construct( $name, $params, $rules, $condition, $variadic = fal foreach ( $params as $p ) { if ( !isset( $p['name'] ) || ( $p['name'] && !isset( $p['value'] ) ) ) { $this->required++; + } else { + $this->optionalParameters[ (string)$p['name'] ] = true; } } } @@ -37,17 +50,10 @@ public function __construct( $name, $params, $rules, $condition, $variadic = fal $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 + /** + * @param Less_Environment $env + * @see less-2.5.3.js#Definition.prototype.evalParams + */ public function compileParams( $env, $mixinFrames, $args = [], &$evaldArguments = [] ) { $frame = new Less_Tree_Ruleset( null, [] ); $params = $this->params; @@ -65,52 +71,53 @@ public function compileParams( $env, $mixinFrames, $args = [], &$evaldArguments foreach ( $params as $j => $param ) { if ( !isset( $evaldArguments[$j] ) && $arg['name'] === $param['name'] ) { $evaldArguments[$j] = $arg['value']->compile( $env ); - array_unshift( $frame->rules, new Less_Tree_Rule( $arg['name'], $arg['value']->compile( $env ) ) ); + array_unshift( $frame->rules, new Less_Tree_Declaration( $arg['name'], $arg['value']->compile( $env ) ) ); $isNamedFound = true; break; } } - if ( !$isNamedFound ) { + if ( $isNamedFound ) { + array_splice( $args, $i, 1 ); + $i--; + $argsLength--; + } else { throw new Less_Exception_Compiler( "Named argument for " . $this->name . ' ' . $args[$i]['name'] . ' not found' ); } - array_splice( $args, $i, 1 ); - $i--; - $argsLength--; } } } $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'] ) { + $arg = $args[$argIndex] ?? null; + $name = $param['name'] ?? null; + if ( $name ) { if ( isset( $param['variadic'] ) ) { $varargs = []; 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 ) ) ); + array_unshift( $frame->rules, new Less_Tree_Declaration( $name, $expression->compile( $env ) ) ); } else { $val = ( $arg && $arg['value'] ) ? $arg['value'] : false; if ( $val ) { - $val = $val->compile( $env ); + // This was a mixin call, pass in a detached ruleset of it's eval'd rules + if ( is_array( $val ) ) { + $val = new Less_Tree_DetachedRuleset( new Less_Tree_Ruleset( null, $val ) ); + } else { + $val = $val->compile( $env ); + } } elseif ( isset( $param['value'] ) ) { if ( !$mixinEnv ) { - $mixinEnv = new Less_Environment(); - $mixinEnv->frames = array_merge( [ $frame ], $mixinFrames ); + $mixinEnv = $env->copyEvalEnv( array_merge( [ $frame ], $mixinFrames ) ); } $val = $param['value']->compile( $mixinEnv ); @@ -119,7 +126,7 @@ public function compileParams( $env, $mixinFrames, $args = [], &$evaldArguments 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 ) ); + array_unshift( $frame->rules, new Less_Tree_Declaration( $name, $val ) ); $evaldArguments[$i] = $val; } } @@ -145,6 +152,12 @@ public function compile( $env ) { return new self( $this->name, $this->params, $this->rules, $this->condition, $this->variadic, $env->frames ); } + /** + * @param Less_Environment $env + * @param array|null $args + * @param bool|null $important + * @return Less_Tree_Ruleset + */ public function evalCall( $env, $args = null, $important = null ) { Less_Environment::$mixin_stack++; @@ -159,13 +172,12 @@ public function evalCall( $env, $args = null, $important = null ) { $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 ) ) ); + array_unshift( $frame->rules, new Less_Tree_Declaration( '@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( [ $this, $frame ], $mixinFrames ); + $ruleSetEnv = $env->copyEvalEnv( array_merge( [ $this, $frame ], $mixinFrames ) ); $ruleset = $ruleset->compile( $ruleSetEnv ); if ( $important ) { @@ -177,7 +189,11 @@ public function evalCall( $env, $args = null, $important = null ) { return $ruleset; } - /** @return bool */ + /** + * @param array $args + * @param Less_Environment $env + * @return bool + */ public function matchCondition( $args, $env ) { if ( !$this->condition ) { return true; @@ -185,40 +201,62 @@ public function matchCondition( $args, $env ) { // set array to prevent error on array_merge if ( !is_array( $this->frames ) ) { - $this->frames = []; + $this->frames = []; } $frame = $this->compileParams( $env, array_merge( $this->frames, $env->frames ), $args ); - $compile_env = new Less_Environment(); - $compile_env->frames = array_merge( - [ $frame ], // the parameter variables - $this->frames, // the parent namespace/mixin frames - $env->frames // the current environment frames - ); - + $compile_env = $env->copyEvalEnv( + array_merge( + [ $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 ); + public function makeImportant() { + $important_rules = []; + foreach ( $this->rules as $rule ) { + if ( $rule instanceof Less_Tree_Declaration || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) { + $important_rules[] = $rule->makeImportant(); + } else { + $important_rules[] = $rule; + } + } + return new self( $this->name, $this->params, $important_rules, $this->condition, $this->variadic, $this->frames ); + } + /** + * @param array[] $args + * @param Less_Environment|null $env + * @see less-2.5.3.js#Definition.prototype.matchArgs + */ + public function matchArgs( $args, $env = null ) { + $allArgsCnt = count( $args ); + $requiredArgsCnt = 0; + foreach ( $args as $arg ) { + if ( !array_key_exists( $arg['name'], $this->optionalParameters ) ) { + $requiredArgsCnt++; + } + } if ( !$this->variadic ) { - if ( $argsLength < $this->required ) { + if ( $requiredArgsCnt < $this->required ) { return false; } - if ( $argsLength > count( $this->params ) ) { + if ( $allArgsCnt > count( $this->params ) ) { return false; } } else { - if ( $argsLength < ( $this->required - 1 ) ) { + if ( $requiredArgsCnt < ( $this->required - 1 ) ) { return false; } } - $len = min( $argsLength, $this->arity ); + $len = min( $requiredArgsCnt, $this->arity ); for ( $i = 0; $i < $len; $i++ ) { if ( !isset( $this->params[$i]['name'] ) && !isset( $this->params[$i]['variadic'] ) ) { diff --git a/wikimedia/less.php/lib/Less/Tree/NameValue.php b/wikimedia/less.php/lib/Less/Tree/NameValue.php index 32c1b09..95b9a49 100644 --- a/wikimedia/less.php/lib/Less/Tree/NameValue.php +++ b/wikimedia/less.php/lib/Less/Tree/NameValue.php @@ -3,7 +3,7 @@ * A simple CSS name-value pair, e.g. `width: 100px;` * * In bootstrap, there are about 600-1000 simple name-value pairs (depending on - * how forgiving the match is) -vs- 6,020 dynamic rules (Less_Tree_Rule). + * how forgiving the match is) -vs- 6,020 dynamic rules (Less_Tree_Declaration). * * Using the name-value object can speed up bootstrap compilation slightly, but * it breaks color keyword interpretation: `color: red` -> `color: #FF0000`. @@ -12,10 +12,15 @@ */ class Less_Tree_NameValue extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $name; + /** @var string */ public $value; + /** @var int|null */ public $index; + /** @var array|null */ public $currentFileInfo; + /** @var string */ public $important = ''; public function __construct( $name, $value = null, $index = null, $currentFileInfo = null ) { @@ -28,11 +33,11 @@ public function __construct( $name, $value = null, $index = null, $currentFileIn 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 ); + . ( Less_Parser::$options['compress'] ? ':' : ': ' ) + . $this->value + . $this->important + . ( ( ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), + $this->currentFileInfo, $this->index ); } public function compile( $env ) { diff --git a/wikimedia/less.php/lib/Less/Tree/NamespaceValue.php b/wikimedia/less.php/lib/Less/Tree/NamespaceValue.php new file mode 100644 index 0000000..de605ae --- /dev/null +++ b/wikimedia/less.php/lib/Less/Tree/NamespaceValue.php @@ -0,0 +1,96 @@ +value = $ruleCall; + $this->lookups = $lookups; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function compile( $env ) { + /** @var Less_Tree_Ruleset $rules */ + $rules = $this->value->compile( $env ); + + foreach ( $this->lookups as $name ) { + /** + * Eval'd DRs return rulesets. + * Eval'd mixins return rules, so let's make a ruleset if we need it. + * We need to do this because of late parsing of values + */ + if ( is_array( $rules ) ) { + $rules = new Less_Tree_Ruleset( [ new Less_Tree_Selector( [] ) ], $rules ); + } + if ( $name === '' ) { + $rules = $rules->lastDeclaration(); + } elseif ( $name[0] === '@' ) { + if ( ( $name[1] ?? '' ) === '@' ) { + $variable = ( new Less_Tree_Variable( substr( $name, 1 ) ) )->compile( $env ); + $name = "@" . $variable->value; + } + if ( $rules instanceof Less_Tree_Ruleset ) { + $rules = $rules->variable( $name ); + } + + if ( !$rules ) { + throw new Less_Exception_Compiler( + "Variable $name not found", + null, + $this->index, + $this->currentFileInfo + ); + } + } else { + if ( strncmp( $name, '$@', 2 ) === 0 ) { + $variable = ( new Less_Tree_Variable( substr( $name, 1 ) ) )->compile( $env ); + $name = "$" . $variable->value; + } else { + $name = $name[0] === '$' ? $name : ( '$' . $name ); + } + + if ( $rules instanceof Less_Tree_Ruleset ) { + $rules = $rules->property( $name ); + } + + if ( !$rules ) { + throw new Less_Exception_Compiler( + "Property $name not found", + null, + $this->index, + $this->currentFileInfo + ); + } + // Properties are an array of values, since a ruleset can have multiple props. + // We pick the last one (the "cascaded" value) + if ( is_array( $rules ) ) { // to satisfy phan checks + $rules = $rules[ count( $rules ) - 1 ]; + } + } + + if ( $rules->value ) { + $rules = $rules->compile( $env )->value; + } + if ( $rules instanceof Less_Tree_DetachedRuleset && $rules->ruleset ) { + // @todo - looks like this is never evaluated, investigate later + // @see https://github.com/less/less.js/commit/29468bffcd8a9f2f + $rules = $rules->ruleset->compile( $env ); + } + } + + return $rules; + } + +} diff --git a/wikimedia/less.php/lib/Less/Tree/Negative.php b/wikimedia/less.php/lib/Less/Tree/Negative.php index 0dbc128..5ed324a 100644 --- a/wikimedia/less.php/lib/Less/Tree/Negative.php +++ b/wikimedia/less.php/lib/Less/Tree/Negative.php @@ -4,16 +4,13 @@ */ class Less_Tree_Negative extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var Less_Tree */ public $value; public function __construct( $node ) { $this->value = $node; } - // function accept($visitor) { - // $this->value = $visitor->visit($this->value); - //} - /** * @see Less_Tree::genCSS */ @@ -23,7 +20,7 @@ public function genCSS( $output ) { } public function compile( $env ) { - if ( Less_Environment::isMathOn() ) { + if ( $env->isMathOn() ) { $ret = new Less_Tree_Operation( '*', [ new Less_Tree_Dimension( -1 ), $this->value ] ); return $ret->compile( $env ); } diff --git a/wikimedia/less.php/lib/Less/Tree/Operation.php b/wikimedia/less.php/lib/Less/Tree/Operation.php index 38f2fac..50eddce 100644 --- a/wikimedia/less.php/lib/Less/Tree/Operation.php +++ b/wikimedia/less.php/lib/Less/Tree/Operation.php @@ -1,11 +1,15 @@ isMathOn( $this->op ) ) { + $op = $this->op === './' ? '/' : $this->op; 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 ( !( $a instanceof Less_Tree_Dimension || $a instanceof Less_Tree_Color ) ) { + if ( !( $a instanceof Less_Tree_Dimension || $a instanceof Less_Tree_Color ) + || !( $b instanceof Less_Tree_Dimension || $b instanceof Less_Tree_Color ) + ) { + if ( $a instanceof Less_Tree_Operation && $a->op === '/' && $env->math === Less_Environment::MATH_PARENS_DIVISION + ) { + return new self( $this->op, [ $a, $b ], $this->isSpaced ); + } throw new Less_Exception_Compiler( "Operation on an invalid type" ); } - if ( $b instanceof Less_Tree_Dimension || $b instanceof Less_Tree_Color ) { - return $a->operate( $this->op, $b ); - } + return $a->operate( $op, $b ); + } else { + return new self( $this->op, [ $a, $b ], $this->isSpaced ); } - - return new self( $this->op, [ $a, $b ], $this->isSpaced ); } /** diff --git a/wikimedia/less.php/lib/Less/Tree/Property.php b/wikimedia/less.php/lib/Less/Tree/Property.php new file mode 100644 index 0000000..894e5cd --- /dev/null +++ b/wikimedia/less.php/lib/Less/Tree/Property.php @@ -0,0 +1,77 @@ +name = $name; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function compile( $env ) { + $name = $this->name; + + if ( $this->evaluating ) { + throw new Less_Exception_Compiler( + "Recursive property reference for " . $name, + null, + $this->index, $this->currentFileInfo + ); + } + + $property = null; + $this->evaluating = true; + /** @var Less_Tree_Ruleset $frame */ + foreach ( $env->frames as $frame ) { + $vArr = $frame->property( $name ); + if ( $vArr ) { + $size = count( $vArr ); + for ( $i = 0; $i < $size; $i++ ) { + $v = $vArr[$i]; + $vArr[$i] = new Less_Tree_Declaration( + $v->name, + $v->value, + $v->important, + $v->merge, + $v->index, + $v->currentFileInfo, + $v->inline, + $v->variable + ); + } + Less_Visitor_toCSS::_mergeRules( $vArr ); + $v = $vArr[ count( $vArr ) - 1 ]; + if ( isset( $v->important ) && $v->important ) { + $importantScopeLength = count( $env->importantScope ); + $env->importantScope[ $importantScopeLength - 1 ]['important'] = $v->important; + } + $property = $v->value->compile( $env ); + break; + } + } + + if ( $property ) { + $this->evaluating = false; + return $property; + } else { + throw new Less_Exception_Compiler( "property '" . $name . "' is undefined in file " . + $this->currentFileInfo["filename"], null, $this->index, $this->currentFileInfo ); + } + } + +} diff --git a/wikimedia/less.php/lib/Less/Tree/Quoted.php b/wikimedia/less.php/lib/Less/Tree/Quoted.php index e9d199e..dfd163d 100644 --- a/wikimedia/less.php/lib/Less/Tree/Quoted.php +++ b/wikimedia/less.php/lib/Less/Tree/Quoted.php @@ -3,17 +3,26 @@ * @private */ class Less_Tree_Quoted extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var bool */ public $escaped; /** @var string */ public $value; + /** @var string */ public $quote; + /** @var int|false */ public $index; + /** @var array|null */ public $currentFileInfo; + /** @var string */ + public $variableRegex = '/@\{([\w-]+)\}/'; + /** @var string */ + public $propRegex = '/\$\{([\w-]+)\}/'; + /** * @param string $str */ - public function __construct( $str, $content = '', $escaped = false, $index = false, $currentFileInfo = null ) { + public function __construct( $str, $content = '', $escaped = true, $index = false, $currentFileInfo = null ) { $this->escaped = $escaped; $this->value = $content; if ( $str ) { @@ -36,40 +45,63 @@ public function genCSS( $output ) { } } - public function compile( $env ) { - $value = $this->value; - if ( preg_match_all( '/`([^`]+)`/', $this->value, $matches ) ) { - foreach ( $matches[1] as $i => $match ) { - $js = new Less_Tree_JavaScript( $match, $this->index, true ); - $js = $js->compile( $env )->value; - $value = str_replace( $matches[0][$i], $js, $value ); + /** + * @see less-3.13.1.js#Quoted.prototype.containsVariables + */ + public function containsVariables() { + return preg_match( $this->variableRegex, $this->value ); + } + + private function variableReplacement( $r, $env ) { + do { + $value = $r; + if ( preg_match_all( $this->variableRegex, $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 self ) ? $v->value : $v->toCSS(); + $r = str_replace( $matches[0][$i], $v, $r ); + } } - } + } while ( $r != $value ); + return $r; + } - 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 self ) ? $v->value : $v->toCSS(); - $value = str_replace( $matches[0][$i], $v, $value ); + private function propertyReplacement( $r, $env ) { + do { + $value = $r; + if ( preg_match_all( $this->propRegex, $value, $matches ) ) { + foreach ( $matches[1] as $i => $match ) { + $v = new Less_Tree_Property( '$' . $match, $this->index, $this->currentFileInfo ); + $v = $v->compile( $env ); + $v = ( $v instanceof self ) ? $v->value : $v->toCSS(); + $r = str_replace( $matches[0][$i], $v, $r ); + } } - } + } while ( $r != $value ); + return $r; + } + public function compile( $env ) { + $value = $this->value; + $value = $this->variableReplacement( $value, $env ); + $value = $this->propertyReplacement( $value, $env ); return new self( $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; + /** + * @param mixed $other + * @return int|null + * @see less-2.5.3.js#Quoted.prototype.compare + */ + public function compare( $other ) { + if ( $other instanceof self && !$this->escaped && !$other->escaped ) { + return Less_Tree::numericCompare( $this->value, $other->value ); + } else { + return ( + $other instanceof Less_Tree + && $this->toCSS() === $other->toCSS() + ) ? 0 : null; } - - return $left < $right ? -1 : 1; } } diff --git a/wikimedia/less.php/lib/Less/Tree/Rule.php b/wikimedia/less.php/lib/Less/Tree/Rule.php deleted file mode 100644 index 9eeab15..0000000 --- a/wikimedia/less.php/lib/Less/Tree/Rule.php +++ /dev/null @@ -1,122 +0,0 @@ - $name - * @param mixed $value - * @param null|false|string $important - * @param null|false|string $merge - * @param int|null $index - * @param array|null $currentFileInfo - * @param bool $inline - */ - public function __construct( $name, $value = null, $important = null, $merge = null, $index = null, $currentFileInfo = null, $inline = false ) { - $this->name = $name; - $this->value = ( $value instanceof Less_Tree ) - ? $value - : new Less_Tree_Value( [ $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 ); - } - - /** - * @param Less_Environment $env - * @return self - */ - 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 instanceof Less_Tree_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 self( $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; - $e->genMessage(); - } - 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 self( $this->name, $this->value, '!important', $this->merge, $this->index, $this->currentFileInfo, $this->inline ); - } - -} diff --git a/wikimedia/less.php/lib/Less/Tree/Ruleset.php b/wikimedia/less.php/lib/Less/Tree/Ruleset.php index 10e501c..f9bc365 100644 --- a/wikimedia/less.php/lib/Less/Tree/Ruleset.php +++ b/wikimedia/less.php/lib/Less/Tree/Ruleset.php @@ -4,19 +4,31 @@ */ class Less_Tree_Ruleset extends Less_Tree { + /** @var array[][] */ protected $lookups; + /** @var array|null */ public $_variables; - public $_rulesets; + /** @var array|null */ + public $_properties; + /** @var null|bool */ public $strictImports; + /** @var Less_Tree_Selector[]|null */ public $selectors; + /** @var Less_Tree[] */ public $rules; + /** @var true|null */ public $root; + /** @var true|null */ public $allowImports; + /** @var Less_Tree_Selector[][]|null */ public $paths; + /** @var true|null */ public $firstRoot; + /** @var true|null */ public $multiMedia; + /** @var Less_Tree_Extend[] */ public $allExtends; /** @var int */ @@ -24,6 +36,7 @@ class Less_Tree_Ruleset extends Less_Tree { /** @var int */ public $originalRuleset; + /** @var array */ public $first_oelements; public function SetRulesetIndex() { @@ -111,7 +124,7 @@ public function compile( $env ) { for ( $j = 0; $j < count( $rule->rules ); $j++ ) { $subRule = $rule->rules[$j]; - if ( !( $subRule instanceof Less_Tree_Rule ) || !$subRule->variable ) { + if ( !( $subRule instanceof Less_Tree_Declaration ) || !$subRule->variable ) { array_splice( $ruleset->rules, ++$i, 0, [ $subRule ] ); $rsRuleCnt++; } @@ -150,7 +163,7 @@ private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) { $temp = []; foreach ( $rule as $r ) { - if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) { + if ( ( $r instanceof Less_Tree_Declaration ) && $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 @@ -167,12 +180,12 @@ private function EvalMixinCalls( $ruleset, $env, &$rsRuleCnt ) { $i += $temp_count; $ruleset->resetCache(); - } elseif ( $rule instanceof Less_Tree_RulesetCall ) { + } elseif ( $rule instanceof Less_Tree_VariableCall ) { $rule = $rule->compile( $env ); $rules = []; foreach ( $rule->rules as $r ) { - if ( ( $r instanceof Less_Tree_Rule ) && $r->variable ) { + if ( ( $r instanceof Less_Tree_Declaration ) && $r->variable ) { continue; } $rules[] = $r; @@ -267,7 +280,7 @@ public function evalImports( $env ) { public function makeImportant() { $important_rules = []; foreach ( $this->rules as $rule ) { - if ( $rule instanceof Less_Tree_Rule || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) { + if ( $rule instanceof Less_Tree_Declaration || $rule instanceof self || $rule instanceof Less_Tree_NameValue ) { $important_rules[] = $rule->makeImportant(); } else { $important_rules[] = $rule; @@ -295,32 +308,139 @@ public function matchCondition( $args, $env ) { } public function resetCache() { - $this->_rulesets = null; $this->_variables = null; $this->lookups = []; } + /** + * @see less-3.13.1.js#Ruleset.prototype.variables + */ public function variables() { $this->_variables = []; foreach ( $this->rules as $r ) { - if ( $r instanceof Less_Tree_Rule && $r->variable === true ) { + if ( $r instanceof Less_Tree_Declaration && $r->variable === true ) { $this->_variables[$r->name] = $r; } + // when evaluating variables in an import statement, imports have not been eval'd + // so we need to go inside import statements. + // guard against root being a string (in the case of inlined less) + if ( $r instanceof Less_Tree_Import && $r->root instanceof Less_Tree_Ruleset ) { + $vars = $r->root->variables(); + foreach ( $vars as $key => $name ) { + $this->_variables[$key] = $name; + } + } } + return $this->_variables; + } + + /** + * @see less-3.13.1#Ruleset.prototype.properties + */ + public function properties() { + $this->_properties = []; + foreach ( $this->rules as $r ) { + + if ( $r instanceof Less_Tree_Declaration && $r->variable !== true ) { + $name = is_array( $r->name ) && count( $r->name ) === 1 && $r->name[0] instanceof Less_Tree_Keyword + ? $r->name[0]->value + : $r->name; + // Properties don't overwrite as they can merge + + // TODO: differs from upstream. Upstream expects $r->name to be only a + // Less_Tree_Keyword but somehow our parser also returns Less_Tree_Property. + // Let's handle it for now, but we should debug why this happens + // caused by test/Fixtures/lessjs-3.13.1/less/_main/property-accessors.less:59 + if ( is_array( $name ) && $name[0] instanceof Less_Tree_Property ) { + $name = $name[0]->name; + } + + $idx = '$' . $name; + if ( !array_key_exists( $idx, $this->_properties ) ) { + $this->_properties[ $idx ] = []; + } + $this->_properties[ $idx ][] = $r; + } + } + return $this->_properties; } /** * @param string $name - * @return Less_Tree_Rule|null + * @return Less_Tree_Declaration|null + * @see less-3.13.1#Ruleset.prototype.variable */ public function variable( $name ) { if ( $this->_variables === null ) { $this->variables(); } - return $this->_variables[$name] ?? null; + return array_key_exists( $name, $this->_variables ) + ? $this->parseValue( $this->_variables[ $name ] ) + : null; + } + + /** + * @param string $name + * @see less-3.13.1#Ruleset.prototype.property + */ + public function property( $name ) { + if ( $this->_properties === null ) { + $this->properties(); + } + return array_key_exists( $name, $this->_properties ) + ? $this->parseValue( $this->_properties[ $name ] ) + : null; } - public function find( $selector, $self = null ) { + /** + * @param Less_Tree_Declaration $decl + * @return mixed + * @throws Less_Exception_Parser + */ + private function transformDeclaration( $decl ) { + if ( $decl->value instanceof Less_Tree_Anonymous && !$decl->parsed ) { + [ $err, $result ] = self::$parse->parseNode( + (string)$decl->value->value, + [ 'value', 'important' ], + $decl->value->index, + $decl->value->currentFileInfo ?? [] + ); + if ( $err ) { + $decl->parsed = true; + } + if ( $result ) { + $decl->value = $result[0]; + $decl->important = $result[1] ?? ''; + $decl->parsed = true; + } + return $decl; + } else { + return $decl; + } + } + + public function lastDeclaration() { + for ( $i = count( $this->rules ); $i > 0; $i-- ) { + $decl = $this->rules[ $i - 1 ]; + if ( $decl instanceof Less_Tree_Declaration ) { + return $this->parseValue( $decl ); + } + } + } + + private function parseValue( $toParse ) { + if ( !is_array( $toParse ) ) { + return $this->transformDeclaration( $toParse ); + } else { + $nodes = []; + foreach ( $toParse as $n ) { + $nodes[] = $this->transformDeclaration( $n ); + } + return $nodes; + } + } + + public function find( $selector, $self = null, $filter = null ) { $key = implode( ' ', $selector->_oelements ); if ( !isset( $this->lookups[$key] ) ) { @@ -342,9 +462,15 @@ public function find( $selector, $self = null ) { $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 ) ); + if ( !$filter || $filter( $rule ) ) { + $foundMixins = $rule->find( new Less_Tree_Selector( array_slice( $selector->elements, $match ) ), $self, $filter ); + for ( $i = 0; $i < count( $foundMixins ); ++$i ) { + $foundMixins[$i]["path"][] = $rule; + } + $this->lookups[$key] = array_merge( $this->lookups[$key], $foundMixins ); + } } else { - $this->lookups[$key][] = $rule; + $this->lookups[$key][] = [ "rule" => $rule, "path" => [] ]; } break; } @@ -352,13 +478,29 @@ public function find( $selector, $self = null ) { } } } + } return $this->lookups[$key]; } + private function isRulesetLikeNode( $rule ) { + // if it has nested rules, then it should be treated like a ruleset + // medias and comments do not have nested rules, but should be treated like rulesets anyway + // some directives and anonymous nodes are ruleset like, others are not + if ( $rule instanceof Less_Tree_Media || $rule instanceof Less_Tree_Ruleset ) { + return true; + } elseif ( $rule instanceof Less_Tree_Anonymous || $rule instanceof Less_Tree_AtRule ) { + return $rule->isRulesetLike(); + } + + // anything else is assumed to be a rule + return false; + } + /** - * @see Less_Tree::genCSS + * @param Less_Output $output + * @see less-2.5.3.js#Ruleset.prototype.genCSS */ public function genCSS( $output ) { if ( !$this->root ) { @@ -376,14 +518,21 @@ public function genCSS( $output ) { } $ruleNodes = []; - $rulesetNodes = []; - foreach ( $this->rules as $rule ) { - if ( $rule instanceof Less_Tree_Media || - $rule instanceof Less_Tree_Directive || - ( $this->root && $rule instanceof Less_Tree_Comment ) || - ( $rule instanceof self && $rule->rules ) - ) { - $rulesetNodes[] = $rule; + $charsetNodeIndex = 0; + $importNodeIndex = 0; + foreach ( $this->rules as $i => $rule ) { + if ( $rule instanceof Less_Tree_Comment ) { + if ( $importNodeIndex === $i ) { + $importNodeIndex++; + } + $ruleNodes[] = $rule; + } elseif ( $rule instanceof Less_Tree_AtRule && $rule->isCharset() ) { + array_splice( $ruleNodes, $charsetNodeIndex, 0, [ $rule ] ); + $charsetNodeIndex++; + $importNodeIndex++; + } elseif ( $rule instanceof Less_Tree_Import ) { + array_splice( $ruleNodes, $importNodeIndex, 0, [ $rule ] ); + $importNodeIndex++; } else { $ruleNodes[] = $rule; } @@ -392,18 +541,25 @@ public function genCSS( $output ) { // If this is the root node, we don't render // a selector, or {}. if ( !$this->root ) { - $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; - } + $sep = ',' . $tabSetStr; + // TODO: Move to Env object + // TODO: Inject Env object to toCSS() and genCSS() + $firstSelector = false; - if ( $i + 1 < $paths_len ) { - $output->add( ',' . $tabSetStr ); + foreach ( $this->paths as $i => $path ) { + $pathSubCnt = count( $path ); + if ( !$pathSubCnt ) { + continue; + } + if ( $i > 0 ) { + $output->add( $sep ); + } + $firstSelector = true; + $path[0]->genCSS( $output, $firstSelector ); + $firstSelector = false; + for ( $j = 1; $j < $pathSubCnt; $j++ ) { + $path[$j]->genCSS( $output, $firstSelector ); } } @@ -411,20 +567,22 @@ public function genCSS( $output ) { } // 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 ) ) { + foreach ( $ruleNodes as $i => $rule ) { + + if ( $i + 1 === count( $ruleNodes ) ) { Less_Environment::$lastRule = true; } + $currentLastRule = Less_Environment::$lastRule; + + if ( $this->isRulesetLikeNode( $rule ) ) { + Less_Environment::$lastRule = false; + } $rule->genCSS( $output ); - if ( !Less_Environment::$lastRule ) { + Less_Environment::$lastRule = $currentLastRule; + + if ( !Less_Environment::$lastRule && $rule->isVisible() ) { $output->add( $tabRuleStr ); } else { Less_Environment::$lastRule = false; @@ -436,31 +594,47 @@ public function genCSS( $output ) { Less_Environment::$tabLevel--; } - $firstRuleset = true; - $space = ( $this->root ? $tabRuleStr : $tabSetStr ); - for ( $i = 0; $i < $rulesetNodes_len; $i++ ) { + if ( !Less_Parser::$options['compress'] && $this->firstRoot ) { + $output->add( "\n" ); + } + } - if ( $ruleNodes_len && $firstRuleset ) { - $output->add( $space ); - } elseif ( !$firstRuleset ) { - $output->add( $space ); + public function markReferenced() { + if ( $this->selectors !== null ) { + foreach ( $this->selectors as $selector ) { + $selector->markReferenced(); } - $firstRuleset = false; - $rulesetNodes[$i]->genCSS( $output ); } - if ( !Less_Parser::$options['compress'] && $this->firstRoot ) { - $output->add( "\n" ); + if ( $this->rules ) { + foreach ( $this->rules as $rule ) { + if ( method_exists( $rule, 'markReferenced' ) ) { + $rule->markReferenced(); + } + } } } - public function markReferenced() { - if ( !$this->selectors ) { - return; + public function getIsReferenced() { + if ( $this->paths ) { + foreach ( $this->paths as $path ) { + foreach ( $path as $p ) { + if ( method_exists( $p, 'getIsReferenced' ) && $p->getIsReferenced() ) { + return true; + } + } + } } - foreach ( $this->selectors as $selector ) { - $selector->markReferenced(); + + if ( $this->selectors ) { + foreach ( $this->selectors as $selector ) { + if ( method_exists( $selector, 'getIsReferenced' ) && $selector->getIsReferenced() ) { + return true; + } + } } + + return false; } /** @@ -604,9 +778,9 @@ private function replaceParentSelector( array &$paths, $context, Less_Tree_Selec foreach ( $newSelectors as &$sel ) { $length = count( $sel ); if ( $length ) { - $paths[] = $sel; $lastSelector = $sel[$length - 1]; $sel[$length - 1] = $lastSelector->createDerived( $lastSelector->elements, $inSelector->extendList ); + $paths[] = $sel; } } diff --git a/wikimedia/less.php/lib/Less/Tree/RulesetCall.php b/wikimedia/less.php/lib/Less/Tree/RulesetCall.php deleted file mode 100644 index 9c162b8..0000000 --- a/wikimedia/less.php/lib/Less/Tree/RulesetCall.php +++ /dev/null @@ -1,26 +0,0 @@ -variable = $variable; - } - - public function accept( $visitor ) { - } - - public function compile( $env ) { - $variable = new Less_Tree_Variable( $this->variable ); - $detachedRuleset = $variable->compile( $env ); - '@phan-var Less_Tree_DetachedRuleset $detachedRuleset'; - return $detachedRuleset->callEval( $env ); - } -} diff --git a/wikimedia/less.php/lib/Less/Tree/Selector.php b/wikimedia/less.php/lib/Less/Tree/Selector.php index 4d71b03..9f75a84 100644 --- a/wikimedia/less.php/lib/Less/Tree/Selector.php +++ b/wikimedia/less.php/lib/Less/Tree/Selector.php @@ -4,21 +4,33 @@ */ class Less_Tree_Selector extends Less_Tree { + /** @var Less_Tree_Element[] */ public $elements; + /** @var Less_Tree_Condition|null */ public $condition; + /** @var Less_Tree_Extend[] */ public $extendList = []; - public $_css; + /** @var int|null */ public $index; + /** @var bool */ public $evaldCondition = false; + /** @var array|null */ public $currentFileInfo = []; + /** @var null|bool */ public $isReferenced; + /** @var null|bool */ public $mediaEmpty; + /** @var int */ public $elements_len = 0; + /** @var string[] */ public $_oelements; + /** @var array */ public $_oelements_assoc; + /** @var int */ public $_oelements_len; + /** @var bool */ public $cacheable = true; /** @@ -85,6 +97,9 @@ public function match( $other ) { return $other->_oelements_len; // return number of matched elements } + /** + * @see less-2.5.3.js#Selector.prototype.CacheElements + */ public function CacheElements() { $this->_oelements = []; $this->_oelements_assoc = []; @@ -94,21 +109,27 @@ public function CacheElements() { foreach ( $this->elements as $v ) { $css .= $v->combinator; - if ( !$v->value_is_object ) { + if ( !( $v->value instanceof Less_Tree ) ) { $css .= $v->value; continue; } - if ( - ( $v->value instanceof Less_Tree_Selector || $v->value instanceof Less_Tree_Variable ) + // @phan-suppress-next-line PhanUndeclaredProperty + if ( isset( $v->value->value ) && is_scalar( $v->value->value ) ) { + // @phan-suppress-next-line PhanUndeclaredProperty + $css .= $v->value->value; + continue; + } + + if ( ( $v->value instanceof Less_Tree_Selector || $v->value instanceof Less_Tree_Variable ) + // @phan-suppress-next-line PhanUndeclaredProperty || !is_string( $v->value->value ) ) { $this->cacheable = false; return; } - $css .= $v->value->value; } - $this->_oelements_len = preg_match_all( '/[,&#\.\w-](?:[\w-]|(?:\\\\.))*/', $css, $matches ); + $this->_oelements_len = preg_match_all( '/[,&#\*\.\w-](?:[\w-]|(?:\\\\.))*/', $css, $matches ); if ( $this->_oelements_len ) { $this->_oelements = $matches[0]; @@ -136,7 +157,7 @@ public function compile( $env ) { $extendList = []; foreach ( $this->extendList as $el ) { - $extendList[] = $el->compile( $el ); + $extendList[] = $el->compile( $env ); } $evaldCondition = false; diff --git a/wikimedia/less.php/lib/Less/Tree/UnicodeDescriptor.php b/wikimedia/less.php/lib/Less/Tree/UnicodeDescriptor.php index cc1b39e..4ca2517 100644 --- a/wikimedia/less.php/lib/Less/Tree/UnicodeDescriptor.php +++ b/wikimedia/less.php/lib/Less/Tree/UnicodeDescriptor.php @@ -4,6 +4,7 @@ */ class Less_Tree_UnicodeDescriptor extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $value; public function __construct( $value ) { diff --git a/wikimedia/less.php/lib/Less/Tree/Unit.php b/wikimedia/less.php/lib/Less/Tree/Unit.php index c4e8fcf..e5ccc2a 100644 --- a/wikimedia/less.php/lib/Less/Tree/Unit.php +++ b/wikimedia/less.php/lib/Less/Tree/Unit.php @@ -1,32 +1,42 @@ numerator = $numerator; $this->denominator = $denominator; - $this->backupUnit = $backupUnit; + sort( $this->numerator ); + sort( $this->denominator ); + $this->backupUnit = $backupUnit ?? $numerator[0] ?? null; } - public function __clone() { + public function clone() { + // we are recreating a new object to trigger logic from constructor + return new Less_Tree_Unit( $this->numerator, $this->denominator, $this->backupUnit ); } /** * @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 ) { + $strictUnits = Less_Parser::$options['strictUnits']; + + if ( count( $this->numerator ) === 1 ) { + $output->add( $this->numerator[0] ); // the ideal situation + } elseif ( !$strictUnits && $this->backupUnit ) { $output->add( $this->backupUnit ); + } elseif ( !$strictUnits && $this->denominator ) { + $output->add( $this->denominator[0] ); } } @@ -50,7 +60,7 @@ public function compare( $other ) { } public function is( $unitString ) { - return $this->toString() === $unitString; + return strtoupper( $this->toString() ) === strtoupper( $unitString ); } public function isLength() { @@ -58,6 +68,7 @@ public function isLength() { return (bool)preg_match( '/px|em|%|in|cm|mm|pc|pt|ex/', $css ); } + // TODO: Remove unused method public function isAngle() { return isset( Less_Tree_UnitConversions::$angle[$this->toCSS()] ); } @@ -92,21 +103,17 @@ public function usedUnits() { return $result; } + /** + * @see less-2.5.3.js#Unit.prototype.cancel + */ public function cancel() { $counter = []; - $backup = null; foreach ( $this->numerator as $atomicUnit ) { - if ( !$backup ) { - $backup = $atomicUnit; - } $counter[$atomicUnit] = ( $counter[$atomicUnit] ?? 0 ) + 1; } foreach ( $this->denominator as $atomicUnit ) { - if ( !$backup ) { - $backup = $atomicUnit; - } $counter[$atomicUnit] = ( $counter[$atomicUnit] ?? 0 ) - 1; } @@ -125,10 +132,6 @@ public function cancel() { } } - if ( !$this->numerator && !$this->denominator && $backup ) { - $this->backupUnit = $backup; - } - sort( $this->numerator ); sort( $this->denominator ); } diff --git a/wikimedia/less.php/lib/Less/Tree/UnitConversions.php b/wikimedia/less.php/lib/Less/Tree/UnitConversions.php index 31efe1c..e144d53 100644 --- a/wikimedia/less.php/lib/Less/Tree/UnitConversions.php +++ b/wikimedia/less.php/lib/Less/Tree/UnitConversions.php @@ -4,28 +4,32 @@ */ class Less_Tree_UnitConversions { - public static $groups = [ 'length','duration','angle' ]; + /** @var string[] */ + public static $groups = [ 'length', 'duration', 'angle' ]; + /** @var array */ public static $length = [ 'm' => 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 - ]; + 'px' => 0.00026458333333, // 0.0254 / 96, + 'pt' => 0.00035277777777777776, // 0.0254 / 72, + 'pc' => 0.004233333333333333, // 0.0254 / 72 * 12 + ]; + /** @var array */ public static $duration = [ 's' => 1, 'ms' => 0.001 - ]; + ]; + /** @var array */ public static $angle = [ - 'rad' => 0.1591549430919, // 1/(2*M_PI), - 'deg' => 0.002777778, // 1/360, - 'grad' => 0.0025, // 1/400, + 'rad' => 0.1591549430919, // 1/(2*M_PI), + 'deg' => 0.002777778, // 1/360, + 'grad' => 0.0025, // 1/400, 'turn' => 1 - ]; + ]; } diff --git a/wikimedia/less.php/lib/Less/Tree/Url.php b/wikimedia/less.php/lib/Less/Tree/Url.php index 545372f..99f203c 100644 --- a/wikimedia/less.php/lib/Less/Tree/Url.php +++ b/wikimedia/less.php/lib/Less/Tree/Url.php @@ -4,9 +4,11 @@ */ class Less_Tree_Url extends Less_Tree implements Less_Tree_HasValueProperty { - public $attrs; + /** @var Less_Tree_Variable|Less_Tree_Quoted|Less_Tree_Anonymous */ public $value; + /** @var array|null */ public $currentFileInfo; + /** @var bool|null */ public $isEvald; /** @@ -59,7 +61,7 @@ public function compile( $env ) { // Add cache buster if enabled if ( Less_Parser::$options['urlArgs'] ) { if ( !preg_match( '/^\s*data:/', $val->value ) ) { - $delimiter = strpos( $val->value, '?' ) === false ? '?' : '&'; + $delimiter = !str_contains( $val->value, '?' ) ? '?' : '&'; $urlArgs = $delimiter . Less_Parser::$options['urlArgs']; $hash_pos = strpos( $val->value, '#' ); if ( $hash_pos !== false ) { diff --git a/wikimedia/less.php/lib/Less/Tree/Value.php b/wikimedia/less.php/lib/Less/Tree/Value.php index ccd7339..6ae5767 100644 --- a/wikimedia/less.php/lib/Less/Tree/Value.php +++ b/wikimedia/less.php/lib/Less/Tree/Value.php @@ -6,12 +6,17 @@ class Less_Tree_Value extends Less_Tree implements Less_Tree_HasValueProperty { /** @var Less_Tree[] */ public $value; + /** @var int|null */ + public $index; + /** @var array */ + public $currentFileInfo; /** * @param array $value */ - public function __construct( $value ) { + public function __construct( $value, $index = null ) { $this->value = $value; + $this->index = $index; } public function accept( $visitor ) { @@ -31,14 +36,14 @@ public function compile( $env ) { } /** - * @see Less_Tree::genCSS + * @see less-2.5.3.js#Value.prototype.genCSS */ public 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[','] ); + $output->add( Less_Parser::$options['compress'] ? ',' : ', ' ); } } } diff --git a/wikimedia/less.php/lib/Less/Tree/Variable.php b/wikimedia/less.php/lib/Less/Tree/Variable.php index e88f16b..b2c765f 100644 --- a/wikimedia/less.php/lib/Less/Tree/Variable.php +++ b/wikimedia/less.php/lib/Less/Tree/Variable.php @@ -4,9 +4,13 @@ */ class Less_Tree_Variable extends Less_Tree { + /** @var string */ public $name; + /** @var int|null */ public $index; + /** @var array|null */ public $currentFileInfo; + /** @var bool */ public $evaluating = false; /** @@ -21,9 +25,10 @@ public function __construct( $name, $index = null, $currentFileInfo = null ) { /** * @param Less_Environment $env * @return Less_Tree|Less_Tree_Keyword|Less_Tree_Quoted - * @see less-2.5.3.js#Ruleset.prototype.eval + * @see less-3.13.1.js#Variable.prototype.eval */ public function compile( $env ) { + // Optimization: Less.js checks if string starts with @@, we only check if second char is @ if ( $this->name[1] === '@' ) { $v = new self( substr( $this->name, 1 ), $this->index + 1, $this->currentFileInfo ); // While some Less_Tree nodes have no 'value', we know these can't occur after a @@ -34,20 +39,46 @@ public function compile( $env ) { } if ( $this->evaluating ) { - throw new Less_Exception_Compiler( "Recursive variable definition for " . $name, null, $this->index, $this->currentFileInfo ); + throw new Less_Exception_Compiler( + "Recursive variable definition for " . $name, + null, + $this->index, + $this->currentFileInfo + ); } $this->evaluating = true; - + $variable = null; foreach ( $env->frames as $frame ) { - if ( $v = $frame->variable( $name ) ) { - $r = $v->value->compile( $env ); - $this->evaluating = false; - return $r; + /** @var Less_Tree_Ruleset $frame */ + $v = $frame->variable( $name ); + if ( $v ) { + if ( isset( $v->important ) && $v->important ) { + $importantScopeLength = count( $env->importantScope ); + $env->importantScope[ $importantScopeLength - 1 ]['important'] = $v->important; + } + // If in calc, wrap vars in a function call to cascade evaluate args first + if ( $env->inCalc ) { + $call = new Less_Tree_Call( '_SELF', [ $v->value ], $this->index, $this->currentFileInfo ); + $variable = $call->compile( $env ); + break; + } else { + $variable = $v->value->compile( $env ); + break; + } } } + if ( $variable ) { + $this->evaluating = false; + return $variable; + } - throw new Less_Exception_Compiler( "variable " . $name . " is undefined in file " . $this->currentFileInfo["filename"], null, $this->index, $this->currentFileInfo ); + throw new Less_Exception_Compiler( + "variable " . $name . " is undefined in file " . $this->currentFileInfo["filename"], + null, + $this->index, + $this->currentFileInfo + ); } } diff --git a/wikimedia/less.php/lib/Less/Tree/VariableCall.php b/wikimedia/less.php/lib/Less/Tree/VariableCall.php new file mode 100644 index 0000000..0a389d6 --- /dev/null +++ b/wikimedia/less.php/lib/Less/Tree/VariableCall.php @@ -0,0 +1,64 @@ +variable = $variable; + $this->index = $index; + $this->currentFileInfo = $currentFileInfo; + } + + public function accept( $visitor ) { + } + + public function compile( $env ) { + $detachedRuleset = ( new Less_Tree_Variable( $this->variable, $this->index, $this->currentFileInfo ) ) + ->compile( $env ); + + if ( !( $detachedRuleset instanceof Less_Tree_DetachedRuleset ) || !$detachedRuleset->ruleset ) { + // order differs from upstream to simplify the code + if ( is_array( $detachedRuleset ) ) { + $rules = new Less_Tree_Ruleset( null, $detachedRuleset ); + } elseif ( + ( $detachedRuleset instanceof Less_Tree_Ruleset + || $detachedRuleset instanceof Less_Tree_AtRule + || $detachedRuleset instanceof Less_Tree_Media + || $detachedRuleset instanceof Less_Tree_Mixin_Definition + ) && $detachedRuleset->rules + ) { + // @todo - note looks like dead code, do we need it ? + $rules = $detachedRuleset; + } elseif ( $detachedRuleset instanceof Less_Tree && is_array( $detachedRuleset->value ) ) { + // @phan-suppress-next-line PhanTypeMismatchArgument False positive + $rules = new Less_Tree_Ruleset( null, $detachedRuleset->value ); + } else { + throw new Less_Exception_Compiler( 'Could not evaluate variable call ' . $this->variable ); + } + $detachedRuleset = new Less_Tree_DetachedRuleset( $rules ); + } + if ( $detachedRuleset->ruleset ) { + return $detachedRuleset->callEval( $env ); + } + throw new Less_Exception_Compiler( 'Could not evaluate variable call ' . $this->variable ); + } +} diff --git a/wikimedia/less.php/lib/Less/Version.php b/wikimedia/less.php/lib/Less/Version.php index 4324805..8ac0eda 100644 --- a/wikimedia/less.php/lib/Less/Version.php +++ b/wikimedia/less.php/lib/Less/Version.php @@ -6,11 +6,11 @@ class Less_Version { /* Current release version of less.php */ - public const version = '4.1.0'; + public const version = '5.4.0'; /* Upstream less.js version that this release should be compatible with */ - public const less_version = '2.5.3'; + public const less_version = '3.13.1'; /* Parser cache version */ - public const cache_version = '253'; + public const cache_version = '3131-10'; } diff --git a/wikimedia/less.php/lib/Less/Visitor.php b/wikimedia/less.php/lib/Less/Visitor.php index 965ddcd..06f7256 100644 --- a/wikimedia/less.php/lib/Less/Visitor.php +++ b/wikimedia/less.php/lib/Less/Visitor.php @@ -4,7 +4,7 @@ */ class Less_Visitor { - protected $methods = []; + /** @var array */ protected $_visitFnCache = []; public function __construct() { @@ -13,16 +13,39 @@ public function __construct() { } public function visitObj( $node ) { - $funcName = 'visit' . str_replace( [ 'Less_Tree_', '_' ], '', get_class( $node ) ); + static $funcNames = []; + + if ( !$node instanceof Less_Tree ) { + return $node; + } + + // Map a class name like "Less_Tree_Foo_Bar" to method like "visitFooBar". + // + // We do this by taking the last part of the class name (instead of doing + // a find-replace from "Less_Tree" to "visit"), so that we support codemod + // tools (such as Strauss and Mozart), which may modify our code in-place + // to add a namespace or class prefix. + // "MyVendor\Something_Less_Tree_Foo_Bar" should also map to "FooBar". + // + // https://packagist.org/packages/brianhenryie/strauss + // https://packagist.org/packages/coenjacobs/mozart + $class = get_class( $node ); + $funcName = $funcNames[$class] ??= 'visit' . str_replace( [ '_', '\\' ], '', + substr( $class, strpos( $class, 'Less_Tree_' ) + 10 ) + ); + if ( isset( $this->_visitFnCache[$funcName] ) ) { $visitDeeper = true; - $this->$funcName( $node, $visitDeeper ); + $newNode = $this->$funcName( $node, $visitDeeper ); + if ( $this instanceof Less_VisitorReplacing ) { + $node = $newNode; + } - if ( $visitDeeper ) { + if ( $visitDeeper && $node instanceof Less_Tree ) { $node->accept( $this ); } - $funcName .= "Out"; + $funcName .= 'Out'; if ( isset( $this->_visitFnCache[$funcName] ) ) { $this->$funcName( $node ); } @@ -34,8 +57,11 @@ public function visitObj( $node ) { return $node; } - public function visitArray( $nodes ) { - foreach ( $nodes as $node ) { + public function visitArray( &$nodes ) { + // NOTE: The use of by-ref in a normal (non-replacing) Visitor may be surprising, + // but upstream relies on this for Less_ImportVisitor, which modifies values of + // `$importParent->rules` yet is not a replacing visitor. + foreach ( $nodes as &$node ) { $this->visitObj( $node ); } return $nodes; diff --git a/wikimedia/less.php/lib/Less/Visitor/extendFinder.php b/wikimedia/less.php/lib/Less/Visitor/extendFinder.php index 604dc91..0b9d955 100644 --- a/wikimedia/less.php/lib/Less/Visitor/extendFinder.php +++ b/wikimedia/less.php/lib/Less/Visitor/extendFinder.php @@ -4,8 +4,11 @@ */ class Less_Visitor_extendFinder extends Less_Visitor { + /** @var Less_Tree_Selector[] */ public $contexts = []; + /** @var Less_Tree_Extend[][] */ public $allExtendsStack; + /** @var bool */ public $foundExtends; public function __construct() { @@ -23,7 +26,7 @@ public function run( $root ) { return $root; } - public function visitRule( $ruleNode, &$visitDeeper ) { + public function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -80,7 +83,7 @@ public function allExtendsStackPush( $rulesetNode, $selectorPath, Less_Tree_Exte } public function visitRulesetOut( $rulesetNode ) { - if ( !is_object( $rulesetNode ) || !$rulesetNode->root ) { + if ( !$rulesetNode instanceof Less_Tree_Ruleset || !$rulesetNode->root ) { array_pop( $this->contexts ); } } @@ -94,12 +97,12 @@ public function visitMediaOut() { array_pop( $this->allExtendsStack ); } - public function visitDirective( $directiveNode ) { - $directiveNode->allExtends = []; - $this->allExtendsStack[] =& $directiveNode->allExtends; + public function visitAtRule( $atRuleNode ) { + $atRuleNode->allExtends = []; + $this->allExtendsStack[] =& $atRuleNode->allExtends; } - public function visitDirectiveOut() { + public function visitAtRuleOut() { array_pop( $this->allExtendsStack ); } } diff --git a/wikimedia/less.php/lib/Less/Visitor/import.php b/wikimedia/less.php/lib/Less/Visitor/import.php deleted file mode 100644 index 7af96eb..0000000 --- a/wikimedia/less.php/lib/Less/Visitor/import.php +++ /dev/null @@ -1,137 +0,0 @@ -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/wikimedia/less.php/lib/Less/Visitor/joinSelector.php b/wikimedia/less.php/lib/Less/Visitor/joinSelector.php index 9d85f10..320638d 100644 --- a/wikimedia/less.php/lib/Less/Visitor/joinSelector.php +++ b/wikimedia/less.php/lib/Less/Visitor/joinSelector.php @@ -4,6 +4,7 @@ */ class Less_Visitor_joinSelector extends Less_Visitor { + /** @var Less_Tree_Selector[][][] */ public $contexts = [ [] ]; /** @@ -13,7 +14,7 @@ public function run( $root ) { return $this->visitObj( $root ); } - public function visitRule( $ruleNode, &$visitDeeper ) { + public function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -59,16 +60,16 @@ public function visitRulesetOut() { public function visitMedia( $mediaNode ) { $context = end( $this->contexts ); - if ( count( $context ) === 0 || ( is_object( $context[0] ) && $context[0]->multiMedia ) ) { + if ( count( $context ) === 0 || ( $context[0] instanceof Less_Tree_Ruleset && $context[0]->multiMedia ) ) { $mediaNode->rules[0]->root = true; } } - public function visitDirective( $directiveNode ) { + public function visitAtRule( $atRuleNode ) { $context = end( $this->contexts ); - if ( $directiveNode->rules && count( $directiveNode->rules ) > 0 ) { - $directiveNode->rules[0]->root = $directiveNode->isRooted || count( $context ) === 0; + if ( $atRuleNode->rules && count( $atRuleNode->rules ) > 0 ) { + $atRuleNode->rules[0]->root = $atRuleNode->isRooted || count( $context ) === 0; } } diff --git a/wikimedia/less.php/lib/Less/Visitor/processExtends.php b/wikimedia/less.php/lib/Less/Visitor/processExtends.php index 787f506..c818112 100644 --- a/wikimedia/less.php/lib/Less/Visitor/processExtends.php +++ b/wikimedia/less.php/lib/Less/Visitor/processExtends.php @@ -4,6 +4,7 @@ */ class Less_Visitor_processExtends extends Less_Visitor { + /** @var Less_Tree_Extend[][] */ public $allExtendsStack; /** @@ -98,15 +99,17 @@ private function doExtendChaining( $extendsList, $extendsListTarget, $iterationC // may no longer be needed. $this->extendChainCount++; if ( $iterationCount > 100 ) { - try{ + try { $selectorOne = $extendsToAdd[0]->selfSelectors[0]->toCSS(); $selectorTwo = $extendsToAdd[0]->selector->toCSS(); - }catch ( Exception $e ) { + } 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 . ")" ); + 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... @@ -116,7 +119,7 @@ private function doExtendChaining( $extendsList, $extendsListTarget, $iterationC return array_merge( $extendsList, $extendsToAdd ); } - protected function visitRule( $ruleNode, &$visitDeeper ) { + protected function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -133,7 +136,7 @@ protected function visitRuleset( $rulesetNode ) { return; } - $allExtends = end( $this->allExtendsStack ); + $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 @@ -199,7 +202,12 @@ private function findMatch( $extend, $haystackSelectorPath ) { // 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[] = [ 'pathIndex' => $haystackSelectorIndex, 'index' => $hackstackElementIndex, 'matched' => 0, 'initialCombinator' => $haystackElement->combinator ]; + $potentialMatches[] = [ + 'pathIndex' => $haystackSelectorIndex, + 'index' => $hackstackElementIndex, + 'matched' => 0, + 'initialCombinator' => $haystackElement->combinator + ]; $potentialMatches_len++; } @@ -212,7 +220,9 @@ private function findMatch( $extend, $haystackSelectorPath ) { if ( $potentialMatch && $potentialMatch['matched'] === $extend->selector->elements_len ) { $potentialMatch['finished'] = true; - if ( !$extend->allowAfter && ( $hackstackElementIndex + 1 < $haystack_elements_len || $haystackSelectorIndex + 1 < $haystack_path_len ) ) { + if ( !$extend->allowAfter && + ( $hackstackElementIndex + 1 < $haystack_elements_len || $haystackSelectorIndex + 1 < $haystack_path_len ) + ) { $potentialMatch = null; } } @@ -399,7 +409,10 @@ private function extendSelector( $matches, $selectorPath, $replacementSelector ) if ( $match['pathIndex'] > $currentSelectorPathIndex && $currentSelectorPathElementIndex > 0 ) { $last_path = end( $path ); - $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) ); + $last_path->elements = array_merge( + $last_path->elements, + array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) + ); $currentSelectorPathElementIndex = 0; $currentSelectorPathIndex++; } @@ -411,9 +424,9 @@ private function extendSelector( $matches, $selectorPath, $replacementSelector ) // last parameter of array_slice is different than the last parameter of javascript's slice $match['index'] - $currentSelectorPathElementIndex ), - [ $firstElement ], - array_slice( $replacementSelector->elements, 1 ) - ); + [ $firstElement ], + array_slice( $replacementSelector->elements, 1 ) + ); if ( $currentSelectorPathIndex === $match['pathIndex'] && $matchIndex > 0 ) { $last_key = count( $path ) - 1; @@ -433,7 +446,10 @@ private function extendSelector( $matches, $selectorPath, $replacementSelector ) if ( $currentSelectorPathIndex < $selectorPath_len && $currentSelectorPathElementIndex > 0 ) { $last_path = end( $path ); - $last_path->elements = array_merge( $last_path->elements, array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) ); + $last_path->elements = array_merge( + $last_path->elements, + array_slice( $selectorPath[$currentSelectorPathIndex]->elements, $currentSelectorPathElementIndex ) + ); $currentSelectorPathIndex++; } @@ -452,12 +468,12 @@ 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 visitAtRule( $atRuleNode ) { + $newAllExtends = array_merge( $atRuleNode->allExtends, end( $this->allExtendsStack ) ); + $this->allExtendsStack[] = $this->doExtendChaining( $newAllExtends, $atRuleNode->allExtends ); } - protected function visitDirectiveOut() { + protected function visitAtRuleOut() { array_pop( $this->allExtendsStack ); } diff --git a/wikimedia/less.php/lib/Less/Visitor/toCSS.php b/wikimedia/less.php/lib/Less/Visitor/toCSS.php index 08d67dd..a64e680 100644 --- a/wikimedia/less.php/lib/Less/Visitor/toCSS.php +++ b/wikimedia/less.php/lib/Less/Visitor/toCSS.php @@ -4,6 +4,7 @@ */ class Less_Visitor_toCSS extends Less_VisitorReplacing { + /** @var bool|null */ private $charset; public function __construct() { @@ -17,11 +18,11 @@ public function run( $root ) { return $this->visitObj( $root ); } - public function visitRule( $ruleNode ) { - if ( $ruleNode->variable ) { + public function visitDeclaration( $declNode ) { + if ( $declNode->variable ) { return []; } - return $ruleNode; + return $declNode; } public function visitMixinDefinition( $mixinNode ) { @@ -52,27 +53,47 @@ public function visitMedia( $mediaNode, &$visitDeeper ) { return $mediaNode; } - public function visitDirective( $directiveNode ) { - if ( isset( $directiveNode->currentFileInfo['reference'] ) && ( !property_exists( $directiveNode, 'isReferenced' ) || !$directiveNode->isReferenced ) ) { - return []; - } - 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 + public function visitAtRule( $atRuleNode, &$visitDeeper ) { + if ( $atRuleNode->name === '@charset' ) { + if ( !$atRuleNode->getIsReferenced() ) { + return; + } if ( isset( $this->charset ) && $this->charset ) { + // NOTE: Skip debugInfo handling (not implemented) + return; + } + $this->charset = true; + } - // if( $directiveNode->debugInfo ){ - // $comment = new Less_Tree_Comment('/* ' . str_replace("\n",'',$directiveNode->toCSS())." */\n"); - // $comment->debugInfo = $directiveNode->debugInfo; - // return $this->visit($comment); - //} + if ( $atRuleNode->rules ) { + self::_mergeRules( $atRuleNode->rules[0]->rules ); + // process childs + $atRuleNode->accept( $this ); + $visitDeeper = false; - return []; + // the directive was directly referenced and therefore needs to be shown in the output + if ( $atRuleNode->getIsReferenced() ) { + return $atRuleNode; + } + + if ( !$atRuleNode->rules ) { + return; + } + if ( $this->hasVisibleChild( $atRuleNode ) ) { + // marking as referenced in case the directive is stored inside another directive + $atRuleNode->markReferenced(); + return $atRuleNode; + } + // The directive was not directly referenced and does not contain anything that + //was referenced. Therefore it must not be shown in output. + return; + } else { + if ( !$atRuleNode->getIsReferenced() ) { + return; } - $this->charset = true; } - return $directiveNode; + + return $atRuleNode; } public function checkPropertiesInRoot( $rulesetNode ) { @@ -81,7 +102,7 @@ public function checkPropertiesInRoot( $rulesetNode ) { } foreach ( $rulesetNode->rules as $ruleNode ) { - if ( $ruleNode instanceof Less_Tree_Rule && !$ruleNode->variable ) { + if ( $ruleNode instanceof Less_Tree_Declaration && !$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 ); @@ -124,7 +145,7 @@ public function visitRuleset( $rulesetNode, &$visitDeeper ) { if ( $rulesetNode->rules ) { if ( count( $rulesetNode->rules ) > 1 ) { - $this->_mergeRules( $rulesetNode->rules ); + self::_mergeRules( $rulesetNode->rules ); $this->_removeDuplicateRules( $rulesetNode->rules ); } @@ -143,6 +164,22 @@ public function visitRuleset( $rulesetNode, &$visitDeeper ) { return $rulesets; } + public function visitAnonymous( $anonymousNode ) { + if ( !$anonymousNode->getIsReferenced() ) { + return; + } + + $anonymousNode->accept( $this ); + return $anonymousNode; + } + + public function visitImport( $importNode ) { + if ( isset( $importNode->path->currentFileInfo["reference"] ) && $importNode->css ) { + return; + } + return $importNode; + } + /** * Helper function for visitiRuleset * @@ -184,14 +221,14 @@ protected function _removeDuplicateRules( &$rules ) { $ruleCache = []; for ( $i = count( $rules ) - 1; $i >= 0; $i-- ) { $rule = $rules[$i]; - if ( $rule instanceof Less_Tree_Rule || $rule instanceof Less_Tree_NameValue ) { + if ( $rule instanceof Less_Tree_Declaration || $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 ) { + if ( $ruleList instanceof Less_Tree_Declaration || $ruleList instanceof Less_Tree_NameValue ) { $ruleList = $ruleCache[$rule->name] = [ $ruleCache[$rule->name]->toCSS() ]; } @@ -206,7 +243,7 @@ protected function _removeDuplicateRules( &$rules ) { } } - protected function _mergeRules( &$rules ) { + public static function _mergeRules( &$rules ) { $groups = []; // obj($rules); @@ -215,7 +252,7 @@ protected function _mergeRules( &$rules ) { for ( $i = 0; $i < $rules_len; $i++ ) { $rule = $rules[$i]; - if ( ( $rule instanceof Less_Tree_Rule ) && $rule->merge ) { + if ( ( $rule instanceof Less_Tree_Declaration ) && $rule->merge ) { $key = $rule->name; if ( $rule->important ) { @@ -273,4 +310,23 @@ public static function toValue( $values ) { } return new Less_Tree_Value( $mapped ); } + + public function hasVisibleChild( $atRuleNode ) { + // prepare list of childs + $rule = $bodyRules = $atRuleNode->rules; + // if there is only one nested ruleset and that one has no path, then it is + //just fake ruleset that got not replaced and we need to look inside it to + //get real childs + if ( count( $bodyRules ) === 1 && ( !$bodyRules[0]->paths || count( $bodyRules[0]->paths ) === 0 ) ) { + $bodyRules = $bodyRules[0]->rules; + } + foreach ( $bodyRules as $rule ) { + if ( method_exists( $rule, 'getIsReferenced' ) && $rule->getIsReferenced() ) { + // the directive contains something that was referenced (likely by extend) + //therefore it needs to be shown in output too + return true; + } + } + return false; + } } diff --git a/wikimedia/less.php/lib/Less/VisitorReplacing.php b/wikimedia/less.php/lib/Less/VisitorReplacing.php index 484f0e0..944843b 100644 --- a/wikimedia/less.php/lib/Less/VisitorReplacing.php +++ b/wikimedia/less.php/lib/Less/VisitorReplacing.php @@ -4,31 +4,7 @@ */ class Less_VisitorReplacing extends Less_Visitor { - public function visitObj( $node ) { - $funcName = 'visit' . str_replace( [ 'Less_Tree_', '_' ], '', get_class( $node ) ); - if ( isset( $this->_visitFnCache[$funcName] ) ) { - $visitDeeper = true; - $node = $this->$funcName( $node, $visitDeeper ); - - if ( $node ) { - if ( $visitDeeper && is_object( $node ) ) { - $node->accept( $this ); - } - - $funcName .= "Out"; - if ( isset( $this->_visitFnCache[$funcName] ) ) { - $this->$funcName( $node ); - } - } - - } else { - $node->accept( $this ); - } - - return $node; - } - - public function visitArray( $nodes ) { + public function visitArray( &$nodes ) { $newNodes = []; foreach ( $nodes as $node ) { $evald = $this->visitObj( $node );