diff --git a/core/includes/common.inc b/core/includes/common.inc
index 5572e54..188268c 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3468,7 +3468,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
  *
  * @return string
  *   The rendered HTML of all children of the element.
-
  * @see drupal_render()
  */
 function drupal_render_children(&$element, $children_keys = NULL) {
@@ -3662,7 +3661,7 @@ function drupal_render_cache_set(&$markup, array $elements) {
  * placeholders, but should also be called by #post_render_cache callbacks that
  * want to replace the placeholder with the final markup.
  *
- * @param callable $callback
+ * @param string $callback
  *   The #post_render_cache callback that will replace the placeholder with its
  *   eventual markup.
  * @param array $context
@@ -3681,7 +3680,7 @@ function drupal_render_cache_generate_placeholder($callback, array &$context) {
     'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55),
   );
 
-  return '<drupal:render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '" />';
+  return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>';
 }
 
 /**
diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
index 7272357..02049b6 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php
@@ -9,8 +9,12 @@
 
 use Drupal\ckeditor\CKEditorPluginBase;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\editor\Entity\Editor;
 use Drupal\filter\Plugin\FilterInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Defines the "internal" plugin (i.e. core plugins part of our CKEditor build).
@@ -20,7 +24,55 @@
  *   label = @Translation("CKEditor core")
  * )
  */
-class Internal extends CKEditorPluginBase {
+class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The cache backend.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Constructs a \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache_backend) {
+    $this->cache = $cache_backend;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * Creates an instance of the plugin.
+   *
+   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
+   *   The container to pull out services used in the plugin.
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin ID for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   *
+   * @return static
+   *   Returns an instance of this plugin.
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('cache.default')
+    );
+  }
 
   /**
    * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
@@ -251,22 +303,42 @@ public function getButtons() {
    *   An array containing the "format_tags" configuration.
    */
   protected function generateFormatTagsSetting(Editor $editor) {
-    // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
-    $format_tags = array('p');
+    // When no text format is associated yet, assume no tag is allowed.
+    // @see \Drupal\Editor\EditorInterface::hasAssociatedFilterFormat()
+    if (!$editor->hasAssociatedFilterFormat()) {
+      return array();
+    }
+
+    $format = $editor->getFilterFormat();
+    $cid = 'ckeditor_internal_format_tags:' . $format->id();
+
+    if ($cached = $this->cache->get($cid)) {
+      $format_tags = $cached->data;
+    }
+    else {
+      // The <p> tag is always allowed — HTML without <p> tags is nonsensical.
+      $format_tags = array('p');
 
-    // Given the list of possible format tags, automatically determine whether
-    // the current text format allows this tag, and thus whether it should show
-    // up in the "Format" dropdown.
-    $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre');
-    foreach ($possible_format_tags as $tag) {
-      $input = '<' . $tag . '>TEST</' . $tag . '>';
-      $output = trim(check_markup($input, $editor->id(), '', TRUE));
-      if ($input == $output) {
-        $format_tags[] = $tag;
+      // Given the list of possible format tags, automatically determine whether
+      // the current text format allows this tag, and thus whether it should show
+      // up in the "Format" dropdown.
+      $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre');
+      foreach ($possible_format_tags as $tag) {
+        $input = '<' . $tag . '>TEST</' . $tag . '>';
+        $output = trim(check_markup($input, $editor->id()));
+        if ($input == $output) {
+          $format_tags[] = $tag;
+        }
       }
+      $format_tags = implode(';', $format_tags);
+
+      // Cache the "format_tags" configuration. This cache item is infinitely
+      // valid; it only changes whenever the text format is changed, hence it's
+      // tagged with the text format's cache tag.
+      $this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTag());
     }
 
-    return implode(';', $format_tags);
+    return $format_tags;
   }
 
   /**
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index b09bfa8..4844daf 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -1369,8 +1369,9 @@ function template_preprocess_comment(&$variables) {
     $variables['user_picture'] = array();
   }
 
-  if (\Drupal::config('user.settings')->get('signatures') && $account->getSignature()) {
-    $variables['signature'] = check_markup($account->getSignature(), $account->getSignatureFormat(), '', TRUE) ;
+  if (isset($variables['elements']['signature'])) {
+    $variables['signature'] = $variables['elements']['signature']['#markup'];
+    unset($variables['elements']['signature']);
   }
   else {
     $variables['signature'] = '';
diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php
index e993e7a..7769b5e 100644
--- a/core/modules/comment/src/CommentViewBuilder.php
+++ b/core/modules/comment/src/CommentViewBuilder.php
@@ -131,6 +131,21 @@ public function buildComponents(array &$build, array $entities, array $displays,
         '#markup' => $placeholder,
       );
 
+      $account = comment_prepare_author($entity);
+      if (\Drupal::config('user.settings')->get('signatures') && $account->getSignature()) {
+        $build[$id]['signature'] = array(
+          '#type' => 'processed_text',
+          '#text' => $account->getSignature(),
+          '#format' => $account->getSignatureFormat(),
+          '#langcode' => $entity->language()->getId(),
+        );
+        // The signature will only be rendered in the theme layer, which means
+        // its associated cache tags will not bubble up. Work around this for
+        // now by already rendering the signature here.
+        // @todo remove this work-around, see https://drupal.org/node/2273277
+        drupal_render($build[$id]['signature'], TRUE);
+      }
+
       if (!isset($build[$id]['#attached'])) {
         $build[$id]['#attached'] = array();
       }
diff --git a/core/modules/editor/src/EditorController.php b/core/modules/editor/src/EditorController.php
index 59c1ef9..e5a768e 100644
--- a/core/modules/editor/src/EditorController.php
+++ b/core/modules/editor/src/EditorController.php
@@ -47,7 +47,7 @@ public function getUntransformedText(EntityInterface $entity, $field_name, $lang
 
     // Direct text editing is only supported for single-valued fields.
     $field = $entity->getTranslation($langcode)->$field_name;
-    $editable_text = check_markup($field->value, $field->format, $langcode, FALSE, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
+    $editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
     $response->addCommand(new GetUntransformedTextCommand($editable_text));
 
     return $response;
diff --git a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php
new file mode 100644
index 0000000..b22bda6
--- /dev/null
+++ b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php
@@ -0,0 +1,94 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Plugin\Filter\EditorFileReference.
+ */
+
+namespace Drupal\editor\Plugin\Filter;
+
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Entity\EntityManagerInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a filter to track images uploaded via a Text Editor.
+ *
+ * Passes the text unchanged, but associates the cache tags of referenced files.
+ *
+ * @Filter(
+ *   id = "editor_file_reference",
+ *   title = @Translation("Track images uploaded via a Text Editor"),
+ *   description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class EditorFileReference extends FilterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * An entity manager object.
+   *
+   * @var \Drupal\Core\Entity\EntityManagerInterface
+   */
+  protected $entityManager;
+
+  /**
+   * Constructs a \Drupal\editor\Plugin\Filter\EditorFileReference object.
+   *
+   * @param array $configuration
+   *   A configuration array containing information about the plugin instance.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
+   *   An entity manager object.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
+    $this->entityManager = $entity_manager;
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('entity.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+
+    if (stristr($text, 'data-editor-file-uuid') !== FALSE) {
+      $dom = Html::load($text);
+      $xpath = new \DOMXPath($dom);
+      $processed_uuids = array();
+      foreach ($xpath->query('//*[@data-editor-file-uuid]') as $node) {
+        $uuid = $node->getAttribute('data-editor-file-uuid');
+        // Only process the first occurrence of each file UUID.
+        if (!isset($processed_uuids[$uuid])) {
+          $processed_uuids[$uuid] = TRUE;
+
+          $file = $this->entityManager->loadEntityByUuid('file', $uuid);
+          if ($file) {
+            $result->addCacheTags($file->getCacheTag());
+          }
+        }
+      }
+    }
+
+    return $result;
+  }
+
+}
diff --git a/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php b/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php
new file mode 100644
index 0000000..3474613
--- /dev/null
+++ b/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Tests\EditorFileReferenceFilterTest.
+ */
+
+namespace Drupal\editor\Tests;
+
+use Drupal\simpletest\KernelTestBase;
+use Drupal\filter\FilterBag;
+
+/**
+ * Tests for the EditorFileReference filter.
+ */
+class EditorFileReferenceFilterTest extends KernelTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'filter', 'editor', 'field', 'file', 'user');
+
+  /**
+   * @var \Drupal\filter\Plugin\FilterInterface[]
+   */
+  protected $filters;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Editor File Reference filter',
+      'description' => "Tests Editor module's file reference filter.",
+      'group' => 'Text Editor',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installConfig(array('system'));
+    $this->installEntitySchema('file');
+    $this->installSchema('file', array('file_usage'));
+
+    $manager = $this->container->get('plugin.manager.filter');
+    $bag = new FilterBag($manager, array());
+    $this->filters = $bag->getAll();
+  }
+
+  /**
+   * Tests the editor file reference filter.
+   */
+  function testEditorFileReferenceFilter() {
+    $filter = $this->filters['editor_file_reference'];
+
+    $test = function($input) use ($filter) {
+      return $filter->process($input, 'und');
+    };
+
+    file_put_contents('public://llama.jpg', $this->randomName());
+    $image = entity_create('file', array('uri' => 'public://llama.jpg'));
+    $image->save();
+    $id = $image->id();
+    $uuid = $image->uuid();
+
+    file_put_contents('public://alpaca.jpg', $this->randomName());
+    $image_2 = entity_create('file', array('uri' => 'public://alpaca.jpg'));
+    $image_2->save();
+    $id_2 = $image_2->id();
+    $uuid_2 = $image_2->uuid();
+
+    $this->pass('No data-editor-file-uuid attribute.');
+    $input = '<img src="llama.jpg" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+
+    $this->pass('One data-editor-file-uuid attribute.');
+    $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array('file' => array($id)), $output->getCacheTags());
+
+    $this->pass('One data-editor-file-uuid attribute with odd capitalization.');
+    $input = '<img src="llama.jpg" DATA-editor-file-UUID =   "' . $uuid . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array('file' => array($id)), $output->getCacheTags());
+
+    $this->pass('One data-editor-file-uuid attribute on a non-image tag.');
+    $input = '<video src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array('file' => array($id)), $output->getCacheTags());
+
+    $this->pass('One data-editor-file-uuid attribute with an invalid value.');
+    $input = '<img src="llama.jpg" data-editor-file-uuid="invalid-' . $uuid . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array(), $output->getCacheTags());
+
+    $this->pass('Two different data-editor-file-uuid attributes.');
+    $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />';
+    $input .= '<img src="alpaca.jpg" data-editor-file-uuid="' . $uuid_2 . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array('file' => array($id, $id_2)), $output->getCacheTags());
+
+    $this->pass('Two identical  data-editor-file-uuid attributes.');
+    $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />';
+    $input .= '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />';
+    $output = $test($input);
+    $this->assertIdentical($input, $output->getProcessedText());
+    $this->assertEqual(array('file' => array($id)), $output->getCacheTags());
+  }
+
+}
diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
index 410a256..4065ea2 100644
--- a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
+++ b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php
@@ -106,6 +106,7 @@ public function viewElements(FieldItemListInterface $items) {
         // its cache tags to be bubbled up and included with those of the
         // main entity when cache tags are collected for a renderable array
         // in drupal_render().
+        // @todo remove this work-around, see https://drupal.org/node/2273277
         $referenced_entity_build = entity_view($item->entity, $view_mode, $item->getLangcode());
         drupal_render($referenced_entity_build, TRUE);
         $elements[$delta] = $referenced_entity_build;
diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
index 37fd4b1..61d0ee5 100644
--- a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
+++ b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
@@ -60,8 +60,6 @@ public static function getInfo() {
   public function setUp() {
     parent::setUp();
 
-    $this->installConfig(array('filter'));
-
     entity_reference_create_instance($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType);
 
     // Set up a field, so that the entity that'll be referenced bubbles up a
@@ -88,6 +86,11 @@ public function setUp() {
       ))
       ->save();
 
+    entity_create('filter_format', array(
+      'format' => 'full_html',
+      'name' => 'Full HTML',
+    ))->save();
+
     // Create the entity to be referenced.
     $this->referencedEntity = entity_create($this->entityType, array('name' => $this->randomName()));
     $this->referencedEntity->body = array(
@@ -180,7 +183,7 @@ public function testEntityFormatter() {
     $expected_rendered_body_field = '<div class="field field-entity-test--body field-name-body field-type-text field-label-above">
       <div class="field-label">Body:&nbsp;</div>
     <div class="field-items">
-          <div class="field-item"></div>
+          <div class="field-item"><p>Hello, world!</p></div>
       </div>
 </div>
 ';
diff --git a/core/modules/filter/config/install/filter.format.plain_text.yml b/core/modules/filter/config/install/filter.format.plain_text.yml
index 70abb88..5de8988 100644
--- a/core/modules/filter/config/install/filter.format.plain_text.yml
+++ b/core/modules/filter/config/install/filter.format.plain_text.yml
@@ -9,7 +9,6 @@ weight: 10
 roles:
   - anonymous
   - authenticated
-cache: true
 filters:
   # Escape all HTML.
   filter_html_escape:
diff --git a/core/modules/filter/config/schema/filter.schema.yml b/core/modules/filter/config/schema/filter.schema.yml
index 25c3541..6a28b4e 100644
--- a/core/modules/filter/config/schema/filter.schema.yml
+++ b/core/modules/filter/config/schema/filter.schema.yml
@@ -30,14 +30,17 @@ filter.format.*:
       sequence:
         - type: string
           label: 'Role'
-    cache:
-      type: boolean
-      label: 'Cache'
     filters:
       type: sequence
       label: 'Enabled filters'
       sequence:
         - type: filter
+    langcode:
+      type: string
+      label: 'Default language'
+    dependencies:
+      type: config_dependencies
+      label: 'Dependencies'
 
 filter_settings.*:
   type: sequence
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index f25fe79..9c140a5 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Cache\Cache;
@@ -98,10 +99,132 @@ function filter_element_info() {
     '#base_type' => 'textarea',
     '#theme_wrappers' => array('text_format_wrapper'),
   );
+  $type['processed_text'] = array(
+    '#text' => '',
+    '#format' => NULL,
+    '#filter_types_to_skip' => array(),
+    '#langcode' => '',
+    '#pre_render' => array('filter_pre_render_text'),
+  );
   return $type;
 }
 
 /**
+ * Pre-render callback: Renders a processed text element into #markup.
+ *
+ * Runs all the enabled filters on a piece of text.
+ *
+ * Note: Because filters can inject JavaScript or execute PHP code, security is
+ * vital here. When a user supplies a text format, you should validate it using
+ * $format->access() before accepting/using it. This is normally done in the
+ * validation stage of the Form API. You should for example never make a
+ * preview of content in a disallowed format.
+ *
+ * @param array $element
+ *   A structured array with the following key-value pairs:
+ *   - #text: containing the text to be filtered
+ *   - #format: containing the machine name of the filter format to be used to
+ *     filter the text. Defaults to the fallback format. See
+ *     filter_fallback_format().
+ *   - #langcode: the language code of the text to be filtered, e.g. 'en' for
+ *     English. This allows filters to be language-aware so language-specific
+ *     text replacement can be implemented. Defaults to an empty string.
+ *   - #filter_types_to_skip: an array of filter types to skip, or an empty
+ *     array (default) to skip no filter types. All of the format's filters will
+ *     be applied, except for filters of the types that are marked to be skipped.
+ *     FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be
+ *     skipped.
+ *
+ * @return array
+ *   The passed-in element with the filtered text in '#markup'.
+ *
+ * @ingroup sanitization
+ */
+function filter_pre_render_text(array $element) {
+  $format_id = $element['#format'];
+  $filter_types_to_skip = $element['#filter_types_to_skip'];
+  $text = $element['#text'];
+  $langcode = $element['#langcode'];
+
+  if (!isset($format_id)) {
+    $format_id = filter_fallback_format();
+  }
+  // If the requested text format does not exist, the text cannot be filtered.
+  /** @var \Drupal\filter\Entity\FilterFormat $format **/
+  if (!$format = entity_load('filter_format', $format_id)) {
+    watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT);
+    $element['#markup'] = '';
+    return $element;
+  }
+
+  $filter_must_be_applied = function($filter) use ($filter_types_to_skip) {
+    $enabled = $filter->status === TRUE;
+    $type = $filter->getType();
+    // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped.
+    $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip);
+    return $enabled && $filter_type_must_be_applied;
+  };
+
+  // Convert all Windows and Mac newlines to a single newline, so filters only
+  // need to deal with one possibility.
+  $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+  // Get a complete list of filters, ordered properly.
+  /** @var \Drupal\filter\FilterBag $filters **/
+  $filters = $format->filters();
+
+  // Give filters the chance to escape HTML-like data such as code or formulas.
+  /** @var \Drupal\filter\Plugin\FilterInterface $filter **/
+  foreach ($filters as $filter) {
+    if ($filter_must_be_applied($filter)) {
+      $text = $filter->prepare($text, $langcode);
+    }
+  }
+
+  // Perform filtering.
+  $all_cache_tags = array();
+  $all_assets = array();
+  $all_post_render_cache_callbacks = array();
+  foreach ($filters as $filter) {
+    if ($filter_must_be_applied($filter)) {
+      $result = $filter->process($text, $langcode);
+      $all_assets[] = $result->getAssets();
+      $all_cache_tags[] = $result->getCacheTags();
+      $all_post_render_cache_callbacks[] = $result->getPostRenderCacheCallbacks();
+      $text = $result->getProcessedText();
+    }
+  }
+
+  // Filtering done, store in #markup.
+  $element['#markup'] = $text;
+
+  // Collect all cache tags.
+  if (isset($element['#cache']) && isset($element['#cache']['tags'])) {
+    // Prepend the original cache tags array.
+    array_unshift($all_cache_tags, $element['#cache']['tags']);
+  }
+  // Prepend the text format's cache tags array.
+  array_unshift($all_cache_tags, $format->getCacheTag());
+  $element['#cache']['tags'] = NestedArray::mergeDeepArray($all_cache_tags);
+
+  // Collect all attached assets.
+  if (isset($element['#attached'])) {
+    // Prepend the original attached assets array.
+    array_unshift($all_assets, $element['#attached']);
+  }
+  $element['#attached'] = NestedArray::mergeDeepArray($all_assets);
+
+  // Collect all #post_render_cache callbacks.
+  if (isset($element['#post_render_cache'])) {
+    // Prepend the original attached #post_render_cache array.
+    array_unshift($all_assets, $element['#post_render_cache']);
+  }
+  $element['#post_render_cache'] = NestedArray::mergeDeepArray($all_post_render_cache_callbacks);
+
+  return $element;
+}
+
+/**
  * Implements hook_permission().
  */
 function filter_permission() {
@@ -301,24 +424,6 @@ function filter_fallback_format() {
 }
 
 /**
- * Checks if the text in a certain text format is allowed to be cached.
- *
- * This function can be used to check whether the result of the filtering
- * process can be cached. A text format may allow caching depending on the
- * filters enabled.
- *
- * @param string $format_id
- *   The text format ID to check.
- *
- * @return bool
- *   TRUE if the given text format allows caching, FALSE otherwise.
- */
-function filter_format_allowcache($format_id) {
-  $format = $format_id ? entity_load('filter_format', $format_id) : FALSE;
-  return !empty($format->cache);
-}
-
-/**
  * Runs all the enabled filters on a piece of text.
  *
  * Note: Because filters can inject JavaScript or execute PHP code, security is
@@ -327,94 +432,46 @@ function filter_format_allowcache($format_id) {
  * validation stage of the Form API. You should for example never make a
  * preview of content in a disallowed format.
  *
- * @param $text
+ * Note: this function should only be used when filtering text for use elsewhere
+ * than on a rendered HTML page. If this is part of a HTML page, then a
+ * renderable array with a #type 'processed_text' element should be used instead
+ * of this, because that will allow cache tags to be set and bubbled up, assets
+ * to be loaded and #post_render_cache callbacks to be associated. In other
+ * words: if you are presenting the filtered text in a HTML page, the only way
+ * this will be presented correctly, is by using the 'processed_text' element.
+ *
+ * @param string $text
  *   The text to be filtered.
- * @param $format_id
+ * @param string|null $format_id
  *   (optional) The machine name of the filter format to be used to filter the
  *   text. Defaults to the fallback format. See filter_fallback_format().
- * @param $langcode
+ * @param string $langcode
  *   (optional) The language code of the text to be filtered, e.g. 'en' for
- *   English. This allows filters to be language aware so language specific
+ *   English. This allows filters to be language-aware so language-specific
  *   text replacement can be implemented. Defaults to an empty string.
- * @param $cache
- *   (optional) A Boolean indicating whether to cache the filtered output in the
- *   {cache_filter} table. The caller may set this to FALSE when the output is
- *   already cached elsewhere to avoid duplicate cache lookups and storage.
- *   Defaults to FALSE.
  * @param array $filter_types_to_skip
  *   (optional) An array of filter types to skip, or an empty array (default)
  *   to skip no filter types. All of the format's filters will be applied,
  *   except for filters of the types that are marked to be skipped.
- *   FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be skipped.
+ *   FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be
+ *   skipped.
  *
- * @return
+ * @return string
  *   The filtered text.
  *
+ * @see filter_process_text()
+ *
  * @ingroup sanitization
  */
-function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE, $filter_types_to_skip = array()) {
-  if (!isset($format_id)) {
-    $format_id = filter_fallback_format();
-  }
-  // If the requested text format does not exist, the text cannot be filtered.
-  if (!$format = entity_load('filter_format', $format_id)) {
-    watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT);
-    return '';
-  }
-
-  // When certain filters should be skipped, don't perform caching.
-  if ($filter_types_to_skip) {
-    $cache = FALSE;
-  }
-
-  // Check for a cached version of this piece of text.
-  $cache = $cache && !empty($format->cache);
-  $cache_id = '';
-  if ($cache) {
-    $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text);
-    if ($cached = \Drupal::cache('filter')->get($cache_id)) {
-      return $cached->data;
-    }
-  }
-
-  // Convert all Windows and Mac newlines to a single newline, so filters only
-  // need to deal with one possibility.
-  $text = str_replace(array("\r\n", "\r"), "\n", $text);
-
-  // Get a complete list of filters, ordered properly.
-  $filters = $format->filters();
-
-  $filter_must_be_applied = function($filter) use ($filter_types_to_skip) {
-    $enabled = $filter->status === TRUE;
-    $type = $filter->getType();
-    // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped.
-    $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip);
-    return $enabled && $filter_type_must_be_applied;
-  };
-
-  // Give filters the chance to escape HTML-like data such as code or formulas.
-  foreach ($filters as $filter) {
-    if ($filter_must_be_applied($filter)) {
-      $text = $filter->prepare($text, $langcode, $cache, $cache_id);
-    }
-  }
-
-  // Perform filtering.
-  foreach ($filters as $filter) {
-    if ($filter_must_be_applied($filter)) {
-      $text = $filter->process($text, $langcode, $cache, $cache_id);
-    }
-  }
-
-  // Cache the filtered text. This cache is infinitely valid. It becomes
-  // obsolete when $text changes (which leads to a new $cache_id). It is
-  // automatically flushed when the text format is updated.
-  // @see \Drupal\filter\Entity\FilterFormat::save()
-  if ($cache) {
-    \Drupal::cache('filter')->set($cache_id, $text, Cache::PERMANENT, array('filter_format' => $format->id()));
-  }
-
-  return $text;
+function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to_skip = array()) {
+  $build = array(
+    '#type' => 'processed_text',
+    '#text' => $text,
+    '#format' => $format_id,
+    '#filter_types_to_skip' => $filter_types_to_skip,
+    '#langcode' => $langcode,
+  );
+  return drupal_render($build);
 }
 
 /**
@@ -1139,10 +1196,3 @@ function filter_filter_secure_image_alter(&$image) {
 /**
  * @} End of "defgroup standard_filters".
  */
-
-/**
- * Implements hook_page_build().
- */
-function filter_page_build(&$page) {
-  $page['#attached']['library'][] = 'filter/caption';
-}
diff --git a/core/modules/filter/filter.services.yml b/core/modules/filter/filter.services.yml
index dc93fdd..406161a 100644
--- a/core/modules/filter/filter.services.yml
+++ b/core/modules/filter/filter.services.yml
@@ -1,11 +1,4 @@
 services:
-  cache.filter:
-    class: Drupal\Core\Cache\CacheBackendInterface
-    tags:
-      - { name: cache.bin }
-    factory_method: get
-    factory_service: cache_factory
-    arguments: [filter]
   plugin.manager.filter:
     class: Drupal\filter\FilterPluginManager
     parent: default_plugin_manager
diff --git a/core/modules/filter/src/Annotation/Filter.php b/core/modules/filter/src/Annotation/Filter.php
index a14b72f..808357c 100644
--- a/core/modules/filter/src/Annotation/Filter.php
+++ b/core/modules/filter/src/Annotation/Filter.php
@@ -65,16 +65,6 @@ class Filter extends Plugin {
   public $status = FALSE;
 
   /**
-   * Specifies whether the filtered text can be cached.
-   *
-   * Note that setting this to FALSE makes the entire text format not cacheable,
-   * which may have an impact on the site's overall performance.
-   *
-   * @var bool (optional)
-   */
-  public $cache = TRUE;
-
-  /**
    * The default settings for the filter.
    *
    * @var array (optional)
diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php
index 6f5f807..9598f77 100644
--- a/core/modules/filter/src/Entity/FilterFormat.php
+++ b/core/modules/filter/src/Entity/FilterFormat.php
@@ -93,13 +93,6 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface, En
   protected $roles;
 
   /**
-   * Whether processed text of this format can be cached.
-   *
-   * @var bool
-   */
-  public $cache = FALSE;
-
-  /**
    * Configured filters for this text format.
    *
    * An associative array of filters assigned to the text format, keyed by the
@@ -207,15 +200,6 @@ public function preSave(EntityStorageInterface $storage) {
 
     // @todo Do not save disabled filters whose properties are identical to
     //   all default properties.
-
-    // Determine whether the format can be cached.
-    // @todo This is a derived/computed definition, not configuration.
-    $this->cache = TRUE;
-    foreach ($this->filters()->getAll() as $filter) {
-      if ($filter->status && !$filter->cache) {
-        $this->cache = FALSE;
-      }
-    }
   }
 
   /**
diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php
new file mode 100644
index 0000000..71edc45
--- /dev/null
+++ b/core/modules/filter/src/FilterProcessResult.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\FilterProcessResult.
+ */
+
+namespace Drupal\filter;
+
+use Drupal\Component\Utility\NestedArray;
+
+/**
+ * Used to return values from a text filter plugin's processing method.
+ *
+ * The typical use case for a text filter plugin's processing method is to just
+ * apply some filtering to the given text, but for more advanced use cases,
+ * it may be necessary to also:
+ * 1. declare asset libraries to be loaded;
+ * 2. declare cache tags that the filtered text depends upon, so when either of
+ *   those cache tags is invalidated, the filtered text should also be
+ *   invalidated;
+ * 3. apply uncacheable filtering, for example because it differs per user.
+ *
+ * In case a filter needs one or more of these advanced use cases, it can use
+ * the additional methods available.
+ *
+ * The typical use case:
+ * @code
+ * public function process($text, $langcode) {
+ *   // Modify $text.
+ *
+ *   return new FilterProcess($text);
+ * }
+ * @endcode
+ *
+ * The advanced use cases:
+ * @code
+ * public function process($text, $langcode) {
+ *   // Modify $text.
+ *
+ *   $result = new FilterProcess($text);
+ *
+ *   // Associate assets to be attached.
+ *   $result->setAssets(array(
+ *     'library' => array(
+ *        'filter/caption',
+ *     ),
+ *   ));
+ *
+ *   // Associate cache tags to be invalidated by.
+ *   $result->setCacheTags($node->getCacheTag());
+ *
+ *   return $result;
+ * }
+ * @endcode
+ */
+class FilterProcessResult {
+
+  /**
+   * The processed text.
+   *
+   * @see \Drupal\filter\Plugin\FilterInterface::process()
+   *
+   * @var string
+   */
+  protected $processedText;
+
+  /**
+   * An array of associated assets to be attached.
+   *
+   * @see drupal_process_attached()
+   *
+   * @var array
+   */
+  protected $assets;
+
+  /**
+   * The attached cache tags.
+   *
+   * @see drupal_render_collect_cache_tags()
+   *
+   * @var array
+   */
+  protected $cacheTags;
+
+  /**
+   * The associated #post_render_cache callbacks.
+   *
+   * @see _drupal_render_process_post_render_cache()
+   *
+   * @var array
+   */
+  protected $postRenderCacheCallbacks;
+
+  /**
+   * Constructs a FilterProcessResult object.
+   *
+   * @param string $processed_text
+   *   The text as processed by a text filter.
+   */
+  public function __construct($processed_text) {
+    $this->processedText = $processed_text;
+
+    $this->assets = array();
+    $this->cacheTags = array();
+    $this->postRenderCacheCallbacks = array();
+  }
+
+  /**
+   * Gets the processed text.
+   *
+   * @return string
+   */
+  public function getProcessedText() {
+    return $this->processedText;
+  }
+
+  /**
+   * Gets the processed text.
+   *
+   * @return string
+   */
+  public function __toString() {
+    return $this->getProcessedText();
+  }
+
+  /**
+   * Sets the processed text.
+   *
+   * @param string $processed_text
+   *   The text as processed by a text filter.
+   *
+   * @return $this
+   */
+  public function setProcessedText($processed_text) {
+    $this->processedText = $processed_text;
+    return $this;
+  }
+
+  /**
+   * Gets cache tags associated with the processed text.
+   *
+   * @return array
+   */
+  public function getCacheTags() {
+    return $this->cacheTags;
+  }
+
+  /**
+   * Adds cache tags associated with the processed text.
+   *
+   * @param array $cache_tags
+   *   The cache tags to be added.
+   *
+   * @return $this
+   */
+  public function addCacheTags(array $cache_tags) {
+    $this->cacheTags = drupal_merge_attached($this->cacheTags, $cache_tags);
+    return $this;
+  }
+
+  /**
+   * Sets cache tags associated with the processed text.
+   *
+   * @param array $cache_tags
+   *   The cache tags to be associated.
+   *
+   * @return $this
+   */
+  public function setCacheTags(array $cache_tags) {
+    $this->cacheTags = $cache_tags;
+    return $this;
+  }
+
+  /**
+   * Gets assets associated with the processed text.
+   *
+   * @return array
+   */
+  public function getAssets() {
+    return $this->assets;
+  }
+
+  /**
+   * Adds assets associated with the processed text.
+   *
+   * @param array $assets
+   *   The associated assets to be attached.
+   *
+   * @return $this
+   */
+  public function addAssets(array $assets) {
+    $this->assets = NestedArray::mergeDeep($this->assets, $assets);
+    return $this;
+  }
+
+  /**
+   * Sets assets associated with the processed text.
+   *
+   * @param array $assets
+   *   The associated assets to be attached.
+   *
+   * @return $this
+   */
+  public function setAssets(array $assets) {
+    $this->assets = $assets;
+    return $this;
+  }
+
+  /**
+   * Gets #post_render_cache callbacks associated with the processed text.
+   *
+   * @return array
+   */
+  public function getPostRenderCacheCallbacks() {
+    return $this->postRenderCacheCallbacks;
+  }
+
+  /**
+   * Adds #post_render_cache callbacks associated with the processed text.
+   *
+   * @param string $callback
+   *   The #post_render_cache callback that will replace the placeholder with
+   *   its eventual markup.
+   * @param array $context
+   *   An array providing context for the #post_render_cache callback.
+   *
+   * @see drupal_render_cache_generate_placeholder()
+   *
+   * @return $this
+   */
+  public function addPostRenderCacheCallback($callback, array $context) {
+    $this->postRenderCacheCallbacks[$callback][] = $context;
+    return $this;
+  }
+
+  /**
+   * Sets #post_render_cache callbacks associated with the processed text.
+   *
+   * @param array $post_render_cache_callbacks
+   *   The associated #post_render_cache callbacks to be executed.
+   *
+   * @return $this
+   */
+  public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) {
+    $this->postRenderCacheCallbacks = $post_render_cache_callbacks;
+    return $this;
+  }
+
+}
diff --git a/core/modules/filter/src/Plugin/Filter/FilterAutoP.php b/core/modules/filter/src/Plugin/Filter/FilterAutoP.php
index a77b19e..0627527 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterAutoP.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterAutoP.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -23,8 +24,8 @@ class FilterAutoP extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return _filter_autop($text);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(_filter_autop($text));
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index 08ad4c8..bccaf02 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -11,6 +11,7 @@
 use Drupal\Component\Utility\String;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\Xss;
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -28,9 +29,11 @@ class FilterCaption extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
 
     if (stristr($text, 'data-caption') !== FALSE || stristr($text, 'data-align') !== FALSE) {
+      $caption_found = FALSE;
       $dom = Html::load($text);
       $xpath = new \DOMXPath($dom);
       foreach ($xpath->query('//*[@data-caption or @data-align]') as $node) {
@@ -71,6 +74,9 @@ public function process($text, $langcode, $cache, $cache_id) {
           }
           continue;
         }
+        else {
+          $caption_found = TRUE;
+        }
 
         // Given the updated node, caption and alignment: re-render it with a
         // caption.
@@ -96,10 +102,18 @@ public function process($text, $langcode, $cache, $cache_id) {
         $node->parentNode->replaceChild($updated_node, $node);
       }
 
-      return Html::serialize($dom);
+      $result->setProcessedText(Html::serialize($dom));
+
+      if ($caption_found) {
+        $result->addAssets(array(
+          'library' => array(
+            'filter/caption',
+          ),
+        ));
+      }
     }
 
-    return $text;
+    return $result;
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtml.php b/core/modules/filter/src/Plugin/Filter/FilterHtml.php
index 109d943..543f147 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterHtml.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterHtml.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -58,8 +59,8 @@ public function settingsForm(array $form, array &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return _filter_html($text, $this);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(_filter_html($text, $this));
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php
index bc0a036..0be711c 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php
@@ -8,6 +8,7 @@
 namespace Drupal\filter\Plugin\Filter;
 
 use Drupal\Component\Utility\Html;
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -25,8 +26,8 @@ class FilterHtmlCorrector extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return Html::normalize($text);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(Html::normalize($text));
   }
 
 }
diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php
index 0825df0..a508298 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -24,8 +25,8 @@ class FilterHtmlEscape extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return _filter_html_escape($text);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(_filter_html_escape($text));
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php
index f00b055..f03c75a 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -25,8 +26,8 @@ class FilterHtmlImageSecure extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return _filter_html_image_secure_process($text);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(_filter_html_image_secure_process($text));
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterNull.php b/core/modules/filter/src/Plugin/Filter/FilterNull.php
index d4fbe62..9c30edc 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterNull.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterNull.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -47,8 +48,8 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return '';
+  public function process($text, $langcode) {
+    return new FilterProcessResult('');
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/Filter/FilterUrl.php b/core/modules/filter/src/Plugin/Filter/FilterUrl.php
index 0918b19..469ff41 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterUrl.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterUrl.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -41,8 +42,8 @@ public function settingsForm(array $form, array &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return _filter_url($text, $this);
+  public function process($text, $langcode) {
+    return new FilterProcessResult(_filter_url($text, $this));
   }
 
   /**
diff --git a/core/modules/filter/src/Plugin/FilterBase.php b/core/modules/filter/src/Plugin/FilterBase.php
index facd818..44421e1 100644
--- a/core/modules/filter/src/Plugin/FilterBase.php
+++ b/core/modules/filter/src/Plugin/FilterBase.php
@@ -45,13 +45,6 @@
   public $weight = 0;
 
   /**
-   * A Boolean indicating whether the text processed by this filter may be cached.
-   *
-   * @var bool
-   */
-  public $cache = TRUE;
-
-  /**
    * An associative array containing the configured settings of this filter.
    *
    * @var array
@@ -72,7 +65,6 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition
     parent::__construct($configuration, $plugin_id, $plugin_definition);
 
     $this->provider = $this->pluginDefinition['provider'];
-    $this->cache = $this->pluginDefinition['cache'];
 
     $this->setConfiguration($configuration);
   }
@@ -154,7 +146,7 @@ public function settingsForm(array $form, array &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function prepare($text, $langcode, $cache, $cache_id) {
+  public function prepare($text, $langcode) {
     return $text;
   }
 
diff --git a/core/modules/filter/src/Plugin/FilterInterface.php b/core/modules/filter/src/Plugin/FilterInterface.php
index f6f7dd2..fea8559 100644
--- a/core/modules/filter/src/Plugin/FilterInterface.php
+++ b/core/modules/filter/src/Plugin/FilterInterface.php
@@ -36,18 +36,19 @@
  * should then actually change the content: transform URLs into hyperlinks,
  * convert smileys into images, etc.
  *
- * For performance reasons, content is only filtered once; the result is stored
- * in the cache table and retrieved from the cache the next time the same piece
- * of content is displayed. If a filter's output is dynamic, it can override
- * the cache mechanism, but obviously this should be used with caution: having
- * one filter that does not support caching in a collection of filters disables
- * caching for the entire collection, not just for one filter.
+ * @see filter_process_text()
+ * @see check_markup()
  *
- * Beware of the filter cache when developing your module: it is advised to set
- * your filter to 'cache' to FALSE while developing, but be sure to remove that
- * setting if it's not needed, when you are no longer in development mode.
+ * Typically, only text processing is applied, but in more advanced use cases,
+ * filters may also:
+ * - declare asset libraries to be loaded;
+ * - declare cache tags that the resulting filtered text depends upon, so when
+ *   either of those cache tags is invalidated, the render-cached HTML that the
+ *   filtered text is part of should also be invalidated;
+ * - declare #post_render_cache callbacks to apply uncacheable filtering, for
+ *   example because it differs per user.
  *
- * @see check_markup()
+ * @see \Drupal\filter\Plugin\FilterInterface::process()
  *
  * Filters are discovered through annotations, which may contain the following
  * definition properties:
@@ -67,9 +68,6 @@
  * - status: The default status for new instances of the filter. Defaults to
  *   FALSE.
  * - weight: A default weight for new instances of the filter. Defaults to 0.
- * - cache: Whether the filtered text can be cached. Defaults to TRUE.
- *   Note that setting this to FALSE disables caching for an entire text format,
- *   which can have a negative impact on the site's overall performance.
  * - settings: An associative array containing default settings for new
  *   instances of the filter.
  *
@@ -151,16 +149,11 @@ public function settingsForm(array $form, array &$form_state);
    *   The text string to be filtered.
    * @param string $langcode
    *   The language code of the text to be filtered.
-   * @param bool $cache
-   *   A Boolean indicating whether the filtered text is going to be cached in
-   *   {cache_filter}.
-   * @param string $cache_id
-   *   The ID of the filtered text in {cache_filter}, if $cache is TRUE.
    *
    * @return string
    *   The prepared, escaped text.
    */
-  public function prepare($text, $langcode, $cache, $cache_id);
+  public function prepare($text, $langcode);
 
   /**
    * Performs the filter processing.
@@ -169,16 +162,14 @@ public function prepare($text, $langcode, $cache, $cache_id);
    *   The text string to be filtered.
    * @param string $langcode
    *   The language code of the text to be filtered.
-   * @param bool $cache
-   *   A Boolean indicating whether the filtered text is going to be cached in
-   *   {cache_filter}.
-   * @param string $cache_id
-   *   The ID of the filtered text in {cache_filter}, if $cache is TRUE.
    *
-   * @return string
-   *   The filtered text.
+   * @return \Drupal\filter\FilterProcessResult
+   *   The filtered text, wrapped in a FilterProcessResult object, and possibly
+   *   with associated assets, cache tags and #post_render_cache callbacks.
+   *
+   * @see \Drupal\filter\FilterProcessResult
    */
-  public function process($text, $langcode, $cache, $cache_id);
+  public function process($text, $langcode);
 
   /**
    * Returns HTML allowed by this filter's configuration.
diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php
index 1c33d74..e0c06d5 100644
--- a/core/modules/filter/src/Tests/FilterAPITest.php
+++ b/core/modules/filter/src/Tests/FilterAPITest.php
@@ -106,12 +106,12 @@ function testCheckMarkupFilterSubset() {
     $expected_filter_text_without_html_generators = "Text with evil content and a URL: http://drupal.org!";
 
     $this->assertIdentical(
-      check_markup($text, 'filtered_html', '', FALSE, array()),
+      check_markup($text, 'filtered_html', '', array()),
       $expected_filtered_text,
       'Expected filter result.'
     );
     $this->assertIdentical(
-      check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_MARKUP_LANGUAGE)),
+      check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_MARKUP_LANGUAGE)),
       $expected_filter_text_without_html_generators,
       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters.'
     );
@@ -120,7 +120,7 @@ function testCheckMarkupFilterSubset() {
     // Drupal core only ships with these two types of filters, so this is the
     // most extensive test possible.
     $this->assertIdentical(
-      check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)),
+      check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)),
       $expected_filter_text_without_html_generators,
       'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.'
     );
@@ -224,6 +224,73 @@ function testFilterFormatAPI() {
   }
 
   /**
+   * Tests the 'processed_text' element.
+   *
+   * check_markup() is a wrapper for the 'processed_text' element, for use in
+   * simple scenarios; the 'processed_text' element has more advanced features:
+   * it lets filters attach assets, associate cache tags and define
+   * #post_render_cache callbacks.
+   * This test focuses solely on those advanced features.
+   */
+  function testProcessedTextElement() {
+    entity_create('filter_format', array(
+      'format' => 'element_test',
+      'name' => 'processed_text element test format',
+      'filters' => array(
+        'filter_test_assets' => array(
+          'weight' => -1,
+          'status' => TRUE,
+        ),
+        'filter_test_cache_tags' => array(
+          'weight' => 0,
+          'status' => TRUE,
+        ),
+        'filter_test_post_render_cache' => array(
+          'weight' => 1,
+          'status' => TRUE,
+        ),
+        // Run the HTML corrector filter last, because it has the potential to
+        // break the render cache placeholders added by the
+        // filter_test_post_render_cache filter.
+        'filter_htmlcorrector' => array(
+          'weight' => 10,
+          'status' => TRUE,
+        ),
+      ),
+    ))->save();
+
+    $build = array(
+      '#type' => 'processed_text',
+      '#text' => '<p>Hello, world!</p>',
+      '#format' => 'element_test',
+    );
+    drupal_render($build);
+
+    // Verify the assets, cache tags and #post_render_cache callbacks.
+    $expected_assets = array(
+      // The assets attached by the filter_test_assets filter.
+      'library' => array(
+        'filter/caption',
+      ),
+    );
+    $this->assertEqual($expected_assets, $build['#attached'], 'Expected assets present');
+    $expected_cache_tags = array(
+      // The cache tag set by the processed_text element itself.
+      'filter_format' => array(
+        'element_test' => 'element_test',
+      ),
+      // The cache tags set by the filter_test_cache_tags filter.
+      'foo' => array(
+        'bar' => 'bar',
+        'baz' => 'baz',
+      ),
+    );
+    $this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.');
+    $expected_markup = '<p>Hello, world!</p><p>This is a dynamic llama.</p>';
+    $this->assertEqual($expected_markup, $build['#markup'], 'Expected #post_render_cache callback has been applied.');
+  }
+
+  /**
    * Tests the function of the typed data type.
    */
   function testTypedDataAPI() {
diff --git a/core/modules/filter/src/Tests/FilterAdminTest.php b/core/modules/filter/src/Tests/FilterAdminTest.php
index 7aa30a8..9e6d74b 100644
--- a/core/modules/filter/src/Tests/FilterAdminTest.php
+++ b/core/modules/filter/src/Tests/FilterAdminTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\filter\Tests;
 
 use Drupal\simpletest\WebTestBase;
-use Drupal\filter\Plugin\FilterInterface;
 
 /**
  * Tests the administrative functionality of the Filter module.
@@ -354,73 +353,4 @@ function testUrlFilterAdmin() {
     $this->assertNoRaw(t('The text format %format has been updated.', array('%format' => 'Basic HTML')));
   }
 
-  /**
-   * Tests that changing filter properties clears the filter cache.
-   */
-  public function testFilterAdminClearsFilterCache() {
-    $restricted = 'restricted_html';
-    $original_markup = '<h4>Small headers</h4> small headers are <em>allowed</em> in restricted html by default';
-
-    // Check that the filter cache is empty for the test markup.
-    $cid = $this->computeFilterCacheId($original_markup, $restricted, '', TRUE);
-    $this->assertFalse(\Drupal::cache('filter')->get($cid));
-
-    // Check that the filter cache gets populated when check_markup is called.
-    $actual_markup = check_markup($original_markup, $restricted, '', TRUE);
-    $this->assertTrue(\Drupal::cache('filter')->get($cid));
-    $this->assertIdentical(strpos($actual_markup, '<h4>'), 0, 'The h4 tag is present in the resulting markup');
-
-    // Edit the restricted filter format.
-    $edit = array();
-    $edit['filters[filter_html][settings][allowed_html]'] = '<a> <em> <strong> <cite> <code>';
-    $this->drupalPostForm('admin/config/content/formats/manage/' . $restricted, $edit, t('Save configuration'));
-    $this->assertUrl('admin/config/content/formats');
-    $this->drupalGet('admin/config/content/formats/manage/' . $restricted);
-    $this->assertFieldByName('filters[filter_html][settings][allowed_html]', $edit['filters[filter_html][settings][allowed_html]'], 'Allowed HTML tag added.');
-
-    // Check that the filter cache is empty after the format was changed.
-    $this->assertFalse(\Drupal::cache('filter')->get($cid));
-
-    // Check that after changind the filter, the changes are reflected in the
-    // filtered markup.
-    $actual_markup = check_markup($original_markup, $restricted, '', TRUE);
-    $this->assertIdentical(strpos($actual_markup, '<h4>'), FALSE, 'The h4 tag is not present in the resulting markup');
-  }
-
-
-  /**
-   * Computes the cache-key for the given text just like check_markup().
-   *
-   * Note that this is copied over from check_markup().
-   *
-   * @return string|NULL
-   *   The cache-key used to store the text in the filter cache.
-   */
-  protected function computeFilterCacheId($text, $format_id = NULL, $langcode = '', $cache = FALSE, $filter_types_to_skip = array()) {
-    if (!isset($format_id)) {
-      $format_id = filter_fallback_format();
-    }
-    // If the requested text format does not exist, the text cannot be filtered.
-    if (!$format = entity_load('filter_format', $format_id)) {
-      return;
-    }
-
-    // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped.
-    if (in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $filter_types_to_skip)) {
-      $filter_types_to_skip = array_diff($filter_types_to_skip, array(FilterInterface::TYPE_HTML_RESTRICTOR));
-    }
-
-    // When certain filters should be skipped, don't perform caching.
-    if ($filter_types_to_skip) {
-      $cache = FALSE;
-    }
-
-    // Compute the cache key if the text is cacheable.
-    $cache = $cache && !empty($format->cache);
-    $cache_id = '';
-    if ($cache) {
-      return $format->format . ':' . $langcode . ':' . hash('sha256', $text);
-    }
-  }
-
 }
diff --git a/core/modules/filter/src/Tests/FilterCrudTest.php b/core/modules/filter/src/Tests/FilterCrudTest.php
index 041f309..b592c5f 100644
--- a/core/modules/filter/src/Tests/FilterCrudTest.php
+++ b/core/modules/filter/src/Tests/FilterCrudTest.php
@@ -65,8 +65,8 @@ function testTextFormatCrud() {
     $format->save();
     $this->verifyTextFormat($format);
 
-    // Add a uncacheable filter and save again.
-    $format->setFilterConfig('filter_test_uncacheable', array(
+    // Add a filter_test_replace  filter and save again.
+    $format->setFilterConfig('filter_test_replace', array(
       'status' => 1,
     ));
     $format->save();
@@ -90,22 +90,9 @@ function verifyTextFormat($format) {
     $filter_format = entity_load('filter_format', $format->format);
     $this->assertEqual($filter_format->format, $format->format, format_string('filter_format_load: Proper format id for text format %format.', $t_args));
     $this->assertEqual($filter_format->name, $format->name, format_string('filter_format_load: Proper title for text format %format.', $t_args));
-    $this->assertEqual($filter_format->cache, $format->cache, format_string('filter_format_load: Proper cache indicator for text format %format.', $t_args));
     $this->assertEqual($filter_format->weight, $format->weight, format_string('filter_format_load: Proper weight for text format %format.', $t_args));
     // Check that the filter was created in site default language.
     $this->assertEqual($format->langcode, $default_langcode, format_string('filter_format_load: Proper language code for text format %format.', $t_args));
-
-    // Verify the 'cache' text format property according to enabled filters.
-    $cacheable = TRUE;
-    foreach ($format->filters() as $name => $filter) {
-      // If this filter is not cacheable, update $cacheable accordingly, so we
-      // can verify $format->cache after iterating over all filters.
-      if ($filter->status && !$filter->cache) {
-        $cacheable = FALSE;
-        break;
-      }
-    }
-    $this->assertEqual($filter_format->cache, $cacheable, 'Text format contains proper cache property.');
   }
 
 }
diff --git a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php
index 7423b70..1ff9773 100644
--- a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php
+++ b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php
@@ -49,7 +49,6 @@ function testInstallation() {
 
     // Verify that format default property values have been added/injected.
     $this->assertTrue($format->uuid());
-    $this->assertEqual($format->get('cache'), 1);
 
     // Verify that the loaded format does not contain any roles.
     $this->assertEqual($format->get('roles'), NULL);
diff --git a/core/modules/filter/src/Tests/FilterSecurityTest.php b/core/modules/filter/src/Tests/FilterSecurityTest.php
index 6321c1b..6aea5de 100644
--- a/core/modules/filter/src/Tests/FilterSecurityTest.php
+++ b/core/modules/filter/src/Tests/FilterSecurityTest.php
@@ -103,7 +103,7 @@ function testDisableFilterModule() {
   function testSkipSecurityFilters() {
     $text = "Text with some disallowed tags: <script />, <em><object>unicorn</object></em>, <i><table></i>.";
     $expected_filtered_text = "Text with some disallowed tags: , <em>unicorn</em>, .";
-    $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array()), $expected_filtered_text, 'Expected filter result.');
-    $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR)), $expected_filtered_text, 'Expected filter result, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.');
+    $this->assertEqual(check_markup($text, 'filtered_html', '', array()), $expected_filtered_text, 'Expected filter result.');
+    $this->assertEqual(check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_HTML_RESTRICTOR)), $expected_filtered_text, 'Expected filter result, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.');
   }
 }
diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php
index a51d6b9..6426bf6 100644
--- a/core/modules/filter/src/Tests/FilterUnitTest.php
+++ b/core/modules/filter/src/Tests/FilterUnitTest.php
@@ -52,107 +52,141 @@ function testCaptionFilter() {
     $filter = $this->filters['filter_caption'];
 
     $test = function($input) use ($filter) {
-      return $filter->process($input, 'und', FALSE, '');
+      return $filter->process($input, 'und');
     };
 
+    $attached_library = array(
+      'library' => array(
+        'filter/caption',
+      ),
+    );
+
     // No data-caption nor data-align attributes.
     $input = '<img src="llama.jpg" />';
     $expected = $input;
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
 
     // Only data-caption attribute.
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Empty data-caption attribute.
     $input = '<img src="llama.jpg" data-caption="" />';
     $expected = '<img src="llama.jpg" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
 
     // HTML entities in the caption.
     $input = '<img src="llama.jpg" data-caption="&ldquo;Loquacious llama!&rdquo;" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>“Loquacious llama!”</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // HTML encoded as HTML entities in data-caption attribute.
     $input = '<img src="llama.jpg" data-caption="&lt;em&gt;Loquacious llama!&lt;/em&gt;" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // HTML (not encoded as HTML entities) in data-caption attribute, which is
     // not allowed by the HTML spec, but may happen when people manually write
     // HTML, so we explicitly support it.
     $input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Security test: attempt an XSS.
     $input = '<img src="llama.jpg" data-caption="<script>alert(\'Loquacious llama!\')</script>" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>alert(\'Loquacious llama!\')</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Only data-align attribute: all 3 allowed values.
     $input = '<img src="llama.jpg" data-align="left" />';
     $expected = '<img src="llama.jpg" class="align-left" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
     $input = '<img src="llama.jpg" data-align="center" />';
     $expected = '<img src="llama.jpg" class="align-center" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
     $input = '<img src="llama.jpg" data-align="right" />';
     $expected = '<img src="llama.jpg" class="align-right" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
 
     // Only data-align attribute: a disallowed value.
     $input = '<img src="llama.jpg" data-align="left foobar" />';
     $expected = '<img src="llama.jpg" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
 
     // Empty data-align attribute.
     $input = '<img src="llama.jpg" data-align="" />';
     $expected = '<img src="llama.jpg" />';
-    $this->assertIdentical($expected, $test($input));
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
 
     // Both data-caption and data-align attributes: all 3 allowed values for the
     // data-align attribute.
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left" />';
     $expected = '<figure class="caption caption-img align-left"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center" />';
     $expected = '<figure class="caption caption-img align-center"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="right" />';
     $expected = '<figure class="caption caption-img align-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Both data-caption and data-align attributes, but a disallowed data-align
     // attribute value.
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left foobar" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Ensure the filter also works with uncommon yet valid attribute quoting.
     $input = '<img src=llama.jpg data-caption=\'Loquacious llama!\' data-align=right />';
     $expected = '<figure class="caption caption-img align-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Security test: attempt to inject an additional class.
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center another-class-here" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Security test: attempt an XSS.
     $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center \'onclick=\'alert(foo);" />';
     $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
     // Finally, ensure that this also works on any other tag.
     $input = '<video src="llama.jpg" data-caption="Loquacious llama!" />';
     $expected = '<figure class="caption caption-video"><video src="llama.jpg"></video><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
     $input = '<foobar data-caption="Loquacious llama!">baz</foobar>';
     $expected = '<figure class="caption caption-foobar"><foobar>baz</foobar><figcaption>Loquacious llama!</figcaption></figure>';
-    $this->assertIdentical($expected, $test($input));
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
   }
 
   /**
@@ -676,7 +710,7 @@ function testUrlFilter() {
    */
   function assertFilteredString($filter, $tests) {
     foreach ($tests as $source => $tasks) {
-      $result = $filter->process($source, $filter, FALSE, '');
+      $result = $filter->process($source, $filter)->getProcessedText();
       foreach ($tasks as $value => $is_expected) {
         // Not using assertIdentical, since combination with strpos() is hard to grok.
         if ($is_expected) {
diff --git a/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml b/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml
index 9b48b54..d3d77fc 100644
--- a/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml
+++ b/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml
@@ -4,7 +4,6 @@ weight: 2
 roles:
   - anonymous
   - authenticated
-cache: true
 status: true
 langcode: en
 filters:
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php
new file mode 100644
index 0000000..a7de6d9
--- /dev/null
+++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter_test\Plugin\Filter\FilterTestAssets.
+ */
+
+namespace Drupal\filter_test\Plugin\Filter;
+
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Provides a test filter to attach assets
+ *
+ * @Filter(
+ *   id = "filter_test_assets",
+ *   title = @Translation("Testing filter"),
+ *   description = @Translation("Does not change content; attaches assets."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class FilterTestAssets extends FilterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+    $result->addAssets(array(
+      'library' => array(
+        'filter/caption',
+      ),
+    ));
+    return $result;
+  }
+
+}
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php
new file mode 100644
index 0000000..3c03c3f
--- /dev/null
+++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter_test\Plugin\Filter\FilterTestCacheTags.
+ */
+
+namespace Drupal\filter_test\Plugin\Filter;
+
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Provides a test filter to associate cache tags
+ *
+ * @Filter(
+ *   id = "filter_test_cache_tags",
+ *   title = @Translation("Testing filter"),
+ *   description = @Translation("Does not change content; associates cache tags."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class FilterTestCacheTags extends FilterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+    $result->addCacheTags(array('foo' => array('bar')));
+    $result->addCacheTags(array('foo' => array('baz')));
+    return $result;
+  }
+
+}
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php
new file mode 100644
index 0000000..aaeb400
--- /dev/null
+++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache.
+ */
+
+namespace Drupal\filter_test\Plugin\Filter;
+
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Provides a test filter to associate #post_render_cache callbacks.
+ *
+ * @Filter(
+ *   id = "filter_test_post_render_cache",
+ *   title = @Translation("Testing filter"),
+ *   description = @Translation("Appends a placeholder to the content; associates #post_render_cache callbacks."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class FilterTestPostRenderCache extends FilterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $callback = '\Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache::renderDynamicThing';
+    $context = array(
+      'thing' => 'llama',
+    );
+    $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
+    $result = new FilterProcessResult($text . '<p>' . $placeholder . '</p>');
+    $result->addPostRenderCacheCallback($callback, $context);
+    return $result;
+  }
+
+  /**
+   * #post_render_cache callback; replaces placeholder with a dynamic thing.
+   *
+   * @param array $element
+   *   The renderable array that contains the to be replaced placeholder.
+   * @param array $context
+   *   An array with the following keys:
+   *   - thing: a "thing" string
+   *
+   * @return array
+   *   A renderable array containing the comment form.
+   */
+  public static function renderDynamicThing(array $element, array $context) {
+    $callback = '\Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache::renderDynamicThing';
+    $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
+    $markup = format_string('This is a dynamic @thing.', array('@thing' => $context['thing']));
+    $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
+    return $element;
+  }
+
+}
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php
index f48c5b1..735c02d 100644
--- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php
+++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter_test\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 
 /**
@@ -24,15 +25,11 @@ class FilterTestReplace extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
+  public function process($text, $langcode) {
     $text = array();
     $text[] = 'Filter: ' . $this->getLabel() . ' (' . $this->getPluginId() . ')';
     $text[] = 'Language: ' . $langcode;
-    $text[] = 'Cache: ' . ($cache ? 'Enabled' : 'Disabled');
-    if ($cache_id) {
-      $text[] = 'Cache ID: ' . $cache_id;
-    }
-    return implode("<br />\n", $text);
+    return new FilterProcessResult(implode("<br />\n", $text));
   }
 
 }
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php
index 6e95d2e..c11160d 100644
--- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php
+++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\filter_test\Plugin\Filter;
 
+use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
 use Drupal\Component\Utility\Xss;
 
@@ -25,11 +26,11 @@ class FilterTestRestrictTagsAndAttributes extends FilterBase {
   /**
    * {@inheritdoc}
    */
-  public function process($text, $langcode, $cache, $cache_id) {
+  public function process($text, $langcode) {
     $allowed_tags = array_filter($this->settings['restrictions']['allowed'], function($value) {
       return is_array($value) || (bool) $value !== FALSE;
     });
-    return Xss::filter($text, array_keys($allowed_tags));
+    return new FilterProcessResult(Xss::filter($text, array_keys($allowed_tags)));
   }
 
   /**
diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php
deleted file mode 100644
index fd8318f..0000000
--- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\filter_test\Plugin\Filter\FilterTestUncacheable.
- */
-
-namespace Drupal\filter_test\Plugin\Filter;
-
-use Drupal\filter\Plugin\FilterBase;
-
-/**
- * Provides a test filter that is uncacheable.
- *
- * @Filter(
- *   id = "filter_test_uncacheable",
- *   title = @Translation("Uncacheable filter"),
- *   description = @Translation("Does nothing, but makes a text format uncacheable"),
- *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
- *   cache = false
- * )
- */
-class FilterTestUncacheable extends FilterBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public function process($text, $langcode, $cache, $cache_id) {
-    return $text;
-  }
-
-}
diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php
index 8edbb07..7587af6 100644
--- a/core/modules/system/src/Tests/Common/RenderTest.php
+++ b/core/modules/system/src/Tests/Common/RenderTest.php
@@ -774,6 +774,8 @@ function testDrupalRenderRenderCachePlaceholder() {
     );
     $callback = 'common_test_post_render_cache_placeholder';
     $placeholder = drupal_render_cache_generate_placeholder($callback, $context);
+    $this->assertIdentical($placeholder, Html::normalize($placeholder), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
+
     $test_element = array(
       '#post_render_cache' => array(
         $callback => array(
@@ -825,7 +827,7 @@ function testDrupalRenderRenderCachePlaceholder() {
     $this->assertIdentical($token, $expected_token, 'The tokens are identical');
     // Verify the token is in the cached element.
     $expected_element = array(
-      '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>',
+      '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>',
       '#post_render_cache' => array(
         'common_test_post_render_cache_placeholder' => array(
           $context
@@ -923,7 +925,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array(
-      '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>',
+      '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>',
       '#post_render_cache' => array(
         'common_test_post_render_cache_placeholder' => array(
           $context,
@@ -948,7 +950,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element');
     // Verify the token is in the cached element.
     $expected_element = array(
-      '#markup' => '<div><foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo></div>' . "\n",
+      '#markup' => '<div><foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo></div>' . "\n",
       '#post_render_cache' => array(
         'common_test_post_render_cache_placeholder' => array(
           $context,
@@ -977,7 +979,7 @@ function testDrupalRenderChildElementRenderCachePlaceholder() {
     $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element');
     // Verify the token is in the cached element.
     $expected_element = array(
-      '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>',
+      '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>',
       '#post_render_cache' => array(
         'common_test_post_render_cache_placeholder' => array(
           $context,
diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
index 5e0f80b..cfd83dc 100644
--- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
+++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php
@@ -32,18 +32,67 @@ class TextDefaultFormatter extends FormatterBase {
    * {@inheritdoc}
    */
   public function viewElements(FieldItemListInterface $items) {
+    if ($this->getFieldSetting('text_processing')) {
+      return $this->viewElementsWithTextProcessing($items);
+    }
+    else {
+      return $this->viewElementsWithoutTextProcessing($items);
+    }
+  }
+
+  /**
+   * Builds a renderable array when text processing is enabled.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $items
+   *   The text field values to be rendered.
+   *
+   * @return array
+   *   A renderable array for $items, as an array of child elements keyed by
+   *   consecutive numeric indexes starting from 0.
+   */
+  protected function viewElementsWithTextProcessing(FieldItemListInterface $items) {
+    $elements = array();
+
+    foreach ($items as $delta => $item) {
+      $elements[$delta] = array(
+        '#type' => 'processed_text',
+        '#text' => $item->value,
+        '#format' => $item->format,
+        '#langcode' => $item->getLangcode(),
+      );
+      // The viewElements() method of entity field formatters is run
+      // during the #pre_render phase of rendering an entity. A formatter
+      // builds the content of the field in preparation for theming.
+      // All cache tags must be available after the #pre_render phase. In order
+      // to collect the cache tags associated with the processed text, it must
+      // be passed to drupal_render() so that its #pre_render callback is
+      // invoked and its full build array is assembled. Rendering the processed
+      // text in place here will allow its cache tags to be bubbled up and
+      // included with those of the main entity when cache tags are collected
+      // for a renderable array in drupal_render().
+      // @todo remove this work-around, see https://drupal.org/node/2273277
+      drupal_render($elements[$delta], TRUE);
+    }
+
+    return $elements;
+  }
+
+  /**
+   * Builds a renderable array when text processing is disabled.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $items
+   *   The text field values to be rendered.
+   *
+   * @return array
+   *   A renderable array for $items, as an array of child elements keyed by
+   *   consecutive numeric indexes starting from 0.
+   */
+  protected function viewElementsWithoutTextProcessing(FieldItemListInterface $items) {
     $elements = array();
 
     foreach ($items as $delta => $item) {
       $elements[$delta] = array(
-        '#markup' => $item->processed, 
-        '#cache' => array(
-          'tags' => array(
-            'filter_format' => array(
-              $item->format,
-            ),
-          ),
-        ),
+        '#markup' => $item->processed,
       );
     }
 
diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
index d4b3c34..f2bbefc 100644
--- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
+++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php
@@ -68,26 +68,84 @@ public function settingsSummary() {
    * {@inheritdoc}
    */
   public function viewElements(FieldItemListInterface $items) {
+    if ($this->getFieldSetting('text_processing')) {
+      return $this->viewElementsWithTextProcessing($items);
+    }
+    else {
+      return $this->viewElementsWithoutTextProcessing($items);
+    }
+  }
+
+  /**
+   * Builds a renderable array when text processing is enabled.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $items
+   *   The text field values to be rendered.
+   *
+   * @return array
+   *   A renderable array for $items, as an array of child elements keyed by
+   *   consecutive numeric indexes starting from 0.
+   */
+  protected function viewElementsWithTextProcessing(FieldItemListInterface $items) {
+    $elements = array();
+
+    foreach ($items as $delta => $item) {
+      $elements[$delta] = array(
+        '#type' => 'processed_text',
+        '#text' => NULL,
+        '#format' => $item->format,
+        '#langcode' => $item->getLangcode(),
+      );
+
+      // The viewElements() method of entity field formatters is run
+      // during the #pre_render phase of rendering an entity. A formatter
+      // builds the content of the field in preparation for theming.
+      // All cache tags must be available after the #pre_render phase. In order
+      // to collect the cache tags associated with the processed text, it must
+      // be passed to drupal_render() so that its #pre_render callback is
+      // invoked and its full build array is assembled. Rendering the processed
+      // text in place here will allow its cache tags to be bubbled up and
+      // included with those of the main entity when cache tags are collected
+      // for a renderable array in drupal_render().
+      if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) {
+        $elements[$delta]['#text'] = $item->summary;
+        // @todo remove this work-around, see https://drupal.org/node/2273277
+        drupal_render($elements[$delta], TRUE);
+      }
+      else {
+        $elements[$delta]['#text'] = $item->value;
+        // @todo remove this work-around, see https://drupal.org/node/2273277
+        drupal_render($elements[$delta], TRUE);
+        $elements[$delta]['#markup'] = text_summary($elements[$delta]['#markup'], $item->format, $this->getSetting('trim_length'));
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * Builds a renderable array when text processing is disabled.
+   *
+   * @param \Drupal\Core\Field\FieldItemListInterface $items
+   *   The text field values to be rendered.
+   *
+   * @return array
+   *   A renderable array for $items, as an array of child elements keyed by
+   *   consecutive numeric indexes starting from 0.
+   */
+  protected function viewElementsWithoutTextProcessing(FieldItemListInterface $items) {
     $elements = array();
 
-    $text_processing = $this->getFieldSetting('text_processing');
     foreach ($items as $delta => $item) {
       if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) {
         $output = $item->summary_processed;
       }
       else {
-        $output = $item->processed;
-        $output = text_summary($output, $text_processing ? $item->format : NULL, $this->getSetting('trim_length'));
+        $output = text_summary($item->processed, NULL, $this->getSetting('trim_length'));
       }
+
       $elements[$delta] = array(
         '#markup' => $output,
-        '#cache' => array(
-          'tags' => array(
-            'filter_format' => array(
-              $item->format,
-            ),
-          ),
-        ),
       );
     }
 
diff --git a/core/modules/text/src/Tests/Formatter/TextFormatterTest.php b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php
new file mode 100644
index 0000000..3745a5a
--- /dev/null
+++ b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php
@@ -0,0 +1,137 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\text\Tests\Formatter\TextFormatterTest.
+ */
+
+namespace Drupal\text\Tests\Formatter;
+
+use Drupal\system\Tests\Entity\EntityUnitTestBase;
+
+/**
+ * Tests Text formatters.
+ */
+class TextFormatterTest extends EntityUnitTestBase {
+
+  /**
+   * The entity type used in this test.
+   *
+   * @var string
+   */
+  protected $entityType = 'entity_test';
+
+  /**
+   * The bundle used in this test.
+   *
+   * @var string
+   */
+  protected $bundle = 'entity_test';
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('text');
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getInfo() {
+    return array(
+      'name' => 'Text formatters',
+      'description' => 'Tests the text formatters functionality.',
+      'group' => 'Text ',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    entity_create('filter_format', array(
+      'format' => 'my_text_format',
+      'name' => 'My text format',
+      'filters' => array(
+        'filter_autop' => array(
+          'module' => 'filter',
+          'status' => TRUE,
+        ),
+      ),
+    ))->save();
+
+    // Set up two fields: one with text processing enabled, the other disabled.
+    entity_create('field_config', array(
+      'name' => 'processed_text',
+      'entity_type' => $this->entityType,
+      'type' => 'text',
+      'settings' => array(),
+    ))->save();
+    entity_create('field_instance_config', array(
+      'entity_type' => $this->entityType,
+      'bundle' => $this->bundle,
+      'field_name' => 'processed_text',
+      'label' => 'Processed text',
+      'settings' => array(
+        'text_processing' => TRUE,
+      ),
+    ))->save();
+    entity_create('field_config', array(
+      'name' => 'unprocessed_text',
+      'entity_type' => $this->entityType,
+      'type' => 'text',
+      'settings' => array(),
+    ))->save();
+    entity_create('field_instance_config', array(
+      'entity_type' => $this->entityType,
+      'bundle' => $this->bundle,
+      'field_name' => 'unprocessed_text',
+      'label' => 'Unprocessed text',
+      'settings' => array(
+        'text_processing' => FALSE,
+      ),
+    ))->save();
+  }
+
+  /**
+   * Tests all text field formatters.
+   */
+  public function testFormatters() {
+    $formatters = array(
+      'text_default',
+      'text_trimmed',
+      'text_summary_or_trimmed',
+    );
+
+    // Create the entity to be referenced.
+    $entity = entity_create($this->entityType, array('name' => $this->randomName()));
+    $entity->processed_text = array(
+      'value' => 'Hello, world!',
+      'format' => 'my_text_format',
+    );
+    $entity->unprocessed_text = array(
+      'value' => 'Hello, world!',
+    );
+    $entity->save();
+
+    foreach ($formatters as $formatter) {
+      // Verify the processed text field formatter's render array.
+      $build = $entity->get('processed_text')->view(array('type' => $formatter));
+      $this->assertEqual($build[0]['#markup'], "<p>Hello, world!</p>\n");
+      $expected_cache_tags = array(
+        'filter_format' => array('my_text_format' => 'my_text_format'),
+      );
+      $this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags when formatting a processed text field.', array('@formatter' => $formatter)));
+
+      // Verify the unprocessed text field formatter's render array.
+      $build = $entity->get('unprocessed_text')->view(array('type' => $formatter));
+      debug($build[0]);
+      $this->assertEqual($build[0]['#markup'], 'Hello, world!');
+      $this->assertTrue(!isset($build[0]['#cache']), format_string('The @formatter formatter has the expected cache tags when formatting an unprocessed text field.', array('@formatter' => $formatter)));
+    }
+  }
+
+}
diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php
index 5d99de3..3666d29 100644
--- a/core/modules/text/src/TextProcessed.php
+++ b/core/modules/text/src/TextProcessed.php
@@ -34,7 +34,7 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
     parent::__construct($definition, $name, $parent);
 
     if ($definition->getSetting('text source') === NULL) {
-      throw new \InvalidArgumentException("The definition's 'source' key has to specify the name of the text property to be processed.");
+      throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed.");
     }
   }
 
diff --git a/core/modules/user/src/Tests/UserSignatureTest.php b/core/modules/user/src/Tests/UserSignatureTest.php
index 83f5973..ce6c5ff 100644
--- a/core/modules/user/src/Tests/UserSignatureTest.php
+++ b/core/modules/user/src/Tests/UserSignatureTest.php
@@ -68,7 +68,7 @@ function setUp() {
     // Create regular and administrative users.
     $this->web_user = $this->drupalCreateUser(array('post comments'));
 
-    $admin_permissions = array('administer comments');
+    $admin_permissions = array('post comments', 'administer comments');
     foreach (filter_formats() as $format) {
       if ($permission = $format->getPermissionName()) {
         $admin_permissions[] = $permission;
@@ -82,7 +82,14 @@ function setUp() {
    * upon display.
    */
   function testUserSignature() {
-    $node = $this->drupalCreateNode();
+    $node = $this->drupalCreateNode(array(
+      'body' => array(
+        0 => array(
+          'value' => $this->randomName(32),
+          'format' => 'full_html',
+        ),
+      ),
+    ));
 
     // Verify that user signature field is not displayed on registration form.
     $this->drupalGet('user/register');
@@ -99,6 +106,10 @@ function testUserSignature() {
     // Verify that values were stored.
     $this->assertFieldByName('signature[value]', $edit['signature[value]'], 'Submitted signature text found.');
 
+    // Verify that the user signature's text format's cache tag is absent.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertTrue(!in_array('filter_format:filtered_html_format', explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'))));
+
     // Create a comment.
     $edit = array();
     $edit['subject'] = $this->randomName(8);
@@ -121,5 +132,8 @@ function testUserSignature() {
     $this->drupalGet('node/' . $node->id());
     $this->assertNoRaw($signature_text, 'Unfiltered signature text not found.');
     $this->assertRaw(check_markup($signature_text, $this->filtered_html_format->format), 'Filtered signature text found.');
+    // Verify that the user signature's text format's cache tag is present.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertTrue(in_array('filter_format:filtered_html_format', explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags'))));
   }
 }
diff --git a/core/modules/views/src/Plugin/views/area/Text.php b/core/modules/views/src/Plugin/views/area/Text.php
index 86288b0..de89210 100644
--- a/core/modules/views/src/Plugin/views/area/Text.php
+++ b/core/modules/views/src/Plugin/views/area/Text.php
@@ -58,20 +58,13 @@ public function render($empty = FALSE) {
     $format = isset($this->options['format']) ? $this->options['format'] : filter_default_format();
     if (!$empty || !empty($this->options['empty'])) {
       return array(
-        '#markup' => $this->renderTextarea($this->options['content'], $format),
+        '#type' => 'processed_text',
+        '#text' => $this->tokenizeValue($this->options['content']),
+        '#format' => $format,
       );
     }
 
     return array();
   }
 
-  /**
-   * Render a text area, using the proper format.
-   */
-  public function renderTextarea($value, $format) {
-    if ($value) {
-      return check_markup($this->tokenizeValue($value), $format, '', FALSE);
-    }
-  }
-
 }
diff --git a/core/modules/views/src/Plugin/views/field/Markup.php b/core/modules/views/src/Plugin/views/field/Markup.php
index ff43934..465f655 100644
--- a/core/modules/views/src/Plugin/views/field/Markup.php
+++ b/core/modules/views/src/Plugin/views/field/Markup.php
@@ -53,7 +53,7 @@ public function render(ResultRow $values) {
     }
     if ($value) {
       $value = str_replace('<!--break-->', '', $value);
-      return check_markup($value, $format, '');
+      return check_markup($value, $format);
     }
   }
 
diff --git a/core/modules/views/src/Tests/Handler/AreaTextTest.php b/core/modules/views/src/Tests/Handler/AreaTextTest.php
index 3ab29d7..7bc2bc2 100644
--- a/core/modules/views/src/Tests/Handler/AreaTextTest.php
+++ b/core/modules/views/src/Tests/Handler/AreaTextTest.php
@@ -60,16 +60,19 @@ public function testAreaText() {
     $this->executeView($view);
 
     $view->display_handler->handlers['header']['area']->options['format'] = $this->randomString();
-    $this->assertEqual(array('#markup' => ''), $view->display_handler->handlers['header']['area']->render(), 'Nonexistent format should return empty markup.');
+    $build = $view->display_handler->handlers['header']['area']->render();
+    $this->assertEqual('', drupal_render($build), 'Nonexistent format should return empty markup.');
 
     $view->display_handler->handlers['header']['area']->options['format'] = filter_default_format();
-    $this->assertEqual(array('#markup' => check_markup($string)), $view->display_handler->handlers['header']['area']->render(), 'Existant format should return something');
+    $build = $view->display_handler->handlers['header']['area']->render();
+    $this->assertEqual(check_markup($string), drupal_render($build), 'Existent format should return something');
 
     // Empty results, and it shouldn't be displayed .
     $this->assertEqual(array(), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result should lead to no header');
     // Empty results, and it should be displayed.
     $view->display_handler->handlers['header']['area']->options['empty'] = TRUE;
-    $this->assertEqual(array('#markup' => check_markup($string)), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result, but empty enabled lead to a full header');
+    $build = $view->display_handler->handlers['header']['area']->render(TRUE);
+    $this->assertEqual(check_markup($string), drupal_render($build), 'No result, but empty enabled lead to a full header');
   }
 
 }
diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml
index 0a9ad13..810a484 100644
--- a/core/profiles/standard/config/install/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/install/filter.format.basic_html.yml
@@ -4,7 +4,6 @@ status: true
 weight: 0
 roles:
   - authenticated
-cache: true
 filters:
   filter_html:
     id: filter_html
@@ -33,4 +32,13 @@ filters:
     status: true
     weight: 10
     settings: {  }
+  editor_file_reference:
+    id: editor_file_reference
+    provider: editor
+    status: true
+    weight: 11
+    settings: {  }
 langcode: en
+dependencies:
+  module:
+    - editor
diff --git a/core/profiles/standard/config/install/filter.format.full_html.yml b/core/profiles/standard/config/install/filter.format.full_html.yml
index 31b6692..9f5ea79 100644
--- a/core/profiles/standard/config/install/filter.format.full_html.yml
+++ b/core/profiles/standard/config/install/filter.format.full_html.yml
@@ -4,7 +4,6 @@ status: true
 weight: 1
 roles:
   - administrator
-cache: true
 filters:
   filter_caption:
     id: filter_caption
@@ -18,4 +17,13 @@ filters:
     status: true
     weight: 10
     settings: {  }
+  editor_file_reference:
+    id: editor_file_reference
+    provider: editor
+    status: true
+    weight: 11
+    settings: {  }
 langcode: en
+dependencies:
+  module:
+    - editor
diff --git a/core/profiles/standard/config/install/filter.format.restricted_html.yml b/core/profiles/standard/config/install/filter.format.restricted_html.yml
index 12a94aa..95a20e5 100644
--- a/core/profiles/standard/config/install/filter.format.restricted_html.yml
+++ b/core/profiles/standard/config/install/filter.format.restricted_html.yml
@@ -4,7 +4,6 @@ status: true
 weight: 0
 roles:
   - anonymous
-cache: true
 filters:
   filter_html:
     id: filter_html
