From b95d2c83b8a7f4789a2c127a8f65e01a7e152d6d Mon Sep 17 00:00:00 2001
From: Alex Pott <alex.a.pott@googlemail.com>
Date: Mon, 5 Oct 2015 10:15:37 +0100
Subject: [PATCH] Issue #2497667 by almaudoh, Wim Leers, Cottser, Fabianx,
 jaxxed: Add libraries-extend to themes' *.info.yml

---
 ...dLibrariesExtendSpecificationException.php | 15 ++++++
 .../Core/Asset/LibraryDiscoveryCollector.php  | 44 ++++++++++++++++
 core/lib/Drupal/Core/Theme/ActiveTheme.php    | 12 +++++
 .../Drupal/Core/Theme/ThemeInitialization.php | 29 +++++++++++
 .../Asset/LibraryDiscoveryIntegrationTest.php | 52 +++++++++++++++++++
 .../test_basetheme/test_basetheme.info.yml    |  4 ++
 .../test_basetheme.libraries.yml              |  1 +
 .../test_subtheme/test_subtheme.info.yml      |  4 ++
 .../test_subtheme/test_subtheme.libraries.yml |  1 +
 .../test_theme_libraries_extend.info.yml      | 15 ++++++
 .../test_theme_libraries_extend.libraries.yml | 11 ++++
 .../Asset/LibraryDiscoveryCollectorTest.php   |  4 +-
 .../Drupal/Tests/Core/Theme/RegistryTest.php  |  1 +
 13 files changed, 191 insertions(+), 2 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Asset/Exception/InvalidLibrariesExtendSpecificationException.php
 create mode 100644 core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.info.yml
 create mode 100644 core/modules/system/tests/themes/test_theme_libraries_extend/test_theme_libraries_extend.libraries.yml

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 000000000000..f1b9df0143bc
--- /dev/null
+++ b/core/lib/Drupal/Core/Asset/Exception/InvalidLibrariesExtendSpecificationException.php
@@ -0,0 +1,15 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException.
+ */
+
+namespace Drupal\Core\Asset\Exception;
+
+/**
+ * Defines a custom exception for an invalid libraries-extend specification.
+ */
+class InvalidLibrariesExtendSpecificationException extends \RuntimeException {
+
+}
diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
index 953e63b1c887..805d11792dff 100644
--- a/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
+++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryCollector.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Asset;
 
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Asset\Exception\InvalidLibrariesExtendSpecificationException;
 use Drupal\Core\Asset\Exception\InvalidLibrariesOverrideSpecificationException;
 use Drupal\Core\Cache\CacheCollector;
 use Drupal\Core\Cache\CacheBackendInterface;
@@ -124,8 +126,50 @@ protected function getLibraryDefinitions($extension) {
           }
         }
       }
+      else {
+        // If libraries are not overridden, then apply libraries-extend.
+        $libraries[$name] = $this->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 2f886d59d65e..44cd04a74a76 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 74ac35cfe19d..95f27a9d028a 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 28862583630a..2de56805955d 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;
 
@@ -158,6 +159,54 @@ public function testBaseThemeLibrariesOverrideInSubTheme() {
     $this->assertAssetInLibrary('core/modules/system/tests/themes/test_basetheme/css/farbtastic.css', 'core', 'jquery.farbtastic', 'css');
   }
 
+  /**
+   * 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.
    *
@@ -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 3637a0c8c7fe..7b76dedeb3d8 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 f7529ea79585..b3d34064c4f8 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 6883e5a19886..b217374234c8 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 931dffe4aca2..1fff3900433f 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 000000000000..597a5b038cb5
--- /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 000000000000..a2761512aa7d
--- /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 1c3733d67e76..61994383352a 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 0cd69143456f..e0c20cd5fc70 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' => [],
-- 
GitLab