diff --git a/core/modules/layout/layout.info b/core/modules/layout/layout.info
new file mode 100644
index 0000000..187bfea
--- /dev/null
+++ b/core/modules/layout/layout.info
@@ -0,0 +1,5 @@
+name = Layout
+description = Makes it possible to swap different page layouts.
+package = Core
+version = VERSION
+core = 8.x
diff --git a/core/modules/layout/layout.module b/core/modules/layout/layout.module
new file mode 100644
index 0000000..7d82798
--- /dev/null
+++ b/core/modules/layout/layout.module
@@ -0,0 +1,33 @@
+get('plugin.manager.layout');
+}
+
+/**
+ * Implements hook_theme().
+ *
+ * Expose all layouts as theme items, so themes can override layout markup.
+ */
+function layout_theme($existing, $type, $theme, $path) {
+ $items = array();
+ foreach (layout_manager()->getDefinitions() as $name => $layout) {
+ $items[$layout['theme']] = array(
+ 'variables' => array('content' => NULL),
+ 'path' => $layout['path'],
+ 'template' => $layout['template'],
+ );
+ }
+ return $items;
+}
diff --git a/core/modules/layout/lib/Drupal/layout/LayoutBundle.php b/core/modules/layout/lib/Drupal/layout/LayoutBundle.php
new file mode 100644
index 0000000..59b8513
--- /dev/null
+++ b/core/modules/layout/lib/Drupal/layout/LayoutBundle.php
@@ -0,0 +1,25 @@
+register('plugin.manager.layout', 'Drupal\layout\Plugin\Type\LayoutManager');
+ }
+}
diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php b/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php
new file mode 100644
index 0000000..ce82c65
--- /dev/null
+++ b/core/modules/layout/lib/Drupal/layout/Plugin/Derivative/Layout.php
@@ -0,0 +1,117 @@
+derivatives) && !empty($this->derivatives[$derivative_id])) {
+ return $this->derivatives[$derivative_id];
+ }
+ $this->getDerivativeDefinitions($base_plugin_definition);
+ return $this->derivatives[$derivative_id];
+ }
+
+ /**
+ * Implements DerivativeInterface::getDerivativeDefinitions().
+ */
+ public function getDerivativeDefinitions(array $base_plugin_definition) {
+ $available_layout_providers = array();
+
+ // Add all modules as possible layout providers.
+ foreach (module_list() as $module) {
+ $available_layout_providers[$module] = array(
+ 'type' => 'module',
+ 'provider' => $module,
+ 'dir' => drupal_get_path('module', $module),
+ );
+ }
+
+ // Add all themes as possible layout providers.
+ foreach (list_themes() as $theme_id => $theme) {
+ $available_layout_providers[$theme_id] = array(
+ 'type' => 'theme',
+ 'provider' => $theme->name,
+ 'dir' => drupal_get_path('theme', $theme->name),
+ );
+ }
+
+ foreach ($available_layout_providers as $provider) {
+ // Looks for layouts in the 'layout' directory under the module/theme.
+ // There could be subdirectories under there with one layout defined
+ // in each.
+ $dir = $provider['dir'] . DIRECTORY_SEPARATOR . 'layouts' . DIRECTORY_SEPARATOR . $this->type;
+ if (file_exists($dir)) {
+ $this->iterateDirectories($dir, $provider);
+ }
+ }
+ return $this->derivatives;
+ }
+
+ /**
+ * Finds layout definitions by looking for layout metadata.
+ */
+ protected function iterateDirectories($dir, $provider) {
+ $directories = new DirectoryIterator($dir);
+ foreach ($directories as $fileinfo) {
+ if ($fileinfo->isDir() && !$fileinfo->isDot()) {
+ // Keep discovering in subdirectories to arbitrary depth.
+ $this->iterateDirectories($fileinfo->getPathname(), $provider);
+ }
+ elseif ($fileinfo->isFile() && pathinfo($fileinfo->getFilename(), PATHINFO_EXTENSION) == 'yml') {
+ // Declarative layout definitions are defined with a .yml file in a
+ // layout subdirectory. This provides all information about the layout
+ // such as layout markup template and CSS and JavaScript files to use.
+ $directory = new FileStorage($fileinfo->getPath());
+ $key = $provider['provider'] . '__' . $fileinfo->getBasename('.yml');
+ $this->derivatives[$key] = $directory->read($fileinfo->getBasename('.yml'));
+ $this->derivatives[$key]['theme'] = $key;
+ $this->derivatives[$key]['path'] = $fileinfo->getPath();
+ // If the layout author didn't specify a template name, assume the same
+ // name as the yml file.
+ if (!isset($this->derivatives[$key]['template'])) {
+ $this->derivatives[$key]['template'] = $fileinfo->getBasename('.yml');
+ }
+ }
+ }
+ }
+}
diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php b/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php
new file mode 100644
index 0000000..5b874f6
--- /dev/null
+++ b/core/modules/layout/lib/Drupal/layout/Plugin/LayoutInterface.php
@@ -0,0 +1,30 @@
+ 'Drupal\layout\Plugin\layout\layout\StaticLayout',
+ );
+
+ /**
+ * Overrides Drupal\Component\Plugin\PluginManagerBase::__construct().
+ */
+ public function __construct() {
+ // Create layout plugin derivatives from declaratively defined layouts.
+ $this->discovery = new DerivativeDiscoveryDecorator(new AnnotatedClassDiscovery('layout', 'layout'));
+ $this->factory = new ReflectionFactory($this);
+ }
+}
diff --git a/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php b/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php
new file mode 100644
index 0000000..4819595
--- /dev/null
+++ b/core/modules/layout/lib/Drupal/layout/Plugin/layout/layout/StaticLayout.php
@@ -0,0 +1,121 @@
+getDefinition($plugin_id);
+ foreach ($definition['regions'] as $region => $title) {
+ if (!isset($configuration['regions'][$region])) {
+ $configuration['regions'][$region] = array();
+ }
+ }
+ parent::__construct($configuration, $plugin_id, $discovery);
+ }
+
+ /**
+ * Implements Drupal\layout\Plugin\LayoutInterface::getRegions().
+ */
+ public function getRegions() {
+ $definition = $this->getDefinition();
+ return $definition['regions'];
+ }
+
+ /**
+ * Returns the list of CSS files associated with this layout.
+ */
+ public function getStylesheetFiles() {
+ $definition = $this->getDefinition();
+ return isset($definition['stylesheets']) ? $definition['stylesheets'] : array();
+ }
+
+ /**
+ * Returns the list of administrative CSS files associated with this layout.
+ */
+ public function getAdminStylesheetFiles() {
+ $definition = $this->getDefinition();
+ // Fall back on regular CSS for the admin page if admin CSS not provided.
+ return isset($definition['admin stylesheets']) ? $definition['admin stylesheets'] : $this->getStylesheetFiles();
+ }
+
+ /**
+ * Returns the list of JS files associated with this layout.
+ */
+ public function getScriptFiles() {
+ $definition = $this->getDefinition();
+ return isset($definition['scripts']) ? $definition['scripts'] : array();
+ }
+
+ /**
+ * Returns the list of administrative JS files associated with this layout.
+ */
+ public function getAdminScriptFiles() {
+ $definition = $this->getDefinition();
+ return isset($definition['admin scripts']) ? $definition['admin scripts'] : $this->getScriptFiles();
+ }
+
+ /**
+ * Implements Drupal\layout\Plugin\LayoutInterface::renderLayout().
+ */
+ public function renderLayout($admin = FALSE) {
+ $definition = $this->getDefinition();
+
+ // Assemble a render array with the regions and attached CSS/JS.
+ $build = array(
+ '#theme' => $definition['theme'],
+ '#content' => array(),
+ );
+
+ // Render all regions needed for this layout.
+ foreach ($this->getRegions() as $region => $title) {
+ // @todo This is just stub code to fill in regions with stuff for now.
+ // When blocks are related to layouts and not themes, we can make this
+ // really be filled in with blocks.
+ $build['#content'][$region] = '
' . $title . '
';
+ }
+
+ // Fill in attached CSS and JS files based on metadata.
+ if (!$admin) {
+ $build['#attached'] = array(
+ 'css' => $this->getStylesheetFiles(),
+ 'js' => $this->getScriptFiles(),
+ );
+ }
+ else {
+ $build['#attached'] = array(
+ 'css' => $this->getAdminStylesheetFiles(),
+ 'js' => $this->getAdminScriptFiles(),
+ );
+ }
+
+ // Include the path of the definition in all CSS and JS files.
+ foreach (array('css', 'js') as $type) {
+ foreach ($build['#attached'][$type] as &$filename) {
+ $filename = $definition['path'] . '/' . $filename;
+ }
+ }
+
+ return drupal_render($build);
+ }
+}
diff --git a/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php b/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php
new file mode 100644
index 0000000..1071713
--- /dev/null
+++ b/core/modules/layout/lib/Drupal/layout/Tests/LayoutDerivativesTest.php
@@ -0,0 +1,91 @@
+ 'Layout derivatives',
+ 'description' => 'Tests layout derivatives discovery.',
+ 'group' => 'Layout',
+ );
+ }
+
+ /**
+ * Tests for module/theme layout derivatives.
+ */
+ function testDerivatives() {
+ $manager = drupal_container()->get('plugin.manager.layout');
+
+ $definitions = $manager->getDefinitions();
+ $this->assertTrue(is_array($definitions), 'Definitions found.');
+ $this->assertTrue(count($definitions) == 2, 'Two definitions available.');
+ $this->assertTrue(isset($definitions['static_layout:layout_test__one-col']), 'One column layout found.');
+ $this->assertTrue(isset($definitions['static_layout:layout_test_theme__two-col']), 'Two column layout found.');
+
+ // Get a one column layout instance. This is defined under the layout_test
+ // module.
+ $layout = $manager->createInstance('static_layout:layout_test__one-col', array());
+ // Verify the expected regions are properly available.
+ $regions = $layout->getRegions();
+ $this->assertTrue(is_array($regions), 'Regions array present.');
+ $this->assertTrue(count($regions) == 1, 'One region defined.');
+ $this->assertTrue(isset($regions['middle']), 'Middle region found.');
+
+ // Render the layout and look at whether expected region names and classes
+ // were in the output.
+ $render = $layout->renderLayout();
+ $this->drupalSetContent($render);
+ $this->assertText('Middle column');
+ $this->assertRaw('class="layout-display layout-one-col');
+
+ // Get the two column page layout defined by the layout test theme.
+ $layout = $manager->createInstance('static_layout:layout_test_theme__two-col', array());
+ // Verify the expected regions are properly available.
+ $regions = $layout->getRegions();
+ $this->assertTrue(is_array($regions), 'Regions array present.');
+ $this->assertTrue(count($regions) == 2, 'Two regions defined.');
+ $this->assertTrue(isset($regions['left']), 'Left region found.');
+ $this->assertTrue(isset($regions['right']), 'Right region found.');
+
+ // Render the layout and look at whether expected region names and classes
+ // were in the output.
+ $render = $layout->renderLayout();
+ $this->drupalSetContent($render);
+ $this->assertText('Left side');
+ $this->assertText('Right side');
+ $this->assertRaw('');
+ }
+
+ /**
+ * Test layout functionality as applies to pages.
+ */
+ function testPageLayout() {
+ // The layout-test page uses the layout_test_theme page layout.
+ $this->drupalGet('layout-test');
+ $this->assertText('Left side');
+ $this->assertText('Right side');
+ $this->assertRaw('
');
+
+ // Ensure the CSS was added.
+ $this->assertRaw('@import url("' . url('', array('absolute' => TRUE)) . drupal_get_path('theme', 'layout_test_theme') . '/layouts/static/two-col/two-col.css');
+ }
+}
diff --git a/core/modules/layout/tests/layout_test.info b/core/modules/layout/tests/layout_test.info
new file mode 100644
index 0000000..7b054a9
--- /dev/null
+++ b/core/modules/layout/tests/layout_test.info
@@ -0,0 +1,6 @@
+name = Layout test
+description = Helps with testing layouts.
+package = Testing
+version = VERSION
+core = 8.x
+hidden = TRUE
diff --git a/core/modules/layout/tests/layout_test.module b/core/modules/layout/tests/layout_test.module
new file mode 100644
index 0000000..36c3915
--- /dev/null
+++ b/core/modules/layout/tests/layout_test.module
@@ -0,0 +1,39 @@
+ 'Layout test',
+ 'page callback' => 'layout_test_page',
+ 'access callback' => TRUE,
+ );
+ return $items;
+}
+
+/**
+ * Page callback for layout testing.
+ */
+function layout_test_page() {
+ // Hack to enable and apply the theme to this page and manually invoke its
+ // layout plugin and render it.
+ global $theme;
+ $theme = 'layout_test_theme';
+ theme_enable(array($theme));
+ $layout = layout_manager()->createInstance('static_layout:layout_test_theme__two-col');
+ return $layout->renderLayout();
+}
+
+/**
+ * Implements hook_system_theme_info().
+ */
+function layout_test_system_theme_info() {
+ $themes['layout_test_theme'] = drupal_get_path('module', 'layout_test') . '/themes/layout_test_theme/layout_test_theme.info';
+ return $themes;
+}
diff --git a/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php b/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php
new file mode 100644
index 0000000..e47f83e
--- /dev/null
+++ b/core/modules/layout/tests/layouts/static/one-col/one-col.tpl.php
@@ -0,0 +1,18 @@
+
+
diff --git a/core/modules/layout/tests/layouts/static/one-col/one-col.yml b/core/modules/layout/tests/layouts/static/one-col/one-col.yml
new file mode 100644
index 0000000..27d7d06
--- /dev/null
+++ b/core/modules/layout/tests/layouts/static/one-col/one-col.yml
@@ -0,0 +1,5 @@
+title: Single column
+category: Columns: 1
+template: one-col
+regions:
+ middle: 'Middle column'
diff --git a/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info b/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info
new file mode 100644
index 0000000..84bcff0
--- /dev/null
+++ b/core/modules/layout/tests/themes/layout_test_theme/layout_test_theme.info
@@ -0,0 +1,4 @@
+name = Layout test theme
+description = Theme for testing the layout system
+core = 8.x
+hidden = TRUE
diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css
new file mode 100644
index 0000000..6044e2d
--- /dev/null
+++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.css
@@ -0,0 +1,17 @@
+.layout-two-col .layout-col-left {
+ float: left;
+ width: 50%;
+}
+
+.layout-two-col .layout-col-left .inside {
+ margin-right: .5em;
+}
+
+.layout-two-col .layout-col-right {
+ float: right;
+ width: 50%;
+}
+
+.layout-two-col .layout-col-right .inside {
+ margin-left: .5em;
+}
diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php
new file mode 100644
index 0000000..1d8dbe4
--- /dev/null
+++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.tpl.php
@@ -0,0 +1,24 @@
+
+
diff --git a/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml
new file mode 100644
index 0000000..7ee126f
--- /dev/null
+++ b/core/modules/layout/tests/themes/layout_test_theme/layouts/static/two-col/two-col.yml
@@ -0,0 +1,8 @@
+title: Two column
+category: Columns: 2
+template: two-col
+stylesheets:
+ - two-col.css
+regions:
+ left: 'Left side'
+ right: 'Right side'