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 21, 2024
1 parent 344573c commit 49dbab2
Show file tree
Hide file tree
Showing 8 changed files with 429 additions and 8 deletions.
80 changes: 72 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\TransformCompiler;
use ScssPhp\ScssPhp\Util\Path;

/**
Expand Down Expand Up @@ -351,6 +352,20 @@ class Compiler
*/
private $warnedChildFunctions = [];

/**
* Optional file loader
*
* @var callable|null
*/
protected $fileLoader = null;

/**
* Used to handle transformations of the tree.
*
* @var TransformCompiler
*/
protected TransformCompiler $transformer;

/**
* Constructor
*
Expand All @@ -369,6 +384,7 @@ public function __construct($cacheOptions = null)
}

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

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

$this->pushCallStack('import ' . $this->getPrettyPath($path));
// see if tree is cached
$realPath = realpath($path);
Expand All @@ -5756,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 @@ -5765,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);
$parser = $this->parserFactory($path);
$tree = $parser->parse($code);
// Allow the custom file loaders
if ($this->fileLoader !== null) {
$code = call_user_func($this->fileLoader, $path);
} else {
$code = file_get_contents($path);
}

$this->importCache[$realPath] = $tree;
// Apply webpack-style transforms to the tree
if ($transforms) {
// Apply the named transformations
$tree = $this->transformer->applyTransformations($transforms, $path, $code, fn($path, $code) => $this->parserFactory($path)->parse($code));
} else {
$parser = $this->parserFactory($path);
$tree = $parser->parse($code);
}

$this->importCache[$cacheKey] = $tree;
}

$currentDirectory = $this->currentDirectory;
Expand Down Expand Up @@ -5817,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 @@ -5828,6 +5862,16 @@ public static function isCssImport($url)
*/
public function findImport($url, $currentDir = null)
{
$pos = strrpos($url, '!');
if ($pos !== false) {
[$path, $transforms] = $this->transformer->extractTransformsFromPath($url);
$result = $this->findImport($path, $currentDir);
if ($result === null) {
throw $this->error("`$path` file not found for @import");
}
return implode('!', $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 @@ -6250,7 +6294,7 @@ protected function handleImportLoop($name)
continue;
}

$file = $this->sourceNames[$env->block->sourceIndex] ?? '';
$file = $this->sourceNames[$env->block->sourceIndex] ?? null;

if ($file === null) {
continue;
Expand Down Expand Up @@ -10470,4 +10514,24 @@ 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 TransformCompiler $compiler
* @return void
*/
public function setTransformer(TransformCompiler $compiler): void
{
$this->transformer = $compiler;
}
}
33 changes: 33 additions & 0 deletions src/Transforms/Resource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

/**
* A resource represents the scss file that is in the process of transforming.
*/
interface Resource
{
/**
* Reads back the specific code if it's still in a readable state.
* If the AST has been generated an exception will be thrown instead.
*/
public function getCode(): string;

/**
* Set the CSS code to transform. This will delete the internal AST tree, marking it ready for parsing again.
*/
public function setCode(string $code): void;

/**
* Load the compiled tree.
*/
public function getAst(): Block;

public function setAst(Block $ast): void;

public function markASTModified(): void;

public function isASTOnly(): bool;
}
20 changes: 20 additions & 0 deletions src/Transforms/ResourceFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

/**
* A resource represents the scss file that is in the process of transforming.
* This factory can be used to replace specific instances of resources.
*/
interface ResourceFactory
{
/**
* Create a new instance of the resource based on the code & path provided.
*
* @param string $path
* @param string $code
* @param callable $astParserFactory
* @return Resource
*/
public function createResource(string $path, string $code, callable $astParserFactory): Resource;
}
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;

/**
* Contract for a transform action. Provided with the resource the transform can modify it in a chain.
*/
interface Transform
{
public function execute(Resource $resource): void;
}
67 changes: 67 additions & 0 deletions src/Transforms/TransformCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

/**
* Base class that applies the transformation actions, an extension to the compiler.
*/
class TransformCompiler
{
protected ResourceFactory $resourceFactory;

/**
* @param array<string, Transform> $transforms
*/
public function __construct(protected array $transforms = [], ?ResourceFactory $resourceFactory = null)
{
$this->resourceFactory = $resourceFactory ?? new TransformResourceFactory();
}

public function registerTransform(string $name, Transform $transform): void
{
$this->transforms[$name] = $transform;
}

/**
* @param string[] $transforms
* @param string $path
* @param string $code
* @param callable $astParserFactory
* @return Block
*/
public function applyTransformations(array $transforms, string $path, string $code, callable $astParserFactory): Block
{
// Make the resource
$resource = $this->resourceFactory->createResource($path, $code, $astParserFactory);

// 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
* @return array{0: string, 1: string[]}
*/
public function extractTransformsFromPath(string $path): array
{
$pos = strrpos($path, '!');
$transforms = [];

if ($pos !== false) {
$pathTransforms = substr($path, 0, $pos);
$transforms = !empty($pathTransforms) ? explode('!', $pathTransforms) : [];
$path = substr($path, $pos + 1);
}
return [$path, $transforms];
}
}
83 changes: 83 additions & 0 deletions src/Transforms/TransformResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

use ScssPhp\ScssPhp\Block;

/**
* A resource represents the scss file that is in the process of transforming.
*/
class TransformResource implements Resource
{
private bool $modified = false;
protected ?Block $ast = null;

/**
* @var callable
*/
protected $parserFactory;

public function __construct(protected string $path, protected string $code, callable $parserFactory)
{
$this->parserFactory = $parserFactory;
}

public function getCode(): string
{
if ($this->modified) {
throw new \Exception('Cannot access code as it is in AST only mode');
}

return $this->code;
}

public function setCode(string $code): void
{
$this->code = $code;
$this->ast = null;
$this->modified = false;
}

/**
* Return the modified Tree
*
* @return Block
*/
public function getAst(): Block
{
if ($this->ast === null) {
if (empty($this->code)) {
throw new \Exception('AST and source are both unavailable for this resource');
}

$this->ast = ($this->parserFactory)($this->path, $this->code);
}
return $this->ast;
}

public function setAst(Block $ast): void
{
$this->ast = $ast;
$this->markASTModified();
}

/**
* 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;
}
}
21 changes: 21 additions & 0 deletions src/Transforms/TransformResourceFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace ScssPhp\ScssPhp\Transforms;

/**
* Default factory to create a resource.
*/
class TransformResourceFactory implements ResourceFactory
{
/**
* Create a new instance of the resource based on the code & path provided.
* @param string $path
* @param string $code
* @param callable $astParserFactory
* @return Resource
*/
public function createResource(string $path, string $code, callable $astParserFactory): Resource
{
return new TransformResource($path, $code, $astParserFactory);
}
}
Loading

0 comments on commit 49dbab2

Please sign in to comment.