@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.
- package/LICENSE.txt +339 -0
- package/README.md +83 -0
- package/Sample/index.html +19 -0
- package/Sample/main.js +21 -0
- package/Sample/main.ts +33 -0
- package/dist/index.d.ts +3 -0
- package/dist/sourceediting-codemirror.css +1 -0
- package/dist/sourceediting-codemirror.d.ts +10 -0
- package/dist/sourceediting-codemirror.js +261 -0
- package/index.d.ts +4 -0
- package/package.json +40 -0
- package/playwright/global.d.ts +7 -0
- package/playwright/source-editing-code-mirror.spec.js +50 -0
- package/playwright/source-editing-code-mirror.spec.ts +67 -0
- package/playwright.config.js +13 -0
- package/playwright.config.ts +14 -0
- package/src/index.js +3 -0
- package/src/index.ts +5 -0
- package/src/sourceediting-codemirror.css +18 -0
- package/src/sourceediting-codemirror.js +114 -0
- package/src/sourceediting-codemirror.ts +146 -0
- package/tsconfig.build.json +17 -0
- package/tsconfig.json +13 -0
- package/vite.config.js +50 -0
|
@@ -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
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,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
package/src/index.ts
ADDED
|
@@ -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
|
+
}
|