From 661e76877ea2dccce1e63b33e29f703727dd1824 Mon Sep 17 00:00:00 2001 From: Cody Finegan Date: Thu, 18 Apr 2024 16:25:15 +1200 Subject: [PATCH] TL-40011: Adding in webkit-style transformation support --- src/Compiler.php | 77 +++++++++++++++++++--- src/Transforms/Resource.php | 43 ++++++++++++ src/Transforms/ResourceFactory.php | 16 +++++ src/Transforms/Transform.php | 11 ++++ src/Transforms/Transformer.php | 57 ++++++++++++++++ tests/TransformerTest.php | 102 +++++++++++++++++++++++++++++ 6 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 src/Transforms/Resource.php create mode 100644 src/Transforms/ResourceFactory.php create mode 100644 src/Transforms/Transform.php create mode 100644 src/Transforms/Transformer.php create mode 100644 tests/TransformerTest.php diff --git a/src/Compiler.php b/src/Compiler.php index 398627ea..df7a3198 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -37,6 +37,7 @@ use ScssPhp\ScssPhp\Logger\StreamLogger; use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; +use ScssPhp\ScssPhp\Transforms\Transformer; use ScssPhp\ScssPhp\Util\Path; /** @@ -326,10 +327,8 @@ class Compiler * The directory of the currently processed file * * @var string|null - * - * Changed to protected by Totara */ - protected $currentDirectory; + private $currentDirectory; /** * The directory of the input file @@ -353,6 +352,20 @@ class Compiler */ private $warnedChildFunctions = []; + /** + * Optional file loader + * + * @var callable|null + */ + private $fileLoader = null; + + /** + * Used to handle transformations of the tree. + * + * @var Transformer + */ + private Transformer $transformer; + /** * Constructor * @@ -371,6 +384,7 @@ public function __construct($cacheOptions = null) } $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); + $this->transformer = new Transformer(); } /** @@ -5750,6 +5764,8 @@ public function addFeature($name) */ protected function importFile($path, OutputBlock $out) { + [$path, $transforms] = $this->transformer->extractTransformsFromPath($path, true); + $this->pushCallStack('import ' . $this->getPrettyPath($path)); // see if tree is cached $realPath = realpath($path); @@ -5758,6 +5774,8 @@ protected function importFile($path, OutputBlock $out) $realPath = $path; } + $cacheKey = ($transforms ? implode('!', $transforms) : '') . $realPath; + if (substr($path, -5) === '.sass') { $this->sourceIndex = \count($this->sourceNames); $this->sourceNames[] = $path; @@ -5767,16 +5785,28 @@ protected function importFile($path, OutputBlock $out) throw $this->error('The Sass indented syntax is not implemented.'); } - if (isset($this->importCache[$realPath])) { + if (isset($this->importCache[$cacheKey])) { $this->handleImportLoop($realPath); - $tree = $this->importCache[$realPath]; + $tree = $this->importCache[$cacheKey]; } else { - $code = file_get_contents($path); + // Allow the custom file loaders + if ($this->fileLoader !== null) { + $code = call_user_func($this->fileLoader, $path); + } else { + $code = file_get_contents($path); + } + $parser = $this->parserFactory($path); $tree = $parser->parse($code); - $this->importCache[$realPath] = $tree; + // Apply webpack-style transforms to the tree + if ($transforms) { + // Apply the named transformations + $tree = $this->transformer->applyTransformations($transforms, $path, $tree); + } + + $this->importCache[$cacheKey] = $tree; } $currentDirectory = $this->currentDirectory; @@ -5819,7 +5849,9 @@ public static function isCssImport($url) } /** - * Return the file path for an import url if it exists + * Return the file path for an import url if it exists. + * + * This includes an override supporting webkit transformations. * * @internal * @@ -5830,6 +5862,17 @@ public static function isCssImport($url) */ public function findImport($url, $currentDir = null) { + $pos = strrpos($url, '!'); + if ($pos !== false) { + $transforms = substr($url, 0, $pos); + $path = substr($url, $pos + 1); + $result = $this->findImport($path, $currentDir); + if ($result === null) { + throw $this->error("`$path` file not found for @import"); + } + return "$transforms!$result"; + } + // Vanilla css and external requests. These are not meant to be Sass imports. // Callback importers are still called for BC. if (self::isCssImport($url)) { @@ -10472,4 +10515,22 @@ protected function libScssphpGlob($args) return [Type::T_LIST, ',', $listParts]; } + + /** + * Set the file loader. + * + * @param callable|null $fileLoader + * @return void + */ + public function setFileLoader(?callable $fileLoader): void { + $this->fileLoader = $fileLoader; + } + + /** + * @param Transformer|null $transformer + * @return void + */ + public function setTransformer(?Transformer $transformer): void { + $this->transformer = $transformer; + } } diff --git a/src/Transforms/Resource.php b/src/Transforms/Resource.php new file mode 100644 index 00000000..8d347c32 --- /dev/null +++ b/src/Transforms/Resource.php @@ -0,0 +1,43 @@ +ast; + } + + /** + * Marks the AST has been modified + * + * @return void + */ + public function markASTModified(): void { + $this->modified = true; + } + + /** + * Indicates if the resource AST has been modified or not. + * + * @return bool + */ + public function isASTOnly(): bool { + return $this->modified; + } +} diff --git a/src/Transforms/ResourceFactory.php b/src/Transforms/ResourceFactory.php new file mode 100644 index 00000000..9ae4747d --- /dev/null +++ b/src/Transforms/ResourceFactory.php @@ -0,0 +1,16 @@ +factory = $factory; + } + + public function registerTransform(string $name, Transform $transform): void { + $this->transforms[$name] = $transform; + } + + public function applyTransformations(array $transforms, string $path, Block $tree): Block { + // Make the resource + $resource = ($this->factory ?? new ResourceFactory())->createResource($path, $tree); + + // transforms execute from right to left (like webpack) + $transforms = array_reverse($transforms, true); + foreach ($transforms as $name) { + if (!isset($this->transforms[$name])) { + throw new \Exception('Unknown transform "' . $name . '"'); + } + $this->transforms[$name]->execute($resource); + } + + return $resource->getAst(); + } + + /** + * @param string $path + * @param bool $split + * @return array + */ + public function extractTransformsFromPath(string $path, bool $split = false): array { + $pos = strrpos($path, '!'); + $transforms = ""; + + if ($pos !== false) { + $transforms = substr($path, 0, $pos); + $path = substr($path, $pos + 1); + } + + if ($split) { + $transforms = empty($transforms) ? [] : explode('!', $transforms); + } + + return [$path, $transforms]; + } +} diff --git a/tests/TransformerTest.php b/tests/TransformerTest.php new file mode 100644 index 00000000..b901ab88 --- /dev/null +++ b/tests/TransformerTest.php @@ -0,0 +1,102 @@ +extractTransformsFromPath('/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertEmpty($transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertSame('a', $transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!b!c!/a/b/c'); + $this->assertSame('/a/b/c', $path); + $this->assertSame('a!b!c', $transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('/a/b/c', true); + $this->assertSame('/a/b/c', $path); + $this->assertIsArray($transforms); + $this->assertEmpty($transforms, var_export($transforms, true)); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!/a/b/c', true); + $this->assertSame('/a/b/c', $path); + $this->assertIsArray($transforms); + $this->assertSame(['a'], $transforms); + + [$path, $transforms] = $transformer->extractTransformsFromPath('a!b!c!/a/b/c', true); + $this->assertSame('/a/b/c', $path); + $this->assertIsArray($transforms); + $this->assertSame(['a', 'b', 'c'], $transforms); + } + + /** + * Assert that a custom transformer can be applied + */ + public function testCustomTransform(): void { + $transform = new class() implements Transform { + public function execute(Resource $resource): void { + $resource->getAst()->children[0][1] = '/* TestTestD */'; + } + }; + + $transformer = new Transformer(); + $transformer->registerTransform('test', $transform); + + $compiler = new Compiler(); + $compiler->setOutputStyle(OutputStyle::EXPANDED); + $compiler->setTransformer($transformer); + + $dummyData = fn($url) => '/* TestC */ a { color: red; }'; + + $compiler->setImportPaths([$dummyData]); + $compiler->setFileLoader($dummyData); + + $input = '@import "test!my_file"; a { color: blue; }'; + $expected = <<compileString($input)->getCss(); + $this->assertSame($expected, $result); + + $compiler->setOutputStyle(OutputStyle::COMPRESSED); + $result = $compiler->compileString($input)->getCss(); + $this->assertSame('a{color:red}a{color:blue}', $result); + + $input = '@import "my_file"; a { color: blue; }'; + $compiler->setOutputStyle(OutputStyle::EXPANDED); + $expected = <<compileString($input)->getCss(); + $this->assertSame($expected, $result); + } + +}