Verified Commit db518452 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3399036 by godotislate, quietone, Wim Leers, kim.pepper, Keshav Patel,...

Issue #3399036 by godotislate, quietone, Wim Leers, kim.pepper, Keshav Patel, smustgrave, alexpott, larowlan: CKEditor5PluginManager: use PHP attributes instead of doctrine annotations
parent c0d8b4bb
Loading
Loading
Loading
Loading
Loading
+25 −23
Original line number Diff line number Diff line
@@ -44,8 +44,8 @@
 * @see https://ckeditor.com/docs/ckeditor5/latest/framework/guides/architecture/editing-engine.html#element-types-and-custom-data
 *
 * @section plugins CKEditor 5 Plugins
 * CKEditor 5 plugins may use either YAML or a PHP annotation for their
 * definitions. A PHP class does not need an annotation if it is defined in yml.
 * CKEditor 5 plugins may use either YAML or a PHP attribute for their
 * definitions. A PHP class does not need an attribute if it is defined in yml.
 *
 * To be discovered, YAML definition files must be named
 * {module_name}.ckeditor5.yml.
@@ -71,22 +71,24 @@
 *       - <marquee behavior>
 * @endcode
 *
 * Declared as an Annotation:
 * Declared as an Attribute:
 * @code
 * # In a scr/Plugin/CKEditor5Plugin/Marquee.php file.
 * /**
 *  * @CKEditor5Plugin(
 *  *   id = "MODULE_NAME_marquee",
 *  *   ckeditor5 = @CKEditor5AspectsOfCKEditor5Plugin(
 *  *     plugins = { "PACKAGE.CLASS" },
 *  *   ),
 *  *   drupal = @DrupalAspectsOfCKEditor5Plugin(
 *  *     label = @Translation("Marquee"),
 *  *     library = "MODULE_NAME/ckeditor5.marquee"
 *  *     elements = { "<marquee>", "<marquee behavior>" },
 *  *   )
 *  * )
 *  * /
 * use Drupal\ckeditor5\Attribute\CKEditor5AspectsOfCKEditor5Plugin;
 * use Drupal\ckeditor5\Attribute\CKEditor5Plugin;
 * use Drupal\ckeditor5\Attribute\DrupalAspectsOfCKEditor5Plugin;
 * use Drupal\Core\StringTranslation\TranslatableMarkup;
 *
 * #[CKEditor5Plugin(
 *   id: 'MODULE_NAME_marquee',
 *   ckeditor5: new CKEditor5AspectsOfCKEditor5Plugin(
 *     plugins: ['PACKAGE.CLASS'],
 *   ),
 *   drupal: new DrupalAspectsOfCKEditor5Plugin(
 *     label: new TranslatableMarkup('Marquee'),
 *     library: 'MODULE_NAME/ckeditor5.marquee',
 *     elements: ['<marquee>', '<marquee behavior>'],
 *   ),
 * )]
 * @endcode
 *
 * The metadata relating strictly to the CKEditor 5 plugin's JS code is stored
@@ -95,7 +97,7 @@
 * If the plugin has a dependency on another module, adding the 'provider' key
 * will prevent the plugin from being loaded if that module is not installed.
 *
 * All of these can be defined in YAML or annotations. A given plugin should
 * All of these can be defined in YAML or attributes. A given plugin should
 * choose one or the other, as a definition can't parse both at once.
 *
 * Overview of all available plugin definition properties:
@@ -158,7 +160,7 @@
 *   need arises, see
 *   https://www.drupal.org/docs/drupal-apis/ckeditor-5-api/overview#conditions.
 *
 * All of these can be defined in YAML or annotations. A given plugin should
 * All of these can be defined in YAML or attributes. A given plugin should
 * choose one or the other, as a definition can't parse both at once.
 *
 * If the CKEditor 5 plugin contains translation they can be automatically
@@ -181,14 +183,14 @@
 * assets/ckeditor5/marquee/translations/* in this example.
 *
 *
 * @see \Drupal\ckeditor5\Annotation\CKEditor5Plugin
 * @see \Drupal\ckeditor5\Annotation\CKEditor5AspectsOfCKEditor5Plugin
 * @see \Drupal\ckeditor5\Annotation\DrupalAspectsOfCKEditor5Plugin
 * @see \Drupal\ckeditor5\Attribute\CKEditor5Plugin
 * @see \Drupal\ckeditor5\Attribute\CKEditor5AspectsOfCKEditor5Plugin
 * @see \Drupal\ckeditor5\Attribute\DrupalAspectsOfCKEditor5Plugin
 *
 * @section public_api Public API
 *
 * The CKEditor 5 module provides no public API, other than:
 * - the annotations and interfaces mentioned above;
 * - the attributes and interfaces mentioned above;
 * - to help implement CKEditor 5 plugins:
 *   \Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait and
 *   \Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
+37 −0
Original line number Diff line number Diff line
<?php

declare(strict_types = 1);

namespace Drupal\ckeditor5\Attribute;

use Drupal\Component\Plugin\Attribute\Plugin;

#[\Attribute(\Attribute::TARGET_CLASS)]
class CKEditor5AspectsOfCKEditor5Plugin extends Plugin {

  /**
   * Constructs a CKEditor5AspectsOfCKEditor5Plugin attribute.
   *
   * @param class-string[] $plugins
   *   The CKEditor 5 plugin classes provided. Found in the CKEditor5 global js
   *   object as {package.Class}.
   * @param array $config
   *   (optional) A keyed array of additional values for the CKEditor 5
   *   configuration.
   */
  public function __construct(
    public readonly array $plugins,
    public readonly array $config = [],
  ) {}

  /**
   * {@inheritdoc}
   */
  public function get(): array|object {
    return [
      'plugins' => $this->plugins,
      'config' => $this->config,
    ];
  }

}
+88 −0
Original line number Diff line number Diff line
<?php

declare(strict_types = 1);

namespace Drupal\ckeditor5\Attribute;

use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition;
use Drupal\Component\Plugin\Attribute\Plugin;

/**
 * The CKEditor5Plugin attribute.
 */
#[\Attribute(\Attribute::TARGET_CLASS)]
class CKEditor5Plugin extends Plugin {

  /**
   * The CKEditor 5 aspects of the plugin definition.
   *
   * @var \Drupal\ckeditor5\Attribute\CKEditor5AspectsOfCKEditor5Plugin|null
   */
  public readonly ?CKEditor5AspectsOfCKEditor5Plugin $ckeditor5;

  /**
   * The Drupal aspects of the plugin definition.
   *
   * @var \Drupal\ckeditor5\Attribute\DrupalAspectsOfCKEditor5Plugin|null
   */
  public readonly ?DrupalAspectsOfCKEditor5Plugin $drupal;

  /**
   * Constructs a CKEditor5Plugin attribute.
   *
   * Overridden for compatibility with the AttributeBridgeDecorator, which
   * ensures YAML-defined CKEditor 5 plugin definitions are also processed by
   * attributes. Unfortunately it does not (yet) support nested attributes.
   * Force YAML-defined plugin definitions to be parsed by the attributes, to
   * ensure consistent handling of defaults.
   *
   * @see \Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator::getDefinitions()
   *
   * @param string $id
   *   The plugin ID.
   * @param array|\Drupal\ckeditor5\Attribute\CKEditor5AspectsOfCKEditor5Plugin|null $ckeditor5
   *   (optional) The CKEditor 5 aspects of the plugin definition. Required
   *   unless set by deriver.
   * @param array|\Drupal\ckeditor5\Attribute\DrupalAspectsOfCKEditor5Plugin|null $drupal
   *   (optional) The Drupal aspects of the plugin definition. Required unless
   *   set by deriver.
   * @param class-string|null $deriver
   *   (optional) The deriver class.
   */
  public function __construct(
    public readonly string $id,
    array|CKEditor5AspectsOfCKEditor5Plugin|null $ckeditor5 = NULL,
    array|DrupalAspectsOfCKEditor5Plugin|null $drupal = NULL,
    public readonly ?string $deriver = NULL,
  ) {
    $this->ckeditor5 = is_array($ckeditor5) ? new CKEditor5AspectsOfCKEditor5Plugin(...$ckeditor5) : $ckeditor5;
    $this->drupal = is_array($drupal) ? new DrupalAspectsOfCKEditor5Plugin(...$drupal) : $drupal;
  }

  /**
   * {@inheritdoc}
   */
  public function getClass(): string {
    return $this->drupal?->getClass() ?? '';
  }

  /**
   * {@inheritdoc}
   */
  public function setClass($class): void {
    $this->drupal?->setClass($class);
  }

  /**
   * {@inheritdoc}
   */
  public function get(): CKEditor5PluginDefinition {
    return new CKEditor5PluginDefinition([
      'id' => $this->id,
      'ckeditor5' => $this->ckeditor5?->get(),
      'drupal' => $this->drupal?->get(),
      'provider' => $this->getProvider(),
    ]);
  }

}
+69 −0
Original line number Diff line number Diff line
<?php

declare(strict_types = 1);

namespace Drupal\ckeditor5\Attribute;

use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;

#[\Attribute(\Attribute::TARGET_CLASS)]
class DrupalAspectsOfCKEditor5Plugin extends Plugin {

  /**
   * Constructs a DrupalAspectsOfCKEditor5Plugin attribute.
   *
   * @param string|\Drupal\Core\StringTranslation\TranslatableMarkup|null $label
   *   (optional) The human-readable name of the CKEditor plugin. Required
   *   unless set by deriver.
   * @param class-string $class
   *   (optional) The CKEditor 5 plugin class.If not specified, the
   *   CKEditor5PluginDefault class is used.
   * @param string|false $library
   *   (optional) The library this plugin requires.
   * @param string|false $admin_library
   *   (optional) The admin library this plugin provides.
   * @param string[]|false|null $elements
   *   (optional) List of elements and attributes provided. An array of strings,
   *   or false if no elements are provided. Required unless set by deriver.
   *   Syntax for each array value:
   *   - <element> only allows that HTML element with no attributes
   *   - <element attrA attrB> only allows that HTML element with attributes
   *     attrA and attrB, and any value for those attributes.
   *   - <element attrA="foo bar baz" attrB="qux-*"> only allows that HTML
   *     element with attributes attrA (if attrA contains one of the three
   *     listed values) and attrB (if its value has the provided prefix).
   *   - <element data-*> only allows that HTML element with any attribute that
   *     has the given prefix.
   *   Note that <element> means such an element (tag) can be created, whereas
   *   <element attrA attrB> means that `attrA` and `attrB` can be created on
   *   the tag. If a plugin supports both creating the element as well as
   *   setting some attributes or attribute values on it, it should have
   *   distinct entries in the list.
   *   For example, for a link plugin: `<a>` and `<a href>`. The first indicates
   *   the plugin can create such tags, the second indicates it can set the
   *   `href` attribute on it. If the first were omitted, the Drupal CKEditor 5
   *   module would interpret that as "this plugin cannot create `<a>`, it can
   *   only set the `href` attribute on it".
   * @param array $toolbar_items
   *   (optional) List of toolbar items the plugin provides.
   * @param array|false $conditions
   *   (optional) List of conditions to enable this plugin.
   * @param class-string|null $deriver
   *   (optional) The deriver class.
   *
   * @see \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition::getCreatableElements()
   */
  public function __construct(
    public readonly string|TranslatableMarkup|null $label = NULL,
    public string $class = CKEditor5PluginDefault::class,
    public readonly string|false $library = FALSE,
    public readonly string|false $admin_library = FALSE,
    public readonly array|false|null $elements = NULL,
    public readonly array $toolbar_items = [],
    public readonly array|false $conditions = FALSE,
    public readonly ?string $deriver = NULL,
  ) {}

}
+13 −6
Original line number Diff line number Diff line
@@ -4,16 +4,16 @@

namespace Drupal\ckeditor5\Plugin;

use Drupal\ckeditor5\Annotation\CKEditor5Plugin;
use Drupal\ckeditor5\Attribute\CKEditor5Plugin;
use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Plugin\Discovery\AttributeBridgeDecorator;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
use Drupal\Core\Plugin\Discovery\AttributeDiscoveryWithAnnotations;
use Drupal\Core\Plugin\Discovery\ContainerDerivativeDiscoveryDecorator;
use Drupal\Core\Plugin\Discovery\YamlDiscoveryDecorator;
use Drupal\editor\EditorInterface;
@@ -46,7 +46,14 @@ class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5Pl
   *   The module handler to invoke the alter hook with.
   */
  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
    parent::__construct('Plugin/CKEditor5Plugin', $namespaces, $module_handler, CKEditor5PluginInterface::class, CKEditor5Plugin::class);
    parent::__construct(
      'Plugin/CKEditor5Plugin',
      $namespaces,
      $module_handler,
      CKEditor5PluginInterface::class,
      CKEditor5Plugin::class,
      '\Drupal\ckeditor5\Annotation\CKEditor5Plugin',
    );

    $this->alterInfo('ckeditor5_plugin_info');
    $this->setCacheBackend($cache_backend, 'ckeditor5_plugins');
@@ -57,12 +64,12 @@ public function __construct(\Traversable $namespaces, CacheBackendInterface $cac
   */
  protected function getDiscovery() {
    if (!$this->discovery) {
      $discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
      $discovery = new AttributeDiscoveryWithAnnotations($this->subdir, $this->namespaces, $this->pluginDefinitionAttributeName, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
      $discovery = new YamlDiscoveryDecorator($discovery, 'ckeditor5', $this->moduleHandler->getModuleDirectories());
      // Note: adding translatable properties here is impossible because it only
      // supports top-level properties.
      // @see \Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition::label()
      $discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
      $discovery = new AttributeBridgeDecorator($discovery, $this->pluginDefinitionAttributeName);
      $discovery = new ContainerDerivativeDiscoveryDecorator($discovery);
      $this->discovery = $discovery;
    }
Loading