@ckeditor/ckeditor5-code-block 48.2.0 → 48.3.0-alpha.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/dist/augmentation.d.ts +23 -23
- package/dist/codeblock.d.ts +28 -28
- package/dist/codeblockcommand.d.ts +56 -56
- package/dist/codeblockconfig.d.ts +142 -142
- package/dist/codeblockediting.d.ts +42 -42
- package/dist/codeblockui.d.ts +28 -28
- package/dist/converters.d.ts +119 -119
- package/dist/indentcodeblockcommand.d.ts +28 -28
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +15 -15
- package/dist/index.js +1140 -1426
- package/dist/index.js.map +1 -1
- package/dist/outdentcodeblockcommand.d.ts +29 -29
- package/dist/utils.d.ts +174 -174
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -2,1487 +2,1201 @@
|
|
|
2
2
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
3
|
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
4
4
|
*/
|
|
5
|
-
import { Command, Plugin } from
|
|
6
|
-
import { ShiftEnter } from
|
|
7
|
-
import { ViewUpcastWriter } from
|
|
8
|
-
import { ClipboardPipeline } from
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { IconCodeBlock } from
|
|
5
|
+
import { Command, Plugin } from "@ckeditor/ckeditor5-core";
|
|
6
|
+
import { ShiftEnter } from "@ckeditor/ckeditor5-enter";
|
|
7
|
+
import { ViewUpcastWriter } from "@ckeditor/ckeditor5-engine";
|
|
8
|
+
import { ClipboardPipeline } from "@ckeditor/ckeditor5-clipboard";
|
|
9
|
+
import { Collection, first } from "@ckeditor/ckeditor5-utils";
|
|
10
|
+
import { MenuBarMenuListItemButtonView, MenuBarMenuListItemView, MenuBarMenuListView, MenuBarMenuView, SplitButtonView, UIModel, addListToDropdown, createDropdown } from "@ckeditor/ckeditor5-ui";
|
|
11
|
+
import { IconCodeBlock } from "@ckeditor/ckeditor5-icons";
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
14
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
15
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Returns code block languages as defined in `config.codeBlock.languages` but processed:
|
|
19
|
+
*
|
|
20
|
+
* * To consider the editor localization, i.e. to display {@link module:code-block/codeblockconfig~CodeBlockLanguageDefinition}
|
|
21
|
+
* in the correct language. There is no way to use {@link module:utils/locale~Locale#t} when the user
|
|
22
|
+
* configuration is defined because the editor does not exist yet.
|
|
23
|
+
* * To make sure each definition has a CSS class associated with it even if not specified
|
|
24
|
+
* in the original configuration.
|
|
25
|
+
*
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
function getNormalizedAndLocalizedLanguageDefinitions(editor) {
|
|
29
|
+
const t = editor.t;
|
|
30
|
+
const languageDefs = editor.config.get("codeBlock.languages");
|
|
31
|
+
for (const def of languageDefs) {
|
|
32
|
+
if (def.label === "Plain text") def.label = t("Plain text");
|
|
33
|
+
if (def.class === void 0) def.class = `language-${def.language}`;
|
|
34
|
+
}
|
|
35
|
+
return languageDefs;
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
association[def[key]] = def[value];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return association;
|
|
38
|
+
* Returns an object associating certain language definition properties with others. For instance:
|
|
39
|
+
*
|
|
40
|
+
* For:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* const definitions = {
|
|
44
|
+
* { language: 'php', class: 'language-php', label: 'PHP' },
|
|
45
|
+
* { language: 'javascript', class: 'js', label: 'JavaScript' },
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* getPropertyAssociation( definitions, 'class', 'language' );
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* returns:
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* {
|
|
55
|
+
* 'language-php': 'php',
|
|
56
|
+
* 'js': 'javascript'
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* and
|
|
61
|
+
*
|
|
62
|
+
* ```ts
|
|
63
|
+
* getPropertyAssociation( definitions, 'language', 'label' );
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* returns:
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* {
|
|
70
|
+
* 'php': 'PHP',
|
|
71
|
+
* 'javascript': 'JavaScript'
|
|
72
|
+
* }
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
function getPropertyAssociation(languageDefs, key, value) {
|
|
78
|
+
const association = {};
|
|
79
|
+
for (const def of languageDefs) if (key === "class") {
|
|
80
|
+
const newKey = def[key].split(" ").shift();
|
|
81
|
+
association[newKey] = def[value];
|
|
82
|
+
} else association[def[key]] = def[value];
|
|
83
|
+
return association;
|
|
87
84
|
}
|
|
88
85
|
/**
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
86
|
+
* For a given model text node, it returns white spaces that precede other characters in that node.
|
|
87
|
+
* This corresponds to the indentation part of the code block line.
|
|
88
|
+
*
|
|
89
|
+
* @internal
|
|
90
|
+
*/
|
|
91
|
+
function getLeadingWhiteSpaces(textNode) {
|
|
92
|
+
return textNode.data.match(/^(\s*)/)[0];
|
|
95
93
|
}
|
|
96
94
|
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return fragment;
|
|
95
|
+
* For plain text containing the code (a snippet), it returns a document fragment containing
|
|
96
|
+
* view text nodes separated by `<br>` elements (in place of new line characters "\n"), for instance:
|
|
97
|
+
*
|
|
98
|
+
* Input:
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* "foo()\n
|
|
102
|
+
* bar()"
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Output:
|
|
106
|
+
*
|
|
107
|
+
* ```html
|
|
108
|
+
* <DocumentFragment>
|
|
109
|
+
* "foo()"
|
|
110
|
+
* <br/>
|
|
111
|
+
* "bar()"
|
|
112
|
+
* </DocumentFragment>
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @param text The raw code text to be converted.
|
|
116
|
+
* @internal
|
|
117
|
+
*/
|
|
118
|
+
function rawSnippetTextToViewDocumentFragment(writer, text) {
|
|
119
|
+
const fragment = writer.createDocumentFragment();
|
|
120
|
+
const textLines = text.split("\n");
|
|
121
|
+
const items = textLines.reduce((nodes, line, lineIndex) => {
|
|
122
|
+
nodes.push(line);
|
|
123
|
+
if (lineIndex < textLines.length - 1) nodes.push(writer.createElement("br"));
|
|
124
|
+
return nodes;
|
|
125
|
+
}, []);
|
|
126
|
+
writer.appendChild(items, fragment);
|
|
127
|
+
return fragment;
|
|
131
128
|
}
|
|
132
129
|
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const parent = node.parent;
|
|
190
|
-
if (!parent.is('element', 'codeBlock') || node.is('element', 'softBreak')) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
// For each item in code block, move backwards until the beginning of the line it is in is found.
|
|
194
|
-
while(node.previousSibling && !node.previousSibling.is('element', 'softBreak')){
|
|
195
|
-
node = node.previousSibling;
|
|
196
|
-
}
|
|
197
|
-
// Take the leading white spaces into account (only for text nodes).
|
|
198
|
-
const startOffset = !node.is('$text') ? node.startOffset : node.startOffset + getLeadingWhiteSpaces(node).length;
|
|
199
|
-
const position = model.createPositionAt(parent, startOffset);
|
|
200
|
-
// Do not add the same position twice. Unfortunately using set doesn't deduplicate positions because
|
|
201
|
-
// they are different objects.
|
|
202
|
-
if (positions.every((pos)=>!pos.isEqual(position))) {
|
|
203
|
-
positions.push(position);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return positions;
|
|
130
|
+
* Returns an array of all model positions within the selection that represent code block lines.
|
|
131
|
+
*
|
|
132
|
+
* If the selection is collapsed, it returns the exact selection anchor position:
|
|
133
|
+
*
|
|
134
|
+
* ```html
|
|
135
|
+
* <codeBlock>[]foo</codeBlock> -> <codeBlock>^foo</codeBlock>
|
|
136
|
+
* <codeBlock>foo[]bar</codeBlock> -> <codeBlock>foo^bar</codeBlock>
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* Otherwise, it returns positions **before** each text node belonging to all code blocks contained by the selection:
|
|
140
|
+
*
|
|
141
|
+
* ```html
|
|
142
|
+
* <codeBlock> <codeBlock>
|
|
143
|
+
* foo[bar ^foobar
|
|
144
|
+
* <softBreak></softBreak> -> <softBreak></softBreak>
|
|
145
|
+
* baz]qux ^bazqux
|
|
146
|
+
* </codeBlock> </codeBlock>
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* It also works across other non–code blocks:
|
|
150
|
+
*
|
|
151
|
+
* ```html
|
|
152
|
+
* <codeBlock> <codeBlock>
|
|
153
|
+
* foo[bar ^foobar
|
|
154
|
+
* </codeBlock> </codeBlock>
|
|
155
|
+
* <paragraph>text</paragraph> -> <paragraph>text</paragraph>
|
|
156
|
+
* <codeBlock> <codeBlock>
|
|
157
|
+
* baz]qux ^bazqux
|
|
158
|
+
* </codeBlock> </codeBlock>
|
|
159
|
+
* ```
|
|
160
|
+
*
|
|
161
|
+
* **Note:** The positions are in reverse order so they do not get outdated when iterating over them and
|
|
162
|
+
* the writer inserts or removes elements at the same time.
|
|
163
|
+
*
|
|
164
|
+
* **Note:** The position is located after the leading white spaces in the text node.
|
|
165
|
+
*
|
|
166
|
+
* @internal
|
|
167
|
+
*/
|
|
168
|
+
function getIndentOutdentPositions(model) {
|
|
169
|
+
const selection = model.document.selection;
|
|
170
|
+
const positions = [];
|
|
171
|
+
if (selection.isCollapsed) return [selection.anchor];
|
|
172
|
+
const walker = selection.getFirstRange().getWalker({
|
|
173
|
+
ignoreElementEnd: true,
|
|
174
|
+
direction: "backward"
|
|
175
|
+
});
|
|
176
|
+
for (const { item } of walker) {
|
|
177
|
+
let node = item.is("$textProxy") ? item.textNode : item;
|
|
178
|
+
const parent = node.parent;
|
|
179
|
+
if (!parent.is("element", "codeBlock") || node.is("element", "softBreak")) continue;
|
|
180
|
+
while (node.previousSibling && !node.previousSibling.is("element", "softBreak")) node = node.previousSibling;
|
|
181
|
+
const startOffset = !node.is("$text") ? node.startOffset : node.startOffset + getLeadingWhiteSpaces(node).length;
|
|
182
|
+
const position = model.createPositionAt(parent, startOffset);
|
|
183
|
+
if (positions.every((pos) => !pos.isEqual(position))) positions.push(position);
|
|
184
|
+
}
|
|
185
|
+
return positions;
|
|
207
186
|
}
|
|
208
187
|
/**
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
188
|
+
* Checks if any of the blocks within the model selection is a code block.
|
|
189
|
+
*
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
function isModelSelectionInCodeBlock(selection) {
|
|
193
|
+
const firstBlock = first(selection.getSelectedBlocks());
|
|
194
|
+
return !!firstBlock && firstBlock.is("element", "codeBlock");
|
|
215
195
|
}
|
|
216
196
|
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return schema.checkChild(element.parent, 'codeBlock');
|
|
197
|
+
* Checks if an {@link module:engine/model/element~ModelElement Element} can become a code block.
|
|
198
|
+
*
|
|
199
|
+
* @param schema Model's schema.
|
|
200
|
+
* @param element The element to be checked.
|
|
201
|
+
* @returns Check result.
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
function canBeCodeBlock(schema, element) {
|
|
205
|
+
if (element.is("rootElement") || schema.isLimit(element)) return false;
|
|
206
|
+
return schema.checkChild(element.parent, "codeBlock");
|
|
228
207
|
}
|
|
229
208
|
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return t('Entering code snippet');
|
|
245
|
-
}
|
|
246
|
-
return t('Leaving code snippet');
|
|
209
|
+
* Get the translated message read by the screen reader when you enter or exit an element with your cursor.
|
|
210
|
+
*
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
function getCodeBlockAriaAnnouncement(t, languageDefs, element, direction) {
|
|
214
|
+
const languagesToLabels = getPropertyAssociation(languageDefs, "language", "label");
|
|
215
|
+
const codeBlockLanguage = element.getAttribute("language");
|
|
216
|
+
if (codeBlockLanguage in languagesToLabels) {
|
|
217
|
+
const language = languagesToLabels[codeBlockLanguage];
|
|
218
|
+
if (direction === "enter") return t("Entering %0 code snippet", language);
|
|
219
|
+
return t("Leaving %0 code snippet", language);
|
|
220
|
+
}
|
|
221
|
+
if (direction === "enter") return t("Entering code snippet");
|
|
222
|
+
return t("Leaving code snippet");
|
|
247
223
|
}
|
|
248
224
|
/**
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
while(position.nodeBefore && !position.nodeBefore.is('element', 'softBreak')){
|
|
289
|
-
position = model.createPositionBefore(position.nodeBefore);
|
|
290
|
-
}
|
|
291
|
-
// Now, the position is at the beginning of a line.
|
|
292
|
-
// Return a text node after the position, if there is one.
|
|
293
|
-
const nodeAtStart = position.nodeAfter;
|
|
294
|
-
return nodeAtStart && nodeAtStart.is('$text') ? nodeAtStart : null;
|
|
225
|
+
* For given position, finds the closest position that is at the beginning of a line of code and returns a text node that is at the
|
|
226
|
+
* beginning of the line (or `null` if there's no text node at the beginning of a given line).
|
|
227
|
+
*
|
|
228
|
+
* Line beings at the start of a code block element and after each `softBreak` element.
|
|
229
|
+
*
|
|
230
|
+
* Note: even though code block doesn't allow inline elements other than `<softBreak>` by default, some features may overwrite this rule,
|
|
231
|
+
* so such inline elements are taken into account.
|
|
232
|
+
*
|
|
233
|
+
* Some examples of expected results:
|
|
234
|
+
*
|
|
235
|
+
* ```
|
|
236
|
+
* <codeBlock>^</codeBlock> -> null
|
|
237
|
+
* <codeBlock>^foobar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
|
|
238
|
+
* <codeBlock>foobar^</codeBlock> -> <codeBlock>[foobar]</codeBlock>
|
|
239
|
+
* <codeBlock>foo^bar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
|
|
240
|
+
* <codeBlock>foo^<softBreak />bar</codeBlock> -> <codeBlock>[foo]<softBreak />bar</codeBlock>
|
|
241
|
+
* <codeBlock>foo<softBreak />bar^</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
|
|
242
|
+
* <codeBlock>foo<softBreak />b^ar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
|
|
243
|
+
* <codeBlock>foo<softBreak />^bar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
|
|
244
|
+
* <codeBlock>^<element /></codeBlock> -> null
|
|
245
|
+
* <codeBlock><element />^</codeBlock> -> null
|
|
246
|
+
* <codeBlock>foo^<element /></codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
|
|
247
|
+
* <codeBlock>foo<element />^</codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
|
|
248
|
+
* <codeBlock>foo<element />bar^</codeBlock> -> <codeBlock>[foo]<element />bar</codeBlock>
|
|
249
|
+
* <codeBlock><element />bar^</codeBlock> -> null
|
|
250
|
+
* <codeBlock>foo<softBreak />^<softBreak /></codeBlock> -> null
|
|
251
|
+
* <codeBlock>foo<softBreak />^<element /></codeBlock> -> null
|
|
252
|
+
* <codeBlock>foo<softBreak /><element />^</codeBlock> -> null
|
|
253
|
+
* <codeBlock>foo<softBreak />bar<element />^</codeBlock> -> <codeBlock>foo<softBreak />[bar]<element /></codeBlock>
|
|
254
|
+
* <codeBlock>foo<softBreak /><element />ba^r</codeBlock> -> null
|
|
255
|
+
* ```
|
|
256
|
+
*
|
|
257
|
+
* @internal
|
|
258
|
+
*/
|
|
259
|
+
function getTextNodeAtLineStart(position, model) {
|
|
260
|
+
if (position.textNode) position = model.createPositionBefore(position.textNode);
|
|
261
|
+
while (position.nodeBefore && !position.nodeBefore.is("element", "softBreak")) position = model.createPositionBefore(position.nodeBefore);
|
|
262
|
+
const nodeAtStart = position.nodeAfter;
|
|
263
|
+
return nodeAtStart && nodeAtStart.is("$text") ? nodeAtStart : null;
|
|
295
264
|
}
|
|
296
265
|
|
|
297
266
|
/**
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
267
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
268
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
269
|
+
*/
|
|
270
|
+
/**
|
|
271
|
+
* The code block command plugin.
|
|
272
|
+
*/
|
|
273
|
+
var CodeBlockCommand = class extends Command {
|
|
274
|
+
/**
|
|
275
|
+
* Contains the last used language.
|
|
276
|
+
*/
|
|
277
|
+
_lastLanguage;
|
|
278
|
+
/**
|
|
279
|
+
* @inheritDoc
|
|
280
|
+
*/
|
|
281
|
+
constructor(editor) {
|
|
282
|
+
super(editor);
|
|
283
|
+
this._lastLanguage = null;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* @inheritDoc
|
|
287
|
+
*/
|
|
288
|
+
refresh() {
|
|
289
|
+
this.value = this._getValue();
|
|
290
|
+
this.isEnabled = this._checkEnabled();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Executes the command. When the command {@link #value is on}, all topmost code blocks within
|
|
294
|
+
* the selection will be removed. If it is off, all selected blocks will be flattened and
|
|
295
|
+
* wrapped by a code block.
|
|
296
|
+
*
|
|
297
|
+
* @fires execute
|
|
298
|
+
* @param options Command options.
|
|
299
|
+
* @param options.language The code block language.
|
|
300
|
+
* @param options.forceValue If set, it will force the command behavior. If `true`, the command will apply a code block,
|
|
301
|
+
* otherwise the command will remove the code block. If not set, the command will act basing on its current value.
|
|
302
|
+
* @param options.usePreviousLanguageChoice If set on `true` and the `options.language` is not specified, the command
|
|
303
|
+
* will apply the previous language (if the command was already executed) when inserting the `codeBlock` element.
|
|
304
|
+
*/
|
|
305
|
+
execute(options = {}) {
|
|
306
|
+
const editor = this.editor;
|
|
307
|
+
const model = editor.model;
|
|
308
|
+
const selection = model.document.selection;
|
|
309
|
+
const firstLanguageInConfig = getNormalizedAndLocalizedLanguageDefinitions(editor)[0];
|
|
310
|
+
const blocks = Array.from(selection.getSelectedBlocks());
|
|
311
|
+
const value = options.forceValue == void 0 ? !this.value : options.forceValue;
|
|
312
|
+
const language = getLanguage(options, this._lastLanguage, firstLanguageInConfig.language);
|
|
313
|
+
model.change((writer) => {
|
|
314
|
+
if (value) this._applyCodeBlock(writer, blocks, language);
|
|
315
|
+
else this._removeCodeBlock(writer, blocks);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Checks the command's {@link #value}.
|
|
320
|
+
*
|
|
321
|
+
* @returns The current value.
|
|
322
|
+
*/
|
|
323
|
+
_getValue() {
|
|
324
|
+
const selection = this.editor.model.document.selection;
|
|
325
|
+
const firstBlock = first(selection.getSelectedBlocks());
|
|
326
|
+
return !!firstBlock?.is("element", "codeBlock") ? firstBlock.getAttribute("language") : false;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Checks whether the command can be enabled in the current context.
|
|
330
|
+
*
|
|
331
|
+
* @returns Whether the command should be enabled.
|
|
332
|
+
*/
|
|
333
|
+
_checkEnabled() {
|
|
334
|
+
if (this.value) return true;
|
|
335
|
+
const selection = this.editor.model.document.selection;
|
|
336
|
+
const schema = this.editor.model.schema;
|
|
337
|
+
const firstBlock = first(selection.getSelectedBlocks());
|
|
338
|
+
if (!firstBlock) return false;
|
|
339
|
+
return canBeCodeBlock(schema, firstBlock);
|
|
340
|
+
}
|
|
341
|
+
_applyCodeBlock(writer, blocks, language) {
|
|
342
|
+
this._lastLanguage = language;
|
|
343
|
+
const schema = this.editor.model.schema;
|
|
344
|
+
const allowedBlocks = blocks.filter((block) => canBeCodeBlock(schema, block));
|
|
345
|
+
for (const block of allowedBlocks) {
|
|
346
|
+
writer.rename(block, "codeBlock");
|
|
347
|
+
writer.setAttribute("language", language, block);
|
|
348
|
+
schema.removeDisallowedAttributes([block], writer);
|
|
349
|
+
Array.from(block.getChildren()).filter((child) => !schema.checkChild(block, child)).forEach((child) => writer.remove(child));
|
|
350
|
+
}
|
|
351
|
+
allowedBlocks.reverse().forEach((currentBlock, i) => {
|
|
352
|
+
const nextBlock = allowedBlocks[i + 1];
|
|
353
|
+
if (currentBlock.previousSibling === nextBlock) {
|
|
354
|
+
writer.appendElement("softBreak", nextBlock);
|
|
355
|
+
writer.merge(writer.createPositionBefore(currentBlock));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
_removeCodeBlock(writer, blocks) {
|
|
360
|
+
const codeBlocks = blocks.filter((block) => block.is("element", "codeBlock"));
|
|
361
|
+
for (const block of codeBlocks) {
|
|
362
|
+
const range = writer.createRangeOn(block);
|
|
363
|
+
for (const item of Array.from(range.getItems()).reverse()) if (item.is("element", "softBreak") && item.parent.is("element", "codeBlock")) {
|
|
364
|
+
const { position } = writer.split(writer.createPositionBefore(item));
|
|
365
|
+
const elementAfter = position.nodeAfter;
|
|
366
|
+
writer.rename(elementAfter, "paragraph");
|
|
367
|
+
writer.removeAttribute("language", elementAfter);
|
|
368
|
+
writer.remove(item);
|
|
369
|
+
}
|
|
370
|
+
writer.rename(block, "paragraph");
|
|
371
|
+
writer.removeAttribute("language", block);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
409
375
|
/**
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return lastLanguage;
|
|
420
|
-
}
|
|
421
|
-
return defaultLanguage;
|
|
376
|
+
* Picks the language for the new code block. If any language is passed as an option,
|
|
377
|
+
* it will be returned. Else, if option usePreviousLanguageChoice is true and some
|
|
378
|
+
* code block was already created (lastLanguage is not null) then previously used
|
|
379
|
+
* language will be returned. If not, it will return default language.
|
|
380
|
+
*/
|
|
381
|
+
function getLanguage(options, lastLanguage, defaultLanguage) {
|
|
382
|
+
if (options.language) return options.language;
|
|
383
|
+
if (options.usePreviousLanguageChoice && lastLanguage) return lastLanguage;
|
|
384
|
+
return defaultLanguage;
|
|
422
385
|
}
|
|
423
386
|
|
|
424
387
|
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
// Previously insertion was done by writer.insertText(). It was changed to insertContent() to enable
|
|
474
|
-
// integration of code block with track changes. It's the easiest way of integration because insertContent()
|
|
475
|
-
// is already integrated with track changes, but if it ever cause any troubles it can be reverted, however
|
|
476
|
-
// some additional work will be required in track changes integration of code block.
|
|
477
|
-
model.insertContent(indentSequenceTextElement, position);
|
|
478
|
-
}
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
|
-
/**
|
|
482
|
-
* Checks whether the command can be enabled in the current context.
|
|
483
|
-
*/ _checkEnabled() {
|
|
484
|
-
if (!this._indentSequence) {
|
|
485
|
-
return false;
|
|
486
|
-
}
|
|
487
|
-
// Indent (forward) command is always enabled when there's any code block in the selection
|
|
488
|
-
// because you can always indent code lines.
|
|
489
|
-
return isModelSelectionInCodeBlock(this.editor.model.document.selection);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
388
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
389
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
390
|
+
*/
|
|
391
|
+
/**
|
|
392
|
+
* @module code-block/indentcodeblockcommand
|
|
393
|
+
*/
|
|
394
|
+
/**
|
|
395
|
+
* The code block indentation increase command plugin.
|
|
396
|
+
*/
|
|
397
|
+
var IndentCodeBlockCommand = class extends Command {
|
|
398
|
+
/**
|
|
399
|
+
* A sequence of characters added to the line when the command is executed.
|
|
400
|
+
*/
|
|
401
|
+
_indentSequence;
|
|
402
|
+
constructor(editor) {
|
|
403
|
+
super(editor);
|
|
404
|
+
this._indentSequence = editor.config.get("codeBlock.indentSequence");
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* @inheritDoc
|
|
408
|
+
*/
|
|
409
|
+
refresh() {
|
|
410
|
+
this.isEnabled = this._checkEnabled();
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Executes the command. When the command {@link #isEnabled is enabled}, the indentation of the
|
|
414
|
+
* code lines in the selection will be increased.
|
|
415
|
+
*
|
|
416
|
+
* @fires execute
|
|
417
|
+
*/
|
|
418
|
+
execute() {
|
|
419
|
+
const model = this.editor.model;
|
|
420
|
+
model.change((writer) => {
|
|
421
|
+
const positions = getIndentOutdentPositions(model);
|
|
422
|
+
for (const position of positions) {
|
|
423
|
+
const indentSequenceTextElement = writer.createText(this._indentSequence);
|
|
424
|
+
model.insertContent(indentSequenceTextElement, position);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Checks whether the command can be enabled in the current context.
|
|
430
|
+
*/
|
|
431
|
+
_checkEnabled() {
|
|
432
|
+
if (!this._indentSequence) return false;
|
|
433
|
+
return isModelSelectionInCodeBlock(this.editor.model.document.selection);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
492
436
|
|
|
493
437
|
/**
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
model.deleteContent(model.createSelection(range));
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Checks whether the command can be enabled in the current context.
|
|
553
|
-
*
|
|
554
|
-
* @private
|
|
555
|
-
* @returns {Boolean} Whether the command should be enabled.
|
|
556
|
-
*/ _checkEnabled() {
|
|
557
|
-
if (!this._indentSequence) {
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
const model = this.editor.model;
|
|
561
|
-
if (!isModelSelectionInCodeBlock(model.document.selection)) {
|
|
562
|
-
return false;
|
|
563
|
-
}
|
|
564
|
-
// Outdent command can execute only when there is an indent character sequence
|
|
565
|
-
// in some of the lines.
|
|
566
|
-
return getIndentOutdentPositions(model).some((position)=>{
|
|
567
|
-
return getLastOutdentableSequenceRange(model, position, this._indentSequence);
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
// For a position coming from `getIndentOutdentPositions()`, it returns the range representing
|
|
572
|
-
// the last occurrence of the indent sequence among the leading whitespaces of the code line the
|
|
573
|
-
// position represents.
|
|
574
|
-
//
|
|
575
|
-
// For instance, assuming the indent sequence is 4x space (" "):
|
|
576
|
-
//
|
|
577
|
-
// <codeBlock>foo^</codeBlock> -> null
|
|
578
|
-
// <codeBlock>foo^<softBreak></softBreak>bar</codeBlock> -> null
|
|
579
|
-
// <codeBlock> ^foo</codeBlock> -> null
|
|
580
|
-
// <codeBlock> ^foo</codeBlock> -> <codeBlock> [ ]foo</codeBlock>
|
|
581
|
-
// <codeBlock> ^foo bar</codeBlock> -> <codeBlock>[ ]foo bar</codeBlock>
|
|
582
|
-
//
|
|
583
|
-
// @param {<module:engine/model/model~Model>} model
|
|
584
|
-
// @param {<module:engine/model/position~ModelPosition>} position
|
|
585
|
-
// @param {String} sequence
|
|
586
|
-
// @returns {<module:engine/model/range~ModelRange>|null}
|
|
438
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
439
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
440
|
+
*/
|
|
441
|
+
/**
|
|
442
|
+
* The code block indentation decrease command plugin.
|
|
443
|
+
*/
|
|
444
|
+
var OutdentCodeBlockCommand = class extends Command {
|
|
445
|
+
/**
|
|
446
|
+
* A sequence of characters removed from the line when the command is executed.
|
|
447
|
+
*/
|
|
448
|
+
_indentSequence;
|
|
449
|
+
constructor(editor) {
|
|
450
|
+
super(editor);
|
|
451
|
+
this._indentSequence = editor.config.get("codeBlock.indentSequence");
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* @inheritDoc
|
|
455
|
+
*/
|
|
456
|
+
refresh() {
|
|
457
|
+
this.isEnabled = this._checkEnabled();
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Executes the command. When the command {@link #isEnabled is enabled}, the indentation of the
|
|
461
|
+
* code lines in the selection will be decreased.
|
|
462
|
+
*
|
|
463
|
+
* @fires execute
|
|
464
|
+
*/
|
|
465
|
+
execute() {
|
|
466
|
+
const model = this.editor.model;
|
|
467
|
+
model.change(() => {
|
|
468
|
+
const positions = getIndentOutdentPositions(model);
|
|
469
|
+
for (const position of positions) {
|
|
470
|
+
const range = getLastOutdentableSequenceRange(model, position, this._indentSequence);
|
|
471
|
+
if (range) model.deleteContent(model.createSelection(range));
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Checks whether the command can be enabled in the current context.
|
|
477
|
+
*
|
|
478
|
+
* @private
|
|
479
|
+
* @returns {Boolean} Whether the command should be enabled.
|
|
480
|
+
*/
|
|
481
|
+
_checkEnabled() {
|
|
482
|
+
if (!this._indentSequence) return false;
|
|
483
|
+
const model = this.editor.model;
|
|
484
|
+
if (!isModelSelectionInCodeBlock(model.document.selection)) return false;
|
|
485
|
+
return getIndentOutdentPositions(model).some((position) => {
|
|
486
|
+
return getLastOutdentableSequenceRange(model, position, this._indentSequence);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
};
|
|
587
490
|
function getLastOutdentableSequenceRange(model, position, sequence) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
//
|
|
597
|
-
// <codeBlock> ^foo</codeBlock> -> null
|
|
598
|
-
//
|
|
599
|
-
if (lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length) {
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
602
|
-
// For instance, assuming the indent sequence is 4x space (" "):
|
|
603
|
-
//
|
|
604
|
-
// <codeBlock> ^foo</codeBlock> -> null
|
|
605
|
-
//
|
|
606
|
-
if (lastIndexOfSequence === -1) {
|
|
607
|
-
return null;
|
|
608
|
-
}
|
|
609
|
-
const { parent, startOffset } = nodeAtPosition;
|
|
610
|
-
// Create a range that contains the **last** indent sequence among the leading whitespaces
|
|
611
|
-
// of the line.
|
|
612
|
-
//
|
|
613
|
-
// For instance, assuming the indent sequence is 4x space (" "):
|
|
614
|
-
//
|
|
615
|
-
// <codeBlock> ^foo</codeBlock> -> <codeBlock> [ ]foo</codeBlock>
|
|
616
|
-
//
|
|
617
|
-
return model.createRange(model.createPositionAt(parent, startOffset + lastIndexOfSequence), model.createPositionAt(parent, startOffset + lastIndexOfSequence + sequence.length));
|
|
491
|
+
const nodeAtPosition = getTextNodeAtLineStart(position, model);
|
|
492
|
+
if (!nodeAtPosition) return null;
|
|
493
|
+
const leadingWhiteSpaces = getLeadingWhiteSpaces(nodeAtPosition);
|
|
494
|
+
const lastIndexOfSequence = leadingWhiteSpaces.lastIndexOf(sequence);
|
|
495
|
+
if (lastIndexOfSequence + sequence.length !== leadingWhiteSpaces.length) return null;
|
|
496
|
+
if (lastIndexOfSequence === -1) return null;
|
|
497
|
+
const { parent, startOffset } = nodeAtPosition;
|
|
498
|
+
return model.createRange(model.createPositionAt(parent, startOffset + lastIndexOfSequence), model.createPositionAt(parent, startOffset + lastIndexOfSequence + sequence.length));
|
|
618
499
|
}
|
|
619
500
|
|
|
620
501
|
/**
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
const codeBlockLanguage = data.item.getAttribute('language');
|
|
671
|
-
const targetViewPosition = mapper.toViewPosition(model.createPositionBefore(data.item));
|
|
672
|
-
const preAttributes = {};
|
|
673
|
-
// Attributes added only in the editing view.
|
|
674
|
-
if (useLabels) {
|
|
675
|
-
preAttributes['data-language'] = languagesToLabels[codeBlockLanguage];
|
|
676
|
-
preAttributes.spellcheck = 'false';
|
|
677
|
-
}
|
|
678
|
-
const codeAttributes = languagesToClasses[codeBlockLanguage] ? {
|
|
679
|
-
class: languagesToClasses[codeBlockLanguage]
|
|
680
|
-
} : undefined;
|
|
681
|
-
const code = writer.createContainerElement('code', codeAttributes);
|
|
682
|
-
const pre = writer.createContainerElement('pre', preAttributes, code);
|
|
683
|
-
writer.insert(targetViewPosition, pre);
|
|
684
|
-
mapper.bindElements(data.item, code);
|
|
685
|
-
};
|
|
502
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
503
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
504
|
+
*/
|
|
505
|
+
/**
|
|
506
|
+
* A model-to-view (both editing and data) converter for the `codeBlock` element.
|
|
507
|
+
*
|
|
508
|
+
* Sample input:
|
|
509
|
+
*
|
|
510
|
+
* ```html
|
|
511
|
+
* <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
|
|
512
|
+
* ```
|
|
513
|
+
*
|
|
514
|
+
* Sample output (editing):
|
|
515
|
+
*
|
|
516
|
+
* ```html
|
|
517
|
+
* <pre data-language="JavaScript"><code class="language-javascript">foo();<br />bar();</code></pre>
|
|
518
|
+
* ```
|
|
519
|
+
*
|
|
520
|
+
* Sample output (data, see {@link module:code-block/converters~modelToDataViewSoftBreakInsertion}):
|
|
521
|
+
*
|
|
522
|
+
* ```html
|
|
523
|
+
* <pre><code class="language-javascript">foo();\nbar();</code></pre>
|
|
524
|
+
* ```
|
|
525
|
+
*
|
|
526
|
+
* @param languageDefs The normalized language configuration passed to the feature.
|
|
527
|
+
* @param useLabels When `true`, the `<pre>` element will get a `data-language` attribute with a
|
|
528
|
+
* human–readable label of the language. Used only in the editing.
|
|
529
|
+
* @returns Returns a conversion callback.
|
|
530
|
+
* @internal
|
|
531
|
+
*/
|
|
532
|
+
function modelToViewCodeBlockInsertion(model, languageDefs, useLabels = false) {
|
|
533
|
+
const languagesToClasses = getPropertyAssociation(languageDefs, "language", "class");
|
|
534
|
+
const languagesToLabels = getPropertyAssociation(languageDefs, "language", "label");
|
|
535
|
+
return (evt, data, conversionApi) => {
|
|
536
|
+
const { writer, mapper, consumable } = conversionApi;
|
|
537
|
+
if (!consumable.consume(data.item, "insert")) return;
|
|
538
|
+
const codeBlockLanguage = data.item.getAttribute("language");
|
|
539
|
+
const targetViewPosition = mapper.toViewPosition(model.createPositionBefore(data.item));
|
|
540
|
+
const preAttributes = {};
|
|
541
|
+
if (useLabels) {
|
|
542
|
+
preAttributes["data-language"] = languagesToLabels[codeBlockLanguage];
|
|
543
|
+
preAttributes.spellcheck = "false";
|
|
544
|
+
}
|
|
545
|
+
const codeAttributes = languagesToClasses[codeBlockLanguage] ? { class: languagesToClasses[codeBlockLanguage] } : void 0;
|
|
546
|
+
const code = writer.createContainerElement("code", codeAttributes);
|
|
547
|
+
const pre = writer.createContainerElement("pre", preAttributes, code);
|
|
548
|
+
writer.insert(targetViewPosition, pre);
|
|
549
|
+
mapper.bindElements(data.item, code);
|
|
550
|
+
};
|
|
686
551
|
}
|
|
687
552
|
/**
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const position = mapper.toViewPosition(model.createPositionBefore(data.item));
|
|
714
|
-
writer.insert(position, writer.createText('\n'));
|
|
715
|
-
};
|
|
553
|
+
* A model-to-data view converter for the new line (`softBreak`) separator.
|
|
554
|
+
*
|
|
555
|
+
* Sample input:
|
|
556
|
+
*
|
|
557
|
+
* ```html
|
|
558
|
+
* <codeBlock ...>foo();<softBreak></softBreak>bar();</codeBlock>
|
|
559
|
+
* ```
|
|
560
|
+
*
|
|
561
|
+
* Sample output:
|
|
562
|
+
*
|
|
563
|
+
* ```html
|
|
564
|
+
* <pre><code ...>foo();\nbar();</code></pre>
|
|
565
|
+
* ```
|
|
566
|
+
*
|
|
567
|
+
* @returns Returns a conversion callback.
|
|
568
|
+
* @internal
|
|
569
|
+
*/
|
|
570
|
+
function modelToDataViewSoftBreakInsertion(model) {
|
|
571
|
+
return (evt, data, conversionApi) => {
|
|
572
|
+
if (data.item.parent.name !== "codeBlock") return;
|
|
573
|
+
const { writer, mapper, consumable } = conversionApi;
|
|
574
|
+
if (!consumable.consume(data.item, "insert")) return;
|
|
575
|
+
const position = mapper.toViewPosition(model.createPositionBefore(data.item));
|
|
576
|
+
writer.insert(position, writer.createText("\n"));
|
|
577
|
+
};
|
|
716
578
|
}
|
|
717
579
|
/**
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
const viewChildClasses = [
|
|
764
|
-
...viewCodeElement.getClassNames()
|
|
765
|
-
];
|
|
766
|
-
// As we're to associate each class with a model language, a lack of class (empty class) can be
|
|
767
|
-
// also associated with a language if the language definition was configured so. Pushing an empty
|
|
768
|
-
// string to make sure the association will work.
|
|
769
|
-
if (!viewChildClasses.length) {
|
|
770
|
-
viewChildClasses.push('');
|
|
771
|
-
}
|
|
772
|
-
// Figure out if any of the <code> element's class names is a valid programming
|
|
773
|
-
// language class. If so, use it on the model element (becomes the language of the entire block).
|
|
774
|
-
for (const className of viewChildClasses){
|
|
775
|
-
const language = classesToLanguages[className];
|
|
776
|
-
if (language) {
|
|
777
|
-
consumable.consume(viewCodeElement, {
|
|
778
|
-
classes: [
|
|
779
|
-
className
|
|
780
|
-
]
|
|
781
|
-
});
|
|
782
|
-
writer.setAttribute('language', language, codeBlock);
|
|
783
|
-
break;
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
// If no language value was set, use the default language from the config.
|
|
787
|
-
if (!codeBlock.hasAttribute('language')) {
|
|
788
|
-
writer.setAttribute('language', defaultLanguageName, codeBlock);
|
|
789
|
-
}
|
|
790
|
-
// Convert children before inserting the code block element
|
|
791
|
-
// to make sure that code block won't be splitted by any block.
|
|
792
|
-
conversionApi.convertChildren(viewCodeElement, codeBlock);
|
|
793
|
-
// Let's try to insert code block.
|
|
794
|
-
if (!conversionApi.safeInsert(codeBlock, data.modelCursor)) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
consumable.consume(viewCodeElement, {
|
|
798
|
-
name: true
|
|
799
|
-
});
|
|
800
|
-
conversionApi.updateConversionResult(codeBlock, data);
|
|
801
|
-
};
|
|
580
|
+
* A view-to-model converter for `<pre>` with the `<code>` HTML.
|
|
581
|
+
*
|
|
582
|
+
* Sample input:
|
|
583
|
+
*
|
|
584
|
+
* ```html
|
|
585
|
+
* <pre><code class="language-javascript">foo();bar();</code></pre>
|
|
586
|
+
* ```
|
|
587
|
+
*
|
|
588
|
+
* Sample output:
|
|
589
|
+
*
|
|
590
|
+
* ```html
|
|
591
|
+
* <codeBlock language="javascript">foo();bar();</codeBlock>
|
|
592
|
+
* ```
|
|
593
|
+
*
|
|
594
|
+
* @param languageDefs The normalized language configuration passed to the feature.
|
|
595
|
+
* @returns Returns a conversion callback.
|
|
596
|
+
* @internal
|
|
597
|
+
*/
|
|
598
|
+
function dataViewToModelCodeBlockInsertion(editingView, languageDefs) {
|
|
599
|
+
const classesToLanguages = getPropertyAssociation(languageDefs, "class", "language");
|
|
600
|
+
const defaultLanguageName = languageDefs[0].language;
|
|
601
|
+
return (evt, data, conversionApi) => {
|
|
602
|
+
const viewCodeElement = data.viewItem;
|
|
603
|
+
const viewPreElement = viewCodeElement.parent;
|
|
604
|
+
if (!viewPreElement || !viewPreElement.is("element", "pre")) return;
|
|
605
|
+
if (data.modelCursor.findAncestor("codeBlock")) return;
|
|
606
|
+
const { consumable, writer } = conversionApi;
|
|
607
|
+
if (!consumable.test(viewCodeElement, { name: true })) return;
|
|
608
|
+
const codeBlock = writer.createElement("codeBlock");
|
|
609
|
+
const viewChildClasses = [...viewCodeElement.getClassNames()];
|
|
610
|
+
if (!viewChildClasses.length) viewChildClasses.push("");
|
|
611
|
+
for (const className of viewChildClasses) {
|
|
612
|
+
const language = classesToLanguages[className];
|
|
613
|
+
if (language) {
|
|
614
|
+
consumable.consume(viewCodeElement, { classes: [className] });
|
|
615
|
+
writer.setAttribute("language", language, codeBlock);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
if (!codeBlock.hasAttribute("language")) writer.setAttribute("language", defaultLanguageName, codeBlock);
|
|
620
|
+
conversionApi.convertChildren(viewCodeElement, codeBlock);
|
|
621
|
+
if (!conversionApi.safeInsert(codeBlock, data.modelCursor)) return;
|
|
622
|
+
consumable.consume(viewCodeElement, { name: true });
|
|
623
|
+
conversionApi.updateConversionResult(codeBlock, data);
|
|
624
|
+
};
|
|
802
625
|
}
|
|
803
626
|
/**
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
position = writer.createPositionAfter(softBreak);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
data.modelRange = writer.createRange(data.modelCursor, position);
|
|
845
|
-
data.modelCursor = position;
|
|
846
|
-
};
|
|
627
|
+
* A view-to-model converter for new line characters in `<pre>`.
|
|
628
|
+
*
|
|
629
|
+
* Sample input:
|
|
630
|
+
*
|
|
631
|
+
* ```html
|
|
632
|
+
* <pre><code class="language-javascript">foo();\nbar();</code></pre>
|
|
633
|
+
* ```
|
|
634
|
+
*
|
|
635
|
+
* Sample output:
|
|
636
|
+
*
|
|
637
|
+
* ```html
|
|
638
|
+
* <codeBlock language="javascript">foo();<softBreak></softBreak>bar();</codeBlock>
|
|
639
|
+
* ```
|
|
640
|
+
*
|
|
641
|
+
* @returns {Function} Returns a conversion callback.
|
|
642
|
+
* @internal
|
|
643
|
+
*/
|
|
644
|
+
function dataViewToModelTextNewlinesInsertion() {
|
|
645
|
+
return (evt, data, { consumable, writer }) => {
|
|
646
|
+
let position = data.modelCursor;
|
|
647
|
+
if (!consumable.test(data.viewItem)) return;
|
|
648
|
+
if (!position.findAncestor("codeBlock")) return;
|
|
649
|
+
consumable.consume(data.viewItem);
|
|
650
|
+
const textLines = data.viewItem.data.split("\n").map((data) => writer.createText(data));
|
|
651
|
+
const lastLine = textLines[textLines.length - 1];
|
|
652
|
+
for (const node of textLines) {
|
|
653
|
+
writer.insert(node, position);
|
|
654
|
+
position = position.getShiftedBy(node.offsetSize);
|
|
655
|
+
if (node !== lastLine) {
|
|
656
|
+
const softBreak = writer.createElement("softBreak");
|
|
657
|
+
writer.insert(softBreak, position);
|
|
658
|
+
position = writer.createPositionAfter(softBreak);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
data.modelRange = writer.createRange(data.modelCursor, position);
|
|
662
|
+
data.modelCursor = position;
|
|
663
|
+
};
|
|
847
664
|
}
|
|
848
665
|
/**
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
}
|
|
895
|
-
for (const child of preChildren){
|
|
896
|
-
if (child === childCodeElement || !child.is('$text')) {
|
|
897
|
-
continue;
|
|
898
|
-
}
|
|
899
|
-
// Consuming the orphan to remove it from the input data.
|
|
900
|
-
// Second argument in `consumable.consume` is discarded for text nodes.
|
|
901
|
-
consumable.consume(child, {
|
|
902
|
-
name: true
|
|
903
|
-
});
|
|
904
|
-
}
|
|
905
|
-
};
|
|
666
|
+
* A view-to-model converter that handles orphan text nodes (white spaces, new lines, etc.)
|
|
667
|
+
* that surround `<code>` inside `<pre>`.
|
|
668
|
+
*
|
|
669
|
+
* Sample input:
|
|
670
|
+
*
|
|
671
|
+
* ```html
|
|
672
|
+
* // White spaces
|
|
673
|
+
* <pre> <code>foo()</code> </pre>
|
|
674
|
+
*
|
|
675
|
+
* // White spaces
|
|
676
|
+
* <pre> <code>foo()</code> </pre>
|
|
677
|
+
*
|
|
678
|
+
* // White spaces
|
|
679
|
+
* <pre> <code>foo()</code> </pre>
|
|
680
|
+
*
|
|
681
|
+
* // New lines
|
|
682
|
+
* <pre>
|
|
683
|
+
* <code>foo()</code>
|
|
684
|
+
* </pre>
|
|
685
|
+
*
|
|
686
|
+
* // Redundant text
|
|
687
|
+
* <pre>ABC<code>foo()</code>DEF</pre>
|
|
688
|
+
* ```
|
|
689
|
+
*
|
|
690
|
+
* Unified output for each case:
|
|
691
|
+
*
|
|
692
|
+
* ```html
|
|
693
|
+
* <codeBlock language="plaintext">foo()</codeBlock>
|
|
694
|
+
* ```
|
|
695
|
+
*
|
|
696
|
+
* @returns Returns a conversion callback.
|
|
697
|
+
* @internal
|
|
698
|
+
*/
|
|
699
|
+
function dataViewToModelOrphanNodeConsumer() {
|
|
700
|
+
return (evt, data, { consumable }) => {
|
|
701
|
+
const preElement = data.viewItem;
|
|
702
|
+
if (preElement.findAncestor("pre")) return;
|
|
703
|
+
const preChildren = Array.from(preElement.getChildren());
|
|
704
|
+
const childCodeElement = preChildren.find((node) => node.is("element", "code"));
|
|
705
|
+
if (!childCodeElement) return;
|
|
706
|
+
for (const child of preChildren) {
|
|
707
|
+
if (child === childCodeElement || !child.is("$text")) continue;
|
|
708
|
+
consumable.consume(child, { name: true });
|
|
709
|
+
}
|
|
710
|
+
};
|
|
906
711
|
}
|
|
907
712
|
|
|
908
|
-
const DEFAULT_ELEMENT = 'paragraph';
|
|
909
713
|
/**
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
*/ class CodeBlockEditing extends Plugin {
|
|
914
|
-
/**
|
|
915
|
-
* @inheritDoc
|
|
916
|
-
*/ static get pluginName() {
|
|
917
|
-
return 'CodeBlockEditing';
|
|
918
|
-
}
|
|
919
|
-
/**
|
|
920
|
-
* @inheritDoc
|
|
921
|
-
*/ static get isOfficialPlugin() {
|
|
922
|
-
return true;
|
|
923
|
-
}
|
|
924
|
-
/**
|
|
925
|
-
* @inheritDoc
|
|
926
|
-
*/ static get requires() {
|
|
927
|
-
return [
|
|
928
|
-
ShiftEnter
|
|
929
|
-
];
|
|
930
|
-
}
|
|
931
|
-
/**
|
|
932
|
-
* @inheritDoc
|
|
933
|
-
*/ constructor(editor){
|
|
934
|
-
super(editor);
|
|
935
|
-
editor.config.define('codeBlock', {
|
|
936
|
-
languages: [
|
|
937
|
-
{
|
|
938
|
-
language: 'plaintext',
|
|
939
|
-
label: 'Plain text'
|
|
940
|
-
},
|
|
941
|
-
{
|
|
942
|
-
language: 'c',
|
|
943
|
-
label: 'C'
|
|
944
|
-
},
|
|
945
|
-
{
|
|
946
|
-
language: 'cs',
|
|
947
|
-
label: 'C#'
|
|
948
|
-
},
|
|
949
|
-
{
|
|
950
|
-
language: 'cpp',
|
|
951
|
-
label: 'C++'
|
|
952
|
-
},
|
|
953
|
-
{
|
|
954
|
-
language: 'css',
|
|
955
|
-
label: 'CSS'
|
|
956
|
-
},
|
|
957
|
-
{
|
|
958
|
-
language: 'diff',
|
|
959
|
-
label: 'Diff'
|
|
960
|
-
},
|
|
961
|
-
{
|
|
962
|
-
language: 'go',
|
|
963
|
-
label: 'Go'
|
|
964
|
-
},
|
|
965
|
-
{
|
|
966
|
-
language: 'html',
|
|
967
|
-
label: 'HTML'
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
language: 'java',
|
|
971
|
-
label: 'Java'
|
|
972
|
-
},
|
|
973
|
-
{
|
|
974
|
-
language: 'javascript',
|
|
975
|
-
label: 'JavaScript'
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
language: 'php',
|
|
979
|
-
label: 'PHP'
|
|
980
|
-
},
|
|
981
|
-
{
|
|
982
|
-
language: 'python',
|
|
983
|
-
label: 'Python'
|
|
984
|
-
},
|
|
985
|
-
{
|
|
986
|
-
language: 'ruby',
|
|
987
|
-
label: 'Ruby'
|
|
988
|
-
},
|
|
989
|
-
{
|
|
990
|
-
language: 'typescript',
|
|
991
|
-
label: 'TypeScript'
|
|
992
|
-
},
|
|
993
|
-
{
|
|
994
|
-
language: 'xml',
|
|
995
|
-
label: 'XML'
|
|
996
|
-
}
|
|
997
|
-
],
|
|
998
|
-
// A single tab.
|
|
999
|
-
indentSequence: '\t'
|
|
1000
|
-
});
|
|
1001
|
-
}
|
|
1002
|
-
/**
|
|
1003
|
-
* @inheritDoc
|
|
1004
|
-
*/ init() {
|
|
1005
|
-
const editor = this.editor;
|
|
1006
|
-
const schema = editor.model.schema;
|
|
1007
|
-
const model = editor.model;
|
|
1008
|
-
const view = editor.editing.view;
|
|
1009
|
-
const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions(editor);
|
|
1010
|
-
// The main command.
|
|
1011
|
-
editor.commands.add('codeBlock', new CodeBlockCommand(editor));
|
|
1012
|
-
// Commands that change the indentation.
|
|
1013
|
-
editor.commands.add('indentCodeBlock', new IndentCodeBlockCommand(editor));
|
|
1014
|
-
editor.commands.add('outdentCodeBlock', new OutdentCodeBlockCommand(editor));
|
|
1015
|
-
this.listenTo(view.document, 'tab', (evt, data)=>{
|
|
1016
|
-
const commandName = data.shiftKey ? 'outdentCodeBlock' : 'indentCodeBlock';
|
|
1017
|
-
const command = editor.commands.get(commandName);
|
|
1018
|
-
if (!command.isEnabled) {
|
|
1019
|
-
return;
|
|
1020
|
-
}
|
|
1021
|
-
editor.execute(commandName);
|
|
1022
|
-
data.stopPropagation();
|
|
1023
|
-
data.preventDefault();
|
|
1024
|
-
evt.stop();
|
|
1025
|
-
}, {
|
|
1026
|
-
context: 'pre'
|
|
1027
|
-
});
|
|
1028
|
-
schema.register('codeBlock', {
|
|
1029
|
-
allowWhere: '$block',
|
|
1030
|
-
allowChildren: '$text',
|
|
1031
|
-
// Disallow `$inlineObject` and its derivatives like `inlineWidget` inside `codeBlock` to ensure that only text,
|
|
1032
|
-
// not other inline elements like inline images, are allowed. This maintains the semantic integrity of code blocks.
|
|
1033
|
-
disallowChildren: '$inlineObject',
|
|
1034
|
-
allowAttributes: [
|
|
1035
|
-
'language'
|
|
1036
|
-
],
|
|
1037
|
-
allowAttributesOf: '$listItem',
|
|
1038
|
-
isBlock: true
|
|
1039
|
-
});
|
|
1040
|
-
// Disallow formatting attributes on `codeBlock` children.
|
|
1041
|
-
schema.addAttributeCheck((context, attributeName)=>{
|
|
1042
|
-
const parent = context.getItem(context.length - 2);
|
|
1043
|
-
const isFormatting = schema.getAttributeProperties(attributeName).isFormatting;
|
|
1044
|
-
if (isFormatting && parent && parent.name == 'codeBlock') {
|
|
1045
|
-
return false;
|
|
1046
|
-
}
|
|
1047
|
-
});
|
|
1048
|
-
// Conversion.
|
|
1049
|
-
editor.editing.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs, true));
|
|
1050
|
-
editor.data.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs));
|
|
1051
|
-
editor.data.downcastDispatcher.on('insert:softBreak', modelToDataViewSoftBreakInsertion(model), {
|
|
1052
|
-
priority: 'high'
|
|
1053
|
-
});
|
|
1054
|
-
editor.data.upcastDispatcher.on('element:code', dataViewToModelCodeBlockInsertion(view, normalizedLanguagesDefs));
|
|
1055
|
-
editor.data.upcastDispatcher.on('text', dataViewToModelTextNewlinesInsertion());
|
|
1056
|
-
editor.data.upcastDispatcher.on('element:pre', dataViewToModelOrphanNodeConsumer(), {
|
|
1057
|
-
priority: 'high'
|
|
1058
|
-
});
|
|
1059
|
-
// Intercept the clipboard input (paste) when the selection is anchored in the code block and force the clipboard
|
|
1060
|
-
// data to be pasted as a single plain text. Otherwise, the code lines will split the code block and
|
|
1061
|
-
// "spill out" as separate paragraphs.
|
|
1062
|
-
this.listenTo(editor.editing.view.document, 'clipboardInput', (evt, data)=>{
|
|
1063
|
-
let insertionRange = model.createRange(model.document.selection.anchor);
|
|
1064
|
-
// Use target ranges in case this is a drop.
|
|
1065
|
-
if (data.targetRanges) {
|
|
1066
|
-
insertionRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
|
|
1067
|
-
}
|
|
1068
|
-
if (!insertionRange.start.parent.is('element', 'codeBlock')) {
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
const text = data.dataTransfer.getData('text/plain');
|
|
1072
|
-
const writer = new ViewUpcastWriter(editor.editing.view.document);
|
|
1073
|
-
// Pass the view fragment to the default clipboardInput handler.
|
|
1074
|
-
data.content = rawSnippetTextToViewDocumentFragment(writer, text);
|
|
1075
|
-
});
|
|
1076
|
-
if (editor.plugins.has('ClipboardPipeline')) {
|
|
1077
|
-
// Elements may have a plain textual representation (hence be present in the 'text/plain' data transfer),
|
|
1078
|
-
// but not be allowed in the code block.
|
|
1079
|
-
// Filter them out before inserting the content to the model.
|
|
1080
|
-
editor.plugins.get(ClipboardPipeline).on('contentInsertion', (evt, data)=>{
|
|
1081
|
-
const model = editor.model;
|
|
1082
|
-
const selection = model.document.selection;
|
|
1083
|
-
if (!selection.anchor.parent.is('element', 'codeBlock')) {
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
model.change((writer)=>{
|
|
1087
|
-
const contentRange = writer.createRangeIn(data.content);
|
|
1088
|
-
for (const item of [
|
|
1089
|
-
...contentRange.getItems()
|
|
1090
|
-
]){
|
|
1091
|
-
// Remove all nodes disallowed in the code block.
|
|
1092
|
-
if (item.is('node') && !schema.checkChild(selection.anchor, item)) {
|
|
1093
|
-
writer.remove(item);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
});
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
// Make sure multi–line selection is always wrapped in a code block when `getSelectedContent()`
|
|
1100
|
-
// is used (e.g. clipboard copy). Otherwise, only the raw text will be copied to the clipboard and,
|
|
1101
|
-
// upon next paste, this bare text will not be inserted as a code block, which is not the best UX.
|
|
1102
|
-
// Similarly, when the selection in a single line, the selected content should be an inline code
|
|
1103
|
-
// so it can be pasted later on and retain it's preformatted nature.
|
|
1104
|
-
this.listenTo(model, 'getSelectedContent', (evt, [selection])=>{
|
|
1105
|
-
const anchor = selection.anchor;
|
|
1106
|
-
if (selection.isCollapsed || !anchor.parent.is('element', 'codeBlock') || !anchor.hasSameParentAs(selection.focus)) {
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
model.change((writer)=>{
|
|
1110
|
-
const docFragment = evt.return;
|
|
1111
|
-
// fo[o<softBreak></softBreak>b]ar -> <codeBlock language="...">[o<softBreak></softBreak>b]<codeBlock>
|
|
1112
|
-
if (anchor.parent.is('element') && (docFragment.childCount > 1 || selection.containsEntireContent(anchor.parent))) {
|
|
1113
|
-
const codeBlock = writer.createElement('codeBlock', anchor.parent.getAttributes());
|
|
1114
|
-
writer.append(docFragment, codeBlock);
|
|
1115
|
-
const newDocumentFragment = writer.createDocumentFragment();
|
|
1116
|
-
writer.append(codeBlock, newDocumentFragment);
|
|
1117
|
-
evt.return = newDocumentFragment;
|
|
1118
|
-
return;
|
|
1119
|
-
}
|
|
1120
|
-
// "f[oo]" -> <$text code="true">oo</text>
|
|
1121
|
-
const textNode = docFragment.getChild(0);
|
|
1122
|
-
if (schema.checkAttribute(textNode, 'code')) {
|
|
1123
|
-
writer.setAttribute('code', true, textNode);
|
|
1124
|
-
}
|
|
1125
|
-
});
|
|
1126
|
-
});
|
|
1127
|
-
}
|
|
1128
|
-
/**
|
|
1129
|
-
* @inheritDoc
|
|
1130
|
-
*/ afterInit() {
|
|
1131
|
-
const editor = this.editor;
|
|
1132
|
-
const commands = editor.commands;
|
|
1133
|
-
const indent = commands.get('indent');
|
|
1134
|
-
const outdent = commands.get('outdent');
|
|
1135
|
-
if (indent) {
|
|
1136
|
-
// Priority is highest due to integration with `IndentList` command of `List` plugin.
|
|
1137
|
-
// If selection is in a code block we give priority to it. This way list item cannot be indented
|
|
1138
|
-
// but if we would give priority to indenting list item then user would have to indent list item
|
|
1139
|
-
// as much as possible and only then he could indent code block.
|
|
1140
|
-
indent.registerChildCommand(commands.get('indentCodeBlock'), {
|
|
1141
|
-
priority: 'highest'
|
|
1142
|
-
});
|
|
1143
|
-
}
|
|
1144
|
-
if (outdent) {
|
|
1145
|
-
outdent.registerChildCommand(commands.get('outdentCodeBlock'));
|
|
1146
|
-
}
|
|
1147
|
-
// Customize the response to the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd>
|
|
1148
|
-
// key press when the selection is in the code block. Upon enter key press we can either
|
|
1149
|
-
// leave the block if it's "two or three enters" in a row or create a new code block line, preserving
|
|
1150
|
-
// previous line's indentation.
|
|
1151
|
-
this.listenTo(editor.editing.view.document, 'enter', (evt, data)=>{
|
|
1152
|
-
const positionParent = editor.model.document.selection.getLastPosition().parent;
|
|
1153
|
-
if (!positionParent.is('element', 'codeBlock')) {
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
if (!leaveBlockStartOnEnter(editor, data.isSoft) && !leaveBlockEndOnEnter(editor, data.isSoft)) {
|
|
1157
|
-
breakLineOnEnter(editor);
|
|
1158
|
-
}
|
|
1159
|
-
data.preventDefault();
|
|
1160
|
-
evt.stop();
|
|
1161
|
-
}, {
|
|
1162
|
-
context: 'pre'
|
|
1163
|
-
});
|
|
1164
|
-
this._initAriaAnnouncements();
|
|
1165
|
-
}
|
|
1166
|
-
/**
|
|
1167
|
-
* Observe when user enters or leaves code block and set proper aria value in global live announcer.
|
|
1168
|
-
* This allows screen readers to indicate when the user has entered and left the specified code block.
|
|
1169
|
-
*
|
|
1170
|
-
* @internal
|
|
1171
|
-
*/ _initAriaAnnouncements() {
|
|
1172
|
-
const { model, ui, t } = this.editor;
|
|
1173
|
-
const languageDefs = getNormalizedAndLocalizedLanguageDefinitions(this.editor);
|
|
1174
|
-
let lastFocusedCodeBlock = null;
|
|
1175
|
-
model.document.selection.on('change:range', ()=>{
|
|
1176
|
-
const focusParent = model.document.selection.focus.parent;
|
|
1177
|
-
if (!ui || lastFocusedCodeBlock === focusParent || !focusParent.is('element')) {
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
if (lastFocusedCodeBlock && lastFocusedCodeBlock.is('element', 'codeBlock')) {
|
|
1181
|
-
ui.ariaLiveAnnouncer.announce(getCodeBlockAriaAnnouncement(t, languageDefs, lastFocusedCodeBlock, 'leave'));
|
|
1182
|
-
}
|
|
1183
|
-
if (focusParent.is('element', 'codeBlock')) {
|
|
1184
|
-
ui.ariaLiveAnnouncer.announce(getCodeBlockAriaAnnouncement(t, languageDefs, focusParent, 'enter'));
|
|
1185
|
-
}
|
|
1186
|
-
lastFocusedCodeBlock = focusParent;
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
714
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
715
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
716
|
+
*/
|
|
1190
717
|
/**
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
718
|
+
* @module code-block/codeblockediting
|
|
719
|
+
*/
|
|
720
|
+
const DEFAULT_ELEMENT = "paragraph";
|
|
721
|
+
/**
|
|
722
|
+
* The editing part of the code block feature.
|
|
723
|
+
*
|
|
724
|
+
* Introduces the `'codeBlock'` command and the `'codeBlock'` model element.
|
|
725
|
+
*/
|
|
726
|
+
var CodeBlockEditing = class extends Plugin {
|
|
727
|
+
/**
|
|
728
|
+
* @inheritDoc
|
|
729
|
+
*/
|
|
730
|
+
static get pluginName() {
|
|
731
|
+
return "CodeBlockEditing";
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* @inheritDoc
|
|
735
|
+
*/
|
|
736
|
+
static get isOfficialPlugin() {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* @inheritDoc
|
|
741
|
+
*/
|
|
742
|
+
static get requires() {
|
|
743
|
+
return [ShiftEnter];
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* @inheritDoc
|
|
747
|
+
*/
|
|
748
|
+
constructor(editor) {
|
|
749
|
+
super(editor);
|
|
750
|
+
editor.config.define("codeBlock", {
|
|
751
|
+
languages: [
|
|
752
|
+
{
|
|
753
|
+
language: "plaintext",
|
|
754
|
+
label: "Plain text"
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
language: "c",
|
|
758
|
+
label: "C"
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
language: "cs",
|
|
762
|
+
label: "C#"
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
language: "cpp",
|
|
766
|
+
label: "C++"
|
|
767
|
+
},
|
|
768
|
+
{
|
|
769
|
+
language: "css",
|
|
770
|
+
label: "CSS"
|
|
771
|
+
},
|
|
772
|
+
{
|
|
773
|
+
language: "diff",
|
|
774
|
+
label: "Diff"
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
language: "go",
|
|
778
|
+
label: "Go"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
language: "html",
|
|
782
|
+
label: "HTML"
|
|
783
|
+
},
|
|
784
|
+
{
|
|
785
|
+
language: "java",
|
|
786
|
+
label: "Java"
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
language: "javascript",
|
|
790
|
+
label: "JavaScript"
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
language: "php",
|
|
794
|
+
label: "PHP"
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
language: "python",
|
|
798
|
+
label: "Python"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
language: "ruby",
|
|
802
|
+
label: "Ruby"
|
|
803
|
+
},
|
|
804
|
+
{
|
|
805
|
+
language: "typescript",
|
|
806
|
+
label: "TypeScript"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
language: "xml",
|
|
810
|
+
label: "XML"
|
|
811
|
+
}
|
|
812
|
+
],
|
|
813
|
+
indentSequence: " "
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* @inheritDoc
|
|
818
|
+
*/
|
|
819
|
+
init() {
|
|
820
|
+
const editor = this.editor;
|
|
821
|
+
const schema = editor.model.schema;
|
|
822
|
+
const model = editor.model;
|
|
823
|
+
const view = editor.editing.view;
|
|
824
|
+
const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions(editor);
|
|
825
|
+
editor.commands.add("codeBlock", new CodeBlockCommand(editor));
|
|
826
|
+
editor.commands.add("indentCodeBlock", new IndentCodeBlockCommand(editor));
|
|
827
|
+
editor.commands.add("outdentCodeBlock", new OutdentCodeBlockCommand(editor));
|
|
828
|
+
this.listenTo(view.document, "tab", (evt, data) => {
|
|
829
|
+
const commandName = data.shiftKey ? "outdentCodeBlock" : "indentCodeBlock";
|
|
830
|
+
if (!editor.commands.get(commandName).isEnabled) return;
|
|
831
|
+
editor.execute(commandName);
|
|
832
|
+
data.stopPropagation();
|
|
833
|
+
data.preventDefault();
|
|
834
|
+
evt.stop();
|
|
835
|
+
}, { context: "pre" });
|
|
836
|
+
schema.register("codeBlock", {
|
|
837
|
+
allowWhere: "$block",
|
|
838
|
+
allowChildren: "$text",
|
|
839
|
+
disallowChildren: "$inlineObject",
|
|
840
|
+
allowAttributes: ["language"],
|
|
841
|
+
allowAttributesOf: "$listItem",
|
|
842
|
+
isBlock: true
|
|
843
|
+
});
|
|
844
|
+
schema.addAttributeCheck((context, attributeName) => {
|
|
845
|
+
const parent = context.getItem(context.length - 2);
|
|
846
|
+
if (schema.getAttributeProperties(attributeName).isFormatting && parent && parent.name == "codeBlock") return false;
|
|
847
|
+
});
|
|
848
|
+
editor.editing.downcastDispatcher.on("insert:codeBlock", modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs, true));
|
|
849
|
+
editor.data.downcastDispatcher.on("insert:codeBlock", modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs));
|
|
850
|
+
editor.data.downcastDispatcher.on("insert:softBreak", modelToDataViewSoftBreakInsertion(model), { priority: "high" });
|
|
851
|
+
editor.data.upcastDispatcher.on("element:code", dataViewToModelCodeBlockInsertion(view, normalizedLanguagesDefs));
|
|
852
|
+
editor.data.upcastDispatcher.on("text", dataViewToModelTextNewlinesInsertion());
|
|
853
|
+
editor.data.upcastDispatcher.on("element:pre", dataViewToModelOrphanNodeConsumer(), { priority: "high" });
|
|
854
|
+
this.listenTo(editor.editing.view.document, "clipboardInput", (evt, data) => {
|
|
855
|
+
let insertionRange = model.createRange(model.document.selection.anchor);
|
|
856
|
+
if (data.targetRanges) insertionRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
|
|
857
|
+
if (!insertionRange.start.parent.is("element", "codeBlock")) return;
|
|
858
|
+
const text = data.dataTransfer.getData("text/plain");
|
|
859
|
+
data.content = rawSnippetTextToViewDocumentFragment(new ViewUpcastWriter(editor.editing.view.document), text);
|
|
860
|
+
});
|
|
861
|
+
if (editor.plugins.has("ClipboardPipeline")) editor.plugins.get(ClipboardPipeline).on("contentInsertion", (evt, data) => {
|
|
862
|
+
const model = editor.model;
|
|
863
|
+
const selection = model.document.selection;
|
|
864
|
+
if (!selection.anchor.parent.is("element", "codeBlock")) return;
|
|
865
|
+
model.change((writer) => {
|
|
866
|
+
const contentRange = writer.createRangeIn(data.content);
|
|
867
|
+
for (const item of [...contentRange.getItems()]) if (item.is("node") && !schema.checkChild(selection.anchor, item)) writer.remove(item);
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
this.listenTo(model, "getSelectedContent", (evt, [selection]) => {
|
|
871
|
+
const anchor = selection.anchor;
|
|
872
|
+
if (selection.isCollapsed || !anchor.parent.is("element", "codeBlock") || !anchor.hasSameParentAs(selection.focus)) return;
|
|
873
|
+
model.change((writer) => {
|
|
874
|
+
const docFragment = evt.return;
|
|
875
|
+
if (anchor.parent.is("element") && (docFragment.childCount > 1 || selection.containsEntireContent(anchor.parent))) {
|
|
876
|
+
const codeBlock = writer.createElement("codeBlock", anchor.parent.getAttributes());
|
|
877
|
+
writer.append(docFragment, codeBlock);
|
|
878
|
+
const newDocumentFragment = writer.createDocumentFragment();
|
|
879
|
+
writer.append(codeBlock, newDocumentFragment);
|
|
880
|
+
evt.return = newDocumentFragment;
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const textNode = docFragment.getChild(0);
|
|
884
|
+
if (schema.checkAttribute(textNode, "code")) writer.setAttribute("code", true, textNode);
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* @inheritDoc
|
|
890
|
+
*/
|
|
891
|
+
afterInit() {
|
|
892
|
+
const editor = this.editor;
|
|
893
|
+
const commands = editor.commands;
|
|
894
|
+
const indent = commands.get("indent");
|
|
895
|
+
const outdent = commands.get("outdent");
|
|
896
|
+
if (indent) indent.registerChildCommand(commands.get("indentCodeBlock"), { priority: "highest" });
|
|
897
|
+
if (outdent) outdent.registerChildCommand(commands.get("outdentCodeBlock"));
|
|
898
|
+
this.listenTo(editor.editing.view.document, "enter", (evt, data) => {
|
|
899
|
+
if (!editor.model.document.selection.getLastPosition().parent.is("element", "codeBlock")) return;
|
|
900
|
+
if (!leaveBlockStartOnEnter(editor, data.isSoft) && !leaveBlockEndOnEnter(editor, data.isSoft)) breakLineOnEnter(editor);
|
|
901
|
+
data.preventDefault();
|
|
902
|
+
evt.stop();
|
|
903
|
+
}, { context: "pre" });
|
|
904
|
+
this._initAriaAnnouncements();
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Observe when user enters or leaves code block and set proper aria value in global live announcer.
|
|
908
|
+
* This allows screen readers to indicate when the user has entered and left the specified code block.
|
|
909
|
+
*
|
|
910
|
+
* @internal
|
|
911
|
+
*/
|
|
912
|
+
_initAriaAnnouncements() {
|
|
913
|
+
const { model, ui, t } = this.editor;
|
|
914
|
+
const languageDefs = getNormalizedAndLocalizedLanguageDefinitions(this.editor);
|
|
915
|
+
let lastFocusedCodeBlock = null;
|
|
916
|
+
model.document.selection.on("change:range", () => {
|
|
917
|
+
const focusParent = model.document.selection.focus.parent;
|
|
918
|
+
if (!ui || lastFocusedCodeBlock === focusParent || !focusParent.is("element")) return;
|
|
919
|
+
if (lastFocusedCodeBlock && lastFocusedCodeBlock.is("element", "codeBlock")) ui.ariaLiveAnnouncer.announce(getCodeBlockAriaAnnouncement(t, languageDefs, lastFocusedCodeBlock, "leave"));
|
|
920
|
+
if (focusParent.is("element", "codeBlock")) ui.ariaLiveAnnouncer.announce(getCodeBlockAriaAnnouncement(t, languageDefs, focusParent, "enter"));
|
|
921
|
+
lastFocusedCodeBlock = focusParent;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
/**
|
|
926
|
+
* Normally, when the Enter (or Shift+Enter) key is pressed, a soft line break is to be added to the
|
|
927
|
+
* code block. Let's try to follow the indentation of the previous line when possible, for instance:
|
|
928
|
+
*
|
|
929
|
+
* ```html
|
|
930
|
+
* // Before pressing enter (or shift enter)
|
|
931
|
+
* <codeBlock>
|
|
932
|
+
* " foo()"[] // Indent of 4 spaces.
|
|
933
|
+
* </codeBlock>
|
|
934
|
+
*
|
|
935
|
+
* // After pressing:
|
|
936
|
+
* <codeBlock>
|
|
937
|
+
* " foo()" // Indent of 4 spaces.
|
|
938
|
+
* <softBreak></softBreak> // A new soft break created by pressing enter.
|
|
939
|
+
* " "[] // Retain the indent of 4 spaces.
|
|
940
|
+
* </codeBlock>
|
|
941
|
+
* ```
|
|
942
|
+
*/
|
|
943
|
+
function breakLineOnEnter(editor) {
|
|
944
|
+
const model = editor.model;
|
|
945
|
+
const modelDoc = model.document;
|
|
946
|
+
const lastSelectionPosition = modelDoc.selection.getLastPosition();
|
|
947
|
+
let leadingWhiteSpaces;
|
|
948
|
+
const node = getTextNodeAtLineStart(lastSelectionPosition, model);
|
|
949
|
+
if (node && node.is("$text")) leadingWhiteSpaces = getLeadingWhiteSpaces(node);
|
|
950
|
+
editor.model.change((writer) => {
|
|
951
|
+
editor.execute("shiftEnter");
|
|
952
|
+
if (leadingWhiteSpaces) writer.insertText(leadingWhiteSpaces, modelDoc.selection.anchor);
|
|
953
|
+
});
|
|
1227
954
|
}
|
|
1228
955
|
/**
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
// Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
|
|
1261
|
-
writer.rename(newBlock, DEFAULT_ELEMENT);
|
|
1262
|
-
writer.setSelection(newBlock, 'in');
|
|
1263
|
-
editor.model.schema.removeDisallowedAttributes([
|
|
1264
|
-
newBlock
|
|
1265
|
-
], writer);
|
|
1266
|
-
// Remove the <softBreak> that originally followed the selection position.
|
|
1267
|
-
writer.remove(nodeAfter);
|
|
1268
|
-
});
|
|
1269
|
-
// Eye candy.
|
|
1270
|
-
view.scrollToTheSelection();
|
|
1271
|
-
return true;
|
|
956
|
+
* Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the beginning
|
|
957
|
+
* of the code block:
|
|
958
|
+
*
|
|
959
|
+
* ```html
|
|
960
|
+
* // Before:
|
|
961
|
+
* <codeBlock>[]<softBreak></softBreak>foo</codeBlock>
|
|
962
|
+
*
|
|
963
|
+
* // After pressing:
|
|
964
|
+
* <paragraph>[]</paragraph><codeBlock>foo</codeBlock>
|
|
965
|
+
* ```
|
|
966
|
+
*
|
|
967
|
+
* @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
|
|
968
|
+
* @returns `true` when selection left the block. `false` if stayed.
|
|
969
|
+
*/
|
|
970
|
+
function leaveBlockStartOnEnter(editor, isSoftEnter) {
|
|
971
|
+
const modelDoc = editor.model.document;
|
|
972
|
+
const view = editor.editing.view;
|
|
973
|
+
const lastSelectionPosition = modelDoc.selection.getLastPosition();
|
|
974
|
+
const nodeAfter = lastSelectionPosition.nodeAfter;
|
|
975
|
+
if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtStart) return false;
|
|
976
|
+
if (!isSoftBreakNode(nodeAfter)) return false;
|
|
977
|
+
editor.model.change((writer) => {
|
|
978
|
+
editor.execute("enter");
|
|
979
|
+
const newBlock = modelDoc.selection.anchor.parent.previousSibling;
|
|
980
|
+
writer.rename(newBlock, DEFAULT_ELEMENT);
|
|
981
|
+
writer.setSelection(newBlock, "in");
|
|
982
|
+
editor.model.schema.removeDisallowedAttributes([newBlock], writer);
|
|
983
|
+
writer.remove(nodeAfter);
|
|
984
|
+
});
|
|
985
|
+
view.scrollToTheSelection();
|
|
986
|
+
return true;
|
|
1272
987
|
}
|
|
1273
988
|
/**
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
} else if (isEmptyishTextNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling) && isEmptyishTextNode(nodeBefore.previousSibling.previousSibling) && nodeBefore.previousSibling.previousSibling && isSoftBreakNode(nodeBefore.previousSibling.previousSibling.previousSibling)) {
|
|
1313
|
-
emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
|
|
1314
|
-
} else {
|
|
1315
|
-
return false;
|
|
1316
|
-
}
|
|
1317
|
-
// We're doing everything in a single change block to have a single undo step.
|
|
1318
|
-
editor.model.change((writer)=>{
|
|
1319
|
-
// Remove the last <softBreak>s and all white space characters that followed them.
|
|
1320
|
-
writer.remove(emptyLineRangeToRemoveOnEnter);
|
|
1321
|
-
// "Clone" the <codeBlock> in the standard way.
|
|
1322
|
-
editor.execute('enter');
|
|
1323
|
-
const newBlock = modelDoc.selection.anchor.parent;
|
|
1324
|
-
// Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
|
|
1325
|
-
writer.rename(newBlock, DEFAULT_ELEMENT);
|
|
1326
|
-
editor.model.schema.removeDisallowedAttributes([
|
|
1327
|
-
newBlock
|
|
1328
|
-
], writer);
|
|
1329
|
-
});
|
|
1330
|
-
// Eye candy.
|
|
1331
|
-
view.scrollToTheSelection();
|
|
1332
|
-
return true;
|
|
989
|
+
* Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the end
|
|
990
|
+
* of the code block:
|
|
991
|
+
*
|
|
992
|
+
* ```html
|
|
993
|
+
* // Before:
|
|
994
|
+
* <codeBlock>foo[]</codeBlock>
|
|
995
|
+
*
|
|
996
|
+
* // After first press:
|
|
997
|
+
* <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
|
|
998
|
+
*
|
|
999
|
+
* // After second press:
|
|
1000
|
+
* <codeBlock>foo</codeBlock><paragraph>[]</paragraph>
|
|
1001
|
+
* ```
|
|
1002
|
+
*
|
|
1003
|
+
* @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
|
|
1004
|
+
* @returns `true` when selection left the block. `false` if stayed.
|
|
1005
|
+
*/
|
|
1006
|
+
function leaveBlockEndOnEnter(editor, isSoftEnter) {
|
|
1007
|
+
const model = editor.model;
|
|
1008
|
+
const modelDoc = model.document;
|
|
1009
|
+
const view = editor.editing.view;
|
|
1010
|
+
const lastSelectionPosition = modelDoc.selection.getLastPosition();
|
|
1011
|
+
const nodeBefore = lastSelectionPosition.nodeBefore;
|
|
1012
|
+
let emptyLineRangeToRemoveOnEnter;
|
|
1013
|
+
if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore || !nodeBefore.previousSibling) return false;
|
|
1014
|
+
if (isSoftBreakNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling)) emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling), model.createPositionAfter(nodeBefore));
|
|
1015
|
+
else if (isEmptyishTextNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling) && isSoftBreakNode(nodeBefore.previousSibling.previousSibling)) emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
|
|
1016
|
+
else if (isEmptyishTextNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling) && isEmptyishTextNode(nodeBefore.previousSibling.previousSibling) && nodeBefore.previousSibling.previousSibling && isSoftBreakNode(nodeBefore.previousSibling.previousSibling.previousSibling)) emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
|
|
1017
|
+
else return false;
|
|
1018
|
+
editor.model.change((writer) => {
|
|
1019
|
+
writer.remove(emptyLineRangeToRemoveOnEnter);
|
|
1020
|
+
editor.execute("enter");
|
|
1021
|
+
const newBlock = modelDoc.selection.anchor.parent;
|
|
1022
|
+
writer.rename(newBlock, DEFAULT_ELEMENT);
|
|
1023
|
+
editor.model.schema.removeDisallowedAttributes([newBlock], writer);
|
|
1024
|
+
});
|
|
1025
|
+
view.scrollToTheSelection();
|
|
1026
|
+
return true;
|
|
1333
1027
|
}
|
|
1334
1028
|
function isEmptyishTextNode(node) {
|
|
1335
|
-
|
|
1029
|
+
return node && node.is("$text") && !node.data.match(/\S/);
|
|
1336
1030
|
}
|
|
1337
1031
|
function isSoftBreakNode(node) {
|
|
1338
|
-
|
|
1032
|
+
return node && node.is("element", "softBreak");
|
|
1339
1033
|
}
|
|
1340
1034
|
|
|
1341
1035
|
/**
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1036
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1037
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1038
|
+
*/
|
|
1039
|
+
/**
|
|
1040
|
+
* @module code-block/codeblockui
|
|
1041
|
+
*/
|
|
1042
|
+
/**
|
|
1043
|
+
* The code block UI plugin.
|
|
1044
|
+
*
|
|
1045
|
+
* Introduces the `'codeBlock'` dropdown.
|
|
1046
|
+
*/
|
|
1047
|
+
var CodeBlockUI = class extends Plugin {
|
|
1048
|
+
/**
|
|
1049
|
+
* @inheritDoc
|
|
1050
|
+
*/
|
|
1051
|
+
static get pluginName() {
|
|
1052
|
+
return "CodeBlockUI";
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* @inheritDoc
|
|
1056
|
+
*/
|
|
1057
|
+
static get isOfficialPlugin() {
|
|
1058
|
+
return true;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* @inheritDoc
|
|
1062
|
+
*/
|
|
1063
|
+
init() {
|
|
1064
|
+
const editor = this.editor;
|
|
1065
|
+
const t = editor.t;
|
|
1066
|
+
const componentFactory = editor.ui.componentFactory;
|
|
1067
|
+
const normalizedLanguageDefs = getNormalizedAndLocalizedLanguageDefinitions(editor);
|
|
1068
|
+
const itemDefinitions = this._getLanguageListItemDefinitions(normalizedLanguageDefs);
|
|
1069
|
+
const command = editor.commands.get("codeBlock");
|
|
1070
|
+
componentFactory.add("codeBlock", (locale) => {
|
|
1071
|
+
const dropdownView = createDropdown(locale, SplitButtonView);
|
|
1072
|
+
const splitButtonView = dropdownView.buttonView;
|
|
1073
|
+
const accessibleLabel = t("Insert code block");
|
|
1074
|
+
splitButtonView.set({
|
|
1075
|
+
label: accessibleLabel,
|
|
1076
|
+
tooltip: true,
|
|
1077
|
+
icon: IconCodeBlock,
|
|
1078
|
+
isToggleable: true
|
|
1079
|
+
});
|
|
1080
|
+
splitButtonView.bind("isOn").to(command, "value", (value) => !!value);
|
|
1081
|
+
splitButtonView.on("execute", () => {
|
|
1082
|
+
editor.execute("codeBlock", { usePreviousLanguageChoice: true });
|
|
1083
|
+
editor.editing.view.focus();
|
|
1084
|
+
});
|
|
1085
|
+
dropdownView.on("execute", (evt) => {
|
|
1086
|
+
editor.execute("codeBlock", {
|
|
1087
|
+
language: evt.source._codeBlockLanguage,
|
|
1088
|
+
forceValue: true
|
|
1089
|
+
});
|
|
1090
|
+
editor.editing.view.focus();
|
|
1091
|
+
});
|
|
1092
|
+
dropdownView.class = "ck-code-block-dropdown";
|
|
1093
|
+
dropdownView.bind("isEnabled").to(command);
|
|
1094
|
+
addListToDropdown(dropdownView, itemDefinitions, {
|
|
1095
|
+
role: "menu",
|
|
1096
|
+
ariaLabel: accessibleLabel
|
|
1097
|
+
});
|
|
1098
|
+
return dropdownView;
|
|
1099
|
+
});
|
|
1100
|
+
componentFactory.add("menuBar:codeBlock", (locale) => {
|
|
1101
|
+
const menuView = new MenuBarMenuView(locale);
|
|
1102
|
+
menuView.buttonView.set({
|
|
1103
|
+
role: "menuitem",
|
|
1104
|
+
label: t("Code block"),
|
|
1105
|
+
icon: IconCodeBlock
|
|
1106
|
+
});
|
|
1107
|
+
menuView.bind("isEnabled").to(command);
|
|
1108
|
+
const listView = new MenuBarMenuListView(locale);
|
|
1109
|
+
listView.set({ ariaLabel: t("Insert code block") });
|
|
1110
|
+
for (const definition of itemDefinitions) {
|
|
1111
|
+
const listItemView = new MenuBarMenuListItemView(locale, menuView);
|
|
1112
|
+
const buttonView = new MenuBarMenuListItemButtonView(locale);
|
|
1113
|
+
buttonView.bind(...Object.keys(definition.model)).to(definition.model);
|
|
1114
|
+
buttonView.set({
|
|
1115
|
+
isToggleable: true,
|
|
1116
|
+
role: "menuitemcheckbox"
|
|
1117
|
+
});
|
|
1118
|
+
buttonView.delegate("execute").to(menuView);
|
|
1119
|
+
buttonView.on("execute", () => {
|
|
1120
|
+
editor.execute("codeBlock", {
|
|
1121
|
+
language: definition.model._codeBlockLanguage,
|
|
1122
|
+
forceValue: command.value == definition.model._codeBlockLanguage ? false : true
|
|
1123
|
+
});
|
|
1124
|
+
editor.editing.view.focus();
|
|
1125
|
+
});
|
|
1126
|
+
listItemView.children.add(buttonView);
|
|
1127
|
+
listView.items.add(listItemView);
|
|
1128
|
+
}
|
|
1129
|
+
menuView.panelView.children.add(listView);
|
|
1130
|
+
return menuView;
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* A helper returning a collection of the `codeBlock` dropdown items representing languages
|
|
1135
|
+
* available for the user to choose from.
|
|
1136
|
+
*/
|
|
1137
|
+
_getLanguageListItemDefinitions(normalizedLanguageDefs) {
|
|
1138
|
+
const command = this.editor.commands.get("codeBlock");
|
|
1139
|
+
const itemDefinitions = new Collection();
|
|
1140
|
+
for (const languageDef of normalizedLanguageDefs) {
|
|
1141
|
+
const definition = {
|
|
1142
|
+
type: "button",
|
|
1143
|
+
model: new UIModel({
|
|
1144
|
+
_codeBlockLanguage: languageDef.language,
|
|
1145
|
+
label: languageDef.label,
|
|
1146
|
+
role: "menuitemradio",
|
|
1147
|
+
withText: true
|
|
1148
|
+
})
|
|
1149
|
+
};
|
|
1150
|
+
definition.model.bind("isOn").to(command, "value", (value) => {
|
|
1151
|
+
return value === definition.model._codeBlockLanguage;
|
|
1152
|
+
});
|
|
1153
|
+
itemDefinitions.add(definition);
|
|
1154
|
+
}
|
|
1155
|
+
return itemDefinitions;
|
|
1156
|
+
}
|
|
1157
|
+
};
|
|
1457
1158
|
|
|
1458
1159
|
/**
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1160
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1161
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1162
|
+
*/
|
|
1163
|
+
/**
|
|
1164
|
+
* @module code-block/codeblock
|
|
1165
|
+
*/
|
|
1166
|
+
/**
|
|
1167
|
+
* The code block plugin.
|
|
1168
|
+
*
|
|
1169
|
+
* For more information about this feature check the {@glink api/code-block package page} and the
|
|
1170
|
+
* {@glink features/code-blocks code block} feature guide.
|
|
1171
|
+
*
|
|
1172
|
+
* This is a "glue" plugin that loads the {@link module:code-block/codeblockediting~CodeBlockEditing code block editing feature}
|
|
1173
|
+
* and the {@link module:code-block/codeblockui~CodeBlockUI code block UI feature}.
|
|
1174
|
+
*/
|
|
1175
|
+
var CodeBlock = class extends Plugin {
|
|
1176
|
+
/**
|
|
1177
|
+
* @inheritDoc
|
|
1178
|
+
*/
|
|
1179
|
+
static get requires() {
|
|
1180
|
+
return [CodeBlockEditing, CodeBlockUI];
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* @inheritDoc
|
|
1184
|
+
*/
|
|
1185
|
+
static get pluginName() {
|
|
1186
|
+
return "CodeBlock";
|
|
1187
|
+
}
|
|
1188
|
+
/**
|
|
1189
|
+
* @inheritDoc
|
|
1190
|
+
*/
|
|
1191
|
+
static get isOfficialPlugin() {
|
|
1192
|
+
return true;
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
1198
|
+
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
|
|
1199
|
+
*/
|
|
1486
1200
|
|
|
1487
1201
|
export { CodeBlock, CodeBlockCommand, CodeBlockEditing, CodeBlockUI, IndentCodeBlockCommand, OutdentCodeBlockCommand, canBeCodeBlock as _canBeCodeBlock, dataViewToModelCodeBlockInsertion as _dataViewToModelCodeBlockInsertion, dataViewToModelOrphanNodeConsumer as _dataViewToModelCodeBlockOrphanNodeConsumer, dataViewToModelTextNewlinesInsertion as _dataViewToModelCodeBlockTextNewlinesInsertion, getCodeBlockAriaAnnouncement as _getCodeBlockAriaAnnouncement, getIndentOutdentPositions as _getCodeBlockIndentOutdentPositions, getLeadingWhiteSpaces as _getCodeBlockLeadingWhiteSpaces, getPropertyAssociation as _getCodeBlockPropertyAssociation, getTextNodeAtLineStart as _getCodeBlockTextNodeAtLineStart, getNormalizedAndLocalizedLanguageDefinitions as _getNormalizedAndLocalizedCodeBlockLanguageDefinitions, isModelSelectionInCodeBlock as _isModelSelectionInCodeBlock, modelToDataViewSoftBreakInsertion as _modelToDataViewCodeBlockSoftBreakInsertion, modelToViewCodeBlockInsertion as _modelToViewCodeBlockInsertion, rawSnippetTextToViewDocumentFragment as _rawCodeBlockSnippetTextToViewDocumentFragment };
|
|
1488
|
-
//# sourceMappingURL=index.js.map
|
|
1202
|
+
//# sourceMappingURL=index.js.map
|