diff --git a/core/lib/Drupal/Core/Asset/Exception/InvalidLibrariesExtendSpecificationException.php b/core/lib/Drupal/Core/Asset/Exception/InvalidLibrariesExtendSpecificationException.php new file mode 100644 index 0000000..f1b9df0 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/InvalidLibrariesExtendSpecificationException.php @@ -0,0 +1,15 @@ +applyLibrariesExtend($extension, $name, $definition); + } } return $libraries; } + /** + * Applies the libraries-extend specified by the active theme. + * + * This extends the library definitions with the those specified by the + * libraries-extend specifications for the active theme. + * + * @param string $extension + * The name of the extension for which library definitions will be extended. + * @param string $library_name + * The name of the library whose definitions is to be extended. + * @param $library_definition + * The library definition to be extended. + * + * @return array + * The library definition extended as specified by libraries-extend. + * + * @throws \Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException + */ + protected function applyLibrariesExtend($extension, $library_name, $library_definition) { + $libraries_extend = $this->themeManager->getActiveTheme()->getLibrariesExtend(); + if (!empty($libraries_extend["$extension/$library_name"])) { + foreach ($libraries_extend["$extension/$library_name"] as $library_extend_name) { + if (!is_string($library_extend_name)) { + // Only string library names are allowed. + throw new InvalidLibrariesExtendSpecificationException('The libraries-extend specification for each library must be a list of strings.'); + } + list($new_extension, $new_library_name) = explode('/', $library_extend_name, 2); + $new_libraries = $this->get($new_extension); + if (isset($new_libraries[$new_library_name])) { + $library_definition = NestedArray::mergeDeep($library_definition, $new_libraries[$new_library_name]); + } + else { + throw new InvalidLibrariesExtendSpecificationException(sprintf('The specified library "%s" does not exist.', $library_extend_name)); + } + } + } + return $library_definition; + } } diff --git a/core/lib/Drupal/Core/Theme/ActiveTheme.php b/core/lib/Drupal/Core/Theme/ActiveTheme.php index 2f886d5..44cd04a 100644 --- a/core/lib/Drupal/Core/Theme/ActiveTheme.php +++ b/core/lib/Drupal/Core/Theme/ActiveTheme.php @@ -104,6 +104,7 @@ public function __construct(array $values) { 'base_themes' => [], 'regions' => [], 'libraries_override' => [], + 'libraries_extend' => [], ]; $this->name = $values['name']; @@ -116,6 +117,7 @@ public function __construct(array $values) { $this->baseThemes = $values['base_themes']; $this->regions = $values['regions']; $this->librariesOverride = $values['libraries_override']; + $this->librariesExtend = $values['libraries_extend']; } /** @@ -219,4 +221,14 @@ public function getLibrariesOverride() { return $this->librariesOverride; } + /** + * Returns the libraries extended by the active theme. + * + * @return array + * The list of libraries-extend definitions. + */ + public function getLibrariesExtend() { + return $this->librariesExtend; + } + } diff --git a/core/lib/Drupal/Core/Theme/ThemeInitialization.php b/core/lib/Drupal/Core/Theme/ThemeInitialization.php index 74ac35c..95f27a9 100644 --- a/core/lib/Drupal/Core/Theme/ThemeInitialization.php +++ b/core/lib/Drupal/Core/Theme/ThemeInitialization.php @@ -185,6 +185,35 @@ public function getActiveTheme(Extension $theme, array $base_themes = []) { } } + // Get libraries extensions declared by base themes. + foreach ($base_themes as $base) { + if (!empty($base->info['libraries-extend'])) { + foreach ($base->info['libraries-extend'] as $library => $extend) { + if (isset($values['libraries_extend'][$library])) { + // Merge if libraries-extend has already been defined for this + // library. + $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend); + } + else { + $values['libraries_extend'][$library] = $extend; + } + } + } + } + // Add libraries extensions declared by this theme. + if (!empty($theme->info['libraries-extend'])) { + foreach ($theme->info['libraries-extend'] as $library => $extend) { + if (isset($values['libraries_extend'][$library])) { + // Merge if libraries-extend has already been defined for this + // library. + $values['libraries_extend'][$library] = array_merge($values['libraries_extend'][$library], $extend); + } + else { + $values['libraries_extend'][$library] = $extend; + } + } + } + // Do basically the same as the above for libraries $values['libraries'] = array(); diff --git a/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php b/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php index 2886258..2de5680 100644 --- a/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php +++ b/core/modules/system/src/Tests/Asset/LibraryDiscoveryIntegrationTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Asset; +use Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException; use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException; use Drupal\simpletest\KernelTestBase; @@ -159,6 +160,54 @@ public function testBaseThemeLibrariesOverrideInSubTheme() { } /** + * Tests libraries-extend. + */ + public function testLibrariesExtend() { + // Activate classy themes and verify the libraries are not extended. + $this->activateTheme('classy'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'classy', 'book-navigation', 'css'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'classy', 'book-navigation', 'js'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'classy', 'book-navigation', 'css'); + + // Activate the theme that extends the book-navigation library in classy. + $this->activateTheme('test_theme_libraries_extend'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_1.css', 'classy', 'book-navigation', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/js/extend_1.js', 'classy', 'book-navigation', 'js'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_theme_libraries_extend/css/extend_2.css', 'classy', 'book-navigation', 'css'); + + // Activate a sub theme and confirm that it inherits the library assets + // extended in the base theme as well as its own. + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/base-libraries-extend.css', 'classy', 'base', 'css'); + $this->assertNoAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'classy', 'base', 'css'); + $this->activateTheme('test_subtheme'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/base-libraries-extend.css', 'classy', 'base', 'css'); + $this->assertAssetInLibrary('core/modules/system/tests/themes/test_subtheme/css/sub-libraries-extend.css', 'classy', 'base', 'css'); + + // Activate test theme that extends with a non-existent library. An + // exception should be thrown. + $this->activateTheme('test_theme_libraries_extend'); + try { + $this->libraryDiscovery->getLibraryByName('core', 'drupal.dialog'); + $this->fail('Throw Exception when specifying non-existent libraries-extend.'); + } + catch (InvalidLibrariesExtendSpecificationException $e) { + $expected_message = 'The specified library "test_theme_libraries_extend/non_existent_library" does not exist.'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when specifying non-existent libraries-extend.'); + } + + // Also, test non-string libraries-extend. An exception should be thrown. + $this->container->get('theme_installer')->install(['test_theme']); + try { + $this->libraryDiscovery->getLibraryByName('test_theme', 'collapse'); + $this->fail('Throw Exception when specifying non-string libraries-extend.'); + } + catch (InvalidLibrariesExtendSpecificationException $e) { + $expected_message = 'The libraries-extend specification for each library must be a list of strings.'; + $this->assertEqual($e->getMessage(), $expected_message, 'Throw Exception when specifying non-string libraries-extend.'); + } + } + + /** * Activates a specified theme. * * Installs the theme if not already installed and makes it the active theme. @@ -178,6 +227,9 @@ protected function activateTheme($theme_name) { $theme_manager->setActiveTheme($theme_initializer->getActiveThemeByName($theme_name)); $this->libraryDiscovery->clearCachedDefinitions(); + + // Assert message. + $this->pass(sprintf('Activated theme "%s"', $theme_name)); } /** 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 3637a0c..7b76ded 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 @@ -15,3 +15,7 @@ libraries-override: css: component: assets/vendor/farbtastic/farbtastic.css: css/farbtastic.css + +libraries-extend: + classy/base: + - test_basetheme/global-styling diff --git a/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml b/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml index f7529ea..b3d3406 100644 --- a/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml +++ b/core/modules/system/tests/themes/test_basetheme/test_basetheme.libraries.yml @@ -5,3 +5,4 @@ global-styling: base-add.css: {} base-add.sub-remove.css: {} samename.css: {} + css/base-libraries-extend.css: {} diff --git a/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml b/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml index 6883e5a..b217374 100644 --- a/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml +++ b/core/modules/system/tests/themes/test_subtheme/test_subtheme.info.yml @@ -9,3 +9,7 @@ libraries: stylesheets-remove: - '@theme_test/css/sub-remove.css' - '@test_basetheme/base-add.sub-remove.css' + +libraries-extend: + classy/base: + - test_subtheme/global-styling diff --git a/core/modules/system/tests/themes/test_subtheme/test_subtheme.libraries.yml b/core/modules/system/tests/themes/test_subtheme/test_subtheme.libraries.yml index 931dffe..1fff390 100644 --- a/core/modules/system/tests/themes/test_subtheme/test_subtheme.libraries.yml +++ b/core/modules/system/tests/themes/test_subtheme/test_subtheme.libraries.yml @@ -4,3 +4,4 @@ global-styling: base: css/sub-add.css: {} css/samename.css: {} + css/sub-libraries-extend.css: {} diff --git a/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.info.yml b/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.info.yml new file mode 100644 index 0000000..597a5b0 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.info.yml @@ -0,0 +1,15 @@ +name: 'Test theme libraries-extend' +type: theme +description: 'Test Theme with libraries-extend' +version: VERSION +base theme: classy +core: 8.x +libraries-extend: + classy/book-navigation: + - test_theme_libraries_extend/extend_one + - test_theme_libraries_extend/extend_two + core/drupal.dialog: + - test_theme_libraries_extend/non_existent_library + test_theme/collapse: + - not_a_string: + expected: 'an exception' diff --git a/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.libraries.yml b/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.libraries.yml new file mode 100644 index 0000000..a276151 --- /dev/null +++ b/core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.libraries.yml @@ -0,0 +1,11 @@ +extend_one: + css: + theme: + css/extend_1.css: { } + js: + js/extend_1.js: { } + +extend_two: + css: + theme: + css/extend_2.css: { } diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryCollectorTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryCollectorTest.php index 1c3733d..6199438 100644 --- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryCollectorTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryCollectorTest.php @@ -94,7 +94,7 @@ public function testResolveCacheMiss() { $this->activeTheme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme') ->disableOriginalConstructor() ->getMock(); - $this->themeManager->expects($this->once()) + $this->themeManager->expects($this->exactly(3)) ->method('getActiveTheme') ->willReturn($this->activeTheme); $this->activeTheme->expects($this->once()) @@ -120,7 +120,7 @@ public function testDestruct() { $this->activeTheme = $this->getMockBuilder('Drupal\Core\Theme\ActiveTheme') ->disableOriginalConstructor() ->getMock(); - $this->themeManager->expects($this->once()) + $this->themeManager->expects($this->exactly(3)) ->method('getActiveTheme') ->willReturn($this->activeTheme); $this->activeTheme->expects($this->once()) diff --git a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php index 0cd6914..e0c20cd 100644 --- a/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php +++ b/core/tests/Drupal/Tests/Core/Theme/RegistryTest.php @@ -94,6 +94,7 @@ public function testGetRegistryForModule() { 'owner' => 'twig', 'stylesheets_remove' => [], 'libraries_override' => [], + 'libraries_extend' => [], 'libraries' => [], 'extension' => '.twig', 'base_themes' => [],