Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 5 additions & 58 deletions src/Evaluators/ExpressionEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use DartSass\Parsers\Nodes\UnaryNode;
use DartSass\Parsers\Nodes\VariableNode;
use DartSass\Utils\ArithmeticCalculator;
use DartSass\Utils\SpreadHelper;
use DartSass\Utils\StringFormatter;
use DartSass\Utils\ValueComparator;
use DartSass\Values\SassList;
Expand Down Expand Up @@ -340,68 +341,14 @@ private function evaluateUrlFunction(array $args): mixed

private function evaluateStandardFunction(string $name, array $args): mixed
{
$args = $this->evaluateArguments($args);

if ($this->hasSpreadArguments($args)) {
$args = $this->expandSpreadArguments($args);
}
$args = SpreadHelper::expand(
$this->evaluateArguments($args),
$this->evaluate(...)
);

return $this->context->functionHandler->call($name, $args);
}

private function isSpreadArgument(mixed $arg): bool
{
return is_array($arg) && isset($arg['type']) && $arg['type'] === 'spread';
}

private function hasSpreadArguments(array $args): bool
{
foreach ($args as $arg) {
if ($this->isSpreadArgument($arg)) {
return true;
}
}

return false;
}

private function expandSpreadArguments(array $args): array
{
$processedArgs = [];

foreach ($args as $arg) {
if ($this->isSpreadArgument($arg)) {
$spreadValue = $this->evaluate($arg['value']);
$processedArgs = $this->appendSpreadValues($processedArgs, $spreadValue);
} else {
$processedArgs[] = $arg;
}
}

return $processedArgs;
}

private function appendSpreadValues(array $processedArgs, mixed $spreadValue): array
{
if ($spreadValue instanceof SassList) {
foreach ($spreadValue->value as $item) {
$processedArgs[] = $item;
}
} elseif ($spreadValue instanceof ListNode) {
foreach ($spreadValue->values as $item) {
$processedArgs[] = $this->evaluate($item);
}
} elseif (is_array($spreadValue)) {
foreach ($spreadValue as $item) {
$processedArgs[] = $this->evaluate($item);
}
} else {
$processedArgs[] = $spreadValue;
}

return $processedArgs;
}

private function hasSlashSeparator(array $args): bool
{
if (empty($args)) {
Expand Down
8 changes: 5 additions & 3 deletions src/Evaluators/UserFunctionEvaluator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
use DartSass\Parsers\Nodes\ListNode;
use DartSass\Parsers\Nodes\NodeType;
use DartSass\Parsers\Nodes\OperationNode;
use DartSass\Utils\SpreadHelper;
use DartSass\Values\SassList;

use function array_slice;
use function count;
use function is_array;
use function is_object;
Expand Down Expand Up @@ -49,8 +49,9 @@ private function processArguments(
$default = $arg['default'] ?? null;

if ($arbitrary) {
$remainingArgs = array_slice($callArgs, $argIndex);
$this->environment->getCurrentScope()->setVariable($paramName, new ListNode($remainingArgs));
$restArgs = SpreadHelper::collect($callArgs, []);

$this->environment->getCurrentScope()->setVariable($paramName, $restArgs);

break;
} else {
Expand All @@ -60,6 +61,7 @@ private function processArguments(
}

$this->environment->getCurrentScope()->setVariable($paramName, $value);

$argIndex++;
}
}
Expand Down
29 changes: 2 additions & 27 deletions src/Handlers/MixinHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@
use DartSass\Compilers\CompilerContext;
use DartSass\Parsers\Nodes\AstNode;
use DartSass\Parsers\Nodes\IdentifierNode;
use DartSass\Utils\SpreadHelper;
use DartSass\Values\SassList;
use DartSass\Values\SassMap;
use Throwable;

use function array_key_exists;
use function array_key_first;
use function count;
use function in_array;
use function is_array;
use function is_int;
use function md5;
use function preg_replace;
use function serialize;
use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function substr;
use function trim;

Expand Down Expand Up @@ -139,29 +136,7 @@ private function bindArguments(array $mixinArgs, array $arguments): void
private function bindSpreadArgument(string $argName, array $arguments, array $usedKeys): void
{
$varName = substr($argName, 0, -3);

$remainingKeywords = [];
$remainingPositional = [];

foreach ($arguments as $key => $val) {
if (in_array($key, $usedKeys, true)) {
continue;
}

if (is_int($key)) {
$remainingPositional[] = $val;
} else {
$keyStr = $key;

if (str_starts_with($keyStr, '$')) {
$keyStr = substr($keyStr, 1);
}

$remainingKeywords[$keyStr] = $val;
}
}

$value = empty($remainingKeywords) ? new SassList($remainingPositional) : new SassMap($remainingKeywords);
$value = SpreadHelper::collectWithKeywords($arguments, $usedKeys);

$this->context->variableHandler->define($varName, $value);
}
Expand Down
101 changes: 101 additions & 0 deletions src/Utils/SpreadHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace DartSass\Utils;

use DartSass\Parsers\Nodes\ListNode;
use DartSass\Values\SassList;
use DartSass\Values\SassMap;

use function in_array;
use function is_array;
use function is_int;
use function is_string;
use function str_starts_with;
use function substr;

final class SpreadHelper
{
public static function isSpread(mixed $arg): bool
{
return is_array($arg) && isset($arg['type']) && $arg['type'] === 'spread';
}

public static function expand(array $args, callable $evaluate): array
{
$result = [];

foreach ($args as $key => $arg) {
if (self::isSpread($arg)) {
$spreadValue = $evaluate($arg['value']);

$result = self::merge($result, $spreadValue, $evaluate);
} else {
if (is_string($key) && str_starts_with($key, '$')) {
$result[$key] = $arg;
} else {
$result[] = $arg;
}
}
}

return $result;
}

public static function collect(array $args, array $usedKeys): SassList
{
[$pos] = self::filter($args, $usedKeys);

return new SassList($pos);
}

public static function collectWithKeywords(array $args, array $usedKeys): SassList|SassMap
{
[$pos, $keywords] = self::filter($args, $usedKeys);

return empty($keywords) ? new SassList($pos) : new SassMap($keywords);
}

private static function filter(array $args, array $usedKeys): array
{
$pos = $keywords = [];

foreach ($args as $key => $val) {
if (in_array($key, $usedKeys, true)) {
continue;
}

if (is_int($key)) {
$pos[] = $val;
} else {
$keyStr = str_starts_with($key, '$') ? substr($key, 1) : $key;

$keywords[$keyStr] = $val;
}
}

return [$pos, $keywords];
}

private static function merge(array $result, mixed $spreadValue, callable $evaluate): array
{
if ($spreadValue instanceof SassList) {
foreach ($spreadValue->value as $item) {
$result[] = $item;
}
} elseif ($spreadValue instanceof ListNode) {
foreach ($spreadValue->values as $item) {
$result[] = $evaluate($item);
}
} elseif (is_array($spreadValue)) {
foreach ($spreadValue as $item) {
$result[] = $evaluate($item);
}
} else {
$result[] = $spreadValue;
}

return $result;
}
}
48 changes: 6 additions & 42 deletions tests/Unit/Evaluators/ExpressionEvaluatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use DartSass\Parsers\Nodes\AstNode;
use DartSass\Parsers\Nodes\FunctionNode;
use DartSass\Parsers\Nodes\IdentifierNode;
use DartSass\Parsers\Nodes\ListNode;
use DartSass\Parsers\Nodes\NumberNode;
use DartSass\Parsers\Nodes\OperationNode;
use DartSass\Parsers\Nodes\OperatorNode;
Expand All @@ -21,6 +20,7 @@
use DartSass\Parsers\Nodes\StringNode;
use DartSass\Parsers\Nodes\UnaryNode;
use DartSass\Parsers\Nodes\VariableNode;
use DartSass\Utils\SpreadHelper;
use DartSass\Values\SassList;
use Tests\ReflectionAccessor;

Expand Down Expand Up @@ -225,15 +225,15 @@

describe('expandSpreadArguments()', function () {
it('handles empty array', function () {
$result = $this->accessor->callMethod('expandSpreadArguments', [[]]);
$result = SpreadHelper::expand([], fn($x) => $x);

expect($result)->toBe([]);
});

it('handles regular args without spread', function () {
$args = ['arg1', 'arg2'];

$result = $this->accessor->callMethod('expandSpreadArguments', [$args]);
$result = SpreadHelper::expand($args, fn($x) => $x);

expect($result)->toBe(['arg1', 'arg2']);
});
Expand All @@ -243,7 +243,7 @@

$args = [['type' => 'spread', 'value' => $spreadValue]];

$result = $this->accessor->callMethod('expandSpreadArguments', [$args]);
$result = SpreadHelper::expand($args, fn($x) => $x);

expect($result)->toBe(['a', 'b', 'c']);
});
Expand All @@ -257,7 +257,7 @@
['type' => 'spread', 'value' => $spread2],
];

$result = $this->accessor->callMethod('expandSpreadArguments', [$args]);
$result = SpreadHelper::expand($args, fn($x) => $x);

expect($result)->toBe(['a', 'b', 'c']);
});
Expand All @@ -267,49 +267,13 @@

$args = ['arg1', ['type' => 'spread', 'value' => $spreadValue], 'arg2'];

$result = $this->accessor->callMethod('expandSpreadArguments', [$args]);
$result = SpreadHelper::expand($args, fn($x) => $x);

expect($result)->toBe(['arg1', 'x', 'y', 'arg2']);
});
});

describe('appendSpreadValues()', function () {
it('handles SassList', function () {
$sassList = new SassList(['item1', 'item2'], 'comma');
$processedArgs = ['existing'];

$result = $this->accessor->callMethod('appendSpreadValues', [$processedArgs, $sassList]);

expect($result)->toBe(['existing', 'item1', 'item2']);
});

it('handles ListNode', function () {
$listNode = new ListNode(['node1', 'node2']);
$processedArgs = ['existing'];

$result = $this->accessor->callMethod('appendSpreadValues', [$processedArgs, $listNode]);

expect($result)->toBe(['existing', 'node1', 'node2']);
});

it('handles array', function () {
$array = ['elem1', 'elem2'];
$processedArgs = ['existing'];

$result = $this->accessor->callMethod('appendSpreadValues', [$processedArgs, $array]);

expect($result)->toBe(['existing', 'elem1', 'elem2']);
});

it('handles non-array value', function () {
$value = 'single';
$processedArgs = ['existing'];

$result = $this->accessor->callMethod('appendSpreadValues', [$processedArgs, $value]);

expect($result)->toBe(['existing', 'single']);
});
});

describe('evaluateVariableString()', function () {
it('successfully accesses property with namespace', function () {
Expand Down
Loading