 core/includes/theme.inc                            |   20 +++++++++---
 core/modules/block/block.module                    |   11 +++++++
 core/modules/edit/edit.module                      |   32 +++-----------------
 .../lib/Drupal/system/Tests/Theme/ThemeTest.php    |   22 ++++++++++++++
 .../templates/theme-test-render-element.tpl.php    |    1 +
 .../tests/modules/theme_test/theme_test.module     |   10 ++++++
 6 files changed, 63 insertions(+), 33 deletions(-)

diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 0d0e739..a0e5676 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -13,6 +13,7 @@
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Utility\ThemeRegistry;
 use Drupal\Core\Theme\ThemeSettings;
+use Drupal\Component\Utility\NestedArray;
 
 /**
  * @defgroup content_flags Content markers
@@ -2681,11 +2682,17 @@ function template_preprocess(&$variables, $hook) {
   if (!isset($default_variables)) {
     $default_variables = _template_preprocess_default_variables();
   }
-  $variables += $default_variables + array(
-    'attributes' => array(),
-    'title_attributes' => array(),
-    'content_attributes' => array(),
-  );
+  $variables += $default_variables;
+
+  // When theming a render element, merge its #attributes into
+  // $variables['attributes'].
+  $hooks = theme_get_registry(FALSE);
+  if (isset($hooks[$hook]['render element'])) {
+    $key = $hooks[$hook]['render element'];
+    if (isset($variables[$key]['#attributes'])) {
+      $variables['attributes'] = NestedArray::mergeDeep($variables['attributes'], $variables[$key]['#attributes']);
+    }
+  }
 
   // Initialize html class attribute for the current hook.
   $variables['attributes']['class'][] = drupal_html_class($hook);
@@ -2697,6 +2704,9 @@ function template_preprocess(&$variables, $hook) {
 function _template_preprocess_default_variables() {
   // Variables that don't depend on a database connection.
   $variables = array(
+    'attributes' => array(),
+    'title_attributes' => array(),
+    'content_attributes' => array(),
     'title_prefix' => array(),
     'title_suffix' => array(),
     'db_is_active' => !defined('MAINTENANCE_MODE'),
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index db497e3..e173e4e 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Component\Plugin\Exception\PluginException;
+use Drupal\Component\Utility\NestedArray;
 
 /**
  * Denotes that a block is not enabled in any region and should not be shown.
@@ -541,6 +542,16 @@ function template_preprocess_block(&$variables) {
 
   $variables['attributes']['class'][] = drupal_html_class('block-' . $variables['configuration']['module']);
 
+  // The block template provides a wrapping element for the content. Render the
+  // #attributes of the content on this wrapping element rather than passing
+  // them through to the content's #theme function/template. This allows the
+  // content to not require a function/template at all, or if it does use one,
+  // to not require it to output an extra wrapping element.
+  if (isset($variables['content']['#attributes'])) {
+    $variables['content_attributes'] = NestedArray::mergeDeep($variables['content_attributes'], $variables['content']['#attributes']);
+    unset($variables['content']['#attributes']);
+  }
+
   // Add default class for block content.
   $variables['content_attributes']['class'][] = 'content';
 
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index 0c8eb0f..502fe43 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -14,6 +14,7 @@
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\edit\Form\EditFieldForm;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\entity\Plugin\Core\Entity\EntityDisplay;
 
 /**
  * Implements hook_custom_theme().
@@ -148,35 +149,10 @@ function edit_preprocess_field(&$variables) {
 }
 
 /**
- * Implements hook_preprocess_HOOK() for node.tpl.php.
- *
- * @todo Remove this, handle in generic way: http://drupal.org/node/1972514.
- */
-function edit_preprocess_node(&$variables) {
-  $node = $variables['elements']['#node'];
-  $variables['attributes']['data-edit-entity'] = 'node/' . $node->nid;
-}
-
-/**
- * Implements hook_preprocess_HOOK() for taxonomy-term.tpl.php.
- *
- * @todo Remove this, handle in generic way: http://drupal.org/node/1972514.
+ * Implements hook_entity_view_alter().
  */
-function edit_preprocess_taxonomy_term(&$variables) {
-  $term = $variables['elements']['#term'];
-  $variables['attributes']['data-edit-entity'] = 'taxonomy_term/' . $term->id();
-}
-
-/**
- * Implements hook_preprocess_HOOK() for block.tpl.php.
- *
- * @todo Remove this, handle in generic way: http://drupal.org/node/1972514.
- */
-function edit_preprocess_block(&$variables) {
-  if (isset($variables['elements']['content']['#custom_block'])) {
-    $custom_block = $variables['elements']['content']['#custom_block'];
-    $variables['attributes']['data-edit-entity'] = 'custom_block/' . $custom_block->id();
-  }
+function edit_entity_view_alter(&$build, EntityInterface $entity, EntityDisplay $display) {
+  $build['#attributes']['data-edit-entity'] = $entity->entityType() . '/' . $entity->id();
 }
 
 /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
index 20ccc2b..cdf5d7b 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Theme/ThemeTest.php
@@ -36,6 +36,28 @@ function setUp() {
   }
 
   /**
+   * Test attribute merging.
+   *
+   * Render arrays that use a render element and templates (and hence call
+   * template_preprocess()) must ensure the attributes at different occassions
+   * are all merged correctly:
+   *   - $variables['attributes'] as passed in to theme()
+   *   - the render element's #attributes
+   *   - any attributes set in the template's preprocessing function
+   */
+  function testAttributeMerging() {
+    $output = theme('theme_test_render_element', array(
+      'elements' => array(
+        '#attributes' => array('data-foo' => 'bar'),
+      ),
+      'attributes' => array(
+        'id' => 'bazinga',
+      ),
+    ));
+    $this->assertIdentical($output, '<div id="bazinga" data-foo="bar" class="theme-test-render-element" data-variables-are-preprocessed></div>' . "\n");
+  }
+
+  /**
    * Test function theme_get_suggestions() for SA-CORE-2009-003.
    */
   function testThemeSuggestions() {
diff --git a/core/modules/system/tests/modules/theme_test/templates/theme-test-render-element.tpl.php b/core/modules/system/tests/modules/theme_test/templates/theme-test-render-element.tpl.php
new file mode 100644
index 0000000..f301d31
--- /dev/null
+++ b/core/modules/system/tests/modules/theme_test/templates/theme-test-render-element.tpl.php
@@ -0,0 +1 @@
+<div<?php print $attributes; ?>></div>
diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module
index 137fdfc..c51eb0c 100644
--- a/core/modules/system/tests/modules/theme_test/theme_test.module
+++ b/core/modules/system/tests/modules/theme_test/theme_test.module
@@ -17,6 +17,10 @@ function theme_test_theme($existing, $type, $theme, $path) {
   $items['theme_test_foo'] = array(
     'variables' => array('foo' => NULL),
   );
+  $items['theme_test_render_element'] = array(
+    'render element' => 'elements',
+    'template' => 'theme-test-render-element',
+  );
   return $items;
 }
 
@@ -172,3 +176,9 @@ function theme_theme_test_foo($variables) {
   return $variables['foo'];
 }
 
+/**
+ * Process variables for theme-test-render-element.tpl.php.
+ */
+function template_preprocess_theme_test_render_element(&$variables) {
+  $variables['attributes']['data-variables-are-preprocessed'] = TRUE;
+}
