View source
<?php
declare (strict_types=1);
namespace Drupal\ckeditor5\Plugin;
use Drupal\ckeditor5\Annotation\CKEditor5Plugin;
use Drupal\ckeditor5\HTMLRestrictions;
use Drupal\Component\Annotation\Plugin\Discovery\AnnotationBridgeDecorator;
use Drupal\Component\Assertion\Inspector;
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\YamlDiscoveryDecorator;
use Drupal\editor\EditorInterface;
use Drupal\filter\FilterPluginCollection;
class CKEditor5PluginManager extends DefaultPluginManager implements CKEditor5PluginManagerInterface {
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/CKEditor5Plugin', $namespaces, $module_handler, CKEditor5PluginInterface::class, CKEditor5Plugin::class);
$this
->alterInfo('ckeditor5_plugin_info');
$this
->setCacheBackend($cache_backend, 'ckeditor5_plugins');
}
protected function getDiscovery() {
if (!$this->discovery) {
$discovery = new AnnotatedClassDiscovery($this->subdir, $this->namespaces, $this->pluginDefinitionAnnotationName, $this->additionalAnnotationNamespaces);
$discovery = new YamlDiscoveryDecorator($discovery, 'ckeditor5', $this->moduleHandler
->getModuleDirectories());
$discovery = new AnnotationBridgeDecorator($discovery, $this->pluginDefinitionAnnotationName);
$this->discovery = $discovery;
}
return $this->discovery;
}
public function getPlugin(string $plugin_id, ?EditorInterface $editor) : CKEditor5PluginInterface {
$configuration = $editor ? self::getPluginConfiguration($editor, $plugin_id) : [];
return $this
->createInstance($plugin_id, $configuration);
}
protected static function getPluginConfiguration(EditorInterface $editor, string $plugin_id) : array {
if ($editor
->getEditor() !== 'ckeditor5') {
throw new \InvalidArgumentException('This method should only be called on text editor config entities using CKEditor 5.');
}
return $editor
->getSettings()['plugins'][$plugin_id] ?? [];
}
public function getToolbarItems() : array {
return $this
->mergeDefinitionValues('getToolbarItems', $this
->getDefinitions());
}
public function getAdminLibraries() : array {
$list = $this
->mergeDefinitionValues('getAdminLibrary', $this
->getDefinitions());
array_unshift($list, 'ckeditor5/admin');
return $list;
}
public function getEnabledLibraries(EditorInterface $editor) : array {
$list = $this
->mergeDefinitionValues('getLibrary', $this
->getEnabledDefinitions($editor));
$list = array_unique($list);
array_unshift($list, 'ckeditor5/drupal.ckeditor5');
sort($list);
return $list;
}
public function getEnabledDefinitions(EditorInterface $editor) : array {
$definitions = $this
->getDefinitions();
ksort($definitions);
$definitions_with_plugins_condition = [];
foreach ($definitions as $plugin_id => $definition) {
if ($definition
->hasConditions()) {
$plugin = $this
->getPlugin($plugin_id, $editor);
if ($this
->isPluginDisabled($plugin, $editor)) {
unset($definitions[$plugin_id]);
}
else {
if (array_key_exists('plugins', $definition
->getConditions())) {
$definitions_with_plugins_condition[$plugin_id] = $definition;
}
}
}
elseif ($definition
->hasToolbarItems()) {
if (empty(array_intersect($editor
->getSettings()['toolbar']['items'], array_keys($definition
->getToolbarItems())))) {
unset($definitions[$plugin_id]);
}
}
}
if ($editor
->getFilterFormat()
->getHtmlRestrictions() !== FALSE) {
unset($definitions['ckeditor5_arbitraryHtmlSupport']);
}
foreach ($definitions_with_plugins_condition as $plugin_id => $definition) {
if (!empty(array_diff($definition
->getConditions()['plugins'], array_keys($definitions)))) {
unset($definitions[$plugin_id]);
}
}
if (!isset($definitions['ckeditor5_arbitraryHtmlSupport'])) {
$restrictions = new HTMLRestrictions($this
->getProvidedElements(array_keys($definitions), $editor, FALSE));
if ($restrictions
->getWildcardSubset()
->allowsNothing()) {
unset($definitions['ckeditor5_wildcardHtmlSupport']);
}
}
else {
unset($definitions['ckeditor5_wildcardHtmlSupport']);
}
return $definitions;
}
public function findPluginSupportingElement(string $tag) : ?string {
$selected_provided_elements = [];
$plugin_id = NULL;
foreach ($this
->getDefinitions() as $id => $definition) {
$provided_elements = $this
->getProvidedElements([
$id,
]);
if (array_key_exists($tag, $provided_elements)) {
if ($definition
->hasConditions()) {
continue;
}
$selected_plugin = isset($selected_provided_elements[$tag]);
$selected_config = $selected_provided_elements[$tag] ?? FALSE;
$adds_attribute_config = is_array($provided_elements[$tag]) && $selected_plugin && !is_array($selected_config);
$broader_attribute_config = FALSE;
if ($selected_plugin && is_array($selected_config) && is_array($provided_elements[$tag])) {
$selected_plugin_full_attributes = array_filter($selected_config, function ($attribute_config) {
return !is_array($attribute_config);
});
$being_checked_plugin_full_attributes = array_filter($provided_elements[$tag], function ($attribute_config) {
return !is_array($attribute_config);
});
if (count($being_checked_plugin_full_attributes) > count($selected_plugin_full_attributes)) {
$broader_attribute_config = TRUE;
}
}
if (empty($selected_provided_elements) || $broader_attribute_config || $adds_attribute_config) {
$selected_provided_elements = $provided_elements;
$plugin_id = $id;
}
}
}
return $plugin_id;
}
public function getCKEditor5PluginConfig(EditorInterface $editor) : array {
$definitions = $this
->getEnabledDefinitions($editor);
$config = [];
foreach ($definitions as $plugin_id => $definition) {
$plugin = $this
->getPlugin($plugin_id, $editor);
$config[$plugin_id] = $plugin
->getDynamicPluginConfig($definition
->getCKEditor5Config(), $editor);
}
if (isset($definitions['ckeditor5_wildcardHtmlSupport'])) {
$allowed_elements = new HTMLRestrictions($this
->getProvidedElements(array_keys($definitions), $editor, FALSE));
$concrete_allowed_elements = $allowed_elements
->getConcreteSubset();
$net_new_elements = $allowed_elements
->diff($concrete_allowed_elements);
$config['ckeditor5_wildcardHtmlSupport'] = [
'htmlSupport' => [
'allow' => $net_new_elements
->toGeneralHtmlSupportConfig(),
],
];
}
return [
'plugins' => $this
->mergeDefinitionValues('getCKEditor5Plugins', $definitions),
'config' => NestedArray::mergeDeepArray($config),
];
}
public function getProvidedElements(array $plugin_ids = [], EditorInterface $editor = NULL, bool $resolve_wildcards = TRUE, bool $creatable_elements_only = FALSE) : array {
$plugins = $this
->getDefinitions();
if (!empty($plugin_ids)) {
$plugins = array_intersect_key($plugins, array_flip($plugin_ids));
}
$elements = HTMLRestrictions::emptySet();
foreach ($plugins as $id => $definition) {
if (!$definition
->hasElements()) {
continue;
}
$defined_elements = $definition
->getElements();
if (is_a($definition
->getClass(), CKEditor5PluginElementsSubsetInterface::class, TRUE)) {
if ($id === 'ckeditor5_sourceEditing') {
$defined_elements = !isset($editor) ? [] : $this
->getPlugin($id, $editor)
->getElementsSubset();
}
elseif (isset($editor)) {
$subset = $this
->getPlugin($id, $editor)
->getElementsSubset();
$subset_restrictions = HTMLRestrictions::fromString(implode($subset));
$defined_restrictions = HTMLRestrictions::fromString(implode($defined_elements));
$max_supported = $defined_restrictions;
if (!$defined_restrictions
->getWildcardSubset()
->allowsNothing()) {
$concrete_tags_to_use_to_resolve_wildcards = $subset_restrictions
->extractPlainTagsSubset();
$max_supported = $max_supported
->merge($concrete_tags_to_use_to_resolve_wildcards)
->diff($concrete_tags_to_use_to_resolve_wildcards);
}
$not_in_max_supported = $subset_restrictions
->diff($max_supported);
if (!$not_in_max_supported
->allowsNothing()) {
if ($editor
->isNew()) {
$subset = [];
}
else {
throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did not return a subset, the following tags are absent from the plugin definition: "%s".', $id, implode(' ', $not_in_max_supported
->toCKEditor5ElementsArray())));
}
}
$defined_creatable = HTMLRestrictions::fromString(implode($definition
->getCreatableElements()));
$subset_creatable_actual = HTMLRestrictions::fromString(implode(array_filter($subset, [
CKEditor5PluginDefinition::class,
'isCreatableElement',
])));
$subset_creatable_needed = $subset_restrictions
->extractPlainTagsSubset()
->intersect($defined_creatable);
$missing_creatable_for_subset = $subset_creatable_needed
->diff($subset_creatable_actual);
if (!$missing_creatable_for_subset
->allowsNothing()) {
throw new \LogicException(sprintf('The "%s" CKEditor 5 plugin implements ::getElementsSubset() and did return a subset ("%s") but the following tags can no longer be created: "%s".', $id, implode($subset_restrictions
->toCKEditor5ElementsArray()), implode($missing_creatable_for_subset
->toCKEditor5ElementsArray())));
}
$defined_elements = $subset;
}
}
assert(Inspector::assertAllStrings($defined_elements));
if ($creatable_elements_only) {
$defined_elements = array_filter($defined_elements, [
CKEditor5PluginDefinition::class,
'isCreatableElement',
]);
}
foreach ($defined_elements as $element) {
$additional_elements = HTMLRestrictions::fromString($element);
$elements = $elements
->merge($additional_elements);
}
}
return $elements
->getAllowedElements($resolve_wildcards);
}
protected function mergeDefinitionValues(string $get_method, array $definitions) : array {
assert(method_exists(CKEditor5PluginDefinition::class, $get_method));
$has_method = 'has' . substr($get_method, 3);
assert(method_exists(CKEditor5PluginDefinition::class, $has_method));
$per_plugin = array_filter(array_map(function (CKEditor5PluginDefinition $definition) use ($get_method, $has_method) {
if ($definition
->{$has_method}()) {
return $definition
->{$get_method}();
}
}, $definitions));
return array_reduce($per_plugin, function (array $result, $current) : array {
return is_array($current) && is_array(reset($current)) ? $result + $current : array_merge($result, (array) $current);
}, []);
}
protected function isPluginDisabled(CKEditor5PluginInterface $plugin, EditorInterface $editor) : bool {
assert($plugin
->getPluginDefinition()
->hasConditions());
foreach ($plugin
->getPluginDefinition()
->getConditions() as $condition_type => $required_value) {
switch ($condition_type) {
case 'toolbarItem':
if (!in_array($required_value, $editor
->getSettings()['toolbar']['items'])) {
return TRUE;
}
break;
case 'imageUploadStatus':
$image_upload_status = $editor
->getImageUploadSettings()['status'] ?? FALSE;
if (!$image_upload_status) {
return TRUE;
}
break;
case 'filter':
$filters = $editor
->getFilterFormat()
->filters();
assert($filters instanceof FilterPluginCollection);
if (!$filters
->has($required_value) || !$filters
->get($required_value)->status) {
return TRUE;
}
break;
case 'requiresConfiguration':
$intersection = array_intersect($plugin
->getConfiguration(), $required_value);
return $intersection !== $required_value;
case 'plugins':
return FALSE;
}
}
return FALSE;
}
}