Disclamer
First off, let me add a little disclamer for the reader :
This blog post is not meant to be a tutorial on upgrading from CKEditor 4 to 5 in Drupal !
This post does not reflect the easiness of CKEditor 5 in Drupal as this will be a straightforward transparent process once Drupal 10 releases.
The purpose here on the contrary is to test the process of upgrading as part of Drupal Dev Days in Ghent for the contributor team to gain feedback on real world scenarios, eventually discovering bugs to fix. So don't be scared while reading : this article should have a short term life expectancy as everything is going to be fixed eventually !
At the time of writing, I hope for a short post, but as things will go, spoiler alert, it will not !
Testing setup
My setup for this test will be this website itself, running locally on Docker. I have downloaded the production database and synced the configs and files, so everything is locally up-to-date and production-like as you are currently reading.
The point of having an exact copy is that I want to make sure I can smoothly compare the editing logic and behavior between the production website (this one you are reading) which stays at CKEditor 4 and my local environment.
At the time of test, the website runs on an up-to-date Drupal version 9.3.9 and PHP version 9.1.4.
Current modules that might interfere here are :
- CKEditor 4 from core 9.3.9
- Filter module from core
- NBSP Filter version 1.0.0: This one is a text format filter : it should not impact anything regarding ckeditor
- A custom filter format of mine, you will read about it later in the article
- Linkit version 6.0.0-beta3
- Layout Paragraph version 1.0.0 : I don’t know if this one is going to impact anything. A version 2.0.0-beta8 exists at the moment. Maybe I will update later on if needed
- Media Directories version 2.0.2 : it integrates with the CKEditor
- CKEditor Templates version 8.x-1.2
- CKEditor Templates User Interface version 8.x-1.4
- GeSHi filter for syntax highlighting version 8.x-2.0-beta1
- Font Awesome version 8.x-2.22
This article will be my comparison point since it actually uses everything. It is built using Paragraph Layout with various layouts. It uses Geshi snippets for both multiligns and monoligns. It integrates some third party tools like codepen demos, and of course media inserted with the media browser library. In short: it is complex enough to be interesting here!
While editing, it is worth mentioning the following custom buttons on my toolbar :
To continue on this migration path, I will now follow the steps written at https://drupal.org/test-cke-4-to-5
Content Format configuration
My most complex configuration is currently my full_html text format which only me as an admin can use. That is the one used for this article for instance. Because it is the most complex one I have, I will go with it for now on.
First, let's export the current configurations using :
1 2 drush cget filter.format.full_html > full_html-format-before.yml drush cget editor.editor. full_html > full_html-editor-before.yml
Those two files contain the full configurations of my full html text format and CKEditor 4 install such as available at : /admin/config/content/formats/manage/full_html
Content of the full_html-format-before.yml file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 uuid: 9559c2ef-a496-4168-8fea-5b2ad8bff0e4 langcode: fr status: true dependencies: config: - core.entity_view_mode.media.full module: - acino_custom - customfilter - editor - entity_embed - geshifilter - linkit - media - nbsp_filter - text_rotator_filter - typedjs_filter _core: default_config_hash: WNeK5FbcY8pXgEpbD_KgRzlF1-5PL3BJXwqaBctPTqw name: 'HTML complet' format: full_html weight: -10 filters: filter_align: id: filter_align provider: filter status: true weight: -45 settings: { } filter_caption: id: filter_caption provider: filter status: true weight: -44 settings: { } filter_htmlcorrector: id: filter_htmlcorrector provider: filter status: true weight: -42 settings: { } editor_file_reference: id: editor_file_reference provider: editor status: true weight: -41 settings: { } filter_html: id: filter_html provider: filter status: false weight: -39 settings: allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <s> <sup> <sub> <img src alt data-entity-type data-entity-uuid data-align data-caption> <table> <caption> <tbody> <thead> <tfoot> <th> <td> <tr> <hr> <p> <h1> <pre>' filter_html_help: true filter_html_nofollow: false filter_typed_js: id: filter_typed_js provider: typedjs_filter status: true weight: -46 settings: typeSpeed: '100' startDelay: '0' backSpeed: '10' backDelay: '1000' smartBackspace: '1' shuffle: '0' fadeOut: '0' fadeOutClass: typed-fade-out fadeOutDelay: '500' loop: '1' loopCount: '-1' showCursor: '1' cursorChar: '|' autoInsertCss: '1' attr: '' contentType: html media_embed: id: media_embed provider: media status: true weight: -40 settings: default_view_mode: full allowed_view_modes: { } allowed_media_types: { } filter_text_rotator: id: filter_text_rotator provider: text_rotator_filter status: true weight: -47 settings: animation: dissolve speed: '3000' filter_custom_elf: id: filter_custom_elf provider: acino_custom status: true weight: -50 settings: { } filter_url: id: filter_url provider: filter status: true weight: -48 settings: filter_url_length: 72 customfilter_filtre_de_mise_en_forme: id: customfilter_filtre_de_mise_en_forme provider: customfilter status: true weight: -49 settings: id: filtre_de_mise_en_forme filter_autop: id: filter_autop provider: filter status: false weight: -37 settings: { } filter_html_escape: id: filter_html_escape provider: filter status: false weight: -38 settings: { } filter_html_image_secure: id: filter_html_image_secure provider: filter status: false weight: -36 settings: { } linkit: id: linkit provider: linkit status: true weight: -43 settings: title: true entity_embed: id: entity_embed provider: entity_embed status: true weight: 100 settings: { } filter_geshifilter: id: filter_geshifilter provider: geshifilter status: true weight: 0 settings: general_tags: { } per_language_settings: { } nbsp_filter: id: nbsp_filter provider: nbsp_filter status: true weight: 0 settings: clean_all: '1' insert_before: '?!;:' insert_after: ¿¡
Content of the full_html-editor-before.yml file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 uuid: a79bd19d-08b4-44e7-891a-735fb773df56 langcode: fr status: true dependencies: config: - filter.format.full_html module: - ckeditor _core: default_config_hash: 967ijj7p6i7rwrYl7r08WQFeCY_c23YAh0h8u-w_CXM format: full_html editor: ckeditor settings: toolbar: rows: - - name: Formatting items: - Bold - Italic - Strike - Superscript - Subscript - '-' - RemoveFormat - name: Alignment items: - JustifyLeft - JustifyCenter - JustifyRight - JustifyBlock - name: Linking items: - DrupalLink - DrupalUnlink - name: Lists items: - BulletedList - NumberedList - name: Media items: - DrupalFontAwesome - media_directories - DrupalImage - name: 'Block Formatting' items: - Format - CodeSnippet - Table - HorizontalRule - Blockquote - name: Tools items: - Templates - ShowBlocks - Source plugins: drupallink: linkit_enabled: true linkit_profile: default language: language_list: un stylescombo: styles: '' templates: template_path: '' replace_content: 0 image_upload: status: true scheme: public directory: inline-images max_size: '' max_dimensions: width: null height: null
The long road of upgrading
Now is the time: let’s activate CKEditor 5 core experimental module, tested both from the UI and from Drush:
drush en ckeditor5
Aaaannnnd… the module activation did not crash! Good point for you Drupal I did not expect anything else.
However, talking with Wim Leers, I understood the magic did not happen now, but at the moment I switch the editor in my text format, so let's do it.
This highlights that you can currently use both CKEditor 4 and 5 at the same time on different formats. That could be good as an intermediate state to test things, but I would not recommend over time as this setup will not survive: ultimately CKEditor5 will be stable in core and replace CKEditor4. Maybe a contrib will let CKEditor 4 still exist in Drupal, but I am unaware of this at the moment of writing.
Let's do the update then. It happens at /admin/config/content/formats/manage/full_html
Issues with the upgrade
Well too bad... things are getting ugly from now on.
The very first error message I have is this one :
CKEditor 5 only works with HTML-based text formats. The "Add an icon to external and mailto links" (filter_custom_elf) filter implies this text format is not HTML anymore.
filter_custom_elf is a custom filter of mine that adds a specific class on external links and mailto links so that it is styled with a custom fontawesome icon on my posts.
Let’s analyze this: according to the error message, my custom filter is suppose to NOT apply on HTML but on markup itself. Hence, it is not CKEditor 5 compatible. In the code, the filter is indeed declared as follow :
1 2 3 4 5 6 * @Filter( * id = "filter_custom_elf", * title = @Translation("Add an icon to external and mailto links"), * description = @Translation("External and mailto links in content links have an icon."), * type = Drupal\filter\Plugin\FilterInterface::TYPE_MARKUP_LANGUAGE, * )
Therefore this filter is declared as working on MARKUP rather than HTML. My bad, let’s change that to :
type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE,
This makes much more sense!
That one is on me, my filter should never have been declared markup in the first place, but it also shows that as a developer the error message was simple enough that I found a solution in less than a couple of minutes.
Feedback 1: Positive feedback here on the error message that let a developper find a solution in a couple of minutes.
After emptying the cache, we are back in business to retry. But a new error occurs:
CKEditor 5 only works with HTML-based text formats. The "Convert URLs to links" (filter_url) filter implies this text format is not HTML anymore.
Hum.. This one is on Drupal as filter_url is a Drupal core filter. This filter operates on markup to catch links via a regexp and convert it to actual links via the HTML <a> tag. Therefore they should be unecessary for CKEditor 5. It is kind of my bad that they are enabled in CKEditor 4 on my website, but just because I can do so and not being warned, this use case MUST be supported.
Multiple filters are available in the filter module. In particular those two are concerned with this markup issue:
- FilterAutoP: converts lines breaks to HTML
- FilterUrl: converts the urls to links in HTML
In my opinion, those filters should be auto-removed from the configuration while CKEditor 5 is activated. But more than that, two things bothers me :
- "Convert URLs to link" filter is considered incompatible with CKEditor 5 and fails the migration, then why is this filter still available in the list below ?
- Having this filter enabled at the moment of conversion fails the conversion, then why can you activate it again after the conversion to CKEditor 5 and use it in conjunction with it then without any problem ?
Feedback 2:
- Conversion to CKEditor 5 should not fail if some core functionality where used in CKEditor 4.
- Conversion should disable silently the FilterUrl and FilterAutoP filters.
- It does not make sense that the "Convert URLs to link" is forbidden during the migration but can be activated later.
- Either remove those filters from the list (via State API ?) when CKEditor 5 is selected and/or forbid them to be selected afterward
Let me add a comment on the related core issues and move on :
- [#3273288] CKE5 and Entity Embed: CKEditor-specific error messages about text filters could be clearer
- [#3273312] Upgrading from CKEditor 4 for a text format that has FilterInterface::TYPE_MARKUP_LANGUAGE filters enabled
To continue on the migration test, I will now disable those filters in CKE4 prior to the upgrade.
Post-upgrade configuration differences
The change to CKEditor 5 is now possible and complete. A few issues where found and worked around. The upgrade now displays as success message wihch sound good.
Or is it ?
If you read carefully, I am actually informed that some filters did not have an upgrade path and therefore are now disabled. This is not a success to me as I am now lacking some functionality I had. This should definitely be at least a warning, eventually expanding on the further actions I can take : update the contrib module, enable manually the filter... Let's open a core issue for that :
Feedback 3: When the conversion is done, I should be displayed a warning if some functionalities had to be lost in the process, eventually and advice on what to do.
Let’s save this without any further due, and retrieve the post-upgrade config files.
1 2 drush cget filter.format.full_html > full_html-format-after.yml drush cget editor.editor. full_html > full_html-editor-after.yml
Content of the full_html-format-after.yml file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 uuid: 9559c2ef-a496-4168-8fea-5b2ad8bff0e4 langcode: fr status: true dependencies: config: - core.entity_view_mode.media.full module: - acino_custom - customfilter - editor - entity_embed - geshifilter - linkit - media - nbsp_filter - text_rotator_filter - typedjs_filter _core: default_config_hash: WNeK5FbcY8pXgEpbD_KgRzlF1-5PL3BJXwqaBctPTqw name: 'HTML complet' format: full_html weight: -10 filters: filter_align: id: filter_align provider: filter status: true weight: -45 settings: { } filter_caption: id: filter_caption provider: filter status: true weight: -44 settings: { } filter_htmlcorrector: id: filter_htmlcorrector provider: filter status: true weight: -42 settings: { } editor_file_reference: id: editor_file_reference provider: editor status: true weight: -41 settings: { } filter_html: id: filter_html provider: filter status: false weight: -39 settings: allowed_html: '<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id> <s> <sup> <sub> <img src alt data-entity-type data-entity-uuid data-align data-caption> <table> <caption> <tbody> <thead> <tfoot> <th> <td> <tr> <hr> <p> <h1> <pre> <drupal-entity data-entity-type data-entity-uuid data-entity-embed-display data-entity-embed-display-settings data-align data-caption data-embed-button data-langcode alt title>' filter_html_help: true filter_html_nofollow: false filter_typed_js: id: filter_typed_js provider: typedjs_filter status: true weight: -46 settings: typeSpeed: '100' startDelay: '0' backSpeed: '10' backDelay: '1000' smartBackspace: '1' shuffle: '0' fadeOut: '0' fadeOutClass: typed-fade-out fadeOutDelay: '500' loop: '1' loopCount: '-1' showCursor: '1' cursorChar: '|' autoInsertCss: '1' attr: '' contentType: html media_embed: id: media_embed provider: media status: true weight: -40 settings: default_view_mode: full allowed_view_modes: { } allowed_media_types: { } filter_text_rotator: id: filter_text_rotator provider: text_rotator_filter status: true weight: -47 settings: animation: dissolve speed: '3000' filter_custom_elf: id: filter_custom_elf provider: acino_custom status: true weight: -50 settings: { } filter_url: id: filter_url provider: filter status: true weight: -48 settings: filter_url_length: 72 customfilter_filtre_de_mise_en_forme: id: customfilter_filtre_de_mise_en_forme provider: customfilter status: true weight: -49 settings: id: filtre_de_mise_en_forme filter_autop: id: filter_autop provider: filter status: false weight: -37 settings: { } filter_html_escape: id: filter_html_escape provider: filter status: false weight: -38 settings: { } filter_html_image_secure: id: filter_html_image_secure provider: filter status: false weight: -36 settings: { } linkit: id: linkit provider: linkit status: true weight: -43 settings: title: true entity_embed: id: entity_embed provider: entity_embed status: true weight: 100 settings: { } filter_geshifilter: id: filter_geshifilter provider: geshifilter status: true weight: 0 settings: general_tags: { } per_language_settings: { } nbsp_filter: id: nbsp_filter provider: nbsp_filter status: true weight: 0 settings: clean_all: '1' insert_before: '?!;:' insert_after: ¿¡
Content of the full_html-editor-after.yml file
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 uuid: 1b8813d5-eaec-450c-bb4d-e00c013cef0f langcode: fr status: true dependencies: config: - filter.format.full_html module: - ckeditor5 format: full_html editor: ckeditor5 settings: toolbar: items: - bold - italic - strikethrough - superscript - subscript - removeFormat - '|' - 'alignment:left' - 'alignment:center' - alignment - '|' - link - '|' - bulletedList - numberedList - '|' - uploadImage - '|' - heading - insertTable - horizontalLine - blockQuote - '|' - sourceEditing plugins: ckeditor5_heading: enabled_headings: - heading2 - heading3 - heading4 - heading5 - heading6 ckeditor5_sourceEditing: allowed_tags: { } ckeditor5_imageResize: allow_resize: true image_upload: status: true scheme: public directory: inline-images max_size: '' max_dimensions: width: 0 height: 0
One more thing before I conclude this part : one upgrade message displays :
The drupallink plugin settings do not have a known upgrade path.
This seems weird at first sight as the drupallink filter comes from core and does not provide any configuration. For this reason, it does not provide an upgrade path as it is not supposed to need it. However there are no issues here, because this core filter is overridden by the LinkIt module which adds custom configs. Therefore it is up to the LinkIt module to provide that upgrade path: it is not at the moment.
The situation should therefore resolve by itself : either you don't use LinkIt module and you won't have the message from core only, or you use LinkIt and it should be updated soon with a working upgrade path.
Conclusion to this point
Our journey just started here as you will discover in part 2 that we are far from done: critical blocker issues are coming on the way but hey, let's not spoil now!
So far, we have noticed that even with a standard core install, you can run into issues. However, we also showed that they are some workaround at the moment, so the testing process can continue.
In a nutshell
- [#3273312] Issue regarding the markup filters
- [#3273325] Issue regarding functionality lost error
Add new comment