diff --git a/core/modules/filter/filter.api.php b/core/modules/filter/filter.api.php
index 8d6aa01a86b3b5c29ec33101d5bb1884dc9f1b7e..7f2c132900e61b91bb6ac833d5f90b21e57dc85d 100644
--- a/core/modules/filter/filter.api.php
+++ b/core/modules/filter/filter.api.php
@@ -23,6 +23,28 @@ function hook_filter_info_alter(&$info) {
   );
 }
 
+/**
+ * Alters images with an invalid source.
+ *
+ * When the 'Restrict images to this site' filter is enabled, any images that
+ * are not hosted on the site will be passed through this hook, most commonly to
+ * replace the invalid image with an error indicator.
+ *
+ * @param DOMElement $image
+ *   An IMG node to format, parsed from the filtered text.
+ */
+function hook_filter_secure_image_alter(&$image) {
+  // Turn an invalid image into an error indicator.
+  $image->setAttribute('src', base_path() . 'core/misc/message-16-error.png');
+  $image->setAttribute('alt', t('Image removed.'));
+  $image->setAttribute('title', t('This image has been removed. For security reasons, only images from the local domain are allowed.'));
+
+  // Add a CSS class to aid in styling.
+  $class = ($image->getAttribute('class') ? trim($image->getAttribute('class')) . ' ' : '');
+  $class .= 'filter-image-invalid';
+  $image->setAttribute('class', $class);
+}
+
 /**
  * Perform actions when a text format has been disabled.
  *
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index ecd4e58fc8e45d8e4826959db04312a583c05453..7d7efbec756a2a6b0a10518dadc93d4de8192ccc 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -81,16 +81,15 @@ function filter_theme() {
   return array(
     'filter_tips' => array(
       'variables' => array('tips' => NULL, 'long' => FALSE),
-      'file' => 'filter.pages.inc',
+      'template' => 'filter-tips',
     ),
     'text_format_wrapper' => array(
-      'render element' => 'element',
+      'variables' => array('children' => NULL, 'description' => NULL),
+      'template' => 'text-format-wrapper',
     ),
     'filter_guidelines' => array(
       'variables' => array('format' => NULL),
-    ),
-    'filter_html_image_secure_image' => array(
-      'variables' => array('image' => NULL),
+      'template' => 'filter-guidelines',
     ),
     'filter_caption' => array(
       'variables' => array(
@@ -831,27 +830,6 @@ function filter_form_access_denied($element) {
   return $element;
 }
 
-/**
- * Returns HTML for a text format-enabled form element.
- *
- * @param array $variables
- *   An associative array containing:
- *   - element: A render element containing #children and #description.
- *
- * @ingroup themeable
- */
-function theme_text_format_wrapper($variables) {
-  $element = $variables['element'];
-  $output = '<div class="text-format-wrapper form-item">';
-  $output .= $element['#children'];
-  if (!empty($element['#description'])) {
-    $output .= '<div class="description">' . $element['#description'] . '</div>';
-  }
-  $output .= "</div>\n";
-
-  return $output;
-}
-
 /**
  * Retrieves the filter tips.
  *
@@ -992,27 +970,77 @@ function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element,
 }
 
 /**
- * Returns HTML for guidelines for a text format.
+ * Prepares variables for text format guideline templates.
+ *
+ * Default template: filter-guidelines.html.twig.
  *
  * @param array $variables
  *   An associative array containing:
  *   - format: An object representing a text format.
- *
- * @ingroup themeable
  */
-function theme_filter_guidelines($variables) {
+function template_preprocess_filter_guidelines(&$variables) {
   $format = $variables['format'];
-  $attributes['class'][] = 'filter-guidelines-item';
-  $attributes['class'][] = 'filter-guidelines-' . $format->format;
-  $output = '<div' . new Attribute($attributes) . '>';
-  $output .= '<h4 class="label">' . check_plain($format->name) . '</h4>';
-  $filter_tips = array(
+  $variables['attributes']['class'][] = 'filter-guidelines-item';
+  $variables['attributes']['class'][] = 'filter-guidelines-' . $format->format;
+
+  $variables['tips'] = array(
     '#theme' => 'filter_tips',
     '#tips' => _filter_tips($format->format, FALSE),
   );
-  $output .= drupal_render($filter_tips);
-  $output .= '</div>';
-  return $output;
+}
+
+/**
+ * Prepares variables for filter tips templates.
+ *
+ * Default template: filter-tips.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - tips: An array containing descriptions and a CSS ID in the form of
+ *     'module-name/filter-id' (only used when $long is TRUE) for each
+ *     filter in one or more text formats. Example:
+ *     @code
+ *       array(
+ *         'Full HTML' => array(
+ *           0 => array(
+ *             'tip' => 'Web page addresses and e-mail addresses turn into links automatically.',
+ *             'id' => 'filter/2',
+ *           ),
+ *         ),
+ *       );
+ *     @endcode
+ *   - long: (optional) Whether the passed-in filter tips contain extended
+ *     explanations, i.e. intended to be output on the path 'filter/tips'
+ *     (TRUE), or are in a short format, i.e. suitable to be displayed below a
+ *     form element. Defaults to FALSE.
+ */
+function template_preprocess_filter_tips(&$variables) {
+  $tips = $variables['tips'];
+  $long = $variables['long'];
+
+  foreach ($variables['tips'] as $name => $tiplist) {
+    foreach ($tiplist as $tip_key => $tip) {
+      $tiplist[$tip_key]['attributes'] = new Attribute(array());
+      if ($long) {
+        $tiplist[$tip_key]['attributes']['class'] = array('filter-' . str_replace("/", "-", $tip['id']));
+      }
+    }
+
+    $attributes = array(
+      'class' => array(
+        'filter-type',
+        'filter-' . drupal_html_class($name),
+      ),
+    );
+
+    $variables['tips'][$name] = array(
+      'attributes' => new Attribute($attributes),
+      'name' => String::checkPlain($name),
+      'list' => $tiplist,
+    );
+  }
+
+  $variables['multiple'] = count($tips) > 1;
 }
 
 /**
@@ -1392,32 +1420,31 @@ function _filter_html_image_secure_process($text) {
         continue;
       }
     }
-    // Replace an invalid image with an error indicator.
-    $filter_html_image_secure_image = array(
-      '#theme' => 'filter_html_image_secure_image',
-      '#image' => $image,
-    );
-    drupal_render($filter_html_image_secure_image);
+    // Allow modules and themes to replace an invalid image with an error
+    // indicator. See filter_filter_secure_image_alter().
+    \Drupal::moduleHandler()->alter('filter_secure_image', $image);
   }
   $text = filter_dom_serialize($html_dom);
   return $text;
 }
 
 /**
+ * Implements hook_filter_secure_image_alter().
+ *
  * Formats an image DOM element that has an invalid source.
  *
  * @param DOMElement $image
  *   An IMG node to format, parsed from the filtered text.
  *
- * @return void
- *   Unlike other theme functions, the passed in $image is altered by reference.
- *
  * @see _filter_html_image_secure_process()
- * @ingroup themeable
  */
-function theme_filter_html_image_secure_image(&$variables) {
-  $image = $variables['image'];
-
+function filter_filter_secure_image_alter(&$image) {
+  t('hey hey');
+  t('hey hey');
+  t('hey hey');
+  t('hey hey');
+  t('hey hey');
+  t('hey hey');
   // Turn an invalid image into an error indicator.
   $image->setAttribute('src', base_path() . 'core/misc/message-16-error.png');
   $image->setAttribute('alt', t('Image removed.'));
diff --git a/core/modules/filter/filter.pages.inc b/core/modules/filter/filter.pages.inc
deleted file mode 100644
index 8f2e2984b89db98eaecd9d8c9ab68ba9d0dddf2e..0000000000000000000000000000000000000000
--- a/core/modules/filter/filter.pages.inc
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * @file
- * User page callbacks for the Filter module.
- */
-
-/**
- * Returns HTML for a set of filter tips.
- *
- * @param array $variables
- *   An associative array containing:
- *   - tips: An array containing descriptions and a CSS ID in the form of
- *     'module-name/filter-id' (only used when $long is TRUE) for each
- *     filter in one or more text formats. Example:
- *     @code
- *       array(
- *         'Full HTML' => array(
- *           0 => array(
- *             'tip' => 'Web page addresses and e-mail addresses turn into links automatically.',
- *             'id' => 'filter/2',
- *           ),
- *         ),
- *       );
- *     @endcode
- *   - long: (optional) Whether the passed-in filter tips contain extended
- *     explanations, i.e. intended to be output on the path 'filter/tips'
- *     (TRUE), or are in a short format, i.e. suitable to be displayed below a
- *     form element. Defaults to FALSE.
- *
- * @see _filter_tips()
- * @ingroup themeable
- */
-function theme_filter_tips($variables) {
-  $tips = $variables['tips'];
-  $long = $variables['long'];
-  $output = '';
-
-  $multiple = count($tips) > 1;
-  if ($multiple) {
-    $output = '<h2>' . t('Text Formats') . '</h2>';
-  }
-
-  if (count($tips)) {
-    if ($multiple) {
-      $output .= '<div class="compose-tips">';
-    }
-    foreach ($tips as $name => $tiplist) {
-      if ($multiple) {
-        $output .= '<div class="filter-type filter-' . drupal_html_class($name) . '">';
-        $output .= '<h3>' . $name . '</h3>';
-      }
-
-      if (count($tiplist) > 0) {
-        $output .= '<ul class="tips">';
-        foreach ($tiplist as $tip) {
-          $output .= '<li' . ($long ? ' class="filter-' . str_replace("/", "-", $tip['id']) . '">' : '>') . $tip['tip'] . '</li>';
-        }
-        $output .= '</ul>';
-      }
-
-      if ($multiple) {
-        $output .= '</div>';
-      }
-    }
-    if ($multiple) {
-      $output .= '</div>';
-    }
-  }
-
-  return $output;
-}
diff --git a/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
index ca4e733405b77feb1caa1131e8b2da754f4208a0..ae8bc0153f208af5640c9dfb58384cf36752015f 100644
--- a/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
+++ b/core/modules/filter/lib/Drupal/filter/Controller/FilterController.php
@@ -23,7 +23,7 @@ class FilterController {
    * @return array
    *   A renderable array.
    *
-   * @see theme_filter_tips()
+   * @see template_preprocess_filter_tips()
    */
   function filterTips(FilterFormatInterface $filter_format = NULL) {
     $tips = $filter_format ? $filter_format->format : -1;
diff --git a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
index 40156e4c385b342e7cd074d0206aa177494a53a3..1d0df9e7c71bb4e64227deea1c2aa6ff9248b86f 100644
--- a/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
+++ b/core/modules/filter/lib/Drupal/filter/Plugin/FilterInterface.php
@@ -262,7 +262,7 @@ public function getHTMLRestrictions();
    * @param bool $long
    *   Whether this callback should return a short tip to display in a form
    *   (FALSE), or whether a more elaborate filter tips should be returned for
-   *   theme_filter_tips() (TRUE).
+   *   template_preprocess_filter_tips() (TRUE).
    *
    * @return string|null
    *   Translated text to display as a tip, or NULL if this filter has no tip.
diff --git a/core/modules/filter/templates/filter-guidelines.html.twig b/core/modules/filter/templates/filter-guidelines.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..88a3b472a67b4a4014fcfd456ca7b69226130281
--- /dev/null
+++ b/core/modules/filter/templates/filter-guidelines.html.twig
@@ -0,0 +1,25 @@
+{#
+/**
+ * @file
+ * Default theme implementation for guidelines for a text format.
+ *
+ * Available variables:
+ * - format: Contains information about the current text format, including the
+ *   following:
+ *   - name: The name of the text format, potentially unsafe and needs to be
+ *     escaped.
+ *   - format: The machine name of the text format, e.g. 'basic_html'.
+ * - attributes: HTML attributes for the containing element.
+ * - tips: Descriptions and a CSS ID in the form of 'module-name/filter-id'
+ *   (only used when 'long' is TRUE) for each filter in one or more text
+ *   formats.
+ *
+ * @see template_preprocess_filter_tips()
+ *
+ * @ingroup themeable
+ */
+#}
+<div{{ attributes }}>
+  <h4 class="label">{{ format.name|escape }}</h4>
+  {{ tips }}
+</div>
diff --git a/core/modules/filter/templates/filter-tips.html.twig b/core/modules/filter/templates/filter-tips.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..d8f70cb7704231ea149738020e925e778bde7315
--- /dev/null
+++ b/core/modules/filter/templates/filter-tips.html.twig
@@ -0,0 +1,52 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a set of filter tips.
+ *
+ * Available variables:
+ * - tips: Descriptions and a CSS ID in the form of 'module-name/filter-id'
+ *   (only used when 'long' is TRUE) for each filter in one or more text
+ *   formats.
+ * - long: A flag indicating whether the passed-in filter tips contain extended
+ *   explanations, i.e. intended to be output on the path 'filter/tips'
+ *   (TRUE), or are in a short format, i.e. suitable to be displayed below a
+ *   form element. Defaults to FALSE.
+ * - multiple: A flag indicating there is more than one filter tip.
+ *
+ * @see template_preprocess_filter_tips()
+ *
+ * @ingroup themeable
+ */
+#}
+{% if multiple %}
+  <h2>{{ 'Text Formats'|t }}</h2>
+{% endif %}
+
+{% if tips|length %}
+  {% if multiple %}
+    <div class="compose-tips">
+  {% endif %}
+
+  {% for tip in tips %}
+    {% if multiple %}
+      <div{{ tip.attributes }}>
+      <h3>{{ tip.name }}</h3>
+    {% endif %}
+
+    {% if tip.list|length %}
+      <ul class="tips">
+      {% for item in tip.list %}
+        <li{{ item.attributes }}>{{ item.tip }}</li>
+      {% endfor %}
+      </ul>
+    {% endif %}
+
+    {% if multiple %}
+      </div>
+    {% endif %}
+  {% endfor %}
+
+  {% if multiple %}
+    </div>
+  {% endif %}
+{% endif %}
diff --git a/core/modules/filter/templates/text-format-wrapper.html.twig b/core/modules/filter/templates/text-format-wrapper.html.twig
new file mode 100644
index 0000000000000000000000000000000000000000..f45397157bb9cc455ac64e5de4e9431a5dceb3e0
--- /dev/null
+++ b/core/modules/filter/templates/text-format-wrapper.html.twig
@@ -0,0 +1,18 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a text format-enabled form element.
+ *
+ * Available variables:
+ * - children: Text format element children.
+ * - description: Text format element description.
+ *
+ * @ingroup themeable
+ */
+#}
+<div class="text-format-wrapper form-item">
+  {{ children }}
+  {% if description %}
+    <div class="description">{{ description }}</div>
+  {% endif %}
+</div>