From a266ba6e62995afe451cdda0a2d7b68b5bdc3cb9 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Wed, 30 Nov 2022 16:33:11 +0000 Subject: [PATCH] Issue #3247795 by heddn, Graber, ravi.shankar, yogeshmpawar, Anchal_gupta, Wim Leers, Fabianx, alexpott, benmorss, catch: Add text filter plugin to support and remove it from editor_file_reference --- .../FunctionalJavascript/CKEditor5Test.php | 4 +- core/modules/editor/editor.post_update.php | 31 +++++++ .../src/Plugin/Filter/EditorFileReference.php | 22 ++--- ...EditorAddLazyLoadImageFilterUpdateTest.php | 48 +++++++++++ .../Kernel/EditorFileReferenceFilterTest.php | 12 +-- .../src/Plugin/Filter/FilterImageLazyLoad.php | 59 +++++++++++++ .../src/Unit/FilterImageLazyLoadTest.php | 86 +++++++++++++++++++ .../install/filter.format.basic_html.yml | 8 +- .../install/filter.format.full_html.yml | 6 ++ .../install/filter.format.basic_html.yml | 6 ++ .../install/filter.format.full_html.yml | 6 ++ 11 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 core/modules/editor/tests/src/Functional/Update/EditorAddLazyLoadImageFilterUpdateTest.php create mode 100644 core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php create mode 100644 core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php diff --git a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php index 76c40a2ce21..61b337f1fb6 100644 --- a/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php +++ b/core/modules/ckeditor5/tests/src/FunctionalJavascript/CKEditor5Test.php @@ -543,7 +543,7 @@ JS; $uploaded_image = File::load(1); $image_url = $this->container->get('file_url_generator')->generateString($uploaded_image->getFileUri()); $image_uuid = $uploaded_image->uuid(); - $assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @loading="lazy" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid)); + $assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid)); // Ensure that width, height, and length attributes are not stored in the // database. @@ -555,7 +555,7 @@ JS; $this->assertNotEmpty($assert_session->waitForElement('css', '.ck-editor')); $page->pressButton('Save'); - $assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @loading="lazy" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid)); + $assert_session->elementExists('xpath', sprintf('//img[@src="%s" and @width and @height and @data-entity-uuid="%s" and @data-entity-type="file"]', $image_url, $image_uuid)); } /** diff --git a/core/modules/editor/editor.post_update.php b/core/modules/editor/editor.post_update.php index 71814f3101f..112409ecb0e 100644 --- a/core/modules/editor/editor.post_update.php +++ b/core/modules/editor/editor.post_update.php @@ -5,6 +5,10 @@ * Post update functions for Editor. */ +use Drupal\filter\Entity\FilterFormat; +use Drupal\filter\FilterFormatInterface; +use Drupal\filter\FilterPluginCollection; + /** * Implements hook_removed_post_updates(). */ @@ -13,3 +17,30 @@ function editor_removed_post_updates() { 'editor_post_update_clear_cache_for_file_reference_filter' => '9.0.0', ]; } + +/** + * Enable filter_image_lazy_load if editor_file_reference is enabled. + */ +function editor_post_update_image_lazy_load(): void { + if (\Drupal::service('plugin.manager.filter')->hasDefinition('editor_file_reference')) { + foreach (FilterFormat::loadMultiple() as $format) { + assert($format instanceof FilterFormatInterface); + $collection = $format->filters(); + $configuration = $collection->getConfiguration(); + assert($collection instanceof FilterPluginCollection); + if (array_key_exists('editor_file_reference', $configuration)) { + $collection->addInstanceId('filter_image_lazy_load'); + $configuration['filter_image_lazy_load'] = [ + 'id' => 'filter_image_lazy_load', + 'provider' => 'editor', + 'status' => TRUE, + // Place lazy loading after editor file reference. + 'weight' => $configuration['editor_file_reference']['weight'] + 1, + 'settings' => [], + ]; + $collection->setConfiguration($configuration); + $format->save(); + } + } + } +} diff --git a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php index e41901f7c41..d107b95347b 100644 --- a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php +++ b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php @@ -19,7 +19,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * @Filter( * id = "editor_file_reference", * title = @Translation("Track images uploaded via a Text Editor"), - * description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."), + * description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed, along with their dimensions."), * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE * ) */ @@ -92,22 +92,16 @@ class EditorFileReference extends FilterBase implements ContainerFactoryPluginIn 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'); - } + // Set dimensions to avoid content layout shift (CLS). + // @see https://web.dev/cls/ + if ($width !== NULL && !$node->hasAttribute('width')) { + $node->setAttribute('width', (string) $width); + } + if ($height !== NULL && !$node->hasAttribute('height')) { + $node->setAttribute('height', (string) $height); } } } diff --git a/core/modules/editor/tests/src/Functional/Update/EditorAddLazyLoadImageFilterUpdateTest.php b/core/modules/editor/tests/src/Functional/Update/EditorAddLazyLoadImageFilterUpdateTest.php new file mode 100644 index 00000000000..a4e9b659b15 --- /dev/null +++ b/core/modules/editor/tests/src/Functional/Update/EditorAddLazyLoadImageFilterUpdateTest.php @@ -0,0 +1,48 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests upgrading filter settings. + * + * @see editor_post_update_image_lazy_load() + */ + public function testUpdateLazyImageLoad(): void { + $config = $this->config('filter.format.full_html'); + $this->assertArrayNotHasKey('filter_image_lazy_load', $config->get('filters')); + + $this->runUpdates(); + + $config = $this->config('filter.format.full_html'); + $filters = $config->get('filters'); + $this->assertArrayHasKey('filter_image_lazy_load', $filters); + $this->assertEquals($filters['editor_file_reference']['weight'] + 1, $filters['filter_image_lazy_load']['weight']); + } + +} diff --git a/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php b/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php index 19877a6fa93..b764eedaef8 100644 --- a/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php +++ b/core/modules/editor/tests/src/Kernel/EditorFileReferenceFilterTest.php @@ -130,7 +130,7 @@ class EditorFileReferenceFilterTest extends KernelTestBase { $this->assertSame($expected_output, $output->getProcessedText()); $this->assertEquals($cache_tag, $output->getCacheTags()); - // Add a valid image for test lazy loading feature. + // Add a valid image for image dimension testing. /** @var array stdClass */ $files = $this->getTestFiles('image'); $image = reset($files); @@ -138,16 +138,16 @@ class EditorFileReferenceFilterTest extends KernelTestBase { [$width, $height] = getimagesize('public://llama.jpg'); $dimensions = 'width="' . $width . '" height="' . $height . '"'; - // Image dimensions and loading attributes are present. + // Image dimensions are present. $input = ''; - $expected_output = ''; + $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 = ''; + // Image dimensions 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 00000000000..ee0802e1c8a --- /dev/null +++ b/core/modules/filter/src/Plugin/Filter/FilterImageLazyLoad.php @@ -0,0 +1,59 @@ +<img loading="eager">."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE, + * weight = 15 + * ) + */ +final class FilterImageLazyLoad extends FilterBase { + + /** + * {@inheritdoc} + */ + public function process($text, $langcode): FilterProcessResult { + $result = new FilterProcessResult($text); + + // If there are no images, return 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 + * The transformed text with loading attribute added. + */ + 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 and + // dimensions are specified. + foreach ($xpath->query('//img[not(@loading="eager") and @width and @height]') as $element) { + assert($element instanceof \DOMElement); + $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 00000000000..6da46a4a55f --- /dev/null +++ b/core/modules/filter/tests/src/Unit/FilterImageLazyLoadTest.php @@ -0,0 +1,86 @@ +filter = new FilterImageLazyLoad([], 'filter_image_lazy_load', ['provider' => 'test']); + 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' => [ + 'input' => '

', + 'output' => '

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

', + 'output' => '

', + ], + 'image dimensions provided' => [ + 'input' => '

', + '

', + ], + 'width image dimensions provided' => [ + 'input' => '

', + '

', + ], + 'height image dimensions provided' => [ + 'input' => '

', + '

', + ], + 'invalid loading attribute' => [ + 'input' => '

', + 'output' => '

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

Lorem ipsum...

', + 'output' => '

Lorem ipsum...

', + ], + 'no image dimensions provided' => [ + 'input' => '

', + 'output' => '

', + ], + ]; + } + +} 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 e3249c8561e..5c753e7f338 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: '