diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php index e7da564..7791869 100644 --- a/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php @@ -11,6 +11,7 @@ use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException; use Drupal\Core\Asset\Exception\InvalidLibraryFileException; use Drupal\Core\Asset\Exception\LibraryDefinitionMissingLicenseException; +use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Drupal\Component\Serialization\Exception\InvalidDataTypeException; @@ -335,55 +336,48 @@ protected function parseLibraryInfo($extension, $path) { */ protected function applyLibrariesOverride($libraries, $extension) { $active_theme = $this->themeManager->getActiveTheme(); - $libraries_overrides = $active_theme->getLibrariesOverride(); - foreach ($libraries as $library_name => $library) { - // Process libraries overrides. - $namespaced_library_name = "$extension/$library_name"; - foreach ($libraries_overrides as $definition => $override) { - // Active theme defines an override for this library. - if ($this->isEntireLibrary($definition, $namespaced_library_name)) { - // The active theme defines an override for the whole library. Use the - // override key to specify that this library will be overridden when - // it is called. - // @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName() - if ($override) { - $libraries[$library_name]['override'] = $override; - } - else { - $libraries[$library_name]['override'] = FALSE; - } - } - elseif ($this->isPartOfLibrary($definition, $namespaced_library_name)) { - // Active theme defines an override for an asset within this library. - // Throw an exception if the asset is not properly specified. - if (substr_count($definition, '/') < 3) { - 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".', $definition)); - } - list(, , $sub_key, $value) = explode('/', $definition, 4); - if ($sub_key === 'drupalSettings') { - // drupalSettings may not be overridden. - throw new InvalidLibrariesOverrideSpecificationException(sprintf('drupalSettings may not be overridden in libraries-override. Trying to override %s. Use hook_library_info_alter() instead.', $definition)); - } - elseif ($sub_key === 'css') { - // SMACSS category should be incorporated into the asset name. - list($category, $value) = explode('/', $value, 2); - $parents = [$sub_key, $category, $value]; - $new_parents = [$sub_key, $category, $override]; - } - else { - $parents = [$sub_key, $value]; - $new_parents = [$sub_key, $override]; + // ActiveTheme::getLibrariesOverride() returns libraries-overrides for the + // current theme as well as all its base themes. + $all_libraries_overrides = $active_theme->getLibrariesOverride(); + foreach ($all_libraries_overrides as $theme_path => $libraries_overrides) { + foreach ($libraries as $library_name => $library) { + // Process libraries overrides. + if (isset($libraries_overrides["$extension/$library_name"])) { + // Active theme defines an override for this library. + $override_definition = $libraries_overrides["$extension/$library_name"]; + if (is_string($override_definition) || $override_definition === FALSE) { + // A string or boolean definition implies an override (or removal) + // for the whole library. Use the override key to specify that this + // library will be overridden when it is called. + // @see \Drupal\Core\Asset\LibraryDiscovery::getLibraryByName() + if ($override_definition) { + $libraries[$library_name]['override'] = $override_definition; + } + else { + $libraries[$library_name]['override'] = FALSE; + } } - // Get the attributes of the asset to be overridden. If the key does - // not exist, then throw an exception. - $key_exists = NULL; - $attributes = NestedArray::getValue($libraries[$library_name], $parents, $key_exists); - if ($key_exists) { - // Remove asset to be overridden, but keep its attributes. - NestedArray::unsetValue($libraries[$library_name], $parents); - if ($override) { - // Replace with an override if specified. - NestedArray::setValue($libraries[$library_name], $new_parents, $attributes); + elseif (is_array($override_definition)) { + // An array definition implies an override for an asset within this + // library. + foreach ($override_definition as $sub_key => $value) { + // Throw an exception if the asset is not properly specified. + if (!is_array($value)) { + 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")); + } + if ($sub_key === 'drupalSettings') { + // drupalSettings may not be overridden. + 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")); + } + elseif ($sub_key === 'css') { + // SMACSS category should be incorporated into the asset name. + foreach ($value as $category => $overrides) { + $this->setOverrideValue($libraries[$library_name], [$sub_key, $category], $overrides, $theme_path); + } + } + else { + $this->setOverrideValue($libraries[$library_name], [$sub_key], $value, $theme_path); + } } } } @@ -415,31 +409,57 @@ protected function isValidUri($string) { } /** - * Checks if the specified definition asset is a full library. + * Overrides the specified library asset. * - * @param string $definition - * The asset definition being checked. - * @param string $library_name - * The name of the library. - * @return bool - * Returns true if $definition is a full library specified by $library_name. + * @param array $library + * The containing library definition. + * @param array $sub_key + * An array containing the sub-keys specifying the library asset, e.g. + * @code['js']@endcode or @code['css', 'component']@endcode + * @param array $overrides + * Specifies the overrides, this is an array where the key is the asset to + * be overridden while the value is overriding asset. */ - protected function isEntireLibrary($definition, $library_name) { - return $definition === $library_name; + protected function setOverrideValue(array &$library, array $sub_key, array $overrides, $theme_path) { + foreach ($overrides as $original => $replacement) { + // Get the attributes of the asset to be overridden. If the key does + // not exist, then throw an exception. + $key_exists = NULL; + $parents = array_merge($sub_key, [$original]); + // Ensure the replacement path is relative to drupal root. + $replacement = $this->resolveThemeAssetPath($theme_path, $replacement); + $new_parents = array_merge($sub_key, [$replacement]); + $attributes = NestedArray::getValue($library, $parents, $key_exists); + if ($key_exists) { + // Remove asset to be overridden, but keep its attributes. + NestedArray::unsetValue($library, $parents); + if ($replacement) { + // Replace with an override if specified. + NestedArray::setValue($library, $new_parents, $attributes); + } + } + } } /** - * Determines if the specified asset definition is a part of a library. + * Ensures that a full path is returned for an overriding theme asset. * - * @param string $definition - * The asset definition being checked. - * @param string $library_name - * The name of the library. - * @return bool - * Returns true if the definition is part of the library, false otherwise. + * @param string $theme_path + * The theme or base theme. + * @param string $overriding_asset + * The overriding library asset. + * + * @return string + * A fully resolved theme asset path relative to the Drupal directory. */ - protected function isPartOfLibrary($definition, $library_name) { - return strpos($definition, "$library_name/") === 0; + protected function resolveThemeAssetPath($theme_path, $overriding_asset) { + if ($overriding_asset[0] !== '/' && !$this->isValidUri($overriding_asset)) { + // The destination is not an absolute path and it's not a URI (e.g. + // public://generated_js/example.js or http://example.com/js/my_js.js), so + // it's relative to the theme. + return '/' . $theme_path . '/' . $overriding_asset; + } + return $overriding_asset; } } diff --git a/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 8a240d5..74ac35c 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -173,7 +173,7 @@ public function getActiveTheme(Extension $theme, array $base_themes = []) { foreach ($base_themes as $base) { if (!empty($base->info['libraries-override'])) { foreach ($base->info['libraries-override'] as $library => $override) { - $values['libraries_override'][$library] = $override ? $this->resolveThemeAssetPath($base, $library, $override) : $override; + $values['libraries_override'][$base->getPath()][$library] = $override; } } } @@ -181,7 +181,7 @@ public function getActiveTheme(Extension $theme, array $base_themes = []) { // Add libraries overrides declared by this theme. if (!empty($theme->info['libraries-override'])) { foreach ($theme->info['libraries-override'] as $library => $override) { - $values['libraries_override'][$library] = $override ? $this->resolveThemeAssetPath($theme, $library, $override) : $override; + $values['libraries_override'][$theme->getPath()][$library] = $override; } } @@ -298,42 +298,4 @@ protected function prepareStylesheetsRemove(Extension $theme, $base_themes) { return $stylesheets_remove; } - /** - * Ensures that a full path is returned for an overriding theme asset. - * - * If the specified $source is a library, then the $destination is returned - * without change. - * - * @param \Drupal\Core\Extension\Extension $theme - * The theme or base theme. - * @param string $source - * The source asset path, i.e. the library to be replaced. - * @param string $destination - * The library or library asset replacing the source. - * - * @return string - * A fully resolved theme asset path relative to the Drupal directory. - */ - protected function resolveThemeAssetPath(Extension $theme, $source, $destination) { - // Full library definitions don't need to be resolved. - if (count(explode('/', $source)) === 2) { - return $destination; - } - // The destination is not an absolute path and it's not a URI (e.g. - // public://generated_js/example.js or http://example.com/js/my_js.js), so - // it's relative to the theme. - if ($destination[0] !== '/' && !$this->isValidUri($destination)) { - $theme_path = $theme->getPath(); - return '/' . $theme_path . '/' . $destination; - } - return $destination; - } - - /** - * Determines if the supplied string is a valid URI. - */ - protected function isValidUri($string) { - return count(explode('://', $string)) === 2; - } - } diff --git a/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php b/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php index f2560fc..07a2ecb 100644 --- a/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php +++ b/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php @@ -30,7 +30,7 @@ class LibraryDiscoveryIntegrationTest extends KernelTestBase { protected function setUp() { parent::setUp(); - $this->container->get('theme_handler')->install(['test_theme', 'classy']); + $this->container->get('theme_installer')->install(['test_theme', 'classy']); $this->libraryDiscovery = $this->container->get('library.discovery'); } @@ -46,14 +46,33 @@ public function testElementInfoByTheme() { * Tests that libraries-override are applied to library definitions. */ public function testLibrariesOverride() { + // Assert some classy libraries exist that we are going to override. + $this->activateTheme('classy'); + $this->assertAssetInLibrary('core/themes/classy/css/components/button.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/collapse-processed.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/container-inline.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/themes/classy/css/components/details.css', 'classy', 'base', 'css'); + + // Confirmatory asserts on other libraries to be removed or overridden. + $this->assertTrue($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Confirmatory test on "core/drupal.progress"'); + $this->assertAssetInLibrary('core/misc/dialog.theme.css', 'core', 'drupal.dialog', 'css'); + // Activate test theme that defines libraries overrides. $this->activateTheme('test_theme'); // Assert that entire library was correctly overridden. $this->assertEqual($this->libraryDiscovery->getLibraryByName('core', 'drupal.collapse'), $this->libraryDiscovery->getLibraryByName('test_theme', 'collapse'), 'Entire library correctly overridden.'); - // Assert that library asset was correctly overridden. - $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/test_theme_layout.css', 'classy', 'base', 'css'); + // Assert that classy library assets were correctly overridden. + $this->assertNoAssetInLibrary('core/themes/classy/css/components/button.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/collapse-processed.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/container-inline.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/themes/classy/css/components/details.css', 'classy', 'base', 'css'); + + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/my-button.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme/css/my-collapse-processed.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('themes/my_theme/css/my-container-inline.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('themes/my_theme/css/my-details.css', 'classy', 'base', 'css'); // Assert that entire library was correctly removed. $this->assertFalse($this->libraryDiscovery->getLibraryByName('core', 'drupal.progress'), 'Entire library correctly removed.'); @@ -84,7 +103,7 @@ public function testLibrariesOverrideDrupalSettings() { $this->fail('Throw Exception when trying to override drupalSettings'); } catch (InvalidLibrariesOverrideSpecificationException $e) { - $expected_message = 'drupalSettings may not be overridden in libraries-override. Trying to override core/drupal.ajax/drupalSettings/ajaxPageState. Use hook_library_info_alter() instead.'; + $expected_message = 'drupalSettings may not be overridden in libraries-override. Trying to override core/drupal.ajax/drupalSettings. Use hook_library_info_alter() instead.'; $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when trying to override drupalSettings'); } } @@ -150,7 +169,7 @@ public function testBaseThemeLibrariesOverrideInSubTheme() { * The name of the theme to be activated. */ protected function activateTheme($theme_name) { - $this->container->get('theme_handler')->install([$theme_name]); + $this->container->get('theme_installer')->install([$theme_name]); /** @var \Drupal\Core\Theme\ThemeInitializationInterface $theme_initializer */ $theme_initializer = $this->container->get('theme.initialization'); @@ -159,6 +178,8 @@ protected function activateTheme($theme_name) { $theme_manager = $this->container->get('theme.manager'); $theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName($theme_name)); + + $this->libraryDiscovery->clearCachedDefinitions(); } /** @@ -180,7 +201,7 @@ protected function activateTheme($theme_name) { */ protected function assertAssetInLibrary($asset, $extension, $library_name, $sub_key, $message = NULL) { if (!isset($message)) { - $message = sprintf('Asset %s found in library %s', $asset, $library_name); + $message = sprintf('Asset %s found in library "%s/%s"', $asset, $extension, $library_name); } $library = $this->libraryDiscovery->getLibraryByName($extension, $library_name); foreach ($library[$sub_key] as $definition) { @@ -210,7 +231,7 @@ protected function assertAssetInLibrary($asset, $extension, $library_name, $sub_ */ protected function assertNoAssetInLibrary($asset, $extension, $library_name, $sub_key, $message = NULL) { if (!isset($message)) { - $message = sprintf('Asset %s not found in library %s', $asset, $library_name); + $message = sprintf('Asset %s not found in library "%s/%s"', $asset, $extension, $library_name); } $library = $this->libraryDiscovery->getLibraryByName($extension, $library_name); foreach ($library[$sub_key] as $definition) { diff --git a/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml b/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml index 587070c..3637a0c 100644 --- a/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml +++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.info.yml @@ -8,5 +8,10 @@ libraries: stylesheets-remove: - '@theme_test/css/base-remove.css' libraries-override: - core/drupal.dialog/js/misc/dialog/dialog.js: false - core/jquery.farbtastic/css/component/assets/vendor/farbtastic/farbtastic.css: css/farbtastic.css + core/drupal.dialog: + js: + misc/dialog/dialog.js: false + core/jquery.farbtastic: + css: + component: + assets/vendor/farbtastic/farbtastic.css: css/farbtastic.css diff --git a/core/modules/system/tests/themes/test_theme/test_theme.info.yml b/core/modules/system/tests/themes/test_theme/test_theme.info.yml index 9e12ea1..142470b 100644 --- a/core/modules/system/tests/themes/test_theme/test_theme.info.yml +++ b/core/modules/system/tests/themes/test_theme/test_theme.info.yml @@ -23,20 +23,44 @@ libraries-override: core/drupal.collapse: test_theme/collapse # Remove an entire library. core/drupal.progress: false - # Replace one particular library asset with another. - classy/base/css/theme/css/layout.css: css/test_theme_layout.css - # Remove one particular asset. - core/drupal.dialog/css/theme/misc/dialog.theme.css: false + # Replace particular library assets. + classy/base: + css: + component: + css/components/button.css: css/my-button.css + css/components/collapse-processed.css: css/my-collapse-processed.css + css/components/container-inline.css: /themes/my_theme/css/my-container-inline.css + css/components/details.css: /themes/my_theme/css/my-details.css + # Remove particular library assets. + core/drupal.dialog: + css: + theme: + misc/dialog.theme.css: false # It works for JS as well. - core/jquery/js/assets/vendor/jquery/jquery.min.js: js/collapse.js + core/jquery: + js: + assets/vendor/jquery/jquery.min.js: js/collapse.js # Use Drupal relative paths. - core/drupal.dropbutton/css/component/misc/dropbutton/dropbutton.css: /themes/my_theme/css/dropbutton.css + core/drupal.dropbutton: + css: + component: + misc/dropbutton/dropbutton.css: /themes/my_theme/css/dropbutton.css # Use stream wrappers. - core/drupal.vertical-tabs/css/component/misc/vertical-tabs.css: public://my_css/vertical-tabs.css + core/drupal.vertical-tabs: + css: + component: + misc/vertical-tabs.css: public://my_css/vertical-tabs.css # Use protocol-relative URI. - core/jquery.ui/css/component/assets/vendor/jquery.ui/themes/base/core.css: //my-server/my_theme/css/jquery_ui.css + core/jquery.ui: + css: + component: + assets/vendor/jquery.ui/themes/base/core.css: //my-server/my_theme/css/jquery_ui.css # Use regular URI. - core/jquery.farbtastic/css/component/assets/vendor/farbtastic/farbtastic.css: http://example.com/my_theme/css/farbtastic.css + core/jquery.farbtastic: + css: + component: + assets/vendor/farbtastic/farbtastic.css: http://example.com/my_theme/css/farbtastic.css + regions: content: Content left: Left diff --git a/core/modules/system/tests/themes/test_theme_libraries_override_with_drupal_settings/test_theme_libraries_override_with_drupal_settings.info.yml b/core/modules/system/tests/themes/test_theme_libraries_override_with_drupal_settings/test_theme_libraries_override_with_drupal_settings.info.yml index e6e6f1f..f55f6d0 100644 --- a/core/modules/system/tests/themes/test_theme_libraries_override_with_drupal_settings/test_theme_libraries_override_with_drupal_settings.info.yml +++ b/core/modules/system/tests/themes/test_theme_libraries_override_with_drupal_settings/test_theme_libraries_override_with_drupal_settings.info.yml @@ -7,4 +7,6 @@ core: 8.x libraries-override: # drupalSettings libraries override. Should throw a # \Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException. - core/drupal.ajax/drupalSettings/ajaxPageState: { } + core/drupal.ajax: + drupalSettings: + ajaxPageState: { } diff --git a/core/modules/system/tests/themes/test_theme_libraries_override_with_invalid_asset/test_theme_libraries_override_with_invalid_asset.info.yml b/core/modules/system/tests/themes/test_theme_libraries_override_with_invalid_asset/test_theme_libraries_override_with_invalid_asset.info.yml index befe8d8..f31ade0 100644 --- a/core/modules/system/tests/themes/test_theme_libraries_override_with_invalid_asset/test_theme_libraries_override_with_invalid_asset.info.yml +++ b/core/modules/system/tests/themes/test_theme_libraries_override_with_invalid_asset/test_theme_libraries_override_with_invalid_asset.info.yml @@ -7,4 +7,5 @@ core: 8.x libraries-override: # A malformed library asset name. Should throw a # \Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException. - core/drupal.dialog/css: false + core/drupal.dialog: + css: false diff --git a/core/themes/bartik/bartik.info.yml b/core/themes/bartik/bartik.info.yml index aa0c18d..a74f90c 100644 --- a/core/themes/bartik/bartik.info.yml +++ b/core/themes/bartik/bartik.info.yml @@ -7,8 +7,6 @@ version: VERSION core: 8.x libraries: - bartik/global-styling -libraries-override: - classy/base/css/theme/css/layout.css: false ckeditor_stylesheets: - css/base/elements.css - css/components/captions.css diff --git a/core/themes/seven/seven.info.yml b/core/themes/seven/seven.info.yml index 1334e99..b983303 100644 --- a/core/themes/seven/seven.info.yml +++ b/core/themes/seven/seven.info.yml @@ -9,7 +9,10 @@ core: 8.x libraries: - seven/global-styling libraries-override: - core/jquery.ui.dialog/css/component/assets/vendor/jquery.ui/themes/base/dialog.css: false + core/jquery.ui.dialog: + css: + component: + assets/vendor/jquery.ui/themes/base/dialog.css: false quickedit_stylesheets: - css/components/quickedit.css regions: