first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

966
class/sacy/sacy.php Normal file
View File

@@ -0,0 +1,966 @@
<?php
namespace sacy;
if (!defined('____SACY_BUNDLED')) {
include_once implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'ext-translators.php']);
}
if (!class_exists('JSMin') && !ExternalProcessorRegistry::typeIsSupported('text/javascript')) {
include_once implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'jsmin.php']);
}
if (!class_exists('Minify_CSS')) {
include_once implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'cssmin.php']);
}
if (!class_exists('lessc') && !ExternalProcessorRegistry::typeIsSupported('text/x-less')) {
$less = implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'lessc.inc.php']);
if (file_exists($less)) {
include_once $less;
}
}
if (function_exists('CoffeeScript\compile')) {
include_once implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'coffeescript.php']);
} elseif (!ExternalProcessorRegistry::typeIsSupported('text/coffeescript')) {
$coffee = implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), '..', 'coffeescript', 'coffeescript.php']);
if (file_exists($coffee)) {
include_once $coffee;
include_once implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), 'coffeescript.php']);
}
}
if (!class_exists('SassParser') && !ExternalProcessorRegistry::typeIsSupported('text/x-sass')) {
$sass = implode(DIRECTORY_SEPARATOR, [dirname(__FILE__), '..', 'sass', 'SassParser.php']);
if (file_exists($sass)) {
include_once $sass;
}
}
class Exception extends \Exception
{
}
/*
* An earlier experiment contained a real framework for tag
* and parser registration. In the end, this turned out
* to be much too complex if we just need to support two tags
* for two types of resources.
*/
class WorkUnitExtractor
{
private $_cfg;
public function __construct(Config $config)
{
$this->_cfg = $config;
}
public function getAcceptedWorkUnits($tags)
{
$work_units = [];
foreach ($tags as $tag) {
$r = $this->workUnitFromTag($tag['tag'], $tag['attrdata'], $tag['content']);
if ($r === false) {
continue;
} // handler has declined
$r = array_merge($r, [
'page_order' => $tag['page_order'],
'position' => $tag['index'],
'length' => strlen($tag['tagdata']),
'tag' => $tag['tag'],
]);
$work_units[] = $r;
}
return $work_units;
}
public function workUnitFromTag($tag, $attrdata, $content)
{
switch ($tag) {
case 'link':
case 'style':
$fn = 'extract_style_unit';
break;
case 'script':
$fn = 'extract_script_unit';
break;
default:
throw new Exception("Cannot handle tag: ({$tag})");
}
return $this->$fn($tag, $attrdata, $content);
}
private function extract_attrs($attstr)
{
// The attribute name regex is too relaxed, but let's
// compromise and keep it simple.
$attextract = '#([a-z\-]+)\s*=\s*(["\'])\s*(.*?)\s*\2#';
if (!preg_match_all($attextract, $attstr, $m)) {
return false;
}
$res = [];
foreach ($m[1] as $idx => $name) {
$res[strtolower($name)] = $m[3][$idx];
}
return $res;
}
private function urlToFile($ref)
{
$u = parse_url($ref);
if ($u === false) {
return false;
}
if (isset($u['host']) || isset($u['scheme'])) {
return $ref;
}
if ($this->_cfg->get('query_strings') == 'ignore') {
if (isset($u['query'])) {
return false;
}
}
$ref = $u['path'];
$path = [$_SERVER['DOCUMENT_ROOT']];
if (isAdministration()) {
$cfg = \KupShop\KupShopBundle\Config::get();
$path[] = $cfg['Path']['admin'];
}
$path[] = $ref;
return realpath(implode(DIRECTORY_SEPARATOR, $path));
}
private function extract_style_unit($tag, $attrdata, $content)
{
$attrs = $this->extract_attrs($attrdata);
$attrs['type'] = strtolower($attrs['type'] ?? '');
// invalid markup
if ($tag == 'link' && !empty($content)) {
return false;
}
if ($tag == 'style' && empty($content)) {
return false;
}
if ($tag == 'link' && empty($attrs['href'])) {
return false;
}
// not a stylesheet
if ($tag == 'link' && strtolower($attrs['rel']) != 'stylesheet') {
return false;
}
// type attribute required
if (!isset($attrs['type'])) {
return false;
}
// not one of the supported types
if (!in_array(strtolower($attrs['type']), CssRenderHandler::supportedTransformations())) {
return false;
}
// in debug mode 3, only transform
if ($this->_cfg->getDebugMode() == 3
&& !CssRenderHandler::willTransformType($attrs['type'])
) {
return false;
}
if (!isset($attrs['media'])) {
$attrs['media'] = '';
}
$include = null;
if (isset($attrs['include'])) {
$include = explode(';', $attrs['include']);
}
$path = null;
if (empty($content)) {
$path = $this->urlToFile($attrs['href']);
if ($path === false) {
return false;
}
}
$group = serialize($this->_cfg->get('merge_tags') ? [$attrs['media'], $attrs['type']] : [$attrs['media']]);
return [
'group' => $group,
'file' => $path,
'content' => $content,
'type' => $attrs['type'],
'paths' => $include,
];
}
private function validTag($attrs)
{
$types = array_merge(['text/javascript', 'application/javascript'], JavaScriptRenderHandler::supportedTransformations());
return in_array($attrs['type'], $types);
}
private function extract_script_unit($tag, $attrdata, $content)
{
$attrs = $this->extract_attrs($attrdata);
if (empty($attrs['type'])) {
$attrs['type'] = 'text/javascript';
}
$attrs['type'] = strtolower($attrs['type']);
if ($this->_cfg->getDebugMode() == 3
&& !JavaScriptRenderHandler::willTransformType($attrs['type'])
) {
return false;
}
if ($this->validTag($attrs)) {
$path = null;
if (!$content) {
$path = $this->urlToFile($attrs['src']);
if ($path === false) {
return false;
}
}
return [
'group' => '',
'content' => $content,
'file' => $path,
'data' => $this->parseDataAttrs($attrs),
'type' => $attrs['type'],
];
}
return false;
}
private function parseDataAttrs($attrs)
{
$data = [];
foreach ($attrs as $key => $value) {
// Compromising again here on the valid
// format of the attr key, to keep the
// regex simple.
if (preg_match('#^data-([a-z\-]+)$#', $key, $match)) {
$name = $match[1];
$data[$name] = $value;
}
}
return $data;
}
}
class Config
{
private $params;
public function get($key)
{
return $this->params[$key] ?? null;
}
public function __construct($params = null)
{
$this->params['query_strings'] = defined('SACY_QUERY_STRINGS') ? SACY_QUERY_STRINGS : 'ignore';
$this->params['write_headers'] = defined('SACY_WRITE_HEADERS') ? SACY_WRITE_HEADERS : true;
$this->params['debug_toggle'] = defined('SACY_DEBUG_TOGGLE') ? SACY_DEBUG_TOGGLE : '_sacy_debug';
$this->params['merge_tags'] = false;
if (is_array($params)) {
$this->setParams($params);
}
}
public function getDebugMode()
{
if ($this->params['debug_toggle'] === false) {
return 0;
}
if (isset($_GET[$this->params['debug_toggle']])) {
return intval($_GET[$this->params['debug_toggle']]);
}
if (isset($_COOKIE[$this->params['debug_toggle']])) {
return intval($_COOKIE[$this->params['debug_toggle']]);
}
if (isDevelopment()) {
return 3;
}
return 0;
}
public function setParams($params)
{
foreach ($params as $key => $value) {
if (!in_array($key, ['merge_tags', 'query_strings', 'write_headers', 'debug_toggle', 'block_ref'])) {
throw new Exception("Invalid option: {$key}");
}
}
if (isset($params['query_strings']) && !in_array($params['query_strings'], ['force-handle', 'ignore'])) {
throw new Exception('Invalid setting for query_strings: '.$params['query_strings']);
}
if (isset($params['write_headers']) && !in_array($params['write_headers'], [true, false], true)) {
throw new Exception('Invalid setting for write_headers: '.$params['write_headers']);
}
$params['merge_tags'] = (bool) getVal('merge_tags', $params);
$this->params = array_merge($this->params, $params);
}
}
class CacheRenderer
{
private $_cfg;
private $_source_file;
/** @var FileCache */
private $fragment_cache;
private $rendered_bits;
public function __construct(Config $config, $source_file)
{
$this->_cfg = $config;
$this->_source_file = $source_file;
$this->rendered_bits = [];
$class = defined('SACY_FRAGMENT_CACHE_CLASS') ?
SACY_FRAGMENT_CACHE_CLASS :
'sacy\FileCache';
$this->fragment_cache = new $class();
foreach (['get', 'set'] as $m) {
if (!method_exists($this->fragment_cache, $m)) {
throw new Exception('Invalid fragment cache class specified');
}
}
}
public function allowMergedTransformOnly($tag)
{
return $tag == 'script';
}
public function renderWorkUnits($tag, $cat, $work_units)
{
switch ($tag) {
case 'link':
case 'style':
$fn = 'render_style_units';
break;
case 'script':
$fn = 'render_script_units';
break;
default:
throw new Exception("Cannot handle tag: {$tag}");
}
return $this->$fn($work_units, $cat);
}
public function getRenderedAssets()
{
return array_reverse($this->rendered_bits);
}
private function render_style_units($work_units, $cat)
{
// we can do this because tags are grouped by the presence of a file or not
$cs = '';
if ($cat) {
$c = unserialize($cat);
$cs = $cat ? sprintf(' media="%s"', htmlspecialchars($c[0], ENT_QUOTES)) : '';
}
if ($work_units[0]['file']) {
if ($res = $this->generate_file_cache($work_units, new CssRenderHandler($this->_cfg, $this->_source_file))) {
$res = sprintf('<link rel="stylesheet" type="text/css"%s href="%s" />'."\n", $cs, htmlspecialchars($res, ENT_QUOTES));
}
} else {
$res = $this->generate_content_cache($work_units, new CssRenderHandler($this->_cfg, $this->_source_file));
$res = sprintf('<style type="text/css"%s>%s</style>'."\n", $cs, $res);
}
return $res;
}
private function render_script_units($work_units, $cat)
{
if ($work_units[0]['file']) {
if ($res = $this->generate_file_cache($work_units, new JavaScriptRenderHandler($this->_cfg, $this->_source_file))) {
$this->rendered_bits[] = ['type' => 'file', 'src' => $res];
return sprintf('<script type="text/javascript" src="%s"></script>'."\n", htmlspecialchars($res, ENT_QUOTES));
}
} else {
$res = $this->generate_content_cache($work_units, new JavaScriptRenderHandler($this->_cfg, $this->_source_file));
if ($res) {
$this->rendered_bits[] = ['type' => 'string', 'content' => $res];
}
return sprintf('<script type="text/javascript">%s</script>'."\n", $res);
}
return '';
}
private function generate_content_cache($work_units, CacheRenderHandler $rh)
{
$content = implode("\n", array_map(function ($u) {
return $u['content'];
}, $work_units));
$key = md5($content.$this->_cfg->getDebugMode());
if ($d = $this->fragment_cache->get($key)) {
return $d;
}
$output = [];
foreach ($work_units as $w) {
$output[] = $rh->getOutput($w);
}
$output = implode("\n", $output);
$this->fragment_cache->set($key, $output);
return $output;
}
private function content_key_for_mtime_key($key, $work_units)
{
if (!(defined('SACY_USE_CONTENT_BASED_CACHE') && SACY_USE_CONTENT_BASED_CACHE)) {
return $key;
}
$cache_key = 'ck-for-mkey-'.$key;
$ck = $this->fragment_cache->get($cache_key);
if (!$ck) {
$ck = '';
foreach ($work_units as $f) {
$ck = md5($ck.md5_file($f['file']));
foreach ($f['additional_files'] as $af) {
$ck = md5($ck.md5_file($af));
}
}
$ck = "{$ck}-content";
$this->fragment_cache->set($cache_key, $ck);
}
return $ck;
}
private function generate_file_cache($work_units, CacheRenderHandler $rh)
{
if (!is_dir(ASSET_COMPILE_OUTPUT_DIR)) {
if (!@mkdir(ASSET_COMPILE_OUTPUT_DIR, 0755, true)) {
throw new Exception('Failed to create output directory');
}
}
$f = function ($f) {
return createScriptURL_Text(pathinfo($f['file'], PATHINFO_FILENAME));
};
$ident = implode('-', array_map($f, $work_units));
if (strlen($ident) > 120) {
$ident = 'many-files-'.md5($ident);
}
$max = 0;
$idents = [];
foreach ($work_units as &$f) {
$idents[] = [
$f['group'], $f['file'], $f['type'], $f['tag'],
];
$f['additional_files'] = $rh->getAdditionalFiles($f);
$max = max($max, filemtime($f['file']));
foreach ($f['additional_files'] as $af) {
$max = max($max, filemtime($af));
}
unset($f);
}
// not using the actual content for quicker access
$key = md5($max.serialize($idents).$rh->getConfig()->getDebugMode());
$key = $this->content_key_for_mtime_key($key, $work_units);
$cfile = ASSET_COMPILE_OUTPUT_DIR.DIRECTORY_SEPARATOR."{$ident}-{$key}".$rh->getFileExtension();
$pub = ASSET_COMPILE_URL_ROOT."/{$ident}-{$key}".$rh->getFileExtension();
if (file_exists($cfile) && ($rh->getConfig()->getDebugMode() != 2)) {
return $pub;
}
$this->write_cache($cfile, $work_units, $rh);
return $pub;
}
private function write_cache($cfile, $files, CacheRenderHandler $rh)
{
$tmpfile = $this->write_cache_tmpfile($cfile, $files, $rh);
if ($tmpfile) {
$ts = time();
$this->write_compressed_cache($tmpfile, $cfile, $ts);
if (rename($tmpfile, $cfile)) {
chmod($cfile, 0644);
touch($cfile, $ts);
} else {
trigger_error("Cannot write file: {$cfile}", E_USER_WARNING);
}
}
return (bool) $tmpfile;
}
private function write_compressed_cache($tmpfile, $cfile, $ts)
{
if (!function_exists('gzencode')) {
return;
}
$tmp_compressed = "{$tmpfile}.gz";
file_put_contents($tmp_compressed, gzencode(file_get_contents($tmpfile), 9));
$compressed = "{$cfile}.gz";
if (rename($tmp_compressed, $compressed)) {
touch($compressed, $ts);
} else {
trigger_error("Cannot write compressed file: {$compressed}", E_USER_WARNING);
}
}
private function write_cache_tmpfile($cfile, $files, CacheRenderHandler $rh)
{
$tmpfile = tempnam(dirname($cfile), $cfile);
$fhc = @fopen($tmpfile, 'w+');
if (!$fhc) {
trigger_error("Cannot write to temporary file: {$tmpfile}", E_USER_WARNING);
return null;
}
if ($rh->getConfig()->get('write_headers')) {
$rh->writeHeader($fhc, $files);
}
$res = true;
$merge = (bool) $rh->getConfig()->get('merge_tags');
if ($merge) {
$rh->startWrite();
}
foreach ($files as $file) {
try {
$rh->processFile($fhc, $file);
} catch (\Exception $e) {
getRaven()->captureException($e);
throw $e;
}
}
if ($merge) {
$rh->endWrite($fhc);
}
fclose($fhc);
return $res ? $tmpfile : null;
}
}
interface CacheRenderHandler
{
public function __construct(Config $cfg, $source_file);
public function getFileExtension();
public static function willTransformType($type);
public function writeHeader($fh, $work_units);
public function getAdditionalFiles($work_unit);
public function processFile($fh, $work_unit);
public function startWrite();
public function endWrite($fh);
public function getOutput($work_unit);
public function getConfig();
}
abstract class ConfiguredRenderHandler implements CacheRenderHandler
{
private $_cfg;
private $_source_file;
public function __construct(Config $cfg, $source_file)
{
$this->_cfg = $cfg;
$this->_source_file = $source_file;
}
protected function getSourceFile()
{
return $this->_source_file;
}
public function getConfig()
{
return $this->_cfg;
}
public static function willTransformType($type)
{
return false;
}
public function startWrite()
{
}
public function endWrite($fh)
{
}
}
class JavaScriptRenderHandler extends ConfiguredRenderHandler
{
public static function supportedTransformations()
{
$supported = [];
if (function_exists('CoffeeScript\compile') || ExternalProcessorRegistry::typeIsSupported('text/coffeescript')) {
$supported[] = 'text/coffeescript';
}
if (ExternalProcessorRegistry::typeIsSupported('text/x-eco')) {
$supported[] = 'text/x-eco';
}
if (ExternalProcessorRegistry::typeIsSupported('text/x-jsx')) {
$supported[] = 'text/x-jsx';
}
return $supported;
}
public static function willTransformType($type)
{
// transforming everything but plain old CSS
return in_array($type, self::supportedTransformations());
}
public function getFileExtension()
{
return '.js';
}
public function writeHeader($fh, $work_units)
{
fwrite($fh, "/*\nsacy javascript cache dump \n\n");
fwrite($fh, "This dump has been created from the following files:\n");
foreach ($work_units as $file) {
fprintf($fh, " - %s\n", str_replace($_SERVER['DOCUMENT_ROOT'], '<root>', $file['file']));
}
fwrite($fh, "*/\n\n");
}
public function getOutput($work_unit)
{
$debug = $this->getConfig()->getDebugMode() == 3;
if ($work_unit['file']) {
$js = @file_get_contents($work_unit['file']);
if (!$js) {
return '/* error accessing file */';
}
$source_file = $work_unit['file'];
} else {
$js = $work_unit['content'];
$source_file = $this->getSourceFile();
}
if ($work_unit['type'] == 'text/coffeescript') {
$js = ExternalProcessorRegistry::typeIsSupported('text/coffeescript') ?
ExternalProcessorRegistry::getTransformerForType('text/coffeescript')->transform($js, $source_file) :
\Coffeescript::build($js);
} elseif ($work_unit['type'] == 'text/x-eco') {
$eco = ExternalProcessorRegistry::getTransformerForType('text/x-eco');
$js = $eco->transform($js, $source_file, $work_unit['data']);
} elseif ($work_unit['type'] == 'text/x-jsx') {
$jsx = ExternalProcessorRegistry::getTransformerForType('text/x-jsx');
$js = $jsx->transform($js, $source_file, $work_unit['data']);
}
if ($debug) {
return $js;
} else {
return ExternalProcessorRegistry::typeIsSupported('text/javascript') ?
ExternalProcessorRegistry::getCompressorForType('text/javascript')->transform($js, $source_file) :
\JSMin::minify($js);
}
}
public function processFile($fh, $work_unit)
{
if ($this->getConfig()->get('write_headers')) {
fprintf($fh, "\n/* %s */\n", str_replace($_SERVER['DOCUMENT_ROOT'], '<root>', $work_unit['file']));
}
fwrite($fh, $this->getOutput($work_unit));
}
public function getAdditionalFiles($work_unit)
{
return [];
}
}
class CssRenderHandler extends ConfiguredRenderHandler
{
private $to_process = [];
private $collecting = false;
public static function supportedTransformations()
{
$res = ['', 'text/css'];
if (class_exists('lessc') || ExternalProcessorRegistry::typeIsSupported('text/x-less')) {
$res[] = 'text/x-less';
}
if (class_exists('SassParser') || ExternalProcessorRegistry::typeIsSupported('text/x-sass')) {
$res = array_merge($res, ['text/x-sass', 'text/x-scss']);
}
if (PhpSassSacy::isAvailable()) {
$res[] = 'text/x-scss';
}
if (ExternalProcessorRegistry::typeIsSupported('image/svg+xml')) {
$res[] = 'image/svg+xml';
}
return array_unique($res);
}
public function getFileExtension()
{
return '.css';
}
public static function willTransformType($type)
{
// transforming everything but plain old CSS
return !in_array($type, ['', 'text/css']);
}
public function writeHeader($fh, $work_units)
{
fwrite($fh, "/*\nsacy css cache dump \n\n");
fwrite($fh, "This dump has been created from the following files:\n");
foreach ($work_units as $file) {
fprintf($fh, " - %s\n", str_replace($_SERVER['DOCUMENT_ROOT'], '<root>', $file['file']));
}
fwrite($fh, "*/\n\n");
}
public function processFile($fh, $work_unit)
{
// for now: only support collecting for scss and sass
if (!in_array($work_unit['type'], ['text/x-scss', 'text/x-sass'])) {
$this->collecting = false;
}
if ($this->collecting) {
$content = @file_get_contents($work_unit['file']);
if (!$content) {
$content = "/* error accessing file {$work_unit['file']} */";
}
$content = \Minify_CSS_UriRewriter::rewrite(
$content,
dirname($work_unit['file']),
$_SERVER['DOCUMENT_ROOT'],
[],
true
);
$this->to_process[] = [
'file' => $work_unit['file'],
'content' => $content,
'type' => $work_unit['type'],
];
} else {
if ($this->getConfig()->get('write_headers')) {
fprintf($fh, "\n/* %s */\n", str_replace($_SERVER['DOCUMENT_ROOT'], '<root>', $work_unit['file']));
}
fwrite($fh, $this->getOutput($work_unit));
}
}
public function endWrite($fh)
{
if (!$this->collecting) {
return;
}
$content = '';
$incpath = [];
foreach ($this->to_process as $job) {
$content .= $job['content'];
$incpath[] = dirname($job['file']);
}
fwrite($fh, $this->getOutput([
'content' => $content,
'type' => $this->to_process[0]['type'],
'paths' => $incpath,
]));
}
public function getOutput($work_unit)
{
$debug = $this->getConfig()->getDebugMode() == 3;
if ($work_unit['file']) {
$css = @file_get_contents($work_unit['file']);
if (!$css) {
return '/* error accessing file */';
}
$source_file = $work_unit['file'];
} else {
$css = $work_unit['content'];
$source_file = $this->getSourceFile();
}
if (ExternalProcessorRegistry::typeIsSupported($work_unit['type'])) {
$opts = [];
if ($work_unit['paths']) {
$opts['library_path'] = $work_unit['paths'];
}
$css = ExternalProcessorRegistry::getTransformerForType($work_unit['type'])
->transform($css, $source_file, $opts);
} else {
if ($work_unit['type'] == 'text/x-less') {
$less = new \lessc();
$less->importDir = dirname($source_file).'/'; // lessphp concatenates without a /
$css = $less->parse($css);
}
if (PhpSassSacy::isAvailable() && $work_unit['type'] == 'text/x-scss') {
$css = PhpSassSacy::compile($work_unit['file'], $css, $work_unit['paths'] ?: [dirname($source_file)]);
} elseif (in_array($work_unit['type'], ['text/x-scss', 'text/x-sass'])) {
$config = [
'cache' => false, // no need. WE are the cache!
'debug_info' => $debug,
'line' => $debug,
'load_paths' => $work_unit['paths'] ?: [dirname($source_file)],
'filename' => $source_file,
'quiet' => true,
'style' => $debug ? 'nested' : 'compressed',
];
$sass = new \SassParser($config);
$css = $sass->toCss($css, false); // isFile?
}
}
if ($debug) {
return \Minify_CSS_UriRewriter::rewrite(
$css,
dirname($source_file),
$_SERVER['DOCUMENT_ROOT'],
[]
);
} else {
return \Minify_CSS::minify($css, [
'currentDir' => dirname($source_file),
]);
}
}
private function extract_import_file($parent_type, $parent_file, $cssdata)
{
$f = null;
if (preg_match('#^\s*url\((["\'])([^\1]+)\1#', $cssdata, $matches)) {
$f = $matches[2];
} elseif (preg_match('#^\s*(["\'])([^\1]+)\1#', $cssdata, $matches)) {
$f = $matches[2];
}
$path_info = pathinfo($parent_file);
if (in_array($parent_type, ['text/x-scss', 'text/x-sass'])) {
$ext = preg_quote($path_info['extension'], '#');
if (!preg_match("#.{$ext}\$#", $f)) {
$f .= '.'.$path_info['extension'];
}
$mixin = $path_info['dirname'].DIRECTORY_SEPARATOR."_{$f}";
if (file_exists($mixin)) {
return $mixin;
}
} elseif ($parent_type == 'text/x-less') {
// less only inlines @import's of .less files (see: http://lesscss.org/#-importing)
if (!preg_match('#\.less$', $f)) {
return null;
}
} else {
return null;
}
$f = $path_info['dirname'].DIRECTORY_SEPARATOR.$f;
return file_exists($f) ? $f : null;
}
private function find_imports($type, $file, $level)
{
$level++;
if (!in_array($type, ['text/x-scss', 'text/x-sass', 'text/x-less'])) {
return [];
}
if ($level > 10) {
throw new Exception("CSS Include nesting level of {$level} too deep");
}
$fh = fopen($file, 'r');
$res = [];
while (false !== ($line = fgets($fh))) {
if (preg_match('#^\s*$#', $line)) {
continue;
}
if (preg_match('#^\s*@import(.*)$#', $line, $matches)) {
$f = $this->extract_import_file($type, $file, $matches[1]);
if ($f) {
$res[] = $f;
$res = array_merge($res, $this->find_imports($type, $f, $level));
}
}
}
fclose($fh);
return $res;
}
public function getAdditionalFiles($work_unit)
{
$level = 0;
return $this->find_imports($work_unit['type'], $work_unit['file'], $level);
}
public function startWrite()
{
$this->to_process = [];
$this->collecting = true;
}
}