Skip to content

Commit

Permalink
TL-40011: Adding in webkit-style transformation support
Browse files Browse the repository at this point in the history
  • Loading branch information
codyfinegan committed Apr 18, 2024
1 parent 022cbf1 commit 661e768
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 8 deletions.
77 changes: 69 additions & 8 deletions src/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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
Expand All @@ -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
*
Expand All @@ -371,6 +384,7 @@ public function __construct($cacheOptions = null)
}

$this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
$this->transformer = new Transformer();
}

/**
Expand Down Expand Up @@ -5750,6 +5764,8 @@ public function addFeature($name)
*/
protected function importFile($path, OutputBlock $out)
{
[$path, $transforms] = $this->transformer->extractTransformsFromPath($path, true);

Check failure on line 5767 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Offset 0 does not exist on array<string, array|string>.

Check failure on line 5767 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Offset 1 does not exist on array<string, array|string>.

Check failure on line 5767 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Offset 0 does not exist on array<string, array|string>.

Check failure on line 5767 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Offset 1 does not exist on array<string, array|string>.

$this->pushCallStack('import ' . $this->getPrettyPath($path));
// see if tree is cached
$realPath = realpath($path);
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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
*
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;

Check failure on line 10534 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Property ScssPhp\ScssPhp\Compiler::$transformer (ScssPhp\ScssPhp\Transforms\Transformer) does not accept ScssPhp\ScssPhp\Transforms\Transformer|null.

Check failure on line 10534 in src/Compiler.php

View workflow job for this annotation

GitHub Actions / Static analysis

Property ScssPhp\ScssPhp\Compiler::$transformer (ScssPhp\ScssPhp\Transforms\Transformer) does not accept ScssPhp\ScssPhp\Transforms\Transformer|null.
}
}
43 changes: 43 additions & 0 deletions src/Transforms/Resource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

/**
* A resource represents the scss file that is in the process of transforming.
*/
class Resource {
private bool $modified = false;

public function __construct(protected string $path, protected Block $ast) {

}

/**
* Return the modified Tree
*
* @return Block
*/
public function getAst(): Block {
return $this->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;
}
}
16 changes: 16 additions & 0 deletions src/Transforms/ResourceFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

class ResourceFactory {
/**
* Create a new resource instance to be used in transformations.
*
* @return Resource
*/
public function createResource(string $path, Block $ast): Resource {
return new Resource($path, $ast);
}
}
11 changes: 11 additions & 0 deletions src/Transforms/Transform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

interface Transform {
/**
* @param Resource $resource
* @return void
*/
public function execute(\ScssPhp\ScssPhp\Transforms\Resource $resource): void;
}
57 changes: 57 additions & 0 deletions src/Transforms/Transformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

class Transformer {

public function __construct(protected ?ResourceFactory $factory = null, protected array $transforms = []) {

}

public function setResourceFactory(?ResourceFactory $factory): void {
$this->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<string, array|string>
*/
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];
}
}
102 changes: 102 additions & 0 deletions tests/TransformerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace ScssPhp\ScssPhp\Tests;

use PHPUnit\Framework\TestCase;
use ScssPhp\ScssPhp\Compiler;
use ScssPhp\ScssPhp\OutputStyle;
use ScssPhp\ScssPhp\Transforms\Resource;
use ScssPhp\ScssPhp\Transforms\Transform;
use ScssPhp\ScssPhp\Transforms\Transformer;

class TransformerTest extends TestCase {

public function testExtractTransformsFromPath(): void {
$transformer = new Transformer();

[$path, $transforms] = $transformer->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 = <<<HERE
/* TestTestD */
a {
color: red;
}
a {
color: blue;
}
HERE;

$result = $compiler->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 = <<<HERE
/* TestC */
a {
color: red;
}
a {
color: blue;
}
HERE;
$result = $compiler->compileString($input)->getCss();
$this->assertSame($expected, $result);
}

}

0 comments on commit 661e768

Please sign in to comment.