diff --git a/lib/Less/Tree.php b/lib/Less/Tree.php index f5ec816d..1a726cd6 100644 --- a/lib/Less/Tree.php +++ b/lib/Less/Tree.php @@ -64,6 +64,63 @@ public static function outputRuleset( $output, $rules ) { public function accept( $visitor ) { } + /** + * @param Less_Tree $a + * @param Less_Tree $b + * @return int|null + * @see less-2.5.3.js#Node.compare + */ + public static function nodeCompare( $a, $b ) { + // Less_Tree subclasses that implement compare() are: + // Anonymous, Color, Dimension, Keyword, Quoted, Unit + if ( $b instanceof Less_Tree_Quoted || $b instanceof Less_Tree_Anonymous ) { + // for "symmetric results" force toCSS-based comparison via b.compare() + // of Quoted or Anonymous if either value is one of those + return -$b->compare( $a ); + } elseif ( $a instanceof Less_Tree_Anonymous || $a instanceof Less_Tree_Color + || $a instanceof Less_Tree_Dimension || $a instanceof Less_Tree_Keyword + || $a instanceof Less_Tree_Quoted || $a instanceof Less_Tree_Unit + ) { + return $a->compare( $b ); + } elseif ( get_class( $a ) !== get_class( $b ) ) { + return null; + } + + // Less_Tree subclasses that have an array value: Less_Tree_Expression, Less_Tree_Value + // @phan-suppress-next-line PhanUndeclaredProperty + $aval = $a->value ?? []; + // @phan-suppress-next-line PhanUndeclaredProperty + $bval = $b->value ?? []; + if ( !( $a instanceof Less_Tree_Expression || $a instanceof Less_Tree_Value ) ) { + return $aval === $bval ? 0 : null; + } + if ( count( $aval ) !== count( $bval ) ) { + return null; + } + foreach ( $aval as $i => $item ) { + if ( self::nodeCompare( $item, $bval[$i] ) !== 0 ) { + return null; + } + } + return 0; + } + + /** + * @param string|float|int $a + * @param string|float|int $b + * @return int|null + * @see less-2.5.3.js#Node.numericCompare + */ + public static function numericCompare( $a, $b ) { + return $a < $b ? -1 + : ( $a === $b ? 0 + : ( $a > $b ? 1 + // NAN is not greater, less, or equal + : null + ) + ); + } + public static function ReferencedArray( $rules ) { foreach ( $rules as $rule ) { if ( method_exists( $rule, 'markReferenced' ) ) { diff --git a/lib/Less/Tree/Anonymous.php b/lib/Less/Tree/Anonymous.php index 381b808e..228d1039 100644 --- a/lib/Less/Tree/Anonymous.php +++ b/lib/Less/Tree/Anonymous.php @@ -31,19 +31,13 @@ public function compile( $env ) { return new self( $this->value, $this->index, $this->currentFileInfo, $this->mapLines ); } + /** + * @param Less_Tree|mixed $x + * @return int|null + * @see less-2.5.3.js#Anonymous.prototype.compare + */ public function compare( $x ) { - if ( !is_object( $x ) ) { - return -1; - } - - $left = $this->toCSS(); - $right = $x->toCSS(); - - if ( $left === $right ) { - return 0; - } - - return $left < $right ? -1 : 1; + return ( is_object( $x ) && $this->toCSS() === $x->toCSS() ) ? 0 : null; } public function isRulesetLike() { diff --git a/lib/Less/Tree/Color.php b/lib/Less/Tree/Color.php index e28dcb5c..0a1fedd6 100644 --- a/lib/Less/Tree/Color.php +++ b/lib/Less/Tree/Color.php @@ -191,15 +191,20 @@ public function toARGB() { return $this->toHex( $argb ); } + /** + * @param mixed $x + * @return int|null + * @see less-2.5.3.js#Color.prototype.compare + */ public function compare( $x ) { - if ( !property_exists( $x, 'rgb' ) ) { + if ( !$x instanceof self ) { return -1; } return ( $x->rgb[0] === $this->rgb[0] && $x->rgb[1] === $this->rgb[1] && $x->rgb[2] === $this->rgb[2] && - $x->alpha === $this->alpha ) ? 0 : -1; + $x->alpha === $this->alpha ) ? 0 : null; } public function toHex( $v ) { diff --git a/lib/Less/Tree/Condition.php b/lib/Less/Tree/Condition.php index dc230d66..073605fe 100644 --- a/lib/Less/Tree/Condition.php +++ b/lib/Less/Tree/Condition.php @@ -23,6 +23,11 @@ public function accept( $visitor ) { $this->rvalue = $visitor->visitObj( $this->rvalue ); } + /** + * @param Less_Environment $env + * @return bool + * @see less-2.5.3.js#Condition.prototype.eval + */ public function compile( $env ) { $a = $this->lvalue->compile( $env ); $b = $this->rvalue->compile( $env ); @@ -37,26 +42,18 @@ public function compile( $env ) { break; default: - if ( Less_Parser::is_method( $a, 'compare' ) ) { - $result = $a->compare( $b ); - } elseif ( Less_Parser::is_method( $b, 'compare' ) ) { - $result = $b->compare( $a ); - } else { - throw new Less_Exception_Compiler( 'Unable to perform comparison', null, $this->index ); - } - - switch ( $result ) { + switch ( Less_Tree::nodeCompare( $a, $b ) ) { case -1: $result = $this->op === '<' || $this->op === '=<' || $this->op === '<='; break; - case 0: $result = $this->op === '=' || $this->op === '>=' || $this->op === '=<' || $this->op === '<='; break; - case 1: $result = $this->op === '>' || $this->op === '>='; break; + default: + $result = false; } break; } diff --git a/lib/Less/Tree/Dimension.php b/lib/Less/Tree/Dimension.php index f4e87a50..33d8ebc8 100644 --- a/lib/Less/Tree/Dimension.php +++ b/lib/Less/Tree/Dimension.php @@ -110,32 +110,28 @@ public function operate( $op, $other ) { return new self( $value, $unit ); } + /** + * @param Less_Tree $other + * @return int|null + * @see less-2.5.3.js#Dimension.prototype.compare + */ public function compare( $other ) { - if ( $other instanceof self ) { - - if ( $this->unit->isEmpty() || $other->unit->isEmpty() ) { - $a = $this; - $b = $other; - } else { - $a = $this->unify(); - $b = $other->unify(); - if ( $a->unit->compare( $b->unit ) !== 0 ) { - return -1; - } - } - $aValue = $a->value; - $bValue = $b->value; + if ( !$other instanceof self ) { + return null; + } - if ( $bValue > $aValue ) { - return -1; - } elseif ( $bValue < $aValue ) { - return 1; - } else { - return 0; - } + if ( $this->unit->isEmpty() || $other->unit->isEmpty() ) { + $a = $this; + $b = $other; } else { - return -1; + $a = $this->unify(); + $b = $other->unify(); + if ( $a->unit->compare( $b->unit ) !== 0 ) { + return null; + } } + + return Less_Tree::numericCompare( $a->value, $b->value ); } public function unify() { diff --git a/lib/Less/Tree/Quoted.php b/lib/Less/Tree/Quoted.php index 7120160b..90335f8f 100644 --- a/lib/Less/Tree/Quoted.php +++ b/lib/Less/Tree/Quoted.php @@ -61,18 +61,19 @@ public function compile( $env ) { return new self( $this->quote . $r . $this->quote, $r, $this->escaped, $this->index, $this->currentFileInfo ); } - public function compare( $x ) { - if ( !Less_Parser::is_method( $x, 'toCSS' ) ) { - return -1; - } - - $left = $this->toCSS(); - $right = $x->toCSS(); - - if ( $left === $right ) { - return 0; + /** + * @param mixed $other + * @return int|null + * @see less-2.5.3.js#Quoted.prototype.compare + */ + public function compare( $other ) { + if ( $other instanceof self && !$this->escaped && !$other->escaped ) { + return Less_Tree::numericCompare( $this->value, $other->value ); + } else { + return ( + Less_Parser::is_method( $other, 'toCSS' ) + && $this->toCSS() === $other->toCSS() + ) ? 0 : null; } - - return $left < $right ? -1 : 1; } } diff --git a/test/Fixtures/less.php/css/T357160-quoted-compare.css b/test/Fixtures/less.php/css/T357160-quoted-compare.css new file mode 100644 index 00000000..04850b43 --- /dev/null +++ b/test/Fixtures/less.php/css/T357160-quoted-compare.css @@ -0,0 +1,8 @@ +double { + background: red; + background: blue; +} +single { + background: red; + background: blue; +} diff --git a/test/Fixtures/less.php/less/T357160-quoted-compare.less b/test/Fixtures/less.php/less/T357160-quoted-compare.less new file mode 100644 index 00000000..3ecccee9 --- /dev/null +++ b/test/Fixtures/less.php/less/T357160-quoted-compare.less @@ -0,0 +1,14 @@ +.test-mixin(@value) when (@value = "test") { + background: red; +} + +.test-mixin(@value) when (@value = 'test') { + background: blue; +} + +double { + .test-mixin("test") +} +single { + .test-mixin('test') +}