 .../Plugin/CKEditorPlugin/DrupalImageCaption.php   |   7 +-
 core/modules/editor/src/Form/EditorImageDialog.php |   6 +-
 core/modules/filter/filter.module                  |   2 +-
 .../filter/src/Plugin/Filter/FilterCaption.php     |  84 ++++--------
 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 +
 8 files changed, 140 insertions(+), 120 deletions(-)

diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
index 5c2fffa..6feb593 100644
--- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
+++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/DrupalImageCaption.php
@@ -73,9 +73,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 893b5a4..b918bf8 100644
--- a/core/modules/editor/src/Form/EditorImageDialog.php
+++ b/core/modules/editor/src/Form/EditorImageDialog.php
@@ -132,9 +132,9 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
       '#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',
@@ -153,7 +153,7 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
 
     // 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 8eb3373..ea14830 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/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index bccaf02..475cdbb 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -15,12 +15,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
  * )
  */
@@ -32,60 +34,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' => $node->C14N(),
           '#tag' => $node->tagName,
           '#caption' => $caption,
-          '#align' => $align,
+          '#classes' => $classes,
         );
         $altered_html = drupal_render($filter_caption);
 
@@ -102,15 +79,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;
@@ -122,15 +96,11 @@ 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>
-        <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>
-        </ul>');
+        <p>You can add caption images. Example: <code>&lt;img src="" data-caption="This is a caption" /&gt;</code>.</p>');
     }
     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 video, blockquotes, and so on.');
     }
   }
+
 }
diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php
index b15f0a0..e67a76e 100644
--- a/core/modules/filter/src/Tests/FilterUnitTest.php
+++ b/core/modules/filter/src/Tests/FilterUnitTest.php
@@ -46,6 +46,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() {
@@ -61,12 +116,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);
@@ -108,26 +163,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.
@@ -154,39 +225,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 0c73b9f..b3f6fd1 100644
--- a/core/modules/filter/templates/filter-caption.html.twig
+++ b/core/modules/filter/templates/filter-caption.html.twig
@@ -6,11 +6,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
