diff --git a/core/includes/theme.inc b/core/includes/theme.inc
index 009bb33..53aeea7 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 2fb5152..91a2f1e 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..6fdeb68 100644
--- a/core/lib/Drupal/Component/Utility/Xss.php
+++ b/core/lib/Drupal/Component/Utility/Xss.php
@@ -15,7 +15,7 @@
 class Xss {
 
   /**
-   * The list of html tags allowed by filterAdmin().
+   * The list of HTML tags allowed by filterAdmin().
    *
    * @var array
    *
@@ -24,18 +24,20 @@ 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');
 
   /**
+   * The default list of HTML tags allowed by filter().
+   *
+   * @var array
+   *
+   * @see \Drupal\Component\Utility\Xss::filter()
+   */
+  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 +56,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 +112,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 +123,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()
    *
    */
@@ -338,13 +334,22 @@ protected static function needsRemoval($html_tags, $elem) {
   }
 
   /**
-   * Gets the list of html tags allowed by Xss::filterAdmin().
+   * Gets the list of HTML tags allowed by Xss::filterAdmin().
    *
    * @return array
-   *   The list of html tags allowed by filterAdmin().
+   *   The list of HTML tags allowed by filterAdmin().
    */
   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/Element/Markup.php b/core/lib/Drupal/Core/Render/Element/Markup.php
new file mode 100644
index 0000000..55d94b7
--- /dev/null
+++ b/core/lib/Drupal/Core/Render/Element/Markup.php
@@ -0,0 +1,109 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Render\Element\Markup.
+ */
+
+namespace Drupal\Core\Render\Element;
+
+use Drupal\Component\Utility\Html as HtmlUtility;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Render\SafeString;
+
+/**
+ * Provides a render element for a details element, similar to a fieldset.
+ *
+ * Fieldsets can only be used in forms, while details elements can be used
+ * outside of forms.
+ *
+ * @see \Drupal\Core\Render\Element\Fieldset
+ *
+ * @RenderElement("markup")
+ */
+class Markup extends RenderElement {
+
+  /**
+   * #markup_safe_strategy indicating #markup should be filtered.
+   *
+   * @see ::ensureMarkupIsSafe()
+   * @see \Drupal\Component\Utility\Xss::filter()
+   */
+  const SAFE_STRATEGY_FILTER = 'xss';
+
+  /**
+   * #markup_safe_strategy indicating #markup should be escaped.
+   *
+   * @see ::ensureMarkupIsSafe()
+   * @see \Drupal\Component\Utility\Html::escape()
+   */
+  const SAFE_STRATEGY_ESCAPE = 'escape';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getInfo() {
+    return [
+      '#pre_render' => [
+        [static::class, 'ensureMarkupIsSafe'],
+      ],
+    ];
+  }
+
+  /**
+   * Escapes or filters #markup as required.
+   *
+   * Drupal uses Twig's auto-escape feature to improve security. This feature
+   * automatically escapes any HTML that is not known to be safe. Due to this
+   * the render system needs to ensure that all markup it generates is marked
+   * safe so that Twig does not do any additional escaping.
+   *
+   * By default all #markup is filtered to protect against XSS using the admin
+   * tag list. Render arrays can alter the list of tags allowed by the filter
+   * using the #markup_allowed_tags property. This value should be an array of
+   * tags that Xss::filter() would accept. Render arrays can escape #markup
+   * instead of XSS filtering by setting the #markup_safe_strategy property to
+   * Markup:SAFE_STRATEGY_ESCAPE. If the escaping strategy is used
+   * #markup_allowed_tags is ignored.
+   *
+   * @param array $elements
+   *   A render array with #markup set.
+   *
+   * @return \Drupal\Component\Utility\SafeStringInterface|string
+   *   The escaped markup wrapped in a SafeString object. If
+   *   SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be
+   *   escaped or filtered again.
+   *
+   * @see \Drupal\Component\Utility\Html::escape()
+   * @see \Drupal\Component\Utility\Xss::filter()
+   * @see \Drupal\Component\Utility\Xss::adminFilter()
+   * @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_FILTER
+   * @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_ESCAPE
+   */
+  public static function ensureMarkupIsSafe(array $elements) {
+    if (empty($elements['#markup'])) {
+      return $elements;
+    }
+
+    $strategy = isset($elements['#markup_safe_strategy']) ? $elements['#markup_safe_strategy'] : static::SAFE_STRATEGY_FILTER;
+    if (SafeMarkup::isSafe($elements['#markup'])) {
+      // Nothing to do as #markup is already marked as safe.
+      return $elements;
+    }
+    elseif ($strategy == static::SAFE_STRATEGY_ESCAPE) {
+      $markup = HtmlUtility::escape($elements['#markup']);
+    }
+    else {
+      // The default behaviour is to XSS filter using the admin tag list.
+      $tags = isset($elements['#markup_allowed_tags']) ? $elements['#markup_allowed_tags'] : Xss::getAdminTagList();
+      $markup = Xss::filter($elements['#markup'], $tags);
+    }
+
+    $elements['#markup'] = SafeString::create($markup);
+
+    return $elements;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 2ef11d1..f2be9ce 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -301,6 +301,12 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
     $pre_bubbling_elements = [];
     $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
 
+    // If #markup is set, ensure #type is set. This allows to specify just
+    // #markup on an element without setting #type.
+    if (isset($elements['#markup']) && !isset($elements['#type'])) {
+      $elements['#type'] = 'markup';
+    }
+
     // If the default values for this element have not been loaded yet, populate
     // them.
     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
@@ -412,12 +418,6 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
       $elements['#children'] = '';
     }
 
-    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']);
-    }
-
     // Assume that if #theme is set it represents an implemented hook.
     $theme_is_implemented = isset($elements['#theme']);
     // Check the elements for insecure HTML and pass through sanitization.
diff --git a/core/lib/Drupal/Core/Render/theme.api.php b/core/lib/Drupal/Core/Render/theme.api.php
index 689dce0..fd61a9c 100644
--- a/core/lib/Drupal/Core/Render/theme.api.php
+++ b/core/lib/Drupal/Core/Render/theme.api.php
@@ -271,10 +271,26 @@
  *   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_allowed_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::escape() then this should be set to
+ *   Markup::SAFE_STRATEGY_ESCAPE
+ * - #markup_allowed_tags: If #markup is supplied this can be used to change
+ *   which tags are using to filter the markup. The value should be an array of
+ *   tags that Xss::filter() would accept. If #markup_safe_strategy is set to
+ *   Markup::SAFE_STRATEGY_ESCAPE this value is ignored.
  *   @see core.libraries.yml
  *   @see hook_theme()
+ *   @see \Drupal\Component\Utility\Html::escape()
+ *   @see \Drupal\Component\Utility\Xss::filter()
+ *   @see \Drupal\Component\Utility\Xss::adminFilter()
+ *   @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_FILTER
+ *   @see \Drupal\Core\Render\Element\Markup::SAFE_STRATEGY_ESCAPE
  *
  * JavaScript and CSS assets are specified in the render array using the
  * #attached property (see @ref sec_attached).
diff --git a/core/modules/aggregator/aggregator.module b/core/modules/aggregator/aggregator.module
index 744861c..20e2555 100644
--- a/core/modules/aggregator/aggregator.module
+++ b/core/modules/aggregator/aggregator.module
@@ -157,16 +157,15 @@ function aggregator_cron() {
 }
 
 /**
- * Renders the HTML content safely, as allowed.
+ * Gets the list of allowed tags.
  *
- * @param string $value
- *   The content to be filtered.
+ * @return array
+ *   The list of allowed tags.
  *
- * @return string
- *   The filtered content.
+ * @internal
  */
-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_allowed_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..2044611 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_allowed_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/aggregator/src/FeedViewBuilder.php b/core/modules/aggregator/src/FeedViewBuilder.php
index 8e4152b..08cec5a 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_allowed_tags' => _aggregator_allowed_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..4cda119 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_allowed_tags' => _aggregator_allowed_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..8c12fb8 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_allowed_tags' => _aggregator_allowed_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..7e5c20a 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_allowed_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_allowed_tags());
       $output = $renderer->executeInRenderContext(new RenderContext(), function () use ($view, $row) {
         return $view->field['description']->advancedRender($row);
       });
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 14f3873..e199538 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -46,7 +46,7 @@ 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 = FilteredString::create(Xss::filter($caption, array('a', 'em', 'strong', 'cite', 'code', 'br')));
 
         // The caption must be non-empty.
         if (Unicode::strlen($caption) === 0) {
diff --git a/core/modules/menu_ui/src/Controller/MenuController.php b/core/modules/menu_ui/src/Controller/MenuController.php
index 560bb18..5a8310d 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_allowed_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/node/src/Controller/NodeController.php b/core/modules/node/src/Controller/NodeController.php
index 3291f33..92788a5 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_allowed_tags' => Xss::getHtmlTagList()],
           ],
         ],
       ];
diff --git a/core/modules/search/search.module b/core/modules/search/search.module
index e7ee180..8bd4e84 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\Element\Markup;
 use Drupal\Core\Routing\RouteMatchInterface;
 
 /**
@@ -628,8 +629,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.
@@ -722,7 +723,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' => Markup::SAFE_STRATEGY_ESCAPE,
+    ];
   }
 
   // Sort the text ranges by starting position.
@@ -768,7 +772,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_allowed_tags' => ['strong']
+  ];
 }
 
 /**
diff --git a/core/modules/search/src/Tests/SearchExcerptTest.php b/core/modules/search/src/Tests/SearchExcerptTest.php
index dd6b259..9f4a5ad2 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, stripped of HTML tags, 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/search/tests/modules/search_embedded_form/search_embedded_form.module b/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module
index 6ce623d..e541f10 100644
--- a/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module
+++ b/core/modules/search/tests/modules/search_embedded_form/search_embedded_form.module
@@ -16,5 +16,5 @@
  */
 function search_embedded_form_preprocess_search_result(&$variables) {
   $form = \Drupal::formBuilder()->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
-  $variables['snippet'] = ['#markup' => SafeMarkup::escape($variables['snippet']), $form];
+  $variables['snippet'] = array_merge($variables['snippet'], $form);
 }
diff --git a/core/modules/taxonomy/src/Controller/TaxonomyController.php b/core/modules/taxonomy/src/Controller/TaxonomyController.php
index 2e356788..de47f3f 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_allowed_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_allowed_tags' => Xss::getHtmlTagList()];
   }
 
 }
diff --git a/core/modules/user/src/Controller/UserController.php b/core/modules/user/src/Controller/UserController.php
index a52372d..fe350fb 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_allowed_tags' => Xss::getHtmlTagList()] : '';
   }
 
   /**
diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php
index 97db7c9..4d42c1c 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_allowed_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..1f079f0 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_allowed_tags' => Xss::getHtmlTagList()],
       );
     }
     return $render;
diff --git a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
index f054b91..18ea94c 100644
--- a/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
+++ b/core/modules/views/tests/src/Unit/Controller/ViewAjaxControllerTest.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\Tests\views\Unit\Controller {
 
+use Drupal\Core\Render\Element\Markup;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Tests\UnitTestCase;
 use Drupal\views\Ajax\ViewAjaxResponse;
@@ -90,12 +91,20 @@ protected function setUp() {
 
     $this->viewAjaxController = new ViewAjaxController($this->viewStorage, $this->executableFactory, $this->renderer, $this->currentPath, $this->redirectDestination);
 
+    $element_info_manager = $this->getMock('\Drupal\Core\Render\ElementInfoManagerInterface');
+    $element_info_manager->expects($this->any())
+      ->method('getInfo')
+      ->with('markup')
+      ->willReturn([
+        '#pre_render' => [[Markup::class, 'ensureMarkupIsSafe']],
+        '#defaults_loaded' => TRUE,
+      ]);
     $request_stack = new RequestStack();
     $request_stack->push(new Request());
     $args = [
       $this->getMock('\Drupal\Core\Controller\ControllerResolverInterface'),
       $this->getMock('\Drupal\Core\Theme\ThemeManagerInterface'),
-      $this->getMock('\Drupal\Core\Render\ElementInfoManagerInterface'),
+      $element_info_manager,
       $this->getMock('\Drupal\Core\Render\RenderCacheInterface'),
       $request_stack,
       [
diff --git a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
index bfa4cb3..6e2e6cf 100644
--- a/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
+++ b/core/tests/Drupal/Tests/Component/Utility/SafeMarkupTest.php
@@ -217,36 +217,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/RendererBubblingTest.php b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
index b9bb4e5..c621e62 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererBubblingTest.php
@@ -38,9 +38,6 @@ public function testBubblingWithoutPreRender() {
     $this->setUpRequest();
     $this->setupMemoryCache();
 
-    $this->elementInfo->expects($this->any())
-      ->method('getInfo')
-      ->willReturn([]);
     $this->cacheContextsManager->expects($this->any())
       ->method('convertTokensToKeys')
       ->willReturnArgument(0);
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
index e08b82e..bf8406d 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererPlaceholdersTest.php
@@ -688,10 +688,6 @@ public function testRenderChildrenPlaceholdersDifferentArguments() {
     $this->cacheContextsManager->expects($this->any())
       ->method('convertTokensToKeys')
       ->willReturnArgument(0);
-    $this->elementInfo->expects($this->any())
-      ->method('getInfo')
-      ->with('details')
-      ->willReturn(['#theme_wrappers' => ['details']]);
     $this->controllerResolver->expects($this->any())
       ->method('getControllerFromDefinition')
       ->willReturnArgument(0);
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index d48346c..37e1aac 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\Element\Markup;
 use Drupal\Core\Render\SafeString;
 use Drupal\Core\Template\Attribute;
 
@@ -108,6 +109,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_allowed_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_allowed_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' => Markup::SAFE_STRATEGY_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"],
@@ -170,10 +187,6 @@ public function providerTestRenderBasic() {
           $attributes = new Attribute(['href' => $vars['#url']] + (isset($vars['#attributes']) ? $vars['#attributes'] : []));
           return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>';
         });
-      $this->elementInfo->expects($this->atLeastOnce())
-        ->method('getInfo')
-        ->with('link')
-        ->willReturn(['#theme' => 'link']);
     };
     $data[] = [$build, '<div class="baz"><a href="https://www.drupal.org" id="foo">bar</a></div>' . "\n", $setup_code_type_link];
 
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
index 4f9a333..905f633 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTestBase.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Cache\Context\ContextCacheKeys;
 use Drupal\Core\Cache\MemoryBackend;
 use Drupal\Core\Render\Element;
+use Drupal\Core\Render\Element\Markup;
 use Drupal\Core\Render\RenderCache;
 use Drupal\Core\Render\Renderer;
 use Drupal\Tests\UnitTestCase;
@@ -112,6 +113,25 @@ protected function setUp() {
     $this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
     $this->themeManager = $this->getMock('Drupal\Core\Theme\ThemeManagerInterface');
     $this->elementInfo = $this->getMock('Drupal\Core\Render\ElementInfoManagerInterface');
+    $this->elementInfo->expects($this->any())
+      ->method('getInfo')
+      ->will($this->returnCallback(function ($type) {
+        switch ($type) {
+          case 'details':
+            $info = ['#theme_wrappers' => ['details']];
+            break;
+          case 'link':
+            $info = ['#theme' => 'link'];
+            break;
+          case 'markup':
+            $info = ['#pre_render' => [[Markup::class, 'ensureMarkupIsSafe']]];
+            break;
+          default:
+            $info = [];
+        }
+        $info['#defaults_loaded'] = TRUE;
+        return $info;
+      }));
     $this->requestStack = new RequestStack();
     $request = new Request();
     $request->server->set('REQUEST_TIME', $_SERVER['REQUEST_TIME']);
