@lstsystems/ckeditor5-source-editing-codemirror 47.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,261 @@
1
+ import { Plugin as g } from "ckeditor5";
2
+ import { lineNumbers as m, highlightActiveLine as w, highlightActiveLineGutter as y, EditorView as f } from "@codemirror/view";
3
+ import { EditorSelection as E, EditorState as x } from "@codemirror/state";
4
+ import { html as S } from "@codemirror/lang-html";
5
+ import { IndentContext as b, getIndentation as v, indentString as C, syntaxHighlighting as N, defaultHighlightStyle as O, indentOnInput as A, indentUnit as k } from "@codemirror/language";
6
+ import B from "prettier/standalone";
7
+ import * as P from "prettier/plugins/html";
8
+ import * as z from "prettier/plugins/postcss";
9
+ import * as D from "prettier/plugins/typescript";
10
+ import * as L from "prettier/plugins/markdown";
11
+ import * as j from "prettier/plugins/yaml";
12
+ let I = 0;
13
+ class s {
14
+ /**
15
+ Create a new node prop type.
16
+ */
17
+ constructor(e = {}) {
18
+ this.id = I++, this.perNode = !!e.perNode, this.deserialize = e.deserialize || (() => {
19
+ throw new Error("This node type doesn't define a deserialize function");
20
+ }), this.combine = e.combine || null;
21
+ }
22
+ /**
23
+ This is meant to be used with
24
+ [`NodeSet.extend`](#common.NodeSet.extend) or
25
+ [`LRParser.configure`](#lr.ParserConfig.props) to compute
26
+ prop values for each node type in the set. Takes a [match
27
+ object](#common.NodeType^match) or function that returns undefined
28
+ if the node type doesn't get this prop, and the prop's value if
29
+ it does.
30
+ */
31
+ add(e) {
32
+ if (this.perNode)
33
+ throw new RangeError("Can't add per-node props to node types");
34
+ return typeof e != "function" && (e = p.match(e)), (o) => {
35
+ let i = e(o);
36
+ return i === void 0 ? null : [this, i];
37
+ };
38
+ }
39
+ }
40
+ s.closedBy = new s({ deserialize: (t) => t.split(" ") });
41
+ s.openedBy = new s({ deserialize: (t) => t.split(" ") });
42
+ s.group = new s({ deserialize: (t) => t.split(" ") });
43
+ s.isolate = new s({ deserialize: (t) => {
44
+ if (t && t != "rtl" && t != "ltr" && t != "auto")
45
+ throw new RangeError("Invalid value for isolate: " + t);
46
+ return t || "auto";
47
+ } });
48
+ s.contextHash = new s({ perNode: !0 });
49
+ s.lookAhead = new s({ perNode: !0 });
50
+ s.mounted = new s({ perNode: !0 });
51
+ const T = /* @__PURE__ */ Object.create(null);
52
+ class p {
53
+ /**
54
+ @internal
55
+ */
56
+ constructor(e, o, i, n = 0) {
57
+ this.name = e, this.props = o, this.id = i, this.flags = n;
58
+ }
59
+ /**
60
+ Define a node type.
61
+ */
62
+ static define(e) {
63
+ let o = e.props && e.props.length ? /* @__PURE__ */ Object.create(null) : T, i = (e.top ? 1 : 0) | (e.skipped ? 2 : 0) | (e.error ? 4 : 0) | (e.name == null ? 8 : 0), n = new p(e.name || "", o, e.id, i);
64
+ if (e.props) {
65
+ for (let r of e.props)
66
+ if (Array.isArray(r) || (r = r(n)), r) {
67
+ if (r[0].perNode)
68
+ throw new RangeError("Can't store a per-node prop on a node type");
69
+ o[r[0].id] = r[1];
70
+ }
71
+ }
72
+ return n;
73
+ }
74
+ /**
75
+ Retrieves a node prop for this type. Will return `undefined` if
76
+ the prop isn't present on this node.
77
+ */
78
+ prop(e) {
79
+ return this.props[e.id];
80
+ }
81
+ /**
82
+ True when this is the top node of a grammar.
83
+ */
84
+ get isTop() {
85
+ return (this.flags & 1) > 0;
86
+ }
87
+ /**
88
+ True when this node is produced by a skip rule.
89
+ */
90
+ get isSkipped() {
91
+ return (this.flags & 2) > 0;
92
+ }
93
+ /**
94
+ Indicates whether this is an error node.
95
+ */
96
+ get isError() {
97
+ return (this.flags & 4) > 0;
98
+ }
99
+ /**
100
+ When true, this node type doesn't correspond to a user-declared
101
+ named node, for example because it is used to cache repetition.
102
+ */
103
+ get isAnonymous() {
104
+ return (this.flags & 8) > 0;
105
+ }
106
+ /**
107
+ Returns true when this node's name or one of its
108
+ [groups](#common.NodeProp^group) matches the given string.
109
+ */
110
+ is(e) {
111
+ if (typeof e == "string") {
112
+ if (this.name == e)
113
+ return !0;
114
+ let o = this.prop(s.group);
115
+ return o ? o.indexOf(e) > -1 : !1;
116
+ }
117
+ return this.id == e;
118
+ }
119
+ /**
120
+ Create a function from node types to arbitrary values by
121
+ specifying an object whose property names are node or
122
+ [group](#common.NodeProp^group) names. Often useful with
123
+ [`NodeProp.add`](#common.NodeProp.add). You can put multiple
124
+ names, separated by spaces, in a single property name to map
125
+ multiple node names to a single value.
126
+ */
127
+ static match(e) {
128
+ let o = /* @__PURE__ */ Object.create(null);
129
+ for (let i in e)
130
+ for (let n of i.split(" "))
131
+ o[n] = e[i];
132
+ return (i) => {
133
+ for (let n = i.prop(s.group), r = -1; r < (n ? n.length : 0); r++) {
134
+ let a = o[r < 0 ? i.name : n[r]];
135
+ if (a)
136
+ return a;
137
+ }
138
+ };
139
+ }
140
+ }
141
+ p.none = new p(
142
+ "",
143
+ /* @__PURE__ */ Object.create(null),
144
+ 0,
145
+ 8
146
+ /* NodeFlag.Anonymous */
147
+ );
148
+ var h;
149
+ (function(t) {
150
+ t[t.ExcludeBuffers = 1] = "ExcludeBuffers", t[t.IncludeAnonymous = 2] = "IncludeAnonymous", t[t.IgnoreMounts = 4] = "IgnoreMounts", t[t.IgnoreOverlays = 8] = "IgnoreOverlays", t[t.EnterBracketed = 16] = "EnterBracketed";
151
+ })(h || (h = {}));
152
+ new s({ perNode: !0 });
153
+ function q(t, e) {
154
+ let o = -1;
155
+ return t.changeByRange((i) => {
156
+ let n = [];
157
+ for (let a = i.from; a <= i.to; ) {
158
+ let l = t.doc.lineAt(a);
159
+ l.number > o && (i.empty || i.to > l.from) && (e(l, n, i), o = l.number), a = l.to + 1;
160
+ }
161
+ let r = t.changes(n);
162
+ return {
163
+ changes: n,
164
+ range: E.range(r.mapPos(i.anchor, 1), r.mapPos(i.head, 1))
165
+ };
166
+ });
167
+ }
168
+ const H = ({ state: t, dispatch: e }) => {
169
+ if (t.readOnly)
170
+ return !1;
171
+ let o = /* @__PURE__ */ Object.create(null), i = new b(t, { overrideIndentation: (r) => {
172
+ let a = o[r];
173
+ return a ?? -1;
174
+ } }), n = q(t, (r, a, l) => {
175
+ let d = v(i, r.from);
176
+ if (d == null)
177
+ return;
178
+ /\S/.test(r.text) || (d = 0);
179
+ let u = /^\s*/.exec(r.text)[0], c = C(t, d);
180
+ (u != c || l.from < r.from + u.length) && (o[r.from] = d, a.push({ from: r.from, to: r.from + u.length, insert: c }));
181
+ });
182
+ return n.changes.empty || e(t.update(n, { userEvent: "indent" })), !0;
183
+ };
184
+ class Y extends g {
185
+ constructor() {
186
+ super(...arguments), this.view = null;
187
+ }
188
+ static get pluginName() {
189
+ return "SourceEditingCodeMirror";
190
+ }
191
+ static get requires() {
192
+ return ["SourceEditing"];
193
+ }
194
+ // Prettier 3 formatter
195
+ async formatWithPrettier(e) {
196
+ return await B.format(e, {
197
+ parser: "html",
198
+ plugins: [
199
+ P,
200
+ z,
201
+ D,
202
+ L,
203
+ j
204
+ ],
205
+ tabWidth: 4,
206
+ useTabs: !1
207
+ });
208
+ }
209
+ // Listen to SourceEditing mode changes
210
+ afterInit() {
211
+ const o = this.editor.plugins.get("SourceEditing");
212
+ this.listenTo(o, "change:isSourceEditingMode", async (i, n, r) => {
213
+ r ? await this.enableCodeMirror() : this.disableCodeMirror();
214
+ });
215
+ }
216
+ // Enable CodeMirror with Prettier formatting
217
+ async enableCodeMirror() {
218
+ const e = this.editor;
219
+ e.plugins.get("SourceEditing").updateEditorData();
220
+ const i = await this.formatWithPrettier(e.getData()), n = document.querySelector(".ck-source-editing-area");
221
+ if (!n)
222
+ return;
223
+ const r = n.querySelector("textarea");
224
+ r && (r.style.display = "none");
225
+ const a = document.createElement("div");
226
+ a.classList.add("cm-wrapper"), n.appendChild(a);
227
+ const l = [
228
+ m(),
229
+ w(),
230
+ y(),
231
+ N(O),
232
+ A(),
233
+ k.of(" ")
234
+ ], u = e.config.get("sourceEditingCodeMirror")?.extensions ?? [];
235
+ this.view = new f({
236
+ state: x.create({
237
+ doc: i,
238
+ extensions: [
239
+ ...l,
240
+ S(),
241
+ ...u,
242
+ f.updateListener.of((c) => {
243
+ c.docChanged && e.setData(c.state.doc.toString());
244
+ })
245
+ ]
246
+ }),
247
+ parent: a
248
+ }), this.view.dispatch({
249
+ selection: { anchor: 0, head: this.view.state.doc.length }
250
+ }), H(this.view);
251
+ }
252
+ // Disable CodeMirror and show original textarea
253
+ disableCodeMirror() {
254
+ this.view && (this.view.destroy(), this.view = null);
255
+ const e = document.querySelector(".ck-source-editing-area textarea");
256
+ e && (e.style.display = "");
257
+ }
258
+ }
259
+ export {
260
+ Y as default
261
+ };
package/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module "ckeditor5-source-editing-codemirror" {
2
+ const plugin: any;
3
+ export default plugin;
4
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@lstsystems/ckeditor5-source-editing-codemirror",
3
+ "description": "Source editing plugin for CKEditor 5 using CodeMirror.",
4
+ "private": false,
5
+ "version": "47.5.1",
6
+ "type": "module",
7
+ "main": "dist/sourceediting-codemirror.js",
8
+ "module": "dist/sourceediting-codemirror.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "import": "./dist/sourceediting-codemirror.js",
13
+ "types": "./dist/index.d.ts"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "dev": "vite",
18
+ "build": "tsc -p tsconfig.build.json && vite build",
19
+ "preview": "vite preview",
20
+ "test": "playwright test",
21
+ "test:ui": "playwright test --ui"
22
+ },
23
+ "dependencies": {
24
+ "@codemirror/commands": "^6.10.2",
25
+ "@codemirror/lang-html": "^6.4.11",
26
+ "@codemirror/language": "^6.12.1",
27
+ "@codemirror/state": "^6.5.4",
28
+ "@codemirror/view": "^6.39.15",
29
+ "ckeditor5": "^47.5.0",
30
+ "prettier": "^3.8.1"
31
+ },
32
+ "devDependencies": {
33
+ "@playwright/test": "^1.58.2",
34
+ "typescript": "^5.9.3",
35
+ "vite": "^7.3.1"
36
+ },
37
+ "author": "LST Systems",
38
+ "license": "SEE LICENSE.md",
39
+ "homepage": "https://github.com/AlphaDepot/ckeditor5-source-editing-codemirror"
40
+ }
@@ -0,0 +1,7 @@
1
+ declare global {
2
+ interface Window {
3
+ editor: any;
4
+ }
5
+ }
6
+
7
+ export {};
@@ -0,0 +1,50 @@
1
+ import { test, expect } from '@playwright/test';
2
+ test.beforeEach(async ({ page }) => {
3
+ await page.goto('/');
4
+ await page.waitForFunction(() => window.editor !== undefined);
5
+ });
6
+ test('CodeMirror initializes in source mode', async ({ page }) => {
7
+ await page.click('button.ck-source-editing-button');
8
+ const cm = await page.waitForSelector('.cm-wrapper');
9
+ expect(cm).not.toBeNull();
10
+ });
11
+ test('CodeMirror destroys when leaving source mode', async ({ page }) => {
12
+ await page.click('button.ck-source-editing-button');
13
+ await page.waitForSelector('.cm-wrapper');
14
+ await page.click('button.ck-source-editing-button');
15
+ const exists = await page.$('.cm-wrapper');
16
+ expect(exists).toBeNull();
17
+ });
18
+ test('CodeMirror shows gutter when enabled', async ({ page }) => {
19
+ // Enter source editing mode
20
+ await page.click('button.ck-source-editing-button');
21
+ await page.waitForSelector('.cm-wrapper');
22
+ // Assert gutter exists
23
+ const gutter = await page.locator('.cm-gutters').count();
24
+ expect(gutter).toBeGreaterThan(0);
25
+ });
26
+ test('CodeMirror renders formatted lines', async ({ page }) => {
27
+ // Enter source editing mode
28
+ await page.click('button.ck-source-editing-button');
29
+ await page.waitForSelector('.cm-wrapper');
30
+ // Ensure CodeMirror content exists
31
+ await page.waitForSelector('.cm-content');
32
+ // Check that at least one formatted line exists
33
+ const lineCount = await page.locator('.cm-content .cm-line').count();
34
+ expect(lineCount).toBeGreaterThan(0);
35
+ });
36
+ test('CodeMirror syncs data back to CKEditor', async ({ page }) => {
37
+ // Enter source editing mode
38
+ await page.click('button.ck-source-editing-button');
39
+ await page.waitForSelector('.cm-wrapper');
40
+ // Focus CodeMirror by clicking a real line
41
+ await page.locator('.cm-line').first().click();
42
+ // Insert a block CKEditor will preserve
43
+ await page.keyboard.type('<p>test</p>');
44
+ // Exit source editing mode
45
+ await page.click('button.ck-source-editing-button');
46
+ // Get CKEditor data
47
+ const data = await page.evaluate(() => window.editor.getData());
48
+ // Assert the update happened
49
+ expect(data).toContain('<p>test</p>');
50
+ });
@@ -0,0 +1,67 @@
1
+ import { test, expect } from '@playwright/test';
2
+
3
+ test.beforeEach(async ({ page }) => {
4
+ await page.goto('/');
5
+ await page.waitForFunction(() => window.editor !== undefined);
6
+ });
7
+
8
+ test('CodeMirror initializes in source mode', async ({ page }) => {
9
+ await page.click('button.ck-source-editing-button');
10
+
11
+ const cm = await page.waitForSelector('.cm-wrapper');
12
+ expect(cm).not.toBeNull();
13
+ });
14
+
15
+ test('CodeMirror destroys when leaving source mode', async ({ page }) => {
16
+ await page.click('button.ck-source-editing-button');
17
+ await page.waitForSelector('.cm-wrapper');
18
+
19
+ await page.click('button.ck-source-editing-button');
20
+
21
+ const exists = await page.$('.cm-wrapper');
22
+ expect(exists).toBeNull();
23
+ });
24
+
25
+ test('CodeMirror shows gutter when enabled', async ({ page }) => {
26
+ // Enter source editing mode
27
+ await page.click('button.ck-source-editing-button');
28
+ await page.waitForSelector('.cm-wrapper');
29
+
30
+ // Assert gutter exists
31
+ const gutter = await page.locator('.cm-gutters').count();
32
+ expect(gutter).toBeGreaterThan(0);
33
+ });
34
+
35
+ test('CodeMirror renders formatted lines', async ({ page }) => {
36
+ // Enter source editing mode
37
+ await page.click('button.ck-source-editing-button');
38
+ await page.waitForSelector('.cm-wrapper');
39
+
40
+ // Ensure CodeMirror content exists
41
+ await page.waitForSelector('.cm-content');
42
+
43
+ // Check that at least one formatted line exists
44
+ const lineCount = await page.locator('.cm-content .cm-line').count();
45
+ expect(lineCount).toBeGreaterThan(0);
46
+ });
47
+
48
+ test('CodeMirror syncs data back to CKEditor', async ({ page }) => {
49
+ // Enter source editing mode
50
+ await page.click('button.ck-source-editing-button');
51
+ await page.waitForSelector('.cm-wrapper');
52
+
53
+ // Focus CodeMirror by clicking a real line
54
+ await page.locator('.cm-line').first().click();
55
+
56
+ // Insert a block CKEditor will preserve
57
+ await page.keyboard.type('<p>test</p>');
58
+
59
+ // Exit source editing mode
60
+ await page.click('button.ck-source-editing-button');
61
+
62
+ // Get CKEditor data
63
+ const data = await page.evaluate(() => window.editor.getData());
64
+
65
+ // Assert the update happened
66
+ expect(data).toContain('<p>test</p>');
67
+ });
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from '@playwright/test';
2
+ export default defineConfig({
3
+ testDir: './playwright',
4
+ use: {
5
+ headless: true,
6
+ viewport: { width: 1280, height: 800 }
7
+ },
8
+ webServer: {
9
+ command: 'npm run dev',
10
+ port: 5173,
11
+ reuseExistingServer: true
12
+ }
13
+ });
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: './playwright',
5
+ use: {
6
+ headless: true,
7
+ viewport: { width: 1280, height: 800 }
8
+ },
9
+ webServer: {
10
+ command: 'npm run dev',
11
+ port: 5173,
12
+ reuseExistingServer: true
13
+ }
14
+ });
package/src/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import './sourceediting-codemirror.css';
2
+ import SourceEditingCodeMirror from './sourceediting-codemirror';
3
+ export default SourceEditingCodeMirror;
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ import './sourceediting-codemirror.css';
2
+ import SourceEditingCodeMirror from './sourceediting-codemirror';
3
+
4
+ export default SourceEditingCodeMirror;
5
+
@@ -0,0 +1,18 @@
1
+ .ck-source-editing-area .cm-editor {
2
+ border: 1px solid var(--ck-color-base-border);
3
+ border-radius: var(--ck-border-radius);
4
+ border-top-left-radius: 0;
5
+ border-top-right-radius: 0;
6
+ }
7
+
8
+ .ck-source-editing-area .cm-editor.cm-focused {
9
+ border-color: var(--ck-color-focus-border);
10
+ box-shadow: var(--ck-inner-shadow), 0 0;
11
+ }
12
+
13
+ /* Hide the area where SourceEditing puts its code content. */
14
+ .ck-source-editing-area::after {
15
+ content: "";
16
+ display: none;
17
+ }
18
+
@@ -0,0 +1,114 @@
1
+ import { Plugin } from 'ckeditor5';
2
+ // CodeMirror 6 imports
3
+ import { EditorView, lineNumbers, highlightActiveLine, highlightActiveLineGutter } from '@codemirror/view';
4
+ import { EditorState } from '@codemirror/state';
5
+ import { html } from '@codemirror/lang-html';
6
+ import { syntaxHighlighting, defaultHighlightStyle, indentOnInput, indentUnit } from '@codemirror/language';
7
+ import { indentSelection } from '@codemirror/commands';
8
+ // Prettier 3 imports
9
+ import prettier from "prettier/standalone";
10
+ import * as prettierHtml from "prettier/plugins/html";
11
+ import * as prettierCss from "prettier/plugins/postcss";
12
+ import * as prettierTs from "prettier/plugins/typescript";
13
+ import * as prettierMarkdown from "prettier/plugins/markdown";
14
+ import * as prettierYaml from "prettier/plugins/yaml";
15
+ export default class SourceEditingCodeMirror extends Plugin {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.view = null;
19
+ }
20
+ static get pluginName() {
21
+ return 'SourceEditingCodeMirror';
22
+ }
23
+ static get requires() {
24
+ return ['SourceEditing'];
25
+ }
26
+ // Prettier 3 formatter
27
+ async formatWithPrettier(code) {
28
+ return await prettier.format(code, {
29
+ parser: "html",
30
+ plugins: [
31
+ prettierHtml,
32
+ prettierCss,
33
+ prettierTs,
34
+ prettierMarkdown,
35
+ prettierYaml
36
+ ],
37
+ tabWidth: 4,
38
+ useTabs: false
39
+ });
40
+ }
41
+ // Listen to SourceEditing mode changes
42
+ afterInit() {
43
+ const editor = this.editor;
44
+ const sourceEditing = editor.plugins.get('SourceEditing');
45
+ this.listenTo(sourceEditing, 'change:isSourceEditingMode', async (_evt, _name, isOn) => {
46
+ if (isOn) {
47
+ await this.enableCodeMirror();
48
+ }
49
+ else {
50
+ this.disableCodeMirror();
51
+ }
52
+ });
53
+ }
54
+ // Enable CodeMirror with Prettier formatting
55
+ async enableCodeMirror() {
56
+ const editor = this.editor;
57
+ const sourceEditing = editor.plugins.get('SourceEditing');
58
+ // Sync CKEditor → source view
59
+ sourceEditing.updateEditorData();
60
+ // Format HTML using Prettier 3
61
+ const htmlData = await this.formatWithPrettier(editor.getData());
62
+ const container = document.querySelector('.ck-source-editing-area');
63
+ if (!container)
64
+ return;
65
+ const textarea = container.querySelector('textarea');
66
+ if (textarea)
67
+ textarea.style.display = 'none';
68
+ const cmDiv = document.createElement('div');
69
+ cmDiv.classList.add('cm-wrapper');
70
+ container.appendChild(cmDiv);
71
+ const cmExtensions = [
72
+ lineNumbers(),
73
+ highlightActiveLine(),
74
+ highlightActiveLineGutter(),
75
+ syntaxHighlighting(defaultHighlightStyle),
76
+ indentOnInput(),
77
+ indentUnit.of(' ')
78
+ ];
79
+ const config = editor.config.get('sourceEditingCodeMirror');
80
+ const extraExtensions = config?.extensions ?? [];
81
+ this.view = new EditorView({
82
+ state: EditorState.create({
83
+ doc: htmlData,
84
+ extensions: [
85
+ ...cmExtensions,
86
+ html(),
87
+ ...extraExtensions,
88
+ EditorView.updateListener.of(update => {
89
+ if (update.docChanged) {
90
+ editor.setData(update.state.doc.toString());
91
+ }
92
+ })
93
+ ]
94
+ }),
95
+ parent: cmDiv
96
+ });
97
+ // Select entire document
98
+ this.view.dispatch({
99
+ selection: { anchor: 0, head: this.view.state.doc.length }
100
+ });
101
+ // Indent entire document (after Prettier formatting)
102
+ indentSelection(this.view);
103
+ }
104
+ // Disable CodeMirror and show original textarea
105
+ disableCodeMirror() {
106
+ if (this.view) {
107
+ this.view.destroy();
108
+ this.view = null;
109
+ }
110
+ const textarea = document.querySelector('.ck-source-editing-area textarea');
111
+ if (textarea)
112
+ textarea.style.display = '';
113
+ }
114
+ }