ckeditor5 1.23.4 → 1.24.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +31 -5
- data/lib/ckeditor5/rails/assets/assets_bundle_html_serializer.rb +36 -17
- data/lib/ckeditor5/rails/assets/webcomponent_bundle.rb +2 -2
- data/lib/ckeditor5/rails/assets/webcomponents/components/context.mjs +10 -0
- data/lib/ckeditor5/rails/assets/webcomponents/components/editor.mjs +23 -6
- data/lib/ckeditor5/rails/assets/webcomponents/utils.mjs +7 -20
- data/lib/ckeditor5/rails/cdn/concerns/inline_plugins_tags_builder.rb +35 -0
- data/lib/ckeditor5/rails/cdn/helpers.rb +62 -17
- data/lib/ckeditor5/rails/context/helpers.rb +9 -1
- data/lib/ckeditor5/rails/editor/helpers/editor_helpers.rb +16 -0
- data/lib/ckeditor5/rails/editor/props_inline_plugin.rb +24 -6
- data/lib/ckeditor5/rails/hooks/importmap.rb +1 -0
- data/lib/ckeditor5/rails/plugins/simple_upload_adapter.rb +1 -1
- data/lib/ckeditor5/rails/presets/concerns/plugin_methods.rb +27 -6
- data/lib/ckeditor5/rails/version.rb +1 -1
- data/spec/e2e/features/context_spec.rb +1 -1
- data/spec/e2e/features/editor_types_spec.rb +9 -0
- data/spec/e2e/features/lazy_assets_spec.rb +7 -0
- data/spec/lib/ckeditor5/rails/assets/assets_bundle_hml_serializer_spec.rb +2 -2
- data/spec/lib/ckeditor5/rails/cdn/helpers_spec.rb +108 -3
- data/spec/lib/ckeditor5/rails/context/helpers_spec.rb +32 -0
- data/spec/lib/ckeditor5/rails/context/preset_builder_spec.rb +13 -3
- data/spec/lib/ckeditor5/rails/context/preset_serializer_spec.rb +8 -4
- data/spec/lib/ckeditor5/rails/editor/helpers/config_helpers_spec.rb +4 -3
- data/spec/lib/ckeditor5/rails/editor/props_base_plugin_spec.rb +1 -1
- data/spec/lib/ckeditor5/rails/editor/props_inline_plugin_spec.rb +5 -10
- data/spec/lib/ckeditor5/rails/hooks/form_spec.rb +5 -1
- data/spec/lib/ckeditor5/rails/presets/preset_builder_spec.rb +18 -3
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0ee1985934d17921821b5d298275a64b71154309e0492f8da8aaae33c7dab2bd
|
4
|
+
data.tar.gz: 880ec9989cd23462c889b35e68c7b49bdc9842c826b19326095d6380550220b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
851
|
+
const { Plugin } = await import( 'ckeditor5' );
|
851
852
|
|
852
|
-
|
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
|
-
#
|
1849
|
+
# const { Plugin } = await import( 'ckeditor5' );
|
1849
1850
|
#
|
1850
|
-
#
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
|
45
|
-
tag.script(src: script.url, nonce:
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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: :
|
19
|
-
|
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
|
-
|
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
|
-
|
32
|
-
|
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
|
@@ -5,7 +5,7 @@ module CKEditor5::Rails::Plugins
|
|
5
5
|
PLUGIN_CODE = <<~JS
|
6
6
|
import { Plugin, FileRepository } from 'ckeditor5';
|
7
7
|
|
8
|
-
|
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
|
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
|
-
#
|
40
|
+
# const { Plugin } = await import( 'ckeditor5' );
|
38
41
|
#
|
39
|
-
#
|
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
|
-
|
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.
|
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
|
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
|
@@ -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.
|
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',
|
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',
|
103
|
+
plugin = builder.inline_plugin('Test', plugin_code)
|
96
104
|
|
97
|
-
expect(plugin.code).to eq(
|
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',
|
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' => '
|
49
|
-
'
|
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
|
-
|
47
|
-
|
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::
|
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, '
|
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
|
-
|
9
|
-
|
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: :
|
38
|
-
|
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
|
-
|
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
|
-
|
247
|
-
|
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(
|
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.
|
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-
|
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
|