Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetCollectionGrouperInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Interface defining a service that logically groups a collection of assets.
|
||||
*/
|
||||
interface AssetCollectionGrouperInterface {
|
||||
|
||||
/**
|
||||
* Groups a collection of assets into logical groups of asset collections.
|
||||
*
|
||||
* @param array $assets
|
||||
* An asset collection.
|
||||
*
|
||||
* @return array
|
||||
* A sorted array of asset groups.
|
||||
*/
|
||||
public function group(array $assets);
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetCollectionOptimizerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Interface defining a service that optimizes a collection of assets.
|
||||
*/
|
||||
interface AssetCollectionOptimizerInterface {
|
||||
|
||||
/**
|
||||
* Optimizes a collection of assets.
|
||||
*
|
||||
* @param array $assets
|
||||
* An asset collection.
|
||||
*
|
||||
* @return array
|
||||
* An optimized asset collection.
|
||||
*/
|
||||
public function optimize(array $assets);
|
||||
|
||||
/**
|
||||
* Returns all optimized asset collections assets.
|
||||
*
|
||||
* @return string[]
|
||||
* URIs for all optimized asset collection assets.
|
||||
*/
|
||||
public function getAll();
|
||||
|
||||
/**
|
||||
* Deletes all optimized asset collections assets.
|
||||
*/
|
||||
public function deleteAll();
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetCollectionRendererInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Interface defining a service that generates a render array to render assets.
|
||||
*/
|
||||
interface AssetCollectionRendererInterface {
|
||||
|
||||
/**
|
||||
* Renders an asset collection.
|
||||
*
|
||||
* @param array $assets
|
||||
* An asset collection.
|
||||
*
|
||||
* @return array
|
||||
* A render array to render the asset collection.
|
||||
*/
|
||||
public function render(array $assets);
|
||||
|
||||
}
|
53
core/lib/Drupal/Core/Asset/AssetDumper.php
Normal file
53
core/lib/Drupal/Core/Asset/AssetDumper.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetDumper.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\AssetDumperInterface;
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
|
||||
/**
|
||||
* Dumps a CSS or JavaScript asset.
|
||||
*/
|
||||
class AssetDumper implements AssetDumperInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* The file name for the CSS or JS cache file is generated from the hash of
|
||||
* the aggregated contents of the files in $data. This forces proxies and
|
||||
* browsers to download new CSS when the CSS changes.
|
||||
*/
|
||||
public function dump($data, $file_extension) {
|
||||
// Prefix filename to prevent blocking by firewalls which reject files
|
||||
// starting with "ad*".
|
||||
$filename = $file_extension. '_' . Crypt::hashBase64($data) . '.' . $file_extension;
|
||||
// Create the css/ or js/ path within the files folder.
|
||||
$path = 'public://' . $file_extension;
|
||||
$uri = $path . '/' . $filename;
|
||||
// Create the CSS or JS file.
|
||||
file_prepare_directory($path, FILE_CREATE_DIRECTORY);
|
||||
if (!file_exists($uri) && !file_unmanaged_save_data($data, $uri, FILE_EXISTS_REPLACE)) {
|
||||
return FALSE;
|
||||
}
|
||||
// If CSS/JS gzip compression is enabled and the zlib extension is available
|
||||
// then create a gzipped version of this file. This file is served
|
||||
// conditionally to browsers that accept gzip using .htaccess rules.
|
||||
// It's possible that the rewrite rules in .htaccess aren't working on this
|
||||
// server, but there's no harm (other than the time spent generating the
|
||||
// file) in generating the file anyway. Sites on servers where rewrite rules
|
||||
// aren't working can set css.gzip to FALSE in order to skip
|
||||
// generating a file that won't be used.
|
||||
if (extension_loaded('zlib') && \Drupal::config('system.performance')->get($file_extension . '.gzip')) {
|
||||
if (!file_exists($uri . '.gz') && !file_unmanaged_save_data(gzencode($data, 9, FORCE_GZIP), $uri . '.gz', FILE_EXISTS_REPLACE)) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
return $uri;
|
||||
}
|
||||
|
||||
}
|
27
core/lib/Drupal/Core/Asset/AssetDumperInterface.php
Normal file
27
core/lib/Drupal/Core/Asset/AssetDumperInterface.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetDumperInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Interface defining a service that dumps an (optimized) asset.
|
||||
*/
|
||||
interface AssetDumperInterface {
|
||||
|
||||
/**
|
||||
* Dumps an (optimized) asset to persistent storage.
|
||||
*
|
||||
* @param string $data
|
||||
* An (optimized) asset's contents.
|
||||
* @param string $file_extension
|
||||
* The file extension of this asset.
|
||||
*
|
||||
* @return string
|
||||
* An URI to access the dumped asset.
|
||||
*/
|
||||
public function dump($data, $file_extension);
|
||||
|
||||
}
|
36
core/lib/Drupal/Core/Asset/AssetOptimizerInterface.php
Normal file
36
core/lib/Drupal/Core/Asset/AssetOptimizerInterface.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetOptimizerInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Interface defining a service that optimizes an asset.
|
||||
*/
|
||||
interface AssetOptimizerInterface {
|
||||
|
||||
/**
|
||||
* Optimizes an asset.
|
||||
*
|
||||
* @param array $asset
|
||||
* An asset.
|
||||
*
|
||||
* @return string
|
||||
* The optimized asset's contents.
|
||||
*/
|
||||
public function optimize(array $asset);
|
||||
|
||||
/**
|
||||
* Removes unwanted content from an asset.
|
||||
*
|
||||
* @param string $content
|
||||
* The content of an asset.
|
||||
*
|
||||
* @return string
|
||||
* The cleaned asset's contents.
|
||||
*/
|
||||
public function clean($content);
|
||||
|
||||
}
|
409
core/lib/Drupal/Core/Asset/AssetResolver.php
Normal file
409
core/lib/Drupal/Core/Asset/AssetResolver.php
Normal file
|
@ -0,0 +1,409 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetResolver.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
|
||||
/**
|
||||
* The default asset resolver.
|
||||
*/
|
||||
class AssetResolver implements AssetResolverInterface {
|
||||
|
||||
/**
|
||||
* The library discovery service.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
|
||||
*/
|
||||
protected $libraryDiscovery;
|
||||
|
||||
/**
|
||||
* The library dependency resolver.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\LibraryDependencyResolverInterface
|
||||
*/
|
||||
protected $libraryDependencyResolver;
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* The theme manager.
|
||||
*
|
||||
* @var \Drupal\Core\Theme\ThemeManagerInterface
|
||||
*/
|
||||
protected $themeManager;
|
||||
|
||||
/**
|
||||
* The language manager.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* The cache backend.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* Constructs a new AssetResolver instance.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
|
||||
* The library discovery service.
|
||||
* @param \Drupal\Core\Asset\LibraryDependencyResolverInterface $library_dependency_resolver
|
||||
* The library dependency resolver.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
|
||||
* The theme manager.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
|
||||
* The cache backend.
|
||||
*/
|
||||
public function __construct(LibraryDiscoveryInterface $library_discovery, LibraryDependencyResolverInterface $library_dependency_resolver, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
|
||||
$this->libraryDiscovery = $library_discovery;
|
||||
$this->libraryDependencyResolver = $library_dependency_resolver;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->themeManager = $theme_manager;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the libraries that need to be loaded.
|
||||
*
|
||||
* For example, with core/a depending on core/c and core/b on core/d:
|
||||
* @code
|
||||
* $assets = new AttachedAssets();
|
||||
* $assets->setLibraries(['core/a', 'core/b', 'core/c']);
|
||||
* $assets->setAlreadyLoadedLibraries(['core/c']);
|
||||
* $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d']
|
||||
* @endcode
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
||||
* The assets attached to the current response.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of libraries and their dependencies, in the order they should be
|
||||
* loaded, excluding any libraries that have already been loaded.
|
||||
*/
|
||||
protected function getLibrariesToLoad(AttachedAssetsInterface $assets) {
|
||||
return array_diff(
|
||||
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()),
|
||||
$this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCssAssets(AttachedAssetsInterface $assets, $optimize) {
|
||||
$theme_info = $this->themeManager->getActiveTheme();
|
||||
// Add the theme name to the cache key since themes may implement
|
||||
// hook_css_alter().
|
||||
$cid = 'css:' . $theme_info->getName() . ':' . Crypt::hashBase64(serialize($assets)) . (int) $optimize;
|
||||
if ($cached = $this->cache->get($cid)) {
|
||||
return $cached->data;
|
||||
}
|
||||
|
||||
$css = [];
|
||||
$default_options = [
|
||||
'type' => 'file',
|
||||
'group' => CSS_AGGREGATE_DEFAULT,
|
||||
'weight' => 0,
|
||||
'every_page' => FALSE,
|
||||
'media' => 'all',
|
||||
'preprocess' => TRUE,
|
||||
'browsers' => [],
|
||||
];
|
||||
|
||||
foreach ($this->getLibrariesToLoad($assets) as $library) {
|
||||
list($extension, $name) = explode('/', $library, 2);
|
||||
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
||||
if (isset($definition['css'])) {
|
||||
foreach ($definition['css'] as $options) {
|
||||
$options += $default_options;
|
||||
$options['browsers'] += [
|
||||
'IE' => TRUE,
|
||||
'!IE' => TRUE,
|
||||
];
|
||||
|
||||
// Files with a query string cannot be preprocessed.
|
||||
if ($options['type'] === 'file' && $options['preprocess'] && strpos($options['data'], '?') !== FALSE) {
|
||||
$options['preprocess'] = FALSE;
|
||||
}
|
||||
|
||||
// Always add a tiny value to the weight, to conserve the insertion
|
||||
// order.
|
||||
$options['weight'] += count($css) / 1000;
|
||||
|
||||
// CSS files are being keyed by the full path.
|
||||
$css[$options['data']] = $options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow modules and themes to alter the CSS assets.
|
||||
$this->moduleHandler->alter('css', $css, $assets);
|
||||
$this->themeManager->alter('css', $css, $assets);
|
||||
|
||||
// Sort CSS items, so that they appear in the correct order.
|
||||
uasort($css, 'static::sort');
|
||||
|
||||
// Allow themes to remove CSS files by CSS files full path and file name.
|
||||
if ($stylesheet_remove = $theme_info->getStyleSheetsRemove()) {
|
||||
foreach ($css as $key => $options) {
|
||||
if (isset($stylesheet_remove[$key])) {
|
||||
unset($css[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($optimize) {
|
||||
$css = \Drupal::service('asset.css.collection_optimizer')->optimize($css);
|
||||
}
|
||||
$this->cache->set($cid, $css, CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
|
||||
|
||||
return $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JavaScript settings assets for this response's libraries.
|
||||
*
|
||||
* Gathers all drupalSettings from all libraries in the attached assets
|
||||
* collection and merges them, then it merges individual attached settings,
|
||||
* and finally invokes hook_js_settings_alter() to allow alterations of
|
||||
* JavaScript settings by modules and themes.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
||||
* The assets attached to the current response.
|
||||
* @return array
|
||||
* A (possibly optimized) collection of JavaScript assets.
|
||||
*/
|
||||
protected function getJsSettingsAssets(AttachedAssetsInterface $assets) {
|
||||
$settings = [];
|
||||
|
||||
foreach ($this->getLibrariesToLoad($assets) as $library) {
|
||||
list($extension, $name) = explode('/', $library, 2);
|
||||
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
||||
if (isset($definition['drupalSettings'])) {
|
||||
$settings = NestedArray::mergeDeepArray([$settings, $definition['drupalSettings']], TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
// Attached settings win over settings in libraries.
|
||||
$settings = NestedArray::mergeDeepArray([$settings, $assets->getSettings()], TRUE);
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getJsAssets(AttachedAssetsInterface $assets, $optimize) {
|
||||
$theme_info = $this->themeManager->getActiveTheme();
|
||||
// Add the theme name to the cache key since themes may implement
|
||||
// hook_js_alter(). Additionally add the current language to support
|
||||
// translation of JavaScript files.
|
||||
$cid = 'js:' . $theme_info->getName() . ':' . $this->languageManager->getCurrentLanguage()->getId() . ':' . Crypt::hashBase64(serialize($assets));
|
||||
|
||||
if ($cached = $this->cache->get($cid)) {
|
||||
list($js_assets_header, $js_assets_footer, $settings, $settings_in_header) = $cached->data;
|
||||
}
|
||||
else {
|
||||
$javascript = [];
|
||||
$default_options = [
|
||||
'type' => 'file',
|
||||
'group' => JS_DEFAULT,
|
||||
'every_page' => FALSE,
|
||||
'weight' => 0,
|
||||
'cache' => TRUE,
|
||||
'preprocess' => TRUE,
|
||||
'attributes' => [],
|
||||
'version' => NULL,
|
||||
'browsers' => [],
|
||||
];
|
||||
|
||||
$libraries_to_load = $this->getLibrariesToLoad($assets);
|
||||
|
||||
// Collect all libraries that contain JS assets and are in the header.
|
||||
$header_js_libraries = [];
|
||||
foreach ($libraries_to_load as $library) {
|
||||
list($extension, $name) = explode('/', $library, 2);
|
||||
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
||||
if (isset($definition['js']) && !empty($definition['header'])) {
|
||||
$header_js_libraries[] = $library;
|
||||
}
|
||||
}
|
||||
// The current list of header JS libraries are only those libraries that
|
||||
// are in the header, but their dependencies must also be loaded for them
|
||||
// to function correctly, so update the list with those.
|
||||
$header_js_libraries = $this->libraryDependencyResolver->getLibrariesWithDependencies($header_js_libraries);
|
||||
|
||||
foreach ($libraries_to_load as $library) {
|
||||
list($extension, $name) = explode('/', $library, 2);
|
||||
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
||||
if (isset($definition['js'])) {
|
||||
foreach ($definition['js'] as $options) {
|
||||
$options += $default_options;
|
||||
|
||||
// 'scope' is a calculated option, based on which libraries are
|
||||
// marked to be loaded from the header (see above).
|
||||
$options['scope'] = in_array($library, $header_js_libraries) ? 'header' : 'footer';
|
||||
|
||||
// Preprocess can only be set if caching is enabled and no
|
||||
// attributes are set.
|
||||
$options['preprocess'] = $options['cache'] && empty($options['attributes']) ? $options['preprocess'] : FALSE;
|
||||
|
||||
// Always add a tiny value to the weight, to conserve the insertion
|
||||
// order.
|
||||
$options['weight'] += count($javascript) / 1000;
|
||||
|
||||
// Local and external files must keep their name as the associative
|
||||
// key so the same JavaScript file is not added twice.
|
||||
$javascript[$options['data']] = $options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow modules and themes to alter the JavaScript assets.
|
||||
$this->moduleHandler->alter('js', $javascript, $assets);
|
||||
$this->themeManager->alter('js', $javascript, $assets);
|
||||
|
||||
// Sort JavaScript assets, so that they appear in the correct order.
|
||||
uasort($javascript, 'static::sort');
|
||||
|
||||
// Prepare the return value: filter JavaScript assets per scope.
|
||||
$js_assets_header = [];
|
||||
$js_assets_footer = [];
|
||||
foreach ($javascript as $key => $item) {
|
||||
if ($item['scope'] == 'header') {
|
||||
$js_assets_header[$key] = $item;
|
||||
}
|
||||
elseif ($item['scope'] == 'footer') {
|
||||
$js_assets_footer[$key] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
if ($optimize) {
|
||||
$collection_optimizer = \Drupal::service('asset.js.collection_optimizer');
|
||||
$js_assets_header = $collection_optimizer->optimize($js_assets_header);
|
||||
$js_assets_footer = $collection_optimizer->optimize($js_assets_footer);
|
||||
}
|
||||
|
||||
// If the core/drupalSettings library is being loaded or is already
|
||||
// loaded, get the JavaScript settings assets, and convert them into a
|
||||
// single "regular" JavaScript asset.
|
||||
$libraries_to_load = $this->getLibrariesToLoad($assets);
|
||||
$settings_required = in_array('core/drupalSettings', $libraries_to_load) || in_array('core/drupalSettings', $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries()));
|
||||
$settings_have_changed = count($libraries_to_load) > 0 || count($assets->getSettings()) > 0;
|
||||
|
||||
// Initialize settings to FALSE since they are not needed by default. This
|
||||
// distinguishes between an empty array which must still allow
|
||||
// hook_js_settings_alter() to be run.
|
||||
$settings = FALSE;
|
||||
if ($settings_required && $settings_have_changed) {
|
||||
$settings = $this->getJsSettingsAssets($assets);
|
||||
// Allow modules to add cached JavaScript settings.
|
||||
foreach ($this->moduleHandler->getImplementations('js_settings_build') as $module) {
|
||||
$function = $module . '_' . 'js_settings_build';
|
||||
$function($settings, $assets);
|
||||
}
|
||||
}
|
||||
$settings_in_header = in_array('core/drupalSettings', $header_js_libraries);
|
||||
$this->cache->set($cid, [$js_assets_header, $js_assets_footer, $settings, $settings_in_header], CacheBackendInterface::CACHE_PERMANENT, ['library_info']);
|
||||
}
|
||||
|
||||
|
||||
if ($settings !== FALSE) {
|
||||
// Allow modules and themes to alter the JavaScript settings.
|
||||
$this->moduleHandler->alter('js_settings', $settings, $assets);
|
||||
$this->themeManager->alter('js_settings', $settings, $assets);
|
||||
$settings_as_inline_javascript = [
|
||||
'type' => 'setting',
|
||||
'group' => JS_SETTING,
|
||||
'every_page' => TRUE,
|
||||
'weight' => 0,
|
||||
'browsers' => [],
|
||||
'data' => $settings,
|
||||
];
|
||||
$settings_js_asset = ['drupalSettings' => $settings_as_inline_javascript];
|
||||
// Prepend to the list of JS assets, to render it first. Preferably in
|
||||
// the footer, but in the header if necessary.
|
||||
if ($settings_in_header) {
|
||||
$js_assets_header = $settings_js_asset + $js_assets_header;
|
||||
}
|
||||
else {
|
||||
$js_assets_footer = $settings_js_asset + $js_assets_footer;
|
||||
}
|
||||
}
|
||||
return [
|
||||
$js_assets_header,
|
||||
$js_assets_footer,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts CSS and JavaScript resources.
|
||||
*
|
||||
* This sort order helps optimize front-end performance while providing
|
||||
* modules and themes with the necessary control for ordering the CSS and
|
||||
* JavaScript appearing on a page.
|
||||
*
|
||||
* @param $a
|
||||
* First item for comparison. The compared items should be associative
|
||||
* arrays of member items.
|
||||
* @param $b
|
||||
* Second item for comparison.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function sort($a, $b) {
|
||||
// First order by group, so that all items in the CSS_AGGREGATE_DEFAULT
|
||||
// group appear before items in the CSS_AGGREGATE_THEME group. Modules may
|
||||
// create additional groups by defining their own constants.
|
||||
if ($a['group'] < $b['group']) {
|
||||
return -1;
|
||||
}
|
||||
elseif ($a['group'] > $b['group']) {
|
||||
return 1;
|
||||
}
|
||||
// Within a group, order all infrequently needed, page-specific files after
|
||||
// common files needed throughout the website. Separating this way allows
|
||||
// for the aggregate file generated for all of the common files to be reused
|
||||
// across a site visit without being cut by a page using a less common file.
|
||||
elseif ($a['every_page'] && !$b['every_page']) {
|
||||
return -1;
|
||||
}
|
||||
elseif (!$a['every_page'] && $b['every_page']) {
|
||||
return 1;
|
||||
}
|
||||
// Finally, order by weight.
|
||||
elseif ($a['weight'] < $b['weight']) {
|
||||
return -1;
|
||||
}
|
||||
elseif ($a['weight'] > $b['weight']) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
85
core/lib/Drupal/Core/Asset/AssetResolverInterface.php
Normal file
85
core/lib/Drupal/Core/Asset/AssetResolverInterface.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AssetResolverInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Resolves asset libraries into concrete CSS and JavaScript assets.
|
||||
*
|
||||
* Given an attached assets collection (to be loaded for the current response),
|
||||
* the asset resolver can resolve those asset libraries into a list of concrete
|
||||
* CSS and JavaScript assets.
|
||||
*
|
||||
* In other words: this allows developers to translate Drupal's asset
|
||||
* abstraction (asset libraries) into concrete assets.
|
||||
*
|
||||
* @see \Drupal\Core\Asset\AttachedAssetsInterface
|
||||
* @see \Drupal\Core\Asset\LibraryDependencyResolverInterface
|
||||
*/
|
||||
interface AssetResolverInterface {
|
||||
|
||||
/**
|
||||
* Returns the CSS assets for the current response's libraries.
|
||||
*
|
||||
* It returns the CSS assets in order, according to the SMACSS categories
|
||||
* specified in the assets' weights:
|
||||
* - CSS_BASE
|
||||
* - CSS_LAYOUT
|
||||
* - CSS_COMPONENT
|
||||
* - CSS_STATE
|
||||
* - CSS_THEME
|
||||
* @see https://www.drupal.org/node/1887918#separate-concerns
|
||||
* This ensures proper cascading of styles so themes can easily override
|
||||
* module styles through CSS selectors.
|
||||
*
|
||||
* Themes may replace module-defined CSS files by adding a stylesheet with the
|
||||
* same filename. For example, themes/bartik/system-menus.css would replace
|
||||
* modules/system/system-menus.css. This allows themes to override complete
|
||||
* CSS files, rather than specific selectors, when necessary.
|
||||
*
|
||||
* Also invokes hook_css_alter(), to allow CSS assets to be altered.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
||||
* The assets attached to the current response.
|
||||
* @param bool $optimize
|
||||
* Whether to apply the CSS asset collection optimizer, to return an
|
||||
* optimized CSS asset collection rather than an unoptimized one.
|
||||
*
|
||||
* @return array
|
||||
* A (possibly optimized) collection of CSS assets.
|
||||
*/
|
||||
public function getCssAssets(AttachedAssetsInterface $assets, $optimize);
|
||||
|
||||
/**
|
||||
* Returns the JavaScript assets for the current response's libraries.
|
||||
*
|
||||
* References to JavaScript files are placed in a certain order: first, all
|
||||
* 'core' files, then all 'module' and finally all 'theme' JavaScript files
|
||||
* are added to the page. Then, all settings are output, followed by 'inline'
|
||||
* JavaScript code. If running update.php, all preprocessing is disabled.
|
||||
*
|
||||
* Note that hook_js_alter(&$javascript) is called during this function call
|
||||
* to allow alterations of the JavaScript during its presentation. The correct
|
||||
* way to add JavaScript during hook_js_alter() is to add another element to
|
||||
* the $javascript array, deriving from drupal_js_defaults(). See
|
||||
* locale_js_alter() for an example of this.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AttachedAssetsInterface $assets
|
||||
* The assets attached to the current response.
|
||||
* @param bool $optimize
|
||||
* Whether to apply the JavaScript asset collection optimizer, to return
|
||||
* optimized JavaScript asset collections rather than an unoptimized ones.
|
||||
*
|
||||
* @return array
|
||||
* A nested array containing 2 values:
|
||||
* - at index zero: the (possibly optimized) collection of JavaScript assets
|
||||
* for the top of the page
|
||||
* - at index one: the (possibly optimized) collection of JavaScript assets
|
||||
* for the bottom of the page
|
||||
*/
|
||||
public function getJsAssets(AttachedAssetsInterface $assets, $optimize);
|
||||
|
||||
}
|
98
core/lib/Drupal/Core/Asset/AttachedAssets.php
Normal file
98
core/lib/Drupal/Core/Asset/AttachedAssets.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AttachedAssets.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* The default attached assets collection.
|
||||
*/
|
||||
class AttachedAssets implements AttachedAssetsInterface {
|
||||
|
||||
/**
|
||||
* The (ordered) list of asset libraries attached to the current response.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $libraries = [];
|
||||
|
||||
/**
|
||||
* The JavaScript settings attached to the current response.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $settings = [];
|
||||
|
||||
/**
|
||||
* The set of asset libraries that the client has already loaded.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $alreadyLoadedLibraries = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createFromRenderArray(array $render_array) {
|
||||
if (!isset($render_array['#attached'])) {
|
||||
throw new \LogicException('The render array has not yet been rendered, hence not all attachments have been collected yet.');
|
||||
}
|
||||
|
||||
$assets = new static();
|
||||
if (isset($render_array['#attached']['library'])) {
|
||||
$assets->setLibraries($render_array['#attached']['library']);
|
||||
}
|
||||
if (isset($render_array['#attached']['drupalSettings'])) {
|
||||
$assets->setSettings($render_array['#attached']['drupalSettings']);
|
||||
}
|
||||
return $assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLibraries(array $libraries) {
|
||||
$this->libraries = array_unique($libraries);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries() {
|
||||
return $this->libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSettings(array $settings) {
|
||||
$this->settings = $settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSettings() {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAlreadyLoadedLibraries() {
|
||||
return $this->alreadyLoadedLibraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setAlreadyLoadedLibraries(array $libraries) {
|
||||
$this->alreadyLoadedLibraries = $libraries;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
85
core/lib/Drupal/Core/Asset/AttachedAssetsInterface.php
Normal file
85
core/lib/Drupal/Core/Asset/AttachedAssetsInterface.php
Normal file
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\AttachedAssetsInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* The attached assets collection for the current response.
|
||||
*
|
||||
* Allows for storage of:
|
||||
* - an ordered list of asset libraries (to be loaded for the current response)
|
||||
* - attached JavaScript settings (to be loaded for the current response)
|
||||
* - a set of asset libraries that the client already has loaded (as indicated
|
||||
* in the request, to *not* be loaded for the current response)
|
||||
*
|
||||
* @see \Drupal\Core\Asset\AssetResolverInterface
|
||||
*/
|
||||
interface AttachedAssetsInterface {
|
||||
|
||||
/**
|
||||
* Creates an AttachedAssetsInterface object from a render array.
|
||||
*
|
||||
* @param array $render_array
|
||||
* A render array.
|
||||
*
|
||||
* @return \Drupal\Core\Asset\AttachedAssetsInterface
|
||||
*
|
||||
* @throws \LogicException
|
||||
*/
|
||||
public static function createFromRenderArray(array $render_array);
|
||||
|
||||
/**
|
||||
* Sets the asset libraries attached to the current response.
|
||||
*
|
||||
* @param string[] $libraries
|
||||
* A list of libraries, in the order they should be loaded.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLibraries(array $libraries);
|
||||
|
||||
/**
|
||||
* Returns the asset libraries attached to the current response.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getLibraries();
|
||||
|
||||
/**
|
||||
* Sets the JavaScript settings that are attached to the current response.
|
||||
*
|
||||
* @param array $settings
|
||||
* The needed JavaScript settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSettings(array $settings);
|
||||
|
||||
/**
|
||||
* Returns the settings attached to the current response.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getSettings();
|
||||
|
||||
/**
|
||||
* Sets the asset libraries that the current request marked as already loaded.
|
||||
*
|
||||
* @param string[] $libraries
|
||||
* The set of already loaded libraries.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setAlreadyLoadedLibraries(array $libraries);
|
||||
|
||||
/**
|
||||
* Returns the set of already loaded asset libraries.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAlreadyLoadedLibraries();
|
||||
|
||||
}
|
98
core/lib/Drupal/Core/Asset/CssCollectionGrouper.php
Normal file
98
core/lib/Drupal/Core/Asset/CssCollectionGrouper.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\CssCollectionGrouper.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
|
||||
|
||||
/**
|
||||
* Groups CSS assets.
|
||||
*/
|
||||
class CssCollectionGrouper implements AssetCollectionGrouperInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Puts multiple items into the same group if they are groupable and if they
|
||||
* are for the same 'media' and 'browsers'. Items of the 'file' type are
|
||||
* groupable if their 'preprocess' flag is TRUE, items of the 'inline' type
|
||||
* are always groupable, and items of the 'external' type are never groupable.
|
||||
*
|
||||
* Also ensures that the process of grouping items does not change their
|
||||
* relative order. This requirement may result in multiple groups for the same
|
||||
* type, media, and browsers, if needed to accommodate other items in between.
|
||||
*/
|
||||
public function group(array $css_assets) {
|
||||
$groups = array();
|
||||
// If a group can contain multiple items, we track the information that must
|
||||
// be the same for each item in the group, so that when we iterate the next
|
||||
// item, we can determine if it can be put into the current group, or if a
|
||||
// new group needs to be made for it.
|
||||
$current_group_keys = NULL;
|
||||
// When creating a new group, we pre-increment $i, so by initializing it to
|
||||
// -1, the first group will have index 0.
|
||||
$i = -1;
|
||||
foreach ($css_assets as $item) {
|
||||
// The browsers for which the CSS item needs to be loaded is part of the
|
||||
// information that determines when a new group is needed, but the order
|
||||
// of keys in the array doesn't matter, and we don't want a new group if
|
||||
// all that's different is that order.
|
||||
ksort($item['browsers']);
|
||||
|
||||
// If the item can be grouped with other items, set $group_keys to an
|
||||
// array of information that must be the same for all items in its group.
|
||||
// If the item can't be grouped with other items, set $group_keys to
|
||||
// FALSE. We put items into a group that can be aggregated together:
|
||||
// whether they will be aggregated is up to the _drupal_css_aggregate()
|
||||
// function or an
|
||||
// override of that function specified in hook_css_alter(), but regardless
|
||||
// of the details of that function, a group represents items that can be
|
||||
// aggregated. Since a group may be rendered with a single HTML tag, all
|
||||
// items in the group must share the same information that would need to
|
||||
// be part of that HTML tag.
|
||||
switch ($item['type']) {
|
||||
case 'file':
|
||||
// Group file items if their 'preprocess' flag is TRUE.
|
||||
// Help ensure maximum reuse of aggregate files by only grouping
|
||||
// together items that share the same 'group' value and 'every_page'
|
||||
// flag.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['media'], $item['browsers']) : FALSE;
|
||||
break;
|
||||
|
||||
case 'inline':
|
||||
// Always group inline items.
|
||||
$group_keys = array($item['type'], $item['media'], $item['browsers']);
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
// Do not group external items.
|
||||
$group_keys = FALSE;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the group keys don't match the most recent group we're working with,
|
||||
// then a new group must be made.
|
||||
if ($group_keys !== $current_group_keys) {
|
||||
$i++;
|
||||
// Initialize the new group with the same properties as the first item
|
||||
// being placed into it. The item's 'data', 'weight' and 'basename'
|
||||
// properties are unique to the item and should not be carried over to
|
||||
// the group.
|
||||
$groups[$i] = $item;
|
||||
unset($groups[$i]['data'], $groups[$i]['weight'], $groups[$i]['basename']);
|
||||
$groups[$i]['items'] = array();
|
||||
$current_group_keys = $group_keys ? $group_keys : NULL;
|
||||
}
|
||||
|
||||
// Add the item to the current group.
|
||||
$groups[$i]['items'][] = $item;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
}
|
202
core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
Normal file
202
core/lib/Drupal/Core/Asset/CssCollectionOptimizer.php
Normal file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\CssCollectionOptimizer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\State\StateInterface;
|
||||
|
||||
/**
|
||||
* Optimizes CSS assets.
|
||||
*/
|
||||
class CssCollectionOptimizer implements AssetCollectionOptimizerInterface {
|
||||
|
||||
/**
|
||||
* A CSS asset grouper.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\CssCollectionGrouper
|
||||
*/
|
||||
protected $grouper;
|
||||
|
||||
/**
|
||||
* A CSS asset optimizer.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\CssOptimizer
|
||||
*/
|
||||
protected $optimizer;
|
||||
|
||||
/**
|
||||
* An asset dumper.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\AssetDumper
|
||||
*/
|
||||
protected $dumper;
|
||||
|
||||
/**
|
||||
* The state key/value store.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Constructs a CssCollectionOptimizer.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface
|
||||
* The grouper for CSS assets.
|
||||
* @param \Drupal\Core\Asset\AssetOptimizerInterface
|
||||
* The optimizer for a single CSS asset.
|
||||
* @param \Drupal\Core\Asset\AssetDumperInterface
|
||||
* The dumper for optimized CSS assets.
|
||||
* @param \Drupal\Core\State\StateInterface
|
||||
* The state key/value store.
|
||||
*/
|
||||
public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) {
|
||||
$this->grouper = $grouper;
|
||||
$this->optimizer = $optimizer;
|
||||
$this->dumper = $dumper;
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* The cache file name is retrieved on a page load via a lookup variable that
|
||||
* contains an associative array. The array key is the hash of the file names
|
||||
* in $css while the value is the cache file name. The cache file is generated
|
||||
* in two cases. First, if there is no file name value for the key, which will
|
||||
* happen if a new file name has been added to $css or after the lookup
|
||||
* variable is emptied to force a rebuild of the cache. Second, the cache file
|
||||
* is generated if it is missing on disk. Old cache files are not deleted
|
||||
* immediately when the lookup variable is emptied, but are deleted after a
|
||||
* configurable period (@code system.performance.stale_file_threshold @endcode)
|
||||
* to ensure that files referenced by a cached page will still be available.
|
||||
*/
|
||||
public function optimize(array $css_assets) {
|
||||
// Group the assets.
|
||||
$css_groups = $this->grouper->group($css_assets);
|
||||
|
||||
// Now optimize (concatenate + minify) and dump each asset group, unless
|
||||
// that was already done, in which case it should appear in
|
||||
// drupal_css_cache_files.
|
||||
// Drupal contrib can override this default CSS aggregator to keep the same
|
||||
// grouping, optimizing and dumping, but change the strategy that is used to
|
||||
// determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …).
|
||||
$map = $this->state->get('drupal_css_cache_files') ?: array();
|
||||
$css_assets = array();
|
||||
foreach ($css_groups as $order => $css_group) {
|
||||
// We have to return a single asset, not a group of assets. It is now up
|
||||
// to one of the pieces of code in the switch statement below to set the
|
||||
// 'data' property to the appropriate value.
|
||||
$css_assets[$order] = $css_group;
|
||||
unset($css_assets[$order]['items']);
|
||||
|
||||
switch ($css_group['type']) {
|
||||
case 'file':
|
||||
// No preprocessing, single CSS asset: just use the existing URI.
|
||||
if (!$css_group['preprocess']) {
|
||||
$uri = $css_group['items'][0]['data'];
|
||||
$css_assets[$order]['data'] = $uri;
|
||||
}
|
||||
// Preprocess (aggregate), unless the aggregate file already exists.
|
||||
else {
|
||||
$key = $this->generateHash($css_group);
|
||||
$uri = '';
|
||||
if (isset($map[$key])) {
|
||||
$uri = $map[$key];
|
||||
}
|
||||
if (empty($uri) || !file_exists($uri)) {
|
||||
// Optimize each asset within the group.
|
||||
$data = '';
|
||||
foreach ($css_group['items'] as $css_asset) {
|
||||
$data .= $this->optimizer->optimize($css_asset);
|
||||
}
|
||||
// Per the W3C specification at
|
||||
// http://www.w3.org/TR/REC-CSS2/cascade.html#at-import, @import
|
||||
// rules must proceed any other style, so we move those to the
|
||||
// top.
|
||||
$regexp = '/@import[^;]+;/i';
|
||||
preg_match_all($regexp, $data, $matches);
|
||||
$data = preg_replace($regexp, '', $data);
|
||||
$data = implode('', $matches[0]) . $data;
|
||||
// Dump the optimized CSS for this group into an aggregate file.
|
||||
$uri = $this->dumper->dump($data, 'css');
|
||||
// Set the URI for this group's aggregate file.
|
||||
$css_assets[$order]['data'] = $uri;
|
||||
// Persist the URI for this aggregate file.
|
||||
$map[$key] = $uri;
|
||||
$this->state->set('drupal_css_cache_files', $map);
|
||||
}
|
||||
else {
|
||||
// Use the persisted URI for the optimized CSS file.
|
||||
$css_assets[$order]['data'] = $uri;
|
||||
}
|
||||
$css_assets[$order]['preprocessed'] = TRUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'inline':
|
||||
// We don't do any caching for inline CSS assets.
|
||||
$data = '';
|
||||
foreach ($css_group['items'] as $css_asset) {
|
||||
$data .= $this->optimizer->optimize($css_asset);
|
||||
}
|
||||
unset($css_assets[$order]['data']['items']);
|
||||
$css_assets[$order]['data'] = $data;
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
// We don't do any aggregation and hence also no caching for external
|
||||
// CSS assets.
|
||||
$uri = $css_group['items'][0]['data'];
|
||||
$css_assets[$order]['data'] = $uri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $css_assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for a given group of CSS assets.
|
||||
*
|
||||
* @param array $css_group
|
||||
* A group of CSS assets.
|
||||
*
|
||||
* @return string
|
||||
* A hash to uniquely identify the given group of CSS assets.
|
||||
*/
|
||||
protected function generateHash(array $css_group) {
|
||||
$css_data = array();
|
||||
foreach ($css_group['items'] as $css_file) {
|
||||
$css_data[] = $css_file['data'];
|
||||
}
|
||||
return hash('sha256', serialize($css_data));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAll() {
|
||||
return $this->state->get('drupal_css_cache_files');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAll() {
|
||||
$this->state->delete('drupal_css_cache_files');
|
||||
|
||||
$delete_stale = function($uri) {
|
||||
// Default stale file threshold is 30 days.
|
||||
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
|
||||
file_unmanaged_delete($uri);
|
||||
}
|
||||
};
|
||||
file_scan_directory('public://css', '/.*/', array('callback' => $delete_stale));
|
||||
}
|
||||
|
||||
}
|
223
core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
Normal file
223
core/lib/Drupal/Core/Asset/CssCollectionRenderer.php
Normal file
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\CssCollectionRenderer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
|
||||
/**
|
||||
* Renders CSS assets.
|
||||
*
|
||||
* For production websites, LINK tags are preferable to STYLE tags with @import
|
||||
* statements, because:
|
||||
* - They are the standard tag intended for linking to a resource.
|
||||
* - On Firefox 2 and perhaps other browsers, CSS files included with @import
|
||||
* statements don't get saved when saving the complete web page for offline
|
||||
* use: https://www.drupal.org/node/145218.
|
||||
* - On IE, if only LINK tags and no @import statements are used, all the CSS
|
||||
* files are downloaded in parallel, resulting in faster page load, but if
|
||||
* @import statements are used and span across multiple STYLE tags, all the
|
||||
* ones from one STYLE tag must be downloaded before downloading begins for
|
||||
* the next STYLE tag. Furthermore, IE7 does not support media declaration on
|
||||
* the @import statement, so multiple STYLE tags must be used when different
|
||||
* files are for different media types. Non-IE browsers always download in
|
||||
* parallel, so this is an IE-specific performance quirk:
|
||||
* http://www.stevesouders.com/blog/2009/04/09/dont-use-import/.
|
||||
*
|
||||
* However, IE has an annoying limit of 31 total CSS inclusion tags
|
||||
* (https://www.drupal.org/node/228818) and LINK tags are limited to one file
|
||||
* per tag, whereas STYLE tags can contain multiple @import statements allowing
|
||||
* multiple files to be loaded per tag. When CSS aggregation is disabled, a
|
||||
* Drupal site can easily have more than 31 CSS files that need to be loaded, so
|
||||
* using LINK tags exclusively would result in a site that would display
|
||||
* incorrectly in IE. Depending on different needs, different strategies can be
|
||||
* employed to decide when to use LINK tags and when to use STYLE tags.
|
||||
*
|
||||
* The strategy employed by this class is to use LINK tags for all aggregate
|
||||
* files and for all files that cannot be aggregated (e.g., if 'preprocess' is
|
||||
* set to FALSE or the type is 'external'), and to use STYLE tags for groups
|
||||
* of files that could be aggregated together but aren't (e.g., if the site-wide
|
||||
* aggregation setting is disabled). This results in all LINK tags when
|
||||
* aggregation is enabled, a guarantee that as many or only slightly more tags
|
||||
* are used with aggregation disabled than enabled (so that if the limit were to
|
||||
* be crossed with aggregation enabled, the site developer would also notice the
|
||||
* problem while aggregation is disabled), and an easy way for a developer to
|
||||
* view HTML source while aggregation is disabled and know what files will be
|
||||
* aggregated together when aggregation becomes enabled.
|
||||
*
|
||||
* This class evaluates the aggregation enabled/disabled condition on a group
|
||||
* by group basis by testing whether an aggregate file has been made for the
|
||||
* group rather than by testing the site-wide aggregation setting. This allows
|
||||
* this class to work correctly even if modules have implemented custom
|
||||
* logic for grouping and aggregating files.
|
||||
*/
|
||||
class CssCollectionRenderer implements AssetCollectionRendererInterface {
|
||||
|
||||
/**
|
||||
* The state key/value store.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Constructs a CssCollectionRenderer.
|
||||
*
|
||||
* @param \Drupal\Core\State\StateInterface
|
||||
* The state key/value store.
|
||||
*/
|
||||
public function __construct(StateInterface $state) {
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render(array $css_assets) {
|
||||
$elements = array();
|
||||
|
||||
// A dummy query-string is added to filenames, to gain control over
|
||||
// browser-caching. The string changes on every update or full cache
|
||||
// flush, forcing browsers to load a new copy of the files, as the
|
||||
// URL changed.
|
||||
$query_string = $this->state->get('system.css_js_query_string') ?: '0';
|
||||
|
||||
// Defaults for LINK and STYLE elements.
|
||||
$link_element_defaults = array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'link',
|
||||
'#attributes' => array(
|
||||
'rel' => 'stylesheet',
|
||||
),
|
||||
);
|
||||
$style_element_defaults = array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'style',
|
||||
);
|
||||
|
||||
// For filthy IE hack.
|
||||
$current_ie_group_keys = NULL;
|
||||
$get_ie_group_key = function ($css_asset) {
|
||||
return array($css_asset['type'], $css_asset['preprocess'], $css_asset['group'], $css_asset['every_page'], $css_asset['media'], $css_asset['browsers']);
|
||||
};
|
||||
|
||||
// Loop through all CSS assets, by key, to allow for the special IE
|
||||
// workaround.
|
||||
$css_assets_keys = array_keys($css_assets);
|
||||
for ($i = 0; $i < count($css_assets_keys); $i++) {
|
||||
$css_asset = $css_assets[$css_assets_keys[$i]];
|
||||
switch ($css_asset['type']) {
|
||||
// For file items, there are three possibilities.
|
||||
// - There are up to 31 CSS assets on the page (some of which may be
|
||||
// aggregated). In this case, output a LINK tag for file CSS assets.
|
||||
// - There are more than 31 CSS assets on the page, yet we must stay
|
||||
// below IE<10's limit of 31 total CSS inclusion tags, we handle this
|
||||
// in two ways:
|
||||
// - file CSS assets that are not eligible for aggregation (their
|
||||
// 'preprocess' flag has been set to FALSE): in this case, output a
|
||||
// LINK tag.
|
||||
// - file CSS assets that can be aggregated (and possibly have been):
|
||||
// in this case, figure out which subsequent file CSS assets share
|
||||
// the same key properties ('group', 'every_page', 'media' and
|
||||
// 'browsers') and output this group into as few STYLE tags as
|
||||
// possible (a STYLE tag may contain only 31 @import statements).
|
||||
case 'file':
|
||||
// The dummy query string needs to be added to the URL to control
|
||||
// browser-caching.
|
||||
$query_string_separator = (strpos($css_asset['data'], '?') !== FALSE) ? '&' : '?';
|
||||
|
||||
// As long as the current page will not run into IE's limit for CSS
|
||||
// assets: output a LINK tag for a file CSS asset.
|
||||
if (count($css_assets) <= 31) {
|
||||
$element = $link_element_defaults;
|
||||
$element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string;
|
||||
$element['#attributes']['media'] = $css_asset['media'];
|
||||
$element['#browsers'] = $css_asset['browsers'];
|
||||
$elements[] = $element;
|
||||
}
|
||||
// The current page will run into IE's limits for CSS assets: work
|
||||
// around these limits by performing a light form of grouping.
|
||||
// Once Drupal only needs to support IE10 and later, we can drop this.
|
||||
else {
|
||||
// The file CSS asset is ineligible for aggregation: output it in a
|
||||
// LINK tag.
|
||||
if (!$css_asset['preprocess']) {
|
||||
$element = $link_element_defaults;
|
||||
$element['#attributes']['href'] = file_create_url($css_asset['data']) . $query_string_separator . $query_string;
|
||||
$element['#attributes']['media'] = $css_asset['media'];
|
||||
$element['#browsers'] = $css_asset['browsers'];
|
||||
$elements[] = $element;
|
||||
}
|
||||
// The file CSS asset can be aggregated, but hasn't been: combine
|
||||
// multiple items into as few STYLE tags as possible.
|
||||
else {
|
||||
$import = array();
|
||||
// Start with the current CSS asset, iterate over subsequent CSS
|
||||
// assets and find which ones have the same 'type', 'group',
|
||||
// 'every_page', 'preprocess', 'media' and 'browsers' properties.
|
||||
$j = $i;
|
||||
$next_css_asset = $css_asset;
|
||||
$current_ie_group_key = $get_ie_group_key($css_asset);
|
||||
do {
|
||||
// The dummy query string needs to be added to the URL to
|
||||
// control browser-caching. IE7 does not support a media type on
|
||||
// the @import statement, so we instead specify the media for
|
||||
// the group on the STYLE tag.
|
||||
$import[] = '@import url("' . SafeMarkup::checkPlain(file_create_url($next_css_asset['data']) . '?' . $query_string) . '");';
|
||||
// Move the outer for loop skip the next item, since we
|
||||
// processed it here.
|
||||
$i = $j;
|
||||
// Retrieve next CSS asset, unless there is none: then break.
|
||||
if ($j + 1 < count($css_assets_keys)) {
|
||||
$j++;
|
||||
$next_css_asset = $css_assets[$css_assets_keys[$j]];
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
} while ($get_ie_group_key($next_css_asset) == $current_ie_group_key);
|
||||
|
||||
// In addition to IE's limit of 31 total CSS inclusion tags, it
|
||||
// also has a limit of 31 @import statements per STYLE tag.
|
||||
while (!empty($import)) {
|
||||
$import_batch = array_slice($import, 0, 31);
|
||||
$import = array_slice($import, 31);
|
||||
$element = $style_element_defaults;
|
||||
// This simplifies the JavaScript regex, allowing each line
|
||||
// (separated by \n) to be treated as a completely different
|
||||
// string. This means that we can use ^ and $ on one line at a
|
||||
// time, and not worry about style tags since they'll never
|
||||
// match the regex.
|
||||
$element['#value'] = "\n" . implode("\n", $import_batch) . "\n";
|
||||
$element['#attributes']['media'] = $css_asset['media'];
|
||||
$element['#browsers'] = $css_asset['browsers'];
|
||||
$elements[] = $element;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
// Output a LINK tag for an external CSS asset. The asset's 'data'
|
||||
// property contains the full URL.
|
||||
case 'external':
|
||||
$element = $link_element_defaults;
|
||||
$element['#attributes']['href'] = $css_asset['data'];
|
||||
$element['#attributes']['media'] = $css_asset['media'];
|
||||
$element['#browsers'] = $css_asset['browsers'];
|
||||
$elements[] = $element;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Invalid CSS asset type.');
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
}
|
272
core/lib/Drupal/Core/Asset/CssOptimizer.php
Normal file
272
core/lib/Drupal/Core/Asset/CssOptimizer.php
Normal file
|
@ -0,0 +1,272 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\CssOptimizer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\AssetOptimizerInterface;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Optimizes a CSS asset.
|
||||
*/
|
||||
class CssOptimizer implements AssetOptimizerInterface {
|
||||
|
||||
/**
|
||||
* The base path used by rewriteFileURI().
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $rewriteFileURIBasePath;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function optimize(array $css_asset) {
|
||||
if (!in_array($css_asset['type'], array('file', 'inline'))) {
|
||||
throw new \Exception('Only file or inline CSS assets can be optimized.');
|
||||
}
|
||||
if ($css_asset['type'] === 'file' && !$css_asset['preprocess']) {
|
||||
throw new \Exception('Only file CSS assets with preprocessing enabled can be optimized.');
|
||||
}
|
||||
|
||||
if ($css_asset['type'] === 'file') {
|
||||
return $this->processFile($css_asset);
|
||||
}
|
||||
else {
|
||||
return $this->processCss($css_asset['data'], $css_asset['preprocess']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the contents of a CSS asset for cleanup.
|
||||
*
|
||||
* @param string $contents
|
||||
* The contents of the CSS asset.
|
||||
*
|
||||
* @return string
|
||||
* Contents of the CSS asset.
|
||||
*/
|
||||
public function clean($contents) {
|
||||
// Remove multiple charset declarations for standards compliance (and fixing
|
||||
// Safari problems).
|
||||
$contents = preg_replace('/^@charset\s+[\'"](\S*?)\b[\'"];/i', '', $contents);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build aggregate CSS file.
|
||||
*/
|
||||
protected function processFile($css_asset) {
|
||||
$contents = $this->loadFile($css_asset['data'], TRUE);
|
||||
|
||||
$contents = $this->clean($contents);
|
||||
|
||||
// Get the parent directory of this file, relative to the Drupal root.
|
||||
$css_base_path = substr($css_asset['data'], 0, strrpos($css_asset['data'], '/'));
|
||||
// Store base path.
|
||||
$this->rewriteFileURIBasePath = $css_base_path . '/';
|
||||
|
||||
// Anchor all paths in the CSS with its base URL, ignoring external and absolute paths.
|
||||
return preg_replace_callback('/url\(\s*[\'"]?(?![a-z]+:|\/+)([^\'")]+)[\'"]?\s*\)/i', array($this, 'rewriteFileURI'), $contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the stylesheet and resolves all @import commands.
|
||||
*
|
||||
* Loads a stylesheet and replaces @import commands with the contents of the
|
||||
* imported file. Use this instead of file_get_contents when processing
|
||||
* stylesheets.
|
||||
*
|
||||
* The returned contents are compressed removing white space and comments only
|
||||
* when CSS aggregation is enabled. This optimization will not apply for
|
||||
* color.module enabled themes with CSS aggregation turned off.
|
||||
*
|
||||
* Note: the only reason this method is public is so color.module can call it;
|
||||
* it is not on the AssetOptimizerInterface, so future refactorings can make
|
||||
* it protected.
|
||||
*
|
||||
* @param $file
|
||||
* Name of the stylesheet to be processed.
|
||||
* @param $optimize
|
||||
* Defines if CSS contents should be compressed or not.
|
||||
* @param $reset_basepath
|
||||
* Used internally to facilitate recursive resolution of @import commands.
|
||||
*
|
||||
* @return
|
||||
* Contents of the stylesheet, including any resolved @import commands.
|
||||
*/
|
||||
public function loadFile($file, $optimize = NULL, $reset_basepath = TRUE) {
|
||||
// These statics are not cache variables, so we don't use drupal_static().
|
||||
static $_optimize, $basepath;
|
||||
if ($reset_basepath) {
|
||||
$basepath = '';
|
||||
}
|
||||
// Store the value of $optimize for preg_replace_callback with nested
|
||||
// @import loops.
|
||||
if (isset($optimize)) {
|
||||
$_optimize = $optimize;
|
||||
}
|
||||
|
||||
// Stylesheets are relative one to each other. Start by adding a base path
|
||||
// prefix provided by the parent stylesheet (if necessary).
|
||||
if ($basepath && !file_uri_scheme($file)) {
|
||||
$file = $basepath . '/' . $file;
|
||||
}
|
||||
// Store the parent base path to restore it later.
|
||||
$parent_base_path = $basepath;
|
||||
// Set the current base path to process possible child imports.
|
||||
$basepath = dirname($file);
|
||||
|
||||
// Load the CSS stylesheet. We suppress errors because themes may specify
|
||||
// stylesheets in their .info.yml file that don't exist in the theme's path,
|
||||
// but are merely there to disable certain module CSS files.
|
||||
$content = '';
|
||||
if ($contents = @file_get_contents($file)) {
|
||||
// If a BOM is found, convert the file to UTF-8, then use substr() to
|
||||
// remove the BOM from the result.
|
||||
if ($encoding = (Unicode::encodingFromBOM($contents))) {
|
||||
$contents = Unicode::substr(Unicode::convertToUtf8($contents, $encoding), 1);
|
||||
}
|
||||
// If no BOM, check for fallback encoding. Per CSS spec the regex is very strict.
|
||||
elseif (preg_match('/^@charset "([^"]+)";/', $contents, $matches)) {
|
||||
if ($matches[1] !== 'utf-8' && $matches[1] !== 'UTF-8') {
|
||||
$contents = substr($contents, strlen($matches[0]));
|
||||
$contents = Unicode::convertToUtf8($contents, $matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the processed stylesheet.
|
||||
$content = $this->processCss($contents, $_optimize);
|
||||
}
|
||||
|
||||
// Restore the parent base path as the file and its children are processed.
|
||||
$basepath = $parent_base_path;
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads stylesheets recursively and returns contents with corrected paths.
|
||||
*
|
||||
* This function is used for recursive loading of stylesheets and
|
||||
* returns the stylesheet content with all url() paths corrected.
|
||||
*
|
||||
* @param array $matches
|
||||
* An array of matches by a preg_replace_callback() call that scans for
|
||||
* @import-ed CSS files, except for external CSS files.
|
||||
*
|
||||
* @return
|
||||
* The contents of the CSS file at $matches[1], with corrected paths.
|
||||
*
|
||||
* @see \Drupal\Core\Asset\AssetOptimizerInterface::loadFile()
|
||||
*/
|
||||
protected function loadNestedFile($matches) {
|
||||
$filename = $matches[1];
|
||||
// Load the imported stylesheet and replace @import commands in there as
|
||||
// well.
|
||||
$file = $this->loadFile($filename, NULL, FALSE);
|
||||
|
||||
// Determine the file's directory.
|
||||
$directory = dirname($filename);
|
||||
// If the file is in the current directory, make sure '.' doesn't appear in
|
||||
// the url() path.
|
||||
$directory = $directory == '.' ? '' : $directory .'/';
|
||||
|
||||
// Alter all internal url() paths. Leave external paths alone. We don't need
|
||||
// to normalize absolute paths here because that will be done later.
|
||||
return preg_replace('/url\(\s*([\'"]?)(?![a-z]+:|\/+)([^\'")]+)([\'"]?)\s*\)/i', 'url(\1' . $directory . '\2\3)', $file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the contents of a stylesheet for aggregation.
|
||||
*
|
||||
* @param $contents
|
||||
* The contents of the stylesheet.
|
||||
* @param $optimize
|
||||
* (optional) Boolean whether CSS contents should be minified. Defaults to
|
||||
* FALSE.
|
||||
*
|
||||
* @return
|
||||
* Contents of the stylesheet including the imported stylesheets.
|
||||
*/
|
||||
protected function processCss($contents, $optimize = FALSE) {
|
||||
// Remove unwanted CSS code that cause issues.
|
||||
$contents = $this->clean($contents);
|
||||
|
||||
if ($optimize) {
|
||||
// Perform some safe CSS optimizations.
|
||||
// Regexp to match comment blocks.
|
||||
$comment = '/\*[^*]*\*+(?:[^/*][^*]*\*+)*/';
|
||||
// Regexp to match double quoted strings.
|
||||
$double_quot = '"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"';
|
||||
// Regexp to match single quoted strings.
|
||||
$single_quot = "'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'";
|
||||
// Strip all comment blocks, but keep double/single quoted strings.
|
||||
$contents = preg_replace(
|
||||
"<($double_quot|$single_quot)|$comment>Ss",
|
||||
"$1",
|
||||
$contents
|
||||
);
|
||||
// Remove certain whitespace.
|
||||
// There are different conditions for removing leading and trailing
|
||||
// whitespace.
|
||||
// @see http://php.net/manual/regexp.reference.subpatterns.php
|
||||
$contents = preg_replace('<
|
||||
# Strip leading and trailing whitespace.
|
||||
\s*([@{};,])\s*
|
||||
# Strip only leading whitespace from:
|
||||
# - Closing parenthesis: Retain "@media (bar) and foo".
|
||||
| \s+([\)])
|
||||
# Strip only trailing whitespace from:
|
||||
# - Opening parenthesis: Retain "@media (bar) and foo".
|
||||
# - Colon: Retain :pseudo-selectors.
|
||||
| ([\(:])\s+
|
||||
>xS',
|
||||
// Only one of the three capturing groups will match, so its reference
|
||||
// will contain the wanted value and the references for the
|
||||
// two non-matching groups will be replaced with empty strings.
|
||||
'$1$2$3',
|
||||
$contents
|
||||
);
|
||||
// End the file with a new line.
|
||||
$contents = trim($contents);
|
||||
$contents .= "\n";
|
||||
}
|
||||
|
||||
// Replaces @import commands with the actual stylesheet content.
|
||||
// This happens recursively but omits external files.
|
||||
$contents = preg_replace_callback('/@import\s*(?:url\(\s*)?[\'"]?(?![a-z]+:)(?!\/\/)([^\'"\()]+)[\'"]?\s*\)?\s*;/', array($this, 'loadNestedFile'), $contents);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefixes all paths within a CSS file for processFile().
|
||||
*
|
||||
* @param array $matches
|
||||
* An array of matches by a preg_replace_callback() call that scans for
|
||||
* url() references in CSS files, except for external or absolute ones.
|
||||
*
|
||||
* Note: the only reason this method is public is so color.module can call it;
|
||||
* it is not on the AssetOptimizerInterface, so future refactorings can make
|
||||
* it protected.
|
||||
*
|
||||
* @return string
|
||||
* The file path.
|
||||
*/
|
||||
public function rewriteFileURI($matches) {
|
||||
// Prefix with base and remove '../' segments where possible.
|
||||
$path = $this->rewriteFileURIBasePath . $matches[1];
|
||||
$last = '';
|
||||
while ($path != $last) {
|
||||
$last = $path;
|
||||
$path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
|
||||
}
|
||||
return 'url(' . file_create_url($path) . ')';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset\Exception;
|
||||
|
||||
/**
|
||||
* Defines a custom exception if a library has no CSS/JS/JS setting specified.
|
||||
*/
|
||||
class IncompleteLibraryDefinitionException extends \RuntimeException {
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\Exception\InvalidLibraryFileException.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset\Exception;
|
||||
|
||||
/**
|
||||
* Defines an exception if the library file could not be parsed.
|
||||
*/
|
||||
class InvalidLibraryFileException extends \RunTimeException {
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset\Exception;
|
||||
|
||||
/**
|
||||
* Defines a custom exception if a library has a remote but no license.
|
||||
*/
|
||||
class LibraryDefinitionMissingLicenseException extends \RuntimeException {
|
||||
|
||||
}
|
81
core/lib/Drupal/Core/Asset/JsCollectionGrouper.php
Normal file
81
core/lib/Drupal/Core/Asset/JsCollectionGrouper.php
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\JsCollectionGrouper.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\AssetCollectionGrouperInterface;
|
||||
|
||||
/**
|
||||
* Groups JavaScript assets.
|
||||
*/
|
||||
class JsCollectionGrouper implements AssetCollectionGrouperInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* Puts multiple items into the same group if they are groupable and if they
|
||||
* are for the same browsers. Items of the 'file' type are groupable if their
|
||||
* 'preprocess' flag is TRUE. Items of the 'inline', 'settings', or 'external'
|
||||
* type are not groupable.
|
||||
*
|
||||
* Also ensures that the process of grouping items does not change their
|
||||
* relative order. This requirement may result in multiple groups for the same
|
||||
* type and browsers, if needed to accommodate other items in between.
|
||||
*/
|
||||
public function group(array $js_assets) {
|
||||
$groups = array();
|
||||
// If a group can contain multiple items, we track the information that must
|
||||
// be the same for each item in the group, so that when we iterate the next
|
||||
// item, we can determine if it can be put into the current group, or if a
|
||||
// new group needs to be made for it.
|
||||
$current_group_keys = NULL;
|
||||
$index = -1;
|
||||
foreach ($js_assets as $item) {
|
||||
// The browsers for which the JavaScript item needs to be loaded is part
|
||||
// of the information that determines when a new group is needed, but the
|
||||
// order of keys in the array doesn't matter, and we don't want a new
|
||||
// group if all that's different is that order.
|
||||
ksort($item['browsers']);
|
||||
|
||||
switch ($item['type']) {
|
||||
case 'file':
|
||||
// Group file items if their 'preprocess' flag is TRUE.
|
||||
// Help ensure maximum reuse of aggregate files by only grouping
|
||||
// together items that share the same 'group' value and 'every_page'
|
||||
// flag.
|
||||
$group_keys = $item['preprocess'] ? array($item['type'], $item['group'], $item['every_page'], $item['browsers']) : FALSE;
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
case 'setting':
|
||||
case 'inline':
|
||||
// Do not group external, settings, and inline items.
|
||||
$group_keys = FALSE;
|
||||
break;
|
||||
}
|
||||
|
||||
// If the group keys don't match the most recent group we're working with,
|
||||
// then a new group must be made.
|
||||
if ($group_keys !== $current_group_keys) {
|
||||
$index++;
|
||||
// Initialize the new group with the same properties as the first item
|
||||
// being placed into it. The item's 'data' and 'weight' properties are
|
||||
// unique to the item and should not be carried over to the group.
|
||||
$groups[$index] = $item;
|
||||
unset($groups[$index]['data'], $groups[$index]['weight']);
|
||||
$groups[$index]['items'] = array();
|
||||
$current_group_keys = $group_keys ? $group_keys : NULL;
|
||||
}
|
||||
|
||||
// Add the item to the current group.
|
||||
$groups[$index]['items'][] = $item;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
}
|
197
core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
Normal file
197
core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php
Normal file
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\JsCollectionOptimizer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\State\StateInterface;
|
||||
|
||||
|
||||
/**
|
||||
* Optimizes JavaScript assets.
|
||||
*/
|
||||
class JsCollectionOptimizer implements AssetCollectionOptimizerInterface {
|
||||
|
||||
/**
|
||||
* A JS asset grouper.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\JsCollectionGrouper
|
||||
*/
|
||||
protected $grouper;
|
||||
|
||||
/**
|
||||
* A JS asset optimizer.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\JsOptimizer
|
||||
*/
|
||||
protected $optimizer;
|
||||
|
||||
/**
|
||||
* An asset dumper.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\AssetDumper
|
||||
*/
|
||||
protected $dumper;
|
||||
|
||||
/**
|
||||
* The state key/value store.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Constructs a JsCollectionOptimizer.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\AssetCollectionGrouperInterface
|
||||
* The grouper for JS assets.
|
||||
* @param \Drupal\Core\Asset\AssetOptimizerInterface
|
||||
* The optimizer for a single JS asset.
|
||||
* @param \Drupal\Core\Asset\AssetDumperInterface
|
||||
* The dumper for optimized JS assets.
|
||||
* @param \Drupal\Core\State\StateInterface
|
||||
* The state key/value store.
|
||||
*/
|
||||
public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) {
|
||||
$this->grouper = $grouper;
|
||||
$this->optimizer = $optimizer;
|
||||
$this->dumper = $dumper;
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* The cache file name is retrieved on a page load via a lookup variable that
|
||||
* contains an associative array. The array key is the hash of the names in
|
||||
* $files while the value is the cache file name. The cache file is generated
|
||||
* in two cases. First, if there is no file name value for the key, which will
|
||||
* happen if a new file name has been added to $files or after the lookup
|
||||
* variable is emptied to force a rebuild of the cache. Second, the cache file
|
||||
* is generated if it is missing on disk. Old cache files are not deleted
|
||||
* immediately when the lookup variable is emptied, but are deleted after a
|
||||
* configurable period (@code system.performance.stale_file_threshold @endcode)
|
||||
* to ensure that files referenced by a cached page will still be available.
|
||||
*/
|
||||
public function optimize(array $js_assets) {
|
||||
// Group the assets.
|
||||
$js_groups = $this->grouper->group($js_assets);
|
||||
|
||||
// Now optimize (concatenate, not minify) and dump each asset group, unless
|
||||
// that was already done, in which case it should appear in
|
||||
// system.js_cache_files.
|
||||
// Drupal contrib can override this default JS aggregator to keep the same
|
||||
// grouping, optimizing and dumping, but change the strategy that is used to
|
||||
// determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …).
|
||||
$map = $this->state->get('system.js_cache_files') ?: array();
|
||||
$js_assets = array();
|
||||
foreach ($js_groups as $order => $js_group) {
|
||||
// We have to return a single asset, not a group of assets. It is now up
|
||||
// to one of the pieces of code in the switch statement below to set the
|
||||
// 'data' property to the appropriate value.
|
||||
$js_assets[$order] = $js_group;
|
||||
unset($js_assets[$order]['items']);
|
||||
|
||||
switch ($js_group['type']) {
|
||||
case 'file':
|
||||
// No preprocessing, single JS asset: just use the existing URI.
|
||||
if (!$js_group['preprocess']) {
|
||||
$uri = $js_group['items'][0]['data'];
|
||||
$js_assets[$order]['data'] = $uri;
|
||||
}
|
||||
// Preprocess (aggregate), unless the aggregate file already exists.
|
||||
else {
|
||||
$key = $this->generateHash($js_group);
|
||||
$uri = '';
|
||||
if (isset($map[$key])) {
|
||||
$uri = $map[$key];
|
||||
}
|
||||
if (empty($uri) || !file_exists($uri)) {
|
||||
// Concatenate each asset within the group.
|
||||
$data = '';
|
||||
foreach ($js_group['items'] as $js_asset) {
|
||||
// Optimize this JS file, but only if it's not yet minified.
|
||||
if (isset($js_asset['minified']) && $js_asset['minified']) {
|
||||
$data .= file_get_contents($js_asset['data']);
|
||||
}
|
||||
else {
|
||||
$data .= $this->optimizer->optimize($js_asset);
|
||||
}
|
||||
// Append a ';' and a newline after each JS file to prevent them
|
||||
// from running together.
|
||||
$data .= ";\n";
|
||||
}
|
||||
// Remove unwanted JS code that cause issues.
|
||||
$data = $this->optimizer->clean($data);
|
||||
// Dump the optimized JS for this group into an aggregate file.
|
||||
$uri = $this->dumper->dump($data, 'js');
|
||||
// Set the URI for this group's aggregate file.
|
||||
$js_assets[$order]['data'] = $uri;
|
||||
// Persist the URI for this aggregate file.
|
||||
$map[$key] = $uri;
|
||||
$this->state->set('system.js_cache_files', $map);
|
||||
}
|
||||
else {
|
||||
// Use the persisted URI for the optimized JS file.
|
||||
$js_assets[$order]['data'] = $uri;
|
||||
}
|
||||
$js_assets[$order]['preprocessed'] = TRUE;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
case 'setting':
|
||||
case 'inline':
|
||||
// We don't do any aggregation and hence also no caching for external,
|
||||
// setting or inline JS assets.
|
||||
$uri = $js_group['items'][0]['data'];
|
||||
$js_assets[$order]['data'] = $uri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $js_assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a hash for a given group of JavaScript assets.
|
||||
*
|
||||
* @param array $js_group
|
||||
* A group of JavaScript assets.
|
||||
*
|
||||
* @return string
|
||||
* A hash to uniquely identify the given group of JavaScript assets.
|
||||
*/
|
||||
protected function generateHash(array $js_group) {
|
||||
$js_data = array();
|
||||
foreach ($js_group['items'] as $js_file) {
|
||||
$js_data[] = $js_file['data'];
|
||||
}
|
||||
return hash('sha256', serialize($js_data));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAll() {
|
||||
return $this->state->get('system.js_cache_files');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function deleteAll() {
|
||||
$this->state->delete('system.js_cache_files');
|
||||
$delete_stale = function($uri) {
|
||||
// Default stale file threshold is 30 days.
|
||||
if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) {
|
||||
file_unmanaged_delete($uri);
|
||||
}
|
||||
};
|
||||
file_scan_directory('public://js', '/.*/', array('callback' => $delete_stale));
|
||||
}
|
||||
|
||||
}
|
111
core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
Normal file
111
core/lib/Drupal/Core/Asset/JsCollectionRenderer.php
Normal file
|
@ -0,0 +1,111 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\JsCollectionRenderer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\State\StateInterface;
|
||||
|
||||
/**
|
||||
* Renders JavaScript assets.
|
||||
*/
|
||||
class JsCollectionRenderer implements AssetCollectionRendererInterface {
|
||||
|
||||
/**
|
||||
* The state key/value store.
|
||||
*
|
||||
* @var \Drupal\Core\State\StateInterface
|
||||
*/
|
||||
protected $state;
|
||||
|
||||
/**
|
||||
* Constructs a JsCollectionRenderer.
|
||||
*
|
||||
* @param \Drupal\Core\State\StateInterface
|
||||
* The state key/value store.
|
||||
*/
|
||||
public function __construct(StateInterface $state) {
|
||||
$this->state = $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* This class evaluates the aggregation enabled/disabled condition on a group
|
||||
* by group basis by testing whether an aggregate file has been made for the
|
||||
* group rather than by testing the site-wide aggregation setting. This allows
|
||||
* this class to work correctly even if modules have implemented custom
|
||||
* logic for grouping and aggregating files.
|
||||
*/
|
||||
public function render(array $js_assets) {
|
||||
$elements = array();
|
||||
|
||||
// A dummy query-string is added to filenames, to gain control over
|
||||
// browser-caching. The string changes on every update or full cache
|
||||
// flush, forcing browsers to load a new copy of the files, as the
|
||||
// URL changed. Files that should not be cached get REQUEST_TIME as
|
||||
// query-string instead, to enforce reload on every page request.
|
||||
$default_query_string = $this->state->get('system.css_js_query_string') ?: '0';
|
||||
|
||||
// For inline JavaScript to validate as XHTML, all JavaScript containing
|
||||
// XHTML needs to be wrapped in CDATA. To make that backwards compatible
|
||||
// with HTML 4, we need to comment out the CDATA-tag.
|
||||
$embed_prefix = "\n<!--//--><![CDATA[//><!--\n";
|
||||
$embed_suffix = "\n//--><!]]>\n";
|
||||
|
||||
// Defaults for each SCRIPT element.
|
||||
$element_defaults = array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'script',
|
||||
'#value' => '',
|
||||
);
|
||||
|
||||
// Loop through all JS assets.
|
||||
foreach ($js_assets as $js_asset) {
|
||||
// Element properties that do not depend on JS asset type.
|
||||
$element = $element_defaults;
|
||||
$element['#browsers'] = $js_asset['browsers'];
|
||||
|
||||
// Element properties that depend on item type.
|
||||
switch ($js_asset['type']) {
|
||||
case 'setting':
|
||||
$element['#value_prefix'] = $embed_prefix;
|
||||
$element['#value'] = 'var drupalSettings = ' . Json::encode($js_asset['data']) . ";";
|
||||
$element['#value_suffix'] = $embed_suffix;
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
$query_string = $js_asset['version'] == -1 ? $default_query_string : 'v=' . $js_asset['version'];
|
||||
$query_string_separator = (strpos($js_asset['data'], '?') !== FALSE) ? '&' : '?';
|
||||
$element['#attributes']['src'] = file_create_url($js_asset['data']);
|
||||
// Only add the cache-busting query string if this isn't an aggregate
|
||||
// file.
|
||||
if (!isset($js_asset['preprocessed'])) {
|
||||
$element['#attributes']['src'] .= $query_string_separator . ($js_asset['cache'] ? $query_string : REQUEST_TIME);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'external':
|
||||
$element['#attributes']['src'] = $js_asset['data'];
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \Exception('Invalid JS asset type.');
|
||||
}
|
||||
|
||||
// Attributes may only be set if this script is output independently.
|
||||
if (!empty($element['#attributes']['src']) && !empty($js_asset['attributes'])) {
|
||||
$element['#attributes'] += $js_asset['attributes'];
|
||||
}
|
||||
|
||||
$elements[] = $element;
|
||||
}
|
||||
|
||||
return $elements;
|
||||
}
|
||||
|
||||
}
|
60
core/lib/Drupal/Core/Asset/JsOptimizer.php
Normal file
60
core/lib/Drupal/Core/Asset/JsOptimizer.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\JsOptimizer.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\AssetOptimizerInterface;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Optimizes a JavaScript asset.
|
||||
*/
|
||||
class JsOptimizer implements AssetOptimizerInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function optimize(array $js_asset) {
|
||||
if ($js_asset['type'] !== 'file') {
|
||||
throw new \Exception('Only file JavaScript assets can be optimized.');
|
||||
}
|
||||
if ($js_asset['type'] === 'file' && !$js_asset['preprocess']) {
|
||||
throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.');
|
||||
}
|
||||
|
||||
// If a BOM is found, convert the file to UTF-8, then use substr() to
|
||||
// remove the BOM from the result.
|
||||
$data = file_get_contents($js_asset['data']);
|
||||
if ($encoding = (Unicode::encodingFromBOM($data))) {
|
||||
$data = Unicode::substr(Unicode::convertToUtf8($data, $encoding), 1);
|
||||
}
|
||||
// If no BOM is found, check for the charset attribute.
|
||||
elseif (isset($js_asset['attributes']['charset'])) {
|
||||
$data = Unicode::convertToUtf8($data, $js_asset['attributes']['charset']);
|
||||
}
|
||||
|
||||
// No-op optimizer: no optimizations are applied to JavaScript assets.
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the contents of a javascript asset for cleanup.
|
||||
*
|
||||
* @param string $contents
|
||||
* The contents of the javascript asset.
|
||||
*
|
||||
* @return string
|
||||
* Contents of the javascript asset.
|
||||
*/
|
||||
public function clean($contents) {
|
||||
// Remove JS source and source mapping urls or these may cause 404 errors.
|
||||
$contents = preg_replace('/\/\/(#|@)\s(sourceURL|sourceMappingURL)=\s*(\S*?)\s*$/m', '', $contents);
|
||||
|
||||
return $contents;
|
||||
}
|
||||
|
||||
}
|
100
core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
Normal file
100
core/lib/Drupal/Core/Asset/LibraryDependencyResolver.php
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDependencyResolver.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
|
||||
*/
|
||||
class LibraryDependencyResolver implements LibraryDependencyResolverInterface {
|
||||
|
||||
/**
|
||||
* The library discovery service.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\LibraryDiscoveryInterface
|
||||
*/
|
||||
protected $libraryDiscovery;
|
||||
|
||||
/**
|
||||
* Constructs a new LibraryDependencyResolver instance.
|
||||
*
|
||||
* @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery
|
||||
* The library discovery service.
|
||||
*/
|
||||
public function __construct(LibraryDiscoveryInterface $library_discovery) {
|
||||
$this->libraryDiscovery = $library_discovery;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibrariesWithDependencies(array $libraries) {
|
||||
return $this->doGetDependencies($libraries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the given libraries with its dependencies.
|
||||
*
|
||||
* Helper method for ::getLibrariesWithDependencies().
|
||||
*
|
||||
* @param string[] $libraries_with_unresolved_dependencies
|
||||
* A list of libraries, with unresolved dependencies, in the order they
|
||||
* should be loaded.
|
||||
* @param string[] $final_libraries
|
||||
* The final list of libraries (the return value) that is being built
|
||||
* recursively.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of libraries, in the order they should be loaded, including their
|
||||
* dependencies.
|
||||
*/
|
||||
protected function doGetDependencies(array $libraries_with_unresolved_dependencies, array $final_libraries = []) {
|
||||
foreach ($libraries_with_unresolved_dependencies as $library) {
|
||||
if (!in_array($library, $final_libraries)) {
|
||||
list($extension, $name) = explode('/', $library, 2);
|
||||
$definition = $this->libraryDiscovery->getLibraryByName($extension, $name);
|
||||
if (!empty($definition['dependencies'])) {
|
||||
$final_libraries = $this->doGetDependencies($definition['dependencies'], $final_libraries);
|
||||
}
|
||||
$final_libraries[] = $library;
|
||||
}
|
||||
}
|
||||
return $final_libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMinimalRepresentativeSubset(array $libraries) {
|
||||
$minimal = [];
|
||||
|
||||
// Determine each library's dependencies.
|
||||
$with_deps = [];
|
||||
foreach ($libraries as $library) {
|
||||
$with_deps[$library] = $this->getLibrariesWithDependencies([$library]);
|
||||
}
|
||||
|
||||
foreach ($libraries as $library) {
|
||||
$exists = FALSE;
|
||||
foreach ($with_deps as $other_library => $dependencies) {
|
||||
if ($library == $other_library) {
|
||||
continue;
|
||||
}
|
||||
if (in_array($library, $dependencies)) {
|
||||
$exists = TRUE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$exists) {
|
||||
$minimal[] = $library;
|
||||
}
|
||||
}
|
||||
|
||||
return $minimal;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDependencyResolverInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Resolves the dependencies of asset (CSS/JavaScript) libraries.
|
||||
*/
|
||||
interface LibraryDependencyResolverInterface {
|
||||
|
||||
/**
|
||||
* Gets the given libraries with their dependencies.
|
||||
*
|
||||
* Given ['core/a', 'core/b', 'core/c'], with core/a depending on core/c and
|
||||
* core/b on core/d, returns ['core/a', 'core/b', 'core/c', 'core/d'].
|
||||
*
|
||||
* @param string[] $libraries
|
||||
* A list of libraries, in the order they should be loaded.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of libraries, in the order they should be loaded, including their
|
||||
* dependencies.
|
||||
*/
|
||||
public function getLibrariesWithDependencies(array $libraries);
|
||||
|
||||
/**
|
||||
* Gets the minimal representative subset of the given libraries.
|
||||
*
|
||||
* A minimal representative subset means that any library in the given set of
|
||||
* libraries that is a dependency of another library in the set, is removed.
|
||||
*
|
||||
* Hence a minimal representative subset is the most compact representation
|
||||
* possible of a set of libraries.
|
||||
*
|
||||
* (Each asset library has dependencies and can therefore be seen as a tree.
|
||||
* Hence the given list of libraries represent a forest. This function returns
|
||||
* all roots of trees that are not a subtree of another tree in the forest.)
|
||||
*
|
||||
* @param string[] $libraries
|
||||
* A set of libraries.
|
||||
*
|
||||
* @return string[]
|
||||
* A representative subset of the given set of libraries.
|
||||
*/
|
||||
public function getMinimalRepresentativeSubset(array $libraries);
|
||||
|
||||
}
|
92
core/lib/Drupal/Core/Asset/LibraryDiscovery.php
Normal file
92
core/lib/Drupal/Core/Asset/LibraryDiscovery.php
Normal file
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDiscovery.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Cache\CacheCollectorInterface;
|
||||
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
|
||||
/**
|
||||
* Discovers available asset libraries in Drupal.
|
||||
*/
|
||||
class LibraryDiscovery implements LibraryDiscoveryInterface {
|
||||
|
||||
/**
|
||||
* The library discovery cache collector.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheCollectorInterface
|
||||
*/
|
||||
protected $collector;
|
||||
|
||||
/**
|
||||
* The cache tag invalidator.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
|
||||
*/
|
||||
protected $cacheTagInvalidator;
|
||||
|
||||
/**
|
||||
* The final library definitions, statically cached.
|
||||
*
|
||||
* hook_library_info_alter() and hook_js_settings_alter() allows modules
|
||||
* and themes to dynamically alter a library definition (once per request).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $libraryDefinitions = [];
|
||||
|
||||
/**
|
||||
* Constructs a new LibraryDiscovery instance.
|
||||
*
|
||||
* @param \Drupal\Core\Cache\CacheCollectorInterface $library_discovery_collector
|
||||
* The library discovery cache collector.
|
||||
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tag_invalidator
|
||||
* The cache tag invalidator.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
* @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
|
||||
* The theme manager.
|
||||
*/
|
||||
public function __construct(CacheCollectorInterface $library_discovery_collector, CacheTagsInvalidatorInterface $cache_tag_invalidator) {
|
||||
$this->collector = $library_discovery_collector;
|
||||
$this->cacheTagInvalidator = $cache_tag_invalidator;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibrariesByExtension($extension) {
|
||||
if (!isset($this->libraryDefinitions[$extension])) {
|
||||
$libraries = $this->collector->get($extension);
|
||||
$this->libraryDefinitions[$extension] = [];
|
||||
foreach ($libraries as $name => $definition) {
|
||||
$library_name = "$extension/$name";
|
||||
$this->libraryDefinitions[$extension][$name] = $definition;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->libraryDefinitions[$extension];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraryByName($extension, $name) {
|
||||
$extension = $this->getLibrariesByExtension($extension);
|
||||
return isset($extension[$name]) ? $extension[$name] : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function clearCachedDefinitions() {
|
||||
$this->cacheTagInvalidator->invalidateTags(['library_info']);
|
||||
}
|
||||
|
||||
}
|
87
core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
Normal file
87
core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDiscoveryCollector.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Cache\CacheCollector;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Lock\LockBackendInterface;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
|
||||
/**
|
||||
* A CacheCollector implementation for building library extension info.
|
||||
*/
|
||||
class LibraryDiscoveryCollector extends CacheCollector {
|
||||
|
||||
/**
|
||||
* The cache backend.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheBackendInterface
|
||||
*/
|
||||
protected $cache;
|
||||
|
||||
/**
|
||||
* The lock backend.
|
||||
*
|
||||
* @var \Drupal\Core\Lock\LockBackendInterface
|
||||
*/
|
||||
protected $lock;
|
||||
|
||||
/**
|
||||
* The library discovery parser.
|
||||
*
|
||||
* @var \Drupal\Core\Asset\LibraryDiscoveryParser
|
||||
*/
|
||||
protected $discoveryParser;
|
||||
|
||||
/**
|
||||
* The theme manager.
|
||||
*
|
||||
* @var \Drupal\Core\Theme\ThemeManagerInterface
|
||||
*/
|
||||
protected $themeManager;
|
||||
|
||||
/**
|
||||
* Constructs a CacheCollector object.
|
||||
*
|
||||
* @param string $cid
|
||||
* The cid for the array being cached.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
|
||||
* The cache backend.
|
||||
* @param \Drupal\Core\Lock\LockBackendInterface $lock
|
||||
* The lock backend.
|
||||
* @param \Drupal\Core\Asset\LibraryDiscoveryParser $discovery_parser
|
||||
* The library discovery parser.
|
||||
*/
|
||||
public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, LibraryDiscoveryParser $discovery_parser, ThemeManagerInterface $theme_manager) {
|
||||
$this->themeManager = $theme_manager;
|
||||
parent::__construct(NULL, $cache, $lock, ['library_info']);
|
||||
|
||||
$this->discoveryParser = $discovery_parser;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getCid() {
|
||||
if (!isset($this->cid)) {
|
||||
$this->cid = 'library_info:' . $this->themeManager->getActiveTheme()->getName();
|
||||
}
|
||||
|
||||
return $this->cid;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function resolveCacheMiss($key) {
|
||||
$this->storage[$key] = $this->discoveryParser->buildByExtension($key);
|
||||
$this->persist($key);
|
||||
|
||||
return $this->storage[$key];
|
||||
}
|
||||
}
|
58
core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php
Normal file
58
core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDiscoveryInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
/**
|
||||
* Discovers information for asset (CSS/JavaScript) libraries.
|
||||
*
|
||||
* Library information is statically cached. Libraries are keyed by extension
|
||||
* for several reasons:
|
||||
* - Libraries are not unique. Multiple extensions might ship with the same
|
||||
* library in a different version or variant. This registry cannot (and does
|
||||
* not attempt to) prevent library conflicts.
|
||||
* - Extensions implementing and thereby depending on a library that is
|
||||
* registered by another extension can only rely on that extension's library.
|
||||
* - Two (or more) extensions can still register the same library and use it
|
||||
* without conflicts in case the libraries are loaded on certain pages only.
|
||||
*/
|
||||
interface LibraryDiscoveryInterface {
|
||||
|
||||
/**
|
||||
* Gets all libraries defined by an extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* The name of the extension that registered a library.
|
||||
*
|
||||
* @return array
|
||||
* An associative array of libraries registered by $extension is returned
|
||||
* (which may be empty).
|
||||
*
|
||||
* @see self::getLibraryByName()
|
||||
*/
|
||||
public function getLibrariesByExtension($extension);
|
||||
|
||||
/**
|
||||
* Gets a single library defined by an extension by name.
|
||||
*
|
||||
* @param string $extension
|
||||
* The name of the extension that registered a library.
|
||||
* @param string $name
|
||||
* The name of a registered library to retrieve.
|
||||
*
|
||||
* @return array|FALSE
|
||||
* The definition of the requested library, if $name was passed and it
|
||||
* exists, otherwise FALSE.
|
||||
*/
|
||||
public function getLibraryByName($extension, $name);
|
||||
|
||||
/**
|
||||
* Clears static and persistent library definition caches.
|
||||
*/
|
||||
public function clearCachedDefinitions();
|
||||
|
||||
}
|
330
core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
Normal file
330
core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
Normal file
|
@ -0,0 +1,330 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\Asset\LibraryDiscoveryParser.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Asset;
|
||||
|
||||
use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException;
|
||||
use Drupal\Core\Asset\Exception\InvalidLibraryFileException;
|
||||
use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Theme\ThemeManagerInterface;
|
||||
use Drupal\Component\Serialization\Exception\InvalidDataTypeException;
|
||||
use Drupal\Component\Serialization\Yaml;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
|
||||
/**
|
||||
* Parses library files to get extension data.
|
||||
*/
|
||||
class LibraryDiscoveryParser {
|
||||
|
||||
/**
|
||||
* The module handler.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* The theme manager.
|
||||
*
|
||||
* @var \Drupal\Core\Theme\ThemeManagerInterface
|
||||
*/
|
||||
protected $themeManager;
|
||||
|
||||
/**
|
||||
* The app root.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $root;
|
||||
|
||||
/**
|
||||
* Constructs a new LibraryDiscoveryParser instance.
|
||||
*
|
||||
* @param string $root
|
||||
* The app root.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
*/
|
||||
public function __construct($root, ModuleHandlerInterface $module_handler, ThemeManagerInterface $theme_manager) {
|
||||
$this->root = $root;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->themeManager = $theme_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and builds up all the libraries information of an extension.
|
||||
*
|
||||
* @param string $extension
|
||||
* The name of the extension that registered a library.
|
||||
*
|
||||
* @return array
|
||||
* All library definitions of the passed extension.
|
||||
*
|
||||
* @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException
|
||||
* Thrown when a library has no js/css/setting.
|
||||
* @throws \UnexpectedValueException
|
||||
* Thrown when a js file defines a positive weight.
|
||||
*/
|
||||
public function buildByExtension($extension) {
|
||||
$libraries = array();
|
||||
|
||||
if ($extension === 'core') {
|
||||
$path = 'core';
|
||||
$extension_type = 'core';
|
||||
}
|
||||
else {
|
||||
if ($this->moduleHandler->moduleExists($extension)) {
|
||||
$extension_type = 'module';
|
||||
}
|
||||
else {
|
||||
$extension_type = 'theme';
|
||||
}
|
||||
$path = $this->drupalGetPath($extension_type, $extension);
|
||||
}
|
||||
|
||||
$libraries = $this->parseLibraryInfo($extension, $path);
|
||||
|
||||
foreach ($libraries as $id => &$library) {
|
||||
if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) {
|
||||
throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension));
|
||||
}
|
||||
$library += array('dependencies' => array(), 'js' => array(), 'css' => array());
|
||||
|
||||
if (isset($library['header']) && !is_bool($library['header'])) {
|
||||
throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension));
|
||||
}
|
||||
|
||||
if (isset($library['version'])) {
|
||||
// @todo Retrieve version of a non-core extension.
|
||||
if ($library['version'] === 'VERSION') {
|
||||
$library['version'] = \Drupal::VERSION;
|
||||
}
|
||||
// Remove 'v' prefix from external library versions.
|
||||
elseif ($library['version'][0] === 'v') {
|
||||
$library['version'] = substr($library['version'], 1);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a 3rd party library, the license info is required.
|
||||
if (isset($library['remote']) && !isset($library['license'])) {
|
||||
throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for definition '%s' extension '%s': it has a remote, but no license.", $id, $extension));
|
||||
}
|
||||
|
||||
// Assign Drupal's license to libraries that don't have license info.
|
||||
if (!isset($library['license'])) {
|
||||
$library['license'] = array(
|
||||
'name' => 'GNU-GPL-2.0-or-later',
|
||||
'url' => 'https://www.drupal.org/licensing/faq',
|
||||
'gpl-compatible' => TRUE,
|
||||
);
|
||||
}
|
||||
|
||||
foreach (array('js', 'css') as $type) {
|
||||
// Prepare (flatten) the SMACSS-categorized definitions.
|
||||
// @todo After Asset(ic) changes, retain the definitions as-is and
|
||||
// properly resolve dependencies for all (css) libraries per category,
|
||||
// and only once prior to rendering out an HTML page.
|
||||
if ($type == 'css' && !empty($library[$type])) {
|
||||
foreach ($library[$type] as $category => $files) {
|
||||
foreach ($files as $source => $options) {
|
||||
if (!isset($options['weight'])) {
|
||||
$options['weight'] = 0;
|
||||
}
|
||||
// Apply the corresponding weight defined by CSS_* constants.
|
||||
$options['weight'] += constant('CSS_' . strtoupper($category));
|
||||
$library[$type][$source] = $options;
|
||||
}
|
||||
unset($library[$type][$category]);
|
||||
}
|
||||
}
|
||||
foreach ($library[$type] as $source => $options) {
|
||||
unset($library[$type][$source]);
|
||||
// Allow to omit the options hashmap in YAML declarations.
|
||||
if (!is_array($options)) {
|
||||
$options = array();
|
||||
}
|
||||
if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) {
|
||||
throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library.");
|
||||
}
|
||||
// Unconditionally apply default groups for the defined asset files.
|
||||
// The library system is a dependency management system. Each library
|
||||
// properly specifies its dependencies instead of relying on a custom
|
||||
// processing order.
|
||||
if ($type == 'js') {
|
||||
$options['group'] = JS_LIBRARY;
|
||||
}
|
||||
elseif ($type == 'css') {
|
||||
$options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT;
|
||||
}
|
||||
// By default, all library assets are files.
|
||||
if (!isset($options['type'])) {
|
||||
$options['type'] = 'file';
|
||||
}
|
||||
if ($options['type'] == 'external') {
|
||||
$options['data'] = $source;
|
||||
}
|
||||
// Determine the file asset URI.
|
||||
else {
|
||||
if ($source[0] === '/') {
|
||||
// An absolute path maps to DRUPAL_ROOT / base_path().
|
||||
if ($source[1] !== '/') {
|
||||
$options['data'] = substr($source, 1);
|
||||
}
|
||||
// A protocol-free URI (e.g., //cdn.com/example.js) is external.
|
||||
else {
|
||||
$options['type'] = 'external';
|
||||
$options['data'] = $source;
|
||||
}
|
||||
}
|
||||
// A stream wrapper URI (e.g., public://generated_js/example.js).
|
||||
elseif ($this->fileValidUri($source)) {
|
||||
$options['data'] = $source;
|
||||
}
|
||||
// By default, file paths are relative to the registering extension.
|
||||
else {
|
||||
$options['data'] = $path . '/' . $source;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($library['version'])) {
|
||||
// @todo Get the information from the extension.
|
||||
$options['version'] = -1;
|
||||
}
|
||||
else {
|
||||
$options['version'] = $library['version'];
|
||||
}
|
||||
|
||||
// Set the 'minified' flag on JS file assets, default to FALSE.
|
||||
if ($type == 'js' && $options['type'] == 'file') {
|
||||
$options['minified'] = isset($options['minified']) ? $options['minified'] : FALSE;
|
||||
}
|
||||
|
||||
$library[$type][] = $options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a given library file and allows modules and themes to alter it.
|
||||
*
|
||||
* This method sets the parsed information onto the library property.
|
||||
*
|
||||
* Library information is parsed from *.libraries.yml files; see
|
||||
* editor.library.yml for an example. Every library must have at least one js
|
||||
* or css entry. Each entry starts with a machine name and defines the
|
||||
* following elements:
|
||||
* - js: A list of JavaScript files to include. Each file is keyed by the file
|
||||
* path. An item can have several attributes (like HTML
|
||||
* attributes). For example:
|
||||
* @code
|
||||
* js:
|
||||
* path/js/file.js: { attributes: { defer: true } }
|
||||
* @endcode
|
||||
* If the file has no special attributes, just use an empty object:
|
||||
* @code
|
||||
* js:
|
||||
* path/js/file.js: {}
|
||||
* @endcode
|
||||
* The path of the file is relative to the module or theme directory, unless
|
||||
* it starts with a /, in which case it is relative to the Drupal root. If
|
||||
* the file path starts with //, it will be treated as a protocol-free,
|
||||
* external resource (e.g., //cdn.com/library.js). Full URLs
|
||||
* (e.g., http://cdn.com/library.js) as well as URLs that use a valid
|
||||
* stream wrapper (e.g., public://path/to/file.js) are also supported.
|
||||
* - css: A list of categories for which the library provides CSS files. The
|
||||
* available categories are:
|
||||
* - base
|
||||
* - layout
|
||||
* - component
|
||||
* - state
|
||||
* - theme
|
||||
* Each category is itself a key for a sub-list of CSS files to include:
|
||||
* @code
|
||||
* css:
|
||||
* component:
|
||||
* css/file.css: {}
|
||||
* @endcode
|
||||
* Just like with JavaScript files, each CSS file is the key of an object
|
||||
* that can define specific attributes. The format of the file path is the
|
||||
* same as for the JavaScript files.
|
||||
* - dependencies: A list of libraries this library depends on.
|
||||
* - version: The library version. The string "VERSION" can be used to mean
|
||||
* the current Drupal core version.
|
||||
* - header: By default, JavaScript files are included in the footer. If the
|
||||
* script must be included in the header (along with all its dependencies),
|
||||
* set this to true. Defaults to false.
|
||||
* - minified: If the file is already minified, set this to true to avoid
|
||||
* minifying it again. Defaults to false.
|
||||
* - remote: If the library is a third-party script, this provides the
|
||||
* repository URL for reference.
|
||||
* - license: If the remote property is set, the license information is
|
||||
* required. It has 3 properties:
|
||||
* - name: The human-readable name of the license.
|
||||
* - url: The URL of the license file/information for the version of the
|
||||
* library used.
|
||||
* - gpl-compatible: A Boolean for whether this library is GPL compatible.
|
||||
*
|
||||
* See https://www.drupal.org/node/2274843#define-library for more
|
||||
* information.
|
||||
*
|
||||
* @param string $extension
|
||||
* The name of the extension that registered a library.
|
||||
* @param string $path
|
||||
* The relative path to the extension.
|
||||
*
|
||||
* @return array
|
||||
* An array of parsed library data.
|
||||
*
|
||||
* @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException
|
||||
* Thrown when a parser exception got thrown.
|
||||
*/
|
||||
protected function parseLibraryInfo($extension, $path) {
|
||||
$libraries = [];
|
||||
|
||||
$library_file = $path . '/' . $extension . '.libraries.yml';
|
||||
if (file_exists($this->root . '/' . $library_file)) {
|
||||
try {
|
||||
$libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file));
|
||||
}
|
||||
catch (InvalidDataTypeException $e) {
|
||||
// Rethrow a more helpful exception to provide context.
|
||||
throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow modules to add dynamic library definitions.
|
||||
$hook = 'library_info_build';
|
||||
if ($this->moduleHandler->implementsHook($extension, $hook)) {
|
||||
$libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook));
|
||||
}
|
||||
|
||||
// Allow modules to alter the module's registered libraries.
|
||||
$this->moduleHandler->alter('library_info', $libraries, $extension);
|
||||
$this->themeManager->alter('library_info', $libraries, $extension);
|
||||
|
||||
return $libraries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps drupal_get_path().
|
||||
*/
|
||||
protected function drupalGetPath($type, $name) {
|
||||
return drupal_get_path($type, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps file_valid_uri().
|
||||
*/
|
||||
protected function fileValidUri($source) {
|
||||
return file_valid_uri($source);
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue