)+(.*)(<\/\2>)#is';
/**
* Closure used by the pipeline to fetch assets.
*
* Useful when file_get_contents() function is not available in your PHP
* installation or when you want to apply any kind of preprocessing to
* your assets before they get pipelined.
*
* The closure will receive as the only parameter a string with the path/URL of the asset and
* it should return the content of the asset file as a string.
*
* @var Closure
*/
protected $fetch_command;
// Configuration toggles to enable/disable the pipelining feature
protected $css_pipeline = false;
protected $css_pipeline_include_externals = true;
protected $css_pipeline_before_excludes = true;
protected $js_pipeline = false;
protected $js_pipeline_include_externals = true;
protected $js_pipeline_before_excludes = true;
// The asset holding arrays
protected $collections = [];
protected $css = [];
protected $js = [];
protected $inline_css = [];
protected $inline_js = [];
protected $imports = [];
// Some configuration variables
protected $config;
protected $base_url;
protected $timestamp = '';
protected $assets_dir;
protected $assets_url;
// Default values for pipeline settings
protected $css_minify = true;
protected $css_minify_windows = false;
protected $css_rewrite = true;
protected $js_minify = true;
// Arrays to hold assets that should NOT be pipelined
protected $css_no_pipeline = [];
protected $js_no_pipeline = [];
/**
* Assets constructor.
*
* @param array $options
*/
public function __construct(array $options = [])
{
// Forward config options
if ($options) {
$this->config((array)$options);
}
}
/**
* Set up configuration options.
*
* All the class properties except 'js' and 'css' are accepted here.
* Also, an extra option 'autoload' may be passed containing an array of
* assets and/or collections that will be automatically added on startup.
*
* @param array $config Configurable options.
*
* @return $this
* @throws \Exception
*/
public function config(array $config)
{
// Set pipeline modes
if (isset($config['css_pipeline'])) {
$this->css_pipeline = $config['css_pipeline'];
}
if (isset($config['css_pipeline_include_externals'])) {
$this->css_pipeline_include_externals = $config['css_pipeline_include_externals'];
}
if (isset($config['css_pipeline_before_excludes'])) {
$this->css_pipeline_before_excludes = $config['css_pipeline_before_excludes'];
}
if (isset($config['js_pipeline'])) {
$this->js_pipeline = $config['js_pipeline'];
}
if (isset($config['js_pipeline_include_externals'])) {
$this->js_pipeline_include_externals = $config['js_pipeline_include_externals'];
}
if (isset($config['js_pipeline_before_excludes'])) {
$this->js_pipeline_before_excludes = $config['js_pipeline_before_excludes'];
}
// Pipeline requires public dir
if (($this->js_pipeline || $this->css_pipeline) && !is_dir($this->assets_dir)) {
throw new \Exception('Assets: Public dir not found');
}
// Set custom pipeline fetch command
if (isset($config['fetch_command']) && ($config['fetch_command'] instanceof Closure)) {
$this->fetch_command = $config['fetch_command'];
}
// Set CSS Minify state
if (isset($config['css_minify'])) {
$this->css_minify = $config['css_minify'];
}
if (isset($config['css_minify_windows'])) {
$this->css_minify_windows = $config['css_minify_windows'];
}
if (isset($config['css_rewrite'])) {
$this->css_rewrite = $config['css_rewrite'];
}
// Set JS Minify state
if (isset($config['js_minify'])) {
$this->js_minify = $config['js_minify'];
}
// Set collections
if (isset($config['collections']) && is_array($config['collections'])) {
$this->collections = $config['collections'];
}
// Autoload assets
if (isset($config['autoload']) && is_array($config['autoload'])) {
foreach ($config['autoload'] as $asset) {
$this->add($asset);
}
}
// Set timestamp
if (isset($config['enable_asset_timestamp']) && $config['enable_asset_timestamp'] === true) {
$this->timestamp = '?' . Grav::instance()['cache']->getKey();
}
return $this;
}
/**
* Initialization called in the Grav lifecycle to initialize the Assets with appropriate configuration
*/
public function init()
{
$grav = Grav::instance();
/** @var Config $config */
$config = $grav['config'];
$base_url = $grav['base_url'];
$asset_config = (array)$config->get('system.assets');
/** @var UniformResourceLocator $locator */
$locator = $grav['locator'];
$this->assets_dir = $locator->findResource('asset://') . DS;
$this->assets_url = $locator->findResource('asset://', false);
$this->config($asset_config);
$this->base_url = ($config->get('system.absolute_urls') ? '' : '/') . ltrim(ltrim($base_url, '/') . '/', '/');
// Register any preconfigured collections
foreach ($config->get('system.assets.collections', []) as $name => $collection) {
$this->registerCollection($name, (array)$collection);
}
}
/**
* Add an asset or a collection of assets.
*
* It automatically detects the asset type (JavaScript, CSS or collection).
* You may add more than one asset passing an array as argument.
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param bool $pipeline false if this should not be pipelined
*
* @return $this
*/
public function add($asset, $priority = null, $pipeline = true)
{
// More than one asset
if (is_array($asset)) {
foreach ($asset as $a) {
$this->add($a, $priority, $pipeline);
}
} elseif (isset($this->collections[$asset])) {
$this->add($this->collections[$asset], $priority, $pipeline);
} else {
// Get extension
$extension = pathinfo(parse_url($asset, PHP_URL_PATH), PATHINFO_EXTENSION);
// JavaScript or CSS
if (strlen($extension) > 0) {
$extension = strtolower($extension);
if ($extension === 'css') {
$this->addCss($asset, $priority, $pipeline);
} elseif ($extension === 'js') {
$this->addJs($asset, $priority, $pipeline);
}
}
}
return $this;
}
/**
* Add an asset to its assembly.
*
* It checks for duplicates.
* You may add more than one asset passing an array as argument.
* The third argument may alternatively contain an array of options which take precedence over positional
* arguments.
*
* @param array $assembly the array assembling the assets
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param bool $pipeline false if this should not be pipelined
* @param string $loading how the asset is loaded (async/defer/inline, for CSS: only inline)
* @param string $group name of the group
*
* @return $this
*/
public function addTo(&$assembly, $asset, $priority = null, $pipeline = true, $loading = null, $group = null)
{
if (is_array($asset)) {
foreach ($asset as $a) {
$this->addTo($assembly, $a, $priority, $pipeline, $loading, $group);
}
return $this;
} elseif (isset($this->collections[$asset])) {
$this->addTo($assembly, $this->collections[$asset], $priority, $pipeline, $loading, $group);
return $this;
}
$modified = false;
$remote = $this->isRemoteLink($asset);
if (!$remote) {
$modified = $this->getLastModificationTime($asset);
$asset = $this->buildLocalLink($asset);
}
// Check for existence
if ($asset === false) {
return $this;
}
$data = [
'asset' => $asset,
'remote' => $remote,
'priority' => intval($priority ?: 10),
'order' => count($assembly),
'pipeline' => (bool) $pipeline,
'loading' => $loading ?: '',
'group' => $group ?: 'head',
'modified' => $modified
];
// check for dynamic array and merge with defaults
if (func_num_args() > 2) {
$dynamic_arg = func_get_arg(2);
if (is_array($dynamic_arg)) {
$data = array_merge($data, $dynamic_arg);
}
}
$key = md5($asset);
if ($asset) {
$assembly[$key] = $data;
}
return $this;
}
/**
* Add a CSS asset.
*
* It checks for duplicates.
* You may add more than one asset passing an array as argument.
* The second argument may alternatively contain an array of options which take precedence over positional
* arguments.
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param bool $pipeline false if this should not be pipelined
* @param string $group
* @param string $loading how the asset is loaded (async/defer/inline, for CSS: only inline)
*
* @return $this
*/
public function addCss($asset, $priority = null, $pipeline = true, $group = null, $loading = null)
{
return $this->addTo($this->css, $asset, $priority, $pipeline, $loading, $group);
}
/**
* Add a JavaScript asset.
*
* It checks for duplicates.
* You may add more than one asset passing an array as argument.
* The second argument may alternatively contain an array of options which take precedence over positional
* arguments.
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param bool $pipeline false if this should not be pipelined
* @param string $loading how the asset is loaded (async/defer)
* @param string $group name of the group
*
* @return $this
*/
public function addJs($asset, $priority = null, $pipeline = true, $loading = null, $group = null)
{
return $this->addTo($this->js, $asset, $priority, $pipeline, $loading, $group);
}
/**
* Convenience wrapper for async loading of JavaScript
*
* @param $asset
* @param int $priority
* @param bool $pipeline
* @param string $group name of the group
*
* @deprecated Please use dynamic method with ['loading' => 'async']
*
* @return \Grav\Common\Assets
*/
public function addAsyncJs($asset, $priority = null, $pipeline = true, $group = null)
{
return $this->addJs($asset, $priority, $pipeline, 'async', $group);
}
/**
* Convenience wrapper for deferred loading of JavaScript
*
* @param $asset
* @param int $priority
* @param bool $pipeline
* @param string $group name of the group
*
* @deprecated Please use dynamic method with ['loading' => 'defer']
*
* @return \Grav\Common\Assets
*/
public function addDeferJs($asset, $priority = null, $pipeline = true, $group = null)
{
return $this->addJs($asset, $priority, $pipeline, 'defer', $group);
}
/**
* Add an inline CSS asset.
*
* It checks for duplicates.
* For adding chunks of string-based inline CSS
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param null $group
*
* @return $this
*/
public function addInlineCss($asset, $priority = null, $group = null)
{
$asset = trim($asset);
if (is_a($asset, 'Twig_Markup')) {
preg_match(self::HTML_TAG_REGEX, $asset, $matches);
if (isset($matches[3])) {
$asset = $matches[3];
}
}
$data = [
'priority' => intval($priority ?: 10),
'order' => count($this->inline_css),
'asset' => $asset,
'group' => $group ?: 'head'
];
// check for dynamic array and merge with defaults
if (func_num_args() == 2) {
$dynamic_arg = func_get_arg(1);
if (is_array($dynamic_arg)) {
$data = array_merge($data, $dynamic_arg);
}
}
$key = md5($asset);
if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_css)) {
$this->inline_css[$key] = $data;
}
return $this;
}
/**
* Add an inline JS asset.
*
* It checks for duplicates.
* For adding chunks of string-based inline JS
*
* @param mixed $asset
* @param int $priority the priority, bigger comes first
* @param string $group name of the group
*
* @return $this
*/
public function addInlineJs($asset, $priority = null, $group = null)
{
$asset = trim($asset);
if (is_a($asset, 'Twig_Markup')) {
preg_match(self::HTML_TAG_REGEX, $asset, $matches);
if (isset($matches[3])) {
$asset = $matches[3];
}
}
$data = [
'asset' => $asset,
'priority' => intval($priority ?: 10),
'order' => count($this->js),
'group' => $group ?: 'head'
];
// check for dynamic array and merge with defaults
if (func_num_args() == 2) {
$dynamic_arg = func_get_arg(1);
if (is_array($dynamic_arg)) {
$data = array_merge($data, $dynamic_arg);
}
}
$key = md5($asset);
if ($asset && is_string($asset) && !array_key_exists($key, $this->inline_js)) {
$this->inline_js[$key] = $data;
}
return $this;
}
/**
* Build the CSS link tags.
*
* @param string $group name of the group
* @param array $attributes
*
* @return string
*/
public function css($group = 'head', $attributes = [])
{
if (!$this->css && !$this->inline_css) {
return null;
}
// Sort array by priorities (larger priority first)
if (Grav::instance()) {
uasort($this->css, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
uasort($this->inline_css, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
}
$this->css = array_reverse($this->css);
$this->inline_css = array_reverse($this->inline_css);
$inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
$attributes = $this->attributes(array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes));
$output = '';
$inline_css = '';
if ($this->css_pipeline) {
$pipeline_result = $this->pipelineCss($group, !$inlineGroup);
$pipeline_html = ($inlineGroup ? '' : '' . "\n");
if ($this->css_pipeline_before_excludes && $pipeline_result) {
if ($inlineGroup) {
$inline_css .= $pipeline_result;
}
else {
$output .= $pipeline_html;
}
}
foreach ($this->css_no_pipeline as $file) {
if ($group && $file['group'] == $group) {
if ($file['loading'] === 'inline') {
$inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
}
else {
$media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
$output .= '' . "\n";
}
}
}
if (!$this->css_pipeline_before_excludes && $pipeline_result) {
if ($inlineGroup) {
$inline_css .= $pipeline_result;
}
else {
$output .= $pipeline_html;
}
}
} else {
foreach ($this->css as $file) {
if ($group && $file['group'] == $group) {
if ($inlineGroup || $file['loading'] === 'inline') {
$inline_css .= $this->gatherLinks([$file], CSS_ASSET) . "\n";
}
else {
$media = isset($file['media']) ? sprintf(' media="%s"', $file['media']) : '';
$output .= '' . "\n";
}
}
}
}
// Render Inline CSS
foreach ($this->inline_css as $inline) {
if ($group && $inline['group'] == $group) {
$inline_css .= $inline['asset'] . "\n";
}
}
if ($inline_css) {
$output .= "\n\n";
}
return $output;
}
/**
* Build the JavaScript script tags.
*
* @param string $group name of the group
* @param array $attributes
*
* @return string
*/
public function js($group = 'head', $attributes = [])
{
if (!$this->js && !$this->inline_js) {
return null;
}
// Sort array by priorities (larger priority first)
uasort($this->js, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
uasort($this->inline_js, function ($a, $b) {
if ($a['priority'] == $b['priority']) {
return $b['order'] - $a['order'];
}
return $a['priority'] - $b['priority'];
});
$this->js = array_reverse($this->js);
$this->inline_js = array_reverse($this->inline_js);
$inlineGroup = array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline';
$attributes = $this->attributes(array_merge(['type' => 'text/javascript'], $attributes));
$output = '';
$inline_js = '';
if ($this->js_pipeline) {
$pipeline_result = $this->pipelineJs($group, !$inlineGroup);
$pipeline_html = ($inlineGroup ? '' : '' . "\n");
if ($this->js_pipeline_before_excludes && $pipeline_result) {
if ($inlineGroup) {
$inline_js .= $pipeline_result;
}
else {
$output .= $pipeline_html;
}
}
foreach ($this->js_no_pipeline as $file) {
if ($group && $file['group'] == $group) {
if ($file['loading'] === 'inline') {
$inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
}
else {
$output .= '' . "\n";
}
}
}
if (!$this->js_pipeline_before_excludes && $pipeline_result) {
if ($inlineGroup) {
$inline_js .= $pipeline_result;
}
else {
$output .= $pipeline_html;
}
}
} else {
foreach ($this->js as $file) {
if ($group && $file['group'] == $group) {
if ($inlineGroup || $file['loading'] === 'inline') {
$inline_js .= $this->gatherLinks([$file], JS_ASSET) . "\n";
}
else {
$output .= '' . "\n";
}
}
}
}
// Render Inline JS
foreach ($this->inline_js as $inline) {
if ($group && $inline['group'] == $group) {
$inline_js .= $inline['asset'] . "\n";
}
}
if ($inline_js) {
$output .= "\n\n";
}
return $output;
}
/**
* Minify and concatenate CSS
*
* @param string $group
* @param bool $returnURL true if pipeline should return the URL, otherwise the content
*
* @return bool|string URL or generated content if available, else false
*/
protected function pipelineCss($group = 'head', $returnURL = true)
{
// temporary list of assets to pipeline
$temp_css = [];
// clear no-pipeline assets lists
$this->css_no_pipeline = [];
// Compute uid based on assets and timestamp
$uid = md5(json_encode($this->css) . $this->css_minify . $this->css_rewrite . $group);
$file = $uid . '.css';
$inline_file = $uid . '-inline.css';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
// If inline files exist set them on object
if (file_exists($this->assets_dir . $inline_file)) {
$this->css_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
}
// If pipeline exist return its URL or content
if (file_exists($this->assets_dir . $file)) {
if ($returnURL) {
return $relative_path . $this->getTimestamp();
}
else {
return file_get_contents($this->assets_dir . $file) . "\n";
}
}
// Remove any non-pipeline files
foreach ($this->css as $id => $asset) {
if ($asset['group'] == $group) {
if (!$asset['pipeline'] ||
($asset['remote'] && $this->css_pipeline_include_externals === false)) {
$this->css_no_pipeline[$id] = $asset;
} else {
$temp_css[$id] = $asset;
}
}
}
//if nothing found get out of here!
if (count($temp_css) == 0) {
return false;
}
// Write non-pipeline files out
if (!empty($this->css_no_pipeline)) {
file_put_contents($this->assets_dir . $inline_file, json_encode($this->css_no_pipeline));
}
$css_minify = $this->css_minify;
// If this is a Windows server, and minify_windows is false (default value) skip the
// minification process because it will cause Apache to die/crash due to insufficient
// ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
if (strtoupper(substr(php_uname('s'), 0, 3)) === 'WIN' && !$this->css_minify_windows) {
$css_minify = false;
}
// Concatenate files
$buffer = $this->gatherLinks($temp_css, CSS_ASSET);
if ($css_minify) {
$minifier = new \MatthiasMullie\Minify\CSS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
// Write file
if (strlen(trim($buffer)) > 0) {
file_put_contents($this->assets_dir . $file, $buffer);
if ($returnURL) {
return $relative_path . $this->getTimestamp();
}
else {
return $buffer . "\n";
}
} else {
return false;
}
}
/**
* Minify and concatenate JS files.
*
* @param string $group
* @param bool $returnURL true if pipeline should return the URL, otherwise the content
*
* @return bool|string URL or generated content if available, else false
*/
protected function pipelineJs($group = 'head', $returnURL = true)
{
// temporary list of assets to pipeline
$temp_js = [];
// clear no-pipeline assets lists
$this->js_no_pipeline = [];
// Compute uid based on assets and timestamp
$uid = md5(json_encode($this->js) . $this->js_minify . $group);
$file = $uid . '.js';
$inline_file = $uid . '-inline.js';
$relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
// If inline files exist set them on object
if (file_exists($this->assets_dir . $inline_file)) {
$this->js_no_pipeline = json_decode(file_get_contents($this->assets_dir . $inline_file), true);
}
// If pipeline exist return its URL or content
if (file_exists($this->assets_dir . $file)) {
if ($returnURL) {
return $relative_path . $this->getTimestamp();
}
else {
return file_get_contents($this->assets_dir . $file) . "\n";
}
}
// Remove any non-pipeline files
foreach ($this->js as $id => $asset) {
if ($asset['group'] == $group) {
if (!$asset['pipeline'] ||
($asset['remote'] && $this->js_pipeline_include_externals === false)) {
$this->js_no_pipeline[] = $asset;
} else {
$temp_js[$id] = $asset;
}
}
}
//if nothing found get out of here!
if (count($temp_js) == 0) {
return false;
}
// Write non-pipeline files out
if (!empty($this->js_no_pipeline)) {
file_put_contents($this->assets_dir . $inline_file, json_encode($this->js_no_pipeline));
}
// Concatenate files
$buffer = $this->gatherLinks($temp_js, JS_ASSET);
if ($this->js_minify) {
$minifier = new \MatthiasMullie\Minify\JS();
$minifier->add($buffer);
$buffer = $minifier->minify();
}
// Write file
if (strlen(trim($buffer)) > 0) {
file_put_contents($this->assets_dir . $file, $buffer);
if ($returnURL) {
return $relative_path . $this->getTimestamp();
}
else {
return $buffer . "\n";
}
} else {
return false;
}
}
/**
* Return the array of all the registered CSS assets
* If a $key is provided, it will try to return only that asset
* else it will return null
*
* @param null|string $key the asset key
* @return array
*/
public function getCss($key = null)
{
if (!empty($key)) {
$asset_key = md5($key);
if (isset($this->css[$asset_key])) {
return $this->css[$asset_key];
} else {
return null;
}
}
return $this->css;
}
/**
* Return the array of all the registered JS assets
* If a $key is provided, it will try to return only that asset
* else it will return null
*
* @param null|string $key the asset key
* @return array
*/
public function getJs($key = null)
{
if (!empty($key)) {
$asset_key = md5($key);
if (isset($this->js[$asset_key])) {
return $this->js[$asset_key];
} else {
return null;
}
}
return $this->js;
}
/**
* Set the whole array of CSS assets
*
* @param $css
*/
public function setCss($css)
{
$this->css = $css;
}
/**
* Set the whole array of JS assets
*
* @param $js
*/
public function setJs($js)
{
$this->js = $js;
}
/**
* Removes an item from the CSS array if set
*
* @param string $key The asset key
*/
public function removeCss($key)
{
$asset_key = md5($key);
if (isset($this->css[$asset_key])) {
unset($this->css[$asset_key]);
}
}
/**
* Removes an item from the JS array if set
*
* @param string $key The asset key
*/
public function removeJs($key)
{
$asset_key = md5($key);
if (isset($this->js[$asset_key])) {
unset($this->js[$asset_key]);
}
}
/**
* Return the array of all the registered collections
*
* @return array
*/
public function getCollections()
{
return $this->collections;
}
/**
* Set the array of collections explicitly
*
* @param $collections
*/
public function setCollection($collections)
{
$this->collections = $collections;
}
/**
* Determines if an asset exists as a collection, CSS or JS reference
*
* @param $asset
*
* @return bool
*/
public function exists($asset)
{
if (isset($this->collections[$asset]) || isset($this->css[$asset]) || isset($this->js[$asset])) {
return true;
} else {
return false;
}
}
/**
* Add/replace collection.
*
* @param string $collectionName
* @param array $assets
* @param bool $overwrite
*
* @return $this
*/
public function registerCollection($collectionName, Array $assets, $overwrite = false)
{
if ($overwrite || !isset($this->collections[$collectionName])) {
$this->collections[$collectionName] = $assets;
}
return $this;
}
/**
* Reset all assets.
*
* @return $this
*/
public function reset()
{
return $this->resetCss()->resetJs();
}
/**
* Reset JavaScript assets.
*
* @return $this
*/
public function resetJs()
{
$this->js = [];
$this->inline_js = [];
return $this;
}
/**
* Reset CSS assets.
*
* @return $this
*/
public function resetCss()
{
$this->css = [];
$this->inline_css = [];
return $this;
}
/**
* Add all JavaScript assets within $directory
*
* @param string $directory Relative to the Grav root path, or a stream identifier
*
* @return $this
*/
public function addDirJs($directory)
{
return $this->addDir($directory, self::JS_REGEX);
}
/**
* Add all CSS assets within $directory
*
* @param string $directory Relative to the Grav root path, or a stream identifier
*
* @return $this
*/
public function addDirCss($directory)
{
return $this->addDir($directory, self::CSS_REGEX);
}
/**
* Add all assets matching $pattern within $directory.
*
* @param string $directory Relative to the Grav root path, or a stream identifier
* @param string $pattern (regex)
*
* @return $this
* @throws Exception
*/
public function addDir($directory, $pattern = self::DEFAULT_REGEX)
{
$root_dir = rtrim(ROOT_DIR, '/');
// Check if $directory is a stream.
if (strpos($directory, '://')) {
$directory = Grav::instance()['locator']->findResource($directory, null);
}
// Get files
$files = $this->rglob($root_dir . DIRECTORY_SEPARATOR . $directory, $pattern, $root_dir . '/');
// No luck? Nothing to do
if (!$files) {
return $this;
}
// Add CSS files
if ($pattern === self::CSS_REGEX) {
foreach ($files as $file) {
$this->addCss($file);
}
return $this;
}
// Add JavaScript files
if ($pattern === self::JS_REGEX) {
foreach ($files as $file) {
$this->addJs($file);
}
return $this;
}
// Unknown pattern.
foreach ($files as $asset) {
$this->add($asset);
}
return $this;
}
/**
* Determine whether a link is local or remote.
*
* Understands both "http://" and "https://" as well as protocol agnostic links "//"
*
* @param string $link
*
* @return bool
*/
protected function isRemoteLink($link)
{
$base = Grav::instance()['uri']->rootUrl(true);
// sanity check for local URLs with absolute URL's enabled
if (Utils::startsWith($link, $base)) {
return false;
}
return ('http://' === substr($link, 0, 7) || 'https://' === substr($link, 0, 8) || '//' === substr($link, 0,
2));
}
/**
* Build local links including grav asset shortcodes
*
* @param string $asset the asset string reference
* @param bool $absolute build absolute asset link
*
* @return string the final link url to the asset
*/
protected function buildLocalLink($asset, $absolute = false)
{
try {
$asset = Grav::instance()['locator']->findResource($asset, $absolute);
} catch (\Exception $e) {
}
$uri = $absolute ? $asset : $this->base_url . ltrim($asset, '/');
return $asset ? $uri : false;
}
/**
* Get the last modification time of asset
*
* @param string $asset the asset string reference
*
* @return string the last modifcation time or false on error
*/
protected function getLastModificationTime($asset)
{
$file = GRAV_ROOT . $asset;
if (Grav::instance()['locator']->isStream($asset)) {
$file = $this->buildLocalLink($asset, true);
}
return file_exists($file) ? filemtime($file) : false;
}
/**
* Build an HTML attribute string from an array.
*
* @param array $attributes
*
* @return string
*/
protected function attributes(array $attributes)
{
$html = '';
$no_key = ['loading'];
foreach ($attributes as $key => $value) {
// For numeric keys we will assume that the key and the value are the same
// as this will convert HTML attributes such as "required" to a correct
// form like required="required" instead of using incorrect numerics.
if (is_numeric($key)) {
$key = $value;
}
if (is_array($value)) {
$value = implode(' ', $value);
}
if (in_array($key, $no_key)) {
$element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
} else {
$element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
}
$html .= ' ' . $element;
}
return $html;
}
/**
* Download and concatenate the content of several links.
*
* @param array $links
* @param bool $css
*
* @return string
*/
protected function gatherLinks(array $links, $css = true)
{
$buffer = '';
foreach ($links as $asset) {
$relative_dir = '';
$local = true;
$link = $asset['asset'];
$relative_path = $link;
if ($this->isRemoteLink($link)) {
$local = false;
if ('//' === substr($link, 0, 2)) {
$link = 'http:' . $link;
}
} else {
// Fix to remove relative dir if grav is in one
if (($this->base_url != '/') && (strpos($this->base_url, $link) == 0)) {
$base_url = '#' . preg_quote($this->base_url, '#') . '#';
$relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
}
$relative_dir = dirname($relative_path);
$link = ROOT_DIR . $relative_path;
}
$file = ($this->fetch_command instanceof Closure) ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
// No file found, skip it...
if ($file === false) {
continue;
}
// Double check last character being
if (!$css) {
$file = rtrim($file, ' ;') . ';';
}
// If this is CSS + the file is local + rewrite enabled
if ($css && $local && $this->css_rewrite) {
$file = $this->cssRewrite($file, $relative_dir);
}
$file = rtrim($file) . PHP_EOL;
$buffer .= $file;
}
// Pull out @imports and move to top
if ($css) {
$buffer = $this->moveImports($buffer);
}
return $buffer;
}
/**
* Finds relative CSS urls() and rewrites the URL with an absolute one
*
* @param string $file the css source file
* @param string $relative_path relative path to the css file
*
* @return mixed
*/
protected function cssRewrite($file, $relative_path)
{
// Strip any sourcemap comments
$file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
// Find any css url() elements, grab the URLs and calculate an absolute path
// Then replace the old url with the new one
$file = preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($relative_path) {
$old_url = $matches[2];
// Ensure link is not rooted to webserver, a data URL, or to a remote host
if (Utils::startsWith($old_url, '/') || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
return $matches[0];
}
$new_url = $this->base_url . ltrim(Utils::normalizePath($relative_path . '/' . $old_url), '/');
return str_replace($old_url, $new_url, $matches[0]);
}, $file);
return $file;
}
/**
* Moves @import statements to the top of the file per the CSS specification
*
* @param string $file the file containing the combined CSS files
*
* @return string the modified file with any @imports at the top of the file
*/
protected function moveImports($file)
{
$this->imports = [];
$file = preg_replace_callback(self::CSS_IMPORT_REGEX, function ($matches) {
$this->imports[] = $matches[0];
return '';
}, $file);
return implode("\n", $this->imports) . "\n\n" . $file;
}
/**
* Recursively get files matching $pattern within $directory.
*
* @param string $directory
* @param string $pattern (regex)
* @param string $ltrim Will be trimmed from the left of the file path
*
* @return array
*/
protected function rglob($directory, $pattern, $ltrim = null)
{
$iterator = new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory,
FilesystemIterator::SKIP_DOTS)), $pattern);
$offset = strlen($ltrim);
$files = [];
foreach ($iterator as $file) {
$files[] = substr($file->getPathname(), $offset);
}
return $files;
}
/**
* Sets the state of CSS Pipeline
*
* @param boolean $value
*/
public function setCssPipeline($value)
{
$this->css_pipeline = (bool)$value;
}
/**
* Sets the state of JS Pipeline
*
* @param boolean $value
*/
public function setJsPipeline($value)
{
$this->js_pipeline = (bool)$value;
}
/**
* Explicitly set's a timestamp for assets
*
* @param $value
*/
public function setTimestamp($value)
{
$this->timestamp = '?' . $value;
}
public function getTimestamp($asset = null)
{
if (is_array($asset)) {
if ($asset['remote'] === false) {
if (Utils::contains($asset['asset'], '?')) {
return str_replace('?', '&', $this->timestamp);
} else {
return $this->timestamp;
}
}
} elseif (empty($asset)) {
return $this->timestamp;
}
}
/**
* @return string
*/
public function __toString()
{
return '';
}
/**
* @param $a
* @param $b
*
* @return mixed
*/
protected function priorityCompare($a, $b)
{
return $a ['priority'] - $b ['priority'];
}
}