diff --git a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
index 3467d34ec8be664f7ae6e227e3d095ea77464333..d57aa63fb625c333bcadf920b50b3e475618cf77 100644
--- a/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
+++ b/core/modules/ckeditor5/ckeditor5.ckeditor5.yml
@@ -62,7 +62,7 @@ ckeditor5_heading:
       - <h5>
       - <h6>
 
-ckeditor5_htmlSupport:
+ckeditor5_arbitraryHtmlSupport:
   ckeditor5:
     plugins: [htmlSupport.GeneralHtmlSupport]
     config:
@@ -82,6 +82,17 @@ ckeditor5_htmlSupport:
     # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface::getEnabledDefinitions()
     conditions: []
 
+ckeditor5_wildcardHtmlSupport:
+  ckeditor5:
+    plugins: [htmlSupport.GeneralHtmlSupport]
+  drupal:
+    label: Wildcard HTML support
+    # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
+    elements: false
+    library: core/ckeditor5.htmlSupport
+    # @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface::getEnabledDefinitions()
+    conditions: []
+
 ckeditor5_specialCharacters:
   ckeditor5:
     plugins:
diff --git a/core/modules/ckeditor5/src/HTMLRestrictions.php b/core/modules/ckeditor5/src/HTMLRestrictions.php
index 08ea69113180ca5a98f2b2f5e4c9f2d826bd223a..c79b9a65a0d01e510497be721d470a5fac2c2b8e 100644
--- a/core/modules/ckeditor5/src/HTMLRestrictions.php
+++ b/core/modules/ckeditor5/src/HTMLRestrictions.php
@@ -770,8 +770,8 @@ public function merge(HTMLRestrictions $other): HTMLRestrictions {
   private static function applyOperation(HTMLRestrictions $a, HTMLRestrictions $b, string $operation_method_name): HTMLRestrictions {
     // 1. Operation applied to wildcard tags that exist in both operands.
     // For example: <$block id> in both operands.
-    $a_wildcard = self::getWildcardSubset($a);
-    $b_wildcard = self::getWildcardSubset($b);
+    $a_wildcard = $a->getWildcardSubset();
+    $b_wildcard = $b->getWildcardSubset();
     $wildcard_op_result = $a_wildcard->$operation_method_name($b_wildcard);
 
     // Early return if both operands contain only wildcard tags.
@@ -828,14 +828,23 @@ private static function getRegExForWildCardAttributeName(string $wildcard_attrib
   /**
    * Gets the subset of allowed elements whose tags are wildcards.
    *
-   * @param \Drupal\ckeditor5\HTMLRestrictions $r
-   *   A set of HTML restrictions.
+   * @return \Drupal\ckeditor5\HTMLRestrictions
+   *   The subset of the given set of HTML restrictions.
+   */
+  public function getWildcardSubset(): HTMLRestrictions {
+    return new self(array_filter($this->elements, [__CLASS__, 'isWildcardTag'], ARRAY_FILTER_USE_KEY));
+  }
+
+  /**
+   * Gets the subset of allowed elements whose tags are concrete.
    *
    * @return \Drupal\ckeditor5\HTMLRestrictions
    *   The subset of the given set of HTML restrictions.
    */
-  private static function getWildcardSubset(HTMLRestrictions $r): HTMLRestrictions {
-    return new self(array_filter($r->elements, [__CLASS__, 'isWildcardTag'], ARRAY_FILTER_USE_KEY));
+  public function getConcreteSubset(): HTMLRestrictions {
+    return new self(array_filter($this->elements, function (string $tag_name) {
+      return !self::isWildcardTag($tag_name);
+    }, ARRAY_FILTER_USE_KEY));
   }
 
   /**
@@ -899,7 +908,7 @@ private static function resolveWildcards(HTMLRestrictions $r): HTMLRestrictions
     // - then $naive will be `<p class="foo">`
     // - merging them yields `<p class> <$block class="foo">` again
     // - diffing the wildcard subsets yields just `<p class>`
-    return $r->merge($naive_resolution)->doDiff(self::getWildcardSubset($r));
+    return $r->merge($naive_resolution)->doDiff($r->getWildcardSubset());
   }
 
   /**
@@ -964,7 +973,10 @@ public function toCKEditor5ElementsArray(): array {
    * @see \Drupal\filter\Plugin\Filter\FilterHtml
    */
   public function toFilterHtmlAllowedTagsString(): string {
-    return implode(' ', $this->toCKEditor5ElementsArray());
+    // Resolve wildcard tags, because Drupal's filter_html filter plugin does
+    // not support those.
+    $concrete = self::resolveWildcards($this);
+    return implode(' ', $concrete->toCKEditor5ElementsArray());
   }
 
   /**
@@ -979,7 +991,16 @@ public function toFilterHtmlAllowedTagsString(): string {
    */
   public function toGeneralHtmlSupportConfig(): array {
     $allowed = [];
-    foreach ($this->elements as $tag => $attributes) {
+    // Resolve any remaining wildcards based on Drupal's assumptions on
+    // wildcards to ensure all HTML tags that Drupal thinks are supported are
+    // truly supported by CKEditor 5. For example: the <$block> wildcard does
+    // NOT correspond to block-level HTML tags, but to CKEditor 5 elements that
+    // behave like blocks. Knowing the list of concrete HTML tags this maps to
+    // is impossible without executing JavaScript, which PHP cannot do. By
+    // generating this GHS configuration, we can guarantee that Drupal's only
+    // possible interpretation also actually works.
+    $elements = self::resolveWildcards($this)->getAllowedElements();
+    foreach ($elements as $tag => $attributes) {
       $to_allow = ['name' => $tag];
       assert($attributes === FALSE || is_array($attributes));
       if (is_array($attributes)) {
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
index 49fe161fe83b12e68b3218829ef8f79e20b47b02..1da134970807b5bf4647ea09be85ca8048c3159e 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
@@ -76,9 +76,13 @@ public function getElementsSubset(): array {
    */
   public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
     $restrictions = HTMLRestrictions::fromString(implode(' ', $this->configuration['allowed_tags']));
+    // Only handle concrete HTML elements to allow the Wildcard HTML support
+    // plugin to handle wildcards.
+    // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
+    $concrete_restrictions = $restrictions->getConcreteSubset();
     return [
       'htmlSupport' => [
-        'allow' => $restrictions->toGeneralHtmlSupportConfig(),
+        'allow' => $concrete_restrictions->toGeneralHtmlSupportConfig(),
       ],
     ];
   }
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
index 295699ee420b9474ab65b6459f002b5819043d34..a3df9c9cb44af8ac409a073e541bc41528c3f339 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
@@ -160,12 +160,12 @@ public function getEnabledDefinitions(EditorInterface $editor): array {
       }
     }
 
-    // Only enable the General HTML Support plugin on text formats with no HTML
-    // restrictions.
+    // Only enable the arbitrary HTML Support plugin on text formats with no
+    // HTML restrictions.
     // @see https://ckeditor.com/docs/ckeditor5/latest/api/html-support.html
     // @see https://github.com/ckeditor/ckeditor5/issues/9856
     if ($editor->getFilterFormat()->getHtmlRestrictions() !== FALSE) {
-      unset($definitions['ckeditor5_htmlSupport']);
+      unset($definitions['ckeditor5_arbitraryHtmlSupport']);
     }
 
     // Evaluate `plugins` condition.
@@ -175,6 +175,22 @@ public function getEnabledDefinitions(EditorInterface $editor): array {
       }
     }
 
+    if (!isset($definitions['ckeditor5_arbitraryHtmlSupport'])) {
+      $restrictions = new HTMLRestrictions($this->getProvidedElements(array_keys($definitions), $editor, FALSE));
+      if ($restrictions->getWildcardSubset()->isEmpty()) {
+        // This is only reached if arbitrary HTML is not enabled. If wildcard
+        // tags (such as $block) are present, they need to be resolved via the
+        // wildcardHtmlSupport plugin.
+        // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
+        unset($definitions['ckeditor5_wildcardHtmlSupport']);
+      }
+    }
+    // When arbitrary HTML is already supported, there is no need to support
+    // wildcard tags.
+    else {
+      unset($definitions['ckeditor5_wildcardHtmlSupport']);
+    }
+
     return $definitions;
   }
 
@@ -254,6 +270,25 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array {
       $config[$plugin_id] = $plugin->getDynamicPluginConfig($definition->getCKEditor5Config(), $editor);
     }
 
+    // CKEditor 5 interprets wildcards from a "CKEditor 5 model element"
+    // perspective, Drupal interprets wildcards from a "HTML element"
+    // perspective. GHS is used to reconcile those two perspectives, to ensure
+    // all expected HTML elements truly are supported.
+    // The `ckeditor5_wildcardHtmlSupport` is automatically enabled when
+    // necessary, and only when necessary.
+    // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getEnabledDefinitions()
+    if (isset($definitions['ckeditor5_wildcardHtmlSupport'])) {
+      $allowed_elements = new HTMLRestrictions($this->getProvidedElements(array_keys($definitions), $editor, FALSE));
+      // Compute the net new elements that the wildcard tags resolve into.
+      $concrete_allowed_elements = $allowed_elements->getConcreteSubset();
+      $net_new_elements = $allowed_elements->diff($concrete_allowed_elements);
+      $config['ckeditor5_wildcardHtmlSupport'] = [
+        'htmlSupport' => [
+          'allow' => $net_new_elements->toGeneralHtmlSupportConfig(),
+        ],
+      ];
+    }
+
     return [
       'plugins' => $this->mergeDefinitionValues('getCKEditor5Plugins', $definitions),
       'config' => NestedArray::mergeDeepArray($config),
@@ -263,7 +298,7 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array {
   /**
    * {@inheritdoc}
    */
-  public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array {
+  public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $resolve_wildcards = TRUE): array {
     $plugins = $this->getDefinitions();
     if (!empty($plugin_ids)) {
       $plugins = array_intersect_key($plugins, array_flip($plugin_ids));
@@ -312,7 +347,7 @@ public function getProvidedElements(array $plugin_ids = [], EditorInterface $edi
       }
     }
 
-    return $elements->getAllowedElements();
+    return $elements->getAllowedElements($resolve_wildcards);
   }
 
   /**
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
index adf8967f914ef23788e35ee27361574eb8111f1d..4c46eac057e70304f5fcf2be3622ab925536eb1d 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
@@ -98,22 +98,29 @@ public function findPluginSupportingElement(string $tag): ?string;
   public function getCKEditor5PluginConfig(EditorInterface $editor): array;
 
   /**
-   * Create a list of elements with attributes declared for the CKEditor5 build.
+   * Gets all supported elements for the given plugins and text editor.
    *
    * @param string[] $plugin_ids
-   *   An array of plugin IDs.
-   * @param \Drupal\editor\EditorInterface $editor
-   *   A configured text editor object.
+   *   (optional) An array of CKEditor 5 plugin IDs. When not set, gets elements
+   *   for all plugins.
+   * @param \Drupal\editor\EditorInterface|null $editor
+   *   (optional) A configured text editor object using CKEditor 5. When not
+   *   set, plugins depending on the text editor cannot provide elements.
+   * @param bool $resolve_wildcards
+   *   (optional) Whether to resolve wildcards. Defaults to TRUE. When set to
+   *   FALSE, the raw allowed elements will be returned (with no processing
+   *   applied hence no resolved wildcards).
    *
    * @return array
    *   A nested array with a structure as described in
    *   \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions().
    *
    * @throws \LogicException
-   *   Thrown when an invalid CKEditor5PluginElementsSubsetInterface implementation is encountered.
+   *   Thrown when an invalid CKEditor5PluginElementsSubsetInterface
+   *   implementation is encountered.
    *
    * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
    */
-  public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array;
+  public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $resolve_wildcards = TRUE): array;
 
 }
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
index fe62d144218f1446531fa95da91510ac3482afc1..ca0854c693ca4e2ad4c6df5c3d227613fd277a9f 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/CKEditor5ElementConstraintValidator.php
@@ -4,7 +4,7 @@
 
 namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
 
-use Drupal\Component\Utility\Html;
+use Drupal\ckeditor5\HTMLRestrictions;
 use Symfony\Component\Validator\ConstraintValidator;
 use Symfony\Component\Validator\Exception\UnexpectedTypeException;
 
@@ -25,9 +25,9 @@ public function validate($element, $constraint) {
     if (!$constraint instanceof CKEditor5ElementConstraint) {
       throw new UnexpectedTypeException($constraint, __NAMESPACE__ . '\CKEditor5Element');
     }
-    $body_child_nodes = Html::load(str_replace('>', ' />', trim($element)))->getElementsByTagName('body')->item(0)->childNodes;
 
-    if ($body_child_nodes->count() !== 1 || $body_child_nodes->item(0)->nodeType !== XML_ELEMENT_NODE) {
+    $parsed = HTMLRestrictions::fromString($element);
+    if ($parsed->isEmpty() || count($parsed->getAllowedElements()) > 1 || $element !== $parsed->toCKEditor5ElementsArray()[0]) {
       $this->context->buildViolation($constraint->message)
         ->setParameter('%provided_element', $element)
         ->addViolation();
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
index a07fad7d021bd8a900519642f642897a08a26557..44cb5252f6e1c5a1f0ca22825fe459dde05b6c2c 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
@@ -46,12 +46,15 @@ public function validate($value, Constraint $constraint) {
     // The single tag for which source editing is enabled, which we are checking
     // now.
     $source_enabled_tags = HTMLRestrictions::fromString($value);
+    // Test for empty allowed elements with resolved wildcards since, for the
+    // purposes of this validator, HTML restrictions containing only wildcards
+    // should be considered empty.
     // @todo Remove this early return in
     //   https://www.drupal.org/project/drupal/issues/2820364. It is only
     //   necessary because CKEditor5ElementConstraintValidator does not run
     //   before this, which means that this validator cannot assume it receives
     //   valid values.
-    if ($source_enabled_tags->isEmpty() || count($source_enabled_tags->getAllowedElements()) > 1) {
+    if (count($source_enabled_tags->getAllowedElements()) !== 1) {
       return;
     }
     // This validation constraint currently only validates tags, not attributes;
diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php
index 692c9db22f2ccf6f71a14c5cb3117e7cbc9bee3f..4542392e7d88a0d5cf3a3ef9d74cbd090e09aabf 100644
--- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php
+++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/SourceEditingTest.php
@@ -13,6 +13,7 @@
 
 /**
  * @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
+ * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
  * @group ckeditor5
  * @internal
  */
@@ -61,7 +62,7 @@ protected function setUp(): void {
         'filter_html' => [
           'status' => TRUE,
           'settings' => [
-            'allowed_html' => '<p> <br> <a href>',
+            'allowed_html' => '<div class> <p> <br> <a href>',
           ],
         ],
         'filter_align' => ['status' => TRUE],
@@ -80,7 +81,7 @@ protected function setUp(): void {
         ],
         'plugins' => [
           'ckeditor5_sourceEditing' => [
-            'allowed_tags' => [],
+            'allowed_tags' => ['<div class>'],
           ],
         ],
       ],
@@ -107,7 +108,7 @@ function (ConstraintViolation $v) {
       'type' => 'page',
       'title' => 'Animals with strange names',
       'body' => [
-        'value' => '<p>The <a href="https://example.com/pirate" class="button" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" class="use-ajax" data-grammar="adjective">irate</a>.</p>',
+        'value' => '<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate" class="button" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" class="use-ajax" data-grammar="adjective">irate</a>.</p></div>',
         'format' => 'test_format',
       ],
     ]);
@@ -168,69 +169,80 @@ function (ConstraintViolation $v) {
   public function providerAllowingExtraAttributes(): array {
     return [
       'no extra attributes allowed' => [
-        '<p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
       ],
 
       // Common case: any attribute that is not `style` or `class`.
       '<a data-grammar="subject">' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
         '<a data-grammar="subject">',
       ],
       '<a data-grammar="adjective">' => [
-        '<p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
         '<a data-grammar="adjective">',
       ],
       '<a data-grammar>' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
         '<a data-grammar>',
       ],
 
       // Edge case: `class`.
       '<a class="button">' => [
-        '<p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
         '<a class="button">',
       ],
       '<a class="use-ajax">' => [
-        '<p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
         '<a class="use-ajax">',
       ],
       '<a class>' => [
-        '<p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a class="button" href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
         '<a class>',
       ],
 
+      // Edge case: $block wildcard with additional attribute.
+      '<$block data-llama>' => [
+        '<div class="llama" data-llama="🦙"><p data-llama="🦙">The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
+        '<$block data-llama>',
+      ],
+      // Edge case: $block wildcard with stricter attribute constrain.
+      '<$block class="not-llama">' => [
+        '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
+        '<$block class="not-llama">',
+      ],
+
       // Edge case: wildcard attribute names:
       // - prefix, f.e. `data-*`
       // - infix, f.e. `*gramma*`
       // - suffix, f.e. `*-grammar`
       '<a data-*>' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
         '<a data-*>',
       ],
       '<a *gramma*>' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
         '<a *gramma*>',
       ],
       '<a *-grammar>' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate" data-grammar="adjective">irate</a>.</p></div>',
         '<a *-grammar>',
       ],
 
       // Edge case: concrete attribute with wildcard class value.
       '<a class="use-*">' => [
-        '<p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate">pirate</a> is <a class="use-ajax" href="https://example.com/irate">irate</a>.</p></div>',
         '<a class="use-*">',
       ],
 
       // Edge case: concrete attribute with wildcard attribute value.
       '<a data-grammar="sub*">' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
         '<a data-grammar="sub*">',
       ],
 
       // Edge case: `data-*` with wildcard attribute value.
       '<a data-*="sub*">' => [
-        '<p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p>',
+        '<div class="llama"><p>The <a href="https://example.com/pirate" data-grammar="subject">pirate</a> is <a href="https://example.com/irate">irate</a>.</p></div>',
         '<a data-*="sub*">',
       ],
 
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index 03d642bc964abfc1b22d0a2d6190080259f5445e..98654422b0ce72884071996eb1e04fe4e884c5a7 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -1113,16 +1113,34 @@ public function testEnabledPlugins() {
     sort($expected_libraries);
     $this->assertSame($expected_libraries, $this->manager->getEnabledLibraries($editor));
 
-    // Case 7: GHS is only enabled for Full HTML (or any other text format that
-    // has no TYPE_HTML_RESTRICTOR filters).
+    // Case 7: GHS is enabled for other text editors if they are using a
+    // CKEditor 5 plugin that uses wildcard tags.
+    $settings['toolbar']['items'][] = 'alignment:center';
+    $editor->setSettings($settings);
+    $plugin_ids = array_keys($this->manager->getEnabledDefinitions($editor));
+    $expected_plugins = array_merge($expected_plugins, [
+      'ckeditor5_alignment.center',
+      'ckeditor5_wildcardHtmlSupport',
+    ]);
+    sort($expected_plugins);
+    $this->assertSame(array_values($expected_plugins), $plugin_ids);
+    $expected_libraries = array_merge($expected_libraries, [
+      'core/ckeditor5.alignment',
+      'core/ckeditor5.htmlSupport',
+    ]);
+    sort($expected_libraries);
+    $this->assertSame($expected_libraries, $this->manager->getEnabledLibraries($editor));
+
+    // Case 8: GHS is enabled for Full HTML (or any other text format that has
+    // no TYPE_HTML_RESTRICTOR filters).
     $editor = Editor::load('full_html');
     $definitions = array_keys($this->manager->getEnabledDefinitions($editor));
     $default_plugins = [
+      'ckeditor5_arbitraryHtmlSupport',
       'ckeditor5_bold',
       'ckeditor5_emphasis',
       'ckeditor5_essentials',
       'ckeditor5_heading',
-      'ckeditor5_htmlSupport',
       'ckeditor5_paragraph',
       'ckeditor5_pasteFromOffice',
     ];
@@ -1165,6 +1183,10 @@ public function testProvidedElements(array $plugins, array $text_editor_settings
       'settings' => $text_editor_settings,
       'image_upload' => [],
     ]);
+    FilterFormat::create([
+      'format' => 'dummy',
+      'name' => 'dummy',
+    ])->save();
     $this->assertConfigSchema(
       $this->typedConfig,
       $text_editor->getConfigDependencyName(),
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
index b879e408e682f69074e4eac0485d071d8d586924..daf3b5162a067f460cf9155f0b337f360d4b7a1e 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
@@ -813,6 +813,38 @@ public function providerPair(): array {
         'settings.toolbar.items.0' => 'The <em class="placeholder">Drupal media</em> toolbar item requires the <em class="placeholder">Embed media</em> filter to be enabled.',
       ],
     ];
+    $data['VALID: HTML format: very minimal toolbar + wildcard in source editing HTML'] = [
+      'settings' => [
+        'toolbar' => [
+          'items' => [
+            'bold',
+            'sourceEditing',
+          ],
+        ],
+        'plugins' => [
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => ['<$block data-llama>'],
+          ],
+        ],
+      ],
+      'image_upload' => [
+        'status' => FALSE,
+      ],
+      'filters' => [
+        'filter_html' => [
+          'id' => 'filter_html',
+          'provider' => 'filter',
+          'status' => TRUE,
+          'weight' => 0,
+          'settings' => [
+            'allowed_html' => '<p data-llama> <br> <strong>',
+            'filter_html_help' => TRUE,
+            'filter_html_nofollow' => TRUE,
+          ],
+        ],
+      ],
+      'violations' => [],
+    ];
 
     return $data;
   }
diff --git a/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php b/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..f936d37ab1ffceaa004396f9e69c4b48dcf35060
--- /dev/null
+++ b/core/modules/ckeditor5/tests/src/Kernel/WildcardHtmlSupportTest.php
@@ -0,0 +1,227 @@
+<?php
+
+namespace Drupal\Tests\ckeditor5\Kernel;
+
+use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
+use Drupal\editor\Entity\Editor;
+use Drupal\filter\Entity\FilterFormat;
+use Drupal\KernelTests\KernelTestBase;
+use Symfony\Component\Validator\ConstraintViolation;
+
+/**
+ * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
+ * @group ckeditor5
+ * @internal
+ */
+class WildcardHtmlSupportTest extends KernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'ckeditor5',
+    'filter',
+    'editor',
+  ];
+
+  /**
+   * The manager for "CKEditor 5 plugin" plugins.
+   *
+   * @var \Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface
+   */
+  protected $manager;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+    $this->manager = $this->container->get('plugin.manager.ckeditor5.plugin');
+  }
+
+  /**
+   * @covers \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing::getDynamicPluginConfig()
+   * @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getCKEditor5PluginConfig()
+   * @dataProvider providerGhsConfiguration
+   */
+  public function testGhsConfiguration(string $filter_html_allowed, array $source_editing_tags, array $expected_ghs_configuration, ?array $additional_toolbar_items = []): void {
+    FilterFormat::create([
+      'format' => 'test_format',
+      'name' => 'Test format',
+      'filters' => [
+        'filter_html' => [
+          'status' => TRUE,
+          'settings' => [
+            'allowed_html' => $filter_html_allowed,
+          ],
+        ],
+      ],
+    ])->save();
+    $editor = Editor::create([
+      'editor' => 'ckeditor5',
+      'format' => 'test_format',
+      'settings' => [
+        'toolbar' => [
+          'items' => array_merge(['sourceEditing'], $additional_toolbar_items),
+        ],
+        'plugins' => [
+          'ckeditor5_sourceEditing' => [
+            'allowed_tags' => $source_editing_tags,
+          ],
+        ],
+      ],
+      'image_upload' => [
+        'status' => FALSE,
+      ],
+    ]);
+    $editor->save();
+    $this->assertSame([], array_map(
+      function (ConstraintViolation $v) {
+        return (string) $v->getMessage();
+      },
+      iterator_to_array(CKEditor5::validatePair(
+        Editor::load('test_format'),
+        FilterFormat::load('test_format')
+      ))
+    ));
+    $config = $this->manager->getCKEditor5PluginConfig($editor);
+    $this->assertEquals($expected_ghs_configuration, $config['config']['htmlSupport']['allow']);
+  }
+
+  public function providerGhsConfiguration(): array {
+    return [
+      'empty source editing' => [
+        '<p> <br>',
+        [],
+        [],
+      ],
+      'without wildcard' => [
+        '<p> <br> <a href> <blockquote> <div data-llama>',
+        ['<div data-llama>'],
+        [
+          [
+            'name' => 'div',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+        ],
+        ['link', 'blockQuote'],
+      ],
+      '<$block> minimal configuration' => [
+        '<p data-llama> <br>',
+        ['<$block data-llama>'],
+        [
+          [
+            'name' => 'p',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+        ],
+      ],
+      '<$block> from multiple plugins' => [
+        '<p data-llama class="text-align-left text-align-center text-align-right text-align-justify"> <br>',
+        ['<$block data-llama>'],
+        [
+          [
+            'name' => 'p',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+            'classes' => [
+              'regexp' => [
+                'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/',
+              ],
+            ],
+          ],
+        ],
+        ['alignment'],
+      ],
+      '<$block> with attribute from multiple plugins' => [
+        '<p data-llama class"> <br>',
+        ['<$block data-llama>', '<p class>'],
+        [
+          [
+            'name' => 'p',
+            'classes' => TRUE,
+          ],
+          [
+            'name' => 'p',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+            'classes' => [
+              'regexp' => [
+                'pattern' => '/^(text-align-left|text-align-center|text-align-right|text-align-justify)$/',
+              ],
+            ],
+          ],
+        ],
+        ['alignment'],
+      ],
+      '<$block> realistic configuration' => [
+        '<p data-llama> <br> <a href> <blockquote data-llama> <div data-llama> <mark> <abbr title>',
+        ['<$block data-llama>', '<div>', '<mark>', '<abbr title>'],
+        [
+          [
+            'name' => 'div',
+          ],
+          [
+            'name' => 'mark',
+          ],
+          [
+            'name' => 'abbr',
+            'attributes' => [
+              [
+                'key' => 'title',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+          [
+            'name' => 'p',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+          [
+            'name' => 'div',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+          [
+            'name' => 'blockquote',
+            'attributes' => [
+              [
+                'key' => 'data-llama',
+                'value' => TRUE,
+              ],
+            ],
+          ],
+        ],
+        ['link', 'blockQuote'],
+      ],
+    ];
+  }
+
+}
diff --git a/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
index e0b7669f0d9e74a4ef80fcc75f1efbf667080558..0a3628ad97722f42e9182cc0e2e7d6cc2e6b32a0 100644
--- a/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
+++ b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
@@ -455,6 +455,39 @@ public function providerRepresentations(): \Generator {
       ],
     ];
 
+    yield '$block wildcard' => [
+      new HTMLRestrictions(['$block' => ['class' => TRUE, 'data-llama' => TRUE], 'div' => FALSE, 'span' => FALSE, 'blockquote' => ['cite' => TRUE]]),
+      ['<$block class data-llama>', '<div>', '<span>', '<blockquote cite>'],
+      '<div class data-llama> <span> <blockquote cite class data-llama>',
+      [
+        [
+          'name' => 'div',
+          'classes' => TRUE,
+          'attributes' => [
+            [
+              'key' => 'data-llama',
+              'value' => TRUE,
+            ],
+          ],
+        ],
+        ['name' => 'span'],
+        [
+          'name' => 'blockquote',
+          'attributes' => [
+            [
+              'key' => 'cite',
+              'value' => TRUE,
+            ],
+            [
+              'key' => 'data-llama',
+              'value' => TRUE,
+            ],
+          ],
+          'classes' => TRUE,
+        ],
+      ],
+    ];
+
     yield 'realistic' => [
       new HTMLRestrictions(['a' => ['href' => TRUE, 'hreflang' => ['en' => TRUE, 'fr' => TRUE]], 'p' => ['data-*' => TRUE, 'class' => ['block' => TRUE]], 'br' => FALSE]),
       ['<a href hreflang="en fr">', '<p data-* class="block">', '<br>'],
@@ -498,12 +531,12 @@ public function providerRepresentations(): \Generator {
 
     // Wildcard tag, attribute and attribute value.
     yield '$block' => [
-      new HTMLRestrictions(['$block' => ['data-*' => TRUE]]),
-      ['<$block data-*>'],
-      '<$block data-*>',
+      new HTMLRestrictions(['p' => FALSE, '$block' => ['data-*' => TRUE]]),
+      ['<p>', '<$block data-*>'],
+      '<p data-*>',
       [
         [
-          'name' => '$block',
+          'name' => 'p',
           'attributes' => [
             [
               'key' => [
@@ -1080,4 +1113,40 @@ public function providerOperands(): \Generator {
     }
   }
 
+  /**
+   * @covers ::getWildcardSubset
+   * @covers ::getConcreteSubset
+   * @dataProvider providerSubsets
+   */
+  public function testSubsets(HTMLRestrictions $input, HTMLRestrictions $expected_wildcard_subset, HTMLRestrictions $expected_concrete_subset): void {
+    $this->assertEquals($expected_wildcard_subset, $input->getWildcardSubset());
+    $this->assertEquals($expected_concrete_subset, $input->getConcreteSubset());
+  }
+
+  public function providerSubsets(): \Generator {
+    yield 'empty set' => [
+      new HTMLRestrictions([]),
+      new HTMLRestrictions([]),
+      new HTMLRestrictions([]),
+    ];
+
+    yield 'without wildcards' => [
+      new HTMLRestrictions(['div' => FALSE]),
+      new HTMLRestrictions([]),
+      new HTMLRestrictions(['div' => FALSE]),
+    ];
+
+    yield 'with wildcards' => [
+      new HTMLRestrictions(['div' => FALSE, '$block' => ['data-llama' => TRUE]]),
+      new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]),
+      new HTMLRestrictions(['div' => FALSE]),
+    ];
+
+    yield 'only wildcards' => [
+      new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]),
+      new HTMLRestrictions(['$block' => ['data-llama' => TRUE]]),
+      new HTMLRestrictions([]),
+    ];
+  }
+
 }