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 :
1: Font Awesome
2: Media Directories
3: Geshi Filter
4: CKEditor template
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 :
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
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
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 :
* @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.
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
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
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