Chris@0: root = $root; Chris@0: $this->moduleHandler = $module_handler; Chris@0: $this->themeManager = $theme_manager; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses and builds up all the libraries information of an extension. Chris@0: * Chris@0: * @param string $extension Chris@0: * The name of the extension that registered a library. Chris@0: * Chris@0: * @return array Chris@0: * All library definitions of the passed extension. Chris@0: * Chris@0: * @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException Chris@0: * Thrown when a library has no js/css/setting. Chris@0: * @throws \UnexpectedValueException Chris@0: * Thrown when a js file defines a positive weight. Chris@0: */ Chris@0: public function buildByExtension($extension) { Chris@0: if ($extension === 'core') { Chris@0: $path = 'core'; Chris@0: $extension_type = 'core'; Chris@0: } Chris@0: else { Chris@0: if ($this->moduleHandler->moduleExists($extension)) { Chris@0: $extension_type = 'module'; Chris@0: } Chris@0: else { Chris@0: $extension_type = 'theme'; Chris@0: } Chris@0: $path = $this->drupalGetPath($extension_type, $extension); Chris@0: } Chris@0: Chris@0: $libraries = $this->parseLibraryInfo($extension, $path); Chris@0: $libraries = $this->applyLibrariesOverride($libraries, $extension); Chris@0: Chris@0: foreach ($libraries as $id => &$library) { Chris@0: if (!isset($library['js']) && !isset($library['css']) && !isset($library['drupalSettings'])) { Chris@0: throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for definition '%s' in extension '%s'", $id, $extension)); Chris@0: } Chris@0: $library += ['dependencies' => [], 'js' => [], 'css' => []]; Chris@0: Chris@0: if (isset($library['header']) && !is_bool($library['header'])) { Chris@0: throw new \LogicException(sprintf("The 'header' key in the library definition '%s' in extension '%s' is invalid: it must be a boolean.", $id, $extension)); Chris@0: } Chris@0: Chris@0: if (isset($library['version'])) { Chris@0: // @todo Retrieve version of a non-core extension. Chris@0: if ($library['version'] === 'VERSION') { Chris@0: $library['version'] = \Drupal::VERSION; Chris@0: } Chris@0: // Remove 'v' prefix from external library versions. Chris@0: elseif ($library['version'][0] === 'v') { Chris@0: $library['version'] = substr($library['version'], 1); Chris@0: } Chris@0: } Chris@0: Chris@0: // If this is a 3rd party library, the license info is required. Chris@0: if (isset($library['remote']) && !isset($library['license'])) { Chris@0: throw new LibraryDefinitionMissingLicenseException(sprintf("Missing license information in library definition for definition '%s' extension '%s': it has a remote, but no license.", $id, $extension)); Chris@0: } Chris@0: Chris@0: // Assign Drupal's license to libraries that don't have license info. Chris@0: if (!isset($library['license'])) { Chris@0: $library['license'] = [ Chris@0: 'name' => 'GNU-GPL-2.0-or-later', Chris@0: 'url' => 'https://www.drupal.org/licensing/faq', Chris@0: 'gpl-compatible' => TRUE, Chris@0: ]; Chris@0: } Chris@0: Chris@0: foreach (['js', 'css'] as $type) { Chris@0: // Prepare (flatten) the SMACSS-categorized definitions. Chris@0: // @todo After Asset(ic) changes, retain the definitions as-is and Chris@0: // properly resolve dependencies for all (css) libraries per category, Chris@0: // and only once prior to rendering out an HTML page. Chris@0: if ($type == 'css' && !empty($library[$type])) { Chris@14: assert(static::validateCssLibrary($library[$type]) < 2, 'CSS files should be specified as key/value pairs, where the values are configuration options. See https://www.drupal.org/node/2274843.'); Chris@14: assert(static::validateCssLibrary($library[$type]) === 0, 'CSS must be nested under a category. See https://www.drupal.org/node/2274843.'); Chris@0: foreach ($library[$type] as $category => $files) { Chris@0: $category_weight = 'CSS_' . strtoupper($category); Chris@14: assert(defined($category_weight), 'Invalid CSS category: ' . $category . '. See https://www.drupal.org/node/2274843.'); Chris@0: foreach ($files as $source => $options) { Chris@0: if (!isset($options['weight'])) { Chris@0: $options['weight'] = 0; Chris@0: } Chris@0: // Apply the corresponding weight defined by CSS_* constants. Chris@0: $options['weight'] += constant($category_weight); Chris@0: $library[$type][$source] = $options; Chris@0: } Chris@0: unset($library[$type][$category]); Chris@0: } Chris@0: } Chris@0: foreach ($library[$type] as $source => $options) { Chris@0: unset($library[$type][$source]); Chris@0: // Allow to omit the options hashmap in YAML declarations. Chris@0: if (!is_array($options)) { Chris@0: $options = []; Chris@0: } Chris@0: if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) { Chris@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."); Chris@0: } Chris@0: // Unconditionally apply default groups for the defined asset files. Chris@0: // The library system is a dependency management system. Each library Chris@0: // properly specifies its dependencies instead of relying on a custom Chris@0: // processing order. Chris@0: if ($type == 'js') { Chris@0: $options['group'] = JS_LIBRARY; Chris@0: } Chris@0: elseif ($type == 'css') { Chris@0: $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT; Chris@0: } Chris@0: // By default, all library assets are files. Chris@0: if (!isset($options['type'])) { Chris@0: $options['type'] = 'file'; Chris@0: } Chris@0: if ($options['type'] == 'external') { Chris@0: $options['data'] = $source; Chris@0: } Chris@0: // Determine the file asset URI. Chris@0: else { Chris@0: if ($source[0] === '/') { Chris@0: // An absolute path maps to DRUPAL_ROOT / base_path(). Chris@0: if ($source[1] !== '/') { Chris@0: $options['data'] = substr($source, 1); Chris@0: } Chris@0: // A protocol-free URI (e.g., //cdn.com/example.js) is external. Chris@0: else { Chris@0: $options['type'] = 'external'; Chris@0: $options['data'] = $source; Chris@0: } Chris@0: } Chris@0: // A stream wrapper URI (e.g., public://generated_js/example.js). Chris@0: elseif ($this->fileValidUri($source)) { Chris@0: $options['data'] = $source; Chris@0: } Chris@0: // A regular URI (e.g., http://example.com/example.js) without Chris@0: // 'external' explicitly specified, which may happen if, e.g. Chris@0: // libraries-override is used. Chris@0: elseif ($this->isValidUri($source)) { Chris@0: $options['type'] = 'external'; Chris@0: $options['data'] = $source; Chris@0: } Chris@0: // By default, file paths are relative to the registering extension. Chris@0: else { Chris@0: $options['data'] = $path . '/' . $source; Chris@0: } Chris@0: } Chris@0: Chris@0: if (!isset($library['version'])) { Chris@0: // @todo Get the information from the extension. Chris@0: $options['version'] = -1; Chris@0: } Chris@0: else { Chris@0: $options['version'] = $library['version']; Chris@0: } Chris@0: Chris@0: // Set the 'minified' flag on JS file assets, default to FALSE. Chris@0: if ($type == 'js' && $options['type'] == 'file') { Chris@0: $options['minified'] = isset($options['minified']) ? $options['minified'] : FALSE; Chris@0: } Chris@0: Chris@0: $library[$type][] = $options; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return $libraries; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Parses a given library file and allows modules and themes to alter it. Chris@0: * Chris@0: * This method sets the parsed information onto the library property. Chris@0: * Chris@0: * Library information is parsed from *.libraries.yml files; see Chris@0: * editor.library.yml for an example. Every library must have at least one js Chris@0: * or css entry. Each entry starts with a machine name and defines the Chris@0: * following elements: Chris@0: * - js: A list of JavaScript files to include. Each file is keyed by the file Chris@0: * path. An item can have several attributes (like HTML Chris@0: * attributes). For example: Chris@0: * @code Chris@0: * js: Chris@0: * path/js/file.js: { attributes: { defer: true } } Chris@0: * @endcode Chris@0: * If the file has no special attributes, just use an empty object: Chris@0: * @code Chris@0: * js: Chris@0: * path/js/file.js: {} Chris@0: * @endcode Chris@0: * The path of the file is relative to the module or theme directory, unless Chris@0: * it starts with a /, in which case it is relative to the Drupal root. If Chris@0: * the file path starts with //, it will be treated as a protocol-free, Chris@0: * external resource (e.g., //cdn.com/library.js). Full URLs Chris@0: * (e.g., http://cdn.com/library.js) as well as URLs that use a valid Chris@0: * stream wrapper (e.g., public://path/to/file.js) are also supported. Chris@0: * - css: A list of categories for which the library provides CSS files. The Chris@0: * available categories are: Chris@0: * - base Chris@0: * - layout Chris@0: * - component Chris@0: * - state Chris@0: * - theme Chris@0: * Each category is itself a key for a sub-list of CSS files to include: Chris@0: * @code Chris@0: * css: Chris@0: * component: Chris@0: * css/file.css: {} Chris@0: * @endcode Chris@0: * Just like with JavaScript files, each CSS file is the key of an object Chris@0: * that can define specific attributes. The format of the file path is the Chris@0: * same as for the JavaScript files. Chris@0: * - dependencies: A list of libraries this library depends on. Chris@0: * - version: The library version. The string "VERSION" can be used to mean Chris@0: * the current Drupal core version. Chris@0: * - header: By default, JavaScript files are included in the footer. If the Chris@0: * script must be included in the header (along with all its dependencies), Chris@0: * set this to true. Defaults to false. Chris@0: * - minified: If the file is already minified, set this to true to avoid Chris@0: * minifying it again. Defaults to false. Chris@0: * - remote: If the library is a third-party script, this provides the Chris@0: * repository URL for reference. Chris@0: * - license: If the remote property is set, the license information is Chris@0: * required. It has 3 properties: Chris@0: * - name: The human-readable name of the license. Chris@0: * - url: The URL of the license file/information for the version of the Chris@0: * library used. Chris@0: * - gpl-compatible: A Boolean for whether this library is GPL compatible. Chris@0: * Chris@0: * See https://www.drupal.org/node/2274843#define-library for more Chris@0: * information. Chris@0: * Chris@0: * @param string $extension Chris@0: * The name of the extension that registered a library. Chris@0: * @param string $path Chris@0: * The relative path to the extension. Chris@0: * Chris@0: * @return array Chris@0: * An array of parsed library data. Chris@0: * Chris@0: * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException Chris@0: * Thrown when a parser exception got thrown. Chris@0: */ Chris@0: protected function parseLibraryInfo($extension, $path) { Chris@0: $libraries = []; Chris@0: Chris@0: $library_file = $path . '/' . $extension . '.libraries.yml'; Chris@0: if (file_exists($this->root . '/' . $library_file)) { Chris@0: try { Chris@0: $libraries = Yaml::decode(file_get_contents($this->root . '/' . $library_file)); Chris@0: } Chris@0: catch (InvalidDataTypeException $e) { Chris@0: // Rethrow a more helpful exception to provide context. Chris@0: throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e); Chris@0: } Chris@0: } Chris@0: Chris@0: // Allow modules to add dynamic library definitions. Chris@0: $hook = 'library_info_build'; Chris@0: if ($this->moduleHandler->implementsHook($extension, $hook)) { Chris@0: $libraries = NestedArray::mergeDeep($libraries, $this->moduleHandler->invoke($extension, $hook)); Chris@0: } Chris@0: Chris@0: // Allow modules to alter the module's registered libraries. Chris@0: $this->moduleHandler->alter('library_info', $libraries, $extension); Chris@0: $this->themeManager->alter('library_info', $libraries, $extension); Chris@0: Chris@0: return $libraries; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Apply libraries overrides specified for the current active theme. Chris@0: * Chris@0: * @param array $libraries Chris@0: * The libraries definitions. Chris@0: * @param string $extension Chris@0: * The extension in which these libraries are defined. Chris@0: * Chris@0: * @return array Chris@0: * The modified libraries definitions. Chris@0: */ Chris@0: protected function applyLibrariesOverride($libraries, $extension) { Chris@0: $active_theme = $this->themeManager->getActiveTheme(); Chris@0: // ActiveTheme::getLibrariesOverride() returns libraries-overrides for the Chris@0: // current theme as well as all its base themes. Chris@0: $all_libraries_overrides = $active_theme->getLibrariesOverride(); Chris@0: foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) { Chris@0: foreach ($libraries as $library_name => $library) { Chris@0: // Process libraries overrides. Chris@0: if (isset($libraries_overrides["$extension/$library_name"])) { Chris@0: // Active theme defines an override for this library. Chris@0: $override_definition = $libraries_overrides["$extension/$library_name"]; Chris@0: if (is_string($override_definition) || $override_definition === FALSE) { Chris@0: // A string or boolean definition implies an override (or removal) Chris@0: // for the whole library. Use the override key to specify that this Chris@0: // library will be overridden when it is called. Chris@0: // @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName() Chris@0: if ($override_definition) { Chris@0: $libraries[$library_name]['override'] = $override_definition; Chris@0: } Chris@0: else { Chris@0: $libraries[$library_name]['override'] = FALSE; Chris@0: } Chris@0: } Chris@0: elseif (is_array($override_definition)) { Chris@0: // An array definition implies an override for an asset within this Chris@0: // library. Chris@0: foreach ($override_definition as $sub_key => $value) { Chris@0: // Throw an exception if the asset is not properly specified. Chris@0: if (!is_array($value)) { Chris@0: throw new InvalidLibrariesOverrideSpecificationException(sprintf('Library asset %s is not correctly specified. It should be in the form "extension/library_name/sub_key/path/to/asset.js".', "$extension/$library_name/$sub_key")); Chris@0: } Chris@0: if ($sub_key === 'drupalSettings') { Chris@0: // drupalSettings may not be overridden. Chris@0: throw new InvalidLibrariesOverrideSpecificationException(sprintf('drupalSettings may not be overridden in libraries-override. Trying to override %s. Use hook_library_info_alter() instead.', "$extension/$library_name/$sub_key")); Chris@0: } Chris@0: elseif ($sub_key === 'css') { Chris@0: // SMACSS category should be incorporated into the asset name. Chris@0: foreach ($value as $category => $overrides) { Chris@0: $this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path); Chris@0: } Chris@0: } Chris@0: else { Chris@0: $this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return $libraries; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Wraps drupal_get_path(). Chris@0: */ Chris@0: protected function drupalGetPath($type, $name) { Chris@0: return drupal_get_path($type, $name); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Wraps file_valid_uri(). Chris@0: */ Chris@0: protected function fileValidUri($source) { Chris@0: return file_valid_uri($source); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Determines if the supplied string is a valid URI. Chris@0: */ Chris@0: protected function isValidUri($string) { Chris@0: return count(explode('://', $string)) === 2; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Overrides the specified library asset. Chris@0: * Chris@0: * @param array $library Chris@0: * The containing library definition. Chris@0: * @param array $sub_key Chris@0: * An array containing the sub-keys specifying the library asset, e.g. Chris@0: * @code['js']@endcode or @code['css', 'component']@endcode Chris@0: * @param array $overrides Chris@0: * Specifies the overrides, this is an array where the key is the asset to Chris@0: * be overridden while the value is overriding asset. Chris@0: */ Chris@0: protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) { Chris@0: foreach ($overrides as $original => $replacement) { Chris@0: // Get the attributes of the asset to be overridden. If the key does Chris@0: // not exist, then throw an exception. Chris@0: $key_exists = NULL; Chris@0: $parents = array_merge($sub_key, [$original]); Chris@0: // Save the attributes of the library asset to be overridden. Chris@0: $attributes = NestedArray::getValue($library, $parents, $key_exists); Chris@0: if ($key_exists) { Chris@0: // Remove asset to be overridden. Chris@0: NestedArray::unsetValue($library, $parents); Chris@0: // No need to replace if FALSE is specified, since that is a removal. Chris@0: if ($replacement) { Chris@0: // Ensure the replacement path is relative to drupal root. Chris@0: $replacement = $this->resolveThemeAssetPath($theme_path, $replacement); Chris@0: $new_parents = array_merge($sub_key, [$replacement]); Chris@0: // Replace with an override if specified. Chris@0: NestedArray::setValue($library, $new_parents, $attributes); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Ensures that a full path is returned for an overriding theme asset. Chris@0: * Chris@0: * @param string $theme_path Chris@0: * The theme or base theme. Chris@0: * @param string $overriding_asset Chris@0: * The overriding library asset. Chris@0: * Chris@0: * @return string Chris@0: * A fully resolved theme asset path relative to the Drupal directory. Chris@0: */ Chris@0: protected function resolveThemeAssetPath($theme_path, $overriding_asset) { Chris@0: if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) { Chris@0: // The destination is not an absolute path and it's not a URI (e.g. Chris@0: // public://generated_js/example.js or http://example.com/js/my_js.js), so Chris@0: // it's relative to the theme. Chris@0: return '/' . $theme_path . '/' . $overriding_asset; Chris@0: } Chris@0: return $overriding_asset; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Validates CSS library structure. Chris@0: * Chris@0: * @param array $library Chris@0: * The library definition array. Chris@0: * Chris@0: * @return int Chris@0: * Returns based on validity: Chris@0: * - 0 if the library definition is valid Chris@0: * - 1 if the library definition has improper nesting Chris@0: * - 2 if the library definition specifies files as an array Chris@0: */ Chris@0: public static function validateCssLibrary($library) { Chris@0: $categories = []; Chris@0: // Verify options first and return early if invalid. Chris@0: foreach ($library as $category => $files) { Chris@0: if (!is_array($files)) { Chris@0: return 2; Chris@0: } Chris@0: $categories[] = $category; Chris@0: foreach ($files as $source => $options) { Chris@0: if (!is_array($options)) { Chris@0: return 1; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return 0; Chris@0: } Chris@0: Chris@0: }