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

View File

@@ -0,0 +1,9 @@
<?php
class CoffeeScript
{
public static function build($file)
{
return CoffeeScript\compile($file);
}
}

625
class/sacy/cssmin.php Normal file
View File

@@ -0,0 +1,625 @@
<?php
/* Taken from minify by Ryan Grove and Steve Clay and distributed
under the following license:
Copyright (c) 2008 Ryan Grove <ryan@wonko.com>
Copyright (c) 2008 Steve Clay <steve@mrclay.org>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of this project nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
*/
class Minify_CSS
{
public static function minify($css, $options = [])
{
if (isset($options['preserveComments'])
&& !$options['preserveComments']
) {
$css = Minify_CSS_Compressor::process($css, $options);
} else {
$css = Minify_CommentPreserver::process(
$css, ['Minify_CSS_Compressor', 'process'], [$options]
);
}
if (!isset($options['currentDir']) && !isset($options['prependRelativePath'])) {
return $css;
}
if (isset($options['currentDir'])) {
return Minify_CSS_UriRewriter::rewrite(
$css, $options['currentDir'], isset($options['docRoot']) ? $options['docRoot'] : $_SERVER['DOCUMENT_ROOT'], isset($options['symlinks']) ? $options['symlinks'] : []
);
} else {
return Minify_CSS_UriRewriter::prepend(
$css, $options['prependRelativePath']
);
}
}
}
class Minify_CSS_UriRewriter
{
/**
* Defines which class to call as part of callbacks, change this
* if you extend Minify_CSS_UriRewriter.
*
* @var string
*/
protected static $className = 'Minify_CSS_UriRewriter';
/**
* rewrite() and rewriteRelative() append debugging information here.
*
* @var string
*/
public static $debugText = '';
/**
* Rewrite file relative URIs as root relative in CSS files.
*
* @param string $css
* @param string $currentDir the directory of the current CSS file
* @param string $docRoot the document root of the web site in which
* the CSS file resides (default = $_SERVER['DOCUMENT_ROOT'])
* @param array $symlinks (default = array()) If the CSS file is stored in
* a symlink-ed directory, provide an array of link paths to
* target paths, where the link paths are within the document root. Because
* paths need to be normalized for this to work, use "//" to substitute
* the doc root in the link paths (the array keys). E.g.:
* <code>
* array('//symlink' => '/real/target/path') // unix
* array('//static' => 'D:\\staticStorage') // Windows
* </code>
* @param bool $leave_imports don't rewrite imports. Only touch URLs
*
* @return string
*/
public static function rewrite($css, $currentDir, $docRoot = null, $symlinks = [], $leave_imports = false)
{
self::$_docRoot = self::_realpath(
$docRoot ? $docRoot : $_SERVER['DOCUMENT_ROOT']
);
self::$_currentDir = self::_realpath($currentDir);
self::$_symlinks = [];
// normalize symlinks
foreach ($symlinks as $link => $target) {
$link = ($link === '//')
? self::$_docRoot
: str_replace('//', self::$_docRoot.'/', $link);
$link = strtr($link, '/', DIRECTORY_SEPARATOR);
self::$_symlinks[$link] = self::_realpath($target);
}
self::$debugText .= 'docRoot : '.self::$_docRoot."\n"
.'currentDir : '.self::$_currentDir."\n";
if (self::$_symlinks) {
self::$debugText .= 'symlinks : '.var_export(self::$_symlinks, 1)."\n";
}
self::$debugText .= "\n";
$css = self::_trimUrls($css);
// rewrite
if (!$leave_imports) {
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/', [self::$className, '_processUriCB'], $css);
}
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/', [self::$className, '_processUriCB'], $css);
return $css;
}
/**
* Prepend a path to relative URIs in CSS files.
*
* @param string $css
* @param string $path the path to prepend
*
* @return string
*/
public static function prepend($css, $path)
{
self::$_prependPath = $path;
$css = self::_trimUrls($css);
// append
$css = preg_replace_callback('/@import\\s+([\'"])(.*?)[\'"]/', [self::$className, '_processUriCB'], $css);
$css = preg_replace_callback('/url\\(\\s*([^\\)\\s]+)\\s*\\)/', [self::$className, '_processUriCB'], $css);
self::$_prependPath = null;
return $css;
}
/**
* @var string directory of this stylesheet
*/
private static $_currentDir = '';
/**
* @var string DOC_ROOT
*/
private static $_docRoot = '';
/**
* @var array directory replacements to map symlink targets back to their
* source (within the document root) E.g. '/var/www/symlink' => '/var/realpath'
*/
private static $_symlinks = [];
/**
* @var string path to prepend
*/
private static $_prependPath;
private static function _trimUrls($css)
{
return preg_replace('/
url\\( # url(
\\s*
([^\\)]+?) # 1 = URI (assuming does not contain ")")
\\s*
\\) # )
/x', 'url($1)', $css);
}
private static function _processUriCB($m)
{
// $m matched either '/@import\\s+([\'"])(.*?)[\'"]/' or '/url\\(\\s*([^\\)\\s]+)\\s*\\)/'
$isImport = ($m[0][0] === '@');
// determine URI and the quote character (if any)
if ($isImport) {
$quoteChar = $m[1];
$uri = $m[2];
} else {
// $m[1] is either quoted or not
$quoteChar = ($m[1][0] === "'" || $m[1][0] === '"')
? $m[1][0]
: '';
$uri = ($quoteChar === '')
? $m[1]
: substr($m[1], 1, strlen($m[1]) - 2);
}
// analyze URI
if ('/' !== $uri[0] // root-relative
&& false === strpos($uri, '//') // protocol (non-data)
&& 0 !== strpos($uri, 'data:') // data protocol
) {
// URI is file-relative: rewrite depending on options
$uri = (self::$_prependPath !== null)
? (self::$_prependPath.$uri)
: self::rewriteRelative($uri, self::$_currentDir, self::$_docRoot, self::$_symlinks);
}
return $isImport
? "@import {$quoteChar}{$uri}{$quoteChar}"
: "url({$quoteChar}{$uri}{$quoteChar})";
}
/**
* Rewrite a file relative URI as root relative.
*
* <code>
* Minify_CSS_UriRewriter::rewriteRelative(
* '../img/hello.gif'
* , '/home/user/www/css' // path of CSS file
* , '/home/user/www' // doc root
* );
* // returns '/img/hello.gif'
*
* // example where static files are stored in a symlinked directory
* Minify_CSS_UriRewriter::rewriteRelative(
* 'hello.gif'
* , '/var/staticFiles/theme'
* , '/home/user/www'
* , array('/home/user/www/static' => '/var/staticFiles')
* );
* // returns '/static/theme/hello.gif'
* </code>
*
* @param string $uri file relative URI
* @param string $realCurrentDir realpath of the current file's directory
* @param string $realDocRoot realpath of the site document root
* @param array $symlinks (default = array()) If the file is stored in
* a symlink-ed directory, provide an array of link paths to
* real target paths, where the link paths "appear" to be within the document
* root. E.g.:
* <code>
* array('/home/foo/www/not/real/path' => '/real/target/path') // unix
* array('C:\\htdocs\\not\\real' => 'D:\\real\\target\\path') // Windows
* </code>
*
* @return string
*/
public static function rewriteRelative($uri, $realCurrentDir, $realDocRoot, $symlinks = [])
{
// prepend path with current dir separator (OS-independent)
$path = strtr($realCurrentDir, '/', DIRECTORY_SEPARATOR)
.DIRECTORY_SEPARATOR.strtr($uri, '/', DIRECTORY_SEPARATOR);
self::$debugText .= "file-relative URI : {$uri}\n"
."path prepended : {$path}\n";
// "unresolve" a symlink back to doc root
foreach ($symlinks as $link => $target) {
if (0 === strpos($path, $target)) {
// replace $target with $link
$path = $link.substr($path, strlen($target));
self::$debugText .= "symlink unresolved : {$path}\n";
break;
}
}
// strip doc root
$path = substr($path, strlen($realDocRoot));
self::$debugText .= "docroot stripped : {$path}\n";
// fix to root-relative URI
$uri = strtr($path, '/\\', '//');
// remove /./ and /../ where possible
$uri = str_replace('/./', '/', $uri);
// inspired by patch from Oleg Cherniy
do {
$uri = preg_replace('@/[^/]+/\\.\\./@', '/', $uri, 1, $changed);
} while ($changed);
self::$debugText .= "traversals removed : {$uri}\n\n";
return $uri;
}
/**
* Get realpath with any trailing slash removed. If realpath() fails,
* just remove the trailing slash.
*
* @param string $path
*
* @return mixed path with no trailing slash
*/
protected static function _realpath($path)
{
$realPath = realpath($path);
if ($realPath !== false) {
$path = $realPath;
}
return rtrim($path, '/\\');
}
}
class Minify_CommentPreserver
{
/**
* String to be prepended to each preserved comment.
*
* @var string
*/
public static $prepend = "\n";
/**
* String to be appended to each preserved comment.
*
* @var string
*/
public static $append = "\n";
/**
* Process a string outside of C-style comments that begin with "/*!".
*
* On each non-empty string outside these comments, the given processor
* function will be called. The first "!" will be removed from the
* preserved comments, and the comments will be surrounded by
* Minify_CommentPreserver::$preprend and Minify_CommentPreserver::$append.
*
* @param string $content
* @param callable $processor function
* @param array $args array of extra arguments to pass to the processor
* function (default = array())
*
* @return string
*/
public static function process($content, $processor, $args = [])
{
$ret = '';
while (true) {
list($beforeComment, $comment, $afterComment) = self::_nextComment($content);
if ('' !== $beforeComment) {
$callArgs = $args;
array_unshift($callArgs, $beforeComment);
$ret .= call_user_func_array($processor, $callArgs);
}
if (false === $comment) {
break;
}
$ret .= $comment;
$content = $afterComment;
}
return $ret;
}
/**
* Extract comments that YUI Compressor preserves.
*
* @param string $in input
*
* @return array 3 elements are returned. If a YUI comment is found, the
* 2nd element is the comment and the 1st and 2nd are the surrounding
* strings. If no comment is found, the entire string is returned as the
* 1st element and the other two are false.
*/
private static function _nextComment($in)
{
if (
false === ($start = strpos($in, '/*!'))
|| false === ($end = strpos($in, '*/', $start + 3))
) {
return [$in, false, false];
}
$ret = [
substr($in, 0, $start), self::$prepend.'/*'.substr($in, $start + 3, $end - $start - 1).self::$append,
];
$endChars = (strlen($in) - $end - 2);
$ret[] = (0 === $endChars)
? ''
: substr($in, -$endChars);
return $ret;
}
}
class Minify_CSS_Compressor
{
/**
* Minify a CSS string.
*
* @param string $css
* @param array $options (currently ignored)
*
* @return string
*/
public static function process($css, $options = [])
{
$obj = new self($options);
return $obj->_process($css);
}
/**
* @var array options
*/
protected $_options;
/**
* @var bool Are we "in" a hack?
*
* I.e. are some browsers targetted until the next comment?
*/
protected $_inHack = false;
/**
* Constructor.
*
* @param array $options (currently ignored)
*/
private function __construct($options)
{
$this->_options = $options;
}
/**
* Minify a CSS string.
*
* @param string $css
*
* @return string
*/
protected function _process($css)
{
$css = str_replace("\r\n", "\n", $css);
// preserve empty comment after '>'
// http://www.webdevout.net/css-hacks#in_css-selectors
$css = preg_replace('@>/\\*\\s*\\*/@', '>/*keep*/', $css);
// preserve empty comment between property and value
// http://css-discuss.incutio.com/?page=BoxModelHack
$css = preg_replace('@/\\*\\s*\\*/\\s*:@', '/*keep*/:', $css);
$css = preg_replace('@:\\s*/\\*\\s*\\*/@', ':/*keep*/', $css);
// apply callback to all valid comments (and strip out surrounding ws
$css = preg_replace_callback('@\\s*/\\*([\\s\\S]*?)\\*/\\s*@', [$this, '_commentCB'], $css);
// remove ws around { } and last semicolon in declaration block
$css = preg_replace('/\\s*{\\s*/', '{', $css);
$css = preg_replace('/;?\\s*}\\s*/', '}', $css);
// remove ws surrounding semicolons
$css = preg_replace('/\\s*;\\s*/', ';', $css);
// remove ws around urls
$css = preg_replace('/
url\\( # url(
\\s*
([^\\)]+?) # 1 = the URL (really just a bunch of non right parenthesis)
\\s*
\\) # )
/x', 'url($1)', $css);
// remove ws between rules and colons
$css = preg_replace('/
\\s*
([{;]) # 1 = beginning of block or rule separator
\\s*
([\\*_]?[\\w\\-]+) # 2 = property (and maybe IE filter)
\\s*
:
\\s*
(\\b|[#\'"]) # 3 = first character of a value
/x', '$1$2:$3', $css);
// remove ws in selectors
$css = preg_replace_callback('/
(?: # non-capture
\\s*
[^~>+,\\s]+ # selector part
\\s*
[,>+~] # combinators
)+
\\s*
[^~>+,\\s]+ # selector part
{ # open declaration block
/x', [$this, '_selectorsCB'], $css);
// minimize hex colors
$css = preg_replace('/([^=])#([a-f\\d])\\2([a-f\\d])\\3([a-f\\d])\\4([\\s;\\}])/i', '$1#$2$3$4$5', $css);
// remove spaces between font families
$css = preg_replace_callback('/font-family:([^;}]+)([;}])/', [$this, '_fontFamilyCB'], $css);
$css = preg_replace('/@import\\s+url/', '@import url', $css);
// replace any ws involving newlines with a single newline
$css = preg_replace('/[ \\t]*\\n+\\s*/', "\n", $css);
// separate common descendent selectors w/ newlines (to limit line lengths)
// $css = preg_replace('/([\\w#\\.\\*]+)\\s+([\\w#\\.\\*]+){/', "$1\n$2{", $css);
// Use newline after 1st numeric value (to limit line lengths).
$css = preg_replace('/
((?:padding|margin|border|outline):\\d+(?:px|em)?) # 1 = prop : 1st numeric value
\\s+
/x', "$1\n", $css);
// prevent triggering IE6 bug: http://www.crankygeek.com/ie6pebug/
$css = preg_replace('/:first-l(etter|ine)\\{/', ':first-l$1 {', $css);
return trim($css);
}
/**
* Replace what looks like a set of selectors.
*
* @param array $m regex matches
*
* @return string
*/
protected function _selectorsCB($m)
{
// remove ws around the combinators
return preg_replace('/\\s*([,>+~])\\s*/', '$1', $m[0]);
}
/**
* Process a comment and return a replacement.
*
* @param array $m regex matches
*
* @return string
*/
protected function _commentCB($m)
{
$hasSurroundingWs = (trim($m[0]) !== $m[1]);
$m = $m[1];
// $m is the comment content w/o the surrounding tokens,
// but the return value will replace the entire comment.
if ($m === 'keep') {
return '/**/';
}
if ($m === '" "') {
// component of http://tantek.com/CSS/Examples/midpass.html
return '/*" "*/';
}
if (preg_match('@";\\}\\s*\\}/\\*\\s+@', $m)) {
// component of http://tantek.com/CSS/Examples/midpass.html
return '/*";}}/* */';
}
if ($this->_inHack) {
// inversion: feeding only to one browser
if (preg_match('@
^/ # comment started like /*/
\\s*
(\\S[\\s\\S]+?) # has at least some non-ws content
\\s*
/\\* # ends like /*/ or /**/
@x', $m, $n)) {
// end hack mode after this comment, but preserve the hack and comment content
$this->_inHack = false;
return "/*/{$n[1]}/**/";
}
}
if (substr($m, -1) === '\\') { // comment ends like \*/
// begin hack mode and preserve hack
$this->_inHack = true;
return '/*\\*/';
}
if ($m !== '' && $m[0] === '/') { // comment looks like /*/ foo */
// begin hack mode and preserve hack
$this->_inHack = true;
return '/*/*/';
}
if ($this->_inHack) {
// a regular comment ends hack mode but should be preserved
$this->_inHack = false;
return '/**/';
}
// Issue 107: if there's any surrounding whitespace, it may be important, so
// replace the comment with a single space
return $hasSurroundingWs // remove all other comments
? ' '
: '';
}
/**
* Process a font-family listing and return a replacement.
*
* @param array $m regex matches
*
* @return string
*/
protected function _fontFamilyCB($m)
{
$m[1] = preg_replace('/
\\s*
(
"[^"]+" # 1 = family in double qutoes
|\'[^\']+\' # or 1 = family in single quotes
|[\\w\\-]+ # or 1 = unquoted family
)
\\s*
/x', '$1', $m[1]);
return 'font-family:'.$m[1].$m[2];
}
}

View File

@@ -0,0 +1,252 @@
<?php
namespace sacy;
abstract class ExternalProcessor
{
abstract protected function getCommandLine($filename, $opts = []);
public function transform($in, $filename, $opts = [])
{
$s = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$cmd = $this->getCommandLine($filename, $opts);
$p = proc_open($cmd, $s, $pipes, getcwd());
if (!is_resource($p)) {
throw new \Exception("Failed to execute {$cmd}");
}
fwrite($pipes[0], $in);
fclose($pipes[0]);
$out = stream_get_contents($pipes[1]);
$err = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$r = proc_close($p);
if ($r != 0) {
throw new \Exception("Command returned {$r}: {$out} {$err}");
}
return $out;
}
}
class ExternalProcessorRegistry
{
private static $transformers;
private static $compressors;
public static function registerTransformer($type, $cls)
{
self::$transformers[$type] = $cls;
}
public static function registerCompressor($type, $cls)
{
self::$compressors[$type] = $cls;
}
private static function lookup($type, $in)
{
return (isset($in[$type])) ? new $in[$type]() : null;
}
public static function typeIsSupported($type)
{
return isset(self::$transformers[$type])
|| isset(self::$compressors[$type]);
}
/**
* @static
*
* @param $type string mime type of input
*
* @return ExternalProcessor
*/
public static function getTransformerForType($type)
{
return self::lookup($type, self::$transformers);
}
/**
* @static
*
* @param $type string mime type of input
*
* @return ExternalProcessor
*/
public static function getCompressorForType($type)
{
return self::lookup($type, self::$compressors);
}
}
class ProcessorUglify extends ExternalProcessor
{
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_COMPRESSOR_UGLIFY)) {
throw new Exception('SACY_COMPRESSOR_UGLIFY defined but not executable');
}
return SACY_COMPRESSOR_UGLIFY;
}
}
class ProcessorCoffee extends ExternalProcessor
{
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_TRANSFORMER_COFFEE)) {
throw new Exception('SACY_TRANSFORMER_COFFEE defined but not executable');
}
return sprintf('%s -c -s', SACY_TRANSFORMER_COFFEE);
}
}
class ProcessorEco extends ExternalProcessor
{
protected function getType()
{
return 'text/x-eco';
}
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_TRANSFORMER_ECO)) {
throw new Exception('SACY_TRANSFORMER_ECO defined but not executable');
}
// Calling eco with the filename here. Using stdin wouldn't
// cut it, as eco uses the filename to figure out the name of
// the js function it outputs.
$eco_root = $opts['eco-root'];
return sprintf('%s %s -p %s',
SACY_TRANSFORMER_ECO,
$eco_root ? sprintf('-i %s', escapeshellarg($eco_root)) : '',
escapeshellarg($filename)
);
}
}
class ProcessorSass extends ExternalProcessor
{
protected function getType()
{
return 'text/x-sass';
}
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_TRANSFORMER_SASS)) {
throw new Exception('SACY_TRANSFORMER_SASS defined but not executable');
}
$libpath = $opts['library_path'] ?: [dirname($filename)];
$libpath[] = $_SERVER['DOCUMENT_ROOT'] ?: getcwd();
$path =
implode(' ', array_map(function ($p) {
return '-I '.escapeshellarg($p);
}, array_unique($libpath)));
return sprintf('%s --cache-location=%s -s %s %s',
SACY_TRANSFORMER_SASS,
escapeshellarg(sys_get_temp_dir()),
$this->getType() == 'text/x-scss' ? '--scss' : '',
$path
);
}
}
class ProcessorScss extends ProcessorSass
{
protected function getType()
{
return 'text/x-scss';
}
}
class ProcessorLess extends ExternalProcessor
{
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_TRANSFORMER_LESS)) {
throw new \Exception('SACY_TRANSFORMER_LESS defined but not executable');
}
return sprintf(
'%s -I%s -',
SACY_TRANSFORMER_LESS,
escapeshellarg(dirname($filename))
);
}
}
class ProcessorJSX extends ExternalProcessor
{
protected function getCommandLine($filename, $opts = [])
{
if (!is_executable(SACY_TRANSFORMER_JSX)) {
throw new Exception('SACY_TRANSFORMER_JSX defined but not executable');
}
return SACY_TRANSFORMER_JSX;
}
}
class ProcessorFontIcons extends ExternalProcessor
{
protected function getCommandLine($filename, $opts = [])
{
return SACY_TRANSFORMER_FONTICONS." compile 'templates/icons/' -F -c {$filename}";
}
public function transform($in, $filename, $opts = [])
{
$css = 'data/tmp/cache/icons.css';
if (@filemtime($filename) > @filemtime($css)) {
parent::transform($in, $filename, $opts);
}
return str_replace('./icons', '/data/tmp/cache/icons', file_get_contents($css));
}
}
if (defined('SACY_COMPRESSOR_UGLIFY')) {
ExternalProcessorRegistry::registerCompressor('text/javascript', 'sacy\ProcessorUglify');
}
if (defined('SACY_TRANSFORMER_COFFEE')) {
ExternalProcessorRegistry::registerTransformer('text/coffeescript', 'sacy\ProcessorCoffee');
}
if (defined('SACY_TRANSFORMER_ECO')) {
ExternalProcessorRegistry::registerTransformer('text/x-eco', 'sacy\ProcessorEco');
}
if (defined('SACY_TRANSFORMER_SASS')) {
ExternalProcessorRegistry::registerTransformer('text/x-sass', 'sacy\ProcessorSass');
ExternalProcessorRegistry::registerTransformer('text/x-scss', 'sacy\ProcessorScss');
}
if (defined('SACY_TRANSFORMER_FONTICONS')) {
ExternalProcessorRegistry::registerTransformer('image/svg+xml', 'sacy\ProcessorFontIcons');
}
if (defined('SACY_TRANSFORMER_LESS')) {
ExternalProcessorRegistry::registerTransformer('text/x-less', 'sacy\ProcessorLess');
}
if (defined('SACY_TRANSFORMER_JSX')) {
ExternalProcessorRegistry::registerTransformer('text/x-jsx', 'sacy\ProcessorJSX');
}

View File

@@ -0,0 +1,52 @@
<?php
namespace sacy;
class FileCache
{
private $cache_dir;
public function __construct()
{
$this->cache_dir = implode(DIRECTORY_SEPARATOR, [
ASSET_COMPILE_OUTPUT_DIR,
'fragments',
]);
if (!is_dir($this->cache_dir)) {
if (!@mkdir($this->cache_dir, 0755, true)) {
throw new Exception('Failed to create fragments cache directory');
}
}
}
public function key2file($key)
{
if (!preg_match('#^[0-9a-z]+$#', $key)) {
throw new Exception('Invalid cache key');
}
return implode(DIRECTORY_SEPARATOR, [
$this->cache_dir,
preg_replace('#^([0-9a-f]{2})([0-9a-f]{2})(.*)$#u', '\1/\2/\3', $key),
]);
}
public function get($key)
{
return null;
$p = $this->key2file($key);
return file_exists($p) ? @file_get_contents($p) : null;
}
public function set($key, $value)
{
return true;
$p = $this->key2file($key);
if (!@mkdir(dirname($p), 0755, true)) {
throw new Exception("Failed to create fragment cache dir: {$p}");
}
return @file_put_contents($p, $value);
}
}

710
class/sacy/jsmin.php Normal file
View File

@@ -0,0 +1,710 @@
<?php
class JSMin
{
public static function minify($js)
{
return Minifier::minify($js);
}
}
/**
* Minifier.
*
* Usage - Minifier::minify($js);
* Usage - Minifier::minify($js, $options);
* Usage - Minifier::minify($js, array('flaggedComments' => false));
*
* @author Robert Hafner <tedivm@tedivm.com>
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*/
class Minifier
{
/**
* The input javascript to be minified.
*
* @var string
*/
protected $input;
/**
* Length of input javascript.
*
* @var int
*/
protected $len = 0;
/**
* The location of the character (in the input string) that is next to be
* processed.
*
* @var int
*/
protected $index = 0;
/**
* The first of the characters currently being looked at.
*
* @var string
*/
protected $a = '';
/**
* The next character being looked at (after a);.
*
* @var string
*/
protected $b = '';
/**
* This character is only active when certain look ahead actions take place.
*
* @var string
*/
protected $c;
/**
* This character is only active when certain look ahead actions take place.
*
* @var string
*/
protected $last_char;
/**
* This character is only active when certain look ahead actions take place.
*
* @var string
*/
protected $output;
/**
* Contains the options for the current minification process.
*
* @var array
*/
protected $options;
/**
* These characters are used to define strings.
*/
protected $stringDelimiters = ['\'' => true, '"' => true, '`' => true];
/**
* Contains the default options for minification. This array is merged with
* the one passed in by the user to create the request specific set of
* options (stored in the $options attribute).
*
* @var array
*/
protected static $defaultOptions = ['flaggedComments' => true];
protected static $keywords = ['delete', 'do', 'for', 'in', 'instanceof', 'return', 'typeof', 'yield'];
/**
* Contains lock ids which are used to replace certain code patterns and
* prevent them from being minified.
*
* @var array
*/
protected $locks = [];
/**
* Takes a string containing javascript and removes unneeded characters in
* order to shrink the code without altering it's functionality.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*
* @return bool|string
*
* @throws \Exception
*/
public static function minify($js, $options = [])
{
try {
$jshrink = new Minifier();
$js = $jshrink->lock($js);
$js = ltrim($jshrink->minifyToString($js, $options));
$js = $jshrink->unlock($js);
unset($jshrink);
return $js;
} catch (\Exception $e) {
if (isset($jshrink)) {
// Since the breakdownScript function probably wasn't finished
// we clean it out before discarding it.
$jshrink->clean();
unset($jshrink);
}
throw $e;
}
}
/**
* Processes a javascript string and outputs only the required characters,
* stripping out all unneeded characters.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*/
protected function minifyToString($js, $options)
{
$this->initialize($js, $options);
$this->loop();
$this->clean();
return $this->output;
}
/**
* Initializes internal variables, normalizes new lines,.
*
* @param string $js The raw javascript to be minified
* @param array $options Various runtime options in an associative array
*/
protected function initialize($js, $options)
{
$this->options = array_merge(static::$defaultOptions, $options);
$this->input = $js;
// We add a newline to the end of the script to make it easier to deal
// with comments at the bottom of the script- this prevents the unclosed
// comment error that can otherwise occur.
$this->input .= PHP_EOL;
// save input length to skip calculation every time
$this->len = strlen($this->input);
// Populate "a" with a new line, "b" with the first character, before
// entering the loop
$this->a = "\n";
$this->b = "\n";
$this->last_char = "\n";
$this->output = '';
}
/**
* Characters that can't stand alone preserve the newline.
*
* @var array
*/
protected $noNewLineCharacters = [
'(' => true,
'-' => true,
'+' => true,
'[' => true,
'#' => true,
'@' => true];
protected function echo($char)
{
$this->output .= $char;
$this->last_char = $char[-1];
}
/**
* The primary action occurs here. This function loops through the input string,
* outputting anything that's relevant and discarding anything that is not.
*/
protected function loop()
{
while ($this->a !== false && !is_null($this->a) && $this->a !== '') {
switch ($this->a) {
// new lines
case "\r":
case "\n":
// if the next line is something that can't stand alone preserve the newline
if ($this->b !== false && isset($this->noNewLineCharacters[$this->b])) {
$this->echo($this->a);
$this->saveString();
break;
}
// if B is a space we skip the rest of the switch block and go down to the
// string/regex check below, resetting $this->b with getReal
if ($this->b === ' ') {
break;
}
// otherwise we treat the newline like a space
// no break
case ' ':
if (static::isAlphaNumeric($this->b)) {
$this->echo($this->a);
}
$this->saveString();
break;
default:
switch ($this->b) {
case "\r":
case "\n":
if (strpos('}])+-"\'', $this->a) !== false) {
$this->echo($this->a);
$this->saveString();
break;
} else {
if (static::isAlphaNumeric($this->a)) {
$this->echo($this->a);
$this->saveString();
}
}
break;
case ' ':
if (!static::isAlphaNumeric($this->a)) {
break;
}
// no break
default:
// check for some regex that breaks stuff
if ($this->a === '/' && ($this->b === '\'' || $this->b === '"')) {
$this->saveRegex();
continue 3;
}
$this->echo($this->a);
$this->saveString();
break;
}
}
// do reg check of doom
$this->b = $this->getReal();
if ($this->b == '/') {
$valid_tokens = "(,=:[!&|?\n";
// Find last "real" token, excluding spaces.
$last_token = $this->a;
if ($last_token == ' ') {
$last_token = $this->last_char;
}
if (strpos($valid_tokens, $last_token) !== false) {
// Regex can appear unquoted after these symbols
$this->saveRegex();
} elseif ($this->endsInKeyword()) {
// This block checks for the "return" token before the slash.
$this->saveRegex();
}
}
// if (($this->b == '/' && strpos('(,=:[!&|?', $this->a) !== false)) {
// $this->saveRegex();
// }
}
}
/**
* Resets attributes that do not need to be stored between requests so that
* the next request is ready to go. Another reason for this is to make sure
* the variables are cleared and are not taking up memory.
*/
protected function clean()
{
unset($this->input);
$this->len = 0;
$this->index = 0;
$this->a = $this->b = '';
unset($this->c);
unset($this->options);
}
/**
* Returns the next string for processing based off of the current index.
*
* @return string
*/
protected function getChar()
{
// Check to see if we had anything in the look ahead buffer and use that.
if (isset($this->c)) {
$char = $this->c;
unset($this->c);
} else {
// Otherwise we start pulling from the input.
$char = $this->index < $this->len ? $this->input[$this->index] : false;
// If the next character doesn't exist return false.
if (isset($char) && $char === false) {
return false;
}
// Otherwise increment the pointer and use this char.
$this->index++;
}
// Convert all line endings to unix standard.
// `\r\n` converts to `\n\n` and is minified.
if ($char == "\r") {
$char = "\n";
}
// Normalize all whitespace except for the newline character into a
// standard space.
if ($char !== "\n" && $char < "\x20") {
return ' ';
}
return $char;
}
/**
* This function returns the next character without moving the index forward.
*
* @return string The next character
*
* @throws \RuntimeException
*/
protected function peek()
{
if ($this->index >= $this->len) {
return false;
}
$char = $this->input[$this->index];
// Convert all line endings to unix standard.
// `\r\n` converts to `\n\n` and is minified.
if ($char == "\r") {
$char = "\n";
}
// Normalize all whitespace except for the newline character into a
// standard space.
if ($char !== "\n" && $char < "\x20") {
return ' ';
}
// Return the next character but don't push the index.
return $char;
}
/**
* This function gets the next "real" character. It is essentially a wrapper
* around the getChar function that skips comments. This has significant
* performance benefits as the skipping is done using native functions (ie,
* c code) rather than in script php.
*
* @return string next 'real' character to be processed
*
* @throws \RuntimeException
*/
protected function getReal()
{
$startIndex = $this->index;
$char = $this->getChar();
// Check to see if we're potentially in a comment
if ($char !== '/') {
return $char;
}
$this->c = $this->getChar();
if ($this->c === '/') {
$this->processOneLineComments($startIndex);
return $this->getReal();
} elseif ($this->c === '*') {
$this->processMultiLineComments($startIndex);
return $this->getReal();
}
return $char;
}
/**
* Removed one line comments, with the exception of some very specific types of
* conditional comments.
*
* @param int $startIndex The index point where "getReal" function started
*
* @return void
*/
protected function processOneLineComments($startIndex)
{
$thirdCommentString = $this->index < $this->len ? $this->input[$this->index] : false;
// kill rest of line
$this->getNext("\n");
unset($this->c);
if ($thirdCommentString == '@') {
$endPoint = $this->index - $startIndex;
$this->c = "\n".substr($this->input, $startIndex, $endPoint);
}
}
/**
* Skips multiline comments where appropriate, and includes them where needed.
* Conditional comments and "license" style blocks are preserved.
*
* @param int $startIndex The index point where "getReal" function started
*
* @return void
*
* @throws \RuntimeException Unclosed comments will throw an error
*/
protected function processMultiLineComments($startIndex)
{
$this->getChar(); // current C
$thirdCommentString = $this->getChar();
// Detect a completely empty comment, ie `/**/`
if ($thirdCommentString == '*') {
$peekChar = $this->peek();
if ($peekChar == '/') {
$this->index++;
return;
}
}
// kill everything up to the next */ if it's there
if ($this->getNext('*/')) {
$this->getChar(); // get *
$this->getChar(); // get /
$char = $this->getChar(); // get next real character
// Now we reinsert conditional comments and YUI-style licensing comments
if (($this->options['flaggedComments'] && $thirdCommentString === '!')
|| ($thirdCommentString === '@')) {
// If conditional comments or flagged comments are not the first thing in the script
// we need to echo a and fill it with a space before moving on.
if ($startIndex > 0) {
$this->echo($this->a);
$this->a = ' ';
// If the comment started on a new line we let it stay on the new line
if ($this->input[$startIndex - 1] === "\n") {
$this->echo("\n");
}
}
$endPoint = ($this->index - 1) - $startIndex;
$this->echo(substr($this->input, $startIndex, $endPoint));
$this->c = $char;
return;
}
} else {
$char = false;
}
if ($char === false) {
throw new \RuntimeException('Unclosed multiline comment at position: '.($this->index - 2));
}
// if we're here c is part of the comment and therefore tossed
$this->c = $char;
}
/**
* Pushes the index ahead to the next instance of the supplied string. If it
* is found the first character of the string is returned and the index is set
* to it's position.
*
* @param string $string
*
* @return string|false returns the first character of the string or false
*/
protected function getNext($string)
{
// Find the next occurrence of "string" after the current position.
$pos = strpos($this->input, $string, $this->index);
// If it's not there return false.
if ($pos === false) {
return false;
}
// Adjust position of index to jump ahead to the asked for string
$this->index = $pos;
// Return the first character of that string.
return $this->index < $this->len ? $this->input[$this->index] : false;
}
/**
* When a javascript string is detected this function crawls for the end of
* it and saves the whole string.
*
* @throws \RuntimeException Unclosed strings will throw an error
*/
protected function saveString()
{
$startpos = $this->index;
// saveString is always called after a gets cleared, so we push b into
// that spot.
$this->a = $this->b;
// If this isn't a string we don't need to do anything.
if (!isset($this->stringDelimiters[$this->a])) {
return;
}
// String type is the quote used, " or '
$stringType = $this->a;
// Echo out that starting quote
$this->echo($this->a);
// Loop until the string is done
// Grab the very next character and load it into a
while (($this->a = $this->getChar()) !== false) {
switch ($this->a) {
// If the string opener (single or double quote) is used
// output it and break out of the while loop-
// The string is finished!
case $stringType:
break 2;
// New lines in strings without line delimiters are bad- actual
// new lines will be represented by the string \n and not the actual
// character, so those will be treated just fine using the switch
// block below.
case "\n":
if ($stringType === '`') {
$this->echo($this->a);
} else {
throw new \RuntimeException('Unclosed string at position: '.$startpos);
}
break;
// Escaped characters get picked up here. If it's an escaped new line it's not really needed
case '\\':
// a is a slash. We want to keep it, and the next character,
// unless it's a new line. New lines as actual strings will be
// preserved, but escaped new lines should be reduced.
$this->b = $this->getChar();
// If b is a new line we discard a and b and restart the loop.
if ($this->b === "\n") {
break;
}
// echo out the escaped character and restart the loop.
$this->echo($this->a.$this->b);
break;
// Since we're not dealing with any special cases we simply
// output the character and continue our loop.
default:
$this->echo($this->a);
}
}
}
/**
* When a regular expression is detected this function crawls for the end of
* it and saves the whole regex.
*
* @throws \RuntimeException Unclosed regex will throw an error
*/
protected function saveRegex()
{
if ($this->a != ' ') {
$this->echo($this->a);
}
$this->echo($this->b);
while (($this->a = $this->getChar()) !== false) {
if ($this->a === '/') {
break;
}
if ($this->a === '\\') {
$this->echo($this->a);
$this->a = $this->getChar();
}
if ($this->a === "\n") {
throw new \RuntimeException('Unclosed regex pattern at position: '.$this->index);
}
$this->echo($this->a);
}
$this->b = $this->getReal();
}
/**
* Checks to see if a character is alphanumeric.
*
* @param string $char Just one character
*
* @return bool
*/
protected static function isAlphaNumeric($char)
{
return preg_match('/^[\w\$\pL]$/', $char) === 1 || $char == '/';
}
protected function endsInKeyword()
{
// When this function is called A is not yet assigned to output.
$testOutput = $this->output.$this->a;
foreach (static::$keywords as $keyword) {
if (preg_match('/[^\w]'.$keyword.'[ ]?$/i', $testOutput) === 1) {
return true;
}
}
return false;
}
/**
* Replace patterns in the given string and store the replacement.
*
* @param string $js The string to lock
*
* @return bool
*/
protected function lock($js)
{
/* lock things like <code>"asd" + ++x;</code> */
$lock = '"LOCK---'.crc32(time()).'"';
$matches = [];
preg_match('/([+-])(\s+)([+-])/S', $js, $matches);
if (empty($matches)) {
return $js;
}
$this->locks[$lock] = $matches[2];
$js = preg_replace('/([+-])\s+([+-])/S', "$1{$lock}$2", $js);
/* -- */
return $js;
}
/**
* Replace "locks" with the original characters.
*
* @param string $js The string to unlock
*
* @return bool
*/
protected function unlock($js)
{
if (empty($this->locks)) {
return $js;
}
foreach ($this->locks as $lock => $replacement) {
$js = str_replace($lock, $replacement, $js);
}
return $js;
}
}

28
class/sacy/phpsass.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace sacy;
class PhpSassSacy
{
public static function isAvailable()
{
return extension_loaded('sass')/* && defined('SASS_FLAVOR') && SASS_FLAVOR == 'sensational' */
;
}
public static function compile($file, $fragment, $load_path)
{
$sass = new \Sass();
$load_path = array_map(function ($x) {
return getcwd().$x;
}, $load_path);
$sass->setIncludePath(implode(':', $load_path));
if ($file) {
return $sass->compile_file($file);
} else {
return $sass->compile($fragment);
}
}
}

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;
}
}