.
+ foreach ($html_tag_attribute_restrictions as $allowed_attribute_value => $merged_result) {
+ if ($merged_result === [0 => TRUE, 1 => TRUE]) {
+ $union[$tag][$html_tag_attribute_name][$allowed_attribute_value] = TRUE;
+ }
+ }
+ }
+ }
+ }
+ }
+ return new self($union);
+ }
+
+ /**
+ * Applies an operation (difference/intersection/union) with wildcard support.
+ *
+ * @param \Drupal\ckeditor5\HTMLRestrictions $a
+ * The first operand.
+ * @param \Drupal\ckeditor5\HTMLRestrictions $b
+ * The second operand.
+ * @param string $operation_method_name
+ * The name of the private method on this class to use as the operation.
+ *
+ * @return \Drupal\ckeditor5\HTMLRestrictions
+ * The result of the operation.
+ */
+ 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);
+ $wildcard_op_result = $a_wildcard->$operation_method_name($b_wildcard);
+
+ // Early return if both operands contain only wildcard tags.
+ if (count($a_wildcard->elements) === count($a->elements) && count($b_wildcard->elements) === count($b->elements)) {
+ return $wildcard_op_result;
+ }
+
+ // 2. Operation applied with wildcard tags resolved into concrete tags.
+ // For example: in the first operand and
+ // <$block class="text-align-center"> in the second operand.
+ $a_concrete = self::resolveWildcards($a);
+ $b_concrete = self::resolveWildcards($b);
+ $concrete_op_result = $a_concrete->$operation_method_name($b_concrete);
+
+ // Using the PHP array union operator is safe because the two operation
+ // result arrays ensure there is no overlap between the array keys.
+ // @codingStandardsIgnoreStart
+ assert(Inspector::assertAll(function ($t) { return self::isWildcardTag($t); }, array_keys($wildcard_op_result->elements)));
+ assert(Inspector::assertAll(function ($t) { return !self::isWildcardTag($t); }, array_keys($concrete_op_result->elements)));
+ // @codingStandardsIgnoreEnd
+
+ return new self($concrete_op_result->elements + $wildcard_op_result->elements);
+ }
+
+ /**
+ * 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.
+ */
+ private static function getWildcardSubset(HTMLRestrictions $r): HTMLRestrictions {
+ return new self(array_filter($r->elements, [__CLASS__, 'isWildcardTag'], ARRAY_FILTER_USE_KEY));
+ }
+
+ /**
+ * Checks whether given tag is a wildcard.
+ *
+ * @param string $tag_name
+ * A tag name.
+ *
+ * @return bool
+ * TRUE if it is a wildcard, otherwise FALSE.
+ */
+ private static function isWildcardTag(string $tag_name): bool {
+ return substr($tag_name, 0, 1) === '$' && array_key_exists($tag_name, self::WILDCARD_ELEMENT_METHODS);
+ }
+
+ /**
+ * Resolves the wildcard tags (this consumes the wildcard tags).
+ *
+ * @param \Drupal\ckeditor5\HTMLRestrictions $r
+ * A set of HTML restrictions.
+ *
+ * @return \Drupal\ckeditor5\HTMLRestrictions
+ * The concrete interpretation of the given set of HTML restrictions. All
+ * wildcard tag restrictions are resolved into restrictions on concrete
+ * elements, if concrete elements are allowed that correspond to the
+ * wildcard tags.
+ *
+ * @see ::getWildcardTags()
+ */
+ private static function resolveWildcards(HTMLRestrictions $r): HTMLRestrictions {
+ // Start by resolving the wildcards in a naive, simple way: generate
+ // tags, attributes and attribute values they support.
+ $naively_resolved_wildcard_elements = [];
+ foreach ($r->elements as $tag_name => $tag_config) {
+ if (self::isWildcardTag($tag_name)) {
+ $wildcard_tags = self::getWildcardTags($tag_name);
+ // Do not resolve to all tags supported by the wildcard tag, but only
+ // those which are explicitly supported. Because wildcard tags only
+ // allow declaring support for additional attributes and attribute
+ // values on already supported tags.
+ foreach ($wildcard_tags as $wildcard_tag) {
+ if (isset($r->elements[$wildcard_tag])) {
+ $naively_resolved_wildcard_elements[$wildcard_tag] = $tag_config;
+ }
+ }
+ }
+ }
+ $naive_resolution = new self($naively_resolved_wildcard_elements);
+
+ // Now merge the naive resolution's elements with the original elements, to
+ // let ::merge() pick the most permissive one.
+ // This is necessary because resolving wildcards may result in concrete tags
+ // becoming either more permissive:
+ // - if $r is `
<$block class="foo">`
+ // - then $naive will be `
`
+ // - merging them yields `
<$block class="foo">`
+ // - diffing the wildcard subsets yields just `
`
+ // Or it could result in concrete tags being unaffected by the resolved
+ // wildcards:
+ // - if $r is `
<$block class="foo">`
+ // - then $naive will be `
`
+ // - merging them yields `
<$block class="foo">` again
+ // - diffing the wildcard subsets yields just `
`
+ return $r->merge($naive_resolution)->doDiff(self::getWildcardSubset($r));
+ }
+
+ /**
+ * Gets allowed 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
+ *
+ * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
+ */
+ public function getAllowedElements(bool $resolve_wildcards = TRUE): array {
+ if ($resolve_wildcards) {
+ return self::resolveWildcards($this)->elements;
+ }
+
+ return $this->elements;
+ }
+
+ /**
+ * Transforms into the CKEditor 5 package metadata "elements" representation.
+ *
+ * @return string[]
+ * A list of strings, with each string expressing an allowed element,
+ * structured in the way expected by the CKEditor 5 package metadata.
+ *
+ * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/contributing/package-metadata.html
+ */
+ public function toCKEditor5ElementsArray(): array {
+ $readable = [];
+ foreach ($this->elements as $tag => $attributes) {
+ $attribute_string = '';
+ if (is_array($attributes)) {
+ foreach ($attributes as $attribute_name => $attribute_values) {
+ if (is_array($attribute_values)) {
+ $attribute_values_string = implode(' ', array_keys($attribute_values));
+ $attribute_string .= "$attribute_name=\"$attribute_values_string\" ";
+ }
+ else {
+ $attribute_string .= "$attribute_name ";
+ }
+ }
+ }
+ $joined = '<' . $tag . (!empty($attribute_string) ? ' ' . trim($attribute_string) : '') . '>';
+ array_push($readable, $joined);
+ }
+ assert(Inspector::assertAllStrings($readable));
+ return $readable;
+ }
+
+ /**
+ * Transforms into the Drupal HTML filter's "allowed_html" representation.
+ *
+ * @return string
+ * A string representing the list of allowed elements, structured in the
+ * manner expected by the "Limit allowed HTML tags and correct faulty HTML"
+ * filter plugin.
+ *
+ * @see \Drupal\filter\Plugin\Filter\FilterHtml
+ */
+ public function toFilterHtmlAllowedTagsString(): string {
+ return implode(' ', $this->toCKEditor5ElementsArray());
+ }
+
+ /**
+ * Transforms into the CKEditor 5 GHS configuration representation.
+ *
+ * @return string[]
+ * An array of allowed elements, structured in the manner expected by the
+ * CKEditor 5 htmlSupport plugin constructor.
+ *
+ * @see https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/features/general-html-support.html#configuration
+ */
+ public function toGeneralHtmlSupportConfig(): array {
+ $allowed = [];
+ foreach ($this->elements as $tag => $attributes) {
+ $to_allow = ['name' => $tag];
+ assert($attributes === FALSE || is_array($attributes));
+ if (is_array($attributes)) {
+ foreach ($attributes as $name => $value) {
+ // Convert the `'hreflang' => ['en' => TRUE, 'fr' => TRUE]` structure
+ // that this class expects to the `['en', 'fr']` structure that the
+ // GHS functionality in CKEditor 5 expects.
+ if (is_array($value)) {
+ $value = array_keys($value);
+ }
+ assert($value === TRUE || Inspector::assertAllStrings($value));
+ $to_allow['attributes'][$name] = $value;
+ }
+ }
+ $allowed[] = $to_allow;
+ }
+
+ return $allowed;
+ }
+
+ /**
+ * Gets a list of block-level elements.
+ *
+ * @return string[]
+ * An array of block-level element tags.
+ */
+ private static function getBlockElementList(): array {
+ return array_filter(array_keys(Elements::$html5), function (string $element): bool {
+ return Elements::isA($element, Elements::BLOCK_TAG);
+ });
+ }
+
+ /**
+ * Computes the tags that match the provided wildcard.
+ *
+ * A wildcard tag in element config is a way of representing multiple tags
+ * with a single item, such as `<$block>` to represent all block tags. Each
+ * wildcard should have a corresponding callback method listed in
+ * WILDCARD_ELEMENT_METHODS that returns the set of tags represented by the
+ * wildcard.
+ *
+ * @param string $wildcard
+ * The wildcard that represents multiple tags.
+ *
+ * @return string[]
+ * An array of HTML tags.
+ */
+ private static function getWildcardTags(string $wildcard): array {
+ $wildcard_element_method = self::WILDCARD_ELEMENT_METHODS[$wildcard];
+ return call_user_func([self::class, $wildcard_element_method]);
+ }
+
+}
diff --git a/core/modules/ckeditor5/src/HTMLRestrictionsUtilities.php b/core/modules/ckeditor5/src/HTMLRestrictionsUtilities.php
deleted file mode 100644
index 2e683cde4c..0000000000
--- a/core/modules/ckeditor5/src/HTMLRestrictionsUtilities.php
+++ /dev/null
@@ -1,284 +0,0 @@
- 'getBlockElementList',
- ];
-
- /**
- * Formats HTML elements for display.
- *
- * @param array $elements
- * List of elements to format. The structure is the same as the allowed tags
- * array documented in FilterInterface::getHTMLRestrictions().
- *
- * @return string[]
- * A formatted list; a string representation of the given HTML elements.
- *
- * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
- */
- public static function toReadableElements(array $elements): array {
- $readable = [];
- foreach ($elements as $tag => $attributes) {
- $attribute_string = '';
- if (is_array($attributes)) {
- foreach ($attributes as $attribute_name => $attribute_values) {
- if (is_array($attribute_values)) {
- $attribute_values_string = implode(' ', array_keys($attribute_values));
- $attribute_string .= "$attribute_name=\"$attribute_values_string\" ";
- }
- else {
- $attribute_string .= "$attribute_name ";
- }
- }
- }
- $joined = '<' . $tag . (!empty($attribute_string) ? ' ' . trim($attribute_string) : '') . '>';
- array_push($readable, $joined);
- }
- assert(Inspector::assertAllStrings($readable));
- return $readable;
- }
-
- /**
- * Parses a HTML restrictions string with >=1 tags in an array of single tags.
- *
- * @param string $elements_string
- * A HTML restrictions string.
- *
- * @return string[]
- * A list of strings, with a HTML tag and potentially attributes in each.
- */
- public static function allowedElementsStringToPluginElementsArray(string $elements_string): array {
- $html_restrictions = static::allowedElementsStringToHtmlFilterArray($elements_string);
- return static::toReadableElements($html_restrictions);
- }
-
- /**
- * Parses an HTML string into an array structured as expected by filter_html.
- *
- * @param string $elements_string
- * A string of HTML tags, potentially with attributes.
- *
- * @return array
- * An elements array. The structure is the same as the allowed tags array
- * documented in FilterInterface::getHTMLRestrictions().
- *
- * @see \Drupal\ckeditor5\HTMLRestrictionsUtilities::WILDCARD_ELEMENT_METHODS
- * Each key in this array represents a valid wildcard tag.
- *
- * @see \Drupal\filter\Plugin\Filter\FilterHtml
- * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
- */
- public static function allowedElementsStringToHtmlFilterArray(string $elements_string): array {
- preg_match('/<(\$[A-Z,a-z]*)/', $elements_string, $wildcard_matches);
-
- $wildcard = NULL;
- if (!empty($wildcard_matches)) {
- $wildcard = $wildcard_matches[1];
- assert(substr($wildcard, 0, 1) === '$', 'Wildcard tags must begin with "$"');
- $elements_string = str_replace($wildcard, 'WILDCARD', $elements_string);
- }
-
- $elements = [];
- $body_child_nodes = Html::load(str_replace('>', ' />', $elements_string))->getElementsByTagName('body')->item(0)->childNodes;
-
- foreach ($body_child_nodes as $node) {
- if ($node->nodeType !== XML_ELEMENT_NODE) {
- // Skip the empty text nodes inside tags.
- continue;
- }
-
- $tag = $wildcard ?? $node->tagName;
- if ($node->hasAttributes()) {
- foreach ($node->attributes as $attribute_name => $attribute) {
- $value = empty($attribute->value) ? TRUE : explode(' ', $attribute->value);
- self::addAllowedAttributeToElements($elements, $tag, $attribute_name, $value);
- }
- }
- else {
- if (!isset($elements[$tag])) {
- $elements[$tag] = FALSE;
- }
- }
- }
- return $elements;
- }
-
- /**
- * Cleans unwanted artifacts from "allowed HTML" arrays.
- *
- * @param array $elements
- * An array of allowed elements. The structure is the same as the allowed
- * tags array documented in FilterInterface::getHTMLRestrictions().
- *
- * @return array
- * The array without unwanted artifacts.
- *
- * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
- */
- public static function cleanAllowedHtmlArray(array $elements): array {
- // When recursively merging elements arrays, unkeyed boolean values can
- // appear in attribute config arrays. This removes them.
- foreach ($elements as $tag => $tag_config) {
- if (is_array($tag_config)) {
- $elements[$tag] = array_filter($tag_config);
- }
- }
- return $elements;
- }
-
- /**
- * Adds allowed attributes to the elements array.
- *
- * @param array $elements
- * The elements array. The structure is the same as the allowed tags array
- * documented in FilterInterface::getHTMLRestrictions().
- * @param string $tag
- * The tag having its attributes configured.
- * @param string $attribute
- * The attribute being configured.
- * @param array|true $value
- * The attribute config value.
- *
- * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
- */
- public static function addAllowedAttributeToElements(array &$elements, string $tag, string $attribute, $value): void {
- if (isset($elements[$tag][$attribute]) && $elements[$tag][$attribute] === TRUE) {
- // There's nothing to change as the tag/attribute combination is already
- // set to allow all.
- return;
- }
-
- if (isset($elements[$tag]) && $elements[$tag] === FALSE) {
- // If the tag is already allowed with no attributes then the value will be
- // FALSE. We need to convert the value to an empty array so that attribute
- // configuration can be added.
- $elements[$tag] = [];
- }
-
- if ($value === TRUE) {
- $elements[$tag][$attribute] = TRUE;
- }
- else {
- foreach ($value as $attribute_value) {
- $elements[$tag][$attribute][$attribute_value] = TRUE;
- }
- }
- }
-
- /**
- * Compares two HTML restrictions.
- *
- * The structure of the arrays is the same as the allowed tags array
- * documented in FilterInterface::getHTMLRestrictions().
- *
- * @param array $elements_array_1
- * The array to compare from.
- * @param array $elements_array_2
- * The array to compare to.
- *
- * @return array
- * Returns an array with all the values in $elements_array_1 that are not
- * present in $elements_array_1, including values that are FALSE
- *
- * @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
- */
- public static function diffAllowedElements(array $elements_array_1, array $elements_array_2): array {
- return array_filter(
- DiffArray::diffAssocRecursive($elements_array_1, $elements_array_2),
- // DiffArray::diffAssocRecursive() does not know the semantics of the
- // HTML restrictions array: unaware that `TAG => FALSE` is a subset of
- // `TAG => foo` and that in turn is a subset of `TAG => TRUE`.
- // @see \Drupal\filter\Entity\FilterFormat::getHtmlRestrictions()
- function ($value, string $tag) use ($elements_array_2) {
- return $value !== FALSE || !array_key_exists($tag, $elements_array_2);
- },
- ARRAY_FILTER_USE_BOTH
- );
- }
-
- /**
- * Parses a HTML restrictions string into htmlSupport plugin config structure.
- *
- * @param string $elements_string
- * A HTML restrictions string.
- *
- * @return string[]
- * An array of allowed elements, structured in the manner expected by the
- * CKEditor 5 htmlSupport plugin constructor.
- *
- * @see https://ckeditor5.github.io/docs/nightly/ckeditor5/latest/features/general-html-support.html#configuration
- */
- public static function allowedElementsStringToHtmlSupportConfig(string $elements_string): array {
- $html_restrictions = static::allowedElementsStringToHtmlFilterArray($elements_string);
- $allowed = [];
- foreach ($html_restrictions as $tag => $attributes) {
- $to_allow['name'] = $tag;
- assert($attributes === FALSE || is_array($attributes));
- if (is_array($attributes)) {
- foreach ($attributes as $name => $value) {
- assert($value === TRUE || Inspector::assertAllStrings($value));
- $to_allow['attributes'][$name] = $value;
- }
- }
- $allowed[] = $to_allow;
- }
-
- return $allowed;
- }
-
- /**
- * Gets a list of block level elements.
- *
- * @return array
- * An array of block level element tags.
- */
- private static function getBlockElementList(): array {
- return array_filter(array_keys(Elements::$html5), function (string $element) {
- return Elements::isA($element, Elements::BLOCK_TAG);
- });
- }
-
- /**
- * Returns the tags that match the provided wildcard.
- *
- * A wildcard tag in element config is a way of representing multiple tags
- * with a single item, such as `<$block>` to represent all block tags. Each
- * wildcard should have a corresponding callback method listed in
- * WILDCARD_ELEMENT_METHODS that returns the set of tags represented by the
- * wildcard.
- *
- * @param string $wildcard
- * The wildcard that represents multiple tags.
- *
- * @return array
- * An array of HTML tags.
- */
- public static function getWildcardTags(string $wildcard):array {
- $wildcard_element_method = self::WILDCARD_ELEMENT_METHODS[$wildcard];
- return call_user_func([self::class, $wildcard_element_method]);
- }
-
-}
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
index 11717006e9..49fe161fe8 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5Plugin/SourceEditing.php
@@ -4,7 +4,7 @@
namespace Drupal\ckeditor5\Plugin\CKEditor5Plugin;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
@@ -43,7 +43,7 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form
// Match the config schema structure at ckeditor5.plugin.ckeditor5_heading.
$form_value = $form_state->getValue('allowed_tags');
if (!is_array($form_value)) {
- $config_value = HTMLRestrictionsUtilities::allowedElementsStringToPluginElementsArray($form_value);
+ $config_value = HTMLRestrictions::fromString($form_value)->toCKEditor5ElementsArray();
$form_state->setValue('allowed_tags', $config_value);
}
}
@@ -75,11 +75,10 @@ public function getElementsSubset(): array {
* {@inheritdoc}
*/
public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
- $allowed = HTMLRestrictionsUtilities::allowedElementsStringToHtmlSupportConfig(implode('', $this->configuration['allowed_tags']));
-
+ $restrictions = HTMLRestrictions::fromString(implode(' ', $this->configuration['allowed_tags']));
return [
'htmlSupport' => [
- 'allow' => $allowed,
+ 'allow' => $restrictions->toGeneralHtmlSupportConfig(),
],
];
}
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
index 7be7aa398f..871e3bef74 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginDefinition.php
@@ -4,7 +4,7 @@
namespace Drupal\ckeditor5\Plugin;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Plugin\Definition\PluginDefinition;
use Drupal\Component\Plugin\Definition\PluginDefinitionInterface;
@@ -140,11 +140,11 @@ private function validateDrupalAspects(string $id, array $definition): void {
if ($definition['id'] === 'ckeditor5_sourceEditing') {
continue;
}
- $parsed_elements = HTMLRestrictionsUtilities::allowedElementsStringToPluginElementsArray($element);
- if (count($parsed_elements) === 0) {
+ $parsed = HTMLRestrictions::fromString($element);
+ if ($parsed->isEmpty()) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has a value at "drupal.elements.%d" that is not an HTML tag with optional attributes: "%s". Expected structure: "".', $id, $index, $element));
}
- elseif (count($parsed_elements) > 1) {
+ if (count($parsed->getAllowedElements()) > 1) {
throw new InvalidPluginDefinitionException($id, sprintf('The "%s" CKEditor 5 plugin definition has a value at "drupal.elements.%d": multiple tags listed, should be one: "%s".', $id, $index, $element));
}
}
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
index 8a7053f33e..05290ebd6f 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManager.php
@@ -5,7 +5,7 @@
namespace Drupal\ckeditor5\Plugin;
use Drupal\ckeditor5\Annotation\CKEditor5Plugin;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Utility\NestedArray;
@@ -263,13 +263,12 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array {
/**
* {@inheritdoc}
*/
- public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $retain_wildcard = FALSE): array {
+ public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array {
$plugins = $this->getDefinitions();
if (!empty($plugin_ids)) {
$plugins = array_intersect_key($plugins, array_flip($plugin_ids));
}
- $elements = [];
- $processed_elements = [];
+ $elements = HTMLRestrictions::emptySet();
foreach ($plugins as $id => $definition) {
// Some CKEditor 5 plugins only provide functionality, not additional
@@ -308,35 +307,12 @@ public function getProvidedElements(array $plugin_ids = [], EditorInterface $edi
}
assert(Inspector::assertAllStrings($defined_elements));
foreach ($defined_elements as $element) {
- if (in_array($element, $processed_elements)) {
- continue;
- }
- $processed_elements[] = $element;
- $additional_elements = HTMLRestrictionsUtilities::allowedElementsStringToHtmlFilterArray($element);
- $elements = array_merge_recursive($elements, $additional_elements);
- }
- }
-
- foreach ($elements as $tag_name => $tag_config) {
- if (substr($tag_name, 0, 1) === '$') {
- $wildcard_tags = HTMLRestrictionsUtilities::getWildcardTags($tag_name);
- foreach ($wildcard_tags as $wildcard_tag) {
- if (isset($elements[$wildcard_tag])) {
- foreach ($tag_config as $attribute_name => $attribute_value) {
- if (is_array($attribute_value)) {
- $attribute_value = array_keys($attribute_value);
- }
- HTMLRestrictionsUtilities::addAllowedAttributeToElements($elements, $wildcard_tag, $attribute_name, $attribute_value);
- }
- }
- }
- if (!$retain_wildcard) {
- unset($elements[$tag_name]);
- }
+ $additional_elements = HTMLRestrictions::fromString($element);
+ $elements = $elements->merge($additional_elements);
}
}
- return HTMLRestrictionsUtilities::cleanAllowedHtmlArray($elements);
+ return $elements->getAllowedElements();
}
/**
diff --git a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
index 800afb2169..adf8967f91 100644
--- a/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
+++ b/core/modules/ckeditor5/src/Plugin/CKEditor5PluginManagerInterface.php
@@ -104,9 +104,6 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array;
* An array of plugin IDs.
* @param \Drupal\editor\EditorInterface $editor
* A configured text editor object.
- * @param bool $retain_wildcard
- * If TRUE, the returned array will include config for wildcard elements
- * such as `<$block>`.
*
* @return array
* A nested array with a structure as described in
@@ -117,6 +114,6 @@ public function getCKEditor5PluginConfig(EditorInterface $editor): array;
*
* @see \Drupal\filter\Plugin\FilterInterface::getHTMLRestrictions()
*/
- public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $retain_wildcard = FALSE): array;
+ public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL): array;
}
diff --git a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
index ed6cc6512e..849f5f7b15 100644
--- a/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
+++ b/core/modules/ckeditor5/src/Plugin/Editor/CKEditor5.php
@@ -4,7 +4,7 @@
namespace Drupal\ckeditor5\Plugin\Editor;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
use Drupal\ckeditor5\Plugin\CKEditor5PluginManagerInterface;
@@ -682,15 +682,14 @@ protected function getEventualEditorWithPrimedFilterFormat(SubformStateInterface
if ($pair->getFilterFormat()->filters('filter_html')->status) {
// Compute elements provided by the current CKEditor 5 settings.
- $elements = $this->ckeditor5PluginManager->getProvidedElements(array_keys($enabled_plugins), $pair);
+ $restrictions = new HTMLRestrictions($this->ckeditor5PluginManager->getProvidedElements(array_keys($enabled_plugins), $pair));
// Compute eventual filter_html setting. Eventual as in: this is the list
// of eventually allowed HTML tags.
// @see \Drupal\filter\FilterFormatFormBase::submitForm()
// @see ckeditor5_form_filter_format_form_alter()
- $allowed_html = implode(' ', HTMLRestrictionsUtilities::toReadableElements($elements));
$filter_html_config = $pair->getFilterFormat()->filters('filter_html')->getConfiguration();
- $filter_html_config['settings']['allowed_html'] = $allowed_html;
+ $filter_html_config['settings']['allowed_html'] = $restrictions->toFilterHtmlAllowedTagsString();
$pair->getFilterFormat()->setFilterConfig('filter_html', $filter_html_config);
}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
index 0c35b73879..073fd50e64 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/FundamentalCompatibilityConstraintValidator.php
@@ -4,7 +4,7 @@
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\editor\EditorInterface;
use Drupal\filter\FilterFormatInterface;
@@ -101,10 +101,11 @@ private function checkNoMarkupFilters(FilterFormatInterface $text_format, Fundam
* The constraint to validate.
*/
private function checkHtmlRestrictionsAreCompatible(FilterFormatInterface $text_format, FundamentalCompatibilityConstraint $constraint): void {
- $minimum_tags = array_keys($this->pluginManager->getProvidedElements(self::FUNDAMENTAL_CKEDITOR5_PLUGINS));
+ $fundamental = new HTMLRestrictions($this->pluginManager->getProvidedElements(self::FUNDAMENTAL_CKEDITOR5_PLUGINS));
+ // @todo Remove in favor of HTMLRestrictions::diff() in https://www.drupal.org/project/drupal/issues/3231334
$html_restrictions = $text_format->getHtmlRestrictions();
-
+ $minimum_tags = array_keys($fundamental->getAllowedElements());
$forbidden_minimum_tags = isset($html_restrictions['forbidden_tags'])
? array_diff($minimum_tags, $html_restrictions['forbidden_tags'])
: [];
@@ -116,11 +117,12 @@ private function checkHtmlRestrictionsAreCompatible(FilterFormatInterface $text_
->addViolation();
}
- $not_allowed_minimum_tags = isset($html_restrictions['allowed'])
- ? array_diff($minimum_tags, array_keys($html_restrictions['allowed']))
- : [];
- if (!empty($not_allowed_minimum_tags)) {
- $offending_filter = static::findHtmlRestrictorFilterNotAllowingTags($text_format, $minimum_tags);
+ // @todo Remove early return in https://www.drupal.org/project/drupal/issues/3231334
+ if (!isset($html_restrictions['allowed'])) {
+ return;
+ }
+ if (!$fundamental->diff(HTMLRestrictions::fromTextFormat($text_format))->isEmpty()) {
+ $offending_filter = static::findHtmlRestrictorFilterNotAllowingTags($text_format, $fundamental);
$this->context->buildViolation($constraint->nonAllowedElementsMessage)
->setParameter('%filter_label', (string) $offending_filter->getLabel())
->setParameter('%filter_plugin_id', $offending_filter->getPluginId())
@@ -146,34 +148,23 @@ private function checkHtmlRestrictionsMatch(EditorInterface $text_editor, Fundam
$provided = $this->pluginManager->getProvidedElements($enabled_plugins, $text_editor);
foreach ($html_restrictor_filters as $filter_plugin_id => $filter) {
- $restrictions = $filter->getHTMLRestrictions();
- if (!isset($restrictions['allowed'])) {
- // @todo Handle HTML restrictor filters that only set forbidden_tags
- // https://www.drupal.org/project/ckeditor5/issues/3231336.
- continue;
- }
-
- $allowed = $restrictions['allowed'];
- // @todo Validate attributes allowed or forbidden on all elements
- // https://www.drupal.org/project/ckeditor5/issues/3231334.
- if (isset($allowed['*'])) {
- unset($allowed['*']);
- }
-
- $diff_allowed = HTMLRestrictionsUtilities::diffAllowedElements($allowed, $provided);
- $diff_elements = HTMLRestrictionsUtilities::diffAllowedElements($provided, $allowed);
+ $allowed = HTMLRestrictions::fromFilterPluginInstance($filter);
+ $provided = new HTMLRestrictions($provided);
+ $diff_allowed = $allowed->diff($provided);
+ $diff_elements = $provided->diff($allowed);
- if (!empty($diff_allowed)) {
+ if (!$diff_allowed->isEmpty()) {
$this->context->buildViolation($constraint->notSupportedElementsMessage)
- ->setParameter('@list', implode(' ', HTMLRestrictionsUtilities::toReadableElements($provided)))
- ->setParameter('@diff', implode(' ', HTMLRestrictionsUtilities::toReadableElements($diff_allowed)))
+ ->setParameter('@list', $provided->toFilterHtmlAllowedTagsString())
+ ->setParameter('@diff', $diff_allowed->toFilterHtmlAllowedTagsString())
->atPath("filters.$filter_plugin_id")
->addViolation();
}
- elseif (!empty($diff_elements)) {
+
+ if (!$diff_elements->isEmpty()) {
$this->context->buildViolation($constraint->missingElementsMessage)
- ->setParameter('@list', implode(' ', HTMLRestrictionsUtilities::toReadableElements($provided)))
- ->setParameter('@diff', implode(' ', HTMLRestrictionsUtilities::toReadableElements($diff_elements)))
+ ->setParameter('@list', $provided->toFilterHtmlAllowedTagsString())
+ ->setParameter('@diff', $diff_elements->toFilterHtmlAllowedTagsString())
->atPath("filters.$filter_plugin_id")
->addViolation();
}
@@ -255,15 +246,15 @@ function (FilterInterface $filter) {
*
* @param \Drupal\filter\FilterFormatInterface $text_format
* A text format whose filters to check for compatibility.
- * @param string[] $required_tags
- * A list of HTML tags that are required.
+ * @param \Drupal\ckeditor5\HTMLRestrictions $required
+ * A set of HTML restrictions, listing required HTML tags.
*
* @return \Drupal\filter\Plugin\FilterInterface
* The filter plugin instance not allowing the required tags.
*
* @throws \InvalidArgumentException
*/
- private static function findHtmlRestrictorFilterNotAllowingTags(FilterFormatInterface $text_format, array $required_tags): FilterInterface {
+ private static function findHtmlRestrictorFilterNotAllowingTags(FilterFormatInterface $text_format, HTMLRestrictions $required): FilterInterface {
// Get HTML restrictor filters that actually restrict HTML.
$filters = static::getFiltersInFormatOfType(
$text_format,
@@ -274,9 +265,8 @@ function (FilterInterface $filter) {
);
foreach ($filters as $filter) {
- $restrictions = $filter->getHTMLRestrictions();
-
- if (isset($restrictions['allowed']) && !empty(array_diff($required_tags, array_keys($restrictions['allowed'])))) {
+ // Return any filter not allowing >=1 of the required tags.
+ if (!$required->diff(HTMLRestrictions::fromFilterPluginInstance($filter))->isEmpty()) {
return $filter;
}
}
diff --git a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
index 9f3e5bb4e1..a07fad7d02 100644
--- a/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
+++ b/core/modules/ckeditor5/src/Plugin/Validation/Constraint/SourceEditingRedundantTagsConstraintValidator.php
@@ -4,7 +4,7 @@
namespace Drupal\ckeditor5\Plugin\Validation\Constraint;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
@@ -40,24 +40,36 @@ public function validate($value, Constraint $constraint) {
unset($enabled_plugins['ckeditor5_sourceEditing']);
// An array of tags enabled by every plugin other than Source Editing.
- $enabled_plugin_tags = $this->pluginManager->getProvidedElements(array_keys($enabled_plugins));
- $disabled_plugin_tags = $this->pluginManager->getProvidedElements(array_keys($disabled_plugins));
+ $enabled_plugin_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($enabled_plugins)));
+ $disabled_plugin_tags = new HTMLRestrictions($this->pluginManager->getProvidedElements(array_keys($disabled_plugins)));
- // An array of just the tags enabled by Source Editing.
- $source_enabled_tags = HTMLRestrictionsUtilities::allowedElementsStringToHtmlFilterArray($value);
- $enabled_plugin_overlap = array_intersect_key($enabled_plugin_tags, $source_enabled_tags);
- $disabled_plugin_overlap = array_intersect_key($disabled_plugin_tags, $source_enabled_tags);
+ // The single tag for which source editing is enabled, which we are checking
+ // now.
+ $source_enabled_tags = HTMLRestrictions::fromString($value);
+ // @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) {
+ return;
+ }
+ // This validation constraint currently only validates tags, not attributes;
+ // so if all attributes are allowed (TRUE) or some attributes are allowed
+ // (an array), return early. Only proceed when no attributes are allowed
+ // (FALSE).
+ // @todo Support attributes and attribute values in
+ // https://www.drupal.org/project/drupal/issues/3260857
+ $tags = array_keys($source_enabled_tags->getAllowedElements());
+ if ($source_enabled_tags->getAllowedElements()[reset($tags)] !== FALSE) {
+ return;
+ }
- foreach ([$enabled_plugin_overlap, $disabled_plugin_overlap] as &$overlap) {
+ $enabled_plugin_overlap = $enabled_plugin_tags->intersect($source_enabled_tags);
+ $disabled_plugin_overlap = $disabled_plugin_tags->intersect($source_enabled_tags);
+ foreach ([$enabled_plugin_overlap, $disabled_plugin_overlap] as $overlap) {
$checking_enabled = $overlap === $enabled_plugin_overlap;
- if (!empty($overlap)) {
- foreach ($overlap as $overlapping_tag => $overlapping_config) {
- if (is_array($source_enabled_tags[$overlapping_tag])) {
- unset($overlap[$overlapping_tag]);
- }
- }
- }
- if (!empty($overlap)) {
+ if (!$overlap->isEmpty()) {
$plugins_to_check_against = $checking_enabled ? $enabled_plugins : $disabled_plugins;
$tags_plugin_report = $this->pluginsSupplyingTagsMessage($overlap, $plugins_to_check_against);
$message = $checking_enabled ? $constraint->enabledPluginsMessage : $constraint->availablePluginsMessage;
@@ -71,7 +83,7 @@ public function validate($value, Constraint $constraint) {
/**
* Creates a message listing plugins and the overlapping tags they provide.
*
- * @param array $tags
+ * @param \Drupal\ckeditor5\HTMLRestrictions $overlap
* An array of overlapping tags.
* @param \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition[] $plugin_definitions
* An array of plugin definitions where overlap was found.
@@ -79,16 +91,14 @@ public function validate($value, Constraint $constraint) {
* @return string
* A list of plugins that provide the overlapping tags.
*/
- private function pluginsSupplyingTagsMessage(array $tags, array $plugin_definitions): string {
+ private function pluginsSupplyingTagsMessage(HTMLRestrictions $overlap, array $plugin_definitions): string {
$message_array = [];
$message_string = '';
foreach ($plugin_definitions as $definition) {
if ($definition->hasElements()) {
- $elements_array = HTMLRestrictionsUtilities::allowedElementsStringToHtmlFilterArray(implode('', $definition->getElements()));
- foreach ($elements_array as $tag_name => $tag_config) {
- if (isset($tags[$tag_name])) {
- $message_array[(string) $definition->label()][] = "<$tag_name>";
- }
+ $plugin_capabilities = HTMLRestrictions::fromString(implode(' ', $definition->getElements()));
+ foreach ($plugin_capabilities->intersect($overlap)->toCKEditor5ElementsArray() as $element) {
+ $message_array[(string) $definition->label()][] = $element;
}
}
}
diff --git a/core/modules/ckeditor5/src/SmartDefaultSettings.php b/core/modules/ckeditor5/src/SmartDefaultSettings.php
index 0fb8adabf1..59986631e0 100644
--- a/core/modules/ckeditor5/src/SmartDefaultSettings.php
+++ b/core/modules/ckeditor5/src/SmartDefaultSettings.php
@@ -139,13 +139,11 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte
['%enabling_message_content' => $enabling_message_content],
);
}
- unset($unsupported['*']);
// Warn user about unsupported tags.
if (!empty($unsupported)) {
- $unsupported_string = implode(' ', HTMLRestrictionsUtilities::toReadableElements($unsupported));
- $this->addTagsToSourceEditing($editor, $unsupported_string);
+ $this->addTagsToSourceEditing($editor, $unsupported);
$messages[] = $this->t("The following tags were permitted by this format's filter configuration, but no plugin was available that supports them. To ensure the tags remain supported by this text format, the following were added to the Source Editing plugin's Manually editable HTML tags: @unsupported_string.", [
- '@unsupported_string' => $unsupported_string,
+ '@unsupported_string' => $unsupported->toFilterHtmlAllowedTagsString(),
]);
}
}
@@ -164,7 +162,7 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte
if ($missing_attributes) {
$this->addTagsToSourceEditing($editor, $missing_attributes);
$messages[] = $this->t("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 Manually editable HTML tags: @missing_attributes.", [
- '@missing_attributes' => $missing_attributes,
+ '@missing_attributes' => $missing_attributes->toFilterHtmlAllowedTagsString(),
]);
}
}
@@ -179,15 +177,16 @@ public function computeSmartDefaultSettings(?EditorInterface $text_editor, Filte
return [$editor, $messages];
}
- private function addTagsToSourceEditing(EditorInterface $editor, string $tags): array {
+ private function addTagsToSourceEditing(EditorInterface $editor, HTMLRestrictions $tags): array {
$messages = [];
$settings = $editor->getSettings();
if (!isset($settings['toolbar']['items']) || !in_array('sourceEditing', $settings['toolbar']['items'])) {
$messages[] = $this->t('The Source Editing plugin was enabled to support tags and/or attributes that are not explicitly supported by any available CKEditor 5 plugins.');
$settings['toolbar']['items'][] = 'sourceEditing';
}
- $source_editing_allowed_tags = $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] ?? [];
- $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = array_merge($source_editing_allowed_tags, HTMLRestrictionsUtilities::allowedElementsStringToPluginElementsArray($tags));
+ $allowed_tags_array = $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] ?? [];
+ $allowed_tags_string = implode(' ', $allowed_tags_array);
+ $settings['plugins']['ckeditor5_sourceEditing']['allowed_tags'] = HTMLRestrictions::fromString($allowed_tags_string)->merge($tags)->toCKEditor5ElementsArray();
$editor->setSettings($settings);
return $messages;
}
@@ -338,7 +337,7 @@ protected function getEnabledCkeditor4Plugins(EditorInterface $editor): array {
* NULL when nothing happened, otherwise an array with two values:
* 1. a description (for use in a message) of which CKEditor 5 plugins were
* enabled to match the HTML tags allowed by the text format.
- * 2. the unsupported tags
+ * 2. the unsupported elements, in an HTMLRestrictions value object
*/
private function addToolbarItemsToMatchHtmlTagsInFormat(FilterFormatInterface $format, EditorInterface $editor): ?array {
$html_restrictions_needed_elements = $format->getHtmlRestrictions();
@@ -396,13 +395,14 @@ private function addToolbarItemsToMatchHtmlTagsInFormat(FilterFormatInterface $f
}
}
+ unset($unsupported['*']);
if (!empty($enabling_message_content)) {
$editor->setSettings($editor_settings_to_update);
$enabling_message_content = substr($enabling_message_content, 0, -1);
- return [$enabling_message_content, $unsupported];
+ return [$enabling_message_content, new HTMLRestrictions($unsupported)];
}
else {
- return [NULL, $unsupported];
+ return [NULL, new HTMLRestrictions($unsupported)];
}
}
@@ -418,7 +418,7 @@ private function addToolbarItemsToMatchHtmlTagsInFormat(FilterFormatInterface $f
* NULL when nothing happened, otherwise an array with two values:
* 1. a description (for use in a message) of which CKEditor 5 plugins were
* enabled to match the HTML attributes allowed by the text format.
- * 2. the unsupported attributes
+ * 2. the unsupported elements, in an HTMLRestrictions value object
*/
private function addToolbarItemsToMatchHtmlAttributesInFormat(FilterFormatInterface $format, EditorInterface $editor): ?array {
$html_restrictions_needed_elements = $format->getHtmlRestrictions();
@@ -428,116 +428,43 @@ private function addToolbarItemsToMatchHtmlAttributesInFormat(FilterFormatInterf
$enabled_plugins = array_keys($this->pluginManager->getEnabledDefinitions($editor));
$provided_elements = $this->pluginManager->getProvidedElements($enabled_plugins);
- $missing = HTMLRestrictionsUtilities::diffAllowedElements($editor->getFilterFormat()->getHtmlRestrictions()['allowed'], $provided_elements);
- $supported_tags_with_unsupported_attributes = array_intersect_key($missing, $provided_elements);
+ $provided = new HTMLRestrictions($provided_elements);
+ $missing = HTMLRestrictions::fromTextFormat($format)->diff($provided);
+ $supported_tags_with_unsupported_attributes = array_intersect_key($missing->getAllowedElements(), $provided_elements);
$supported_tags_with_unsupported_attributes = array_filter($supported_tags_with_unsupported_attributes, function ($tag_config) {
return is_array($tag_config);
});
+ $still_needed = new HTMLRestrictions($supported_tags_with_unsupported_attributes);
- if (!empty($supported_tags_with_unsupported_attributes)) {
- // This will be populated with plugins that aren't currently enabled, but
- // provide element configuration that include attributes. I.e. they are
- // the only plugins that can potentially address unsupported attributes
- // in supported tags.
- $disabled_plugins_with_attribute_config = [];
+ if (!$still_needed->isEmpty()) {
$all_plugins_definitions = $this->pluginManager->getDefinitions();
foreach ($all_plugins_definitions as $plugin_id => $definition) {
// Only proceed if the plugin has configured elements and the plugin
// does not have conditions. In the future we could add support for
// automatically enabling filters, but for now we assume that the filter
// configuration cannot be modified.
- if (!in_array($plugin_id, $enabled_plugins) && !$definition->hasConditions()) {
- $plugins_provided_elements = $this->pluginManager->getProvidedElements([$plugin_id], NULL, TRUE);
- if (!empty($plugins_provided_elements)) {
- // Filter elements that do not have attribute configuration.
- $elements_with_attribute_config = array_filter($plugins_provided_elements, function ($elements) {
- return $elements !== FALSE;
- });
- if (!empty($elements_with_attribute_config)) {
- foreach ($elements_with_attribute_config as $tag_name => $attribute_config) {
- // If the 'tag' is a wildcard, add the attribute config to
- // all qualifying tags.
- if (substr($tag_name, 0, 1) === '$') {
- // An array of all the tags that match the wildcard value.
- $wildcard_tags = HTMLRestrictionsUtilities::getWildcardTags($tag_name);
-
- // Matching wildcard tags that are also tags that have
- // attribute config that is not yet supported.
- $wildcard_tags_in_config_missing_attributes = array_intersect_key(array_flip($wildcard_tags), $supported_tags_with_unsupported_attributes);
- foreach (array_keys($wildcard_tags_in_config_missing_attributes) as $wildcard_provided_tag) {
- $elements_with_attribute_config[$wildcard_provided_tag] = $attribute_config;
- }
-
- // Remove the wildcard 'tag', as the tags it represents are
- // now accounted for.
- unset($elements_with_attribute_config[$tag_name]);
- }
- }
- $disabled_plugins_with_attribute_config[$plugin_id] = $elements_with_attribute_config;
- }
- }
- }
- }
-
- // This will contain plugins to be enabled if they provide support for the
- // not-yet-supported attributes.
- $plugins_to_enable_to_support_attribute_config = [];
- foreach ($supported_tags_with_unsupported_attributes as $tag_name => $attributes_config) {
- foreach ($attributes_config as $attribute_name => $attribute_config) {
- // This means the existing config must allow all values of the
- // attribute.
- if ($attribute_config === TRUE) {
- // See if there is a disabled plugin that will provide full use of
- // the attribute for a given tag.
- foreach ($disabled_plugins_with_attribute_config as $disabled_plugin_id => $disabled_plugin_elements_config) {
- if (isset($disabled_plugin_elements_config[$tag_name][$attribute_name]) && $disabled_plugin_elements_config[$tag_name][$attribute_name] === TRUE) {
- // Add this to the 'plugins to enable' array. Setting this value
- // to TRUE instead of an array indicates to the message system
- // that the attribute is allowed for the tag with any value.
- $plugins_to_enable_to_support_attribute_config[$disabled_plugin_id][$attribute_name][$tag_name] = TRUE;
-
- // This attribute can be removed from the list of unsupported
- // attributes for the tag.
- unset($supported_tags_with_unsupported_attributes[$tag_name][$attribute_name]);
- }
- }
- }
- else {
- // This condition is reached if the existing configuration has the
- // attribute value restricted to specific values.
- // @todo currently, this will enable plugins that allow ALL values
- // for an attribute. This means the attribute+value is now allowed
- // but additional attribute values are permitted as well. This may
- // need to be more selective
- // https://www.drupal.org/project/ckeditor5/issues/3231328.
- foreach ($attribute_config as $allowed_attribute_value => $noop) {
- foreach ($disabled_plugins_with_attribute_config as $disabled_plugin_id => $disabled_plugin_config) {
- if (isset($disabled_plugin_config[$tag_name][$attribute_name])) {
- if ($disabled_plugin_config[$tag_name][$attribute_name] === TRUE) {
- unset($supported_tags_with_unsupported_attributes[$tag_name][$attribute_name]);
- $plugins_to_enable_to_support_attribute_config[$disabled_plugin_id][$attribute_name][$tag_name] = TRUE;
- }
- elseif (is_array($disabled_plugin_config[$tag_name][$attribute_name])) {
- foreach ($disabled_plugin_config[$tag_name][$attribute_name] as $disabled_plugin_attribute_name => $disabled_plugin_allowed_value) {
- if ($disabled_plugin_attribute_name === $allowed_attribute_value) {
- unset($supported_tags_with_unsupported_attributes[$tag_name][$attribute_name][$allowed_attribute_value]);
- if (empty($supported_tags_with_unsupported_attributes[$tag_name][$attribute_name])) {
- unset($supported_tags_with_unsupported_attributes[$tag_name][$attribute_name]);
- }
- $plugins_to_enable_to_support_attribute_config[$disabled_plugin_id][$attribute_name][$tag_name][] = $allowed_attribute_value;
- }
- }
- }
- }
+ if (!in_array($plugin_id, $enabled_plugins, TRUE) && !$definition->hasConditions() && $definition->hasElements()) {
+ $plugin_support = HTMLRestrictions::fromString(implode(' ', $definition->getElements()));
+ // Do not inspect just $plugin_support, but the union of that with the
+ // already supported elements: wildcard restrictions will only resolve
+ // if the concrete tags they support are also present.
+ $potential_future = $provided->merge($plugin_support);
+ // This is the heart of the operation: intersect the potential future
+ // with what we need to achieve, then subtract what is already
+ // supported. This yields the net new elements.
+ $net_new = $potential_future->intersect($still_needed)->diff($provided);
+ if (!$net_new->isEmpty()) {
+ foreach ($net_new->getAllowedElements() as $tag_name => $attributes_config) {
+ foreach ($attributes_config as $attribute_name => $attribute_config) {
+ $plugins_to_enable_to_support_attribute_config[$plugin_id][$attribute_name][$tag_name] = $attribute_config;
}
}
+ // Fewer attributes are still needed.
+ $still_needed = $still_needed->diff($net_new);
}
}
}
- $supported_tags_with_unsupported_attributes = array_filter($supported_tags_with_unsupported_attributes);
- $missing_attributes = implode(' ', HTMLRestrictionsUtilities::toReadableElements($supported_tags_with_unsupported_attributes));
-
// If additional plugins need to be enable to support attribute config,
// loop through the list to enable the plugins and build a UI message that
// will convey this plugin-enabling to the user.
@@ -555,7 +482,7 @@ private function addToolbarItemsToMatchHtmlAttributesInFormat(FilterFormatInterf
$enabled_for_attributes_message_content .= " for tag: <$tag_name> to support: $attribute_name";
if (is_array($attribute_value_config)) {
$enabled_for_attributes_message_content .= " with value(s): ";
- foreach ($attribute_value_config as $allowed_value) {
+ foreach (array_keys($attribute_value_config) as $allowed_value) {
$enabled_for_attributes_message_content .= " $allowed_value,";
}
$enabled_for_attributes_message_content = substr($enabled_for_attributes_message_content, 0, -1) . '), ';
@@ -568,14 +495,14 @@ private function addToolbarItemsToMatchHtmlAttributesInFormat(FilterFormatInterf
// Some plugins enabled, maybe some missing attributes.
return [
substr($enabled_for_attributes_message_content, 0, -2),
- $missing_attributes,
+ $still_needed,
];
}
else {
// No plugins enabled, maybe some missing attributes.
return [
NULL,
- $missing_attributes,
+ $still_needed,
];
}
}
diff --git a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
index d638a4c3ac..9c3b0d45df 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/CKEditor5PluginManagerTest.php
@@ -3,7 +3,7 @@
namespace Drupal\Tests\ckeditor5\Kernel;
use Composer\Autoload\ClassLoader;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\ckeditor5\Plugin\CKEditor5Plugin\Heading;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\DependencyInjection\ContainerBuilder;
@@ -1020,7 +1020,6 @@ public function testEnabledPlugins() {
* in the filter_html "Allowed tags" field.
*
* @covers \Drupal\ckeditor5\Plugin\CKEditor5PluginManager::getProvidedElements
- * @covers \Drupal\ckeditor5\HTMLRestrictionsUtilities::toReadableElements
* @dataProvider providerTestProvidedElements
*/
public function testProvidedElements(array $plugins, array $text_editor_settings, array $expected_elements, string $expected_readable_string) {
@@ -1040,8 +1039,7 @@ public function testProvidedElements(array $plugins, array $text_editor_settings
$provided_elements = $this->manager->getProvidedElements($plugins, $text_editor);
$this->assertSame($expected_elements, $provided_elements);
- $readable_string = implode(' ', HTMLRestrictionsUtilities::toReadableElements($provided_elements));
- $this->assertSame($expected_readable_string, $readable_string);
+ $this->assertSame($expected_readable_string, (new HTMLRestrictions($provided_elements))->toFilterHtmlAllowedTagsString());
}
/**
diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
index abe7693f98..a2ff35e0ef 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
@@ -4,7 +4,7 @@
namespace Drupal\Tests\ckeditor5\Kernel;
-use Drupal\ckeditor5\HTMLRestrictionsUtilities;
+use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Component\Utility\NestedArray;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
@@ -306,21 +306,20 @@ public function test(string $format_id, array $filters_to_drop, array $expected_
// If the text format has HTML restrictions, ensure that a strict superset
// is allowed after switching to CKEditor 5.
$html_restrictions = $text_format->getHtmlRestrictions();
- $allowed_tags = $html_restrictions['allowed'] ?? [];
- if ($allowed_tags) {
- unset($allowed_tags['*']);
+ if (is_array($html_restrictions) && array_key_exists('allowed', $html_restrictions)) {
+ $allowed_tags = HTMLRestrictions::fromTextFormat($text_format);
$enabled_plugins = array_keys($this->manager->getEnabledDefinitions($updated_text_editor));
- $updated_allowed_tags = $this->manager->getProvidedElements($enabled_plugins, $updated_text_editor);
- $unsupported_tags_attributes = HTMLRestrictionsUtilities::diffAllowedElements($allowed_tags, $updated_allowed_tags);
- $superset_tags_attributes = HTMLRestrictionsUtilities::diffAllowedElements($updated_allowed_tags, $allowed_tags);
- $this->assertSame($expected_superset, implode(' ', HTMLRestrictionsUtilities::toReadableElements($superset_tags_attributes)));
- $this->assertEmpty($unsupported_tags_attributes, "The following tags/attributes are not allowed in the updated text format:" . print_r($unsupported_tags_attributes, TRUE));
+ $updated_allowed_tags = new HTMLRestrictions($this->manager->getProvidedElements($enabled_plugins, $updated_text_editor));
+ $unsupported_tags_attributes = $allowed_tags->diff($updated_allowed_tags);
+ $superset_tags_attributes = $updated_allowed_tags->diff($allowed_tags);
+ $this->assertSame($expected_superset, $superset_tags_attributes->toFilterHtmlAllowedTagsString());
+ $this->assertTrue($unsupported_tags_attributes->isEmpty(), "The following tags/attributes are not allowed in the updated text format:" . $unsupported_tags_attributes->toFilterHtmlAllowedTagsString());
// Update the text format like ckeditor5_form_filter_format_form_alter()
// would.
$updated_text_format = clone $text_format;
$filter_html_config = $text_format->filters('filter_html')->getConfiguration();
- $filter_html_config['settings']['allowed_html'] = implode(' ', HTMLRestrictionsUtilities::toReadableElements($updated_allowed_tags));
+ $filter_html_config['settings']['allowed_html'] = $updated_allowed_tags->toFilterHtmlAllowedTagsString();
$updated_text_format->setFilterConfig('filter_html', $filter_html_config);
}
else {
@@ -499,11 +498,8 @@ public function provider() {
'toolbar' => [
'items' => array_merge(
$basic_html_test_case['expected_ckeditor5_settings']['toolbar']['items'],
- // @todo Improve in https://www.drupal.org/project/drupal/issues/3259593
[
'alignment',
- 'alignment:center',
- 'alignment:justify',
]
),
],
@@ -536,7 +532,7 @@ public function provider() {
'expected_messages' => array_merge($basic_html_test_case['expected_messages'],
[
- 'The following plugins were enabled to support specific attributes that are allowed by this text format: Alignment ( for tag: <p> to support: class with value(s): text-align-center, text-align-justify), Align center ( for tag: <p> to support: class with value(s): text-align-center), Justify ( for tag: <p> to support: class with value(s): text-align-justify).',
+ 'The following plugins were enabled to support specific attributes that are allowed by this text format: Alignment ( for tag: <p> to support: class with value(s): text-align-center, text-align-justify).',
'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 Manually editable HTML tags: <a hreflang> <blockquote cite> <ul type> <ol start type> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>.',
]),
];
diff --git a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
index 2cb9442e70..48093d6d77 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/ValidatorsTest.php
@@ -712,6 +712,8 @@ public function providerPair(): array {
'roy',
'<#donk>',
'cruft',
+ '',
+ ' ',
],
],
],
@@ -724,6 +726,8 @@ public function providerPair(): array {
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.2' => 'The following tag is not valid HTML: roy.',
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.3' => 'The following tag is not valid HTML: <#donk>.',
'settings.plugins.ckeditor5_sourceEditing.allowed_tags.4' => 'The following tag is not valid HTML: <junior>cruft.',
+ 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.5' => 'The following tag is not valid HTML: .',
+ 'settings.plugins.ckeditor5_sourceEditing.allowed_tags.6' => 'The following tag is not valid HTML: .',
],
];
diff --git a/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
new file mode 100644
index 0000000000..4a87794202
--- /dev/null
+++ b/core/modules/ckeditor5/tests/src/Unit/HTMLRestrictionsTest.php
@@ -0,0 +1,895 @@
+expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage($expected_exception_message);
+ }
+ new HTMLRestrictions($elements);
+ }
+
+ public function providerConstruct(): \Generator {
+ // Fundamental structure.
+ yield 'INVALID: list instead of key-value pairs' => [
+ ['', ''],
+ 'An array of key-value pairs must be provided, with HTML tag names as keys.',
+ ];
+
+ // Invalid HTML tag names.
+ yield 'INVALID: key-value pairs now, but invalid keys due to angular brackets' => [
+ ['' => '', ' ' => ''],
+ '"" is not a HTML tag name, it is an actual HTML tag. Omit the angular brackets.',
+ ];
+ yield 'INVALID: no more angular brackets, but still leading or trailing whitespace' => [
+ ['foo' => '', 'bar ' => ''],
+ 'The "bar " HTML tag contains trailing or leading whitespace.',
+ ];
+ yield 'INVALID: invalid character range' => [
+ ['🦙' => ''],
+ '"🦙" is not a valid HTML tag name.',
+ ];
+ yield 'INVALID: invalid custom element name' => [
+ ['foo-bar' => '', '1-foo-bar' => ''],
+ '"1-foo-bar" is not a valid HTML tag name.',
+ ];
+ yield 'INVALID: unknown wildcard element name' => [
+ ['$foo' => TRUE],
+ '"$foo" is not a valid HTML tag name.',
+ ];
+
+ // Invalid HTML tag attribute name restrictions.
+ yield 'INVALID: keys valid, but not yet the values' => [
+ ['foo' => '', 'bar' => ''],
+ 'The value for the "foo" HTML tag is neither a boolean nor an array of attribute restrictions.',
+ ];
+ yield 'INVALID: keys valid, values can be arrays … but not empty arrays' => [
+ ['foo' => [], 'bar' => []],
+ 'The value for the "foo" HTML tag is an empty array. This is not permitted, specify FALSE instead to indicate no attributes are allowed. Otherwise, list allowed attributes.',
+ ];
+ yield 'INVALID: keys valid, values invalid attribute restrictions' => [
+ ['foo' => ['baz'], 'bar' => [' qux']],
+ 'The "foo" HTML tag has attribute restrictions, but it is not an array of key-value pairs, with HTML tag attribute names as keys.',
+ ];
+ yield 'INVALID: keys valid, values invalid attribute restrictions due to invalid attribute name' => [
+ ['foo' => ['baz' => ''], 'bar' => [' qux' => '']],
+ 'The "bar" HTML tag has an attribute restriction " qux" which contains whitespace. Omit the whitespace.',
+ ];
+
+ // Invalid HTML tag attribute value restrictions.
+ yield 'INVALID: keys valid, values invalid attribute restrictions due to empty strings' => [
+ ['foo' => ['baz' => ''], 'bar' => ['qux' => '']],
+ 'The "foo" HTML tag has an attribute restriction "baz" which is neither TRUE nor an array of attribute value restrictions.',
+ ];
+ yield 'INVALID: keys valid, values invalid attribute restrictions due to an empty array of allowed attribute values' => [
+ ['foo' => ['baz' => TRUE], 'bar' => ['qux' => []]],
+ 'The "bar" HTML tag has an attribute restriction "qux" which is set to the empty array. This is not permitted, specify either TRUE to allow all attribute values, or list the attribute value restrictions.',
+ ];
+ yield 'INVALID: keys valid, values invalid attribute restrictions due to a list of allowed attribute values' => [
+ ['foo' => ['baz' => TRUE], 'bar' => ['qux' => ['a', 'b']]],
+ 'The "bar" HTML tag has attribute restriction "qux", but it is not an array of key-value pairs, with HTML tag attribute values as keys and TRUE as values.',
+ ];
+
+ // Valid values.
+ yield 'VALID: keys valid, boolean attribute restriction values: also valid' => [
+ ['foo' => TRUE, 'bar' => FALSE],
+ NULL,
+ ];
+ yield 'VALID: keys valid, array attribute restriction values: also valid' => [
+ ['foo' => ['baz' => TRUE], 'bar' => ['qux' => ['a' => TRUE, 'b' => TRUE]]],
+ NULL,
+ ];
+ }
+
+ /**
+ * @covers ::isEmpty()
+ * @covers ::getAllowedElements()
+ * @dataProvider providerCounting
+ */
+ public function testCounting(array $elements, bool $expected_is_empty, int $expected_concrete_only_count, int $expected_concrete_plus_wildcard_count): void {
+ $r = new HTMLRestrictions($elements);
+ $this->assertSame($expected_is_empty, $r->isEmpty());
+ $this->assertCount($expected_concrete_only_count, $r->getAllowedElements());
+ $this->assertCount($expected_concrete_only_count, $r->getAllowedElements(TRUE));
+ $this->assertCount($expected_concrete_plus_wildcard_count, $r->getAllowedElements(FALSE));
+ }
+
+ public function providerCounting(): \Generator {
+ yield 'empty' => [
+ [],
+ TRUE,
+ 0,
+ 0,
+ ];
+
+ yield 'one' => [
+ ['a' => TRUE],
+ FALSE,
+ 1,
+ 1,
+ ];
+
+ yield 'two' => [
+ ['a' => TRUE, 'b' => FALSE],
+ FALSE,
+ 2,
+ 2,
+ ];
+
+ yield 'two of which one is a wildcard' => [
+ ['a' => TRUE, '$block' => FALSE],
+ FALSE,
+ 1,
+ 2,
+ ];
+ }
+
+ /**
+ * @covers ::fromString()
+ * @covers ::fromTextFormat()
+ * @covers ::fromFilterPluginInstance()
+ * @dataProvider providerConvenienceConstructors
+ */
+ public function testConvenienceConstructors($input, array $expected, ?array $expected_raw = NULL): void {
+ $expected_raw = $expected_raw ?? $expected;
+
+ // ::fromString()
+ $this->assertSame($expected, HTMLRestrictions::fromString($input)->getAllowedElements());
+ $this->assertSame($expected_raw, HTMLRestrictions::fromString($input)->getAllowedElements(FALSE));
+
+ // ::fromTextFormat()
+ $text_format = $this->prophesize(FilterFormatInterface::class);
+ $text_format->getHTMLRestrictions()->willReturn([
+ 'allowed' => $expected_raw,
+ ]);
+ $this->assertSame($expected, HTMLRestrictions::fromTextFormat($text_format->reveal())->getAllowedElements());
+ $this->assertSame($expected_raw, HTMLRestrictions::fromTextFormat($text_format->reveal())->getAllowedElements(FALSE));
+
+ // ::fromFilterPluginInstance()
+ $filter_plugin_instance = $this->prophesize(FilterInterface::class);
+ $filter_plugin_instance->getHTMLRestrictions()->willReturn([
+ 'allowed' => $expected_raw + [
+ // @see \Drupal\filter\Plugin\Filter\FilterHtml::getHTMLRestrictions()
+ '*' => [
+ 'style' => FALSE,
+ 'on*' => FALSE,
+ 'lang' => TRUE,
+ 'dir' => ['ltr' => TRUE, 'rtl' => TRUE],
+ ],
+ ],
+ ]);
+ $this->assertSame($expected, HTMLRestrictions::fromFilterPluginInstance($filter_plugin_instance->reveal())->getAllowedElements());
+ $this->assertSame($expected_raw, HTMLRestrictions::fromFilterPluginInstance($filter_plugin_instance->reveal())->getAllowedElements(FALSE));
+ }
+
+ public function providerConvenienceConstructors(): \Generator {
+ // All empty cases.
+ yield 'empty string' => [
+ '',
+ [],
+ ];
+ yield 'empty array' => [
+ implode(' ', []),
+ [],
+ ];
+ yield 'whitespace string' => [
+ ' ',
+ [],
+ ];
+
+ // Some nonsense cases.
+ yield 'nonsense string' => [
+ 'Hello there, this looks nothing like a HTML restriction.',
+ [],
+ ];
+ yield 'nonsense array #1' => [
+ implode(' ', ['foo', 'bar']),
+ [],
+ ];
+ yield 'nonsense array #2' => [
+ implode(' ', ['foo' => TRUE, 'bar' => FALSE]),
+ [],
+ ];
+
+ // Single tag cases.
+ yield 'tag without attributes' => [
+ '',
+ ['a' => FALSE],
+ ];
+ yield 'tag with wildcard attribute' => [
+ '',
+ ['a' => TRUE],
+ ];
+ yield 'tag with single attribute allowing any value' => [
+ '',
+ ['a' => ['target' => TRUE]],
+ ];
+ yield 'tag with single attribute allowing single specific value' => [
+ '',
+ ['a' => ['target' => ['_blank' => TRUE]]],
+ ];
+ yield 'tag with single attribute allowing multiple specific values' => [
+ '',
+ ['a' => ['target' => ['_self' => TRUE, '_blank' => TRUE]]],
+ ];
+ yield 'tag with single attribute allowing multiple specific values (reverse order)' => [
+ '',
+ ['a' => ['target' => ['_blank' => TRUE, '_self' => TRUE]]],
+ ];
+ yield 'tag with two attributes' => [
+ '',
+ ['a' => ['target' => TRUE, 'class' => TRUE]],
+ ];
+ yield 'tag with two attributes, one with a partial wildcard' => [
+ '',
+ ['a' => ['target' => TRUE, 'class' => TRUE]],
+ ];
+
+ // Multiple tag cases.
+ yield 'two tags' => [
+ ' ',
+ ['a' => FALSE, 'p' => FALSE],
+ ];
+ yield 'two tags (reverse order)' => [
+ ' ',
+ ['a' => FALSE, 'p' => FALSE],
+ ];
+
+ // Wildcard tag.
+ yield '$block' => [
+ '<$block class="text-align-left text-align-center text-align-right text-align-justify">',
+ [],
+ [
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ yield '$block + one concrete tag to resolve into' => [
+ '
<$block class="text-align-left text-align-center text-align-right text-align-justify">',
+ [
+ 'p' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ [
+ 'p' => FALSE,
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ yield '$block + two concrete tag to resolve into' => [
+ '
<$block class="text-align-left text-align-center text-align-right text-align-justify">
',
+ [
+ 'p' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ 'blockquote' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ [
+ 'p' => FALSE,
+ 'blockquote' => FALSE,
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ yield '$block + one concrete tag to resolve into that already allows a subset of attributes: concrete less permissive than wildcard' => [
+ ' <$block class="text-align-left text-align-center text-align-right text-align-justify">',
+ [
+ 'p' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ [
+ 'p' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ ],
+ ],
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ yield '$block + one concrete tag to resolve into that already allows all attribute values: concrete more permissive than wildcard' => [
+ '
<$block class="text-align-left text-align-center text-align-right text-align-justify">',
+ [
+ 'p' => [
+ 'class' => TRUE,
+ ],
+ ],
+ [
+ 'p' => [
+ 'class' => TRUE,
+ ],
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ yield '$block + one concrete tag to resolve into that already allows all attributes: concrete more permissive than wildcard' => [
+ '
<$block class="text-align-left text-align-center text-align-right text-align-justify">',
+ [
+ 'p' => TRUE,
+ ],
+ [
+ 'p' => TRUE,
+ '$block' => [
+ 'class' => [
+ 'text-align-left' => TRUE,
+ 'text-align-center' => TRUE,
+ 'text-align-right' => TRUE,
+ 'text-align-justify' => TRUE,
+ ],
+ ],
+ ],
+ ];
+
+ // @todo Test `data-*` attribute: https://www.drupal.org/project/drupal/issues/3260853
+ }
+
+ /**
+ * @covers ::toCKEditor5ElementsArray()
+ * @covers ::toFilterHtmlAllowedTagsString()
+ * @covers ::toGeneralHtmlSupportConfig()
+ * @dataProvider providerRepresentations
+ */
+ public function testRepresentations(HTMLRestrictions $restrictions, array $expected_elements_array, string $expected_allowed_html_string, array $expected_ghs_config): void {
+ $this->assertSame($expected_elements_array, $restrictions->toCKEditor5ElementsArray());
+ $this->assertSame($expected_allowed_html_string, $restrictions->toFilterHtmlAllowedTagsString());
+ $this->assertSame($expected_ghs_config, $restrictions->toGeneralHtmlSupportConfig());
+ }
+
+ public function providerRepresentations(): \Generator {
+ yield 'empty set' => [
+ HTMLRestrictions::emptySet(),
+ [],
+ '',
+ [],
+ ];
+
+ yield 'only tags' => [
+ new HTMLRestrictions(['a' => FALSE, 'p' => FALSE, 'br' => FALSE]),
+ ['', '', '
'],
+ '
',
+ [
+ ['name' => 'a'],
+ ['name' => 'p'],
+ ['name' => 'br'],
+ ],
+ ];
+
+ yield 'single tag with multiple attributes allowing all values' => [
+ new HTMLRestrictions(['script' => ['src' => TRUE, 'defer' => TRUE]]),
+ ['