diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php
index 2f92b22..750a6fc 100644
--- a/core/modules/filter/src/Element/ProcessedText.php
+++ b/core/modules/filter/src/Element/ProcessedText.php
@@ -8,13 +8,13 @@
 namespace Drupal\filter\Element;
 
 use Drupal\Component\Utility\NestedArray;
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Core\Render\Element\RenderElement;
 use Drupal\Core\Render\Renderer;
 use Drupal\filter\Entity\FilterFormat;
 use Drupal\filter\Plugin\FilterInterface;
+use Drupal\filter\Render\FilteredString;
 
 /**
  * Provides a processed text render element.
@@ -124,7 +124,7 @@ public static function preRenderText($element) {
     // safe, but it has been passed through the filter system and checked with
     // a text format, so it must be printed as is. (See the note about security
     // in the method documentation above.)
-    $element['#markup'] = SafeMarkup::set($text);
+    $element['#markup'] = FilteredString::create($text);
 
     // Set the updated bubbleable rendering metadata and the text format's
     // cache tag.
diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
index ed241d8..14f3873 100644
--- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php
+++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php
@@ -13,6 +13,7 @@
 use Drupal\Component\Utility\Xss;
 use Drupal\filter\FilterProcessResult;
 use Drupal\filter\Plugin\FilterBase;
+use Drupal\filter\Render\FilteredString;
 
 /**
  * Provides a filter to caption elements.
@@ -59,7 +60,10 @@ public function process($text, $langcode) {
         $node->removeAttribute('class');
         $filter_caption = array(
           '#theme' => 'filter_caption',
-          '#node' => SafeMarkup::set($node->C14N()),
+          // We pass the unsanitized string because this is a text format
+          // filter, and after filtering, we always assume the output is safe.
+          // @see \Drupal\filter\Element\ProcessedText::preRenderText()
+          '#node' => FilteredString::create($node->C14N()),
           '#tag' => $node->tagName,
           '#caption' => $caption,
           '#classes' => $classes,
diff --git a/core/modules/filter/src/Render/FilteredString.php b/core/modules/filter/src/Render/FilteredString.php
new file mode 100644
index 0000000..2c694e6
--- /dev/null
+++ b/core/modules/filter/src/Render/FilteredString.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\filter\Render\FilteredString.
+ */
+
+namespace Drupal\filter\Render;
+
+use Drupal\Component\Utility\SafeStringInterface;
+use Drupal\Component\Utility\SafeStringTrait;
+
+/**
+ * Defines an object that passes safe strings through the Filter system.
+ *
+ * This object should only be constructed with a known safe string. If there is
+ * any risk that the string contains user-entered data that has not been
+ * filtered first, it must not be used.
+ *
+ * @internal
+ *   This object is marked as internal because it should only be used in the
+ *   Filter module on strings that have already been been filtered and sanitized
+ *   in \Drupal\filter\Plugin\FilterInterface.
+ *
+ * @see \Drupal\Core\Render\SafeString
+ */
+final class FilteredString implements SafeStringInterface, \Countable {
+  use SafeStringTrait;
+}
diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
index 80ceb75..d0d9761 100644
--- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
+++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php
@@ -1246,7 +1246,6 @@ public function renderText($alter) {
       $value = strip_tags($value, $alter['preserve_tags']);
     }
 
-    $suffix = '';
     if (!empty($alter['trim']) && !empty($alter['max_length'])) {
       $length = strlen($value);
       $value = $this->renderTrimText($alter, $value);
@@ -1267,8 +1266,6 @@ public function renderText($alter) {
         // @todo Views should expect and store a leading /. See
         //   https://www.drupal.org/node/2423913.
         $more_link = \Drupal::l($more_link_text, CoreUrl::fromUserInput('/' . $more_link_path, array('attributes' => array('class' => array('views-more-link')))));
-
-        $suffix .= " " . $more_link;
       }
     }
 
@@ -1283,8 +1280,10 @@ public function renderText($alter) {
       }
       $value = $this->renderAsLink($alter, $value, $tokens);
     }
-
-    return $value . $suffix;
+    if (isset($more_link)) {
+      $value .= " " . $more_link;
+    }
+    return $value;
   }
 
   /**
