ckeditor5 1.23.4 → 1.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -5
  3. data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +36 -17
  4. data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +2 -2
  5. data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +10 -0
  6. data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +23 -6
  7. data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +7 -20
  8. data/lib/ckeditor5/rails/cdn/concerns/inline_plugins_tags_builder.rb +35 -0
  9. data/lib/ckeditor5/rails/cdn/helpers.rb +62 -17
  10. data/lib/ckeditor5/rails/context/helpers.rb +9 -1
  11. data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +16 -0
  12. data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +24 -6
  13. data/lib/ckeditor5/rails/hooks/importmap.rb +1 -0
  14. data/lib/ckeditor5/rails/plugins/simple_upload_adapter.rb +1 -1
  15. data/lib/ckeditor5/rails/presets/concerns/plugin_methods.rb +27 -6
  16. data/lib/ckeditor5/rails/version.rb +1 -1
  17. data/spec/e2e/features/context_spec.rb +1 -1
  18. data/spec/e2e/features/editor_types_spec.rb +9 -0
  19. data/spec/e2e/features/lazy_assets_spec.rb +7 -0
  20. data/spec/lib/ckeditor5/rails/assets/assets_bundle_hml_serializer_spec.rb +2 -2
  21. data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +108 -3
  22. data/spec/lib/ckeditor5/rails/context/helpers_spec.rb +32 -0
  23. data/spec/lib/ckeditor5/rails/context/preset_builder_spec.rb +13 -3
  24. data/spec/lib/ckeditor5/rails/context/preset_serializer_spec.rb +8 -4
  25. data/spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb +4 -3
  26. data/spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb +1 -1
  27. data/spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb +5 -10
  28. data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +5 -1
  29. data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +18 -3
  30. metadata +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d99c9fa1e8761c6520144f71741d0159c4e9509f524082e59b38a489635a37a
4
- data.tar.gz: 951dacff990b009ba778901a4603a30315e5d95215c6b03f6837cba89a08e2e5
3
+ metadata.gz: 0ee1985934d17921821b5d298275a64b71154309e0492f8da8aaae33c7dab2bd
4
+ data.tar.gz: 880ec9989cd23462c889b35e68c7b49bdc9842c826b19326095d6380550220b8
5
5
  SHA512:
6
- metadata.gz: cbc115e51be9f39716846ee4037a37d67deb81d7894b1eb460b7b4775e10a240a949c176033512fa44fd71bc4ec8f5f15feadc36bddfdb15ca0ab06016f7c345
7
- data.tar.gz: 735541e4c765dfb8fe86537ee0261066be0fe18f9fec3b706da3c6e3d23b188cff772671e892fca55b9f3f1c4323ba9f5f2703bac35da54ac97aded3bbbb2113
6
+ metadata.gz: d7b01be500143a77b4457b50115daea67bc64351cb58b37e7f8cfe50f2c28cd0b8007633b85697ca90878a09b0c3a37df00fcae4d3927aff7bad57e27d21ca92
7
+ data.tar.gz: 202ed9359e0b7b57d260f391c46bcd6a985eda11fb91df409bc345470c879a587487f8f7a23382d440b5963a797411dc6c58a60ddfe43e2ab24dc70b642a7fb0
data/README.md CHANGED
@@ -177,6 +177,7 @@ For extending CKEditor's functionality, refer to the [plugins directory](https:/
177
177
  - [Integration with Turbolinks 🚀](#integration-with-turbolinks-)
178
178
  - [Custom Styling 🎨](#custom-styling-)
179
179
  - [Custom plugins 🧩](#custom-plugins-)
180
+ - [Content Security Policy (CSP) 🛡️](#content-security-policy-csp-️)
180
181
  - [Events fired by the editor 🔊](#events-fired-by-the-editor-)
181
182
  - [`editor-ready` event](#editor-ready-event)
182
183
  - [`editor-error` event](#editor-error-event)
@@ -847,9 +848,9 @@ CKEditor5::Rails.configure do
847
848
  # ... other configuration
848
849
 
849
850
  inline_plugin :MyCustomPlugin, <<~JS
850
- import { Plugin } from 'ckeditor5';
851
+ const { Plugin } = await import( 'ckeditor5' );
851
852
 
852
- export default class MyCustomPlugin extends Plugin {
853
+ return class extends Plugin {
853
854
  static get pluginName() {
854
855
  return 'MyCustomPlugin';
855
856
  }
@@ -1845,9 +1846,9 @@ CKEditor5::Rails.configure do
1845
1846
 
1846
1847
  # 4. Or even define it inline:
1847
1848
  # inline_plugin :MyCustomPlugin, <<~JS
1848
- # import { Plugin } from 'ckeditor5';
1849
+ # const { Plugin } = await import( 'ckeditor5' );
1849
1850
  #
1850
- # export default class MyCustomPlugin extends Plugin {
1851
+ # return class MyCustomPlugin extends Plugin {
1851
1852
  # // ...
1852
1853
  # }
1853
1854
  # JS
@@ -1866,7 +1867,7 @@ end
1866
1867
  // app/javascript/custom_plugins/highlight.js
1867
1868
  import { Plugin, Command, ButtonView } from 'ckeditor5';
1868
1869
 
1869
- export default class MyCustomPlugin extends Plugin {
1870
+ return class MyCustomPlugin extends Plugin {
1870
1871
  static get pluginName() {
1871
1872
  return 'MyCustomPlugin';
1872
1873
  }
@@ -1973,6 +1974,31 @@ class HighlightCommand extends Command {
1973
1974
 
1974
1975
  </details>
1975
1976
 
1977
+ ### Content Security Policy (CSP) 🛡️
1978
+
1979
+ If you're using a Content Security Policy (CSP) in your Rails application, you may need to adjust it to allow CKEditor 5 to work correctly. CKEditor 5 uses inline scripts and styles to render the editor, so you need to allow them in your CSP configuration. The example below shows how to configure the CSP to allow CKEditor 5 to work correctly:
1980
+
1981
+ ```rb
1982
+ # config/initializers/content_security_policy.rb
1983
+
1984
+ Rails.application.configure do
1985
+ config.content_security_policy do |policy|
1986
+ policy.default_src :self, :https
1987
+ policy.font_src :self, :https, :data
1988
+ policy.img_src :self, :https, :data
1989
+ policy.object_src :none
1990
+ policy.script_src "'strict-dynamic'"
1991
+ policy.style_src :self, :https
1992
+ policy.style_src_elem :self, :https, :unsafe_inline
1993
+ policy.style_src_attr :self, :https, :unsafe_inline
1994
+ policy.base_uri :self
1995
+ end
1996
+
1997
+ config.content_security_policy_nonce_generator = ->(_request) { SecureRandom.base64(16) }
1998
+ config.content_security_policy_nonce_directives = %w[script-src style-src]
1999
+ end
2000
+ ```
2001
+
1976
2002
  ## Events fired by the editor 🔊
1977
2003
 
1978
2004
  CKEditor 5 provides a set of events that you can listen to in order to react to changes in the editor. You can listen to these events using the `addEventListener` method or by defining event handlers directly in the view.
@@ -19,13 +19,24 @@ module CKEditor5::Rails::Assets
19
19
  @lazy = lazy
20
20
  end
21
21
 
22
- def to_html
22
+ def to_html(nonce: nil)
23
23
  tags = [
24
- WebComponentBundle.instance.to_html
24
+ WebComponentBundle.instance.to_html(nonce: nonce)
25
25
  ]
26
26
 
27
- tags.prepend(preload_tags, styles_tags, window_scripts_tags) unless lazy
28
- tags.prepend(AssetsImportMap.new(bundle).to_html) if importmap
27
+ unless lazy
28
+ tags.prepend(
29
+ preload_tags(nonce: nonce),
30
+ styles_tags(nonce: nonce),
31
+ window_scripts_tags(nonce: nonce)
32
+ )
33
+ end
34
+
35
+ if importmap
36
+ tags.prepend(
37
+ AssetsImportMap.new(bundle).to_html(nonce: nonce)
38
+ )
39
+ end
29
40
 
30
41
  safe_join(tags)
31
42
  end
@@ -40,34 +51,42 @@ module CKEditor5::Rails::Assets
40
51
 
41
52
  private
42
53
 
43
- def window_scripts_tags
44
- @window_scripts_tags ||= safe_join(bundle.scripts.filter_map do |script|
45
- tag.script(src: script.url, nonce: true, crossorigin: 'anonymous') if script.window?
46
- end)
54
+ def window_scripts_tags(nonce: nil)
55
+ scripts = bundle.scripts.filter_map do |script|
56
+ tag.script(src: script.url, nonce: nonce, crossorigin: 'anonymous') if script.window?
57
+ end
58
+
59
+ safe_join(scripts)
47
60
  end
48
61
 
49
- def styles_tags
50
- @styles_tags ||= safe_join(bundle.stylesheets.map do |url|
51
- tag.link(href: url, rel: 'stylesheet', crossorigin: 'anonymous')
52
- end)
62
+ def styles_tags(nonce: nil)
63
+ styles = bundle.stylesheets.map do |url|
64
+ tag.link(href: url, nonce: nonce, rel: 'stylesheet', crossorigin: 'anonymous')
65
+ end
66
+
67
+ safe_join(styles)
53
68
  end
54
69
 
55
- def preload_tags
56
- @preload_tags ||= safe_join(bundle.preloads.map do |preload|
70
+ def preload_tags(nonce: nil)
71
+ preloads = bundle.preloads.map do |preload|
57
72
  if preload.is_a?(Hash) && preload[:as] && preload[:href]
58
73
  tag.link(
59
74
  **preload,
75
+ nonce: nonce,
60
76
  crossorigin: 'anonymous'
61
77
  )
62
78
  else
63
79
  tag.link(
64
80
  href: preload,
65
81
  rel: 'preload',
82
+ nonce: nonce,
66
83
  as: self.class.url_resource_preload_type(preload),
67
84
  crossorigin: 'anonymous'
68
85
  )
69
86
  end
70
- end)
87
+ end
88
+
89
+ safe_join(preloads)
71
90
  end
72
91
  end
73
92
 
@@ -90,11 +109,11 @@ module CKEditor5::Rails::Assets
90
109
  { imports: import_map }.to_json
91
110
  end
92
111
 
93
- def to_html
112
+ def to_html(nonce: nil)
94
113
  tag.script(
95
114
  to_json.html_safe,
96
115
  type: 'importmap',
97
- nonce: true
116
+ nonce: nonce
98
117
  )
99
118
  end
100
119
 
@@ -21,8 +21,8 @@ module CKEditor5::Rails::Assets
21
21
  @source ||= compress_source(raw_source)
22
22
  end
23
23
 
24
- def to_html
25
- @to_html ||= tag.script(source, type: 'module', nonce: true)
24
+ def to_html(nonce: nil)
25
+ tag.script(source, type: 'module', nonce: nonce)
26
26
  end
27
27
 
28
28
  private
@@ -66,10 +66,20 @@ class CKEditorContextComponent extends HTMLElement {
66
66
  this.instance = null;
67
67
  }
68
68
 
69
+ // Broadcast context initialization event
70
+ window.dispatchEvent(
71
+ new CustomEvent('ckeditor:context:attach:before', { detail: { element: this } })
72
+ );
73
+
69
74
  const { Context, ContextWatchdog } = await import('ckeditor5');
70
75
  const plugins = await this.#getPlugins();
71
76
  const config = this.#getConfig();
72
77
 
78
+ // Broadcast context mounting event with configuration
79
+ window.dispatchEvent(
80
+ new CustomEvent('ckeditor:context:attach', { detail: { config, element: this } })
81
+ );
82
+
73
83
  this.instance = new ContextWatchdog(Context, {
74
84
  crashNumberLimit: 10
75
85
  });
@@ -245,12 +245,6 @@ class CKEditorComponent extends HTMLElement {
245
245
  this.#ensureWindowScriptsInjected(),
246
246
  ]);
247
247
 
248
- const Editor = await this.#getEditorConstructor();
249
- const [plugins, translations] = await Promise.all([
250
- this.#getPlugins(),
251
- this.#getTranslations()
252
- ]);
253
-
254
248
  // Depending on the type of the editor the content supplied on the first
255
249
  // argument is different. For ClassicEditor it's a element or string, for MultiRootEditor
256
250
  // it's an object with editables, for DecoupledEditor it's string.
@@ -262,6 +256,24 @@ class CKEditorComponent extends HTMLElement {
262
256
  content = editablesOrContent.main;
263
257
  }
264
258
 
259
+ // Broadcast editor initialization event. It's good time to load add inline window plugins.
260
+ const beforeInitEventDetails = {
261
+ ...content instanceof HTMLElement && { element: content },
262
+ ...content instanceof String && { data: content },
263
+ ...content instanceof Object && { editables: content }
264
+ };
265
+
266
+ window.dispatchEvent(
267
+ new CustomEvent('ckeditor:attach:before', { detail: beforeInitEventDetails})
268
+ );
269
+
270
+ // Start fetching constructor.
271
+ const Editor = await this.#getEditorConstructor();
272
+ const [plugins, translations] = await Promise.all([
273
+ this.#getPlugins(),
274
+ this.#getTranslations()
275
+ ]);
276
+
265
277
  const config = {
266
278
  ...this.#getConfig(),
267
279
  ...translations.length && {
@@ -270,6 +282,11 @@ class CKEditorComponent extends HTMLElement {
270
282
  plugins,
271
283
  };
272
284
 
285
+ // Broadcast editor mounting event. It's good time to map configuration.
286
+ window.dispatchEvent(
287
+ new CustomEvent('ckeditor:attach', { detail: { config, ...beforeInitEventDetails } })
288
+ );
289
+
273
290
  console.warn('Initializing CKEditor with:', { config, watchdog: this.hasWatchdog(), context: this.#context });
274
291
 
275
292
  // Initialize watchdog if needed
@@ -34,16 +34,6 @@ function execIfDOMReady(callback) {
34
34
  * @throws {Error} When plugin loading fails
35
35
  */
36
36
  function loadAsyncImports(imports = []) {
37
- const loadInlinePlugin = async ({ name, code }) => {
38
- const module = await import(`data:text/javascript,${encodeURIComponent(code)}`);
39
-
40
- if (!module.default) {
41
- throw new Error(`Inline plugin "${name}" must export a default class/function!`);
42
- }
43
-
44
- return module.default;
45
- };
46
-
47
37
  const loadExternalPlugin = async ({ url, import_name, import_as, window_name, stylesheets }) => {
48
38
  if (stylesheets?.length) {
49
39
  await loadAsyncCSS(stylesheets);
@@ -58,6 +48,12 @@ function loadAsyncImports(imports = []) {
58
48
  await injectScript(url);
59
49
  }
60
50
 
51
+ if (!isScriptPresent()) {
52
+ window.dispatchEvent(
53
+ new CustomEvent(`ckeditor:request-cjs-plugin:${window_name}`)
54
+ );
55
+ }
56
+
61
57
  if (!isScriptPresent()) {
62
58
  throw new Error(
63
59
  `Plugin window['${window_name}'] not found in global scope. ` +
@@ -82,16 +78,7 @@ function loadAsyncImports(imports = []) {
82
78
  return imported;
83
79
  };
84
80
 
85
- return Promise.all(imports.map(item => {
86
- switch(item.type) {
87
- case 'inline':
88
- return loadInlinePlugin(item);
89
-
90
- case 'external':
91
- default:
92
- return loadExternalPlugin(item);
93
- }
94
- }));
81
+ return Promise.all(imports.map(loadExternalPlugin));
95
82
  }
96
83
 
97
84
  /**
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CKEditor5::Rails
4
+ module Cdn::Concerns
5
+ module InlinePluginsTagsBuilder
6
+ # Includes JavaScript code for inline plugins that use CommonJS module format.
7
+ # This helper generates script tags that initialize plugins before CKEditor loads.
8
+ #
9
+ # @param preset [PresetBuilder, nil] Optional preset to filter plugins from.
10
+ # If nil, includes plugins from all registered presets.
11
+ # @return [ActiveSupport::SafeBuffer] HTML script tags with plugin initializers
12
+ # @example Including CJS plugins for specific preset
13
+ # <%= ckeditor5_inline_plugins_tags(@my_preset) %>
14
+ # @example Including CJS plugins from all presets
15
+ # <%= ckeditor5_inline_plugins_tags %>
16
+ def ckeditor5_inline_plugins_tags(preset = nil)
17
+ plugins = if preset
18
+ preset.plugins.items
19
+ else
20
+ Engine.presets.to_h.values.flat_map { |p| p.plugins.items }
21
+ end
22
+
23
+ initializers = plugins.filter_map do |plugin|
24
+ next unless plugin.is_a?(Editor::PropsInlinePlugin)
25
+
26
+ Editor::InlinePluginWindowInitializer.new(plugin).to_html(
27
+ nonce: content_security_policy_nonce
28
+ )
29
+ end
30
+
31
+ safe_join(initializers)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -9,13 +9,17 @@ require_relative '../assets/assets_bundle_html_serializer'
9
9
  require_relative 'url_generator'
10
10
  require_relative 'ckeditor_bundle'
11
11
  require_relative 'ckbox_bundle'
12
+
12
13
  require_relative 'concerns/bundle_builder'
14
+ require_relative 'concerns/inline_plugins_tags_builder'
13
15
 
14
16
  module CKEditor5::Rails
15
17
  module Cdn::Helpers
16
- include Cdn::Concerns::BundleBuilder
17
18
  include ActionView::Helpers::TagHelper
18
19
 
20
+ include Cdn::Concerns::BundleBuilder
21
+ include Cdn::Concerns::InlinePluginsTagsBuilder
22
+
19
23
  class ImportmapAlreadyRenderedError < ArgumentError; end
20
24
 
21
25
  # The `ckeditor5_assets` helper includes CKEditor 5 assets in your application.
@@ -80,7 +84,7 @@ module CKEditor5::Rails
80
84
  preset: mapped_preset
81
85
  }
82
86
 
83
- build_assets_html_tags(bundle, importmap: importmap, lazy: lazy)
87
+ build_assets_html_tags(bundle, mapped_preset, importmap: importmap, lazy: lazy)
84
88
  end
85
89
 
86
90
  # Helper for dynamically loading CKEditor assets when working with Turbo/Stimulus.
@@ -97,18 +101,22 @@ module CKEditor5::Rails
97
101
  def ckeditor5_lazy_javascript_tags
98
102
  ensure_importmap_not_rendered!
99
103
 
104
+ tags = [
105
+ Assets::WebComponentBundle.instance.to_html(nonce: content_security_policy_nonce),
106
+ ckeditor5_inline_plugins_tags
107
+ ]
108
+
100
109
  if importmap_available?
101
110
  @__ckeditor_context = {
102
111
  bundle: combined_bundle
103
112
  }
104
-
105
- return Assets::WebComponentBundle.instance.to_html
113
+ else
114
+ tags.prepend(
115
+ Assets::AssetsImportMap.new(combined_bundle).to_html(nonce: content_security_policy_nonce)
116
+ )
106
117
  end
107
118
 
108
- safe_join([
109
- Assets::AssetsImportMap.new(combined_bundle).to_html,
110
- Assets::WebComponentBundle.instance.to_html
111
- ])
119
+ safe_join(tags)
112
120
  end
113
121
 
114
122
  # Dynamically generates helper methods for each third-party CDN provider.
@@ -131,6 +139,10 @@ module CKEditor5::Rails
131
139
 
132
140
  private
133
141
 
142
+ # Combines all preset bundles into a single bundle for lazy loading.
143
+ # This is useful when dynamically loading editors with unknown preset configurations.
144
+ #
145
+ # @return [AssetsBundle] Combined bundle containing all preset assets
134
146
  def combined_bundle
135
147
  acc = Assets::AssetsBundle.new(scripts: [], stylesheets: [])
136
148
 
@@ -141,6 +153,14 @@ module CKEditor5::Rails
141
153
  acc
142
154
  end
143
155
 
156
+ # Merges user-provided configuration with the editor preset.
157
+ # Sets default language if not specified and validates required parameters.
158
+ #
159
+ # @param preset [Symbol, PresetBuilder] Base preset to merge with
160
+ # @param language [Symbol, nil] UI language code
161
+ # @param kwargs [Hash] Additional configuration options
162
+ # @return [PresetBuilder] New preset instance with merged configuration
163
+ # @raise [ArgumentError] If required parameters are missing
144
164
  def merge_with_editor_preset(preset, language: nil, **kwargs)
145
165
  found_preset = Engine.find_preset!(preset)
146
166
  new_preset = found_preset.clone.merge_with_hash!(**kwargs)
@@ -152,21 +172,21 @@ module CKEditor5::Rails
152
172
  new_preset.language(I18n.locale)
153
173
  end
154
174
 
155
- %i[version type].each do |key|
156
- next if new_preset.public_send(key).present?
157
-
158
- raise ArgumentError,
159
- "Poor thing. You forgot to define #{key}. Make sure you passed `#{key}:` parameter to " \
160
- "`ckeditor5_assets` or defined default one in your `#{preset}` preset!"
161
- end
175
+ validate_required_preset_params!(new_preset, preset)
162
176
 
163
177
  new_preset
164
178
  end
165
179
 
180
+ # Checks if importmap support is available in the current context.
181
+ #
182
+ # @return [Boolean] true if importmap is supported
166
183
  def importmap_available?
167
184
  respond_to?(:importmap_rendered?)
168
185
  end
169
186
 
187
+ # Ensures that importmap hasn't been rendered yet to prevent conflicts.
188
+ #
189
+ # @raise [ImportmapAlreadyRenderedError] If importmap was already rendered
170
190
  def ensure_importmap_not_rendered!
171
191
  return unless importmap_available? && importmap_rendered?
172
192
 
@@ -175,14 +195,24 @@ module CKEditor5::Rails
175
195
  'Please move ckeditor5_assets helper before javascript_importmap_tags in your layout.'
176
196
  end
177
197
 
178
- def build_assets_html_tags(bundle, importmap:, lazy: nil)
198
+ # Builds HTML tags for CKEditor assets with proper configuration.
199
+ #
200
+ # @param bundle [AssetsBundle] Bundle containing assets to include
201
+ # @param preset [PresetBuilder] Preset configuration
202
+ # @param importmap [Boolean] Whether to use importmap for dependencies
203
+ # @param lazy [Boolean] Whether to enable lazy loading
204
+ # @return [String, nil] HTML tags string or nil if using importmap
205
+ def build_assets_html_tags(bundle, preset, importmap:, lazy: nil)
179
206
  serializer = Assets::AssetsBundleHtmlSerializer.new(
180
207
  bundle,
181
208
  importmap: importmap && !importmap_available?,
182
209
  lazy: lazy
183
210
  )
184
211
 
185
- html = serializer.to_html
212
+ html = safe_join([
213
+ serializer.to_html(nonce: content_security_policy_nonce),
214
+ ckeditor5_inline_plugins_tags(preset)
215
+ ])
186
216
 
187
217
  if importmap_available?
188
218
  @__ckeditor_context[:html_tags] = html
@@ -191,5 +221,20 @@ module CKEditor5::Rails
191
221
  html
192
222
  end
193
223
  end
224
+
225
+ # Validates that required parameters are present in the preset configuration.
226
+ #
227
+ # @param preset [PresetBuilder] Preset to validate
228
+ # @param preset_name [Symbol] Name of the preset for error messages
229
+ # @raise [ArgumentError] If version or type is missing
230
+ def validate_required_preset_params!(preset, preset_name)
231
+ %i[version type].each do |key|
232
+ next if preset.public_send(key).present?
233
+
234
+ raise ArgumentError,
235
+ "Poor thing. You forgot to define #{key}. Make sure you passed `#{key}:` parameter to " \
236
+ "`ckeditor5_assets` or defined default one in your `#{preset_name}` preset!"
237
+ end
238
+ end
194
239
  end
195
240
  end
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../cdn/concerns/inline_plugins_tags_builder'
4
+
3
5
  require_relative 'preset_builder'
4
6
  require_relative 'preset_serializer'
5
7
 
6
8
  module CKEditor5::Rails::Context
7
9
  module Helpers
10
+ include CKEditor5::Rails::Cdn::Concerns::InlinePluginsTagsBuilder
11
+
8
12
  # Creates a CKEditor context component that can be shared between multiple editors.
9
13
  # This allows you to define common plugins that will be available to all editors
10
14
  # within the context.
@@ -25,7 +29,11 @@ module CKEditor5::Rails::Context
25
29
  preset ||= PresetBuilder.new
26
30
  context_props = PresetSerializer.new(preset)
27
31
 
28
- tag.public_send(:'ckeditor-context-component', **context_props.to_attributes, &block)
32
+ tags = []
33
+ tags << ckeditor5_inline_plugins_tags(preset)
34
+ tags << tag.public_send(:'ckeditor-context-component', **context_props.to_attributes, &block)
35
+
36
+ safe_join(tags)
29
37
  end
30
38
 
31
39
  # Creates a new preset builder object for use with ckeditor5_context.
@@ -132,12 +132,24 @@ module CKEditor5::Rails
132
132
 
133
133
  private
134
134
 
135
+ # Validates that initial_data and block are not provided simultaneously
136
+ #
137
+ # @param initial_data [String] Initial content for the editor
138
+ # @param block [Proc] Block containing nested components
139
+ # @raise [ArgumentError] If both initial_data and block are provided
135
140
  def validate_editor_input!(initial_data, block)
136
141
  return unless initial_data && block
137
142
 
138
143
  raise ArgumentError, 'Cannot pass initial data and block at the same time.'
139
144
  end
140
145
 
146
+ # Builds the complete editor configuration by merging preset, custom and extra configs
147
+ #
148
+ # @param preset [PresetBuilder] The preset configuration object
149
+ # @param config [Hash] Custom configuration that overrides preset config
150
+ # @param extra_config [Hash] Additional configuration to merge
151
+ # @param initial_data [String] Initial content for the editor
152
+ # @return [Hash] The merged configuration hash
141
153
  def build_editor_config(preset, config, extra_config, initial_data)
142
154
  editor_config = config || preset.config
143
155
  editor_config = editor_config.deep_merge(extra_config)
@@ -151,6 +163,10 @@ module CKEditor5::Rails
151
163
  editor_config
152
164
  end
153
165
 
166
+ # Retrieves or creates a context for the editor initialization
167
+ #
168
+ # @param preset [Symbol, PresetBuilder] The preset name or object
169
+ # @return [Hash] Context hash containing bundle and preset information
154
170
  def ckeditor5_context_or_fallback(preset)
155
171
  return @__ckeditor_context if @__ckeditor_context.present?
156
172
 
@@ -15,9 +15,8 @@ module CKEditor5::Rails::Editor
15
15
 
16
16
  def to_h
17
17
  {
18
- type: :inline,
19
- name: name,
20
- code: code
18
+ type: :external,
19
+ window_name: name
21
20
  }
22
21
  end
23
22
 
@@ -25,11 +24,30 @@ module CKEditor5::Rails::Editor
25
24
 
26
25
  def validate_code!
27
26
  raise ArgumentError, 'Code must be a String' unless code.is_a?(String)
27
+ end
28
+ end
29
+
30
+ class InlinePluginWindowInitializer
31
+ include ActionView::Helpers::TagHelper
32
+
33
+ def initialize(plugin)
34
+ @plugin = plugin
35
+ end
28
36
 
29
- return if code.include?('export default')
37
+ def to_html(nonce: nil)
38
+ code = wrap_with_handlers(@plugin.code)
39
+
40
+ tag.script(code.html_safe, nonce: nonce)
41
+ end
42
+
43
+ private
30
44
 
31
- raise ArgumentError,
32
- 'Code must include `export default` that exports plugin definition!'
45
+ def wrap_with_handlers(code)
46
+ <<~JS
47
+ window.addEventListener('ckeditor:request-cjs-plugin:#{@plugin.name}', () => {
48
+ window['#{@plugin.name}'] = #{code.html_safe};
49
+ });
50
+ JS
33
51
  end
34
52
  end
35
53
  end
@@ -47,6 +47,7 @@ module CKEditor5::Rails::Hooks
47
47
  def merge_import_maps_json(a_json, b_json)
48
48
  a = JSON.parse(a_json)
49
49
  b = JSON.parse(b_json)
50
+
50
51
  a['imports'].merge!(b['imports'])
51
52
  a.to_json
52
53
  rescue JSON::ParserError => e
@@ -5,7 +5,7 @@ module CKEditor5::Rails::Plugins
5
5
  PLUGIN_CODE = <<~JS
6
6
  import { Plugin, FileRepository } from 'ckeditor5';
7
7
 
8
- export default class SimpleUploadAdapter extends Plugin {
8
+ return class SimpleUploadAdapter extends Plugin {
9
9
  static get requires() {
10
10
  return [FileRepository];
11
11
  }
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_support'
4
+ require 'terser'
4
5
 
5
6
  module CKEditor5::Rails
6
7
  module Presets
@@ -8,7 +9,9 @@ module CKEditor5::Rails
8
9
  module PluginMethods
9
10
  extend ActiveSupport::Concern
10
11
 
11
- class DisallowedInlinePlugin < ArgumentError; end
12
+ class DisallowedInlinePluginError < ArgumentError; end
13
+ class MissingInlinePluginError < StandardError; end
14
+ class UnsupportedESModuleError < StandardError; end
12
15
 
13
16
  included do
14
17
  attr_reader :disallow_inline_plugins
@@ -34,9 +37,9 @@ module CKEditor5::Rails
34
37
  # @param code [String] JavaScript code defining the plugin
35
38
  # @example Define custom highlight plugin
36
39
  # inline_plugin :MyCustomPlugin, <<~JS
37
- # import { Plugin } from 'ckeditor5';
40
+ # const { Plugin } = await import( 'ckeditor5' );
38
41
  #
39
- # export default class MyCustomPlugin extends Plugin {
42
+ # return class MyCustomPlugin extends Plugin {
40
43
  # static get pluginName() {
41
44
  # return 'MyCustomPlugin';
42
45
  # }
@@ -47,7 +50,21 @@ module CKEditor5::Rails
47
50
  # }
48
51
  # JS
49
52
  def inline_plugin(name, code)
50
- register_plugin(Editor::PropsInlinePlugin.new(name, code))
53
+ if code.match?(/export default/)
54
+ raise UnsupportedESModuleError,
55
+ 'Inline plugins must not use ES module syntax!' \
56
+ 'Use async async imports instead!'
57
+ end
58
+
59
+ unless code.match?(/return class(\s+\w+)?\s+extends\s+Plugin/)
60
+ raise MissingInlinePluginError,
61
+ 'Plugin code must return a class that extends Plugin!'
62
+ end
63
+
64
+ wrapped_code = "(async () => { #{code} })();"
65
+ minified_code = Terser.new(compress: false, mangle: true).compile(wrapped_code)
66
+
67
+ register_plugin(Editor::PropsInlinePlugin.new(name, minified_code))
51
68
  end
52
69
 
53
70
  # Register a single plugin by name
@@ -89,8 +106,11 @@ module CKEditor5::Rails
89
106
 
90
107
  private
91
108
 
109
+ # Check if the plugin looks like an inline plugin
110
+ # @param plugin [Editor::PropsBasePlugin] Plugin instance
111
+ # @return [Boolean] True if the plugin is an inline plugin
92
112
  def looks_like_inline_plugin?(plugin)
93
- plugin.to_h[:type] == :inline
113
+ plugin.respond_to?(:code) && plugin.code.present?
94
114
  end
95
115
 
96
116
  # Register a plugin in the editor configuration.
@@ -103,9 +123,10 @@ module CKEditor5::Rails
103
123
  # @return [Editor::PropsBasePlugin] The registered plugin
104
124
  def register_plugin(plugin_obj)
105
125
  if disallow_inline_plugins && looks_like_inline_plugin?(plugin_obj)
106
- raise DisallowedInlinePlugin, 'Inline plugins are not allowed here.'
126
+ raise DisallowedInlinePluginError, 'Inline plugins are not allowed here.'
107
127
  end
108
128
 
129
+ config[:plugins] ||= []
109
130
  config[:plugins] << plugin_obj
110
131
  plugin_obj
111
132
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module CKEditor5
4
4
  module Rails
5
- VERSION = '1.23.4'
5
+ VERSION = '1.24.0'
6
6
 
7
7
  DEFAULT_CKEDITOR_VERSION = '44.1.0'
8
8
  end
@@ -11,7 +11,7 @@ RSpec.describe 'CKEditor5 Context Integration', type: :feature, js: true do
11
11
 
12
12
  it 'initializes the magic context plugin' do
13
13
  eventually do
14
- plugin_exists = page.evaluate_script('window.MagicContextPlugin !== undefined')
14
+ plugin_exists = page.evaluate_script('window.__magicPluginInitialized !== undefined')
15
15
  expect(plugin_exists).to be true
16
16
  end
17
17
  end
@@ -87,6 +87,15 @@ RSpec.describe 'CKEditor5 Types Integration', type: :feature, js: true do
87
87
  describe 'Classic Editor' do
88
88
  it_behaves_like 'an editor', 'classic'
89
89
  it_behaves_like 'an editor that fires change event with main payload', 'classic'
90
+
91
+ it 'initializes the inline plugin' do
92
+ visit 'classic'
93
+
94
+ eventually do
95
+ plugin_exists = page.evaluate_script('window.__customPlugin !== undefined')
96
+ expect(plugin_exists).to be true
97
+ end
98
+ end
90
99
  end
91
100
 
92
101
  describe 'Decoupled Editor' do
@@ -38,6 +38,13 @@ RSpec.describe 'Lazy Assets', type: :feature do
38
38
  expect(editor).to have_text('Test content')
39
39
  end
40
40
 
41
+ it 'initializes the inline plugin' do
42
+ eventually do
43
+ plugin_exists = page.evaluate_script('window.__customPlugin !== undefined')
44
+ expect(plugin_exists).to be true
45
+ end
46
+ end
47
+
41
48
  it 'supports multiple editor instances' do
42
49
  visit 'classic_lazy_assets?multiple=true'
43
50
 
@@ -32,7 +32,7 @@ RSpec.describe CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer do
32
32
  end
33
33
 
34
34
  describe '#to_html' do
35
- subject(:html) { serializer.to_html }
35
+ subject(:html) { serializer.to_html(nonce: 'true') }
36
36
 
37
37
  it 'includes window script tags' do
38
38
  expect(html).to have_tag('script', with: {
@@ -240,7 +240,7 @@ RSpec.describe CKEditor5::Rails::Assets::AssetsImportMap do
240
240
 
241
241
  describe '#to_html' do
242
242
  it 'generates script tag with import map' do
243
- html = import_map.to_html
243
+ html = import_map.to_html(nonce: 'true')
244
244
  expect(html).to have_tag('script', with: { type: 'importmap', nonce: 'true' }) do
245
245
  with_text('{"imports":{"@ckeditor/module":"https://cdn.com/script2.js"}}')
246
246
  end
@@ -10,6 +10,10 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
10
10
  def importmap_rendered?
11
11
  false
12
12
  end
13
+
14
+ def content_security_policy_nonce
15
+ 'test-nonce'
16
+ end
13
17
  end
14
18
  end
15
19
 
@@ -29,7 +33,7 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
29
33
  helper.instance_variable_get(:@__ckeditor_context)
30
34
  end
31
35
 
32
- let(:bundle_html) { '<script src="test.js"></script>' }
36
+ let(:bundle_html) { '<script src="test.js"></script>'.html_safe }
33
37
  let(:serializer) do
34
38
  instance_double(CKEditor5::Rails::Assets::AssetsBundleHtmlSerializer, to_html: bundle_html)
35
39
  end
@@ -303,6 +307,14 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
303
307
  .and_return(CKEditor5::Rails::Assets::AssetsBundle.new(
304
308
  scripts: ['test2.js']
305
309
  ))
310
+
311
+ allow(test_preset1).to receive(:plugins).and_return(
312
+ instance_double('PluginsBuilder', items: [])
313
+ )
314
+
315
+ allow(test_preset2).to receive(:plugins).and_return(
316
+ instance_double('PluginsBuilder', items: [])
317
+ )
306
318
  end
307
319
 
308
320
  context 'when importmap is available' do
@@ -312,8 +324,7 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
312
324
  end
313
325
 
314
326
  it 'stores bundle in context and returns web component script' do
315
- result = helper.ckeditor5_lazy_javascript_tags
316
-
327
+ result = helper.ckeditor5_lazy_javascript_tags.html_safe
317
328
  expect(result).to have_tag('script', with: {
318
329
  type: 'module',
319
330
  src: 'web-component.js'
@@ -348,6 +359,100 @@ RSpec.describe CKEditor5::Rails::Cdn::Helpers do
348
359
  end
349
360
  end
350
361
 
362
+ describe '#ckeditor5_inline_plugins_tags' do
363
+ let(:preset) do
364
+ CKEditor5::Rails::Presets::PresetBuilder.new do
365
+ inline_plugin 'Plugin1', <<~JAVASCRIPT
366
+ const { Plugin } = await import( 'ckeditor5' );
367
+
368
+ return class Plugin1 extends Plugin {
369
+ init() {
370
+ window.Plugin1 = true;
371
+ }
372
+ }
373
+ JAVASCRIPT
374
+
375
+ inline_plugin 'Plugin2', <<~JAVASCRIPT
376
+ const { Plugin } = await import( 'ckeditor5' );
377
+
378
+ return class Plugin2 extends Plugin {
379
+ init() {
380
+ window.Plugin2 = true;
381
+ }
382
+ }
383
+ JAVASCRIPT
384
+ end
385
+ end
386
+
387
+ let(:another_preset) do
388
+ CKEditor5::Rails::Presets::PresetBuilder.new do
389
+ inline_plugin 'Plugin3', <<~JAVASCRIPT
390
+ const { Plugin } = await import( 'ckeditor5' );
391
+
392
+ return class Plugin3 extends Plugin {
393
+ init() {
394
+ window.Plugin3 = true;
395
+ }
396
+ }
397
+ JAVASCRIPT
398
+ end
399
+ end
400
+
401
+ before do
402
+ allow(CKEditor5::Rails::Engine).to receive(:presets).and_return(
403
+ double('PresetManager', to_h: { default: preset, another: another_preset })
404
+ )
405
+ end
406
+
407
+ it 'generates script tags for inline plugins from given preset' do
408
+ result = helper.ckeditor5_inline_plugins_tags(preset)
409
+
410
+ expect(result).to have_tag('script', count: 2)
411
+ expect(result).to include('window.Plugin1=true')
412
+ expect(result).to include('window.Plugin2=true')
413
+ expect(result).not_to include('window.Plugin3=true')
414
+ end
415
+
416
+ it 'generates script tags for inline plugins from all presets when no preset given' do
417
+ result = helper.ckeditor5_inline_plugins_tags
418
+
419
+ expect(result).to have_tag('script', count: 3)
420
+ expect(result).to include('window.Plugin1=true')
421
+ expect(result).to include('window.Plugin2=true')
422
+ expect(result).to include('window.Plugin3=true')
423
+ end
424
+
425
+ it 'adds nonce to script tags when available' do
426
+ result = helper.ckeditor5_inline_plugins_tags(preset)
427
+ expect(result).to have_tag('script', with: { nonce: 'test-nonce' })
428
+ end
429
+
430
+ context 'with preset having no inline plugins' do
431
+ let(:empty_preset) do
432
+ CKEditor5::Rails::Presets::PresetBuilder.new do
433
+ plugins :Bold, :Italic # Regular plugins, not inline
434
+ end
435
+ end
436
+
437
+ it 'returns empty safe buffer when no inline plugins are present' do
438
+ result = helper.ckeditor5_inline_plugins_tags(empty_preset)
439
+ expect(result).to be_html_safe
440
+ expect(result).to be_empty
441
+ end
442
+ end
443
+
444
+ context 'with nil preset' do
445
+ it 'includes plugins from all registered presets' do
446
+ result = helper.ckeditor5_inline_plugins_tags(nil)
447
+
448
+ expect(result).to have_tag('script', count: 3)
449
+ expect(result).to include('window.Plugin1=true')
450
+ expect(result).to include('window.Plugin2=true')
451
+ expect(result).to include('window.Plugin3=true')
452
+ end
453
+ end
454
+ end
455
+
351
456
  describe 'cdn helper methods' do
352
457
  it 'generates helper methods for third-party CDNs' do
353
458
  expect(helper).to respond_to(:ckeditor5_unpkg_assets)
@@ -8,6 +8,10 @@ RSpec.describe CKEditor5::Rails::Context::Helpers do
8
8
  Class.new do
9
9
  include ActionView::Helpers::TagHelper
10
10
  include CKEditor5::Rails::Context::Helpers
11
+
12
+ def content_security_policy_nonce
13
+ 'test-nonce'
14
+ end
11
15
  end
12
16
  end
13
17
 
@@ -35,6 +39,14 @@ RSpec.describe CKEditor5::Rails::Context::Helpers do
35
39
  end
36
40
  end
37
41
 
42
+ it 'returns empty component when preset is nil' do
43
+ result = helper.ckeditor5_context(nil)
44
+
45
+ expect(result).to be_html_safe
46
+ expect(result).to have_tag('ckeditor-context-component', count: 1)
47
+ expect(result).not_to have_tag('script')
48
+ end
49
+
38
50
  it 'is optional to pass a preset' do
39
51
  expect(helper.ckeditor5_context).to have_tag(
40
52
  'ckeditor-context-component',
@@ -87,6 +99,26 @@ RSpec.describe CKEditor5::Rails::Context::Helpers do
87
99
  )
88
100
  end
89
101
 
102
+ it 'includes inline plugins script tags when preset has inline plugins' do
103
+ preset = CKEditor5::Rails::Context::PresetBuilder.new do
104
+ inline_plugin :CustomPlugin, <<~JS
105
+ const { Plugin } = await import('ckeditor5');
106
+
107
+ return class CustomPlugin extends Plugin {
108
+ static get pluginName() { return 'CustomPlugin'; }
109
+ }
110
+ JS
111
+ end
112
+
113
+ result = helper.ckeditor5_context(preset)
114
+
115
+ expect(result).to have_tag('script', with: { nonce: 'test-nonce' }) do
116
+ with_text(/CustomPlugin/)
117
+ end
118
+
119
+ expect(result).to have_tag('ckeditor-context-component')
120
+ end
121
+
90
122
  it 'accepts block content' do
91
123
  result = helper.ckeditor5_context(empty_preset) { 'Content' }
92
124
 
@@ -84,17 +84,27 @@ RSpec.describe CKEditor5::Rails::Context::PresetBuilder do
84
84
  end
85
85
 
86
86
  describe '#inline_plugin' do
87
+ let(:plugin_code) do
88
+ <<~JAVASCRIPT
89
+ const { Plugin } = await import( 'ckeditor5' );
90
+
91
+ return class Abc extends Plugin {}
92
+ JAVASCRIPT
93
+ end
94
+
87
95
  it 'adds inline plugin to config' do
88
- plugin = builder.inline_plugin('Test', 'export default class Abc {}')
96
+ plugin = builder.inline_plugin('Test', plugin_code)
89
97
 
90
98
  expect(builder.config[:plugins]).to include(plugin)
91
99
  expect(plugin).to be_a(CKEditor5::Rails::Editor::PropsInlinePlugin)
92
100
  end
93
101
 
94
102
  it 'accepts plugin options' do
95
- plugin = builder.inline_plugin('Test', 'export default class Abc {}')
103
+ plugin = builder.inline_plugin('Test', plugin_code)
96
104
 
97
- expect(plugin.code).to eq('export default class Abc {}')
105
+ expect(plugin.code).to eq(
106
+ '(async()=>{const{Plugin:t}=await import("ckeditor5");return class n extends t{}})();'
107
+ )
98
108
  end
99
109
  end
100
110
 
@@ -6,7 +6,11 @@ RSpec.describe CKEditor5::Rails::Context::PresetSerializer do
6
6
  let(:preset) do
7
7
  CKEditor5::Rails::Context::PresetBuilder.new do
8
8
  plugin 'Plugin1', import_name: '@ckeditor/plugin1'
9
- inline_plugin 'plugin2', 'export default class Plugin2 {}'
9
+ inline_plugin 'plugin2', <<~JAVASCRIPT
10
+ const { Plugin } = await import( 'ckeditor5' );
11
+
12
+ return class Abc extends Plugin {}
13
+ JAVASCRIPT
10
14
 
11
15
  configure :toolbar, { items: %w[bold italic] }
12
16
  configure :language, 'en'
@@ -39,15 +43,15 @@ RSpec.describe CKEditor5::Rails::Context::PresetSerializer do
39
43
 
40
44
  it 'normalizes and includes all plugins' do
41
45
  plugins = JSON.parse(plugins_json)
46
+
42
47
  expect(plugins.size).to eq(2)
43
48
  expect(plugins.first).to include(
44
49
  'type' => 'external',
45
50
  'import_name' => '@ckeditor/plugin1'
46
51
  )
47
52
  expect(plugins.last).to include(
48
- 'type' => 'inline',
49
- 'name' => 'plugin2',
50
- 'code' => 'export default class Plugin2 {}'
53
+ 'type' => 'external',
54
+ 'window_name' => 'plugin2'
51
55
  )
52
56
  end
53
57
  end
@@ -43,14 +43,15 @@ RSpec.describe CKEditor5::Rails::Editor::Helpers::Config do
43
43
  expect do
44
44
  helper.ckeditor5_preset do
45
45
  inline_plugin :CustomPlugin, <<~JS
46
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
47
- export default class CustomPlugin extends Plugin {
46
+ const { Plugin } = await import( 'ckeditor5' );
47
+
48
+ return class CustomPlugin extends Plugin {
48
49
  static get pluginName() { return 'CustomPlugin'; }
49
50
  }
50
51
  JS
51
52
  end
52
53
  end.to raise_error(
53
- CKEditor5::Rails::Presets::Concerns::PluginMethods::DisallowedInlinePlugin,
54
+ CKEditor5::Rails::Presets::Concerns::PluginMethods::DisallowedInlinePluginError,
54
55
  'Inline plugins are not allowed here.'
55
56
  )
56
57
  end
@@ -23,7 +23,7 @@ RSpec.describe CKEditor5::Rails::Editor::PropsBasePlugin do
23
23
  end
24
24
 
25
25
  it 'returns inline plugin instances unchanged' do
26
- inline = CKEditor5::Rails::Editor::PropsInlinePlugin.new(:Custom, 'export default class {}')
26
+ inline = CKEditor5::Rails::Editor::PropsInlinePlugin.new(:Custom, 'return class {}')
27
27
  plugin = described_class.normalize(inline)
28
28
  expect(plugin).to be(inline)
29
29
  end
@@ -5,8 +5,9 @@ require 'spec_helper'
5
5
  RSpec.describe CKEditor5::Rails::Editor::PropsInlinePlugin do
6
6
  let(:valid_code) do
7
7
  <<~JAVASCRIPT
8
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
9
- export default class CustomPlugin extends Plugin {
8
+ const { Plugin } = await import( 'ckeditor5' );
9
+
10
+ return class CustomPlugin extends Plugin {
10
11
  init() {
11
12
  console.log('Custom plugin initialized');
12
13
  }
@@ -23,20 +24,14 @@ RSpec.describe CKEditor5::Rails::Editor::PropsInlinePlugin do
23
24
  expect { described_class.new(:CustomPlugin, nil) }
24
25
  .to raise_error(ArgumentError, 'Code must be a String')
25
26
  end
26
-
27
- it 'raises error when code lacks export default' do
28
- expect { described_class.new(:CustomPlugin, 'class CustomPlugin {}') }
29
- .to raise_error(ArgumentError, /must include `export default`/)
30
- end
31
27
  end
32
28
 
33
29
  describe '#to_h' do
34
30
  it 'returns correct hash representation' do
35
31
  plugin = described_class.new(:CustomPlugin, valid_code)
36
32
  expect(plugin.to_h).to eq({
37
- type: :inline,
38
- name: :CustomPlugin,
39
- code: valid_code
33
+ type: :external,
34
+ window_name: :CustomPlugin
40
35
  })
41
36
  end
42
37
  end
@@ -7,7 +7,11 @@ RSpec.describe CKEditor5::Rails::Hooks::Form do
7
7
  let(:post) { Post.new(content: 'Initial content') }
8
8
  let(:builder) { described_class.new(:post, post, template) }
9
9
  let(:template) do
10
- ActionView::Base.new(ActionView::LookupContext.new([]), {}, nil)
10
+ Class.new(ActionView::Base) do
11
+ def content_security_policy_nonce
12
+ 'test-nonce'
13
+ end
14
+ end.new(ActionView::LookupContext.new([]), {}, nil)
11
15
  end
12
16
 
13
17
  before do
@@ -243,8 +243,9 @@ RSpec.describe CKEditor5::Rails::Presets::PresetBuilder do
243
243
  describe '#inline_plugin' do
244
244
  let(:plugin_code) do
245
245
  <<~JAVASCRIPT
246
- import Plugin from 'ckeditor5/src/plugin';
247
- export default class CustomPlugin extends Plugin {
246
+ const { Plugin } = await import( 'ckeditor5' );
247
+
248
+ return class CustomPlugin extends Plugin {
248
249
  init() {
249
250
  // plugin initialization
250
251
  }
@@ -258,7 +259,9 @@ RSpec.describe CKEditor5::Rails::Presets::PresetBuilder do
258
259
  plugin = builder.config[:plugins].first
259
260
  expect(plugin).to be_a(CKEditor5::Rails::Editor::PropsInlinePlugin)
260
261
  expect(plugin.name).to eq(:CustomPlugin)
261
- expect(plugin.code).to eq(plugin_code)
262
+ expect(plugin.code).to eq(
263
+ '(async()=>{const{Plugin:t}=await import("ckeditor5");return class i extends t{init(){}}})();'
264
+ )
262
265
  end
263
266
 
264
267
  it 'allows multiple inline plugins' do
@@ -268,6 +271,18 @@ RSpec.describe CKEditor5::Rails::Presets::PresetBuilder do
268
271
  plugin_names = builder.config[:plugins].map(&:name)
269
272
  expect(plugin_names).to eq(%i[Plugin1 Plugin2])
270
273
  end
274
+
275
+ it 'should raise UnsupportedESModuleError when ES module is passed' do
276
+ expect do
277
+ builder.inline_plugin(:CustomPlugin, 'export default class CustomPlugin {}')
278
+ end.to raise_error(CKEditor5::Rails::Presets::Concerns::PluginMethods::UnsupportedESModuleError)
279
+ end
280
+
281
+ it 'should raise MissingInlinePluginError when plugin code is invalid' do
282
+ expect do
283
+ builder.inline_plugin(:CustomPlugin, 'return class CustomPlugin {}')
284
+ end.to raise_error(CKEditor5::Rails::Presets::Concerns::PluginMethods::MissingInlinePluginError)
285
+ end
271
286
  end
272
287
 
273
288
  describe '#plugin' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ckeditor5
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.23.4
4
+ version: 1.24.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mateusz Bagiński
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-12-19 00:00:00.000000000 Z
12
+ date: 2024-12-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rails
@@ -68,6 +68,7 @@ files:
68
68
  - lib/ckeditor5/rails/cdn/ckbox_bundle.rb
69
69
  - lib/ckeditor5/rails/cdn/ckeditor_bundle.rb
70
70
  - lib/ckeditor5/rails/cdn/concerns/bundle_builder.rb
71
+ - lib/ckeditor5/rails/cdn/concerns/inline_plugins_tags_builder.rb
71
72
  - lib/ckeditor5/rails/cdn/helpers.rb
72
73
  - lib/ckeditor5/rails/cdn/url_generator.rb
73
74
  - lib/ckeditor5/rails/context/helpers.rb