<commit_before><?php
/**
 * @package    Grav.Common
 *
 * @copyright  Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Common;

use Closure;
use Exception;
use FilesystemIterator;
use Grav\Common\Config\Config;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;

define('CSS_ASSET', true);
define('JS_ASSET', false);

class Assets
{
    /** @const Regex to match CSS and JavaScript files */
    const DEFAULT_REGEX = '/.\.(css|js)$/i';

    /** @const Regex to match CSS files */
    const CSS_REGEX = '/.\.css$/i';

    /** @const Regex to match JavaScript files */
    const JS_REGEX = '/.\.js$/i';

    /** @const Regex to match CSS urls */
    const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';

    /** @const Regex to match CSS sourcemap comments */
    const CSS_SOURCEMAP_REGEX = '{\/\*# (.*) \*\/}';

    /** @const Regex to match CSS import content */
    const CSS_IMPORT_REGEX = '{@import(.*);}';

    const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\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;
        }

        $query = [];

        $modified = false;
        $remote = $this->isRemoteLink($asset);
        if (!$remote) {

            $asset_parts = parse_url($asset);
            if (isset($asset_parts['query'])) {
                $query[] = $asset_parts['query'];
                unset($asset_parts['query']);
                $asset = Uri::buildUrl($asset_parts);
            }


            $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,
            'query'    => implode('&', $query),
        ];

        // 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, $attributes = 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',
            'type'     => $attributes ?: '',
        ];

        // 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, array($this, 'sortAssetsByPriorityThenOrder'));
            uasort($this->inline_css, array($this, 'sortAssetsByPriorityThenOrder'));
        }

        $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 ? '' : '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\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 .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\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 .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\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<style>\n" . $inline_css . "\n</style>\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, array($this, 'sortAssetsByPriorityThenOrder'));
        uasort($this->inline_js, array($this, 'sortAssetsByPriorityThenOrder'));

        $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 ? '' : '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\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 .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\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 .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
                    }
                }
            }
        }

        // Render Inline JS
        foreach ($this->inline_js as $inline) {
            if ($group && $inline['group'] == $group) {
                $inline_js .= $inline['asset'] . "\n";
            }
        }

        if ($inline_js) {
            if ($inline['type']){
            $attributeString = " type=\"" . $inline['type'] . "\""; 
            }
            $output .= "\n<script" . $attributeString . ">" . $inline_js . "\n</script>\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';<commit_msg>Fix for bugs arising from last PR merge<commit_after><?php
/**
 * @package    Grav.Common
 *
 * @copyright  Copyright (C) 2014 - 2017 RocketTheme, LLC. All rights reserved.
 * @license    MIT License; see LICENSE file for details.
 */

namespace Grav\Common;

use Closure;
use Exception;
use FilesystemIterator;
use Grav\Common\Config\Config;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;

define('CSS_ASSET', true);
define('JS_ASSET', false);

class Assets
{
    /** @const Regex to match CSS and JavaScript files */
    const DEFAULT_REGEX = '/.\.(css|js)$/i';

    /** @const Regex to match CSS files */
    const CSS_REGEX = '/.\.css$/i';

    /** @const Regex to match JavaScript files */
    const JS_REGEX = '/.\.js$/i';

    /** @const Regex to match CSS urls */
    const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';

    /** @const Regex to match CSS sourcemap comments */
    const CSS_SOURCEMAP_REGEX = '{\/\*# (.*) \*\/}';

    /** @const Regex to match CSS import content */
    const CSS_IMPORT_REGEX = '{@import(.*);}';

    const HTML_TAG_REGEX = '#(<([A-Z][A-Z0-9]*)>)+(.*)(<\/\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;
        }

        $query = [];

        $modified = false;
        $remote = $this->isRemoteLink($asset);
        if (!$remote) {

            $asset_parts = parse_url($asset);
            if (isset($asset_parts['query'])) {
                $query[] = $asset_parts['query'];
                unset($asset_parts['query']);
                $asset = Uri::buildUrl($asset_parts);
            }


            $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,
            'query'    => implode('&', $query),
        ];

        // 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
     * @param null $attributes
     *
     * @return $this
     */
    public function addInlineJs($asset, $priority = null, $group = null, $attributes = 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',
            'type'     => $attributes ?: '',
        ];

        // 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, array($this, 'sortAssetsByPriorityThenOrder'));
            uasort($this->inline_css, array($this, 'sortAssetsByPriorityThenOrder'));
        }

        $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 ? '' : '<link href="' . $pipeline_result . '"' . $attributes . ' />' . "\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 .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\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 .= '<link href="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . $media . ' />' . "\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<style>\n" . $inline_css . "\n</style>\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, array($this, 'sortAssetsByPriorityThenOrder'));
        uasort($this->inline_js, array($this, 'sortAssetsByPriorityThenOrder'));

        $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 ? '' : '<script src="' . $pipeline_result . '"' . $attributes . ' ></script>' . "\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 .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\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 .= '<script src="' . $file['asset'] . $this->getQuerystring($file) . '"' . $attributes . ' ' . $file['loading'] . '></script>' . "\n";
                    }
                }
            }
        }

        // Render Inline JS
        foreach ($this->inline_js as $inline) {
            if ($group && $inline['group'] == $group) {
                $inline_js .= $inline['asset'] . "\n";
            }
        }

        if ($inline_js) {
            $attribute_string = isset($inline) && $inline['type'] ? " type=\"" . $inline['type'] . "\"" : '';
            $output .= "\n<script" . $attribute_string . ">\n" . $inline_js . "\n</script>\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';
{'fraction_non_alphanumeric': 0.14432217179069967, 'fraction_numerical': 0.0014130341697353773, 'mean_word_length': 1.8604936003429482, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 265, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4991270', 'n_tokens_mistral': 13466, 'n_tokens_neox': 13164, 'n_words': 3328}