custom_elements-8.x-2.x-dev/src/RenderConverter/CanvasRenderConverter.php

src/RenderConverter/CanvasRenderConverter.php
<?php

namespace Drupal\custom_elements\RenderConverter;

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\custom_elements\CustomElement;
use Drupal\custom_elements\CustomElementsBlockRenderHelperTrait;

/**
 * Service for converting Canvas render arrays to custom elements.
 */
class CanvasRenderConverter {

  use CustomElementsBlockRenderHelperTrait;

  /**
   * SDC component IDs that should be rendered as custom elements.
   *
   * By default, SDCs are rendered via twig. This associative array maps
   * component IDs to TRUE for SDCs that should be converted to custom
   * elements instead.
   *
   * @var array<string, bool>
   */
  protected $sdcCustomElementComponents = [];

  /**
   * Converts a Canvas field render array to a custom elements tree.
   *
   * @param array $render_array
   *   The render array from a Canvas field.
   *
   * @return \Drupal\custom_elements\CustomElement
   *   The generated custom element. Returns an empty renderless-container
   *   if nothing to render, which can carry cache metadata.
   */
  public function convertRenderArray(array $render_array): CustomElement {
    // Create empty renderless-container to hold cache metadata if needed.
    $empty_container = CustomElement::create('renderless-container');

    // Check access first if present.
    if (isset($render_array['#access']) && $render_array['#access'] instanceof AccessResultInterface) {
      $empty_container->addCacheableDependency($render_array['#access']);
      if (!$render_array['#access']->isAllowed()) {
        return $empty_container;
      }
    }

    // Check if a CustomElement is already provided (e.g., from canvas_extjs).
    if (isset($render_array['#custom_element']) && $render_array['#custom_element'] instanceof CustomElement) {
      $custom_element = $render_array['#custom_element'];

      // Process slots from render array if present.
      // This handles slots for API output where #pre_render hasn't run yet.
      if (!empty($render_array['#slots'])) {
        foreach ($render_array['#slots'] as $slot_name => $slot_content) {
          if (!is_array($slot_content)) {
            continue;
          }
          // Recursively convert slot content to CustomElement.
          $slot_element = $this->convertRenderArray($slot_content);
          $custom_element->setSlotFromCustomElement($slot_name, $slot_element);
        }
      }
      return $custom_element;
    }

    // Process the render array based on its type.
    switch ($render_array['#type'] ?? '') {
      case 'component':
        return $this->convertComponent($render_array);

      case 'component_container':
        return $this->convertComponentContainer($render_array);

      default:
        if ($this->isBlockRenderArray($render_array)) {
          return $this->convertBlockRenderArray($render_array);
        }

        // Process render arrays with children (drilling down).
        return $this->processGeneralRenderArray($render_array);
    }
  }

  /**
   * Normalizes a component ID to "module:component" format.
   *
   * @param string $component_id
   *   The component ID to normalize.
   *
   * @return string
   *   The normalized component ID in "module:component" format.
   */
  private function normalizeComponentId(string $component_id): string {
    // Transform 'sdc.module.component' -> 'module:component'.
    if (str_starts_with($component_id, 'sdc.')) {
      return preg_replace('/^sdc\.([^.]+)\.(.+)$/', '$1:$2', $component_id);
    }
    return $component_id;
  }

  /**
   * Sets which SDC components should be rendered as custom elements.
   *
   * This is useful for overriding SDCs regular twig-based rendering with a
   * custom elements based rendering approach. Can be called multiple times
   * to add or remove components.
   *
   * @param string[] $component_ids
   *   Array of SDC component IDs to configure.
   * @param bool $enable
   *   TRUE to render as custom element, FALSE to render via twig (default).
   */
  public function setSdcCustomElementComponents(array $component_ids, bool $enable = TRUE): void {
    foreach ($component_ids as $component_id) {
      // Normalize to "module:component" format for internal storage.
      $normalized_id = $this->normalizeComponentId($component_id);

      if ($enable) {
        $this->sdcCustomElementComponents[$normalized_id] = TRUE;
      }
      else {
        unset($this->sdcCustomElementComponents[$normalized_id]);
      }
    }
  }

  /**
   * Determines if an SDC component should be rendered as a custom element.
   *
   * @param string $component_id
   *   The component ID to check.
   *
   * @return bool
   *   TRUE if the component should be rendered as a custom element, FALSE if
   *   it should be rendered via twig.
   */
  protected function shouldRenderSdcAsCustomElement(string $component_id): bool {
    // Normalize component ID to "module:component" format.
    $normalized_id = $this->normalizeComponentId($component_id);

    // Non-SDC components (those without ':') always render as custom elements.
    if (!str_contains($normalized_id, ':')) {
      return TRUE;
    }

    // SDC components render via twig by default, unless explicitly configured.
    return isset($this->sdcCustomElementComponents[$normalized_id]);
  }

  /**
   * Converts a component render array to custom element.
   *
   * @param array $render_array
   *   The component render array.
   *
   * @return \Drupal\custom_elements\CustomElement
   *   The custom element.
   */
  protected function convertComponent(array $render_array): CustomElement {
    $component_id = $render_array['#component'] ?? ($render_array['#component_id'] ?? '');

    // Check if this SDC should be rendered via twig or as custom element.
    if (!$this->shouldRenderSdcAsCustomElement($component_id)) {
      return $this->renderSdcComponentAsTwig($render_array);
    }

    // Get the custom element tag for this component.
    $tag = $this->getCustomElementTag($render_array);
    $element = CustomElement::create($tag);

    // Add component props as attributes.
    if (isset($render_array['#props']) && is_array($render_array['#props'])) {
      foreach ($render_array['#props'] as $key => $value) {
        // Skip Canvas internal props (still use xb_ prefix for compatibility).
        if (str_starts_with($key, 'xb_')) {
          continue;
        }
        // Pass through all props, including non-scalar ones.
        $element->setAttribute($key, $value);
      }
    }

    // Process slots. Each slot has its own render array that we process.
    if (isset($render_array['#slots']) && is_array($render_array['#slots'])) {
      foreach ($render_array['#slots'] as $slot_name => $slot_content) {
        if (is_array($slot_content)) {
          $slot_element = $this->convertRenderArray($slot_content);
          $element->setSlotFromCustomElement($slot_name, $slot_element);
        }
      }
    }
    return $element;
  }

  /**
   * Converts a component container render array to custom element.
   *
   * @param array $render_array
   *   The component container render array.
   *
   * @return \Drupal\custom_elements\CustomElement
   *   The custom element. Returns empty renderless-container if no component.
   */
  protected function convertComponentContainer(array $render_array): CustomElement {
    // If there's a #component key, this is actually a component wrapped in a
    // container.
    if (isset($render_array['#component']) && is_array($render_array['#component'])) {
      // Delegate to the main conversion method which handles all types.
      return $this->convertRenderArray($render_array['#component']);
    }

    // If there's no #component key, return empty renderless-container.
    $empty = CustomElement::create('renderless-container');
    // Preserve cache metadata if present.
    if (isset($render_array['#cache'])) {
      $empty->addCacheableDependency(BubbleableMetadata::createFromRenderArray($render_array));
    }
    return $empty;
  }

  /**
   * Renders an SDC component using twig.
   *
   * @param array $render_array
   *   The component render array.
   *
   * @return \Drupal\custom_elements\CustomElement
   *   The custom element wrapping the twig-rendered content.
   */
  protected function renderSdcComponentAsTwig(array $render_array): CustomElement {
    // Create a drupal-markup element to wrap the rendered content.
    $markup_element = CustomElement::create('drupal-markup');

    // Use setSlotFromRenderArray which handles rendering internally.
    // This renders the SDC component via Drupal's render system and
    // sets it as the default slot content.
    // For now, we keep the #slots untouched and let them render as is.
    $markup_element->setSlotFromRenderArray('default', $render_array);

    // Cache metadata is preserved automatically by setSlotFromRenderArray.
    return $markup_element;
  }

  /**
   * Gets the custom element tag name for a component render array.
   *
   * @param array $render_array
   *   The component render array.
   *
   * @return string
   *   The custom element tag name.
   */
  protected function getCustomElementTag(array $render_array): string {
    $tag = $render_array['#component'] ?? ($render_array['#component_id'] ?? '');
    // Convert to valid custom element tag format.
    $tag = str_replace([':', '.', '_'], '-', $tag);
    return strtolower($tag);
  }

  /**
   * Checks if a render array represents a block.
   *
   * @param array $render_array
   *   The render array to check.
   *
   * @return bool
   *   TRUE if this is a block render array.
   */
  protected function isBlockRenderArray(array $render_array): bool {
    $theme = $render_array['#theme'] ?? '';
    return $theme === 'block' || strpos($theme, 'block_') === 0;
  }

  /**
   * Processes a general render array while handling children.
   *
   * @param array $render_array
   *   The render array to process.
   *
   * @return \Drupal\custom_elements\CustomElement
   *   The converted custom element.
   */
  protected function processGeneralRenderArray(array $render_array): CustomElement {
    $children = Element::children($render_array);

    // When there is a special type for which we have no mapping, fallback to
    // rendering it and wrapping the result in drupal-markup.
    if (isset($render_array['#type']) || isset($render_array['#theme']) || empty($children)) {
      return CustomElement::createFromRenderArray($render_array);
    }

    // Single child with no other properties - simply drill down.
    if (count($children) === 1) {
      $first_child = reset($children);

      if (isset($render_array[$first_child]) && is_array($render_array[$first_child])) {
        $child_element = $this->convertRenderArray($render_array[$first_child]);
        // Add cache metadata if present.
        if (isset($render_array['#cache'])) {
          $child_element->addCacheableDependency(BubbleableMetadata::createFromRenderArray($render_array));
        }
        return $child_element;
      }
    }

    // For multiple children use renderless-container.
    $parent_element = CustomElement::create('renderless-container');

    // Add cache metadata if present.
    if (isset($render_array['#cache'])) {
      $parent_element->addCacheableDependency(BubbleableMetadata::createFromRenderArray($render_array));
    }

    $elements = [];
    foreach ($children as $child_key) {
      if (isset($render_array[$child_key]) && is_array($render_array[$child_key])) {
        $child_element = $this->convertRenderArray($render_array[$child_key]);
        $elements[] = $child_element;
      }
    }

    // If we have elements, add them to the renderless-container.
    if ($elements) {
      $parent_element->setSlotFromNestedElements('default', $elements);
    }

    return $parent_element;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc