@ckeditor/ckeditor5-code-block 38.0.1 → 38.1.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.
@@ -1,383 +1,383 @@
1
- /**
2
- * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
- * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
- */
5
- /**
6
- * @module code-block/codeblockediting
7
- */
8
- import { Plugin } from 'ckeditor5/src/core';
9
- import { ShiftEnter } from 'ckeditor5/src/enter';
10
- import { UpcastWriter } from 'ckeditor5/src/engine';
11
- import CodeBlockCommand from './codeblockcommand';
12
- import IndentCodeBlockCommand from './indentcodeblockcommand';
13
- import OutdentCodeBlockCommand from './outdentcodeblockcommand';
14
- import { getNormalizedAndLocalizedLanguageDefinitions, getLeadingWhiteSpaces, rawSnippetTextToViewDocumentFragment } from './utils';
15
- import { modelToViewCodeBlockInsertion, modelToDataViewSoftBreakInsertion, dataViewToModelCodeBlockInsertion, dataViewToModelTextNewlinesInsertion, dataViewToModelOrphanNodeConsumer } from './converters';
16
- const DEFAULT_ELEMENT = 'paragraph';
17
- /**
18
- * The editing part of the code block feature.
19
- *
20
- * Introduces the `'codeBlock'` command and the `'codeBlock'` model element.
21
- */
22
- export default class CodeBlockEditing extends Plugin {
23
- /**
24
- * @inheritDoc
25
- */
26
- static get pluginName() {
27
- return 'CodeBlockEditing';
28
- }
29
- /**
30
- * @inheritDoc
31
- */
32
- static get requires() {
33
- return [ShiftEnter];
34
- }
35
- /**
36
- * @inheritDoc
37
- */
38
- constructor(editor) {
39
- super(editor);
40
- editor.config.define('codeBlock', {
41
- languages: [
42
- { language: 'plaintext', label: 'Plain text' },
43
- { language: 'c', label: 'C' },
44
- { language: 'cs', label: 'C#' },
45
- { language: 'cpp', label: 'C++' },
46
- { language: 'css', label: 'CSS' },
47
- { language: 'diff', label: 'Diff' },
48
- { language: 'html', label: 'HTML' },
49
- { language: 'java', label: 'Java' },
50
- { language: 'javascript', label: 'JavaScript' },
51
- { language: 'php', label: 'PHP' },
52
- { language: 'python', label: 'Python' },
53
- { language: 'ruby', label: 'Ruby' },
54
- { language: 'typescript', label: 'TypeScript' },
55
- { language: 'xml', label: 'XML' }
56
- ],
57
- // A single tab.
58
- indentSequence: '\t'
59
- });
60
- }
61
- /**
62
- * @inheritDoc
63
- */
64
- init() {
65
- const editor = this.editor;
66
- const schema = editor.model.schema;
67
- const model = editor.model;
68
- const view = editor.editing.view;
69
- const isDocumentListEditingLoaded = editor.plugins.has('DocumentListEditing');
70
- const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions(editor);
71
- // The main command.
72
- editor.commands.add('codeBlock', new CodeBlockCommand(editor));
73
- // Commands that change the indentation.
74
- editor.commands.add('indentCodeBlock', new IndentCodeBlockCommand(editor));
75
- editor.commands.add('outdentCodeBlock', new OutdentCodeBlockCommand(editor));
76
- this.listenTo(view.document, 'tab', (evt, data) => {
77
- const commandName = data.shiftKey ? 'outdentCodeBlock' : 'indentCodeBlock';
78
- const command = editor.commands.get(commandName);
79
- if (!command.isEnabled) {
80
- return;
81
- }
82
- editor.execute(commandName);
83
- data.stopPropagation();
84
- data.preventDefault();
85
- evt.stop();
86
- }, { context: 'pre' });
87
- schema.register('codeBlock', {
88
- allowWhere: '$block',
89
- allowChildren: '$text',
90
- isBlock: true,
91
- allowAttributes: ['language']
92
- });
93
- // Allow all list* attributes on `codeBlock` (integration with DocumentList).
94
- // Disallow all attributes on $text inside `codeBlock`.
95
- schema.addAttributeCheck((context, attributeName) => {
96
- const isDocumentListAttributeOnCodeBlock = context.endsWith('codeBlock') &&
97
- attributeName.startsWith('list') &&
98
- attributeName !== 'list';
99
- if (isDocumentListEditingLoaded && isDocumentListAttributeOnCodeBlock) {
100
- return true;
101
- }
102
- if (context.endsWith('codeBlock $text')) {
103
- return false;
104
- }
105
- });
106
- // Disallow object elements inside `codeBlock`. See #9567.
107
- editor.model.schema.addChildCheck((context, childDefinition) => {
108
- if (context.endsWith('codeBlock') && childDefinition.isObject) {
109
- return false;
110
- }
111
- });
112
- // Conversion.
113
- editor.editing.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs, true));
114
- editor.data.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs));
115
- editor.data.downcastDispatcher.on('insert:softBreak', modelToDataViewSoftBreakInsertion(model), { priority: 'high' });
116
- editor.data.upcastDispatcher.on('element:code', dataViewToModelCodeBlockInsertion(view, normalizedLanguagesDefs));
117
- editor.data.upcastDispatcher.on('text', dataViewToModelTextNewlinesInsertion());
118
- editor.data.upcastDispatcher.on('element:pre', dataViewToModelOrphanNodeConsumer(), { priority: 'high' });
119
- // Intercept the clipboard input (paste) when the selection is anchored in the code block and force the clipboard
120
- // data to be pasted as a single plain text. Otherwise, the code lines will split the code block and
121
- // "spill out" as separate paragraphs.
122
- this.listenTo(editor.editing.view.document, 'clipboardInput', (evt, data) => {
123
- let insertionRange = model.createRange(model.document.selection.anchor);
124
- // Use target ranges in case this is a drop.
125
- if (data.targetRanges) {
126
- insertionRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
127
- }
128
- if (!insertionRange.start.parent.is('element', 'codeBlock')) {
129
- return;
130
- }
131
- const text = data.dataTransfer.getData('text/plain');
132
- const writer = new UpcastWriter(editor.editing.view.document);
133
- // Pass the view fragment to the default clipboardInput handler.
134
- data.content = rawSnippetTextToViewDocumentFragment(writer, text);
135
- });
136
- // Make sure multi–line selection is always wrapped in a code block when `getSelectedContent()`
137
- // is used (e.g. clipboard copy). Otherwise, only the raw text will be copied to the clipboard and,
138
- // upon next paste, this bare text will not be inserted as a code block, which is not the best UX.
139
- // Similarly, when the selection in a single line, the selected content should be an inline code
140
- // so it can be pasted later on and retain it's preformatted nature.
141
- this.listenTo(model, 'getSelectedContent', (evt, [selection]) => {
142
- const anchor = selection.anchor;
143
- if (selection.isCollapsed || !anchor.parent.is('element', 'codeBlock') || !anchor.hasSameParentAs(selection.focus)) {
144
- return;
145
- }
146
- model.change(writer => {
147
- const docFragment = evt.return;
148
- // fo[o<softBreak></softBreak>b]ar -> <codeBlock language="...">[o<softBreak></softBreak>b]<codeBlock>
149
- if (anchor.parent.is('element') &&
150
- (docFragment.childCount > 1 || selection.containsEntireContent(anchor.parent))) {
151
- const codeBlock = writer.createElement('codeBlock', anchor.parent.getAttributes());
152
- writer.append(docFragment, codeBlock);
153
- const newDocumentFragment = writer.createDocumentFragment();
154
- writer.append(codeBlock, newDocumentFragment);
155
- evt.return = newDocumentFragment;
156
- return;
157
- }
158
- // "f[oo]" -> <$text code="true">oo</text>
159
- const textNode = docFragment.getChild(0);
160
- if (schema.checkAttribute(textNode, 'code')) {
161
- writer.setAttribute('code', true, textNode);
162
- }
163
- });
164
- });
165
- }
166
- /**
167
- * @inheritDoc
168
- */
169
- afterInit() {
170
- const editor = this.editor;
171
- const commands = editor.commands;
172
- const indent = commands.get('indent');
173
- const outdent = commands.get('outdent');
174
- if (indent) {
175
- // Priority is highest due to integration with `IndentList` command of `List` plugin.
176
- // If selection is in a code block we give priority to it. This way list item cannot be indented
177
- // but if we would give priority to indenting list item then user would have to indent list item
178
- // as much as possible and only then he could indent code block.
179
- indent.registerChildCommand(commands.get('indentCodeBlock'), { priority: 'highest' });
180
- }
181
- if (outdent) {
182
- outdent.registerChildCommand(commands.get('outdentCodeBlock'));
183
- }
184
- // Customize the response to the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd>
185
- // key press when the selection is in the code block. Upon enter key press we can either
186
- // leave the block if it's "two or three enters" in a row or create a new code block line, preserving
187
- // previous line's indentation.
188
- this.listenTo(editor.editing.view.document, 'enter', (evt, data) => {
189
- const positionParent = editor.model.document.selection.getLastPosition().parent;
190
- if (!positionParent.is('element', 'codeBlock')) {
191
- return;
192
- }
193
- if (!leaveBlockStartOnEnter(editor, data.isSoft) && !leaveBlockEndOnEnter(editor, data.isSoft)) {
194
- breakLineOnEnter(editor);
195
- }
196
- data.preventDefault();
197
- evt.stop();
198
- }, { context: 'pre' });
199
- }
200
- }
201
- /**
202
- * Normally, when the Enter (or Shift+Enter) key is pressed, a soft line break is to be added to the
203
- * code block. Let's try to follow the indentation of the previous line when possible, for instance:
204
- *
205
- * ```html
206
- * // Before pressing enter (or shift enter)
207
- * <codeBlock>
208
- * " foo()"[] // Indent of 4 spaces.
209
- * </codeBlock>
210
- *
211
- * // After pressing:
212
- * <codeBlock>
213
- * " foo()" // Indent of 4 spaces.
214
- * <softBreak></softBreak> // A new soft break created by pressing enter.
215
- * " "[] // Retain the indent of 4 spaces.
216
- * </codeBlock>
217
- * ```
218
- */
219
- function breakLineOnEnter(editor) {
220
- const model = editor.model;
221
- const modelDoc = model.document;
222
- const lastSelectionPosition = modelDoc.selection.getLastPosition();
223
- const node = lastSelectionPosition.nodeBefore || lastSelectionPosition.textNode;
224
- let leadingWhiteSpaces;
225
- // Figure out the indentation (white space chars) at the beginning of the line.
226
- if (node && node.is('$text')) {
227
- leadingWhiteSpaces = getLeadingWhiteSpaces(node);
228
- }
229
- // Keeping everything in a change block for a single undo step.
230
- editor.model.change(writer => {
231
- editor.execute('shiftEnter');
232
- // If the line before being broken in two had some indentation, let's retain it
233
- // in the new line.
234
- if (leadingWhiteSpaces) {
235
- writer.insertText(leadingWhiteSpaces, modelDoc.selection.anchor);
236
- }
237
- });
238
- }
239
- /**
240
- * Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the beginning
241
- * of the code block:
242
- *
243
- * ```html
244
- * // Before:
245
- * <codeBlock>[]<softBreak></softBreak>foo</codeBlock>
246
- *
247
- * // After pressing:
248
- * <paragraph>[]</paragraph><codeBlock>foo</codeBlock>
249
- * ```
250
- *
251
- * @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
252
- * @returns `true` when selection left the block. `false` if stayed.
253
- */
254
- function leaveBlockStartOnEnter(editor, isSoftEnter) {
255
- const model = editor.model;
256
- const modelDoc = model.document;
257
- const view = editor.editing.view;
258
- const lastSelectionPosition = modelDoc.selection.getLastPosition();
259
- const nodeAfter = lastSelectionPosition.nodeAfter;
260
- if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtStart) {
261
- return false;
262
- }
263
- if (!isSoftBreakNode(nodeAfter)) {
264
- return false;
265
- }
266
- // We're doing everything in a single change block to have a single undo step.
267
- editor.model.change(writer => {
268
- // "Clone" the <codeBlock> in the standard way.
269
- editor.execute('enter');
270
- // The cloned block exists now before the original code block.
271
- const newBlock = modelDoc.selection.anchor.parent.previousSibling;
272
- // Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
273
- writer.rename(newBlock, DEFAULT_ELEMENT);
274
- writer.setSelection(newBlock, 'in');
275
- editor.model.schema.removeDisallowedAttributes([newBlock], writer);
276
- // Remove the <softBreak> that originally followed the selection position.
277
- writer.remove(nodeAfter);
278
- });
279
- // Eye candy.
280
- view.scrollToTheSelection();
281
- return true;
282
- }
283
- /**
284
- * Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the end
285
- * of the code block:
286
- *
287
- * ```html
288
- * // Before:
289
- * <codeBlock>foo[]</codeBlock>
290
- *
291
- * // After first press:
292
- * <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
293
- *
294
- * // After second press:
295
- * <codeBlock>foo</codeBlock><paragraph>[]</paragraph>
296
- * ```
297
- *
298
- * @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
299
- * @returns `true` when selection left the block. `false` if stayed.
300
- */
301
- function leaveBlockEndOnEnter(editor, isSoftEnter) {
302
- const model = editor.model;
303
- const modelDoc = model.document;
304
- const view = editor.editing.view;
305
- const lastSelectionPosition = modelDoc.selection.getLastPosition();
306
- const nodeBefore = lastSelectionPosition.nodeBefore;
307
- let emptyLineRangeToRemoveOnEnter;
308
- if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore || !nodeBefore.previousSibling) {
309
- return false;
310
- }
311
- // When the position is directly preceded by two soft breaks
312
- //
313
- // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>
314
- //
315
- // it creates the following range that will be cleaned up before leaving:
316
- //
317
- // <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak>]</codeBlock>
318
- //
319
- if (isSoftBreakNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling)) {
320
- emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling), model.createPositionAfter(nodeBefore));
321
- }
322
- // When there's some text before the position that is
323
- // preceded by two soft breaks and made purely of white–space characters
324
- //
325
- // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> []</codeBlock>
326
- //
327
- // it creates the following range to clean up before leaving:
328
- //
329
- // <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak> ]</codeBlock>
330
- //
331
- else if (isEmptyishTextNode(nodeBefore) &&
332
- isSoftBreakNode(nodeBefore.previousSibling) &&
333
- isSoftBreakNode(nodeBefore.previousSibling.previousSibling)) {
334
- emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
335
- }
336
- // When there's some text before the position that is made purely of white–space characters
337
- // and is preceded by some other text made purely of white–space characters
338
- //
339
- // <codeBlock>foo<softBreak></softBreak> <softBreak></softBreak> []</codeBlock>
340
- //
341
- // it creates the following range to clean up before leaving:
342
- //
343
- // <codeBlock>foo[<softBreak></softBreak> <softBreak></softBreak> ]</codeBlock>
344
- //
345
- else if (isEmptyishTextNode(nodeBefore) &&
346
- isSoftBreakNode(nodeBefore.previousSibling) &&
347
- isEmptyishTextNode(nodeBefore.previousSibling.previousSibling) &&
348
- nodeBefore.previousSibling.previousSibling &&
349
- isSoftBreakNode(nodeBefore.previousSibling.previousSibling.previousSibling)) {
350
- emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
351
- }
352
- // Not leaving the block in the following cases:
353
- //
354
- // <codeBlock> []</codeBlock>
355
- // <codeBlock> a []</codeBlock>
356
- // <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
357
- // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>bar[]</codeBlock>
358
- // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> a []</codeBlock>
359
- //
360
- else {
361
- return false;
362
- }
363
- // We're doing everything in a single change block to have a single undo step.
364
- editor.model.change(writer => {
365
- // Remove the last <softBreak>s and all white space characters that followed them.
366
- writer.remove(emptyLineRangeToRemoveOnEnter);
367
- // "Clone" the <codeBlock> in the standard way.
368
- editor.execute('enter');
369
- const newBlock = modelDoc.selection.anchor.parent;
370
- // Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
371
- writer.rename(newBlock, DEFAULT_ELEMENT);
372
- editor.model.schema.removeDisallowedAttributes([newBlock], writer);
373
- });
374
- // Eye candy.
375
- view.scrollToTheSelection();
376
- return true;
377
- }
378
- function isEmptyishTextNode(node) {
379
- return node && node.is('$text') && !node.data.match(/\S/);
380
- }
381
- function isSoftBreakNode(node) {
382
- return node && node.is('element', 'softBreak');
383
- }
1
+ /**
2
+ * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
+ */
5
+ /**
6
+ * @module code-block/codeblockediting
7
+ */
8
+ import { Plugin } from 'ckeditor5/src/core';
9
+ import { ShiftEnter } from 'ckeditor5/src/enter';
10
+ import { UpcastWriter } from 'ckeditor5/src/engine';
11
+ import CodeBlockCommand from './codeblockcommand';
12
+ import IndentCodeBlockCommand from './indentcodeblockcommand';
13
+ import OutdentCodeBlockCommand from './outdentcodeblockcommand';
14
+ import { getNormalizedAndLocalizedLanguageDefinitions, getLeadingWhiteSpaces, rawSnippetTextToViewDocumentFragment } from './utils';
15
+ import { modelToViewCodeBlockInsertion, modelToDataViewSoftBreakInsertion, dataViewToModelCodeBlockInsertion, dataViewToModelTextNewlinesInsertion, dataViewToModelOrphanNodeConsumer } from './converters';
16
+ const DEFAULT_ELEMENT = 'paragraph';
17
+ /**
18
+ * The editing part of the code block feature.
19
+ *
20
+ * Introduces the `'codeBlock'` command and the `'codeBlock'` model element.
21
+ */
22
+ export default class CodeBlockEditing extends Plugin {
23
+ /**
24
+ * @inheritDoc
25
+ */
26
+ static get pluginName() {
27
+ return 'CodeBlockEditing';
28
+ }
29
+ /**
30
+ * @inheritDoc
31
+ */
32
+ static get requires() {
33
+ return [ShiftEnter];
34
+ }
35
+ /**
36
+ * @inheritDoc
37
+ */
38
+ constructor(editor) {
39
+ super(editor);
40
+ editor.config.define('codeBlock', {
41
+ languages: [
42
+ { language: 'plaintext', label: 'Plain text' },
43
+ { language: 'c', label: 'C' },
44
+ { language: 'cs', label: 'C#' },
45
+ { language: 'cpp', label: 'C++' },
46
+ { language: 'css', label: 'CSS' },
47
+ { language: 'diff', label: 'Diff' },
48
+ { language: 'html', label: 'HTML' },
49
+ { language: 'java', label: 'Java' },
50
+ { language: 'javascript', label: 'JavaScript' },
51
+ { language: 'php', label: 'PHP' },
52
+ { language: 'python', label: 'Python' },
53
+ { language: 'ruby', label: 'Ruby' },
54
+ { language: 'typescript', label: 'TypeScript' },
55
+ { language: 'xml', label: 'XML' }
56
+ ],
57
+ // A single tab.
58
+ indentSequence: '\t'
59
+ });
60
+ }
61
+ /**
62
+ * @inheritDoc
63
+ */
64
+ init() {
65
+ const editor = this.editor;
66
+ const schema = editor.model.schema;
67
+ const model = editor.model;
68
+ const view = editor.editing.view;
69
+ const isDocumentListEditingLoaded = editor.plugins.has('DocumentListEditing');
70
+ const normalizedLanguagesDefs = getNormalizedAndLocalizedLanguageDefinitions(editor);
71
+ // The main command.
72
+ editor.commands.add('codeBlock', new CodeBlockCommand(editor));
73
+ // Commands that change the indentation.
74
+ editor.commands.add('indentCodeBlock', new IndentCodeBlockCommand(editor));
75
+ editor.commands.add('outdentCodeBlock', new OutdentCodeBlockCommand(editor));
76
+ this.listenTo(view.document, 'tab', (evt, data) => {
77
+ const commandName = data.shiftKey ? 'outdentCodeBlock' : 'indentCodeBlock';
78
+ const command = editor.commands.get(commandName);
79
+ if (!command.isEnabled) {
80
+ return;
81
+ }
82
+ editor.execute(commandName);
83
+ data.stopPropagation();
84
+ data.preventDefault();
85
+ evt.stop();
86
+ }, { context: 'pre' });
87
+ schema.register('codeBlock', {
88
+ allowWhere: '$block',
89
+ allowChildren: '$text',
90
+ isBlock: true,
91
+ allowAttributes: ['language']
92
+ });
93
+ // Allow all list* attributes on `codeBlock` (integration with DocumentList).
94
+ // Disallow all attributes on $text inside `codeBlock`.
95
+ schema.addAttributeCheck((context, attributeName) => {
96
+ const isDocumentListAttributeOnCodeBlock = context.endsWith('codeBlock') &&
97
+ attributeName.startsWith('list') &&
98
+ attributeName !== 'list';
99
+ if (isDocumentListEditingLoaded && isDocumentListAttributeOnCodeBlock) {
100
+ return true;
101
+ }
102
+ if (context.endsWith('codeBlock $text')) {
103
+ return false;
104
+ }
105
+ });
106
+ // Disallow object elements inside `codeBlock`. See #9567.
107
+ editor.model.schema.addChildCheck((context, childDefinition) => {
108
+ if (context.endsWith('codeBlock') && childDefinition.isObject) {
109
+ return false;
110
+ }
111
+ });
112
+ // Conversion.
113
+ editor.editing.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs, true));
114
+ editor.data.downcastDispatcher.on('insert:codeBlock', modelToViewCodeBlockInsertion(model, normalizedLanguagesDefs));
115
+ editor.data.downcastDispatcher.on('insert:softBreak', modelToDataViewSoftBreakInsertion(model), { priority: 'high' });
116
+ editor.data.upcastDispatcher.on('element:code', dataViewToModelCodeBlockInsertion(view, normalizedLanguagesDefs));
117
+ editor.data.upcastDispatcher.on('text', dataViewToModelTextNewlinesInsertion());
118
+ editor.data.upcastDispatcher.on('element:pre', dataViewToModelOrphanNodeConsumer(), { priority: 'high' });
119
+ // Intercept the clipboard input (paste) when the selection is anchored in the code block and force the clipboard
120
+ // data to be pasted as a single plain text. Otherwise, the code lines will split the code block and
121
+ // "spill out" as separate paragraphs.
122
+ this.listenTo(editor.editing.view.document, 'clipboardInput', (evt, data) => {
123
+ let insertionRange = model.createRange(model.document.selection.anchor);
124
+ // Use target ranges in case this is a drop.
125
+ if (data.targetRanges) {
126
+ insertionRange = editor.editing.mapper.toModelRange(data.targetRanges[0]);
127
+ }
128
+ if (!insertionRange.start.parent.is('element', 'codeBlock')) {
129
+ return;
130
+ }
131
+ const text = data.dataTransfer.getData('text/plain');
132
+ const writer = new UpcastWriter(editor.editing.view.document);
133
+ // Pass the view fragment to the default clipboardInput handler.
134
+ data.content = rawSnippetTextToViewDocumentFragment(writer, text);
135
+ });
136
+ // Make sure multi–line selection is always wrapped in a code block when `getSelectedContent()`
137
+ // is used (e.g. clipboard copy). Otherwise, only the raw text will be copied to the clipboard and,
138
+ // upon next paste, this bare text will not be inserted as a code block, which is not the best UX.
139
+ // Similarly, when the selection in a single line, the selected content should be an inline code
140
+ // so it can be pasted later on and retain it's preformatted nature.
141
+ this.listenTo(model, 'getSelectedContent', (evt, [selection]) => {
142
+ const anchor = selection.anchor;
143
+ if (selection.isCollapsed || !anchor.parent.is('element', 'codeBlock') || !anchor.hasSameParentAs(selection.focus)) {
144
+ return;
145
+ }
146
+ model.change(writer => {
147
+ const docFragment = evt.return;
148
+ // fo[o<softBreak></softBreak>b]ar -> <codeBlock language="...">[o<softBreak></softBreak>b]<codeBlock>
149
+ if (anchor.parent.is('element') &&
150
+ (docFragment.childCount > 1 || selection.containsEntireContent(anchor.parent))) {
151
+ const codeBlock = writer.createElement('codeBlock', anchor.parent.getAttributes());
152
+ writer.append(docFragment, codeBlock);
153
+ const newDocumentFragment = writer.createDocumentFragment();
154
+ writer.append(codeBlock, newDocumentFragment);
155
+ evt.return = newDocumentFragment;
156
+ return;
157
+ }
158
+ // "f[oo]" -> <$text code="true">oo</text>
159
+ const textNode = docFragment.getChild(0);
160
+ if (schema.checkAttribute(textNode, 'code')) {
161
+ writer.setAttribute('code', true, textNode);
162
+ }
163
+ });
164
+ });
165
+ }
166
+ /**
167
+ * @inheritDoc
168
+ */
169
+ afterInit() {
170
+ const editor = this.editor;
171
+ const commands = editor.commands;
172
+ const indent = commands.get('indent');
173
+ const outdent = commands.get('outdent');
174
+ if (indent) {
175
+ // Priority is highest due to integration with `IndentList` command of `List` plugin.
176
+ // If selection is in a code block we give priority to it. This way list item cannot be indented
177
+ // but if we would give priority to indenting list item then user would have to indent list item
178
+ // as much as possible and only then he could indent code block.
179
+ indent.registerChildCommand(commands.get('indentCodeBlock'), { priority: 'highest' });
180
+ }
181
+ if (outdent) {
182
+ outdent.registerChildCommand(commands.get('outdentCodeBlock'));
183
+ }
184
+ // Customize the response to the <kbd>Enter</kbd> and <kbd>Shift</kbd>+<kbd>Enter</kbd>
185
+ // key press when the selection is in the code block. Upon enter key press we can either
186
+ // leave the block if it's "two or three enters" in a row or create a new code block line, preserving
187
+ // previous line's indentation.
188
+ this.listenTo(editor.editing.view.document, 'enter', (evt, data) => {
189
+ const positionParent = editor.model.document.selection.getLastPosition().parent;
190
+ if (!positionParent.is('element', 'codeBlock')) {
191
+ return;
192
+ }
193
+ if (!leaveBlockStartOnEnter(editor, data.isSoft) && !leaveBlockEndOnEnter(editor, data.isSoft)) {
194
+ breakLineOnEnter(editor);
195
+ }
196
+ data.preventDefault();
197
+ evt.stop();
198
+ }, { context: 'pre' });
199
+ }
200
+ }
201
+ /**
202
+ * Normally, when the Enter (or Shift+Enter) key is pressed, a soft line break is to be added to the
203
+ * code block. Let's try to follow the indentation of the previous line when possible, for instance:
204
+ *
205
+ * ```html
206
+ * // Before pressing enter (or shift enter)
207
+ * <codeBlock>
208
+ * " foo()"[] // Indent of 4 spaces.
209
+ * </codeBlock>
210
+ *
211
+ * // After pressing:
212
+ * <codeBlock>
213
+ * " foo()" // Indent of 4 spaces.
214
+ * <softBreak></softBreak> // A new soft break created by pressing enter.
215
+ * " "[] // Retain the indent of 4 spaces.
216
+ * </codeBlock>
217
+ * ```
218
+ */
219
+ function breakLineOnEnter(editor) {
220
+ const model = editor.model;
221
+ const modelDoc = model.document;
222
+ const lastSelectionPosition = modelDoc.selection.getLastPosition();
223
+ const node = lastSelectionPosition.nodeBefore || lastSelectionPosition.textNode;
224
+ let leadingWhiteSpaces;
225
+ // Figure out the indentation (white space chars) at the beginning of the line.
226
+ if (node && node.is('$text')) {
227
+ leadingWhiteSpaces = getLeadingWhiteSpaces(node);
228
+ }
229
+ // Keeping everything in a change block for a single undo step.
230
+ editor.model.change(writer => {
231
+ editor.execute('shiftEnter');
232
+ // If the line before being broken in two had some indentation, let's retain it
233
+ // in the new line.
234
+ if (leadingWhiteSpaces) {
235
+ writer.insertText(leadingWhiteSpaces, modelDoc.selection.anchor);
236
+ }
237
+ });
238
+ }
239
+ /**
240
+ * Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the beginning
241
+ * of the code block:
242
+ *
243
+ * ```html
244
+ * // Before:
245
+ * <codeBlock>[]<softBreak></softBreak>foo</codeBlock>
246
+ *
247
+ * // After pressing:
248
+ * <paragraph>[]</paragraph><codeBlock>foo</codeBlock>
249
+ * ```
250
+ *
251
+ * @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
252
+ * @returns `true` when selection left the block. `false` if stayed.
253
+ */
254
+ function leaveBlockStartOnEnter(editor, isSoftEnter) {
255
+ const model = editor.model;
256
+ const modelDoc = model.document;
257
+ const view = editor.editing.view;
258
+ const lastSelectionPosition = modelDoc.selection.getLastPosition();
259
+ const nodeAfter = lastSelectionPosition.nodeAfter;
260
+ if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtStart) {
261
+ return false;
262
+ }
263
+ if (!isSoftBreakNode(nodeAfter)) {
264
+ return false;
265
+ }
266
+ // We're doing everything in a single change block to have a single undo step.
267
+ editor.model.change(writer => {
268
+ // "Clone" the <codeBlock> in the standard way.
269
+ editor.execute('enter');
270
+ // The cloned block exists now before the original code block.
271
+ const newBlock = modelDoc.selection.anchor.parent.previousSibling;
272
+ // Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
273
+ writer.rename(newBlock, DEFAULT_ELEMENT);
274
+ writer.setSelection(newBlock, 'in');
275
+ editor.model.schema.removeDisallowedAttributes([newBlock], writer);
276
+ // Remove the <softBreak> that originally followed the selection position.
277
+ writer.remove(nodeAfter);
278
+ });
279
+ // Eye candy.
280
+ view.scrollToTheSelection();
281
+ return true;
282
+ }
283
+ /**
284
+ * Leave the code block when Enter (but NOT Shift+Enter) has been pressed twice at the end
285
+ * of the code block:
286
+ *
287
+ * ```html
288
+ * // Before:
289
+ * <codeBlock>foo[]</codeBlock>
290
+ *
291
+ * // After first press:
292
+ * <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
293
+ *
294
+ * // After second press:
295
+ * <codeBlock>foo</codeBlock><paragraph>[]</paragraph>
296
+ * ```
297
+ *
298
+ * @param isSoftEnter When `true`, enter was pressed along with <kbd>Shift</kbd>.
299
+ * @returns `true` when selection left the block. `false` if stayed.
300
+ */
301
+ function leaveBlockEndOnEnter(editor, isSoftEnter) {
302
+ const model = editor.model;
303
+ const modelDoc = model.document;
304
+ const view = editor.editing.view;
305
+ const lastSelectionPosition = modelDoc.selection.getLastPosition();
306
+ const nodeBefore = lastSelectionPosition.nodeBefore;
307
+ let emptyLineRangeToRemoveOnEnter;
308
+ if (isSoftEnter || !modelDoc.selection.isCollapsed || !lastSelectionPosition.isAtEnd || !nodeBefore || !nodeBefore.previousSibling) {
309
+ return false;
310
+ }
311
+ // When the position is directly preceded by two soft breaks
312
+ //
313
+ // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>[]</codeBlock>
314
+ //
315
+ // it creates the following range that will be cleaned up before leaving:
316
+ //
317
+ // <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak>]</codeBlock>
318
+ //
319
+ if (isSoftBreakNode(nodeBefore) && isSoftBreakNode(nodeBefore.previousSibling)) {
320
+ emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling), model.createPositionAfter(nodeBefore));
321
+ }
322
+ // When there's some text before the position that is
323
+ // preceded by two soft breaks and made purely of white–space characters
324
+ //
325
+ // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> []</codeBlock>
326
+ //
327
+ // it creates the following range to clean up before leaving:
328
+ //
329
+ // <codeBlock>foo[<softBreak></softBreak><softBreak></softBreak> ]</codeBlock>
330
+ //
331
+ else if (isEmptyishTextNode(nodeBefore) &&
332
+ isSoftBreakNode(nodeBefore.previousSibling) &&
333
+ isSoftBreakNode(nodeBefore.previousSibling.previousSibling)) {
334
+ emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
335
+ }
336
+ // When there's some text before the position that is made purely of white–space characters
337
+ // and is preceded by some other text made purely of white–space characters
338
+ //
339
+ // <codeBlock>foo<softBreak></softBreak> <softBreak></softBreak> []</codeBlock>
340
+ //
341
+ // it creates the following range to clean up before leaving:
342
+ //
343
+ // <codeBlock>foo[<softBreak></softBreak> <softBreak></softBreak> ]</codeBlock>
344
+ //
345
+ else if (isEmptyishTextNode(nodeBefore) &&
346
+ isSoftBreakNode(nodeBefore.previousSibling) &&
347
+ isEmptyishTextNode(nodeBefore.previousSibling.previousSibling) &&
348
+ nodeBefore.previousSibling.previousSibling &&
349
+ isSoftBreakNode(nodeBefore.previousSibling.previousSibling.previousSibling)) {
350
+ emptyLineRangeToRemoveOnEnter = model.createRange(model.createPositionBefore(nodeBefore.previousSibling.previousSibling.previousSibling), model.createPositionAfter(nodeBefore));
351
+ }
352
+ // Not leaving the block in the following cases:
353
+ //
354
+ // <codeBlock> []</codeBlock>
355
+ // <codeBlock> a []</codeBlock>
356
+ // <codeBlock>foo<softBreak></softBreak>[]</codeBlock>
357
+ // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak>bar[]</codeBlock>
358
+ // <codeBlock>foo<softBreak></softBreak><softBreak></softBreak> a []</codeBlock>
359
+ //
360
+ else {
361
+ return false;
362
+ }
363
+ // We're doing everything in a single change block to have a single undo step.
364
+ editor.model.change(writer => {
365
+ // Remove the last <softBreak>s and all white space characters that followed them.
366
+ writer.remove(emptyLineRangeToRemoveOnEnter);
367
+ // "Clone" the <codeBlock> in the standard way.
368
+ editor.execute('enter');
369
+ const newBlock = modelDoc.selection.anchor.parent;
370
+ // Make the cloned <codeBlock> a regular <paragraph> (with clean attributes, so no language).
371
+ writer.rename(newBlock, DEFAULT_ELEMENT);
372
+ editor.model.schema.removeDisallowedAttributes([newBlock], writer);
373
+ });
374
+ // Eye candy.
375
+ view.scrollToTheSelection();
376
+ return true;
377
+ }
378
+ function isEmptyishTextNode(node) {
379
+ return node && node.is('$text') && !node.data.match(/\S/);
380
+ }
381
+ function isSoftBreakNode(node) {
382
+ return node && node.is('element', 'softBreak');
383
+ }