 core/modules/ckeditor/ckeditor.module              |   6 +
 .../js/plugins/drupalimagecaption/plugin.js        | 123 ++++++++++--------
 .../Plugin/CKEditorPlugin/DrupalImageCaption.php   |  12 +-
 core/modules/editor/src/Form/EditorImageDialog.php |   6 +-
 core/modules/filter/filter.module                  |   2 +-
 .../filter/src/Plugin/Filter/FilterAlign.php       |  73 +++++++++++
 .../filter/src/Plugin/Filter/FilterCaption.php     |  86 +++++-------
 core/modules/filter/src/Tests/FilterUnitTest.php   | 144 +++++++++++++--------
 .../filter/templates/filter-caption.html.twig      |   5 +-
 .../config/install/filter.format.basic_html.yml    |   6 +
 .../config/install/filter.format.full_html.yml     |   6 +
 11 files changed, 298 insertions(+), 171 deletions(-)

diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 421da85..5cb8ef3 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -58,6 +58,12 @@ function ckeditor_ckeditor_css_alter(array &$css, Editor $editor) {
   // CKEditor DrupalImageCaption plugin.
   if ($editor->getFilterFormat()->filters('filter_caption')->status) {
     $css[] = drupal_get_path('module', 'filter') . '/css/filter.caption.css';
+  }
+
+  // Add the filter caption CSS if the text format associated with this text
+  // editor uses the filter_align filter. This is used by the included
+  // CKEditor DrupalImageCaption plugin.
+  if ($editor->getFilterFormat()->filters('filter_align')->status) {
     $css[] = drupal_get_path('module', 'ckeditor') . '/css/plugins/drupalimagecaption/ckeditor.drupalimagecaption.css';
   }
 }
diff --git a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
index 5ebad71..b49fa00 100644
--- a/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.js
@@ -33,6 +33,10 @@
           return;
         }
 
+        // Only perform the downcasting/upcasting for to the enabled filters.
+        var captionFilterEnabled = editor.config.drupalImageCaption_captionFilterEnabled;
+        var alignFilterEnabled = editor.config.drupalImageCaption_alignFilterEnabled;
+
         // Override default features definitions for drupalimagecaption.
         CKEDITOR.tools.extend(widgetDefinition.features, {
           caption: {
@@ -63,13 +67,17 @@
           var captionHtml = caption && caption.getData();
           var attrs = img.attributes;
 
-          // If image contains a non-empty caption, serialize caption to the
-          // data-caption attribute.
-          if (captionHtml) {
-            attrs['data-caption'] = captionHtml;
+          if (captionFilterEnabled) {
+            // If image contains a non-empty caption, serialize caption to the
+            // data-caption attribute.
+            if (captionHtml) {
+              attrs['data-caption'] = captionHtml;
+            }
           }
-          if (this.data.align !== 'none') {
-            attrs['data-align'] = this.data.align;
+          if (alignFilterEnabled) {
+            if (this.data.align !== 'none') {
+              attrs['data-align'] = this.data.align;
+            }
           }
           attrs['data-editor-file-uuid'] = this.data['data-editor-file-uuid'];
 
@@ -96,60 +104,69 @@
 
           // We won't need the attributes during editing: we'll use widget.data
           // to store them (except the caption, which is stored in the DOM).
-          var caption = attrs['data-caption'];
-          data.align = attrs['data-align'];
+          if (captionFilterEnabled) {
+            var caption = attrs['data-caption'];
+            delete attrs['data-caption'];
+          }
+          if (alignFilterEnabled) {
+            data.align = attrs['data-align'];
+            delete attrs['data-align'];
+          }
           data['data-editor-file-uuid' ] = attrs['data-editor-file-uuid'];
-          delete attrs['data-caption'];
-          delete attrs['data-align'];
           delete attrs['data-editor-file-uuid'];
 
-          // Unwrap from <p> wrapper created by HTML parser for captioned image.
-          // Captioned image will be transformed to <figure>, so we don't want
-          // the <p> anymore.
-          if (element.parent.name === 'p' && caption) {
-            var index = element.getIndex();
-            var splitBefore = index > 0;
-            var splitAfter = index + 1 < element.parent.children.length;
-
-            if (splitBefore) {
-              element.parent.split(index);
+          if (captionFilterEnabled) {
+            // Unwrap from <p> wrapper created by HTML parser for a captioned
+            // image. The captioned image will be transformed to <figure>, so we
+            // don't want the <p> anymore.
+            if (element.parent.name === 'p' && caption) {
+              var index = element.getIndex();
+              var splitBefore = index > 0;
+              var splitAfter = index + 1 < element.parent.children.length;
+
+              if (splitBefore) {
+                element.parent.split(index);
+              }
+              index = element.getIndex();
+              if (splitAfter) {
+                element.parent.split(index + 1);
+              }
+
+              element.parent.replaceWith(element);
+              retElement = element;
             }
-            index = element.getIndex();
-            if (splitAfter) {
-              element.parent.split(index + 1);
-            }
-
-            element.parent.replaceWith(element);
-            retElement = element;
-          }
 
-          // If this image has a caption, create a full <figure> structure.
-          if (caption) {
-            var figure = new CKEDITOR.htmlParser.element('figure');
-            caption = new CKEDITOR.htmlParser.fragment.fromHtml(caption, 'figcaption');
-
-            // Use Drupal's data-placeholder attribute to insert a CSS-based,
-            // translation-ready placeholder for empty captions. Note that it
-            // also must to be done for new instances (see
-            // widgetDefinition._createDialogSaveCallback).
-            caption.attributes['data-placeholder'] = placeholderText;
-
-            element.replaceWith(figure);
-            figure.add(element);
-            figure.add(caption);
-            figure.attributes['class'] = editor.config.image2_captionedClass;
-            retElement = figure;
+            // If this image has a caption, create a full <figure> structure.
+            if (caption) {
+              var figure = new CKEDITOR.htmlParser.element('figure');
+              caption = new CKEDITOR.htmlParser.fragment.fromHtml(caption, 'figcaption');
+
+              // Use Drupal's data-placeholder attribute to insert a CSS-based,
+              // translation-ready placeholder for empty captions. Note that it
+              // also must to be done for new instances (see
+              // widgetDefinition._createDialogSaveCallback).
+              caption.attributes['data-placeholder'] = placeholderText;
+
+              element.replaceWith(figure);
+              figure.add(element);
+              figure.add(caption);
+              figure.attributes['class'] = editor.config.image2_captionedClass;
+              retElement = figure;
+            }
           }
 
-          // If this image doesn't have a caption, but it is centered, make sure
-          // that it's wrapped with <p>, which will become a part of the widget.
-          if (data.align === 'center' && !caption) {
-            var p = new CKEDITOR.htmlParser.element('p');
-            element.replaceWith(p);
-            p.add(element);
-            // Apply the class for centered images.
-            p.addClass(editor.config.image2_alignClasses[1]);
-            retElement = p;
+          if (alignFilterEnabled) {
+            // If this image doesn't have a caption (or the caption filter is
+            // disabled), but it is centered, make sure that it's wrapped with
+            // <p>, which will become a part of the widget.
+            if (data.align === 'center' && (!captionFilterEnabled || !caption)) {
+              var p = new CKEDITOR.htmlParser.element('p');
+              element.replaceWith(p);
+              p.add(element);
+              // Apply the class for centered images.
+              p.addClass(editor.config.image2_alignClasses[1]);
+              retElement = p;
+            }
           }
 
           // Return the upcasted element (<img>, <figure> or <p>).
diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
index 5c2fffa..7ca296e 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
@@ -57,10 +57,15 @@ public function getFile() {
    * {@inheritdoc}
    */
   public function getConfig(Editor $editor) {
+    $format = $editor->getFilterFormat();
     return array(
       'image2_captionedClass' => 'caption caption-img',
       'image2_alignClasses' => array('align-left', 'align-center', 'align-right'),
       'drupalImageCaption_captionPlaceholderText' => t('Enter caption here'),
+      // Only enable those parts of DrupalImageCaption for which the
+      // corresponding Drupal text filters are enabled.
+      'drupalImageCaption_captionFilterEnabled' => $format->filters('filter_caption')->status,
+      'drupalImageCaption_alignFilterEnabled' => $format->filters('filter_align')->status,
     );
   }
 
@@ -73,9 +78,10 @@ function isEnabled(Editor $editor) {
     }
 
     // Automatically enable this plugin if the text format associated with this
-    // text editor uses the filter_caption filter and the DrupalImage button is
-    // enabled.
-    if ($editor->getFilterFormat()->filters('filter_caption')->status) {
+    // text editor uses the filter_align or filter_caption filter and the
+    // DrupalImage button is enabled.
+    $format = $editor->getFilterFormat();
+    if ($format->filters('filter_align')->status || $format->filters('filter_caption')->status) {
       $enabled = FALSE;
       $settings = $editor->getSettings();
       foreach ($settings['toolbar']['rows'] as $row) {
diff --git a/core/modules/editor/src/Form/EditorImageDialog.php b/core/modules/editor/src/Form/EditorImageDialog.php
index f8c45e9..cfe096c 100644
--- a/core/modules/editor/src/Form/EditorImageDialog.php
+++ b/core/modules/editor/src/Form/EditorImageDialog.php
@@ -135,9 +135,9 @@ public function buildForm(array $form, FormStateInterface $form_state, FilterFor
       '#parents' => array('attributes', 'height'),
     );
 
-    // When Drupal core's filter_caption is being used, the text editor may
+    // When Drupal core's filter_align is being used, the text editor may
     // offer the ability to change the alignment.
-    if (isset($image_element['data-align'])) {
+    if (isset($image_element['data-align']) && $filter_format->filters('filter_align')->status) {
       $form['align'] = array(
         '#title' => $this->t('Align'),
         '#type' => 'radios',
@@ -156,7 +156,7 @@ public function buildForm(array $form, FormStateInterface $form_state, FilterFor
 
     // When Drupal core's filter_caption is being used, the text editor may
     // offer the ability to in-place edit the image's caption: show a toggle.
-    if (isset($image_element['hasCaption'])) {
+    if (isset($image_element['hasCaption']) && $filter_format->filters('filter_caption')->status) {
       $form['caption'] = array(
         '#title' => $this->t('Caption'),
         '#type' => 'checkbox',
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index 204c0cb..82e0ffc 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -80,7 +80,7 @@ function filter_theme() {
         'node' => NULL,
         'tag' => NULL,
         'caption' => NULL,
-        'align' => NULL,
+        'classes' => NULL,
       ),
       'template' => 'filter-caption',
     )
diff --git a/core/modules/filter/src/Plugin/Filter/FilterAlign.php b/core/modules/filter/src/Plugin/Filter/FilterAlign.php
new file mode 100644
index 0000000..168d0d6
--- /dev/null
+++ b/core/modules/filter/src/Plugin/Filter/FilterAlign.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Plugin\Filter\FilterAlign.
+ */
+
+namespace Drupal\filter\Plugin\Filter;
+
+use Drupal\Component\Utility\Html;
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+
+/**
+ * Provides a filter to align elements.
+ *
+ * @Filter(
+ *   id = "filter_align",
+ *   title = @Translation("Align images"),
+ *   description = @Translation("Uses a <code>data-align</code> attribute on <code>&lt;img&gt;</code> tags to align images."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class FilterAlign extends FilterBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+
+    if (stristr($text, 'data-align') !== FALSE) {
+      $dom = Html::load($text);
+      $xpath = new \DOMXPath($dom);
+      foreach ($xpath->query('//*[@data-align]') as $node) {
+        // Read the data-align attribute's value, then delete it.
+        $align = $node->getAttribute('data-align');
+        $node->removeAttribute('data-align');
+
+        // If one of the allowed alignments, add the corresponding class.
+        if (in_array($align, array('left', 'center', 'right'))) {
+          $classes = $node->getAttribute('class');
+          $classes = (strlen($classes) > 0) ? explode(' ', $classes) : array();
+          $classes[] = 'align-' . $align;
+          $node->setAttribute('class', implode(' ', $classes));
+        }
+      }
+      $result->setProcessedText(Html::serialize($dom));
+    }
+
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tips($long = FALSE) {
+    if ($long) {
+      return $this->t('
+        <p>You can align images, videos, blockquotes and so on to the left, right or center. Examples:</p>
+        <ul>
+          <li>Align an image to the left: <code>&lt;img src="" data-align="left" /&gt;</code></li>
+          <li>Align an image to the center: <code>&lt;img src="" data-align="center" /&gt;</code></li>
+          <li>Align an image to the right: <code>&lt;img src="" data-align="right" /&gt;</code></li>
+          <li>… and you can apply this to other elements as well: <code>&lt;video src="" data-align="center" /&gt;</code></li>
+        </ul>');
+    }
+    else {
+      return $this->t('You can align images (<code>data-align="center"</code>code>), but also videos, blockquotes, and so on.');
+    }
+  }
+
+}
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index c283908..5b74f43 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -16,12 +16,14 @@
 use Drupal\filter\Plugin\FilterBase;
 
 /**
- * Provides a filter to display image captions and align images.
+ * Provides a filter to caption elements.
+ *
+ * When used in combination with the filter_align filter, this must run last.
  *
  * @Filter(
  *   id = "filter_caption",
- *   title = @Translation("Display image captions and align images"),
- *   description = @Translation("Uses data-caption and data-align attributes on &lt;img&gt; tags to caption and align images."),
+ *   title = @Translation("Caption images"),
+ *   description = @Translation("Uses a <code>data-caption</code> attribute on <code>&lt;img&gt;</code> tags to caption images."),
  *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
  * )
  */
@@ -33,60 +35,35 @@ class FilterCaption extends FilterBase {
   public function process($text, $langcode) {
     $result = new FilterProcessResult($text);
 
-    if (stristr($text, 'data-caption') !== FALSE || stristr($text, 'data-align') !== FALSE) {
-      $caption_found = FALSE;
+    if (stristr($text, 'data-caption') !== FALSE) {
       $dom = Html::load($text);
       $xpath = new \DOMXPath($dom);
-      foreach ($xpath->query('//*[@data-caption or @data-align]') as $node) {
-        $caption = NULL;
-        $align = NULL;
+      foreach ($xpath->query('//*[@data-caption]') as $node) {
+        // Read the data-caption attribute's value, then delete it.
+        $caption = String::checkPlain($node->getAttribute('data-caption'));
+        $node->removeAttribute('data-caption');
 
-        // Retrieve, then remove the data-caption and data-align attributes.
-        if ($node->hasAttribute('data-caption')) {
-          $caption = String::checkPlain($node->getAttribute('data-caption'));
-          $node->removeAttribute('data-caption');
-          // Sanitize caption: decode HTML encoding, limit allowed HTML tags;
-          // only allow inline tags that are allowed by default, plus <br>.
-          $caption = String::decodeEntities($caption);
-          $caption = Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br'));
-          // The caption must be non-empty.
-          if (Unicode::strlen($caption) === 0) {
-            $caption = NULL;
-          }
-        }
-        if ($node->hasAttribute('data-align')) {
-          $align = $node->getAttribute('data-align');
-          $node->removeAttribute('data-align');
-          // Only allow 3 values: 'left', 'center' and 'right'.
-          if (!in_array($align, array('left', 'center', 'right'))) {
-            $align = NULL;
-          }
-        }
+        // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only
+        // allow inline tags that are allowed by default, plus <br>.
+        $caption = String::decodeEntities($caption);
+        $caption = Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br'));
 
-        // Don't transform the HTML if there isn't a caption after validation.
-        if ($caption === NULL) {
-          // If there is a valid alignment, then transform the data-align
-          // attribute to a corresponding alignment class.
-          if ($align !== NULL) {
-            $classes = $node->getAttribute('class');
-            $classes = (strlen($classes) > 0) ? explode(' ', $classes) : array();
-            $classes[] = 'align-' . $align;
-            $node->setAttribute('class', implode(' ', $classes));
-          }
+        // The caption must be non-empty.
+        if (Unicode::strlen($caption) === 0) {
           continue;
         }
-        else {
-          $caption_found = TRUE;
-        }
 
-        // Given the updated node, caption and alignment: re-render it with a
-        // caption.
+        // Given the updated node and caption: re-render it with a caption, but
+        // bubble up the value of the class attribute of the captioned element,
+        // this allows it to collaborate with e.g. the filter_align filter.
+        $classes = $node->getAttribute('class');
+        $node->removeAttribute('class');
         $filter_caption = array(
           '#theme' => 'filter_caption',
           '#node' => SafeMarkup::set($node->C14N()),
           '#tag' => $node->tagName,
           '#caption' => $caption,
-          '#align' => $align,
+          '#classes' => $classes,
         );
         $altered_html = drupal_render($filter_caption);
 
@@ -103,15 +80,12 @@ public function process($text, $langcode) {
         $node->parentNode->replaceChild($updated_node, $node);
       }
 
-      $result->setProcessedText(Html::serialize($dom));
-
-      if ($caption_found) {
-        $result->addAssets(array(
+      $result->setProcessedText(Html::serialize($dom))
+        ->addAssets(array(
           'library' => array(
             'filter/caption',
           ),
         ));
-      }
     }
 
     return $result;
@@ -123,15 +97,17 @@ public function process($text, $langcode) {
   public function tips($long = FALSE) {
     if ($long) {
       return $this->t('
-        <p>You can add image captions and align images left, right or centered. Examples:</p>
+        <p>You can caption images, videos, blockquotes, and so on. Examples:</p>
         <ul>
-          <li>Caption an image: <code>&lt;img src="" data-caption="This is a caption" /&gt;</code></li>
-          <li>Align an image: <code>&lt;img src="" data-align="center" /&gt;</code></li>
-          <li>Caption & align an image: <code>&lt;img src="" data-caption="Alpaca" data-align="right" /&gt;</code></li>
+            <li><code>&lt;img src="" data-caption="This is a caption" /&gt;</code></li>
+            <li><code>&lt;video src="" data-caption="The Drupal Dance" /&gt;</code></li>
+            <li><code>&lt;blockquote data-caption="Dries Buytaert"&gt;Drupal is awesome!&lt;/blockquote&gt;</code></li>
+            <li><code>&lt;code data-caption="Hello world in JavaScript."&gt;alert("Hello world!");&lt;/code&gt;</code></li>
         </ul>');
     }
     else {
-      return $this->t('You can caption (data-caption="Text") and align images (data-align="center"), but also video, blockquotes, and so on.');
+      return $this->t('You can caption images (<code>data-caption="Text"<code>), but also videos, blockquotes, and so on.');
     }
   }
+
 }
diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php
index aa74bcc..b17c800 100644
--- a/core/modules/filter/src/Tests/FilterUnitTest.php
+++ b/core/modules/filter/src/Tests/FilterUnitTest.php
@@ -41,6 +41,61 @@ protected function setUp() {
   }
 
   /**
+   * Tests the align filter.
+   */
+  function testAlignFilter() {
+    $filter = $this->filters['filter_align'];
+
+    $test = function($input) use ($filter) {
+      return $filter->process($input, 'und');
+    };
+
+    // No data-align attribute.
+    $input = '<img src="llama.jpg" />';
+    $expected = $input;
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
+
+    // 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)->getProcessedText());
+    $input = '<img src="llama.jpg" data-align="center" />';
+    $expected = '<img src="llama.jpg" class="align-center" />';
+    $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)->getProcessedText());
+
+    // 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)->getProcessedText());
+
+    // Empty data-align attribute.
+    $input = '<img src="llama.jpg" data-align="" />';
+    $expected = '<img src="llama.jpg" />';
+    $this->assertIdentical($expected, $test($input)->getProcessedText());
+
+    // Ensure the filter also works with uncommon yet valid attribute quoting.
+    $input = '<img src=llama.jpg data-align=right />';
+    $expected = '<img src="llama.jpg" class="align-right" />';
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+
+    // Security test: attempt to inject an additional class.
+    $input = '<img src="llama.jpg" data-align="center another-class-here" />';
+    $expected = '<img src="llama.jpg" />';
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+
+    // Security test: attempt an XSS.
+    $input = '<img src="llama.jpg" data-align="center \'onclick=\'alert(foo);" />';
+    $expected = '<img src="llama.jpg" />';
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+  }
+
+  /**
    * Tests the caption filter.
    */
   function testCaptionFilter() {
@@ -56,12 +111,12 @@ function testCaptionFilter() {
       ),
     );
 
-    // No data-caption nor data-align attributes.
+    // No data-caption attribute.
     $input = '<img src="llama.jpg" />';
     $expected = $input;
     $this->assertIdentical($expected, $test($input)->getProcessedText());
 
-    // Only data-caption attribute.
+    // 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>';
     $output = $test($input);
@@ -103,26 +158,42 @@ function testCaptionFilter() {
     $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)->getProcessedText());
-    $input = '<img src="llama.jpg" data-align="center" />';
-    $expected = '<img src="llama.jpg" class="align-center" />';
-    $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)->getProcessedText());
+    // Ensure the filter also works with uncommon yet valid attribute quoting.
+    $input = '<img src=llama.jpg data-caption=\'Loquacious llama!\' />';
+    $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>';
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
 
-    // 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)->getProcessedText());
+    // 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>';
+    $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>';
+    $output = $test($input);
+    $this->assertIdentical($expected, $output->getProcessedText());
+    $this->assertIdentical($attached_library, $output->getAssets());
+  }
 
-    // Empty data-align attribute.
-    $input = '<img src="llama.jpg" data-align="" />';
-    $expected = '<img src="llama.jpg" />';
-    $this->assertIdentical($expected, $test($input)->getProcessedText());
+  /**
+   * Tests the combination of the align and caption filters.
+   */
+  function testAlignAndCaptionFilters() {
+    $align_filter = $this->filters['filter_align'];
+    $caption_filter = $this->filters['filter_caption'];
+
+    $test = function($input) use ($align_filter, $caption_filter) {
+      return $caption_filter->process($align_filter->process($input, 'und'), 'und');
+    };
+
+    $attached_library = array(
+      'library' => array(
+        'filter/caption',
+      ),
+    );
 
     // Both data-caption and data-align attributes: all 3 allowed values for the
     // data-align attribute.
@@ -149,39 +220,6 @@ function testCaptionFilter() {
     $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>';
-    $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>';
-    $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>';
-    $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>';
-    $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>';
-    $output = $test($input);
-    $this->assertIdentical($expected, $output->getProcessedText());
-    $this->assertIdentical($attached_library, $output->getAssets());
   }
 
   /**
diff --git a/core/modules/filter/templates/filter-caption.html.twig b/core/modules/filter/templates/filter-caption.html.twig
index 40f0538..eb9d248 100644
--- a/core/modules/filter/templates/filter-caption.html.twig
+++ b/core/modules/filter/templates/filter-caption.html.twig
@@ -9,11 +9,10 @@
  * - string node: The complete HTML tag whose contents are being captioned.
  * - string tag: The name of the HTML tag whose contents are being captioned.
  * - string caption: The caption text.
- * - string|NULL align: (optional) The alignment: 'left', 'center', 'right' or
- *   NULL.
+ * - string classes: The classes of the captioned HTML tag.
  */
 #}
-<figure class="caption caption-{{ tag }} {%- if align %} align-{{ align }} {%- endif %}">
+<figure class="caption caption-{{ tag }}{%- if classes %} {{ classes }}{%- endif %}">
 {{ node }}
 <figcaption>{{ caption }}</figcaption>
 </figure>
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 810a484..41e9a8c 100644
--- a/core/profiles/standard/config/install/filter.format.basic_html.yml
+++ b/core/profiles/standard/config/install/filter.format.basic_html.yml
@@ -14,6 +14,12 @@ filters:
       allowed_html: '<a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h4> <h5> <h6> <p> <span> <img>'
       filter_html_help: false
       filter_html_nofollow: false
+  filter_align:
+    id: filter_align
+    provider: filter
+    status: true
+    weight: 7
+    settings: {  }
   filter_caption:
     id: filter_caption
     provider: filter
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 9f5ea79..cfdf9e2 100644
--- a/core/profiles/standard/config/install/filter.format.full_html.yml
+++ b/core/profiles/standard/config/install/filter.format.full_html.yml
@@ -5,6 +5,12 @@ weight: 1
 roles:
   - administrator
 filters:
+  filter_align:
+    id: filter_align
+    provider: filter
+    status: true
+    weight: 8
+    settings: {  }
   filter_caption:
     id: filter_caption
     provider: filter
