Testing CKEditor 4 to 5 upgrade path

Table of contents

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 :

CKEditor 4 extra integrated buttons
Extra integrated CKEditor tools :
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 :

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

CKEditor 4 to 5 conversion
Switch to CKEditor 5.

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.

CKEditor custom filter demo
My custom filter in use.

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 :

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 ?

CKEditor success message at upgrade
Errors or warnings displayed as success ?

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

The issues to follow so far are :

Add new comment

Your name will be publicly displayed along with your comment.
Your email will be kept private and only used to notify you.
On internet, you can be who you want. Please be someone nice :)