diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php index addcc53a3d..6b11b0a20f 100644 --- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php +++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php @@ -381,7 +381,16 @@ public function provider() { $basic_html_test_case, [ 'expected_messages' => [ - '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>.', + '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> <img loading>.', + ], + 'expected_ckeditor5_settings' => [ + 'plugins' => [ + 'ckeditor5_sourceEditing' => [ + 'allowed_tags' => [ + '', + ], + ], + ], ], ] ); @@ -396,13 +405,13 @@ public function provider() { 'plugins' => [ 'ckeditor5_sourceEditing' => [ 'allowed_tags' => [ - '', + '', ], ], ], ], 'expected_messages' => [ - '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> <img data-caption>.', + '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> <img loading data-caption>.', ], ]); @@ -416,13 +425,13 @@ public function provider() { 'plugins' => [ 'ckeditor5_sourceEditing' => [ 'allowed_tags' => [ - '', + '', ], ], ], ], 'expected_messages' => [ - '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> <img data-align>.', + '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> <img loading data-align>.', ], ]); @@ -433,10 +442,10 @@ public function provider() { 'toolbar' => $basic_html_test_case['expected_ckeditor5_settings']['toolbar'], 'plugins' => [ 'ckeditor5_sourceEditing' => [ - 'allowed_tags' => array_values(array_diff( + 'allowed_tags' => array_merge(array_values(array_diff( $basic_html_test_case['expected_ckeditor5_settings']['plugins']['ckeditor5_sourceEditing']['allowed_tags'], ['

', '

'], - )), + )), ['']), ], 'ckeditor5_heading' => [ 'enabled_headings' => [ @@ -452,7 +461,7 @@ public function provider() { 'expected_fundamental_compatibility_violations' => $basic_html_test_case['expected_fundamental_compatibility_violations'], 'expected_messages' => array_merge( $basic_html_test_case['expected_messages'], - ['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> <h5 id>.'], + ['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> <h5 id> <img loading>.'], ), ]; diff --git a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php index 8cb9aad2d6..d802d6e974 100644 --- a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php +++ b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php @@ -50,16 +50,14 @@ class EditorFileReference extends FilterBase implements ContainerFactoryPluginIn * The plugin implementation definition. * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository * The entity repository. - * @param \Drupal\Core\Image\ImageFactory $image_factory - * The image factory. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, ImageFactory $image_factory = NULL) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository) { $this->entityRepository = $entity_repository; - if ($image_factory === NULL) { - @trigger_error('Calling ' . __METHOD__ . '() without the $image_factory argument is deprecated in drupal:9.1.0 and is required in drupal:10.0.0. See https://www.drupal.org/node/3173719', E_USER_DEPRECATED); - $image_factory = \Drupal::service('image.factory'); + $parameters = func_get_args(); + if (array_key_exists(4, $parameters) && $parameters[4] instanceof ImageFactory) { + @trigger_error('Calling ' . __METHOD__ . '() with the $image_factory argument is deprecated in drupal:9.4.0 and is removed in drupal:10.0.0. See https://www.drupal.org/node/3173719', E_USER_DEPRECATED); + $this->imageFactory = $parameters[4]; } - $this->imageFactory = $image_factory; parent::__construct($configuration, $plugin_id, $plugin_definition); } @@ -71,8 +69,7 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, - $container->get('entity.repository'), - $container->get('image.factory') + $container->get('entity.repository') ); } @@ -95,25 +92,6 @@ public function process($text, $langcode) { $file = $this->entityRepository->loadEntityByUuid('file', $uuid); if ($file instanceof FileInterface) { $node->setAttribute('src', $file->createFileUrl()); - if ($node->nodeName == 'img') { - // Without dimensions specified, layout shifts can occur, - // which are more noticeable on pages that take some time to load. - // As a result, only mark images as lazy load that have dimensions. - $image = $this->imageFactory->get($file->getFileUri()); - $width = $image->getWidth(); - $height = $image->getHeight(); - if ($width !== NULL && $height !== NULL) { - if (!$node->hasAttribute('width')) { - $node->setAttribute('width', $width); - } - if (!$node->hasAttribute('height')) { - $node->setAttribute('height', $height); - } - if (!$node->hasAttribute('loading')) { - $node->setAttribute('loading', 'lazy'); - } - } - } } } diff --git a/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php b/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php index 19877a6fa9..2894c4524d 100644 --- a/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php +++ b/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\editor\Kernel; use Drupal\Core\Cache\Cache; -use Drupal\Core\File\FileSystemInterface; use Drupal\file\Entity\File; use Drupal\filter\FilterPluginCollection; use Drupal\KernelTests\KernelTestBase; @@ -129,28 +128,6 @@ public function testEditorFileReferenceFilter() { $output = $test($input); $this->assertSame($expected_output, $output->getProcessedText()); $this->assertEquals($cache_tag, $output->getCacheTags()); - - // Add a valid image for test lazy loading feature. - /** @var array stdClass */ - $files = $this->getTestFiles('image'); - $image = reset($files); - \Drupal::service('file_system')->copy($image->uri, 'public://llama.jpg', FileSystemInterface::EXISTS_REPLACE); - [$width, $height] = getimagesize('public://llama.jpg'); - $dimensions = 'width="' . $width . '" height="' . $height . '"'; - - // Image dimensions and loading attributes are present. - $input = ''; - $expected_output = ''; - $output = $test($input); - $this->assertSame($expected_output, $output->getProcessedText()); - $this->assertEquals($cache_tag, $output->getCacheTags()); - - // Image dimensions and loading attributes are set manually. - $input = ''; - $expected_output = ''; - $output = $test($input); - $this->assertSame($expected_output, $output->getProcessedText()); - $this->assertEquals($cache_tag, $output->getCacheTags()); } } diff --git a/core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php b/core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php new file mode 100644 index 0000000000..608fc850d0 --- /dev/null +++ b/core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php @@ -0,0 +1,124 @@ +<img loading="eager">."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, + * weight = 15 + * ) + */ +final class FilterImageLazyLoad extends FilterBase implements ContainerFactoryPluginInterface { + + /** + * The entity repository. + * + * @var \Drupal\Core\Entity\EntityRepositoryInterface + */ + protected $entityRepository; + + /** + * The image factory. + * + * @var \Drupal\Core\Image\ImageFactory + */ + protected $imageFactory; + + /** + * Constructs a \Drupal\filter\Plugin\Filter\FilterImageLazyLoad object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. + * @param \Drupal\Core\Image\ImageFactory $image_factory + * The image factory. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, ImageFactory $image_factory) { + $this->entityRepository = $entity_repository; + $this->imageFactory = $image_factory; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { + return new self( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.repository'), + $container->get('image.factory') + ); + + } + + /** + * {@inheritdoc} + */ + public function process($text, $langcode): FilterProcessResult { + $result = new FilterProcessResult($text); + + // If there are no images, exit early. + if (stripos($text, 'setProcessedText($this->transformImages($text)); + } + + /** + * Transform markup of images to include loading="lazy". + * + * @param string $text + * The markup to transform. + * + * @return string + */ + private function transformImages(string $text): string { + $dom = Html::load($text); + $xpath = new \DOMXPath($dom); + // Only set loading="lazy" if no existing loading attribute is specified. + foreach ($xpath->query('//img[not(@loading) and @data-entity-type="file" and @data-entity-uuid]') as $element) { + assert($element instanceof \DOMElement); + $uuid = $element->getAttribute('data-entity-uuid'); + $file = $this->entityRepository->loadEntityByUuid('file', $uuid); + if ($file instanceof FileInterface) { + $image = $this->imageFactory->get($file->getFileUri()); + $width = $image->getWidth(); + $height = $image->getHeight(); + // Set dimensions to avoid content layout shift (CLS). + if ($width !== NULL && !$element->hasAttribute('width')) { + $element->setAttribute('width', (string) $width); + } + if ($height !== NULL && !$element->hasAttribute('height')) { + $element->setAttribute('height', (string) $height); + } + // If dimensions are specified, then set lazy loading. + if ($element->hasAttribute('width') && $element->hasAttribute('height')) { + $element->setAttribute('loading', 'lazy'); + } + } + } + return Html::serialize($dom); + } + +} diff --git a/core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php b/core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php new file mode 100644 index 0000000000..b3c5191676 --- /dev/null +++ b/core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php @@ -0,0 +1,72 @@ +prophesize(FileInterface::class); + $file->getFileUri()->willReturn('foo.png'); + $entity_repository = $this->prophesize(EntityRepositoryInterface::class); + $entity_repository->loadEntityByUuid('file', 'a6d88b01-3b5e-4c02-bf26-24a0c48d61cd')->willReturn($file->reveal()); + $image = $this->prophesize(Image::class); + $image->getHeight()->willReturn(100); + $image->getWidth()->willReturn(100); + $image_factory = $this->prophesize(ImageFactory::class); + $image_factory->get('foo.png')->willReturn($image->reveal()); + $this->filter = new FilterImageLazyLoad([], 'filter_image_lazy_load', ['provider' => 'test'], $entity_repository->reveal(), $image_factory->reveal()); + parent::setUp(); + } + + /** + * @covers ::process + * + * @dataProvider providerHtml + * + * @param string $html + * Input HTML. + * @param string $expected + * The expected output string. + */ + public function testProcess(string $html, string $expected): void { + $this->assertSame($expected, $this->filter->process($html, 'en')->getProcessedText()); + } + + /** + * Provides data for testProcess. + * + * @return array + * An array of test data. + */ + public function providerHtml(): array { + return [ + 'lazy loading attribute already added' => ['

', '

'], + 'eager loading attribute already added' => ['

', '

'], + 'no image tag' => ['

Lorem ipsum...

', '

Lorem ipsum...

'], + 'no loading attribute nor uuid' => ['

', '

'], + 'no loading attribute but uuid' => ['

', '

'], + ]; + } + +} diff --git a/core/profiles/demo_umami/config/install/filter.format.basic_html.yml b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml index 6e647a233a..5ff8381623 100644 --- a/core/profiles/demo_umami/config/install/filter.format.basic_html.yml +++ b/core/profiles/demo_umami/config/install/filter.format.basic_html.yml @@ -16,7 +16,7 @@ filters: status: true weight: -10 settings: - allowed_html: '