Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/support asset collector #43

Open
wants to merge 6 commits into
base: legacy
Choose a base branch
from
Open
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
87 changes: 87 additions & 0 deletions Classes/Compiler/ScssCompiler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php
namespace WapplerSystems\WsScss\Compiler;


use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ScssCompiler
{

/**
* Compiling Scss with scss
*
* @param string $scssFilename Existing scss file absolute path
* @param string $cssFilename File to be written with compiled CSS
* @param array $vars Variables to compile
* @param boolean $showLineNumber Show line numbers
* @param string $formatter name
* @param string $cssRelativeFilename
* @param boolean $useSourceMap Use SourceMap
* @return string
* @throws \BadFunctionCallException
* @throws \ScssPhp\ScssPhp\Exception\CompilerException
*/
public static function compileScss(
string $scssFilename,
string $cssFilename,
array $vars = [],
bool $showLineNumber = false,
$formatter = null,
$cssRelativeFilename = null,
bool $useSourceMap = false): string {

if (!class_exists(\ScssPhp\ScssPhp\Version::class, true)) {
$extPath = ExtensionManagementUtility::extPath('ws_scss');
require_once $extPath . 'Resources/Private/scssphp/scss.inc.php';
}
$sitePath = Environment::getPublicPath() . '/';

$cacheDir = $sitePath . 'typo3temp/assets/css/cache/';

if (!is_dir($cacheDir)) {
GeneralUtility::mkdir_deep($cacheDir);
}

if (!is_writable($cacheDir)) {
// TODO: Error message
return '';
}

$cacheOptions = [
'cacheDir' => $cacheDir,
'prefix' => md5($cssFilename),
];
GeneralUtility::mkdir_deep($cacheOptions['cacheDir']);
$parser = new \ScssPhp\ScssPhp\Compiler($cacheOptions);
if (file_exists($scssFilename)) {

$parser->setVariables($vars);

if ($showLineNumber) {
$parser->setLineNumberStyle(\ScssPhp\ScssPhp\Compiler::LINE_COMMENTS);
}
if ($formatter !== null) {
$parser->setFormatter($formatter);
}

if ($useSourceMap) {
$parser->setSourceMap(\ScssPhp\ScssPhp\Compiler::SOURCE_MAP_INLINE);

$parser->setSourceMapOptions([
'sourceMapWriteTo' => $cssFilename . '.map',
'sourceMapURL' => $cssRelativeFilename . '.map',
'sourceMapBasepath' => $sitePath,
'sourceMapRootpath' => '/',
]);
}

$css = $parser->compile('@import "' . $scssFilename . '";');
GeneralUtility::writeFile($cssFilename, $css);
return $css;
}
return '';
}

}
206 changes: 206 additions & 0 deletions Classes/Compiler/ScssResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
<?php

namespace WapplerSystems\WsScss\Compiler;

use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\DebugUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class ScssResolver
{

/**
* @var FrontendInterface
*/
private $cache;

private static $defaultOutputDir = 'typo3temp/assets/css/';

private static $visitedFiles = [];

public function __construct(FrontendInterface $cache) {
$this->cache = $cache;
}

private static function calcCacheKey(string $cssRelativeFilename) {
return hash('sha1', $cssRelativeFilename);
}

/**
* Calculating content hash to detect changes
*
* @param string $scssFilename Existing scss file absolute path
* @param string $vars
* @return string
*/
protected function calculateContentHash(string $scssFilename, string $vars = ''): string {
if (\in_array($scssFilename, self::$visitedFiles, true)) {
return '';
}
self::$visitedFiles[] = $scssFilename;

$content = file_get_contents($scssFilename);
$pathinfo = pathinfo($scssFilename);

$hash = hash('sha1', $content);
if ($vars !== '') {
$hash = hash('sha1', $hash . $vars);
} // hash variables too

$imports = self::collectImports($content);
foreach ($imports as $import) {
$hashImport = '';


if (file_exists($pathinfo['dirname'] . '/' . $import . '.scss')) {
$hashImport = $this->calculateContentHash($pathinfo['dirname'] . '/' . $import . '.scss');
} else {
$parts = explode('/', $import);
$filename = '_' . array_pop($parts);
$parts[] = $filename;
if (file_exists($pathinfo['dirname'] . '/' . implode('/', $parts) . '.scss')) {
$hashImport = $this->calculateContentHash($pathinfo['dirname'] . '/' . implode('/',
$parts) . '.scss');
}
}
if ($hashImport !== '') {
$hash = hash('sha1', $hash . $hashImport);
}
}

return $hash;
}

/**
* Collect all @import files in the given content.
*
* @param string $content
* @return array
*/
private static function collectImports(string $content): array {
$matches = [];
$imports = [];

preg_match_all('/@import([^;]*);/', $content, $matches);

foreach ($matches[1] as $importString) {
$files = explode(',', $importString);

array_walk($files, static function (string &$file) {
$file = trim($file, " \t\n\r\0\x0B'\"");
});

$imports = array_merge($imports, $files);
}

return $imports;
}

public function resolve(string $file, $outputDir = null, $formatter = null, array $variables = [], $showLineNumber = false, $useSourceMap = false, $outputFile = null, $inline = false): ?array {
$sitePath = Environment::getPublicPath() . '/';
$pathInfo = pathinfo($file);
$filename = $pathInfo['filename'];
if (empty($outputDir)) {
$outputDir = self::$defaultOutputDir;
}

if ($outputFile !== null) {
$outputDir = \dirname($outputFile);
$filename = basename($outputFile);
}

$outputDir = (substr($outputDir, -1) === '/') ? $outputDir : $outputDir . '/';

if (!strcmp(substr($outputDir, 0, 4), 'EXT:')) {
[$extKey, $script] = explode('/', substr($outputDir, 4), 2);
if ($extKey && ExtensionManagementUtility::isLoaded($extKey)) {
$extPath = ExtensionManagementUtility::extPath($extKey);
$outputDir = substr($extPath, \strlen($sitePath)) . $script;
}
}


$scssFilename = GeneralUtility::getFileAbsFileName($file);

// create filename - hash is important due to the possible
// conflicts with same filename in different folders
GeneralUtility::mkdir_deep($sitePath . $outputDir);
$fileEnding = (substr($filename,-4) === '.css') ? '' : '.css';
if ($outputFile === null) {
$variablesCount = \count($variables);
$variablesHash = $variablesCount > 0 ? hash('md5', implode(',', $variables)) : null;
$variablesHashString = $variablesCount > 0 ? '_' . $variablesHash : '';
$fileNameOutputString = ($outputDir === self::$defaultOutputDir) ? '_' . hash('sha1', $file) : $variablesHashString;

$cssRelativeFilename = $outputDir . $filename . $fileNameOutputString . $fileEnding;
} else {
$cssRelativeFilename = $outputDir . $filename . $fileEnding;
}


$cssFilename = $sitePath . $cssRelativeFilename;

$cacheKey = self::calcCacheKey($cssRelativeFilename);
$contentHash = $this->calculateContentHash($scssFilename, implode(',', $variables));
if ($showLineNumber) {
$contentHash .= 'l1';
}
if ($useSourceMap) {
$contentHash .= 'sm';
}
$contentHash .= $formatter;

$contentHashCache = '';
if ($this->cache->has($cacheKey)) {
$contentHashCache = $this->cache->get($cacheKey);
}


$css = $this->compile($scssFilename, $cssFilename, $contentHashCache, $contentHash, $cacheKey, $variables, $showLineNumber, $formatter, $cssRelativeFilename, $useSourceMap);
// error
if ($css === null) {
return null;
}
if ($inline) {
if ($css === '') {
$css = file_get_contents($cssFilename);
}
return [$cssRelativeFilename, $css];
}
return [$cssRelativeFilename, null];
}

private function compile(string $scssFilename,
string $cssFilename,
string $contentHashCache,
string $contentHash,
string $cacheKey,
array $variables = [],
bool $showLineNumber = false,
$formatter = null,
$cssRelativeFilename = null,
bool $useSourceMap = false): ?string {
$css = '';

try {
if ($contentHashCache === '' || $contentHashCache !== $contentHash) {
$css = ScssCompiler::compileScss($scssFilename, $cssFilename, $variables, $showLineNumber, $formatter, $cssRelativeFilename, $useSourceMap);

$this->cache->set($cacheKey, $contentHash, ['scss'], 0);
}
return $css;
} catch (\Exception $ex) {
DebugUtility::debug($ex->getMessage());

/** @var $logger Logger */
$logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
$logger->error($ex->getMessage());
}
return null;
}

}
44 changes: 44 additions & 0 deletions Classes/EventListener/CompileScssAssets.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace WapplerSystems\WsScss\EventListener;

use \TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use WapplerSystems\WsScss\Compiler\ScssResolver;

class CompileScssAssets
{

public function __invoke(BeforeStylesheetsRenderingEvent $event): void {
if ($event->isInline()) {
return;
}
$assetCollector = $event->getAssetCollector();
$assets = $assetCollector->getStyleSheets($event->isPriority());

foreach ($assets as $identifier => $asset) {
if (!empty($asset['source'])) {
$pathInfo = pathinfo($asset['source']);
if ($pathInfo['extension'] === 'scss') {
$assetCollector->removeStyleSheet($identifier);
/** @var ScssResolver $scssResolver */
$scssResolver = GeneralUtility::makeInstance(ScssResolver::class);
$resolved = $scssResolver->resolve(
$asset['source'],
null,
'WapplerSystems\WsScss\Formatter\Autoprefixer',
[],
false,
false,
null,
false);
if ($resolved === null) {
// error remove
return;
}
$assetCollector->addStyleSheet($identifier, $resolved[0], $asset['attributes'], $asset['options']);
}
}
}
}
}
Loading