diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 2595dd0..1ed3bf1 100644
--- a/core/includes/theme.inc
+++ b/core/includes/theme.inc
@@ -1254,6 +1254,10 @@ function template_preprocess_html(&$variables) {
 
   $site_config = \Drupal::config('system.site');
   // Construct page title.
+  if (isset($variables['page']['#title']) && is_array($variables['page']['#title'])) {
+    // Do an early render if the title is a render array.
+    $variables['page']['#title'] = (string) \Drupal::service('renderer')->render($variables['page']['#title']);
+  }
   if (!empty($variables['page']['#title'])) {
     $head_title = array(
       'title' => trim(strip_tags($variables['page']['#title'])),
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index af9170d..28b53e0 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -15,9 +15,9 @@
  * provides a store for known safe strings and methods to manage them
  * throughout the page request.
  *
- * Strings sanitized by self::checkPlain() and self::escape() or
- * self::xssFilter() are automatically marked safe, as are markup strings
- * created from @link theme_render render arrays @endlink via drupal_render().
+ * Strings sanitized by self::checkPlain() and self::escape() are automatically
+ * marked safe, as are markup strings created from @link theme_render render
+ * arrays @endlink via drupal_render().
  *
  * This class should be limited to internal use only. Module developers should
  * instead use the appropriate
@@ -139,60 +139,6 @@ public static function escape($string) {
   }
 
   /**
-   * Filters HTML for XSS vulnerabilities and marks the result as safe.
-   *
-   * Calling this method unnecessarily will result in bloating the safe string
-   * list and increases the chance of unintended side effects.
-   *
-   * If Twig receives a value that is not marked as safe then it will
-   * automatically encode special characters in a plain-text string for display
-   * as HTML. Therefore, SafeMarkup::xssFilter() should only be used when the
-   * string might contain HTML that needs to be rendered properly by the
-   * browser.
-   *
-   * If you need to filter for admin use, like Xss::filterAdmin(), then:
-   * - If the string is used as part of a @link theme_render render array @endlink,
-   *   use #markup to allow the render system to filter by the admin tag list
-   *   automatically.
-   * - Otherwise, use the SafeMarkup::xssFilter() with tag list provided by
-   *   Xss::getAdminTagList() instead.
-   *
-   * This method should only be used instead of Xss::filter() when the result is
-   * being added to a render array that is constructed before rendering begins.
-   *
-   * In the rare instance that the caller does not want to filter strings that
-   * are marked safe already, it needs to check SafeMarkup::isSafe() itself.
-   *
-   * @param $string
-   *   The string with raw HTML in it. It will be stripped of everything that
-   *   can cause an XSS attack. The string provided will always be escaped
-   *   regardless of whether the string is already marked as safe.
-   * @param array $html_tags
-   *   (optional) An array of HTML tags. If omitted, it uses the default tag
-   *   list defined by \Drupal\Component\Utility\Xss::filter().
-   *
-   * @return string
-   *   An XSS-safe version of $string, or an empty string if $string is not
-   *   valid UTF-8. The string is marked as safe.
-   *
-   * @ingroup sanitization
-   *
-   * @see \Drupal\Component\Utility\Xss::filter()
-   * @see \Drupal\Component\Utility\Xss::filterAdmin()
-   * @see \Drupal\Component\Utility\Xss::getAdminTagList()
-   * @see \Drupal\Component\Utility\SafeMarkup::isSafe()
-   */
-  public static function xssFilter($string, $html_tags = NULL) {
-    if (is_null($html_tags)) {
-      $string = Xss::filter($string);
-    }
-    else {
-      $string = Xss::filter($string, $html_tags);
-    }
-    return static::set($string);
-  }
-
-  /**
   * Gets all strings currently marked as safe.
   *
   * This is useful for the batch and form APIs, where it is important to
diff --git a/core/lib/Drupal/Component/Utility/Xss.php b/core/lib/Drupal/Component/Utility/Xss.php
index a68ca2b..17e3d4d 100644
--- a/core/lib/Drupal/Component/Utility/Xss.php
+++ b/core/lib/Drupal/Component/Utility/Xss.php
@@ -23,19 +23,13 @@ class Xss {
    */
   protected static $adminTags = array('a', 'abbr', 'acronym', 'address', 'article', 'aside', 'b', 'bdi', 'bdo', 'big', 'blockquote', 'br', 'caption', 'cite', 'code', 'col', 'colgroup', 'command', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'figcaption', 'figure', 'footer', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'i', 'img', 'ins', 'kbd', 'li', 'mark', 'menu', 'meter', 'nav', 'ol', 'output', 'p', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'small', 'span', 'strong', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time', 'tr', 'tt', 'u', 'ul', 'var', 'wbr');
 
+  protected static $htmlTags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd');
   /**
    * Filters HTML to prevent cross-site-scripting (XSS) vulnerabilities.
    *
    * Based on kses by Ulf Harnhammar, see http://sourceforge.net/projects/kses.
    * For examples of various XSS attacks, see: http://ha.ckers.org/xss.html.
    *
-   * This method is preferred to
-   * \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is not
-   * being used directly in the rendering system (for example, when its result
-   * is being combined with other strings before rendering). This avoids
-   * bloating the safe string list with partial strings if the whole result will
-   * be marked safe.
-   *
    * This code does four things:
    * - Removes characters and constructs that can trick browsers.
    * - Makes sure all HTML entities are well-formed.
@@ -54,11 +48,13 @@ class Xss {
    *   valid UTF-8.
    *
    * @see \Drupal\Component\Utility\Unicode::validateUtf8()
-   * @see \Drupal\Component\Utility\SafeMarkup::xssFilter()
    *
    * @ingroup sanitization
    */
-  public static function filter($string, $html_tags = array('a', 'em', 'strong', 'cite', 'blockquote', 'code', 'ul', 'ol', 'li', 'dl', 'dt', 'dd')) {
+  public static function filter($string, array $html_tags = NULL) {
+    if (is_null($html_tags)) {
+      $html_tags = static::$htmlTags;
+    }
     // Only operate on valid UTF-8 strings. This is necessary to prevent cross
     // site scripting issues on Internet Explorer 6.
     if (!Unicode::validateUtf8($string)) {
@@ -108,13 +104,6 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', '
    * is desired (so \Drupal\Component\Utility\SafeMarkup::checkPlain() is
    * not acceptable).
    *
-   * This method is preferred to
-   * \Drupal\Component\Utility\SafeMarkup::xssFilter() when the result is
-   * not being used directly in the rendering system (for example, when its
-   * result is being combined with other strings before rendering). This avoids
-   * bloating the safe string list with partial strings if the whole result will
-   * be marked safe.
-   *
    * Allows all tags that can be used inside an HTML body, save
    * for scripts and styles.
    *
@@ -126,7 +115,6 @@ public static function filter($string, $html_tags = array('a', 'em', 'strong', '
    *
    * @ingroup sanitization
    *
-   * @see \Drupal\Component\Utility\SafeMarkup::xssFilter()
    * @see \Drupal\Component\Utility\Xss::getAdminTagList()
    *
    */
@@ -347,4 +335,13 @@ public static function getAdminTagList() {
     return static::$adminTags;
   }
 
+  /**
+   * Gets the standard list of html tags allowed by Xss::filter().
+   *
+   * @return array
+   *   The list of html tags allowed by Xss::filter().
+   */
+  public static function getHtmlTagList() {
+    return static::$htmlTags;
+  }
 }
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 52a849c..ade044d 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -415,7 +415,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     if (!empty($elements['#markup'])) {
       // @todo Decide how to support non-HTML in the render API in
       //   https://www.drupal.org/node/2501313.
-      $elements['#markup'] = $this->xssFilterAdminIfUnsafe($elements['#markup']);
+      $elements['#markup'] = $this->safeMarkup($elements);
     }
 
     // Assume that if #theme is set it represents an implemented hook.
@@ -518,7 +518,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
     $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
 
-    $elements['#markup'] = $prefix . $elements['#children'] . $suffix;
+    $elements['#markup'] = SafeString::create($prefix . $elements['#children'] . $suffix);
 
     // We've rendered this element (and its subtree!), now update the context.
     $context->update($elements);
@@ -553,7 +553,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $context->bubble();
 
     $elements['#printed'] = TRUE;
-    return SafeString::create($elements['#markup']);
+    return $elements['#markup'];
   }
 
   /**
@@ -737,6 +737,36 @@ public function addCacheableDependency(array &$elements, $dependency) {
   }
 
   /**
+   * Escapes or filters #markup as required.
+   *
+   * @param array $elements
+   *   A render array with #markup set.
+   *
+   * @return \Drupal\Component\Utility\SafeStringInterface|string
+   *   The escaped string wrapped in a SafeString object. If
+   *   SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
+   *   escaped again.
+   */
+  protected function safeMarkup(array $elements) {
+    $strategy = isset($elements['#markup_safe_strategy']) ? $elements['#markup_safe_strategy'] : $this::SAFE_MARKUP_XSS;
+    if (SafeMarkup::isSafe($elements['#markup'])) {
+      // Nothing to do as #markup is already marked as safe.
+      $string = $elements['#markup'];
+    }
+    elseif ($strategy == $this::SAFE_MARKUP_ESCAPE) {
+      $string = htmlspecialchars($elements['#markup'], ENT_QUOTES, 'UTF-8');
+    }
+    else {
+      // The default behaviour is to XSS filter using the admin tag list.
+      $tags = isset($elements['#markup_xss_tags']) ? $elements['#markup_xss_tags'] : Xss::getAdminTagList();
+      $string = Xss::filter($elements['#markup'], $tags);
+    }
+
+    // @todo Do we need to clean up #markup_safe_strategy and #markup_xss_tags?
+    return SafeString::create($string);
+  }
+
+  /**
    * Applies a very permissive XSS/HTML filter for admin-only use.
    *
    * Note: This method only filters if $string is not marked safe already. This
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 155eec4..af7fda1 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -13,6 +13,22 @@
 interface RendererInterface {
 
   /**
+   * #markup_safe_strategy indicating #markup should be filtered.
+   *
+   * @see \Drupal\Core\Render\Renderer::safeMarkup()
+   * @see \Drupal\Component\Utility\Xss::filter()
+   */
+  const SAFE_MARKUP_XSS = 'xss';
+
+  /**
+   * #markup_safe_strategy indicating #markup should be escaped.
+   *
+   * @see \Drupal\Core\Render\Renderer::safeMarkup()
+   * @see \Drupal\Component\Utility\Html::encodeEntities()
+   */
+  const SAFE_MARKUP_ESCAPE = 'escape';
+
+  /**
    * Renders final HTML given a structured array tree.
    *
    * Calls ::render() in such a way that placeholders are replaced.
diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index 689dce0..c6c3704 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -271,10 +271,24 @@
  *   vectors. (I.e, <script> and <style> are not allowed.) See
  *   \Drupal\Component\Utility\Xss::$adminTags for the list of tags that will
  *   be allowed. If your markup needs any of the tags that are not in this
- *   whitelist, then you should implement a theme hook and template file and/or
- *   an asset library.
+ *   whitelist, then you can implement a theme hook and template file and/or
+ *   an asset library. Alternatively, you can use the render array keys
+ *   #markup_safe_strategy and #markup_xss_tags to alter how #markup is made
+ *   safe.
+ * - #markup_safe_strategy: If #markup is supplied this can be used to change
+ *   how the string is made safe for render. By default, all #markup is filtered
+ *   using Xss::adminFilter(). However, if the string should be escaped using
+ *   Html::encodeEntities() then this should be set to
+ *   RendererInterface::SAFE_MARKUP_ESCAPE
+ * - #markup_xss_tags: If #markup is supplied this can be used to change which
+ *   tags are using to filter the string. The value should be array of tags that
+ *   Xss::filter() would accept.
  *   @see core.libraries.yml
  *   @see hook_theme()
+ *   @see \Drupal\Component\Utility\Xss::filter()
+ *   @see \Drupal\Component\Utility\Xss::adminFilter()
+ *   @see \Drupal\Component\Utility\Html::encodeEntities()
+ *   @see \Drupal\Core\Render\RendererInterface::SAFE_MARKUP_ESCAPE
  *
  * JavaScript and CSS assets are specified in the render array using the
  * #attached property (see @ref sec_attached).
diff --git a/core/lib/Drupal/Core/Theme/ThemeManager.php b/core/lib/Drupal/Core/Theme/ThemeManager.php
index 31c10a7..f6c5714 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManager.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManager.php
@@ -7,12 +7,13 @@
 
 namespace Drupal\Core\Theme;
 
+use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Routing\StackedRouteMatchInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Template\Attribute;
-use Drupal\Component\Utility\SafeMarkup;
 
 /**
  * Provides the default implementation of a theme manager.
@@ -317,7 +318,10 @@ public function render($hook, array $variables) {
     $output = '';
     if (isset($info['function'])) {
       if (function_exists($info['function'])) {
-        $output = SafeMarkup::set($info['function']($variables));
+        // Theme functions do not render via the theme engine, so the output is
+        // not autoescaped. However, we can only presume that the theme function
+        // has been written correctly and that the markup is safe.
+        $output = SafeString::create($info['function']($variables));
       }
     }
     else {
@@ -387,7 +391,7 @@ public function render($hook, array $variables) {
       $output = $render_function($template_file, $variables);
     }
 
-    return (string) $output;
+    return ($output instanceof SafeStringInterface) ? $output : (string) $output;
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php b/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php
index ebbfcb5..b61ff84 100644
--- a/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php
+++ b/core/lib/Drupal/Core/Theme/ThemeManagerInterface.php
@@ -26,8 +26,8 @@
    * @param array $variables
    *   An associative array of theme variables.
    *
-   * @return string
-   *   The rendered output.
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   The rendered output, or a SafeString object.
    */
   public function render($hook, array $variables);
 
diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index 744861c..4cc4cb4 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -157,16 +157,13 @@ function aggregator_cron() {
 }
 
 /**
- * Renders the HTML content safely, as allowed.
+ * Gets the permitted list of tags.
  *
- * @param string $value
- *   The content to be filtered.
- *
- * @return string
- *   The filtered content.
+ * @return array
+ *   The array of permitted tags.
  */
-function aggregator_filter_xss($value) {
-  return SafeMarkup::xssFilter($value, preg_split('/\s+|<|>/', \Drupal::config('aggregator.settings')->get('items.allowed_html'), -1, PREG_SPLIT_NO_EMPTY));
+function aggregator_xss_tags() {
+  return preg_split('/\s+|<|>/', \Drupal::config('aggregator.settings')->get('items.allowed_html'), -1, PREG_SPLIT_NO_EMPTY);
 }
 
 /**
diff --git a/core/modules/aggregator/src/Controller/AggregatorController.php b/core/modules/aggregator/src/Controller/AggregatorController.php
index 13ff3d9..7f86c98 100644
--- a/core/modules/aggregator/src/Controller/AggregatorController.php
+++ b/core/modules/aggregator/src/Controller/AggregatorController.php
@@ -8,6 +8,7 @@
 namespace Drupal\aggregator\Controller;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Datetime\DateFormatter;
 use Drupal\aggregator\FeedInterface;
@@ -183,11 +184,11 @@ public function pageLast() {
    * @param \Drupal\aggregator\FeedInterface $aggregator_feed
    *   The aggregator feed.
    *
-   * @return string
-   *   The feed label.
+   * @return array
+   *   The feed label as a render array.
    */
   public function feedTitle(FeedInterface $aggregator_feed) {
-    return SafeMarkup::xssFilter($aggregator_feed->label());
+    return ['#markup' => $aggregator_feed->label(), '#markup_xss_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/aggregator/src/FeedViewBuilder.php b/core/modules/aggregator/src/FeedViewBuilder.php
index 8e4152b..176f35a 100644
--- a/core/modules/aggregator/src/FeedViewBuilder.php
+++ b/core/modules/aggregator/src/FeedViewBuilder.php
@@ -79,7 +79,8 @@ public function buildComponents(array &$build, array $entities, array $displays,
 
       if ($display->getComponent('description')) {
         $build[$id]['description'] = array(
-          '#markup' => aggregator_filter_xss($entity->getDescription()),
+          '#markup' => $entity->getDescription(),
+          '#markup_xss_tags' => aggregator_xss_tags(),
           '#prefix' => '<div class="feed-description">',
           '#suffix' => '</div>',
         );
diff --git a/core/modules/aggregator/src/ItemViewBuilder.php b/core/modules/aggregator/src/ItemViewBuilder.php
index 9207b71..d42a2cf 100644
--- a/core/modules/aggregator/src/ItemViewBuilder.php
+++ b/core/modules/aggregator/src/ItemViewBuilder.php
@@ -26,7 +26,8 @@ public function buildComponents(array &$build, array $entities, array $displays,
 
       if ($display->getComponent('description')) {
         $build[$id]['description'] = array(
-          '#markup' => aggregator_filter_xss($entity->getDescription()),
+          '#markup' => $entity->getDescription(),
+          '#markup_xss_tags' => aggregator_xss_tags(),
           '#prefix' => '<div class="item-description">',
           '#suffix' => '</div>',
         );
diff --git a/core/modules/aggregator/src/Plugin/Field/FieldFormatter/AggregatorXSSFormatter.php b/core/modules/aggregator/src/Plugin/Field/FieldFormatter/AggregatorXSSFormatter.php
index 58d31e1..6bf73bc 100644
--- a/core/modules/aggregator/src/Plugin/Field/FieldFormatter/AggregatorXSSFormatter.php
+++ b/core/modules/aggregator/src/Plugin/Field/FieldFormatter/AggregatorXSSFormatter.php
@@ -36,7 +36,8 @@ public function viewElements(FieldItemListInterface $items) {
     foreach ($items as $delta => $item) {
       $elements[$delta] = [
         '#type' => 'markup',
-        '#markup' => aggregator_filter_xss($item->value),
+        '#markup' => $item->value,
+        '#markup_xss_tags' => aggregator_xss_tags(),
       ];
     }
     return $elements;
diff --git a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php
index 9444808..8b499f5 100644
--- a/core/modules/aggregator/src/Tests/Views/IntegrationTest.php
+++ b/core/modules/aggregator/src/Tests/Views/IntegrationTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\aggregator\Tests\Views;
 
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Core\Url;
 use Drupal\views\Views;
@@ -121,13 +122,13 @@ public function testAggregatorItemView() {
       });
       $this->assertEqual($output, $expected_link, 'Ensure the right link is generated');
 
-      $expected_author = aggregator_filter_xss($items[$iid]->getAuthor());
+      $expected_author = Xss::filter($items[$iid]->getAuthor(), aggregator_xss_tags());
       $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
         return $view->field['author']->advancedRender($row);
       });
       $this->assertEqual($output, $expected_author, 'Ensure the author got filtered');
 
-      $expected_description = aggregator_filter_xss($items[$iid]->getDescription());
+      $expected_description = Xss::filter($items[$iid]->getDescription(), aggregator_xss_tags());
       $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
         return $view->field['description']->advancedRender($row);
       });
diff --git a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php
index 2fcf468..42182c9 100644
--- a/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php
+++ b/core/modules/comment/src/Tests/Views/CommentFieldNameTest.php
@@ -91,11 +91,11 @@ public function testCommentFieldName() {
     $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['field_name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($this->comment->getFieldName(), $output);
+    $this->assertEqual($this->comment->getFieldName(), $output);
     $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['field_name']->advancedRender($view->result[1]);
     });
-    $this->assertIdentical($this->customComment->getFieldName(), $output);
+    $this->assertEqual($this->customComment->getFieldName(), $output);
   }
 
 }
diff --git a/core/modules/dblog/src/Controller/DbLogController.php b/core/modules/dblog/src/Controller/DbLogController.php
index 0d1e4f2..81f9156 100644
--- a/core/modules/dblog/src/Controller/DbLogController.php
+++ b/core/modules/dblog/src/Controller/DbLogController.php
@@ -206,7 +206,7 @@ public function overview() {
           $this->dateFormatter->format($dblog->timestamp, 'short'),
           $message,
           array('data' => $username),
-          SafeMarkup::xssFilter($dblog->link),
+          array('data' => array('#markup' => $dblog->link)),
         ),
         // Attributes for table row.
         'class' => array(Html::getClass('dblog-' . $dblog->type), $classes[$dblog->severity]),
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index ed241d8..74145fc 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -37,6 +37,7 @@ public function process($text, $langcode) {
     if (stristr($text, 'data-caption') !== FALSE) {
       $dom = Html::load($text);
       $xpath = new \DOMXPath($dom);
+      $renderer = \Drupal::service('renderer');
       foreach ($xpath->query('//*[@data-caption]') as $node) {
         // Read the data-caption attribute's value, then delete it.
         $caption = SafeMarkup::checkPlain($node->getAttribute('data-caption'));
@@ -45,8 +46,8 @@ public function process($text, $langcode) {
         // Sanitize caption: decode HTML encoding, limit allowed HTML tags; only
         // allow inline tags that are allowed by default, plus <br>.
         $caption = Html::decodeEntities($caption);
-        $caption = SafeMarkup::xssFilter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br'));
-
+        $caption = ['#markup' => $caption, '#markup_xss_tags' => ['a', 'em', 'strong', 'cite', 'code', 'br']];
+        $caption = $renderer->render($caption);
         // The caption must be non-empty.
         if (Unicode::strlen($caption) === 0) {
           continue;
@@ -64,7 +65,7 @@ public function process($text, $langcode) {
           '#caption' => $caption,
           '#classes' => $classes,
         );
-        $altered_html = drupal_render($filter_caption);
+        $altered_html = $renderer->render($filter_caption);
 
         // Load the altered HTML into a new DOMDocument and retrieve the element.
         $updated_node = Html::load($altered_html)->getElementsByTagName('body')
diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php
index 560bb18..027a6e0 100644
--- a/core/modules/menu_ui/src/Controller/MenuController.php
+++ b/core/modules/menu_ui/src/Controller/MenuController.php
@@ -8,6 +8,7 @@
 namespace Drupal\menu_ui\Controller;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Menu\MenuParentFormSelectorInterface;
 use Drupal\system\MenuInterface;
@@ -73,11 +74,11 @@ public function getParentOptions(Request $request) {
    * @param \Drupal\system\MenuInterface $menu
    *   The menu entity.
    *
-   * @return string
-   *   The menu label.
+   * @return array
+   *   The menu label as a render array.
    */
   public function menuTitle(MenuInterface $menu) {
-    return SafeMarkup::xssFilter($menu->label());
+    return ['#markup' => $menu->label(), '#markup_xss_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 3291f33..25164e6 100644
--- a/core/modules/node/src/Controller/NodeController.php
+++ b/core/modules/node/src/Controller/NodeController.php
@@ -8,6 +8,7 @@
 namespace Drupal\node\Controller;
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Xss;
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
@@ -194,7 +195,7 @@ public function revisionOverview(NodeInterface $node) {
           '#context' => [
             'date' => $link,
             'username' => $this->renderer->renderPlain($username),
-            'message' => SafeMarkup::xssFilter($revision->revision_log->value),
+            'message' => ['#markup' => $revision->revision_log->value, '#markup_xss_tags' => Xss::getHtmlTagList()],
           ],
         ],
       ];
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index 25d52b5..3f10a54 100644
--- a/core/modules/search/search.module
+++ b/core/modules/search/search.module
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
@@ -627,8 +628,8 @@ function search_mark_for_reindex($type = NULL, $sid = NULL, $langcode = NULL) {
  * @param string|null $langcode
  *   Language code for the language of $text, if known.
  *
- * @return string
- *   A string containing HTML for the excerpt.
+ * @return array
+ *   A render array containing HTML for the excerpt.
  */
 function search_excerpt($keys, $text, $langcode = NULL) {
   // We highlight around non-indexable or CJK characters.
@@ -721,7 +722,10 @@ function search_excerpt($keys, $text, $langcode = NULL) {
     // We didn't find any keyword matches, so just return the first part of the
     // text. We also need to re-encode any HTML special characters that we
     // entity-decoded above.
-    return SafeMarkup::checkPlain(Unicode::truncate($text, 256, TRUE, TRUE));
+    return [
+      '#markup' => Unicode::truncate($text, 256, TRUE, TRUE),
+      '#markup_safe_strategy' => RendererInterface::SAFE_MARKUP_ESCAPE,
+    ];
   }
 
   // Sort the text ranges by starting position.
@@ -767,7 +771,10 @@ function search_excerpt($keys, $text, $langcode = NULL) {
   // Highlight keywords. Must be done at once to prevent conflicts ('strong'
   // and '<strong>').
   $text = trim(preg_replace('/' . $boundary . '(?:' . implode('|', $keys) . ')' . $boundary . '/iu', '<strong>\0</strong>', ' ' . $text . ' '));
-  return SafeMarkup::xssFilter($text, ['strong']);
+  return [
+    '#markup' => $text,
+    '#markup_xss_tags' => ['strong']
+  ];
 }
 
 /**
diff --git a/core/modules/search/src/Tests/SearchExcerptTest.php b/core/modules/search/src/Tests/SearchExcerptTest.php
index 9ed4403..659e3b4 100644
--- a/core/modules/search/src/Tests/SearchExcerptTest.php
+++ b/core/modules/search/src/Tests/SearchExcerptTest.php
@@ -38,39 +38,39 @@ function testSearchExcerpt() {
     // Note: The search_excerpt() function adds some extra spaces -- not
     // important for HTML formatting. Remove these for comparison.
     $expected = 'The quick brown fox &amp; jumps over the lazy dog';
-    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('nothing', $text));
     $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string is returned when keyword is not found in short string');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('fox', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('fox', $text));
     $this->assertEqual($result, 'The quick brown <strong>fox</strong> &amp; jumps over the lazy dog', 'Found keyword is highlighted');
 
     $expected = '<strong>The</strong> quick brown fox &amp; jumps over <strong>the</strong> lazy dog';
-    $result = preg_replace('| +|', ' ', search_excerpt('The', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('The', $text));
     $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at beginning of short string');
 
     $expected = 'The quick brown fox &amp; jumps over the lazy <strong>dog</strong>';
-    $result = preg_replace('| +|', ' ', search_excerpt('dog', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('dog', $text));
     $this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at end of short string');
 
     $longtext = str_repeat(str_replace('brown', 'silver', $text) . ' ', 10) . $text . str_repeat(' ' . str_replace('brown', 'pink', $text), 10);
-    $result = preg_replace('| +|', ' ', search_excerpt('brown', $longtext));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('brown', $longtext));
     $expected = '… silver fox &amp; jumps over the lazy dog The quick <strong>brown</strong> fox &amp; jumps over the lazy dog The quick …';
     $this->assertEqual($result, $expected, 'Snippet around keyword in long text is correctly capped');
 
     $longtext = str_repeat($text . ' ', 10);
-    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $longtext));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('nothing', $longtext));
     $expected = 'The quick brown fox &amp; jumps over the lazy dog';
     $this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
 
     $entities = str_repeat('k&eacute;sz&iacute;t&eacute;se ', 20);
-    $result = preg_replace('| +|', ' ', search_excerpt('nothing', $entities));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('nothing', $entities));
     $this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
     $this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
 
     // The node body that will produce this rendered $text is:
     // 123456789 HTMLTest +123456789+&lsquo;  +&lsquo;  +&lsquo;  +&lsquo;  +12345678  &nbsp;&nbsp;  +&lsquo;  +&lsquo;  +&lsquo;   &lsquo;
     $text = "<div class=\"field field-name-body field-type-text-with-summary field-label-hidden\"><div class=\"field-items\"><div class=\"field-item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘  +‘  +‘  +‘  +12345678      +‘  +‘  +‘   ‘</p>\n</div></div></div> ";
-    $result = search_excerpt('HTMLTest', $text);
+    $result = $this->doSearchExcerpt('HTMLTest', $text);
     $this->assertFalse(empty($result),  'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
   }
 
@@ -89,37 +89,37 @@ function testSearchExcerptSimplified() {
     $text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
     // Note: The search_excerpt() function adds some extra spaces -- not
     // important for HTML formatting. Remove these for comparison.
-    $result = preg_replace('| +|', ' ', search_excerpt('123456.7890', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('123456.7890', $text));
     $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('1234567890', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('1234567890', $text));
     $this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('Number 1234567890', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('Number 1234567890', $text));
     $this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('"Number 1234567890"', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('"Number 1234567890"', $text));
     $this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('"Hyphenated onetwo"', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('"Hyphenated onetwo"', $text));
     $this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('"abc def"', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('"abc def"', $text));
     $this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
 
     // Test phrases with characters which are being truncated.
-    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum _"', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('"ipsum _"', $text));
     $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('"ipsum 0000"', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('"ipsum 0000"', $text));
     $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
 
     // Test combination of the valid keyword and keyword containing only
     // characters which are being truncated during simplification.
-    $result = preg_replace('| +|', ' ', search_excerpt('ipsum _', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('ipsum _', $text));
     $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('ipsum 0000', $text));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('ipsum 0000', $text));
     $this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
 
     // Test using the hook_search_preprocess() from the test module.
@@ -127,37 +127,55 @@ function testSearchExcerptSimplified() {
     // So, if we search for "find" or "finds" or "finding", we should
     // highlight "finding".
     $text = "this tests finding a string";
-    $result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('finds', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for finds');
-    $result = preg_replace('| +|', ' ', search_excerpt('find', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('find', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for find');
 
     // Just to be sure, test with the replacement at the beginning and end.
     $text = "finding at the beginning";
-    $result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('finds', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at start');
 
     $text = "at the end finding";
-    $result = preg_replace('| +|', ' ', search_excerpt('finds', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('finds', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at end');
 
     // Testing with a one-to-many replacement: the test module replaces DIC
     // with Dependency Injection Container.
     $text = "something about the DIC is happening";
-    $result = preg_replace('| +|', ' ', search_excerpt('Dependency', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('Dependency', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym first word');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('Injection', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('Injection', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym second word');
 
-    $result = preg_replace('| +|', ' ', search_excerpt('Container', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('Container', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym third word');
 
     // Testing with a many-to-one replacement: the test module replaces
     // hypertext markup language with HTML.
     $text = "we always use hypertext markup language to describe things";
-    $result = preg_replace('| +|', ' ', search_excerpt('html', $text, 'ex'));
+    $result = preg_replace('| +|', ' ', $this->doSearchExcerpt('html', $text, 'ex'));
     $this->assertTrue(strpos($result, '<strong>hypertext markup language</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym many to one');
+  }
 
+  /**
+   * Calls search_excerpt() and renders output.
+   *
+   * @param string $keys
+   *   A string containing a search query.
+   * @param string $render_array
+   *   The text to extract fragments from.
+   * @param string|null $langcode
+   *   Language code for the language of $text, if known.
+   *
+   * @return string
+   *   A string containing HTML for the excerpt.
+   */
+  protected function doSearchExcerpt($keys, $render_array, $langcode = NULL) {
+    $render_array = search_excerpt($keys, $render_array, $langcode);
+    return \Drupal::service('renderer')->renderPlain($render_array);
   }
+
 }
diff --git a/core/modules/serialization/serialization.services.yml b/core/modules/serialization/serialization.services.yml
index 4c9397a..318a90c 100644
--- a/core/modules/serialization/serialization.services.yml
+++ b/core/modules/serialization/serialization.services.yml
@@ -30,6 +30,10 @@ services:
       arguments: ['Drupal\Core\Field\Plugin\Field\FieldType\PasswordItem']
       tags:
         - { name: normalizer, priority: 20 }
+  serializer.normalizer.safe_string:
+      class: Drupal\serialization\Normalizer\SafeStringNormalizer
+      tags:
+        - { name: normalizer }
   serializer.normalizer.typed_data:
     class: Drupal\serialization\Normalizer\TypedDataNormalizer
     tags:
diff --git a/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php b/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php
new file mode 100644
index 0000000..16b7fcd
--- /dev/null
+++ b/core/modules/serialization/src/Normalizer/SafeStringNormalizer.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\serialization\Normalizer\SafeStringNormalizer.
+ */
+
+namespace Drupal\serialization\Normalizer;
+
+/**
+ * Normalizes SafeStringInterface objects into a string.
+ */
+class SafeStringNormalizer extends NormalizerBase {
+
+  /**
+   * The interface or class that this Normalizer supports.
+   *
+   * @var array
+   */
+  protected $supportedInterfaceOrClass = array('Drupal\Component\Utility\SafeStringInterface');
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = array()) {
+    return (string) $object;
+  }
+
+}
diff --git a/core/modules/simpletest/src/AssertContentTrait.php b/core/modules/simpletest/src/AssertContentTrait.php
index b0efbe5..6fe6225 100644
--- a/core/modules/simpletest/src/AssertContentTrait.php
+++ b/core/modules/simpletest/src/AssertContentTrait.php
@@ -815,7 +815,10 @@ protected function assertThemeOutput($callback, array $variables = array(), $exp
     /** @var \Drupal\Core\Render\RendererInterface $renderer */
     $renderer = \Drupal::service('renderer');
 
-    $output = $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) {
+    // The string cast is necessary because theme functions return
+    // SafeStringInterface objects. This means we can assert that $expected
+    // matches the theme output without having to worry about 0 == ''.
+    $output = (string) $renderer->executeInRenderContext(new RenderContext(), function() use ($callback, $variables) {
       return \Drupal::theme()->render($callback, $variables);
     });
     $this->verbose(
diff --git a/core/modules/system/src/Tests/Theme/ThemeTest.php b/core/modules/system/src/Tests/Theme/ThemeTest.php
index c2c0e0b..a0095b8 100644
--- a/core/modules/system/src/Tests/Theme/ThemeTest.php
+++ b/core/modules/system/src/Tests/Theme/ThemeTest.php
@@ -13,6 +13,7 @@
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\Routing\Route;
+use Drupal\Component\Utility\SafeStringInterface;
 
 /**
  * Tests low-level theme functions.
@@ -59,12 +60,19 @@ function testAttributeMerging() {
    * Test that _theme() returns expected data types.
    */
   function testThemeDataTypes() {
-    // theme_test_false is an implemented theme hook so \Drupal::theme() service should
-    // return a string, even though the theme function itself can return anything.
-    $foos = array('null' => NULL, 'false' => FALSE, 'integer' => 1, 'string' => 'foo');
+    // theme_test_false is an implemented theme hook so \Drupal::theme() service
+    // should return a string or an object that implements SafeStringInterface,
+    // even though the theme function itself can return anything.
+    $foos = array('null' => NULL, 'false' => FALSE, 'integer' => 1, 'string' => 'foo', 'empty_string' => '');
     foreach ($foos as $type => $example) {
       $output = \Drupal::theme()->render('theme_test_foo', array('foo' => $example));
-      $this->assertTrue(is_string($output), format_string('\Drupal::theme() returns a string for data type !type.', array('!type' => $type)));
+      $this->assertTrue($output instanceof SafeStringInterface || is_string($output), format_string('\Drupal::theme() returns an object that implements SafeStringInterface or a string for data type !type.', array('!type' => $type)));
+      if ($output instanceof SafeStringInterface) {
+        $this->assertIdentical((string) $example, $output->__toString());
+      }
+      elseif (is_string($output)) {
+        $this->assertIdentical($output, '', 'A string will be return when the theme returns an empty string.');
+      }
     }
 
     // suggestionnotimplemented is not an implemented theme hook so \Drupal::theme() service
diff --git a/core/modules/taxonomy/src/Controller/TaxonomyController.php b/core/modules/taxonomy/src/Controller/TaxonomyController.php
index 2e35678..2ed4ef9 100644
--- a/core/modules/taxonomy/src/Controller/TaxonomyController.php
+++ b/core/modules/taxonomy/src/Controller/TaxonomyController.php
@@ -49,13 +49,13 @@ public function addForm(VocabularyInterface $taxonomy_vocabulary) {
    * Route title callback.
    *
    * @param \Drupal\taxonomy\VocabularyInterface $taxonomy_vocabulary
-   *   The taxonomy term.
+   *   The vocabulary.
    *
    * @return string
-   *   The term label.
+   *   The vocabulary label as a render array.
    */
   public function vocabularyTitle(VocabularyInterface $taxonomy_vocabulary) {
-    return SafeMarkup::xssFilter($taxonomy_vocabulary->label());
+    return ['#markup' => $taxonomy_vocabulary->label(), '#markup_xss_tags' => Xss::getHtmlTagList()];
   }
 
   /**
@@ -64,11 +64,11 @@ public function vocabularyTitle(VocabularyInterface $taxonomy_vocabulary) {
    * @param \Drupal\taxonomy\TermInterface $taxonomy_term
    *   The taxonomy term.
    *
-   * @return string
-   *   The term label.
+   * @return array
+   *   The term label as a render array.
    */
   public function termTitle(TermInterface $taxonomy_term) {
-    return SafeMarkup::xssFilter($taxonomy_term->getName());
+    return ['#markup' => $taxonomy_term->getName(), '#markup_xss_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php
index a52372d..fa6e0ec 100644
--- a/core/modules/user/src/Controller/UserController.php
+++ b/core/modules/user/src/Controller/UserController.php
@@ -158,11 +158,12 @@ public function userPage() {
    * @param \Drupal\user\UserInterface $user
    *   The user account.
    *
-   * @return string
-   *   The user account name.
+   * @return string|array
+   *   The user account name as a render array or an empty string if $user is
+   *   NULL.
    */
   public function userTitle(UserInterface $user = NULL) {
-    return $user ? SafeMarkup::xssFilter($user->getUsername()) : '';
+    return $user ? ['#markup' => $user->getUsername(), '#markup_xss_tags' => Xss::getHtmlTagList()] : '';
   }
 
   /**
diff --git a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php
index 5db1e3e..42f9b429 100644
--- a/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php
+++ b/core/modules/user/src/Tests/Views/HandlerFieldUserNameTest.php
@@ -63,7 +63,7 @@ public function testUserName() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $username, 'If the user is not linked the username should be printed out for a normal user.');
+    $this->assertEqual($render, $username, 'If the user is not linked the username should be printed out for a normal user.');
 
   }
 
diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php
index 97db7c9..edad533 100644
--- a/core/modules/views/src/Plugin/Block/ViewsBlock.php
+++ b/core/modules/views/src/Plugin/Block/ViewsBlock.php
@@ -33,8 +33,7 @@ public function build() {
     if ($output = $this->view->buildRenderable($this->displayID, [], FALSE)) {
       // Override the label to the dynamic title configured in the view.
       if (empty($this->configuration['views_label']) && $this->view->getTitle()) {
-        // @todo https://www.drupal.org/node/2527360 remove call to SafeMarkup.
-        $output['#title'] = SafeMarkup::xssFilter($this->view->getTitle(), Xss::getAdminTagList());
+        $output['#title'] = ['#markup' => $this->view->getTitle(), '#markup_xss_tags' => Xss::getHtmlTagList()];
       }
 
       // Before returning the block output, convert it to a renderable array
diff --git a/core/modules/views/src/Plugin/views/display/Page.php b/core/modules/views/src/Plugin/views/display/Page.php
index 7ec3560..7a9b7f5 100644
--- a/core/modules/views/src/Plugin/views/display/Page.php
+++ b/core/modules/views/src/Plugin/views/display/Page.php
@@ -182,8 +182,7 @@ public function execute() {
     //   it should be dropped.
     if (is_array($render)) {
       $render += array(
-        // @todo https://www.drupal.org/node/2527360 remove call to SafeMarkup.
-        '#title' => SafeMarkup::xssFilter($this->view->getTitle(), Xss::getAdminTagList()),
+        '#title' => ['#markup' => $this->view->getTitle(), '#markup_xss_tags' => Xss::getHtmlTagList()],
       );
     }
     return $render;
diff --git a/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php b/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php
index 6cd22ac..b25e419 100644
--- a/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php
+++ b/core/modules/views/src/Plugin/views/field/FieldHandlerInterface.php
@@ -167,8 +167,10 @@ public function preRender(&$values);
    * @param \Drupal\views\ResultRow $values
    *   The values retrieved from a single row of a view's query result.
    *
-   * @return string
-   *   The rendered output.
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   The rendered output. If the output is safe it will be wrapped in a
+   *   \Drupal\Core\Render\SafeString object. If it is empty or unsafe it will
+   *   be a string.
    *
    */
   public function render(ResultRow $values);
@@ -202,8 +204,10 @@ public function postRender(ResultRow $row, $output);
    * @param \Drupal\views\ResultRow $values
    *   The values retrieved from a single row of a view's query result.
    *
-   * @return string
-   *   The advanced rendered output.
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   The advanced rendered output. If the output is safe it will be wrapped in
+   *   a \Drupal\Core\Render\SafeString object. If it is empty or unsafe it will
+   *   be a string.
    *
    */
   public function advancedRender(ResultRow $values);
@@ -236,30 +240,14 @@ public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE);
    *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
    *     - html: Make sure that the html is correct.
    *
-   * @return string
-   *   The rendered string.
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   The rendered output. If the output is safe it will be wrapped in a
+   *   \Drupal\Core\Render\SafeString object. If it is empty or unsafe it will
+   *   be a string.
    */
   public function renderText($alter);
 
   /**
-   * Trims the field down to the specified length.
-   *
-   * @param array $alter
-   *   The alter array of options to use.
-   *     - max_length: Maximum length of the string, the rest gets truncated.
-   *     - word_boundary: Trim only on a word boundary.
-   *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
-   *     - html: Make sure that the html is correct.
-   *
-   * @param string $value
-   *   The string which should be trimmed.
-   *
-   * @return string
-   *   The rendered trimmed string.
-   */
-  public function renderTrimText($alter, $value);
-
-  /**
    * Gets the 'render' tokens to use for advanced rendering.
    *
    * This runs through all of the fields and arguments that
@@ -280,8 +268,10 @@ public function getRenderTokens($item);
    * @param \Drupal\views\ResultRow $values
    *   Holds single row of a view's result set.
    *
-   * @return string|false
-   *   Returns rendered output of the given theme implementation.
+   * @return string|\Drupal\Component\Utility\SafeStringInterface
+   *   Returns rendered output of the given theme implementation. If the output
+   *   is safe it will be wrapped in a \Drupal\Core\Render\SafeString object. If
+   *   it is empty or unsafe it will be a string.
    */
   function theme(ResultRow $values);
 
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index ee16994..973916b 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\SafeStringInterface;
 use Drupal\Component\Utility\Unicode;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Component\Utility\Xss;
@@ -1138,7 +1139,7 @@ public function advancedRender(ResultRow $values) {
     else {
       $value = $this->render($values);
       if (is_array($value)) {
-        $value = (string) $this->getRenderer()->render($value);
+        $value = $this->getRenderer()->render($value);
       }
       $this->last_render = $value;
       $this->original_value = $value;
@@ -1169,14 +1170,16 @@ public function advancedRender(ResultRow $values) {
       }
 
       if (is_array($value)) {
-        $value = (string) $this->getRenderer()->render($value);
+        $value = $this->getRenderer()->render($value);
       }
       // This happens here so that renderAsLink can get the unaltered value of
       // this field as a token rather than the altered value.
       $this->last_render = $value;
     }
 
-    if (empty($this->last_render)) {
+    // String cast is necessary to test emptiness of SafeStringInterface
+    // objects.
+    if (empty((string) $this->last_render)) {
       if ($this->isValueEmpty($this->last_render, $this->options['empty_zero'], FALSE)) {
         $alter = $this->options['alter'];
         $alter['alter_text'] = 1;
@@ -1185,9 +1188,6 @@ public function advancedRender(ResultRow $values) {
         $this->last_render = $this->renderText($alter);
       }
     }
-    // @todo Fix this in https://www.drupal.org/node/2280961.
-    $this->last_render = SafeMarkup::set($this->last_render);
-
 
     return $this->last_render;
   }
@@ -1196,6 +1196,10 @@ public function advancedRender(ResultRow $values) {
    * {@inheritdoc}
    */
   public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
+    // Convert SafeStringInterface to a string for checking.
+    if ($value instanceof SafeStringInterface) {
+      $value = (string) $value;
+    }
     if (!isset($value)) {
       $empty = TRUE;
     }
@@ -1213,7 +1217,14 @@ public function isValueEmpty($value, $empty_zero, $no_skip_empty = TRUE) {
    * {@inheritdoc}
    */
   public function renderText($alter) {
-    $value = $this->last_render;
+    // We need to preserve the safeness of the value regardless of the
+    // alterations made by this method. Any alterations or replacements made
+    // within this method need to ensure that at the minimum the result is
+    // Xss admin filtered. See self::renderAltered() as an exmaple that does.
+    $value_is_safe = SafeMarkup::isSafe($this->last_render);
+    // Cast to a string so that empty checks and string functions work as
+    // expected.
+    $value = (string) $this->last_render;
 
     if (!empty($alter['alter_text']) && $alter['text'] !== '') {
       $tokens = $this->getRenderTokens($alter);
@@ -1239,6 +1250,9 @@ public function renderText($alter) {
     if ($alter['phase'] == static::RENDER_TEXT_PHASE_EMPTY && $no_rewrite_for_empty) {
       // If we got here then $alter contains the value of "No results text"
       // and so there is nothing left to do.
+      if ($value_is_safe) {
+        $value = SafeString::create($value);
+      }
       return $value;
     }
 
@@ -1275,6 +1289,12 @@ public function renderText($alter) {
     if (!empty($alter['nl2br'])) {
       $value = nl2br($value);
     }
+
+    // Preserve whether or not the string is safe. Since $suffix comes from
+    // \Drupal::l(), it is safe to append.
+    if ($value_is_safe) {
+      $value = SafeString::create($value . $suffix);
+    }
     $this->last_render_text = $value;
 
     if (!empty($alter['make_link']) && (!empty($alter['path']) || !empty($alter['url']))) {
@@ -1284,20 +1304,42 @@ public function renderText($alter) {
       $value = $this->renderAsLink($alter, $value, $tokens);
     }
 
-    return $value . $suffix;
+    // Preserve whether or not the string is safe. Since $suffix comes from
+    // \Drupal::l(), it is safe to append.
+    if ($value_is_safe) {
+      return SafeString::create($value . $suffix);
+    }
+    else {
+      // If the string is not already marked safe, it is still OK to return it
+      // because it will be sanitized by Twig.
+      return $value . $suffix;
+    }
   }
 
   /**
    * Render this field as user-defined altered text.
    */
   protected function renderAltered($alter, $tokens) {
-    return SafeString::create($this->viewsTokenReplace($alter['text'], $tokens));
+    return $this->viewsTokenReplace($alter['text'], $tokens);
   }
 
   /**
-   * {@inheritdoc}
+   * Trims the field down to the specified length.
+   *
+   * @param array $alter
+   *   The alter array of options to use.
+   *     - max_length: Maximum length of the string, the rest gets truncated.
+   *     - word_boundary: Trim only on a word boundary.
+   *     - ellipsis: Show an ellipsis (…) at the end of the trimmed string.
+   *     - html: Make sure that the html is correct.
+   *
+   * @param string $value
+   *   The string which should be trimmed.
+   *
+   * @return string
+   *   The rendered trimmed string.
    */
-  public function renderTrimText($alter, $value) {
+  protected function renderTrimText($alter, $value) {
     if (!empty($alter['strip_tags'])) {
       // NOTE: It's possible that some external fields might override the
       // element type.
diff --git a/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php b/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php
index c2c9af5..70edf22 100644
--- a/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldFileSizeTest.php
@@ -64,9 +64,9 @@ public function testFieldFileSize() {
     // Test with the bytes option.
     $view->field['age']->options['file_size_display'] = 'bytes';
     $this->assertEqual($view->field['age']->advancedRender($view->result[0]), '');
-    $this->assertEqual($view->field['age']->advancedRender($view->result[1]), 10);
-    $this->assertEqual($view->field['age']->advancedRender($view->result[2]), 1000);
-    $this->assertEqual($view->field['age']->advancedRender($view->result[3]), 10000);
+    $this->assertEqual($view->field['age']->advancedRender($view->result[1]), '10');
+    $this->assertEqual($view->field['age']->advancedRender($view->result[2]), '1000');
+    $this->assertEqual($view->field['age']->advancedRender($view->result[3]), '10000');
   }
 
 }
diff --git a/core/modules/views/src/Tests/Handler/FieldUnitTest.php b/core/modules/views/src/Tests/Handler/FieldUnitTest.php
index 85fabe3..7ad9cdc 100644
--- a/core/modules/views/src/Tests/Handler/FieldUnitTest.php
+++ b/core/modules/views/src/Tests/Handler/FieldUnitTest.php
@@ -324,7 +324,8 @@ function testEmpty() {
   /**
    * Tests the hide if empty functionality.
    *
-   * This tests alters the result to get easier and less coupled results.
+   * This tests alters the result to get easier and less coupled results. It is
+   * important that assertIdentical() is used in this test since in PHP 0 == ''.
    */
   function _testHideIfEmpty() {
     /** @var \Drupal\Core\Render\RendererInterface $renderer */
@@ -349,7 +350,7 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'By default, a string should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'By default, a string should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
@@ -363,14 +364,14 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, '0', 'By default, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, '0', 'By default, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'By default, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'By default, "0" should not be treated as empty.');
 
     // Test when results are not rewritten and non-zero empty values are hidden.
     $view->field['name']->options['hide_alter_empty'] = TRUE;
@@ -382,7 +383,7 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If hide_empty is checked, a string should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
@@ -396,14 +397,14 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, '0', 'If hide_empty is checked, but not empty_zero, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If hide_empty is checked, but not empty_zero, "0" should not be treated as empty.');
 
     // Test when results are not rewritten and all empty values are hidden.
     $view->field['name']->options['hide_alter_empty'] = TRUE;
@@ -437,28 +438,28 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, it should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, "" should not be treated as empty.');
 
     // Test zero as an integer.
     $view->result[0]->{$column_map_reversed['name']} = 0;
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is not empty, "0" should not be treated as empty.');
 
     // Test when results are rewritten to an empty string and non-zero empty results are hidden.
     $view->field['name']->options['hide_alter_empty'] = TRUE;
@@ -472,7 +473,7 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_name, 'If the rewritten string is empty, it should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
@@ -486,14 +487,14 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, '0', 'If the rewritten string is empty, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If the rewritten string is empty, "0" should not be treated as empty.');
 
     // Test when results are rewritten to zero as a string and non-zero empty
     // results are hidden.
@@ -508,28 +509,28 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, the string rewritten as 0 should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, "" rewritten as 0 should not be treated as empty.');
 
     // Test zero as an integer.
     $view->result[0]->{$column_map_reversed['name']} = 0;
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, "0", 'If the rewritten string is zero and empty_zero is not checked, "0" should not be treated as empty.');
 
     // Test when results are rewritten to a valid string and non-zero empty
     // results are hidden.
@@ -544,7 +545,7 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, it should not be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
@@ -558,14 +559,14 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, 0 should not be treated as empty.');
 
     // Test zero as a string.
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.');
+    $this->assertIdentical((string) $render, $random_value, 'If the original and rewritten strings are valid, "0" should not be treated as empty.');
 
     // Test when results are rewritten to zero as a string and all empty
     // original values and results are hidden.
@@ -580,7 +581,7 @@ function _testHideIfEmpty() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "", 'If the rewritten string is zero, it should be treated as empty.');
+    $this->assertIdentical((string) $render, "", 'If the rewritten string is zero, it should be treated as empty.');
 
     // Test an empty string.
     $view->result[0]->{$column_map_reversed['name']} = "";
@@ -623,20 +624,20 @@ function _testEmptyText() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $empty_text, 'If a field is empty, the empty text should be used for the output.');
+    $this->assertIdentical((string) $render, $empty_text, 'If a field is empty, the empty text should be used for the output.');
 
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.');
+    $this->assertIdentical((string) $render, "0", 'If a field is 0 and empty_zero is not checked, the empty text should not be used for the output.');
 
     $view->result[0]->{$column_map_reversed['name']} = "0";
     $view->field['name']->options['empty_zero'] = TRUE;
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.');
+    $this->assertIdentical((string) $render, $empty_text, 'If a field is 0 and empty_zero is checked, the empty text should be used for the output.');
 
     $view->result[0]->{$column_map_reversed['name']} = "";
     $view->field['name']->options['alter']['alter_text'] = TRUE;
@@ -645,13 +646,13 @@ function _testEmptyText() {
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.');
+    $this->assertIdentical((string) $render, $alter_text, 'If a field is empty, some rewrite text exists, but hide_alter_empty is not checked, render the rewrite text.');
 
     $view->field['name']->options['hide_alter_empty'] = TRUE;
     $render = $renderer->executeInRenderContext(new RenderContext(), function () use ($view) {
       return $view->field['name']->advancedRender($view->result[0]);
     });
-    $this->assertIdentical($render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.');
+    $this->assertIdentical((string) $render, $empty_text, 'If a field is empty, some rewrite text exists, and hide_alter_empty is checked, use the empty text.');
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index 90a475f..4769548 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -216,36 +216,20 @@ public function testReplace($search, $replace, $subject, $expected, $is_safe) {
   /**
    * Tests the interaction between the safe list and XSS filtering.
    *
-   * @covers ::xssFilter
    * @covers ::escape
    */
   public function testAdminXss() {
-    // Use the predefined XSS admin tag list. This strips the <marquee> tags.
-    $this->assertEquals('text', SafeMarkup::xssFilter('<marquee>text</marquee>', Xss::getAdminTagList()));
-    $this->assertTrue(SafeMarkup::isSafe('text'), 'The string \'text\' is marked as safe.');
-
-    // This won't strip the <marquee> tags and the string with HTML will be
-    // marked as safe.
-    $filtered = SafeMarkup::xssFilter('<marquee>text</marquee>', array('marquee'));
-    $this->assertEquals('<marquee>text</marquee>', $filtered);
-    $this->assertTrue(SafeMarkup::isSafe('<marquee>text</marquee>'), 'The string \'<marquee>text</marquee>\' is marked as safe.');
-
-    // SafeMarkup::xssFilter() with the default tag list will strip the
-    // <marquee> tag even though the string was marked safe above.
-    $this->assertEquals('text', SafeMarkup::xssFilter('<marquee>text</marquee>'));
+    // Mark the string as safe. This is for test purposes only.
+    $text = '<marquee>text</marquee>';
+    SafeMarkup::set($text);
 
     // SafeMarkup::escape() will not escape the markup tag since the string was
     // marked safe above.
-    $this->assertEquals('<marquee>text</marquee>', SafeMarkup::escape($filtered));
+    $this->assertEquals('<marquee>text</marquee>', SafeMarkup::escape($text));
 
     // SafeMarkup::checkPlain() will escape the markup tag even though the
     // string was marked safe above.
-    $this->assertEquals('&lt;marquee&gt;text&lt;/marquee&gt;', SafeMarkup::checkPlain($filtered));
-
-    // Ensure that SafeMarkup::xssFilter strips all tags when passed an empty
-    // array and uses the default tag list when not passed a tag list.
-    $this->assertEquals('text', SafeMarkup::xssFilter('<em>text</em>', []));
-    $this->assertEquals('<em>text</em>', SafeMarkup::xssFilter('<em>text</em>'));
+    $this->assertEquals('&lt;marquee&gt;text&lt;/marquee&gt;', SafeMarkup::checkPlain($text));
   }
 
   /**
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index c6845f5..76104e5 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -13,6 +13,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Render\SafeString;
 use Drupal\Core\Template\Attribute;
 
@@ -88,6 +89,22 @@ public function providerTestRenderBasic() {
     $data[] = [[
       'child' => ['#markup' => "This is <script>alert('XSS')</script> test"],
     ], "This is alert('XSS') test"];
+    // XSS filtering test.
+    $data[] = [[
+      'child' => ['#markup' => "This is <script>alert('XSS')</script> test", '#markup_xss_tags' => ['script']],
+    ], "This is <script>alert('XSS')</script> test"];
+    // XSS filtering test.
+    $data[] = [[
+      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", '#markup_xss_tags' => ['em', 'strong']],
+    ], "This is <em>alert('XSS')</em> <strong>test</strong>"];
+    // Html escaping test.
+    $data[] = [[
+      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", '#markup_safe_strategy' => RendererInterface::SAFE_MARKUP_ESCAPE],
+    ], "This is &lt;script&gt;&lt;em&gt;alert(&#039;XSS&#039;)&lt;/em&gt;&lt;/script&gt; &lt;strong&gt;test&lt;/strong&gt;"];
+    // XSS filtering by default test.
+    $data[] = [[
+      'child' => ['#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>", '#markup_safe_strategy' => 'nonsense'],
+    ], "This is <em>alert('XSS')</em> <strong>test</strong>"];
     // Ensure non-XSS tags are not filtered out.
     $data[] = [[
       'child' => ['#markup' => "This is <strong><script>alert('not a giraffe')</script></strong> test"],
diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine
index af5ebe7..de085ff 100644
--- a/core/themes/engines/twig/twig.engine
+++ b/core/themes/engines/twig/twig.engine
@@ -6,6 +6,7 @@
  */
 
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Render\SafeString;
 use Drupal\Core\Extension\Extension;
 
 /**
@@ -44,7 +45,7 @@ function twig_init(Extension $theme) {
  * @param array $variables
  *   A keyed array of variables that will appear in the output.
  *
- * @return string
+ * @return string|\Drupal\Component\Utility\SafeStringInterface
  *   The output generated by the template, plus any debug information.
  */
 function twig_render_template($template_file, array $variables) {
@@ -74,7 +75,7 @@ function twig_render_template($template_file, array $variables) {
   }
   if ($twig_service->isDebug()) {
     $output['debug_prefix'] .= "\n\n<!-- THEME DEBUG -->";
-    $output['debug_prefix'] .= "\n<!-- THEME HOOK: '" . SafeMarkup::checkPlain($variables['theme_hook_original']) . "' -->";
+    $output['debug_prefix'] .= "\n<!-- THEME HOOK: '" . htmlspecialchars($variables['theme_hook_original'], ENT_QUOTES, 'UTF-8') . "' -->";
     // If there are theme suggestions, reverse the array so more specific
     // suggestions are shown first.
     if (!empty($variables['theme_hook_suggestions'])) {
@@ -108,12 +109,13 @@ function twig_render_template($template_file, array $variables) {
         $prefix = ($template == $current_template) ? 'x' : '*';
         $suggestion = $prefix . ' ' . $template;
       }
-      $output['debug_info'] .= "\n<!-- FILE NAME SUGGESTIONS:\n   " . SafeMarkup::checkPlain(implode("\n   ", $suggestions)) . "\n-->";
+      $output['debug_info'] .= "\n<!-- FILE NAME SUGGESTIONS:\n   " . htmlspecialchars(implode("\n   ", $suggestions), ENT_QUOTES, 'UTF-8') . "\n-->";
     }
-    $output['debug_info']   .= "\n<!-- BEGIN OUTPUT from '" . SafeMarkup::checkPlain($template_file) . "' -->\n";
-    $output['debug_suffix'] .= "\n<!-- END OUTPUT from '" . SafeMarkup::checkPlain($template_file) . "' -->\n\n";
+    $output['debug_info']   .= "\n<!-- BEGIN OUTPUT from '" . htmlspecialchars($template_file, ENT_QUOTES, 'UTF-8') . "' -->\n";
+    $output['debug_suffix'] .= "\n<!-- END OUTPUT from '" . htmlspecialchars($template_file, ENT_QUOTES, 'UTF-8') . "' -->\n\n";
   }
-  return SafeMarkup::set(implode('', $output));
+  // This output has already been rendered and is therefore considered safe.
+  return SafeString::create(implode('', $output));
 }
 
 /**
