diff --git a/composer.json b/composer.json index 21a54a2d7..95d77967d 100644 --- a/composer.json +++ b/composer.json @@ -66,8 +66,8 @@ "php": "^8.1" }, "conflict": { - "drupal/core": "<10.2 || >=10.3", - "drupal/core-composer-scaffold": "<10.2 || >=10.3", + "drupal/core": "<10.3", + "drupal/core-composer-scaffold": "<10.3", "drupal/ctools": "<3.11 || ^4.0.1", "drupal/gin_toolbar": ">1.0.0-rc5", "drupal/helfi_media_map": "*", @@ -82,11 +82,11 @@ "drupal/core": { "[#UHF-181] Hide untranslated menu links (https://www.drupal.org/project/drupal/issues/3091246)": "https://www.drupal.org/files/issues/2023-12-18/3091246--allow-menu-tree-manipulators-alter--24.patch", "[#UHF-3812] Ajax exposed filters not working for multiple instances of the same Views block placed on one page (https://www.drupal.org/project/drupal/issues/3163299)": "https://www.drupal.org/files/issues/2023-05-07/3163299-104-D10.patch", - "[#UHF-4325] Strip whitespaces from twig debug comments": "https://raw.githubusercontent.com/City-of-Helsinki/drupal-helfi-platform-config/b628bc051d82a1883768364050aa833824bd48c8/patches/drupal_core_strip_debug_mode_whitespaces_10.1.x.patch", + "[#UHF-4325] Strip whitespaces from twig debug comments": "https://raw.githubusercontent.com/City-of-Helsinki/drupal-helfi-platform-config/ebee28c484e86152970b3f5534b7080831322316/patches/drupal_core_strip_debug_mode_whitespaces_10.3.x.patch", "[#UHF-7008] Core localization file download URL is wrong (https://www.drupal.org/project/drupal/issues/3022876)": "https://git.drupalcode.org/project/drupal/-/commit/40a96136b2dfe4322338508dffa636f6cb407900.patch", "[#UHF-7008] Add multilingual support for caching basefield definitions (https://www.drupal.org/project/drupal/issues/3114824)": "https://www.drupal.org/files/issues/2020-02-20/3114824_2.patch", "[#UHF-7008] Admin toolbar and contextual links should always be rendered in the admin language (https://www.drupal.org/project/drupal/issues/2313309)": "https://www.drupal.org/files/issues/2023-12-19/2313309-179.patch", - "[#UHF-9388] Process translation config files for custom modules (https://www.drupal.org/i/2845437)": "https://www.drupal.org/files/issues/2023-10-16/2845437-61.patch", + "[#UHF-9388] Process configuration translation files for custom modules (https://www.drupal.org/i/2845437)": "https://raw.githubusercontent.com/City-of-Helsinki/drupal-helfi-platform-config/fd68277191b8f8ec290e53b5fbbae699b2260384/patches/drupal-2845437-process-custom-module-translation-config-10.3.x.patch", "[#UHF-9690] Allow updating lists when switching from allowed values to allowed values function (https://www.drupal.org/i/2873353)": "https://www.drupal.org/files/issues/2021-05-18/allow-allowed-values-function-update-D9-2873353_1.patch", "[#UHF-9952, #UHF-9980] Duplicate
tags (https://www.drupal.org/i/3083786)": "https://www.drupal.org/files/issues/2024-05-14/3083786-duplicate-br-tags-2024-05-14-no-tests.patch" }, diff --git a/helfi_platform_config.install b/helfi_platform_config.install index e328a7361..f3b60861b 100644 --- a/helfi_platform_config.install +++ b/helfi_platform_config.install @@ -126,3 +126,10 @@ function helfi_platform_config_update_9308() : void { $module_installer->install(['help']); } } + +/** + * Rerun hal/rdf module disabling since the first one did not run. + */ +function helfi_platform_config_update_9309() { + helfi_platform_config_update_9307(); +} diff --git a/patches/drupal-2845437-process-custom-module-translation-config-10.3.x.patch b/patches/drupal-2845437-process-custom-module-translation-config-10.3.x.patch new file mode 100644 index 000000000..92cb0c62f --- /dev/null +++ b/patches/drupal-2845437-process-custom-module-translation-config-10.3.x.patch @@ -0,0 +1,384 @@ +diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php +index 9b9855737e..6f482df009 100644 +--- a/core/lib/Drupal/Core/Config/ConfigInstaller.php ++++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php +@@ -158,16 +158,22 @@ public function installDefaultConfig($type, $name) { + $profile_installed = in_array($this->drupalGetProfile(), $this->getEnabledExtensions(), TRUE); + if (!$this->isSyncing() && (!InstallerKernel::installationAttempted() || $profile_installed)) { + $optional_install_path = $extension_path . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; ++ $collection_info = $this->configManager->getConfigCollectionInfo(); + if (is_dir($optional_install_path)) { + // Install any optional config the module provides. + $storage = new FileStorage($optional_install_path, StorageInterface::DEFAULT_COLLECTION); +- $this->installOptionalConfig($storage, ''); ++ foreach ($collection_info->getCollectionNames() as $collection) { ++ $this->installOptionalConfig($storage, [], $collection); ++ } + } + // Install any optional configuration entities whose dependencies can now + // be met. This searches all the installed modules config/optional + // directories. + $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, FALSE, $this->installProfile); +- $this->installOptionalConfig($storage, [$type => $name]); ++ $dependency = $name == 'update' ? [] : [$type => $name]; ++ foreach ($collection_info->getCollectionNames() as $collection) { ++ $this->installOptionalConfig($storage, $dependency, $collection); ++ } + } + + // Reset all the static caches and list caches. +@@ -177,22 +183,26 @@ public function installDefaultConfig($type, $name) { + /** + * {@inheritdoc} + */ +- public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = []) { ++ public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = [], $collection = StorageInterface::DEFAULT_COLLECTION) { + $profile = $this->drupalGetProfile(); + $enabled_extensions = $this->getEnabledExtensions(); + $existing_config = $this->getActiveStorages()->listAll(); + ++ if (!empty($storage) && $storage->getCollectionName() != $collection) { ++ $storage = $storage->createCollection($collection); ++ } ++ + // Create the storages to read configuration from. + if (!$storage) { + // Search the install profile's optional configuration too. +- $storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE, $this->installProfile); ++ $storage = new ExtensionInstallStorage($this->getActiveStorages($collection), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, $collection, TRUE, $this->installProfile); + // The extension install storage ensures that overrides are used. + $profile_storage = NULL; + } + elseif (!empty($profile)) { + // Creates a profile storage to search for overrides. + $profile_install_path = $this->extensionPathResolver->getPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY; +- $profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION); ++ $profile_storage = new FileStorage($profile_install_path, $collection); + } + else { + // Profile has not been set yet. For example during the first steps of the +@@ -211,7 +221,10 @@ public function installOptionalConfig(?StorageInterface $storage = NULL, $depend + + // Filter the list of configuration to only include configuration that + // should be created. +- $list = array_filter($list, function ($config_name) use ($existing_config) { ++ $list = array_filter($list, function ($config_name) use ($existing_config, $collection) { ++ if ($collection != StorageInterface::DEFAULT_COLLECTION) { ++ return TRUE; ++ } + // Only list configuration that: + // - does not already exist + // - is a configuration entity (this also excludes config that has an +@@ -257,7 +270,7 @@ public function installOptionalConfig(?StorageInterface $storage = NULL, $depend + + // Create the optional configuration if there is any left after filtering. + if (!empty($config_to_create)) { +- $this->createConfiguration(StorageInterface::DEFAULT_COLLECTION, $config_to_create); ++ $this->createConfiguration($collection, $config_to_create); + } + } + +diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +index d6ac429ce3..032510c95d 100644 +--- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php ++++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +@@ -50,8 +50,10 @@ public function installDefaultConfig($type, $name); + * this dependency. The format is dependency type as the key ('module', + * 'theme', or 'config') and the dependency name as the value + * ('node', 'olivero', 'views.view.frontpage'). ++ * @param string $collection ++ * (optional) The configuration collection. + */ +- public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = []); ++ public function installOptionalConfig(?StorageInterface $storage = NULL, $dependency = [], $collection = StorageInterface::DEFAULT_COLLECTION); + + /** + * Installs all default configuration in the specified collection. +diff --git a/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php b/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php +index 76ce0ecb12..eb31633592 100644 +--- a/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php ++++ b/core/lib/Drupal/Core/ProxyClass/Config/ConfigInstaller.php +@@ -79,9 +79,9 @@ public function installDefaultConfig($type, $name) + * {@inheritdoc} + */ + public function installOptionalConfig(?\Drupal\Core\Config\StorageInterface $storage = NULL, $dependency = array ( +- )) ++ ), $collection = '') + { +- return $this->lazyLoadItself()->installOptionalConfig($storage, $dependency); ++ return $this->lazyLoadItself()->installOptionalConfig($storage, $dependency, $collection); + } + + /** +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/config_install_optional_test.settings.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/config_install_optional_test.settings.yml +new file mode 100644 +index 0000000000..0a694c1799 +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/config_install_optional_test.settings.yml +@@ -0,0 +1,5 @@ ++langcode: en ++data: ++ item: 'Item (en)' ++label: 'Label (en)' ++text: 'Text (en)' +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/language/fr/config_install_optional_test.settings.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/language/fr/config_install_optional_test.settings.yml +new file mode 100644 +index 0000000000..a6eb7636be +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config/install/language/fr/config_install_optional_test.settings.yml +@@ -0,0 +1,4 @@ ++data: ++ item: 'Item (fr)' ++label: 'Label (fr)' ++text: 'Text (fr)' +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/block.block.test_translate.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/block.block.test_translate.yml +new file mode 100644 +index 0000000000..34a724ad91 +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/block.block.test_translate.yml +@@ -0,0 +1,19 @@ ++langcode: en ++status: true ++dependencies: ++ theme: ++ - stark ++id: test_translate ++theme: stark ++region: content ++weight: -40 ++provider: null ++plugin: local_tasks_block ++settings: ++ id: local_tasks_block ++ label: 'Title (en)' ++ label_display: '0' ++ provider: core ++ primary: true ++ secondary: true ++visibility: { } +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/language/fr/block.block.test_translate.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/language/fr/block.block.test_translate.yml +new file mode 100644 +index 0000000000..0e1f711cd7 +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config/optional/language/fr/block.block.test_translate.yml +@@ -0,0 +1,2 @@ ++settings: ++ label: 'Title (fr)' +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config/schema/config_install_optional_test.schema.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config/schema/config_install_optional_test.schema.yml +new file mode 100644 +index 0000000000..c1f1ab76f1 +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config/schema/config_install_optional_test.schema.yml +@@ -0,0 +1,17 @@ ++config_install_optional_test.settings: ++ type: config_object ++ label: 'Config install/optional test settings' ++ mapping: ++ data: ++ type: mapping ++ label: 'Data' ++ mapping: ++ item: ++ type: label ++ label: Item ++ label: ++ type: label ++ label: 'Label' ++ text: ++ type: text ++ label: 'Text' +diff --git a/core/modules/config_translation/tests/modules/config_install_optional_test/config_install_optional_test.info.yml b/core/modules/config_translation/tests/modules/config_install_optional_test/config_install_optional_test.info.yml +new file mode 100644 +index 0000000000..181d93d34a +--- /dev/null ++++ b/core/modules/config_translation/tests/modules/config_install_optional_test/config_install_optional_test.info.yml +@@ -0,0 +1,6 @@ ++name: 'Config install/optional test' ++type: module ++description: 'Support module for configuration install/optional testing.' ++package: Testing ++version: '1.2' ++hidden: true +diff --git a/core/modules/config_translation/tests/src/Kernel/ConfigTranslationTest.php b/core/modules/config_translation/tests/src/Kernel/ConfigTranslationTest.php +new file mode 100644 +index 0000000000..ac36f873d1 +--- /dev/null ++++ b/core/modules/config_translation/tests/src/Kernel/ConfigTranslationTest.php +@@ -0,0 +1,72 @@ ++installConfig(['language']); ++ $locale_tables = [ ++ 'locales_source', ++ 'locales_target', ++ 'locales_location', ++ ]; ++ $this->installSchema('locale', $locale_tables); ++ $language = ConfigurableLanguage::createFromLangcode('fr'); ++ $language->save(); ++ \Drupal::service('module_installer')->install(['config_install_optional_test']); ++ $this->installConfig(['config_install_optional_test']); ++ // Check, if 'config_install_optional_test.settings' has proper translation. ++ $config_translation = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'config_install_optional_test.settings'); ++ $this->assertTrue($config_translation->get('data.item') == 'Item (fr)'); ++ $this->assertTrue($config_translation->get('label') == 'Label (fr)'); ++ $this->assertTrue($config_translation->get('text') == 'Text (fr)'); ++ } ++ ++ /** ++ * Tests optional configuration translation. ++ */ ++ public function testOptionalConfigTranslate(): void { ++ $this->installConfig(['language']); ++ $locale_tables = [ ++ 'locales_source', ++ 'locales_target', ++ 'locales_location', ++ ]; ++ $this->installSchema('locale', $locale_tables); ++ \Drupal::service('theme_installer')->install(['stark']); ++ $language = ConfigurableLanguage::createFromLangcode('fr'); ++ $language->save(); ++ \Drupal::service('module_installer')->install(['config_install_optional_test']); ++ $this->installConfig(['config_install_optional_test']); ++ // Check, if block 'test_translate' has proper translation. ++ $config_translation = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'block.block.test_translate'); ++ $this->assertTrue($config_translation->get('settings.label') == 'Title (fr)'); ++ } ++ ++} +diff --git a/core/modules/locale/src/LocaleConfigManager.php b/core/modules/locale/src/LocaleConfigManager.php +index 374bae750f..9fe00bf0a5 100644 +--- a/core/modules/locale/src/LocaleConfigManager.php ++++ b/core/modules/locale/src/LocaleConfigManager.php +@@ -630,20 +630,20 @@ public function updateConfigTranslations(array $names, array $langcodes = []) { + protected function filterOverride(array $override_data, array $translatable) { + $filtered_data = []; + foreach ($override_data as $key => $value) { +- if (isset($translatable[$key])) { ++ if (isset($translatable[$key]) && is_array($value)) { + // If the translatable default configuration has this key, look further +- // for subkeys or ignore this element for scalar values. +- if (is_array($value)) { +- $value = $this->filterOverride($value, $translatable[$key]); +- if (!empty($value)) { +- $filtered_data[$key] = $value; +- } ++ // for subkeys. ++ $value = $this->filterOverride($value, $translatable[$key]); ++ if (!empty($value)) { ++ $filtered_data[$key] = $value; + } + } + else { + // If this key was not in the translatable default configuration, +- // keep it. +- $filtered_data[$key] = $value; ++ // or the element has scalar values, then keep it. ++ if (!empty($value)) { ++ $filtered_data[$key] = $value; ++ } + } + } + return $filtered_data; +diff --git a/core/modules/locale/tests/src/Functional/LocaleConfigTranslationImportTest.php b/core/modules/locale/tests/src/Functional/LocaleConfigTranslationImportTest.php +index fc518b12f1..acc87f4e62 100644 +--- a/core/modules/locale/tests/src/Functional/LocaleConfigTranslationImportTest.php ++++ b/core/modules/locale/tests/src/Functional/LocaleConfigTranslationImportTest.php +@@ -195,7 +195,7 @@ public function testConfigTranslationModuleInstall(): void { + } + + /** +- * Tests removing a string from Locale deletes configuration translations. ++ * Tests removing a string from Locale shouldn't delete config translations. + */ + public function testLocaleRemovalAndConfigOverrideDelete(): void { + // Enable the locale module. +@@ -224,7 +224,10 @@ public function testLocaleRemovalAndConfigOverrideDelete(): void { + $this->submitForm(['predefined_langcode' => 'af'], 'Add language'); + + $override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings'); +- $this->assertEquals(['translatable_default_with_translation' => 'Locale can translate Afrikaans'], $override->get()); ++ $expected = [ ++ 'translatable_default_with_translation' => 'Locale can translate Afrikaans', ++ ]; ++ $this->assertEquals($expected, $override->get()); + + // Remove the string from translation to simulate a Locale removal. Note + // that is no current way of doing this in the UI. +@@ -235,13 +238,15 @@ public function testLocaleRemovalAndConfigOverrideDelete(): void { + $count = \Drupal::service('locale.config_manager')->updateConfigTranslations(['locale_test_translate.settings'], ['af']); + $this->assertEquals(1, $count, 'Correct count of updated translations'); + ++ // Deleting the locale translation should not delete configuration ++ // translations. + $override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings'); +- $this->assertEquals([], $override->get()); +- $this->assertTrue($override->isNew(), 'The configuration override was deleted when the Locale string was deleted.'); ++ $this->assertEquals($expected, $override->get()); ++ $this->assertFalse($override->isNew(), 'The configuration override was not deleted when the Locale string was deleted.'); + } + + /** +- * Tests removing a string from Locale changes configuration translations. ++ * Tests removing a string from Locale shouldn't change config translations. + */ + public function testLocaleRemovalAndConfigOverridePreserve(): void { + // Enable the locale module. +@@ -298,11 +303,9 @@ public function testLocaleRemovalAndConfigOverridePreserve(): void { + $this->drupalGet('admin/config/regional/translate'); + $this->submitForm($edit, 'Save translations'); + ++ // Deleting the locale translation should not change configuration ++ // translations. + $override = \Drupal::languageManager()->getLanguageConfigOverride('af', 'locale_test_translate.settings'); +- $expected = [ +- 'translatable_no_default' => 'This translation is preserved', +- 'translatable_default_with_no_translation' => 'This translation is preserved', +- ]; + $this->assertEquals($expected, $override->get()); + } + +diff --git a/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php b/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php +index 8648ca59f7..d588657b5a 100644 +--- a/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php ++++ b/core/modules/locale/tests/src/Kernel/LocaleConfigSubscriberForeignTest.php +@@ -137,7 +137,7 @@ public function testEnglish(): void { + $this->assertTranslation($config_name, 'Updated English', 'en'); + + $this->deleteLocaleTranslationData($config_name, 'test', 'English test', 'en'); +- $this->assertNoConfigOverride($config_name, 'en'); ++ $this->assertNoTranslation($config_name, 'en'); + } + + /** diff --git a/patches/drupal_core_strip_debug_mode_whitespaces_10.1.x.patch b/patches/drupal_core_strip_debug_mode_whitespaces_10.3.x.patch similarity index 56% rename from patches/drupal_core_strip_debug_mode_whitespaces_10.1.x.patch rename to patches/drupal_core_strip_debug_mode_whitespaces_10.3.x.patch index e40768c2e..b03923714 100644 --- a/patches/drupal_core_strip_debug_mode_whitespaces_10.1.x.patch +++ b/patches/drupal_core_strip_debug_mode_whitespaces_10.3.x.patch @@ -1,11 +1,11 @@ diff --git a/core/themes/engines/twig/twig.engine b/core/themes/engines/twig/twig.engine -index cb32cbaa95..e73e9c498f 100644 +index 45a95cae78..de28e0f1de 100644 --- a/core/themes/engines/twig/twig.engine +++ b/core/themes/engines/twig/twig.engine -@@ -63,8 +63,8 @@ function twig_render_template($template_file, array $variables) { - throw $e; - } - if ($twig_service->isDebug()) { +@@ -50,8 +50,8 @@ function twig_render_template($template_file, array $variables) { + 'debug_suffix' => '', + ]; + - $output['debug_prefix'] .= "\n\n"; - $output['debug_prefix'] .= "\n"; + $output['debug_prefix'] .= ""; @@ -13,8 +13,8 @@ index cb32cbaa95..e73e9c498f 100644 // If there are theme suggestions, reverse the array so more specific // suggestions are shown first. if (!empty($variables['theme_hook_suggestions'])) { -@@ -106,17 +106,17 @@ function twig_render_template($template_file, array $variables) { - $prefix = ($template == $current_template) ? 'x' : '*'; +@@ -93,10 +93,10 @@ function twig_render_template($template_file, array $variables) { + $prefix = ($template == $current_template) ? '✅' : '▪️'; $suggestion = $prefix . ' ' . $template; } - $output['debug_info'] .= "\n"; @@ -26,12 +26,14 @@ index cb32cbaa95..e73e9c498f 100644 $output['debug_info'] .= "\n See https://api.drupal.org/api/drupal/core!lib!Drupal!Core!Render!theme.api.php/function/hook_theme_suggestions_alter"; $output['debug_info'] .= "\n " . Html::escape(implode("\n ", $invalid_suggestions)); $output['debug_info'] .= "\n-->"; - } +@@ -109,8 +109,8 @@ function twig_render_template($template_file, array $variables) { + $template_override_status_output = "💡 BEGIN CUSTOM TEMPLATE OUTPUT"; + $template_override_suffix_output = "END CUSTOM TEMPLATE OUTPUT"; } -- $output['debug_info'] .= "\n\n"; -- $output['debug_suffix'] .= "\n\n\n"; -+ $output['debug_info'] .= "\n"; -+ $output['debug_suffix'] .= "\n\n"; +- $output['debug_info'] .= "\n\n"; +- $output['debug_suffix'] .= "\n\n\n"; ++ $output['debug_info'] .= "\n"; ++ $output['debug_suffix'] .= "\n\n"; + // This output has already been rendered and is therefore considered safe. + return Markup::create(implode('', $output)); } - // This output has already been rendered and is therefore considered safe. - return Markup::create(implode('', $output));