Skip to content
Snippets Groups Projects

Add html to custom element Sanitizer and custom element to html normalizer

3 unresolved threads
Files
5
@@ -7,21 +7,36 @@ use Drupal\Core\Render\BubbleableMetadata;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Formats a custom element structure into an array.
* Formats a custom element structure into an array or HTML string.
    • I don't see why we are changing/extending the purpose of this class. Rendering custom_elements to html markup works via the drupal render system and I don't see why this issue requires us to change it.

      We can and maybe should mention this in the class docblock comment here though + clearly document which "format" is supported

Please register or sign in to reply
*/
class CustomElementNormalizer implements NormalizerInterface {
/**
* List of boolean attributes that do not need a value.
*
* @var array
*/
protected const BOOLEAN_HTML_ATTRIBUTES = [
'checked', 'selected', 'disabled', 'readonly',
'multiple', 'required', 'autofocus', 'formnovalidate', 'novalidate',
];
/**
* {@inheritdoc}
*/
public function normalize(mixed $object, ?string $format = NULL, array $context = []): array|string|int|float|bool|\ArrayObject|null {
$cache_metadata = $context['cache_metadata'] ?? new BubbleableMetadata();
if ($format === 'html') {
return $this->normalizeToHtml($object, $cache_metadata);
}
$result = $this->normalizeCustomElement($object, $cache_metadata);
// By default, convert keys in the outer result array to be valid JS
// identifiers. (Actually,
// https://vuejs.org/guide/components/registration.html indicates that
// PascalCase names, not camelCase, are valid identifiers - but camelCase
// was used since the noram was introduced in v2.) 'key_casing' context
// was used since the norm was introduced in v2.) 'key_casing' context
// parameter can override this.
if (!isset($context['key_casing']) || $context['key_casing'] !== 'ignore') {
$result = $this->convertKeysToCamelCase($result);
@@ -47,11 +62,11 @@ class CustomElementNormalizer implements NormalizerInterface {
* @return array
* Normalized custom element.
*/
protected function normalizeCustomElement(CustomElement $element, BubbleableMetadata $cache_metadata) {
protected function normalizeCustomElement(CustomElement $element, BubbleableMetadata $cache_metadata): array {
$result = ['element' => $element->getPrefixedTag()];
$result = array_merge($result, $this->normalizeAttributes($element->getAttributes(), $cache_metadata));
// Remove dumb default html wrapping elements.
// Remove dumb default HTML wrapping elements.
if ($result['element'] == 'div' || $result['element'] == 'span') {
unset($result['element']);
}
@@ -76,7 +91,7 @@ class CustomElementNormalizer implements NormalizerInterface {
* @return array
* Normalized custom element attributes.
*/
protected function normalizeAttributes(array $attributes, BubbleableMetadata $cache_metadata) {
protected function normalizeAttributes(array $attributes, BubbleableMetadata $cache_metadata): array {
$result = [];
foreach ($attributes as $key => $value) {
if ($key == 'slot') {
@@ -89,7 +104,7 @@ class CustomElementNormalizer implements NormalizerInterface {
}
/**
* Normalize slots.
* Normalize slots for non-HTML format.
*
* @param \Drupal\custom_elements\CustomElement $element
* The element for which to normalize slots.
@@ -99,7 +114,7 @@ class CustomElementNormalizer implements NormalizerInterface {
* @return array
* Normalized slots.
*/
protected function normalizeSlots(CustomElement $element, BubbleableMetadata $cache_metadata) {
protected function normalizeSlots(CustomElement $element, BubbleableMetadata $cache_metadata): array {
$data = [];
foreach ($element->getSortedSlotsByName() as $slot_key => $slot_entries) {
$slot_data = [];
@@ -138,7 +153,7 @@ class CustomElementNormalizer implements NormalizerInterface {
* @return array
* Converted keys.
*/
protected function convertKeysToCamelCase(array $array) {
protected function convertKeysToCamelCase(array $array): array {
$keys = array_map(function ($key) use (&$array) {
if (is_array($array[$key])) {
$array[$key] = $this->convertKeysToCamelCase($array[$key]);
@@ -160,4 +175,134 @@ class CustomElementNormalizer implements NormalizerInterface {
];
}
/**
* Normalize custom element to HTML string.
*
* @param \Drupal\custom_elements\CustomElement $element
* The custom element.
* @param \Drupal\Core\Render\BubbleableMetadata $cache_metadata
* The cache metadata.
*
* @return string
* HTML string representation of the custom element.
*/
protected function normalizeToHtml(CustomElement $element, BubbleableMetadata $cache_metadata): string {
$tag = $element->getPrefixedTag();
$attributes = $this->normalizeAttributes($element->getAttributes(), $cache_metadata);
$slots = $this->normalizeSlotsHtml($element, $cache_metadata);
if ($tag === 'text') {
// We assume there's only content for a text element.
return implode('', $slots);
}
$attributeString = $this->normalizeAttributesHtml($attributes);
$content = implode('', $slots);
// Define self-closing tags.
$selfClosingTags = CustomElement::getNoEndTags();
// Check if the tag is self-closing.
if (in_array(strtolower($tag), $selfClosingTags)) {
return "<{$tag}{$attributeString} />";
}
return "<{$tag}{$attributeString}>{$content}</{$tag}>";
}
/**
* Convert attributes array to string format for HTML.
*
* @param array $attributes
* The attributes array.
*
* @return string
* Attributes as a string suitable for HTML.
*/
protected function normalizeAttributesHtml(array $attributes): string {
$attributeString = '';
foreach ($attributes as $key => $value) {
if ($key == 'slot') {
continue;
}
if (in_array(strtolower($key), self::BOOLEAN_HTML_ATTRIBUTES)) {
// For boolean attributes, if the value is truthy (not 'false' or 0),
// we just add the attribute name.
if ($value !== FALSE && $value !== 'false' && $value !== 0) {
$attributeString .= " {$key}";
}
// If the value is explicitly false or 0, we skip adding this attribute.
}
else {
// For non-boolean attributes, proceed with key-value.
$attributeString .= " {$key}=\"" . htmlspecialchars($value) . "\"";
}
}
return $attributeString;
}
/**
* Normalize slots to HTML strings.
*
* @param \Drupal\custom_elements\CustomElement $element
* The element for which to normalize slots.
* @param \Drupal\Core\Render\BubbleableMetadata $cache_metadata
* The cache metadata.
*
* @return array
* An array of HTML strings for each slot.
*/
protected function normalizeSlotsHtml(CustomElement $element, BubbleableMetadata $cache_metadata): array {
$html = [];
foreach ($element->getSortedSlotsByName() as $slot_key => $slot_entries) {
foreach ($slot_entries as $slot) {
if (isset($slot['content'])) {
// Treat content as an array for consistent processing.
$contentItems = is_array($slot['content']) ? $slot['content'] : [$slot['content']];
foreach ($contentItems as $item) {
if ($item instanceof CustomElement) {
$html[] = $this->normalizeToHtml($item, $cache_metadata);
}
elseif ($item instanceof MarkupInterface) {
$html[] = (string) $item;
}
elseif (is_array($item)) {
// Recursively process nested arrays.
$html = array_merge($html, $this->normalizeSlotsHtmlItem($item, $cache_metadata));
}
else {
// Handle scalars (strings, numbers, etc.).
$html[] = htmlspecialchars((string) $item);
}
}
}
}
}
return $html;
}
/**
* Helper to process nested arrays in slot content.
*/
private function normalizeSlotsHtmlItem($item, BubbleableMetadata $cache_metadata): array {
$result = [];
if (is_array($item)) {
foreach ($item as $subItem) {
$result = array_merge($result, $this->normalizeSlotsHtmlItem($subItem, $cache_metadata));
}
}
else {
if ($item instanceof CustomElement) {
$result[] = $this->normalizeToHtml($item, $cache_metadata);
}
elseif ($item instanceof MarkupInterface) {
$result[] = (string) $item;
}
else {
$result[] = htmlspecialchars((string) $item);
}
}
return $result;
}
}
Loading