diff --git a/include/thirdparty/less.php/CHANGES.md b/include/thirdparty/less.php/CHANGES.md deleted file mode 100644 index 846d672..0000000 --- a/include/thirdparty/less.php/CHANGES.md +++ /dev/null @@ -1,187 +0,0 @@ -# Changelog - -## 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) -* Less_Parser: Faster `parseUnicodeDescriptor` and `parseEntitiesJavascript` by first-char checks. (Thiemo Kreuz) -* 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) - -## 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) - -## 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. -* Fix `url(#myid)` to be treated as absolute URL. [T331649](https://phabricator.wikimedia.org/T331688) -* 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) - -## 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) - -## v3.2.0 - -* Fix "Implicit conversion" PHP 8.1 warnings (Ayokunle Odusan) -* Fix "Creation of dynamic property" PHP 8.2 warnings (Bas Couwenberg) -* Fix "Creation of dynamic property" PHP 8.2 warnings (Rajesh Kumar) -* 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) - -## 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) - -## v3.0.0 - -* Raise PHP requirement from 7.1 to 7.2.9 (James Forrester) - -## 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) - -## 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) - -## v1.8.1 - -* Another PHP 7.3 compatibility tweak - -## v1.8.0 - -Library forked by Wikimedia, from [oyejorge/less.php](https://github.com/oyejorge/less.php). - -* Supports up to PHP 7.3 -* No longer tested against PHP 5, though it's still remains allowed in `composer.json` for HHVM compatibility -* Switched to [semantic versioning](https://semver.org/), hence version numbers now use 3 digits - -## v1.7.0.13 - -* Fix composer.json (PSR-4 was invalid) - -## v1.7.0.12 - -* set bin/lessc bit executable -* Add `gettingVariables` method to `Less_Parser` - -## 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) - -## v1.7.0.10 - -* Add indentation option -* Add `optional` modifier for `@import` -* Fix $color in Exception messages -* take relative-url into account when building the cache filename -* urlArgs should be string no array() -* fix missing on NameValue type [#269](https://github.com/oyejorge/less.php/issues/269) - -## v1.7.0.9 - -* Remove space at beginning of Version.php -* Revert require() paths in test interface diff --git a/include/thirdparty/less.php/CONTRIBUTING.md b/include/thirdparty/less.php/CONTRIBUTING.md deleted file mode 100644 index de5e072..0000000 --- a/include/thirdparty/less.php/CONTRIBUTING.md +++ /dev/null @@ -1,78 +0,0 @@ -# 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) diff --git a/include/thirdparty/less.php/Cache.php b/include/thirdparty/less.php/Cache.php index 6d89d84..5c68abb 100644 --- a/include/thirdparty/less.php/Cache.php +++ b/include/thirdparty/less.php/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 @@ class Less_Cache { 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 @@ class Less_Cache { 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 ); } @@ -180,45 +188,46 @@ class Less_Cache { } public static function SetCacheDir( $dir ) { - self::$cache_dir = $dir; - self::CheckCacheDir(); - } - - 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 ); - } - - } 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 ); - - } + self::$cache_dir = self::CheckCacheDir( $dir ); } /** - * Delete unused less.php files + * @deprecated since 5.3.0 Internal for use by Less_Cache and Less_Parser only. */ - public static function CleanCache() { - static $clean = false; + public static function CheckCacheDir( $dir = null ) { + $dir ??= self::$cache_dir; + $dir = Less_Parser::WinPath( $dir ); + $dir = rtrim( $dir, '/' ) . '/'; - if ( $clean || empty( self::$cache_dir ) ) { - return; + if ( !file_exists( $dir ) ) { + if ( !mkdir( $dir ) ) { + 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 ); } - $clean = true; + return $dir; + } + + /** + * @deprecated since 5.3.0 Called automatically. Internal for use by Less_Cache and Less_Parser only. + */ + public static function CleanCache( $dir = null ) { + $dir ??= self::$cache_dir; + if ( !$dir ) { + return; + } // 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 ]; - $files = scandir( self::$cache_dir ); + $files = scandir( $dir ); if ( !$files ) { return; } @@ -238,8 +247,8 @@ class Less_Cache { 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 @@ class Less_Cache { // 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/include/thirdparty/less.php/Environment.php b/include/thirdparty/less.php/Environment.php index 5031cb9..4f3fe8e 100644 --- a/include/thirdparty/less.php/Environment.php +++ b/include/thirdparty/less.php/Environment.php @@ -20,6 +20,15 @@ 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 = []; @@ -29,9 +38,6 @@ class Less_Environment { /** @var string[] */ public $imports = []; - /** @var array */ - public $importantScope = []; - /** * This is the equivalent of `importVisitor.onceFileDetectionMap` * as used by the dynamic `importNode.skip` function. @@ -41,19 +47,27 @@ class Less_Environment { */ public $importVisitorOnceMap = []; - public static $parensStack = 0; - + /** @var int */ public static $tabLevel = 0; + /** @var bool */ public static $lastRule = false; + /** @var array */ public static $_noSpaceCombinators; + /** @var int */ public static $mixin_stack = 0; - public $strictMath = false; + /** @var int */ + public $math = self::MATH_PARENS_DIVISION; - public $importCallback = null; + /** @var true[] */ + public $parensStack = []; + + public const MATH_ALWAYS = 0; + public const MATH_PARENS_DIVISION = 1; + public const MATH_PARENS = 2; /** * @var array @@ -61,7 +75,6 @@ class Less_Environment { public $functions = []; public function Init() { - self::$parensStack = 0; self::$tabLevel = 0; self::$lastRule = false; self::$mixin_stack = 0; @@ -101,16 +114,41 @@ class Less_Environment { $new_env = new self(); $new_env->frames = $frames; $new_env->importantScope = $this->importantScope; - $new_env->strictMath = $this->strictMath; + $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 function isMathOn() { - return $this->strictMath ? (bool)self::$parensStack : true; + public function isMathOn( $op = "" ) { + if ( !$this->mathOn ) { + return false; + } + if ( $op === '/' && $this->math !== $this::MATH_ALWAYS && !$this->parensStack ) { + return false; + } + + 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 ); } /** @@ -122,18 +160,15 @@ class Less_Environment { return !preg_match( '/^(?:[a-z-]+:|\/|#)/', $path ); } - /** - * Apply legacy 'import_callback' option. - * - * See Less_Parser::$default_options to learn more about the 'import_callback' option. - * This option is deprecated in favour of Less_Parser::SetImportDirs. - * - * @param Less_Tree_Import $importNode - * @return array{0:string,1:string|null}|null Array containing path and (optional) uri or null - */ - public function callImportCallback( Less_Tree_Import $importNode ) { - if ( is_callable( $this->importCallback ) ) { - return ( $this->importCallback )( $importNode ); + public function enterCalc() { + $this->calcStack[] = true; + $this->inCalc = true; + } + + public function exitCalc() { + array_pop( $this->calcStack ); + if ( !$this->calcStack ) { + $this->inCalc = false; } } diff --git a/include/thirdparty/less.php/Exception/Chunk.php b/include/thirdparty/less.php/Exception/Chunk.php index 5697cd6..f9e83cf 100644 --- a/include/thirdparty/less.php/Exception/Chunk.php +++ b/include/thirdparty/less.php/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; diff --git a/include/thirdparty/less.php/Exception/Parser.php b/include/thirdparty/less.php/Exception/Parser.php index 22d9d19..ff30ff8 100644 --- a/include/thirdparty/less.php/Exception/Parser.php +++ b/include/thirdparty/less.php/Exception/Parser.php @@ -19,10 +19,9 @@ class Less_Exception_Parser extends Exception { */ public $index; + /** @var string|null */ protected $input; - protected $details = []; - /** * @param string|null $message * @param Exception|null $previous Previous exception @@ -30,7 +29,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; diff --git a/include/thirdparty/less.php/FileManager.php b/include/thirdparty/less.php/FileManager.php index 4f8f342..7b3d005 100644 --- a/include/thirdparty/less.php/FileManager.php +++ b/include/thirdparty/less.php/FileManager.php @@ -8,7 +8,7 @@ class Less_FileManager { * @see less-node/FileManager.getPath https://github.com/less/less.js/blob/v2.5.3/lib/less-node/file-manager.js#L70 * @param string $filename * @param null|array $currentFileInfo - * @return null|array + * @return null|array{0:string,1:string} */ public static function getFilePath( $filename, $currentFileInfo ) { if ( !$filename ) { diff --git a/include/thirdparty/less.php/Functions.php b/include/thirdparty/less.php/Functions.php index 3e98819..390f9ef 100644 --- a/include/thirdparty/less.php/Functions.php +++ b/include/thirdparty/less.php/Functions.php @@ -6,7 +6,9 @@ */ class Less_Functions { + /** @var Less_Environment */ public $env; + /** @var array|null */ public $currentFileInfo; public function __construct( $env, ?array $currentFileInfo = null ) { @@ -105,7 +107,7 @@ class Less_Functions { * @param float $a */ public function hsva( $h, $s, $v, $a ) { - $h = ( ( self::_number( $h ) % 360 ) / 360 ) * 360; + $h = ( ( (int)self::_number( $h ) % 360 ) / 360 ) * 360; $s = self::_number( $s ); $v = self::_number( $v ); $a = self::_number( $a ); @@ -139,7 +141,9 @@ class Less_Functions { 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(); @@ -148,7 +152,9 @@ class Less_Functions { 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(); @@ -157,7 +163,9 @@ class Less_Functions { 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(); @@ -166,7 +174,9 @@ class Less_Functions { 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(); @@ -175,7 +185,9 @@ class Less_Functions { 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(); @@ -184,7 +196,9 @@ class Less_Functions { 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(); @@ -193,7 +207,9 @@ class Less_Functions { 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] ); @@ -201,7 +217,9 @@ class Less_Functions { 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] ); @@ -209,7 +227,9 @@ class Less_Functions { 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] ); @@ -217,7 +237,9 @@ class Less_Functions { 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(); @@ -226,7 +248,9 @@ class Less_Functions { 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( $color->luma() * $color->alpha * 100, '%' ); @@ -234,13 +258,15 @@ class Less_Functions { 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( $luminance * $color->alpha * 100, '%' ); } @@ -253,10 +279,14 @@ class Less_Functions { } 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(); @@ -273,13 +303,18 @@ class Less_Functions { /** * @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, $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(); @@ -297,10 +332,14 @@ class Less_Functions { 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(); @@ -318,10 +357,14 @@ class Less_Functions { 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(); @@ -337,10 +380,14 @@ class Less_Functions { 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(); @@ -357,10 +404,14 @@ class Less_Functions { 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(); @@ -377,10 +428,14 @@ class Less_Functions { 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(); @@ -392,10 +447,14 @@ class Less_Functions { 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(); @@ -419,18 +478,24 @@ class Less_Functions { public function mix( $color1 = null, $color2 = null, $weight = null ) { if ( !$color1 instanceof Less_Tree_Color ) { $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?)' : '' ) ); + 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 ) { $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?)' : '' ) ); + 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 ) { $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?)' : '' ) ); + 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; @@ -471,10 +536,14 @@ class Less_Functions { } 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! @@ -504,7 +573,18 @@ class Less_Functions { } 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 ) ); } @@ -562,7 +642,9 @@ class Less_Functions { 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 ) { @@ -712,7 +794,7 @@ class Less_Functions { $unit = $currentUnified->unit->toString(); } - if ( $unit !== '' && !$unitStatic || $unit !== '' && $order[0]->unify()->unit->toString() === "" ) { + if ( ( $unit !== '' && !$unitStatic ) || ( $unit !== '' && $order[0]->unify()->unit->toString() === "" ) ) { $unitStatic = $unit; } @@ -769,7 +851,9 @@ class Less_Functions { 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() ); @@ -856,27 +940,42 @@ class Less_Functions { 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; - - } elseif ( (int)$index === 0 ) { - return $values; - } - - 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 ]; } + /** + * @see less-3.13.1.js#_SELF + */ + public function _self( $args ) { + return $args; + } + + /** + * @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 ) ) ); } /** @@ -974,7 +1073,9 @@ class Less_Functions { $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 = '' . @@ -991,7 +1092,9 @@ class Less_Functions { $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 ) { @@ -1002,12 +1105,24 @@ class Less_Functions { $positionValue = '100%'; } $alpha = $color->alpha; - $returner .= ''; + $returner .= ''; } $returner .= ''; - $revert = [ '%21' => '!', '%2A' => '*', '%27' => "'", '%26' => '&', '%2C' => ',', '%40' => '@', '%2B' => '+', '%24' => '$', '%28' => '(', '%29' => ')' ]; + $revert = [ + '%21' => '!', + '%2A' => '*', + '%27' => "'", + '%26' => '&', + '%2C' => ',', + '%40' => '@', + '%2B' => '+', + '%24' => '$', + '%28' => '(', + '%29' => ')' + ]; $returner = strtr( rawurlencode( $returner ), $revert ); $returner = "data:image/svg+xml," . $returner; @@ -1091,10 +1206,14 @@ class Less_Functions { 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 ); @@ -1106,10 +1225,14 @@ class Less_Functions { 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 ); @@ -1121,10 +1244,14 @@ class Less_Functions { 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 ); @@ -1139,10 +1266,14 @@ class Less_Functions { 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 ); @@ -1161,10 +1292,14 @@ class Less_Functions { 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 ); @@ -1176,10 +1311,14 @@ class Less_Functions { 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 ); @@ -1191,10 +1330,14 @@ class Less_Functions { 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 ); @@ -1206,10 +1349,14 @@ class Less_Functions { 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 ); @@ -1222,10 +1369,14 @@ class Less_Functions { 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 ); diff --git a/include/thirdparty/less.php/ImportVisitor.php b/include/thirdparty/less.php/ImportVisitor.php index ec42c63..4eab0aa 100644 --- a/include/thirdparty/less.php/ImportVisitor.php +++ b/include/thirdparty/less.php/ImportVisitor.php @@ -1,12 +1,19 @@ */ public $variableImports = []; + /** @var array */ public $recursionDetector = []; + /** @var int */ public $_currentDepth = 0; + /** @var mixed */ public $importItem; public function __construct( $env ) { @@ -101,16 +108,8 @@ class Less_ImportVisitor extends Less_Visitor { $path = preg_match( '/(\.[a-z]*$)|([\?;].*)$/', $path ) ? $path : $path . '.less'; } - $path_and_uri = $env->callImportCallback( $importNode ); - if ( !$path_and_uri ) { - $path_and_uri = Less_FileManager::getFilePath( $path, $importNode->currentFileInfo ); - } - if ( $path_and_uri ) { - [ $full_path, $uri ] = $path_and_uri; - } else { - $full_path = $uri = $importNode->getPath(); - } - '@phan-var string $full_path'; + [ $fullPath, $uri ] = + Less_FileManager::getFilePath( $path, $importNode->currentFileInfo ) ?? [ $path, $path ]; // @see less-2.5.3.js#ImportManager.prototype.push/loadFileCallback @@ -125,22 +124,22 @@ class Less_ImportVisitor extends Less_Visitor { $e = null; try { if ( $importNode->options['inline'] ) { - if ( !file_exists( $full_path ) ) { + if ( !file_exists( $fullPath ) ) { throw new Less_Exception_Parser( - sprintf( 'File `%s` not found.', $full_path ), + sprintf( 'File `%s` not found.', $fullPath ), null, $importNode->index, $importNode->currentFileInfo ); } - $root = file_get_contents( $full_path ); + $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( $full_path, $uri, true ); + $root = $parser->parseFile( $fullPath, $uri, true ); } } catch ( Less_Exception_Parser $err ) { $e = $err; @@ -151,7 +150,7 @@ class Less_ImportVisitor extends Less_Visitor { if ( $importNode->options['optional'] && $e ) { $e = null; $root = new Less_Tree_Ruleset( null, [] ); - $full_path = null; + $fullPath = null; } // @see less-2.5.3.js#ImportVisitor.prototype.onImported @@ -165,7 +164,7 @@ class Less_ImportVisitor extends Less_Visitor { throw $e; } - $duplicateImport = isset( $this->recursionDetector[$full_path] ); + $duplicateImport = isset( $this->recursionDetector[$fullPath] ); if ( !$env->importMultiple ) { if ( $duplicateImport ) { @@ -178,16 +177,16 @@ class Less_ImportVisitor extends Less_Visitor { } } - if ( !$full_path && $importNode->options['optional'] ) { + if ( !$fullPath && $importNode->options['optional'] ) { $importNode->doSkip = true; } if ( $root ) { $importNode->root = $root; - $importNode->importedFilename = $full_path; + $importNode->importedFilename = $fullPath; - if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $full_path ) { - $this->recursionDetector[$full_path] = true; + if ( !$inlineCSS && ( $env->importMultiple || !$duplicateImport ) && $fullPath ) { + $this->recursionDetector[$fullPath] = true; $oldContext = $this->env; $this->env = $env; $this->visitObj( $root ); @@ -222,19 +221,19 @@ class Less_ImportVisitor extends Less_Visitor { } } - public function visitRule( $ruleNode, $visitDeeper ) { + public function visitDeclaration( $declNode, $visitDeeper ) { // TODO: We might need upstream's `if (… DetachedRuleset) { this.context.frames.unshift(ruleNode); }` $visitDeeper = false; } - // TODO: We might need upstream's visitRuleOut() + // TODO: Implement less-3.13.1.js#ImportVisitor.prototype.visitDeclarationOut // if (… DetachedRuleset) { this.context.frames.shift(); } - public function visitDirective( $directiveNode, $visitArgs ) { - array_unshift( $this->env->frames, $directiveNode ); + public function visitAtRule( $atRuleNode, $visitArgs ) { + array_unshift( $this->env->frames, $atRuleNode ); } - public function visitDirectiveOut( $directiveNode ) { + public function visitAtRuleOut( $atRuleNode ) { array_shift( $this->env->frames ); } diff --git a/include/thirdparty/less.php/Less.php.combine b/include/thirdparty/less.php/Less.php.combine deleted file mode 100644 index d63cc78..0000000 --- a/include/thirdparty/less.php/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/include/thirdparty/less.php/Output/Mapped.php b/include/thirdparty/less.php/Output/Mapped.php index 7ae26a0..1fe8e87 100644 --- a/include/thirdparty/less.php/Output/Mapped.php +++ b/include/thirdparty/less.php/Output/Mapped.php @@ -63,8 +63,7 @@ class Less_Output_Mapped extends Less_Output { $sourceLines = []; $sourceColumns = ' '; - if ( $fileInfo ) { - + if ( isset( $fileInfo['currentUri'] ) ) { $url = $fileInfo['currentUri']; if ( isset( $this->contentsMap[$url] ) ) { diff --git a/include/thirdparty/less.php/Parser.php b/include/thirdparty/less.php/Parser.php index ca372f6..008ddfb 100644 --- a/include/thirdparty/less.php/Parser.php +++ b/include/thirdparty/less.php/Parser.php @@ -10,68 +10,71 @@ 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' => [], - - // Override how imported file names are resolved. + // whether to compress + 'compress' => false, + // whether units need to evaluate correctly + 'strictUnits' => false, + // How to process math // - // This legacy calllback exposes internal objects and their implementation - // details and is therefore deprecated. Use Less_Parser::SetImportDirs instead - // to override the resolution of imported file names. + // always - eagerly try to solve all operations + // parens-division - require parens for division "/" + // parens | strict - require parens for all operations // - // Example: - // - // $parser = new Less_Parser( [ - // 'import_callback' => function ( $importNode ) { - // $path = $importNode->getPath(); - // if ( $path === 'special.less' ) { - // return [ $mySpecialFilePath, null ]; - // } - // } - // ] ); - // - // @since 1.5.1 - // @deprecated since 4.3.0 - // @see Less_Environment::callImportCallback - // @see Less_Parser::SetImportDirs - // - 'import_callback' => null, - 'cache_dir' => null, - 'cache_method' => 'serialize', // false, 'serialize', 'callback'; - 'cache_callback_get' => null, - 'cache_callback_set' => null, + // 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, - 'sourceMap' => false, // whether to output a source map - 'sourceMapBasepath' => null, - 'sourceMapWriteTo' => null, - 'sourceMapURL' => null, + 'import_dirs' => [], - 'indentation' => ' ', + /** + * 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, - 'plugins' => [], - 'functions' => [], + // 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,relativeUrls:bool,urlArgs:string,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 = []; - /** @var Less_Environment */ - private static $envCompat; - - 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 string for remember exists value of mbstring.internal_encoding */ + private $mb_internal_encoding = ''; + /** @var bool */ private $autoCommentAbsorb = true; /** * @var array @@ -83,6 +86,7 @@ class Less_Parser { */ private $env; + /** @var Less_Tree[] */ protected $rules = []; /** @@ -91,8 +95,10 @@ class Less_Parser { */ private $cachedEvaldRules; + /** @var bool */ public static $has_extends = false; + /** @var int */ public static $next_id = 0; /** @@ -110,7 +116,6 @@ class Less_Parser { // which will then be passed around by reference. if ( $env instanceof Less_Environment ) { $this->env = $env; - self::$envCompat = $this->env; } else { $this->Reset( $env ); } @@ -122,6 +127,7 @@ class Less_Parser { $this->mb_internal_encoding = ini_get( 'mbstring.internal_encoding' ); @ini_set( 'mbstring.internal_encoding', 'ascii' ); } + Less_Tree::$parse = $this; } /** @@ -134,7 +140,6 @@ class Less_Parser { self::$contentsMap = []; $this->env = new Less_Environment(); - self::$envCompat = $this->env; // set new options $this->SetOptions( self::$default_options ); @@ -147,7 +152,6 @@ class Less_Parser { /** * Set one or more compiler options - * options: import_dirs, cache_dir, cache_method */ public function SetOptions( $options ) { foreach ( $options as $option => $value ) { @@ -161,24 +165,34 @@ class Less_Parser { public function SetOption( $option, $value ) { switch ( $option ) { case 'strictMath': - $this->env->strictMath = (bool)$value; - self::$options[$option] = $value; + 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 ); return; - case 'import_callback': - $this->env->importCallback = $value; - return; - case 'cache_dir': if ( is_string( $value ) ) { - Less_Cache::SetCacheDir( $value ); - Less_Cache::CheckCacheDir(); + $value = Less_Cache::CheckCacheDir( $value ); } - return; + break; + case 'functions': foreach ( $value as $key => $function ) { $this->registerFunction( $key, $function ); @@ -278,6 +292,7 @@ class Less_Parser { $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 ); } @@ -286,36 +301,18 @@ class Less_Parser { } /** - * 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, - ]; - $rules = $this->cachedEvaldRules ?? $this->rules; - foreach ( $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 ) { + 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; @@ -325,7 +322,9 @@ class Less_Parser { $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 ); } @@ -339,7 +338,8 @@ class Less_Parser { * 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 ) ) { @@ -349,18 +349,33 @@ class Less_Parser { 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: $values = []; foreach ( $var->value as $sub_value ) { $values[] = $this->getVariableValue( $sub_value ); } - return implode( ' ', $values ); + return count( $values ) === 1 ? $values[0] : $values; case Less_Tree_Quoted::class: return $var->quote . $var->value . $var->quote; case Less_Tree_Dimension::class: @@ -567,26 +582,11 @@ class Less_Parser { } /** - * @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 ); } /** @@ -608,11 +608,11 @@ class Less_Parser { * } * } * - * - * @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'] = []; @@ -649,33 +649,26 @@ class Less_Parser { * @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; } } } @@ -686,23 +679,18 @@ class Less_Parser { 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'] ); } } @@ -712,7 +700,7 @@ class Less_Parser { /** * @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 ); @@ -733,16 +721,14 @@ class Less_Parser { /** * @internal since 4.3.0 No longer a public API. */ - public function UnsetInput() { + private function unsetInput() { // Free up some memory - $this->input = $this->pos = $this->input_len = $this->furthest = null; + $this->input = ''; + $this->pos = $this->input_len = $this->furthest = 0; $this->saveStack = []; } - /** - * @internal since 4.3.0 Use Less_Cache instead. - */ - public function CacheFile( $file_path ) { + private function cacheFile( $file_path ) { if ( $file_path && $this->CacheEnabled() ) { $env = get_object_vars( $this->env ); @@ -755,18 +741,10 @@ class Less_Parser { $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'; } } - /** - * @deprecated since 4.3.0 Use $parser->getParsedFiles() instead. - * @return string[] - */ - public static function AllParsedFiles() { - return self::$envCompat->imports; - } - /** * @since 4.3.0 * @return string[] @@ -778,7 +756,7 @@ class Less_Parser { /** * @internal since 4.3.0 No longer a public API. */ - public function save() { + private function save() { $this->saveStack[] = $this->pos; } @@ -849,6 +827,153 @@ class Less_Parser { } } + /** + * @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; + } + /** * Same as match(), but don't change the state of the parser, * just return the match. @@ -894,8 +1019,7 @@ class Less_Parser { if ( $nextStarSlash !== false ) { $comment = [ 'index' => $this->pos, - 'text' => substr( $this->input, $this->pos, $nextStarSlash + 2 - - $this->pos ), + 'text' => substr( $this->input, $this->pos, $nextStarSlash + 2 - $this->pos ), 'isLineComment' => false, ]; $this->pos += strlen( $comment['text'] ) - 1; @@ -922,18 +1046,15 @@ class Less_Parser { * * @param string $tok * @param string|null $msg - * @see less-2.5.3.js#Parser.expect + * @return string|array|never + * @see less-3.13.1.js#Parser.expect */ private function expect( $tok, $msg = null ) { - if ( $tok[0] === '/' ) { - $result = $this->matchReg( $tok ); - } else { - $result = $this->$tok(); - } - if ( $result !== null ) { + $result = $this->matchReg( $tok ); + if ( $result ) { return $result; } - $this->Error( $msg ? "Expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" : $msg ); + $this->Error( $msg ?? "expected '" . $tok . "' got '" . $this->input[$this->pos] . "'" ); } /** @@ -950,12 +1071,67 @@ class Less_Parser { } } + /** + * @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: // @@ -969,9 +1145,9 @@ class Less_Parser { // 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'], [...]) // ]) // @@ -988,7 +1164,7 @@ class Less_Parser { // 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 '}' // @@ -1026,12 +1202,21 @@ class Less_Parser { $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->parseRulesetCall() - ?? $this->parseDirective(); + ?? $this->parseMixinCall( false, false ) + ?? $this->parseVariableCall() + ?? $this->parseAtRule(); if ( $node ) { $root[] = $node; @@ -1063,58 +1248,48 @@ class Less_Parser { } } + /** + * @see less-3.13.1.js#parsers.entities.mixinLookup + */ + private function parseEntitiesMixinLookup() { + return $this->parseMixinCall( true, true ); + } + /** * A string, which supports escaping " and ' * * "milky way" 'he\'s the one!' * * @return Less_Tree_Quoted|null - * @see less-2.5.3.js#entities.quoted + * @see less-3.13.1.js#entities.quoted */ - private function parseEntitiesQuoted() { - // Optimization: Determine match potential without save()/restore() overhead + private function parseEntitiesQuoted( $forceEscaped = false ) { // Optimization: Inline matchChar() here, with its skipWhitespace(1) call below - $startChar = $this->input[$this->pos] ?? null; - $isEscaped = $startChar === '~'; - if ( !$isEscaped && $startChar !== "'" && $startChar !== '"' ) { + $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 + ); } /** @@ -1123,16 +1298,13 @@ class Less_Parser { * 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 ) { - $color = Less_Tree_Color::fromKeyword( $k ); - if ( $color ) { - return $color; - } - return new Less_Tree_Keyword( $k ); + return Less_Tree_Color::fromKeyword( $k ) ?? new Less_Tree_Keyword( $k ); } } @@ -1252,14 +1424,22 @@ class Less_Parser { return; } - $value = $this->parseEntitiesQuoted() ?? $this->parseEntitiesVariable() ?? $this->matchReg( '/\\Gdata\:.*?[^\)]+/' ) ?? $this->matchReg( '/\\G(?:(?:\\\\[\(\)\'"])|[^\(\)\'"])+/' ) ?? null; + $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 ); } @@ -1274,17 +1454,32 @@ class Less_Parser { * We use a different parser for variable definitions, * see `parsers.variable`. * - * @return Less_Tree_Variable|null - * @see less-2.5.3.js#parsers.entities.variable + * @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; + $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(); } /** @@ -1303,6 +1498,34 @@ class Less_Parser { } } + /** + * 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 ); + } + } + } + /** * A Hexadecimal color * @@ -1316,7 +1539,7 @@ class Less_Parser { 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, null, $rgb[0] ); + return new Less_Tree_Color( $rgb[1], 1, $rgb[0] ); } } } @@ -1329,7 +1552,7 @@ class Less_Parser { * @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 ) { @@ -1371,7 +1594,7 @@ class Less_Parser { * `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 @@ -1398,17 +1621,16 @@ class Less_Parser { $js = $this->matchReg( '/\\G[^`]*`/' ); if ( $js ) { $this->forget(); - return new Less_Tree_JavaScript( substr( $js, 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*:/' ); @@ -1418,18 +1640,46 @@ class Less_Parser { } } + // 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; + // color: @fink[@color]; // - // @fink(); - // - // @see less-2.5.3.js#parsers.rulesetCall - private function parseRulesetCall() { - if ( $this->peekChar( '@' ) ) { - $name = $this->matchReg( '/\\G(@[\w-]+)\s*\(\s*\)\s*;/' ); - if ( $name ) { - 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 ); } } @@ -1481,45 +1731,82 @@ class Less_Parser { // 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']; + $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; @@ -1534,7 +1821,7 @@ class Less_Parser { $c = $this->matchChar( '>' ); } - return $elements; + return $elements ?: null; } /** @@ -1571,7 +1858,10 @@ class Less_Parser { } break; } - $arg = $this->parseEntitiesVariable() ?? $this->parseEntitiesLiteral() ?? $this->parseEntitiesKeyword(); + $arg = $this->parseEntitiesVariable() + ?? $this->parseEntitiesProperty() + ?? $this->parseEntitiesLiteral() + ?? $this->parseEntitiesKeyword(); } if ( !$arg ) { @@ -1594,7 +1884,7 @@ class Less_Parser { $val = $arg; } - if ( $val instanceof Less_Tree_Variable ) { + if ( $val instanceof Less_Tree_Variable || $val instanceof Less_Tree_Property ) { if ( $this->matchChar( ':' ) ) { if ( $expressions ) { @@ -1675,6 +1965,54 @@ class Less_Parser { 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 // @@ -1727,7 +2065,7 @@ class Less_Parser { $this->commentStore = []; if ( $this->matchStr( 'when' ) ) { // Guard - $cond = $this->expect( 'parseConditions', 'Expected conditions' ); + $cond = $this->parseConditions() ?? $this->Error( 'Expected conditions' ); } $ruleset = $this->parseBlock(); @@ -1752,13 +2090,15 @@ class Less_Parser { $this->parseEntitiesLiteral() ?? $this->parseEntitiesVariable() ?? $this->parseEntitiesUrl() ?? + $this->parseEntitiesProperty() ?? $this->parseEntitiesCall() ?? $this->parseEntitiesKeyword() ?? + $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. // @@ -1778,8 +2118,8 @@ class Less_Parser { } $value = $this->matchReg( '/\\G[0-9]+/' ); - if ( !$value ) { - $value = $this->expect( 'parseEntitiesVariable', 'Could not parse alpha' ); + if ( $value === null ) { + $value = $this->parseEntitiesVariable() ?? $this->Error( 'Could not parse alpha' ); } $this->expectChar( ')' ); @@ -1911,7 +2251,7 @@ class Less_Parser { // 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 ) { @@ -1923,7 +2263,9 @@ class Less_Parser { if ( $this->pos < $this->input_len ) { $c = $this->input[ $this->pos ]; } - $elements[] = $e; + if ( $e !== null ) { + $elements[] = $e; + } $e = null; } @@ -2010,7 +2352,12 @@ class Less_Parser { $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 ) { @@ -2075,14 +2422,14 @@ class Less_Parser { $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; @@ -2096,60 +2443,180 @@ class Less_Parser { 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 && strpos( $name[0]->value, '--' ) === 0 ) { + $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 && $this->parseEnd() ) { - $this->forget(); - return new Less_Tree_Rule( $name, $value, $important, $merge, $startOfRule, $this->env->currentFileInfo ); - } else { - $this->restore(); - if ( $value && !$tryAnonymous ) { - return $this->parseRule( true ); + if ( $value ) { + $important = $this->parseImportant(); + } elseif ( $isVariable ) { + $value = $this->parsePermissiveValue(); } } + if ( $value && ( $this->parseEnd() || $hasDR ) ) { + $this->forget(); + return new Less_Tree_Declaration( $name, $value, $important, $merge, $index, $this->env->currentFileInfo ); + } else { + $this->restore(); + } } else { - $this->forget(); + $this->restore(); } } + /** + * @see less-3.13.1.js#parsers.anonymousValue + */ private function parseAnonymousValue() { - $match = $this->matchReg( '/\\G([^@+\/\'"*`(;{}-]*);/' ); + $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"; // @@ -2233,7 +2700,7 @@ class Less_Parser { $e = $this->parseValue(); 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 ); @@ -2296,12 +2763,13 @@ class Less_Parser { } /** - * A CSS Directive like `@charset "utf-8";` + * A CSS AtRule like `@charset "utf-8";` * - * @return Less_Tree_Import|Less_Tree_Media|Less_Tree_Directive|null - * @see less-2.5.3.js#parsers.directive + * @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 parseDirective() { + private function parseAtRule() { if ( !$this->peekChar( '@' ) ) { return; } @@ -2380,6 +2848,10 @@ class Less_Parser { $hasUnknown = true; $isRooted = false; break; + default: + // TODO: port other parts of https://github.com/less/less.js/commit/e3c13121dfdca48ba8fe26335cc12dd3f7948676 + $hasUnknown = true; + break; } $this->commentStore = []; @@ -2387,18 +2859,22 @@ class Less_Parser { 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 ) ); + $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; } } @@ -2408,7 +2884,7 @@ class Less_Parser { 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(); @@ -2424,6 +2900,7 @@ class Less_Parser { // private function parseValue() { $expressions = []; + $index = $this->pos; do { $e = $this->parseExpression(); @@ -2436,7 +2913,7 @@ class Less_Parser { } while ( $e ); if ( $expressions ) { - return new Less_Tree_Value( $expressions ); + return new Less_Tree_Value( $expressions, $index ); } } @@ -2452,7 +2929,9 @@ class Less_Parser { $a = $this->parseAddition(); if ( $a && $this->matchChar( ')' ) ) { $this->forget(); - return new Less_Tree_Expression( [ $a ], true ); + $e = new Less_Tree_Expression( [ $a ] ); + $e->parens = true; + return $e; } } $this->restore(); @@ -2462,12 +2941,12 @@ class Less_Parser { * Parses multiplication operation * * @return Less_Tree_Operation|null + * @see less-3.13.1.js#parsers.multiplication */ private function parseMultiplication() { $return = $m = $this->parseOperand(); if ( $return ) { while ( true ) { - $isSpaced = $this->isWhitespace( -1 ); if ( $this->peekReg( '/\\G\/[*\/]/' ) ) { @@ -2475,13 +2954,10 @@ class Less_Parser { } $this->save(); - $op = $this->matchChar( '/' ); + $op = $this->matchChar( '/' ) ?? $this->matchChar( '*' ) ?? $this->matchStr( './' ); if ( !$op ) { - $op = $this->matchChar( '*' ); - if ( !$op ) { - $this->forget(); - break; - } + $this->forget(); + break; } $a = $this->parseOperand(); @@ -2572,12 +3048,20 @@ class Less_Parser { $negate = true; } $this->expectChar( '(' ); - $a = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted(); + /** @see less-3.13.1.js parsers.atomicCondition */ + $a = $this->parseAddition() + ?? $this->parseEntitiesKeyword() + ?? $this->parseEntitiesQuoted() + ?? $this->parseEntitiesMixinLookup(); if ( $a ) { $op = $this->matchReg( '/\\G(?:>=|<=|=<|[<=>])/' ); if ( $op ) { - $b = $this->parseAddition() ?? $this->parseEntitiesKeyword() ?? $this->parseEntitiesQuoted(); + /** @see less-3.13.1.js parsers.atomicCondition */ + $b = $this->parseAddition() + ?? $this->parseEntitiesKeyword() + ?? $this->parseEntitiesQuoted() + ?? $this->parseEntitiesMixinLookup(); if ( $b ) { $c = new Less_Tree_Condition( $op, $a, $b, $index, $negate ); } else { @@ -2596,6 +3080,8 @@ class Less_Parser { /** * 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; @@ -2604,11 +3090,20 @@ class Less_Parser { return; } $char = $this->input[$offset]; - if ( $char === '@' || $char === '(' ) { + + 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; @@ -2623,9 +3118,11 @@ class Less_Parser { * 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(); @@ -2634,13 +3131,16 @@ class Less_Parser { 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 ( $delim ) { - $entities[] = new Less_Tree_Anonymous( $delim ); + $entities[] = new Less_Tree_Anonymous( $delim, $index ); } } } @@ -2669,7 +3169,7 @@ class Less_Parser { * eg: 'color', 'width', 'height', etc * * @return array - * @see less-2.5.3.js#parsers.ruleProperty + * @see less-3.13.1.js#parsers.ruleProperty */ private function parseRuleProperty() { $name = []; @@ -2688,7 +3188,8 @@ class Less_Parser { // 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*:/', $index, $name ) ) { $this->forget(); @@ -2700,11 +3201,13 @@ class Less_Parser { 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 { @@ -2726,25 +3229,20 @@ class Less_Parser { $s = ''; foreach ( $vars as $name => $value ) { + if ( strval( $value ) === "" ) { + $value = '~""'; + } $s .= ( ( $name[0] === '@' ) ? '' : '@' ) . $name . ': ' . $value . ( ( substr( $value, -1 ) === ';' ) ? '' : ';' ); } return $s; } - /** - * Some versions of PHP have trouble with method_exists($a,$b) if $a is not an object - * - * @param 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 ); @@ -2781,7 +3279,6 @@ class Less_Parser { } 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/include/thirdparty/less.php/README.md b/include/thirdparty/less.php/README.md deleted file mode 100644 index 9ec38ec..0000000 --- a/include/thirdparty/less.php/README.md +++ /dev/null @@ -1,83 +0,0 @@ -[![Packagist](https://img.shields.io/packagist/v/wikimedia/less.php.svg?style=flat)](https://packagist.org/packages/wikimedia/less.php) - -Less.php -======== - -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. - -* [API § Caching](./API.md#caching), Less.php includes a file-based cache. -* [API § Source maps](./API.md#source-maps), Less.php supports v3 sourcemaps. -* [API § Command line](./API.md#command-line), the `lessc` command includes a watch mode. - -## Installation - -You can install the library with Composer or standalone. - -If you have [Composer](https://getcomposer.org/download/) installed: - -1. Run `composer require wikimedia/less.php` -2. Use `Less_Parser` in your code. - -Or standalone: - -1. [Download Less.php](https://gerrit.wikimedia.org/g/mediawiki/libs/less.php/+archive/HEAD.tar.gz) and upload the PHP files to your server. -2. Include the library: - ```php - require_once '[path to]/less.php/lib/Less/Autoloader.php'; - Less_Autoloader::register(); - ``` -3. Use `Less_Parser` in your code. - -## Security - -The LESS processor language is powerful and includes features that may read or embed arbitrary files that the web server has access to, and features that may be computationally exensive if misused. - -In general you should treat LESS files as being in the same trust domain as other server-side executables, such as PHP code. In particular, it is not recommended to allow people that use your web service to provide arbitrary LESS code for server-side processing. - -_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://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)) - -## Integrations - -Less.php has been integrated with various other projects. - -#### Transitioning from Leafo/lessphp - -If you're looking to transition from the [Leafo/lessphp](https://github.com/leafo/lessphp) library, use the `lessc.inc.php` adapter file that comes with Less.php. - -This allows Less.php to be a drop-in replacement for Leafo/lessphp. - -[Download Less.php](https://gerrit.wikimedia.org/g/mediawiki/libs/less.php/+archive/HEAD.tar.gz), unzip the files into your project, and include its `lessc.inc.php` instead. - -Note: The `setPreserveComments` option is ignored. Less.php already preserves CSS block comments by default, and removes LESS inline comments. - -#### Drupal - -Less.php can be used with [Drupal's less module](https://drupal.org/project/less) via the `lessc.inc.php` adapter. [Download Less.php](https://gerrit.wikimedia.org/g/mediawiki/libs/less.php/+archive/HEAD.tar.gz) and unzip it so that `lessc.inc.php` is located at `sites/all/libraries/lessphp/lessc.inc.php`, then install the Drupal less module as usual. - -#### WordPress - -* [wp_enqueue_less](https://github.com/Ed-ITSolutions/wp_enqueue_less) is a Composer package for use in WordPress themes and plugins. It provides a `wp_enqueue_less()` function to automatically manage caching and compilation on-demand, and loads the compressed CSS on the page. -* [JBST framework](https://github.com/bassjobsen/jamedo-bootstrap-start-theme) bundles a copy of Less.php. -* The [lessphp plugin](https://wordpress.org/plugins/lessphp/) bundles a copy of Less.php for use in other plugins or themes. This dependency can also be combined with the [TGM Library](http://tgmpluginactivation.com/). - -## Credits - -Less.php was originally ported to PHP in 2011 by [Matt Agar](https://github.com/agar) and then updated by [Martin Jantošovič](https://github.com/Mordred) in 2012. From 2013 to 2017, [Josh Schmidt](https://github.com/oyejorge) lead development of the library. Since 2019, the library is maintained by Wikimedia Foundation. - -## Contribute - -* Issue tracker: https://phabricator.wikimedia.org/tag/less.php/ -* Source code: https://gerrit.wikimedia.org/g/mediawiki/libs/less.php ([Get started with Gerrit](https://www.mediawiki.org/wiki/Gerrit/Tutorial/tl;dr)) diff --git a/include/thirdparty/less.php/SourceMap/Generator.php b/include/thirdparty/less.php/SourceMap/Generator.php index 5ecd730..19e3d9c 100644 --- a/include/thirdparty/less.php/SourceMap/Generator.php +++ b/include/thirdparty/less.php/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 = []; /** @@ -117,10 +118,10 @@ class Less_SourceMap_Generator extends Less_Configurable { // 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 ); @@ -204,10 +205,12 @@ class Less_SourceMap_Generator extends Less_Configurable { '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']; + } } /** @@ -229,7 +232,8 @@ class Less_SourceMap_Generator extends Less_Configurable { $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; @@ -343,7 +347,7 @@ class Less_SourceMap_Generator extends Less_Configurable { * @return int|false */ protected function findFileIndex( $filename ) { - return $this->source_keys[$filename]; + return $this->source_keys[$filename] ?? false; } /** diff --git a/include/thirdparty/less.php/Tree.php b/include/thirdparty/less.php/Tree.php index 0d3cea8..3ad3abf 100644 --- a/include/thirdparty/less.php/Tree.php +++ b/include/thirdparty/less.php/Tree.php @@ -5,9 +5,26 @@ */ 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 @@ -174,6 +191,7 @@ class Less_Tree { public static function ReferencedArray( $rules ) { foreach ( $rules as $rule ) { if ( method_exists( $rule, 'markReferenced' ) ) { + // @phan-suppress-next-line PhanUndeclaredMethod False positive $rule->markReferenced(); } } @@ -191,4 +209,11 @@ class Less_Tree { return $obj; } + /** + * @see less-3.13.1.js#Node.prototype.isVisible + */ + public function isVisible() { + return $this->nodeVisible; + } + } diff --git a/include/thirdparty/less.php/Tree/Alpha.php b/include/thirdparty/less.php/Tree/Alpha.php index 9503a85..822542c 100644 --- a/include/thirdparty/less.php/Tree/Alpha.php +++ b/include/thirdparty/less.php/Tree/Alpha.php @@ -4,6 +4,7 @@ * @see less-2.5.3.js#Alpha.prototype */ class Less_Tree_Alpha extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string|Less_Tree */ public $value; /** diff --git a/include/thirdparty/less.php/Tree/Anonymous.php b/include/thirdparty/less.php/Tree/Anonymous.php index 9c41abc..55edd96 100644 --- a/include/thirdparty/less.php/Tree/Anonymous.php +++ b/include/thirdparty/less.php/Tree/Anonymous.php @@ -4,13 +4,19 @@ * @see less-2.5.3.js#Anonymous.prototype */ class Less_Tree_Anonymous extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $value; + /** @var string|null */ public $quote; + /** @var int|null */ public $index; + /** @var bool|null */ public $mapLines; + /** @var array|null */ public $currentFileInfo; /** @var bool */ public $rulesetLike; + /** @var bool */ public $isReferenced; /** @@ -27,6 +33,8 @@ class Less_Tree_Anonymous extends Less_Tree implements Less_Tree_HasValuePropert $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; } @@ -37,18 +45,24 @@ class Less_Tree_Anonymous extends Less_Tree implements Less_Tree_HasValuePropert /** * @param Less_Tree|mixed $x * @return int|null - * @see less-2.5.3.js#Anonymous.prototype.compare + * @see less-3.13.1.js#Anonymous.prototype.compare */ public function compare( $x ) { - return ( is_object( $x ) && $this->toCSS() === $x->toCSS() ) ? 0 : null; + return ( $x instanceof Less_Tree && $this->toCSS() === $x->toCSS() ) ? 0 : null; } public function isRulesetLike() { return $this->rulesetLike; } + /** + * @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 markReferenced() { diff --git a/include/thirdparty/less.php/Tree/Assignment.php b/include/thirdparty/less.php/Tree/Assignment.php index d77c9d4..025e6f3 100644 --- a/include/thirdparty/less.php/Tree/Assignment.php +++ b/include/thirdparty/less.php/Tree/Assignment.php @@ -4,7 +4,9 @@ * @see less-2.5.3.js#Assignment.prototype */ class Less_Tree_Assignment extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $key; + /** @var Less_Tree */ public $value; public function __construct( string $key, Less_Tree $val ) { diff --git a/include/thirdparty/less.php/Tree/Directive.php b/include/thirdparty/less.php/Tree/AtRule.php similarity index 77% rename from include/thirdparty/less.php/Tree/Directive.php rename to include/thirdparty/less.php/Tree/AtRule.php index 0d295a3..e0c75c1 100644 --- a/include/thirdparty/less.php/Tree/Directive.php +++ b/include/thirdparty/less.php/Tree/AtRule.php @@ -1,20 +1,38 @@ name = $name; + // TODO: Less.js 3.13 handles `$value instanceof Less_Tree` and creates Anonymous here. $this->value = $value; if ( $rules !== null ) { @@ -28,6 +46,7 @@ class Less_Tree_Directive extends Less_Tree implements Less_Tree_HasValuePropert foreach ( $this->rules as $rule ) { $rule->allowImports = true; } + // TODO: Less.js 3.13 handles setParent() here } $this->index = $index; @@ -106,11 +125,18 @@ class Less_Tree_Directive extends Less_Tree implements Less_Tree_HasValuePropert } 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 ); } } + // 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; if ( $this->rules ) { diff --git a/include/thirdparty/less.php/Tree/Attribute.php b/include/thirdparty/less.php/Tree/Attribute.php index eefd623..6eec785 100644 --- a/include/thirdparty/less.php/Tree/Attribute.php +++ b/include/thirdparty/less.php/Tree/Attribute.php @@ -4,8 +4,11 @@ * @see less-2.5.3.js#Attribute.prototype */ class Less_Tree_Attribute extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $key; + /** @var null|string */ public $op; + /** @var null|string|Less_Tree */ public $value; /** @@ -41,7 +44,7 @@ class Less_Tree_Attribute extends Less_Tree implements Less_Tree_HasValuePropert 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/include/thirdparty/less.php/Tree/Call.php b/include/thirdparty/less.php/Tree/Call.php index 9f6e7b4..7a9ee40 100644 --- a/include/thirdparty/less.php/Tree/Call.php +++ b/include/thirdparty/less.php/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; } @@ -46,6 +48,17 @@ class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty { 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, @@ -56,18 +69,24 @@ class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty { // 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. - // + // TODO less.js#3.13.1 provide better parity with upstream. public function compile( $env ) { - // Turn off math for calc(). https://phabricator.wikimedia.org/T331688 - $currentMathContext = $env->strictMath; - $env->strictMath = !$this->mathOn; + /** + * Turn off math for calc(), and switch back on for evaluating nested functions + */ + $currentMathContext = $env->mathOn; + $env->mathOn = !$this->calc; + + if ( $this->calc || $env->inCalc ) { + $env->enterCalc(); + } $args = []; foreach ( $this->args as $a ) { $args[] = $a->compile( $env ); } - $env->strictMath = $currentMathContext; + $env->mathOn = $currentMathContext; $nameLC = strtolower( $this->name ); switch ( $nameLC ) { @@ -114,6 +133,7 @@ class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty { if ( $func ) { try { $result = $this->functionCaller( $func, $args ); + $this->exitCalc( $env, $currentMathContext ); } catch ( Exception $e ) { // Preserve original trace, especially from custom functions. // https://github.com/wikimedia/less.php/issues/38 @@ -129,7 +149,7 @@ class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty { if ( $result !== null ) { return $result; } - + $this->exitCalc( $env, $currentMathContext ); return new self( $this->name, $args, $this->index, $this->currentFileInfo ); } diff --git a/include/thirdparty/less.php/Tree/Color.php b/include/thirdparty/less.php/Tree/Color.php index 1a24f8e..c41ed18 100644 --- a/include/thirdparty/less.php/Tree/Color.php +++ b/include/thirdparty/less.php/Tree/Color.php @@ -1,37 +1,38 @@ */ public $rgb; + /** @var int */ public $alpha; - public $isTransparentKeyword; + /** @var null|string */ public $value; - public function __construct( $rgb, $a = 1, $isTransparentKeyword = null, $originalForm = null ) { - if ( $isTransparentKeyword ) { - $this->rgb = $rgb; - $this->alpha = $a; - $this->isTransparentKeyword = true; - return; - } - if ( isset( $originalForm ) ) { - $this->value = $originalForm; - } - - $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() { @@ -56,20 +57,21 @@ class Less_Tree_Color extends Less_Tree { public function toCSS( $doNotCompress = false ) { $compress = Less_Parser::$options['compress'] && !$doNotCompress; $alpha = $this->fround( $this->alpha ); + + // `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 some transparency, the only way to represent it - // is via `rgba`. Otherwise, we use the hex representation, + + // 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[] = $this->clamp( round( $c ), 255 ); @@ -78,30 +80,24 @@ class Less_Tree_Color extends Less_Tree { $glue = ( $compress ? ',' : ', ' ); return "rgba(" . implode( $glue, $values ) . ")"; - } else { - - $color = $this->toRGB(); - - if ( $compress ) { - - // Convert color to short format - if ( $color[1] === $color[2] && $color[3] === $color[4] && $color[5] === $color[6] ) { - $color = '#' . $color[1] . $color[3] . $color[5]; - } - } - - return $color; } + + $color = $this->toRGB(); + if ( $compress ) { + // Convert color to short format + if ( $color[1] === $color[2] && $color[3] === $color[4] && $color[5] === $color[6] ) { + $color = '#' . $color[1] . $color[3] . $color[5]; + } + } + return $color; } - // - // Operations have to be done per-channel, if not, - // channels will spill onto each other. Once we have - // our result, in the form of an integer triplet, - // we create a new Color node to hold the result. - // - /** + * 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 */ @@ -129,8 +125,9 @@ class Less_Tree_Color extends Less_Tree { $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 ) { @@ -168,8 +165,9 @@ class Less_Tree_Color extends Less_Tree { $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 ); @@ -194,14 +192,11 @@ class Less_Tree_Color extends Less_Tree { /** * @param mixed $x * @return int|null - * @see less-2.5.3.js#Color.prototype.compare + * @see less-3.13.1.js#Color.prototype.compare */ public function compare( $x ) { - if ( !$x instanceof self ) { - 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 : null; @@ -211,7 +206,7 @@ class Less_Tree_Color extends Less_Tree { * @param int|float $val * @param int $max * @return int|float - * @see less-2.5.3.js#Color.prototype + * @see less-3.13.1.js#Color.prototype */ private function clamp( $val, $max ) { return min( max( $val, 0 ), $max ); @@ -226,7 +221,6 @@ class Less_Tree_Color extends Less_Tree { } $ret .= dechex( $c ); } - return $ret; } @@ -234,21 +228,19 @@ class Less_Tree_Color extends Less_Tree { * @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/include/thirdparty/less.php/Tree/Comment.php b/include/thirdparty/less.php/Tree/Comment.php index c299b8c..a5937a2 100644 --- a/include/thirdparty/less.php/Tree/Comment.php +++ b/include/thirdparty/less.php/Tree/Comment.php @@ -4,9 +4,13 @@ * @see less-2.5.3.js#Comment.prototype */ class Less_Tree_Comment extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var string */ public $value; + /** @var bool */ public $isLineComment; + /** @var bool|null */ public $isReferenced; + /** @var array|null */ public $currentFileInfo; public function __construct( $value, $isLineComment, $index = null, $currentFileInfo = null ) { diff --git a/include/thirdparty/less.php/Tree/Condition.php b/include/thirdparty/less.php/Tree/Condition.php index 033ac2e..e10282d 100644 --- a/include/thirdparty/less.php/Tree/Condition.php +++ b/include/thirdparty/less.php/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 ) { diff --git a/include/thirdparty/less.php/Tree/Rule.php b/include/thirdparty/less.php/Tree/Declaration.php similarity index 71% rename from include/thirdparty/less.php/Tree/Rule.php rename to include/thirdparty/less.php/Tree/Declaration.php index 75020ca..5c0e907 100644 --- a/include/thirdparty/less.php/Tree/Rule.php +++ b/include/thirdparty/less.php/Tree/Declaration.php @@ -1,23 +1,39 @@ */ public $name; - /** @var Less_Tree */ + /** @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 mixed $value + * @param Less_Tree|string|null $value * @param null|false|string $important * @param null|false|string $merge * @param int|null $index @@ -25,12 +41,20 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { * @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 ) { + 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_Value( [ $value ? new Less_Tree_Anonymous( $value ) : null ] ); $this->important = $important ? ' ' . trim( $important ) : ''; $this->merge = $merge; $this->index = $index; @@ -56,7 +80,11 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { $e->currentFile = $this->currentFileInfo; throw $e; } - $output->add( $this->important . ( ( $this->inline || ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), $this->currentFileInfo, $this->index ); + $output->add( + $this->important . ( ( $this->inline || ( Less_Environment::$lastRule && Less_Parser::$options['compress'] ) ) ? "" : ";" ), + $this->currentFileInfo, + $this->index + ); } /** @@ -78,10 +106,11 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { $variable = false; // never treat expanded interpolation as new variable name } - $strictMathBypass = false; - if ( $name === "font" && !$env->strictMath ) { - $strictMathBypass = true; - $env->strictMath = true; + $mathBypass = false; + $prevMath = $env->math; + if ( $name === "font" && $env->math === Less_Environment::MATH_ALWAYS ) { + $mathBypass = true; + $env->math = Less_Environment::MATH_PARENS_DIVISION; } try { @@ -99,7 +128,8 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { $important = $importantResult['important']; } - $return = new Less_Tree_Rule( $name, + $return = new Less_Tree_Declaration( + $name, $evaldValue, $important, $this->merge, @@ -118,8 +148,8 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { throw $e; } - if ( $strictMathBypass ) { - $env->strictMath = false; + if ( $mathBypass ) { + $env->math = $prevMath; } return $return; @@ -141,7 +171,6 @@ class Less_Tree_Rule extends Less_Tree implements Less_Tree_HasValueProperty { if ( !is_array( $this->value ) ) { if ( method_exists( $value, 'markReferenced' ) ) { - // @phan-suppress-next-line PhanUndeclaredMethod $value->markReferenced(); } } else { diff --git a/include/thirdparty/less.php/Tree/DefaultFunc.php b/include/thirdparty/less.php/Tree/DefaultFunc.php index 0c07f38..e4bdf95 100644 --- a/include/thirdparty/less.php/Tree/DefaultFunc.php +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/DetachedRuleset.php b/include/thirdparty/less.php/Tree/DetachedRuleset.php index 53b25fb..632ac6e 100644 --- a/include/thirdparty/less.php/Tree/DetachedRuleset.php +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Dimension.php b/include/thirdparty/less.php/Tree/Dimension.php index 347f689..1e7fd5b 100644 --- a/include/thirdparty/less.php/Tree/Dimension.php +++ b/include/thirdparty/less.php/Tree/Dimension.php @@ -7,6 +7,7 @@ class Less_Tree_Dimension extends Less_Tree implements Less_Tree_HasValuePropert /** @var float */ public $value; + /** @var Less_Tree_Unit */ public $unit; public function __construct( $value, $unit = null ) { @@ -34,7 +35,9 @@ class Less_Tree_Dimension extends Less_Tree implements Less_Tree_HasValuePropert */ public function genCSS( $output ) { if ( Less_Parser::$options['strictUnits'] && !$this->unit->isSingular() ) { - throw new Less_Exception_Compiler( "Multiple units in dimension. Correct the units or use the unit function. Bad unit: " . $this->unit->toString() ); + throw new Less_Exception_Compiler( + "Multiple units in dimension. Correct the units or use the unit function. Bad unit: " . $this->unit->toString() + ); } $value = $this->fround( $this->value ); @@ -91,7 +94,10 @@ class Less_Tree_Dimension extends Less_Tree implements Less_Tree_HasValuePropert $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 = $this->_operate( $op, $this->value, $other->value ); diff --git a/include/thirdparty/less.php/Tree/Element.php b/include/thirdparty/less.php/Tree/Element.php index de3700b..30ac2ec 100644 --- a/include/thirdparty/less.php/Tree/Element.php +++ b/include/thirdparty/less.php/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 @@ class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty } 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 @@ class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty 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,14 +55,16 @@ class Less_Tree_Element extends Less_Tree implements Less_Tree_HasValueProperty } public function toCSS() { - if ( $this->value_is_object ) { + if ( $this->value instanceof Less_Tree ) { $value = $this->value->toCSS(); } else { $value = $this->value; } $spaceOrEmpty = ' '; - if ( Less_Parser::$options['compress'] || ( isset( Less_Environment::$_noSpaceCombinators[$this->combinator] ) && Less_Environment::$_noSpaceCombinators[$this->combinator] ) ) { + if ( Less_Parser::$options['compress'] || + ( isset( Less_Environment::$_noSpaceCombinators[$this->combinator] ) && Less_Environment::$_noSpaceCombinators[$this->combinator] ) + ) { $spaceOrEmpty = ''; } diff --git a/include/thirdparty/less.php/Tree/Expression.php b/include/thirdparty/less.php/Tree/Expression.php index 20dbe91..986b7e0 100644 --- a/include/thirdparty/less.php/Tree/Expression.php +++ b/include/thirdparty/less.php/Tree/Expression.php @@ -1,16 +1,20 @@ value = $value; - $this->parens = $parens; + $this->noSpacing = $noSpacing; } public function accept( $visitor ) { @@ -18,46 +22,42 @@ class Less_Tree_Expression extends Less_Tree implements Less_Tree_HasValueProper } 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--; + if ( $inParenthesis ) { + $env->outOfParenthesis(); + } - } elseif ( !$env->isMathOn() && !$doubleParen ) { - $returnValue = new Less_Tree_Paren( $returnValue ); - - } + if ( $this->parens && $this->parensInOp && !$mathOn && !$doubleParen && + ( !( $returnValue instanceof Less_Tree_Dimension ) ) + ) { + $returnValue = new Less_Tree_Paren( $returnValue ); } return $returnValue; } @@ -69,8 +69,13 @@ class Less_Tree_Expression extends Less_Tree implements Less_Tree_HasValueProper $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( ' ' ); + } } } } diff --git a/include/thirdparty/less.php/Tree/Extend.php b/include/thirdparty/less.php/Tree/Extend.php index e0a9b2a..e121202 100644 --- a/include/thirdparty/less.php/Tree/Extend.php +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Import.php b/include/thirdparty/less.php/Tree/Import.php index 6d5da72..fcad61f 100644 --- a/include/thirdparty/less.php/Tree/Import.php +++ b/include/thirdparty/less.php/Tree/Import.php @@ -16,11 +16,17 @@ */ 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; /** @var bool|null This is populated by Less_ImportVisitor */ public $doSkip = false; @@ -124,7 +130,7 @@ class Less_Tree_Import extends Less_Tree { public function compileForImport( $env ) { $path = $this->path; if ( $path instanceof Less_Tree_Url ) { - $path = $path->value; + $path = $path->value; } return new self( $path->compile( $env ), $this->features, $this->options, $this->index, $this->currentFileInfo ); } @@ -154,30 +160,15 @@ class Less_Tree_Import extends Less_Tree { public function compile( $env ) { $features = ( $this->features ? $this->features->compile( $env ) : null ); - // TODO: Upstream doesn't do path resolution here. The reason we need it here is - // because skip() takes a $path_and_uri argument. Once the TODO in ImportVisitor - // about Less_Tree_Import::PathAndUri() is fixed, this can be removed by letting - // skip() call $this->PathAndUri() on its own. - // get path & uri - $path_and_uri = $env->callImportCallback( $this ); - - if ( !$path_and_uri ) { - $path_and_uri = Less_FileManager::getFilePath( $this->getPath(), $this->currentFileInfo ); - } - if ( $path_and_uri ) { - [ $full_path, $uri ] = $path_and_uri; - } else { - $full_path = $uri = $this->getPath(); - } - '@phan-var string $full_path'; - // import once - if ( $this->skip( $full_path, $env ) ) { + if ( $this->skip( $env ) ) { return []; } if ( $this->options['inline'] ) { - $contents = new Less_Tree_Anonymous( $this->root, 0, + $contents = new Less_Tree_Anonymous( + $this->root, + 0, [ 'filename' => $this->importedFilename, 'reference' => $this->currentFileInfo['reference'] ?? null, @@ -208,21 +199,26 @@ class Less_Tree_Import extends Less_Tree { /** * Should the import be skipped? * - * @param string|null $path * @param Less_Environment $env * @return bool|null */ - public function skip( $path, $env ) { + 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 ( $this->doSkip !== null ) { return $this->doSkip; } // @see less-2.5.3.js#ImportVisitor.prototype.onImported - if ( isset( $env->importVisitorOnceMap[$path] ) ) { + if ( isset( $env->importVisitorOnceMap[$fullPath] ) ) { return true; } - $env->importVisitorOnceMap[$path] = true; + $env->importVisitorOnceMap[$fullPath] = true; return false; } } diff --git a/include/thirdparty/less.php/Tree/Javascript.php b/include/thirdparty/less.php/Tree/Javascript.php index ff432c1..50be0a1 100644 --- a/include/thirdparty/less.php/Tree/Javascript.php +++ b/include/thirdparty/less.php/Tree/Javascript.php @@ -1,19 +1,23 @@ escaped = $escaped; $this->expression = $string; $this->index = $index; diff --git a/include/thirdparty/less.php/Tree/Media.php b/include/thirdparty/less.php/Tree/Media.php index 0c890d0..f52e49e 100644 --- a/include/thirdparty/less.php/Tree/Media.php +++ b/include/thirdparty/less.php/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 @@ class Less_Tree_Media extends Less_Tree { public function compile( $env ) { $media = new self( [], [], $this->index, $this->currentFileInfo ); - $strictMathBypass = false; - if ( !$env->strictMath ) { - $strictMathBypass = true; - $env->strictMath = true; + $mathBypass = false; + if ( !$env->mathOn ) { + $mathBypass = true; + $env->mathOn = true; } $media->features = $this->features->compile( $env ); - if ( $strictMathBypass ) { - $env->strictMath = false; + if ( $mathBypass ) { + $env->mathOn = false; } $env->mediaPath[] = $media; @@ -134,7 +139,7 @@ class Less_Tree_Media extends Less_Tree { 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/include/thirdparty/less.php/Tree/Mixin/Call.php b/include/thirdparty/less.php/Tree/Mixin/Call.php index 45117d5..df759dc 100644 --- a/include/thirdparty/less.php/Tree/Mixin/Call.php +++ b/include/thirdparty/less.php/Tree/Mixin/Call.php @@ -4,11 +4,16 @@ */ 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; public function __construct( $elements, $args, $index, $currentFileInfo, $important = false ) { @@ -28,7 +33,7 @@ class Less_Tree_Mixin_Call extends Less_Tree { $isOneFound = false; $candidates = []; $conditionResult = []; - + $this->selector = $this->selector->compile( $env ); $args = []; foreach ( $this->arguments as $a ) { $argValue = $a['value']->compile( $env ); @@ -153,10 +158,19 @@ class Less_Tree_Mixin_Call extends Less_Tree { 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 + ); } } @@ -171,7 +185,7 @@ class Less_Tree_Mixin_Call extends Less_Tree { 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/include/thirdparty/less.php/Tree/Mixin/Definition.php b/include/thirdparty/less.php/Tree/Mixin/Definition.php index 08c8818..13f210c 100644 --- a/include/thirdparty/less.php/Tree/Mixin/Definition.php +++ b/include/thirdparty/less.php/Tree/Mixin/Definition.php @@ -3,16 +3,27 @@ * @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 = []; public function __construct( $name, $params, $rules, $condition, $variadic = false, $frames = [] ) { @@ -60,7 +71,7 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { 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; } @@ -92,12 +103,17 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { $varargs[] = $args[$j]['value']->compile( $env ); } $expression = new Less_Tree_Expression( $varargs ); - array_unshift( $frame->rules, new Less_Tree_Rule( $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 ) { @@ -110,7 +126,7 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { throw new Less_Exception_Compiler( "Wrong number of arguments for " . $this->name . " (" . $argsLength . ' for ' . $this->arity . ")" ); } - array_unshift( $frame->rules, new Less_Tree_Rule( $name, $val ) ); + array_unshift( $frame->rules, new Less_Tree_Declaration( $name, $val ) ); $evaldArguments[$i] = $val; } } @@ -156,7 +172,7 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { $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; @@ -185,7 +201,7 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { // 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 ); @@ -205,7 +221,7 @@ class Less_Tree_Mixin_Definition extends Less_Tree_Ruleset { 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; diff --git a/include/thirdparty/less.php/Tree/NameValue.php b/include/thirdparty/less.php/Tree/NameValue.php index f35d819..95b9a49 100644 --- a/include/thirdparty/less.php/Tree/NameValue.php +++ b/include/thirdparty/less.php/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 @@ class Less_Tree_NameValue extends Less_Tree implements Less_Tree_HasValuePropert public function genCSS( $output ) { $output->add( $this->name - . ( Less_Parser::$options['compress'] ? ':' : ': ' ) - . $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/include/thirdparty/less.php/Tree/NamespaceValue.php b/include/thirdparty/less.php/Tree/NamespaceValue.php new file mode 100644 index 0000000..de605ae --- /dev/null +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Negative.php b/include/thirdparty/less.php/Tree/Negative.php index 7bade41..5ed324a 100644 --- a/include/thirdparty/less.php/Tree/Negative.php +++ b/include/thirdparty/less.php/Tree/Negative.php @@ -4,6 +4,7 @@ */ class Less_Tree_Negative extends Less_Tree implements Less_Tree_HasValueProperty { + /** @var Less_Tree */ public $value; public function __construct( $node ) { diff --git a/include/thirdparty/less.php/Tree/Operation.php b/include/thirdparty/less.php/Tree/Operation.php index d5df55a..50eddce 100644 --- a/include/thirdparty/less.php/Tree/Operation.php +++ b/include/thirdparty/less.php/Tree/Operation.php @@ -1,11 +1,15 @@ isMathOn() ) { + if ( $env->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/include/thirdparty/less.php/Tree/Property.php b/include/thirdparty/less.php/Tree/Property.php new file mode 100644 index 0000000..894e5cd --- /dev/null +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Quoted.php b/include/thirdparty/less.php/Tree/Quoted.php index 1509f58..dfd163d 100644 --- a/include/thirdparty/less.php/Tree/Quoted.php +++ b/include/thirdparty/less.php/Tree/Quoted.php @@ -3,13 +3,22 @@ * @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 */ @@ -36,23 +45,17 @@ class Less_Tree_Quoted extends Less_Tree implements Less_Tree_HasValueProperty { } } + /** + * @see less-3.13.1.js#Quoted.prototype.containsVariables + */ public function containsVariables() { - return preg_match( '/(`([^`]+)`)|@\{([\w-]+)\}/', $this->value ); + return preg_match( $this->variableRegex, $this->value ); } - 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 ); - } - } - $r = $value; + private function variableReplacement( $r, $env ) { do { $value = $r; - if ( preg_match_all( '/@\{([\w-]+)\}/', $value, $matches ) ) { + 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 ); @@ -61,8 +64,29 @@ class Less_Tree_Quoted extends Less_Tree implements Less_Tree_HasValueProperty { } } } while ( $r != $value ); + return $r; + } - return new self( $this->quote . $r . $this->quote, $r, $this->escaped, $this->index, $this->currentFileInfo ); + 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 ); } /** @@ -75,7 +99,7 @@ class Less_Tree_Quoted extends Less_Tree implements Less_Tree_HasValueProperty { return Less_Tree::numericCompare( $this->value, $other->value ); } else { return ( - Less_Parser::is_method( $other, 'toCSS' ) + $other instanceof Less_Tree && $this->toCSS() === $other->toCSS() ) ? 0 : null; } diff --git a/include/thirdparty/less.php/Tree/Ruleset.php b/include/thirdparty/less.php/Tree/Ruleset.php index ca05462..f9bc365 100644 --- a/include/thirdparty/less.php/Tree/Ruleset.php +++ b/include/thirdparty/less.php/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 @@ class Less_Tree_Ruleset extends Less_Tree { 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 @@ class Less_Tree_Ruleset extends Less_Tree { $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 @@ class Less_Tree_Ruleset extends Less_Tree { $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 @@ class Less_Tree_Ruleset extends Less_Tree { 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,18 +308,17 @@ class Less_Tree_Ruleset extends Less_Tree { } public function resetCache() { - $this->_rulesets = null; $this->_variables = null; $this->lookups = []; } /** - * @see less-2.5.3.js#Ruleset.prototype.variables + * @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 @@ -322,15 +334,110 @@ class Less_Tree_Ruleset extends Less_Tree { 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; + } + + /** + * @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 ) { @@ -383,7 +490,7 @@ class Less_Tree_Ruleset extends Less_Tree { // 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_Directive ) { + } elseif ( $rule instanceof Less_Tree_Anonymous || $rule instanceof Less_Tree_AtRule ) { return $rule->isRulesetLike(); } @@ -419,7 +526,7 @@ class Less_Tree_Ruleset extends Less_Tree { $importNodeIndex++; } $ruleNodes[] = $rule; - } elseif ( $rule instanceof Less_Tree_Directive && $rule->isCharset() ) { + } elseif ( $rule instanceof Less_Tree_AtRule && $rule->isCharset() ) { array_splice( $ruleNodes, $charsetNodeIndex, 0, [ $rule ] ); $charsetNodeIndex++; $importNodeIndex++; @@ -475,7 +582,7 @@ class Less_Tree_Ruleset extends Less_Tree { Less_Environment::$lastRule = $currentLastRule; - if ( !Less_Environment::$lastRule ) { + if ( !Less_Environment::$lastRule && $rule->isVisible() ) { $output->add( $tabRuleStr ); } else { Less_Environment::$lastRule = false; diff --git a/include/thirdparty/less.php/Tree/RulesetCall.php b/include/thirdparty/less.php/Tree/RulesetCall.php deleted file mode 100644 index 9c162b8..0000000 --- a/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Selector.php b/include/thirdparty/less.php/Tree/Selector.php index 3759730..9f75a84 100644 --- a/include/thirdparty/less.php/Tree/Selector.php +++ b/include/thirdparty/less.php/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; /** @@ -97,17 +109,20 @@ class Less_Tree_Selector extends Less_Tree { foreach ( $this->elements as $v ) { $css .= $v->combinator; - if ( !$v->value_is_object ) { + if ( !( $v->value instanceof Less_Tree ) ) { $css .= $v->value; continue; } - if ( isset( $v->value->value ) && !is_object( $v->value->value ) ) { + // @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; @@ -142,7 +157,7 @@ class Less_Tree_Selector extends Less_Tree { $extendList = []; foreach ( $this->extendList as $el ) { - $extendList[] = $el->compile( $el ); + $extendList[] = $el->compile( $env ); } $evaldCondition = false; diff --git a/include/thirdparty/less.php/Tree/UnicodeDescriptor.php b/include/thirdparty/less.php/Tree/UnicodeDescriptor.php index cc1b39e..4ca2517 100644 --- a/include/thirdparty/less.php/Tree/UnicodeDescriptor.php +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Tree/Unit.php b/include/thirdparty/less.php/Tree/Unit.php index da7c05d..e5ccc2a 100644 --- a/include/thirdparty/less.php/Tree/Unit.php +++ b/include/thirdparty/less.php/Tree/Unit.php @@ -5,8 +5,11 @@ */ class Less_Tree_Unit extends Less_Tree { + /** @var string[] */ public $numerator = []; + /** @var string[] */ public $denominator = []; + /** @var string|null */ public $backupUnit; public function __construct( $numerator = [], $denominator = [], $backupUnit = null ) { diff --git a/include/thirdparty/less.php/Tree/UnitConversions.php b/include/thirdparty/less.php/Tree/UnitConversions.php index 9ce7f3c..e144d53 100644 --- a/include/thirdparty/less.php/Tree/UnitConversions.php +++ b/include/thirdparty/less.php/Tree/UnitConversions.php @@ -4,8 +4,10 @@ */ class Less_Tree_UnitConversions { + /** @var string[] */ public static $groups = [ 'length', 'duration', 'angle' ]; + /** @var array */ public static $length = [ 'm' => 1, 'cm' => 0.01, @@ -14,18 +16,20 @@ class Less_Tree_UnitConversions { '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, 'turn' => 1 - ]; + ]; } diff --git a/include/thirdparty/less.php/Tree/Url.php b/include/thirdparty/less.php/Tree/Url.php index 545372f..954c7a1 100644 --- a/include/thirdparty/less.php/Tree/Url.php +++ b/include/thirdparty/less.php/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; /** diff --git a/include/thirdparty/less.php/Tree/Value.php b/include/thirdparty/less.php/Tree/Value.php index 68336c7..6ae5767 100644 --- a/include/thirdparty/less.php/Tree/Value.php +++ b/include/thirdparty/less.php/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 ) { diff --git a/include/thirdparty/less.php/Tree/Variable.php b/include/thirdparty/less.php/Tree/Variable.php index 47f721c..839debf 100644 --- a/include/thirdparty/less.php/Tree/Variable.php +++ b/include/thirdparty/less.php/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 @@ class Less_Tree_Variable extends Less_Tree { /** * @param Less_Environment $env * @return Less_Tree|Less_Tree_Keyword|Less_Tree_Quoted - * @see less-2.5.3.js#Variable.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,25 +39,45 @@ class Less_Tree_Variable extends Less_Tree { } 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 ) { + /** @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; } - $r = $v->value->compile( $env ); - $this->evaluating = false; - return $r; + // 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/include/thirdparty/less.php/Tree/VariableCall.php b/include/thirdparty/less.php/Tree/VariableCall.php new file mode 100644 index 0000000..0a389d6 --- /dev/null +++ b/include/thirdparty/less.php/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/include/thirdparty/less.php/Version.php b/include/thirdparty/less.php/Version.php index ad6cafd..8e62157 100644 --- a/include/thirdparty/less.php/Version.php +++ b/include/thirdparty/less.php/Version.php @@ -6,11 +6,11 @@ class Less_Version { /* Current release version of less.php */ - public const version = '4.4.1'; + public const version = '5.3.1'; /* 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-3'; + public const cache_version = '3131-9'; } diff --git a/include/thirdparty/less.php/Visitor.php b/include/thirdparty/less.php/Visitor.php index 4de205f..06f7256 100644 --- a/include/thirdparty/less.php/Visitor.php +++ b/include/thirdparty/less.php/Visitor.php @@ -4,7 +4,7 @@ */ class Less_Visitor { - protected $methods = []; + /** @var array */ protected $_visitFnCache = []; public function __construct() { @@ -13,10 +13,27 @@ class Less_Visitor { } public function visitObj( $node ) { - if ( !$node || !is_object( $node ) ) { + static $funcNames = []; + + if ( !$node instanceof Less_Tree ) { return $node; } - $funcName = 'visit' . str_replace( [ 'Less_Tree_', '_' ], '', get_class( $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; $newNode = $this->$funcName( $node, $visitDeeper ); @@ -24,7 +41,7 @@ class Less_Visitor { $node = $newNode; } - if ( $visitDeeper && is_object( $node ) ) { + if ( $visitDeeper && $node instanceof Less_Tree ) { $node->accept( $this ); } diff --git a/include/thirdparty/less.php/Visitor/extendFinder.php b/include/thirdparty/less.php/Visitor/extendFinder.php index 604dc91..0b9d955 100644 --- a/include/thirdparty/less.php/Visitor/extendFinder.php +++ b/include/thirdparty/less.php/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 @@ class Less_Visitor_extendFinder extends Less_Visitor { return $root; } - public function visitRule( $ruleNode, &$visitDeeper ) { + public function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -80,7 +83,7 @@ class Less_Visitor_extendFinder extends Less_Visitor { } 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 @@ class Less_Visitor_extendFinder extends Less_Visitor { 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/include/thirdparty/less.php/Visitor/joinSelector.php b/include/thirdparty/less.php/Visitor/joinSelector.php index 9d85f10..320638d 100644 --- a/include/thirdparty/less.php/Visitor/joinSelector.php +++ b/include/thirdparty/less.php/Visitor/joinSelector.php @@ -4,6 +4,7 @@ */ class Less_Visitor_joinSelector extends Less_Visitor { + /** @var Less_Tree_Selector[][][] */ public $contexts = [ [] ]; /** @@ -13,7 +14,7 @@ class Less_Visitor_joinSelector extends Less_Visitor { return $this->visitObj( $root ); } - public function visitRule( $ruleNode, &$visitDeeper ) { + public function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -59,16 +60,16 @@ class Less_Visitor_joinSelector extends Less_Visitor { 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/include/thirdparty/less.php/Visitor/processExtends.php b/include/thirdparty/less.php/Visitor/processExtends.php index 8126acf..c818112 100644 --- a/include/thirdparty/less.php/Visitor/processExtends.php +++ b/include/thirdparty/less.php/Visitor/processExtends.php @@ -4,6 +4,7 @@ */ class Less_Visitor_processExtends extends Less_Visitor { + /** @var Less_Tree_Extend[][] */ public $allExtendsStack; /** @@ -106,7 +107,9 @@ class Less_Visitor_processExtends extends Less_Visitor { $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 @@ class Less_Visitor_processExtends extends Less_Visitor { return array_merge( $extendsList, $extendsToAdd ); } - protected function visitRule( $ruleNode, &$visitDeeper ) { + protected function visitDeclaration( $declNode, &$visitDeeper ) { $visitDeeper = false; } @@ -133,7 +136,7 @@ class Less_Visitor_processExtends extends Less_Visitor { 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 @@ class Less_Visitor_processExtends extends Less_Visitor { // 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 @@ class Less_Visitor_processExtends extends Less_Visitor { 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 @@ class Less_Visitor_processExtends extends Less_Visitor { 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 @@ class Less_Visitor_processExtends extends Less_Visitor { // 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 @@ class Less_Visitor_processExtends extends Less_Visitor { 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 @@ class Less_Visitor_processExtends extends Less_Visitor { 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/include/thirdparty/less.php/Visitor/toCSS.php b/include/thirdparty/less.php/Visitor/toCSS.php index d7f80d7..a64e680 100644 --- a/include/thirdparty/less.php/Visitor/toCSS.php +++ b/include/thirdparty/less.php/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 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { 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,9 +53,9 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { return $mediaNode; } - public function visitDirective( $directiveNode, &$visitDeeper ) { - if ( $directiveNode->name === '@charset' ) { - if ( !$directiveNode->getIsReferenced() ) { + public function visitAtRule( $atRuleNode, &$visitDeeper ) { + if ( $atRuleNode->name === '@charset' ) { + if ( !$atRuleNode->getIsReferenced() ) { return; } if ( isset( $this->charset ) && $this->charset ) { @@ -64,35 +65,35 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { $this->charset = true; } - if ( $directiveNode->rules ) { - $this->_mergeRules( $directiveNode->rules[0]->rules ); + if ( $atRuleNode->rules ) { + self::_mergeRules( $atRuleNode->rules[0]->rules ); // process childs - $directiveNode->accept( $this ); + $atRuleNode->accept( $this ); $visitDeeper = false; // the directive was directly referenced and therefore needs to be shown in the output - if ( $directiveNode->getIsReferenced() ) { - return $directiveNode; + if ( $atRuleNode->getIsReferenced() ) { + return $atRuleNode; } - if ( !$directiveNode->rules ) { + if ( !$atRuleNode->rules ) { return; } - if ( $this->hasVisibleChild( $directiveNode ) ) { + if ( $this->hasVisibleChild( $atRuleNode ) ) { // marking as referenced in case the directive is stored inside another directive - $directiveNode->markReferenced(); - return $directiveNode; + $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 ( !$directiveNode->getIsReferenced() ) { + if ( !$atRuleNode->getIsReferenced() ) { return; } } - return $directiveNode; + return $atRuleNode; } public function checkPropertiesInRoot( $rulesetNode ) { @@ -101,7 +102,7 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { } 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 ); @@ -144,7 +145,7 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { if ( $rulesetNode->rules ) { if ( count( $rulesetNode->rules ) > 1 ) { - $this->_mergeRules( $rulesetNode->rules ); + self::_mergeRules( $rulesetNode->rules ); $this->_removeDuplicateRules( $rulesetNode->rules ); } @@ -220,14 +221,14 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { $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() ]; } @@ -242,7 +243,7 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { } } - protected function _mergeRules( &$rules ) { + public static function _mergeRules( &$rules ) { $groups = []; // obj($rules); @@ -251,7 +252,7 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { 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 ) { @@ -310,9 +311,9 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing { return new Less_Tree_Value( $mapped ); } - public function hasVisibleChild( $directiveNode ) { + public function hasVisibleChild( $atRuleNode ) { // prepare list of childs - $rule = $bodyRules = $directiveNode->rules; + $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 diff --git a/include/thirdparty/less.php/bin/lessc b/include/thirdparty/less.php/bin/lessc deleted file mode 100644 index 7e3d128..0000000 --- a/include/thirdparty/less.php/bin/lessc +++ /dev/null @@ -1,191 +0,0 @@ -#!/usr/bin/env php - false, 'relativeUrls' => false); -$silent = false; -$watch = false; -$rootpath = ''; - -// Check for arguments -array_shift($argv); -if (!count($argv)) { - $argv[] = '-h'; -} - -// parse arguments -foreach ($argv as $key => $arg) { - if (preg_match('/^--?([a-z][0-9a-z-]*)(?:=([^\s]+))?$/i', $arg, $matches)) { - $option = $matches[1]; - $value = isset($matches[2]) ? $matches[2] : false; - unset($argv[$key]); - - switch ($option) { - case 'h': - case 'help': - echo << 1) { - $output = array_pop($argv); - $inputs = $argv; -} -else { - $inputs = $argv; - $output = false; -} - -if (!count($inputs)) { - echo("lessc: no input files\n"); - exit; -} - -if ($watch) { - if (!$output) { - echo("lessc: you must specify the output file if --watch is given\n"); - exit; - } - - $lastAction = 0; - - echo("lessc: watching input files\n"); - - while (1) { - clearstatcache(); - - $updated = false; - foreach ($inputs as $input) { - if ($input == '-') { - if (count($inputs) == 1) { - echo("lessc: during watching files is not possible to watch stdin\n"); - exit; - } - else { - continue; - } - } - - if (filemtime($input) > $lastAction) { - $updated = true; - break; - } - } - - if ($updated) { - $lastAction = time(); - $parser = new Less_Parser($env); - foreach ($inputs as $input) { - try { - $parser->parseFile($input, $rootpath); - } - catch (Exception $e) { - echo("lessc: " . $e->getMessage() . " \n"); - continue; // Invalid processing - } - } - - file_put_contents($output, $parser->getCss()); - echo("lessc: output file recompiled\n"); - } - - sleep(1); - } -} -else { - $parser = new Less_Parser($env); - foreach ($inputs as $input) { - if ($input == '-') { - $content = file_get_contents('php://stdin'); - $parser->parse($content); - } - else { - try { - $parser->parseFile($input); - } - catch (Exception $e) { - if (!$silent) { - echo("lessc: " . ((string)$e) . " \n"); - } - } - } - } - - if ($output) { - file_put_contents($output, $parser->getCss()); - } - else { - echo $parser->getCss(); - } -} diff --git a/include/thirdparty/less.php/lessc.inc.php b/include/thirdparty/less.php/lessc.inc.php index f942042..c2db9cb 100644 --- a/include/thirdparty/less.php/lessc.inc.php +++ b/include/thirdparty/less.php/lessc.inc.php @@ -8,19 +8,26 @@ // Register autoloader for non-composer installations if ( !class_exists( 'Less_Parser' ) ) { - require_once __DIR__ . '/lib/Less/Autoloader.php'; + require_once __DIR__ . '/Autoloader.php'; Less_Autoloader::register(); } 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 ) { diff --git a/include/tool/Output/Css.php b/include/tool/Output/Css.php index 6130a84..4538289 100644 --- a/include/tool/Output/Css.php +++ b/include/tool/Output/Css.php @@ -272,8 +272,10 @@ class Css{ $parser->SetImportDirs($import_dirs); /* $parser->cache_method = 'php'; */ - $parser::$options['cache_method'] == 'php'; - $parser->SetCacheDir($dataDir . '/data/_cache'); + \Less_Parser::$options['cache_method'] = 'php'; + + // $parser->SetCacheDir($dataDir . '/data/_cache'); + \Less_Cache::SetCacheDir( $dataDir . '/data/_cache' ); // combine files try{ @@ -306,7 +308,7 @@ class Css{ gc_collect_cycles(); } - $less_files = $parser->allParsedFiles(); + $less_files = $parser->getParsedFiles(); return [$compiled, $temp_sourcemap_name]; }