diff --git a/lib/Less/Environment.php b/lib/Less/Environment.php index 934885ca..057be4a6 100644 --- a/lib/Less/Environment.php +++ b/lib/Less/Environment.php @@ -25,6 +25,8 @@ class Less_Environment { public $inCalc = false; public $mathOn = true; + private $calcStack = []; + /** @var Less_Tree_Media[] */ public $mediaBlocks = []; /** @var Less_Tree_Media[] */ @@ -151,6 +153,18 @@ public static function isPathRelative( $path ) { return !preg_match( '/^(?:[a-z-]+:|\/|#)/', $path ); } + public function enterCalc() { + $this->calcStack[] = true; + $this->inCalc = true; + } + + public function exitCalc() { + array_pop( $this->calcStack ); + if ( !$this->calcStack ) { + $this->inCalc = false; + } + } + /** * Canonicalize a path by resolving references to '/./', '/../' * Does not remove leading "../" diff --git a/lib/Less/Functions.php b/lib/Less/Functions.php index bc175bb4..1bf084cb 100644 --- a/lib/Less/Functions.php +++ b/lib/Less/Functions.php @@ -856,6 +856,13 @@ public function shade( $color, $amount = null ) { return $this->mix( $this->rgb( 0, 0, 0 ), $color, $amount ); } + /** + * @see less-3.13.1.js#functions _SELF + */ + public function _self( $args ) { + return $args; + } + public function extract( $values, $index ) { $index = (int)$index->value - 1; // (1-based index) // handle non-array values as an array of length 1 diff --git a/lib/Less/Parser.php b/lib/Less/Parser.php index 26d54999..17227f76 100644 --- a/lib/Less/Parser.php +++ b/lib/Less/Parser.php @@ -1050,14 +1050,14 @@ private function parseComment() { * "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() { + private function parseEntitiesQuoted( $forceEscaped = false ) { // Optimization: Determine match potential without save()/restore() overhead // Optimization: Inline matchChar() here, with its skipWhitespace(1) call below $startChar = $this->input[$this->pos] ?? null; $isEscaped = $startChar === '~'; - if ( !$isEscaped && $startChar !== "'" && $startChar !== '"' ) { + if ( ( !$isEscaped && $startChar !== "'" && $startChar !== '"' ) || ( $forceEscaped && !$isEscaped ) ) { return; } @@ -2574,6 +2574,8 @@ private function parseCondition() { /** * 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; @@ -2582,11 +2584,20 @@ private function parseOperand() { return; } $char = $this->input[$offset]; + // TODO: handle char `$` if ( $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() + // TODO: from less-3.13.1.js missing entities.property() + ?? $this->parseEntitiesCall() + ?? $this->parseEntitiesQuoted( true ); + // TODO: from less-3.13.1.js missing entities.colorKeyword() + // TODO: from less-3.13.1.js missing entities.mixinLookup() if ( $negate ) { $o->parensInOp = true; diff --git a/lib/Less/Tree/Call.php b/lib/Less/Tree/Call.php index c1790853..1177c6c4 100644 --- a/lib/Less/Tree/Call.php +++ b/lib/Less/Tree/Call.php @@ -15,7 +15,7 @@ class Less_Tree_Call extends Less_Tree implements Less_Tree_HasValueProperty { public function __construct( $name, $args, $index, $currentFileInfo = null ) { $this->name = $name; $this->args = $args; - $this->calc = ( $name === 'calc' ); + $this->calc = $name === 'calc'; $this->index = $index; $this->currentFileInfo = $currentFileInfo; } @@ -45,6 +45,17 @@ private function functionCaller( $function, array $arguments ) { 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, @@ -55,12 +66,17 @@ private function functionCaller( $function, array $arguments ) { // 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 + /** + * Turn off math for calc(), and switch back on for evaluating nested functions + */ $currentMathContext = $env->mathOn; $env->mathOn = !$this->calc; - // TODO: Less.js 3.13 also checks/toggles $env->inCalc + + if ( $this->calc || $env->inCalc ) { + $env->enterCalc(); + } $args = []; foreach ( $this->args as $a ) { @@ -114,6 +130,7 @@ public function compile( $env ) { 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 +146,7 @@ public function compile( $env ) { if ( $result !== null ) { return $result; } - + $this->exitCalc( $env, $currentMathContext ); return new self( $this->name, $args, $this->index, $this->currentFileInfo ); } diff --git a/lib/Less/Tree/Variable.php b/lib/Less/Tree/Variable.php index 47f721c6..7e0d1243 100644 --- a/lib/Less/Tree/Variable.php +++ b/lib/Less/Tree/Variable.php @@ -21,9 +21,10 @@ public function __construct( $name, $index = null, $currentFileInfo = null ) { /** * @param Less_Environment $env * @return Less_Tree|Less_Tree_Keyword|Less_Tree_Quoted - * @see less-2.5.3.js#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 @@ -38,7 +39,7 @@ public function compile( $env ) { } $this->evaluating = true; - + $variable = null; foreach ( $env->frames as $frame ) { $v = $frame->variable( $name ); if ( $v ) { @@ -46,11 +47,21 @@ public function compile( $env ) { $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 ); } diff --git a/test/Fixtures/lessjs-3.13.1/override/_main/calc.css b/test/Fixtures/lessjs-3.13.1/override/_main/calc.css new file mode 100644 index 00000000..9b29d7bf --- /dev/null +++ b/test/Fixtures/lessjs-3.13.1/override/_main/calc.css @@ -0,0 +1,19 @@ +.no-math { + root: calc(100% - 30px); + root2: calc(100% - 40px); + width: calc(50% + (25vh - 20px)); + height: calc(50% + (25vh - 20px)); + min-height: calc(10vh + calc(5vh)); + foo: 3 calc(3 + 4) 11; + bar: calc(1 + 20%); +} +.b { + one: calc(100% - 20px); + two: calc(100% - (10px + 10px)); + three: calc(100% - (3 * 1)); + four: calc(100% - (3 * 1)); + nested: calc(calc(2.25rem + 2px) - 1px * 2); +} +.c { + height: calc(100% - ((10px * 3) + (10px * 2))); +} diff --git a/test/Fixtures/lessjs-3.13.1/override/_main/calc.less b/test/Fixtures/lessjs-3.13.1/override/_main/calc.less new file mode 100644 index 00000000..8ac51dad --- /dev/null +++ b/test/Fixtures/lessjs-3.13.1/override/_main/calc.less @@ -0,0 +1,32 @@ +@val: 10px; +.no-math { + @c: 10px + 20px; + @calc: (@val + 30px); + root: calc(100% - @c); + root2: calc(100% - @calc); + @var: 50vh/2; + width: calc(50% + (@var - 20px)); + height: calc(50% + ((@var - 20px))); + min-height: calc(((10vh)) + calc((5vh))); + foo: 1 + 2 calc(3 + 4) 5 + 6; + @floor: floor(1 + .1); + bar: calc(@floor + 20%); +} + +.b { + @a: 10px; + @b: 10px; + + one: calc(100% - ((min(@a + @b)))); + two: calc(100% - (((@a + @b)))); + three: calc(e('100%') - (3 * 1)); + four: calc(~'100%' - (3 * 1)); + nested: calc(calc(2.25rem + 2px) - 1px * 2); +} + +.c { + @v: 10px; + height: calc(100% - ((@v * 3) + (@v * 2))); +} + +// TODO: provide support to parse `@p: .mk-map();` see less/_main/calc.less line 43 diff --git a/test/compare.php b/test/compare.php index 24bd2b64..20ac6d13 100644 --- a/test/compare.php +++ b/test/compare.php @@ -113,10 +113,15 @@ public function compare( string $fixtureDir, bool $useOverride ): void { $this->summaryUnsupported[] = basename( $lessFile ); continue; } - $overrideFile = $overrideDir ? "$overrideDir/$name.css" : null; - if ( $overrideFile && file_exists( $overrideFile ) ) { - $cssFile = $overrideFile; + $overrideCssFile = $overrideDir ? "$overrideDir/$name.css" : null; + if ( $overrideCssFile && file_exists( $overrideCssFile ) ) { + $cssFile = $overrideCssFile; } + $overrideLessFile = $overrideDir ? "$overrideDir/$name.less" : null; + if ( $overrideLessFile && file_exists( $overrideLessFile ) ) { + $lessFile = $overrideLessFile; + } + $this->handleFixture( $cssFile, $lessFile, $options ); } diff --git a/test/fixtures.php b/test/fixtures.php index 0fb7000b..0087dda0 100644 --- a/test/fixtures.php +++ b/test/fixtures.php @@ -77,7 +77,7 @@ 'lessjs-3.13.1' => [ 'lessDir' => "$fixtureDir/lessjs-3.13.1/less/_main", 'cssDir' => "$fixtureDir/lessjs-3.13.1/css/_main", - // 'overrideDir' => "$fixtureDir/lessjs-3.13.1/override", + 'overrideDir' => "$fixtureDir/lessjs-3.13.1/override/_main", 'unsupported' => [ // Permanently disabled, intentionally not supported. 'javascript', diff --git a/test/phpunit/FixturesTest.php b/test/phpunit/FixturesTest.php index 03a95356..d69f1ee4 100644 --- a/test/phpunit/FixturesTest.php +++ b/test/phpunit/FixturesTest.php @@ -9,7 +9,6 @@ class FixturesTest extends LessTestCase { 'parens' => true, ], 'lessjs-3.13.1' => [ - 'calc' => true, // New Feature 'functions' => true, 'functions-each' => true, 'import-reference-issues' => true, @@ -50,13 +49,21 @@ public static function provideFixtures() { foreach ( glob( "$cssDir/*.css" ) as $cssFile ) { $name = basename( $cssFile, '.css' ); $lessFile = "$lessDir/$name.less"; - $overrideFile = $overrideDir ? "$overrideDir/$name.css" : null; - if ( $overrideFile && file_exists( $overrideFile ) ) { - if ( file_get_contents( $overrideFile ) === file_get_contents( $cssFile ) ) { - print "WARNING: Redundant override for $overrideFile\n"; + $overrideCssFile = $overrideDir ? "$overrideDir/$name.css" : null; + if ( $overrideCssFile && file_exists( $overrideCssFile ) ) { + if ( file_get_contents( $overrideCssFile ) === file_get_contents( $cssFile ) ) { + print "WARNING: Redundant override for $overrideCssFile\n"; } - $cssFile = $overrideFile; + $cssFile = $overrideCssFile; } + $overrideLessFile = $overrideDir ? "$overrideDir/$name.less" : null; + if ( $overrideLessFile && file_exists( $overrideLessFile ) ) { + if ( file_get_contents( $overrideLessFile ) === file_get_contents( $lessFile ) ) { + print "WARNING: Redundant override for $overrideLessFile\n"; + } + $lessFile = $overrideLessFile; + } + if ( in_array( $name, $unsupported ) ) { continue; }