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