diff --git a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
index 5922b3f..7450b03 100644
--- a/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
+++ b/core/lib/Drupal/Component/Utility/PlaceholderTrait.php
@@ -33,7 +33,11 @@ protected static function placeholderFormat($string, array $args, &$safe = TRUE)
     foreach ($args as $key => $value) {
       switch ($key[0]) {
         case '@':
-          // Escaped only.
+          // Escape if not already safe. SafeMarkup::isSafe() may return TRUE
+          // for content that is safe within HTML fragments, but not within
+          // attributes, so this placeholder type must not be used within
+          // attributes. See \Drupal\Component\Utility\SafeMarkup::format()
+          // documentation for details.
           if (!SafeMarkup::isSafe($value)) {
             $args[$key] = Html::escape($value);
           }
@@ -41,7 +45,9 @@ protected static function placeholderFormat($string, array $args, &$safe = TRUE)
 
         case '%':
         default:
-          // Escaped and placeholder.
+          // Escape if not already safe and add wrapping markup to render as a
+          // placeholder. Not for use within attributes, per the warning above
+          // about SafeMarkup::isSafe() and also due to the wrapping markup.
           if (!SafeMarkup::isSafe($value)) {
             $value = Html::escape($value);
           }
@@ -49,11 +55,17 @@ protected static function placeholderFormat($string, array $args, &$safe = TRUE)
           break;
 
         case ':':
-          // URL attributes must be escaped unconditionally (even if they were
-          // already marked safe) since content that has been filtered for XSS
-          // can still contain characters that are unsafe for use in attributes.
-          // @todo decide what to do about non-URL attribute values (#2570431)
-          $args[$key] = Html::escape(UrlHelper::stripDangerousProtocols($value));
+          // Strip URL protocols that can be XSS vectors.
+          $value = UrlHelper::stripDangerousProtocols($value);
+
+          // This placeholder type is for URLs and URLs are not markup, so we
+          // unconditionally Html::escape() rather than checking
+          // SafeMarkup::isSafe(). This placeholder type may therefore be used
+          // within "href" attributes. If a caller wants to pass a value that
+          // is extracted from HTML and therefore is already HTML encoded, it
+          // must invoke Html::decodeEntities() on it prior to passing it in as
+          // a placeholder value of this type.
+          $args[$key] = Html::escape($value);
           break;
 
         case '!':
diff --git a/core/lib/Drupal/Component/Utility/SafeMarkup.php b/core/lib/Drupal/Component/Utility/SafeMarkup.php
index c1c86f5..73fb3f5 100644
--- a/core/lib/Drupal/Component/Utility/SafeMarkup.php
+++ b/core/lib/Drupal/Component/Utility/SafeMarkup.php
@@ -168,24 +168,34 @@ public static function checkPlain($text) {
    * HTML page (especially text that may have come from untrusted users, since
    * in that case it prevents cross-site scripting and other security problems).
    *
-   * This method is not intended for passing arbitrary user input into any
-   * HTML attribute value, as only URL attributes such as "src" and "href" are
-   * supported (using ":variable"). Never use this method on unsafe HTML
-   * attributes such as "on*" and "style" and take care when using this with
-   * unsupported attributes such as "title" or "alt" as this can lead to
-   * unexpected output.
-   *
    * In most cases, you should use t() rather than calling this function
    * directly, since it will translate the text (on non-English-only sites) in
-   * addition to formatting it.
+   * addition to formatting it. Exception messages, messages for test
+   * assertions, and variables concatenated without the insertion of
+   * language-specific words or punctuation are some examples where translation
+   * is not applicable and calling this function directly is appropriate.
+   *
+   * This function is designed for formatting messages that are mostly text,
+   * not as an HTML template language. As such, it is recommended for $string
+   * to contain minimal HTML and to not contain any attributes, other than
+   * "href", with dynamic (placeholder-replaced) values. For any such
+   * non-trivial HTML building, use an HTML template language, such as Twig,
+   * rather than this function. The "href" attribute is supported via the
+   * ":variable" placeholder type, because hyperlinking text within messages is
+   * a common use case for which requiring an HTML template would be
+   * inconvenient. The other placeholder types are unsafe to use within any
+   * attribute and the ":variable" placeholder type may or may not be safe and
+   * may or may not produce the expected output when used for attributes other
+   * than "href".
    *
    * @param string $string
-   *   A string containing placeholders. The string itself is not escaped, any
-   *   unsafe content must be in $args and inserted via placeholders.
+   *   A string containing placeholders. The string itself is treated as
+   *   correctly formatted HTML. Any unsafe content must be in $args and
+   *   inserted via placeholders.
    * @param array $args
    *   An associative array of replacements to make. Occurrences in $string of
    *   any key in $args are replaced with the corresponding value, after
-   *   optional sanitization and formatting. The type of sanitization and
+   *   sanitization and formatting. The type of sanitization and
    *   formatting depends on the first character of the key:
    *   - @variable: Escaped to HTML using Html::escape() unless the value is
    *     already HTML-safe. Use this as the default choice for anything
@@ -198,8 +208,8 @@ public static function checkPlain($text) {
    *     As with @variable, do not use this within HTML attributes.
    *   - :variable: Escaped to HTML using Html::escape() and filtered for
    *     dangerous protocols using UrlHelper::stripDangerousProtocols(). Use
-   *     this when passing in a URL, such as when using the "src" or "href"
-   *     attributes, ensuring the value is always wrapped in quotes:
+   *     this when using the "href" attribute, ensuring the value is always
+   *     wrapped in quotes:
    *     - Secure: <a href=":variable">@variable</a>
    *     - Insecure: <a href=:variable>@variable</a>
    *     When ":variable" comes from arbitrary user input, the result is secure,
diff --git a/core/lib/Drupal/Core/Template/Attribute.php b/core/lib/Drupal/Core/Template/Attribute.php
index 3629cda..7bfceab 100644
--- a/core/lib/Drupal/Core/Template/Attribute.php
+++ b/core/lib/Drupal/Core/Template/Attribute.php
@@ -41,7 +41,7 @@
  *
  * The attribute keys and values are automatically escaped for output with
  * Html::escape(). No protocol filtering is applied, so when using user-entered
- * input as a value for an attribute that expects an URI (href, src, ...),
+ * input as a value for an attribute that expects a URI (href, src, ...),
  * UrlHelper::stripDangerousProtocols() should be used to ensure dangerous
  * protocols (such as 'javascript:') are removed. For example:
  * @code
