Issue #3260853 by Wim Leers, bnjmnm: [GHS] Partial wildcard attributes (<foo data-*>, <foo *-bar-*>, <foo *-bar>) and attribute values (<h2 id="jump-*">) not yet supported

merge-requests/1919/head
Lauri Eskola 2022-03-02 14:09:25 +02:00
parent c91106efb3
commit 359a11a77c
No known key found for this signature in database
GPG Key ID: 382FC0F5B0DF53F8
4 changed files with 429 additions and 26 deletions

View File

@ -164,6 +164,9 @@ final class HTMLRestrictions {
if (trim($html_tag_attribute_name) !== $html_tag_attribute_name) {
throw new \InvalidArgumentException(sprintf('The "%s" HTML tag has an attribute restriction "%s" which contains whitespace. Omit the whitespace.', $html_tag_name, $html_tag_attribute_name));
}
if ($html_tag_attribute_name === '*') {
throw new \InvalidArgumentException(sprintf('The "%s" HTML tag has an attribute restriction "*". This implies all attributes are allowed. Remove the attribute restriction instead, or use a prefix (`*-foo`), infix (`*-foo-*`) or suffix (`foo-*`) wildcard restriction instead.', $html_tag_name));
}
}
}
}
@ -413,6 +416,40 @@ final class HTMLRestrictions {
ARRAY_FILTER_USE_BOTH
);
// Special case: wildcard attributes, and the ability to define restrictions
// for all concrete attributes matching them using:
// - prefix wildcard, f.e. `data-*`, to match `data-foo`, `data-bar`, etc.
// - infix wildcard, f.e. `*-entity-*`
// - suffix wildcard, f.e. `foo-*`
foreach ($diff_elements as $tag => $tag_config) {
// If there are no per-attribute restrictions for this tag in either
// operand, then no wildcard attribute postprocessing is needed.
if (!(isset($other->elements[$tag]) && is_array($other->elements[$tag]))) {
continue;
}
$wildcard_attributes = array_filter(array_keys($other->elements[$tag]), [__CLASS__, 'isWildcardAttributeName']);
foreach ($wildcard_attributes as $wildcard_attribute_name) {
$regex = self::getRegExForWildCardAttributeName($wildcard_attribute_name);
foreach ($tag_config as $html_tag_attribute_name => $html_tag_attribute_restrictions) {
// If a wildcard attribute name (f.e. `data-*`) is allowed in $other
// with the same attribute value restrictions (e.g. TRUE to allow all
// attribute values or an array of specific allowed attribute values),
// then all concrete matches (f.e. `data-foo`, `data-bar`, etc.) are
// allowed and should be explicitly omitted from the difference.
if ($html_tag_attribute_restrictions === $other->elements[$tag][$wildcard_attribute_name] && preg_match($regex, $html_tag_attribute_name) === 1) {
unset($tag_config[$html_tag_attribute_name]);
}
}
if ($tag_config !== []) {
$diff_elements[$tag] = $tag_config;
}
else {
unset($diff_elements[$tag]);
}
}
}
return new self($diff_elements);
}
@ -506,6 +543,52 @@ final class HTMLRestrictions {
}
}
// Special case: wildcard attributes, and the ability to define restrictions
// for all concrete attributes matching them using:
// - prefix wildcard, f.e. `data-*`, to match `data-foo`, `data-bar`, etc.
// - infix wildcard, f.e. `*-entity-*`
// - suffix wildcard, f.e. `foo-*`
foreach ($intersection as $tag => $tag_config) {
// If there are no per-attribute restrictions for this tag in either
// operand, then no wildcard attribute postprocessing is needed.
if (!(is_array($this->elements[$tag]) && is_array($other->elements[$tag]))) {
continue;
}
$other_wildcard_attributes = array_filter(array_keys($other->elements[$tag]), [__CLASS__, 'isWildcardAttributeName']);
$this_wildcard_attributes = array_filter(array_keys($this->elements[$tag]), [__CLASS__, 'isWildcardAttributeName']);
// If the same wildcard attribute restrictions are present in both or
// neither, no adjustment necessary: the intersection is already correct.
$in_both = array_intersect($other_wildcard_attributes, $this_wildcard_attributes);
$other_wildcard_attributes = array_diff($other_wildcard_attributes, $in_both);
$this_wildcard_attributes = array_diff($this_wildcard_attributes, $in_both);
$wildcard_attributes_to_analyze = array_merge($other_wildcard_attributes, $this_wildcard_attributes);
if (empty($wildcard_attributes_to_analyze)) {
continue;
}
// Otherwise, the wildcard attribute name (f.e. `data-*`) is allowed in
// one of the two with the same attribute value restrictions (e.g. TRUE to
// allow all attribute values, or an array of specific allowed attribute
// values), and the intersection must contain the most restrictive
// configuration.
foreach ($wildcard_attributes_to_analyze as $wildcard_attribute_name) {
$other_has_wildcard = isset($other->elements[$tag][$wildcard_attribute_name]);
$wildcard_operand = $other_has_wildcard ? $other : $this;
$concrete_operand = $other_has_wildcard ? $this : $other;
$concrete_tag_config = $concrete_operand->elements[$tag];
$wildcard_attribute_restriction = $wildcard_operand->elements[$tag][$wildcard_attribute_name];
$regex = self::getRegExForWildCardAttributeName($wildcard_attribute_name);
foreach ($concrete_tag_config as $html_tag_attribute_name => $html_tag_attribute_restrictions) {
if ($html_tag_attribute_restrictions === $wildcard_attribute_restriction && preg_match($regex, $html_tag_attribute_name) === 1) {
$tag_config = $tag_config === FALSE ? [] : $tag_config;
$tag_config[$html_tag_attribute_name] = $html_tag_attribute_restrictions;
}
}
$intersection[$tag] = $tag_config;
}
}
return new self($intersection);
}
@ -598,6 +681,39 @@ final class HTMLRestrictions {
}
}
}
// Special case: wildcard attributes, and the ability to define restrictions
// for all concrete attributes matching them using:
// - prefix wildcard, f.e. `data-*`, to match `data-foo`, `data-bar`, etc.
// - infix wildcard, f.e. `*-entity-*`
// - suffix wildcard, f.e. `foo-*`
foreach ($union as $tag => $tag_config) {
// If there are no per-attribute restrictions for this tag, then no
// wildcard attribute postprocessing is needed.
if (!is_array($tag_config)) {
continue;
}
$wildcard_attributes = array_filter(array_keys($tag_config), [__CLASS__, 'isWildcardAttributeName']);
foreach ($wildcard_attributes as $wildcard_attribute_name) {
$regex = self::getRegExForWildCardAttributeName($wildcard_attribute_name);
foreach ($tag_config as $html_tag_attribute_name => $html_tag_attribute_restrictions) {
// The wildcard attribute restriction itself must be kept.
if ($html_tag_attribute_name === $wildcard_attribute_name) {
continue;
}
// If a concrete attribute restriction (f.e. `data-foo`, `data-bar`,
// etc.) exists whose attribute value restrictions are the same as the
// wildcard attribute value restrictions (f.e. `data-*`), we must
// explicitly drop the concrete attribute restriction in favor of the
// wildcard one.
if ($html_tag_attribute_restrictions === $tag_config[$wildcard_attribute_name] && preg_match($regex, $html_tag_attribute_name) === 1) {
unset($tag_config[$html_tag_attribute_name]);
}
}
$union[$tag] = $tag_config;
}
}
return new self($union);
}
@ -643,6 +759,35 @@ final class HTMLRestrictions {
return new self($concrete_op_result->elements + $wildcard_op_result->elements);
}
/**
* Checks whether the given attribute name contains a wildcard, e.g. `data-*`.
*
* @param string $attribute_name
* The attribute name to check.
*
* @return bool
* Whether the given attribute name contains a wildcard.
*/
private static function isWildcardAttributeName(string $attribute_name): bool {
// @see ::validateAllowedRestrictionsPhase3()
assert($attribute_name !== '*');
return strpos($attribute_name, '*') !== FALSE;
}
/**
* Computes a regular expression for matching a wildcard attribute name.
*
* @param string $wildcard_attribute_name
* The wildcard attribute name for which to compute a regular expression.
*
* @return string
* The computed regular expression.
*/
private static function getRegExForWildCardAttributeName(string $wildcard_attribute_name): string {
assert(self::isWildcardAttributeName($wildcard_attribute_name));
return '/^' . str_replace('*', '.*', $wildcard_attribute_name) . '$/';
}
/**
* Gets the subset of allowed elements whose tags are wildcards.
*
@ -793,6 +938,7 @@ final class HTMLRestrictions {
* CKEditor 5 htmlSupport plugin constructor.
*
* @see https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/features/general-html-support.html#configuration
* @see https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/api/module_engine_view_matcher-MatcherPattern.html
*/
public function toGeneralHtmlSupportConfig(): array {
$allowed = [];
@ -813,20 +959,25 @@ final class HTMLRestrictions {
continue;
}
assert($value === TRUE || Inspector::assertAllStrings($value));
if ($name === 'class') {
$to_allow['classes'] = $value;
continue;
}
// If a single attribute value is allowed, it must be TRUE (see the
// assertion above). Otherwise, it must be an array of strings (see
// the assertion above), which lists all allowed attribute values. To
// be able to configure GHS to a range of values, we need to use a
// regular expression.
// @todo Expand to support partial wildcards in
// https://www.drupal.org/project/drupal/issues/3260853.
$to_allow['attributes'][$name] = is_array($value)
? ['regexp' => ['pattern' => '/^(' . implode('|', $value) . ')$/']]
$allowed_attribute_value = is_array($value)
? ['regexp' => ['pattern' => '/^(' . implode('|', str_replace('*', '.*', $value)) . ')$/']]
: $value;
if ($name === 'class') {
$to_allow['classes'] = $allowed_attribute_value;
continue;
}
// Most attribute restrictions specify a concrete attribute name. When
// the attribute name contains a partial wildcard, more complex syntax
// is needed.
$to_allow['attributes'][] = [
'key' => strpos($name, '*') === FALSE ? $name : ['regexp' => ['pattern' => self::getRegExForWildCardAttributeName($name)]],
'value' => $allowed_attribute_value,
];
}
}
$allowed[] = $to_allow;

View File

@ -9,6 +9,8 @@ use Drupal\Tests\ckeditor5\Traits\CKEditor5TestTrait;
use Drupal\ckeditor5\Plugin\Editor\CKEditor5;
use Symfony\Component\Validator\ConstraintViolation;
// cspell:ignore gramma
/**
* @coversDefaultClass \Drupal\ckeditor5\Plugin\CKEditor5Plugin\SourceEditing
* @group ckeditor5
@ -197,6 +199,41 @@ class SourceEditingTest extends CKEditor5TestBase {
'<a class>',
],
// 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>',
'<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>',
'<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>',
'<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>',
'<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>',
'<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>',
'<a data-*="sub*">',
],
// Edge case: `style`.
// @todo https://www.drupal.org/project/drupal/issues/3260857
];

View File

@ -133,6 +133,18 @@ class SmartDefaultSettingsTest extends KernelTestBase {
$basic_html_editor_with_media_embed->setSettings($settings);
$basic_html_editor_with_media_embed->save();
$new_value = str_replace('<img src alt height width data-entity-type data-entity-uuid data-align data-caption>', '<img src alt height width data-*>', $current_value);
$basic_html_format_with_any_data_attr = $basic_html_format;
$basic_html_format_with_any_data_attr['name'] .= ' (with any data-* attribute on images)';
$basic_html_format_with_any_data_attr['format'] = 'basic_html_with_any_data_attr';
NestedArray::setValue($basic_html_format_with_any_data_attr, $allowed_html_parents, $new_value);
FilterFormat::create($basic_html_format_with_any_data_attr)->save();
Editor::create(
['format' => 'basic_html_with_any_data_attr']
+
Yaml::parseFile('core/profiles/standard/config/install/editor.editor.basic_html.yml')
)->save();
$filter_plugin_manager = $this->container->get('plugin.manager.filter');
FilterFormat::create([
'format' => 'filter_only__filter_html',
@ -561,6 +573,28 @@ class SmartDefaultSettingsTest extends KernelTestBase {
]),
];
yield "basic_html_with_any_data_attr can be switched to CKEditor 5 without problems (3 upgrade messages)" => [
'format_id' => 'basic_html_with_any_data_attr',
'filters_to_drop' => $basic_html_test_case['filters_to_drop'],
'expected_ckeditor5_settings' => [
'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'],
'plugins' => [
'ckeditor5_sourceEditing' => [
'allowed_tags' => array_merge(
$basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'],
['<img data-*>'],
),
],
] + $basic_html_test_case['expected_ckeditor5_settings']['plugins'],
],
'expected_superset' => $basic_html_test_case['expected_superset'],
'expected_fundamental_compatibility_violations' => $basic_html_test_case['expected_fundamental_compatibility_violations'],
'expected_messages' => array_merge($basic_html_test_case['expected_messages'],
[
'This format\'s HTML filters includes plugins that support the following tags, but not some of their attributes. To ensure these attributes remain supported by this text format, the following were added to the Source Editing plugin\'s <em>Manually editable HTML tags</em>: &lt;a hreflang&gt; &lt;blockquote cite&gt; &lt;ul type&gt; &lt;ol start type&gt; &lt;h2 id&gt; &lt;h3 id&gt; &lt;h4 id&gt; &lt;h5 id&gt; &lt;h6 id&gt; &lt;img data-*&gt;.',
]),
];
yield "restricted_html can be switched to CKEditor 5 after dropping the two markup-creating filters (3 upgrade messages)" => [
'format_id' => 'restricted_html',
'filters_to_drop' => [

View File

@ -73,6 +73,10 @@ class HTMLRestrictionsTest extends UnitTestCase {
['foo' => ['baz' => ''], 'bar' => [' qux' => '']],
'The "bar" HTML tag has an attribute restriction " qux" which contains whitespace. Omit the whitespace.',
];
yield 'INVALID: keys valid, values invalid attribute restrictions due to broad wildcard instead of prefix/infix/suffix wildcard attribute name' => [
['foo' => ['*' => TRUE]],
'The "foo" HTML tag has an attribute restriction "*". This implies all attributes are allowed. Remove the attribute restriction instead, or use a prefix (`*-foo`), infix (`*-foo-*`) or suffix (`foo-*`) wildcard restriction instead.',
];
// Invalid HTML tag attribute value restrictions.
yield 'INVALID: keys valid, values invalid attribute restrictions due to empty strings' => [
@ -238,10 +242,6 @@ class HTMLRestrictionsTest extends UnitTestCase {
'<a target class>',
['a' => ['target' => TRUE, 'class' => TRUE]],
];
yield 'tag with two attributes, one with a partial wildcard' => [
'<a target class>',
['a' => ['target' => TRUE, 'class' => TRUE]],
];
// Multiple tag cases.
yield 'two tags' => [
@ -253,7 +253,7 @@ class HTMLRestrictionsTest extends UnitTestCase {
['a' => FALSE, 'p' => FALSE],
];
// Wildcard tag.
// Wildcard tag, attribute and attribute value.
yield '$block' => [
'<$block class="text-align-left text-align-center text-align-right text-align-justify">',
[],
@ -391,8 +391,22 @@ class HTMLRestrictionsTest extends UnitTestCase {
],
],
];
// @todo Test `data-*` attribute: https://www.drupal.org/project/drupal/issues/3260853
yield '<drupal-media data-*>' => [
'<drupal-media data-*>',
['drupal-media' => ['data-*' => TRUE]],
];
yield '<drupal-media foo-*-bar>' => [
'<drupal-media foo-*-bar>',
['drupal-media' => ['foo-*-bar' => TRUE]],
];
yield '<drupal-media *-foo>' => [
'<drupal-media *-foo>',
['drupal-media' => ['*-foo' => TRUE]],
];
yield '<h2 id="jump-*">' => [
'<h2 id="jump-*">',
['h2' => ['id' => ['jump-*' => TRUE]]],
];
}
/**
@ -434,8 +448,8 @@ class HTMLRestrictionsTest extends UnitTestCase {
[
'name' => 'script',
'attributes' => [
'src' => TRUE,
'defer' => TRUE,
['key' => 'src', 'value' => TRUE],
['key' => 'defer', 'value' => TRUE],
],
],
],
@ -449,10 +463,13 @@ class HTMLRestrictionsTest extends UnitTestCase {
[
'name' => 'a',
'attributes' => [
'href' => TRUE,
'hreflang' => [
'regexp' => [
'pattern' => '/^(en|fr)$/',
['key' => 'href', 'value' => TRUE],
[
'key' => 'hreflang',
'value' => [
'regexp' => [
'pattern' => '/^(en|fr)$/',
],
],
],
],
@ -460,15 +477,126 @@ class HTMLRestrictionsTest extends UnitTestCase {
[
'name' => 'p',
'attributes' => [
'data-*' => TRUE,
[
'key' => [
'regexp' => [
'pattern' => '/^data-.*$/',
],
],
'value' => TRUE,
],
],
'classes' => [
'block',
'regexp' => [
'pattern' => '/^(block)$/',
],
],
],
['name' => 'br'],
],
];
// Wildcard tag, attribute and attribute value.
yield '$block' => [
new HTMLRestrictions(['$block' => ['data-*' => TRUE]]),
['<$block data-*>'],
'<$block data-*>',
[
[
'name' => '$block',
'attributes' => [
[
'key' => [
'regexp' => [
'pattern' => '/^data-.*$/',
],
],
'value' => TRUE,
],
],
],
],
];
yield '<drupal-media data-*>' => [
new HTMLRestrictions(['drupal-media' => ['data-*' => TRUE]]),
['<drupal-media data-*>'],
'<drupal-media data-*>',
[
[
'name' => 'drupal-media',
'attributes' => [
[
'key' => [
'regexp' => [
'pattern' => '/^data-.*$/',
],
],
'value' => TRUE,
],
],
],
],
];
yield '<drupal-media foo-*-bar>' => [
new HTMLRestrictions(['drupal-media' => ['foo-*-bar' => TRUE]]),
['<drupal-media foo-*-bar>'],
'<drupal-media foo-*-bar>',
[
[
'name' => 'drupal-media',
'attributes' => [
[
'key' => [
'regexp' => [
'pattern' => '/^foo-.*-bar$/',
],
],
'value' => TRUE,
],
],
],
],
];
yield '<drupal-media *-bar>' => [
new HTMLRestrictions(['drupal-media' => ['*-bar' => TRUE]]),
['<drupal-media *-bar>'],
'<drupal-media *-bar>',
[
[
'name' => 'drupal-media',
'attributes' => [
[
'key' => [
'regexp' => [
'pattern' => '/^.*-bar$/',
],
],
'value' => TRUE,
],
],
],
],
];
yield '<h2 id="jump-*">' => [
new HTMLRestrictions(['h2' => ['id' => ['jump-*' => TRUE]]]),
['<h2 id="jump-*">'],
'<h2 id="jump-*">',
[
[
'name' => 'h2',
'attributes' => [
[
'key' => 'id',
'value' => [
'regexp' => [
'pattern' => '/^(jump-.*)$/',
],
],
],
],
],
],
];
}
/**
@ -738,7 +866,7 @@ class HTMLRestrictionsTest extends UnitTestCase {
'union' => 'b',
];
// Wildcard + matching tag cases.
// Wildcard tag + matching tag cases.
yield 'wildcard + matching tag: attribute intersection — without possible resolving' => [
'a' => new HTMLRestrictions(['p' => ['class' => TRUE]]),
'b' => new HTMLRestrictions(['$block' => ['class' => TRUE]]),
@ -810,7 +938,7 @@ class HTMLRestrictionsTest extends UnitTestCase {
'union' => 'b',
];
// Wildcard + non-matching cases.
// Wildcard tag + non-matching tag cases.
yield 'wildcard + non-matching tag: attribute diff — without possible resolving' => [
'a' => new HTMLRestrictions(['span' => ['class' => TRUE]]),
'b' => new HTMLRestrictions(['$block' => ['class' => TRUE]]),
@ -868,7 +996,7 @@ class HTMLRestrictionsTest extends UnitTestCase {
'union' => new HTMLRestrictions(['span' => ['class' => ['vertical-align-top' => TRUE, 'vertical-align-bottom' => TRUE]], '$block' => ['class' => ['vertical-align-top' => TRUE]]]),
];
// Wildcard + wildcard cases.
// Wildcard tag + wildcard tag cases.
yield 'wildcard + wildcard tag: attributes' => [
'a' => new HTMLRestrictions(['$block' => ['class' => TRUE, 'foo' => TRUE]]),
'b' => new HTMLRestrictions(['$block' => ['class' => TRUE]]),
@ -897,6 +1025,59 @@ class HTMLRestrictionsTest extends UnitTestCase {
'intersection' => 'a',
'union' => 'b',
];
// Concrete attributes + wildcard attribute cases for all 3 possible
// wildcard locations. Parametrized to prevent excessive repetition and
// subtle differences.
$wildcard_locations = [
'prefix' => 'data-*',
'infix' => '*-entity-*',
'suffix' => '*-type',
];
foreach ($wildcard_locations as $wildcard_location => $wildcard_attr_name) {
yield "concrete attrs + wildcard $wildcard_location attr that covers a superset" => [
'a' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE]]),
'b' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'diff' => HTMLRestrictions::emptySet(),
'intersection' => 'a',
'union' => 'b',
];
yield "concrete attrs + wildcard $wildcard_location attr that covers a superset — vice versa" => [
'a' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'b' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE]]),
'diff' => 'a',
'intersection' => 'b',
'union' => 'a',
];
yield "concrete attrs + wildcard $wildcard_location attr that covers a subset" => [
'a' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE, 'class' => TRUE]]),
'b' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'diff' => new HTMLRestrictions(['img' => ['class' => TRUE]]),
'intersection' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE]]),
'union' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE, 'class' => TRUE]]),
];
yield "concrete attrs + wildcard $wildcard_location attr that covers a subset — vice versa" => [
'a' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'b' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE, 'class' => TRUE]]),
'diff' => 'a',
'intersection' => new HTMLRestrictions(['img' => ['data-entity-bundle-type' => TRUE, 'data-entity-type' => TRUE]]),
'union' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE, 'class' => TRUE]]),
];
yield "wildcard $wildcard_location attr + wildcard $wildcard_location attr" => [
'a' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE, 'class' => TRUE]]),
'b' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'diff' => new HTMLRestrictions(['img' => ['class' => TRUE]]),
'intersection' => 'b',
'union' => 'a',
];
yield "wildcard $wildcard_location attr + wildcard $wildcard_location attr — vice versa" => [
'a' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE]]),
'b' => new HTMLRestrictions(['img' => [$wildcard_attr_name => TRUE, 'class' => TRUE]]),
'diff' => HTMLRestrictions::emptySet(),
'intersection' => 'a',
'union' => 'b',
];
}
}
}