@ckeditor/ckeditor5-code-block 0.0.0-nightly-20240715.0 → 0.0.0-nightly-20240717.0

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.
@@ -6,6 +6,9 @@
6
6
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
7
7
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
8
8
  */
9
+ /**
10
+ * @module code-block/codeblockediting
11
+ */
9
12
  import { Plugin, type Editor } from 'ckeditor5/src/core.js';
10
13
  import { ShiftEnter } from 'ckeditor5/src/enter.js';
11
14
  /**
@@ -145,3 +145,37 @@ export declare function canBeCodeBlock(schema: Schema, element: Element): boolea
145
145
  * Get the translated message read by the screen reader when you enter or exit an element with your cursor.
146
146
  */
147
147
  export declare function getCodeBlockAriaAnnouncement(t: LocaleTranslate, languageDefs: Array<CodeBlockLanguageDefinition>, element: Element, direction: 'enter' | 'leave'): string;
148
+ /**
149
+ * 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
150
+ * beginning of the line (or `null` if there's no text node at the beginning of a given line).
151
+ *
152
+ * Line beings at the start of a code block element and after each `softBreak` element.
153
+ *
154
+ * Note: even though code block doesn't allow inline elements other than `<softBreak>` by default, some features may overwrite this rule,
155
+ * so such inline elements are taken into account.
156
+ *
157
+ * Some examples of expected results:
158
+ *
159
+ * ```
160
+ * <codeBlock>^</codeBlock> -> null
161
+ * <codeBlock>^foobar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
162
+ * <codeBlock>foobar^</codeBlock> -> <codeBlock>[foobar]</codeBlock>
163
+ * <codeBlock>foo^bar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
164
+ * <codeBlock>foo^<softBreak />bar</codeBlock> -> <codeBlock>[foo]<softBreak />bar</codeBlock>
165
+ * <codeBlock>foo<softBreak />bar^</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
166
+ * <codeBlock>foo<softBreak />b^ar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
167
+ * <codeBlock>foo<softBreak />^bar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
168
+ * <codeBlock>^<element /></codeBlock> -> null
169
+ * <codeBlock><element />^</codeBlock> -> null
170
+ * <codeBlock>foo^<element /></codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
171
+ * <codeBlock>foo<element />^</codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
172
+ * <codeBlock>foo<element />bar^</codeBlock> -> <codeBlock>[foo]<element />bar</codeBlock>
173
+ * <codeBlock><element />bar^</codeBlock> -> null
174
+ * <codeBlock>foo<softBreak />^<softBreak /></codeBlock> -> null
175
+ * <codeBlock>foo<softBreak />^<element /></codeBlock> -> null
176
+ * <codeBlock>foo<softBreak /><element />^</codeBlock> -> null
177
+ * <codeBlock>foo<softBreak />bar<element />^</codeBlock> -> <codeBlock>foo<softBreak />[bar]<element /></codeBlock>
178
+ * <codeBlock>foo<softBreak /><element />ba^r</codeBlock> -> null
179
+ * ```
180
+ */
181
+ export declare function getTextNodeAtLineStart(position: Position, model: Model): Text | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-code-block",
3
- "version": "0.0.0-nightly-20240715.0",
3
+ "version": "0.0.0-nightly-20240717.0",
4
4
  "description": "Code block feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -13,8 +13,7 @@
13
13
  "type": "module",
14
14
  "main": "src/index.js",
15
15
  "dependencies": {
16
- "ckeditor5": "0.0.0-nightly-20240715.0",
17
- "lodash-es": "4.17.21"
16
+ "ckeditor5": "0.0.0-nightly-20240717.0"
18
17
  },
19
18
  "author": "CKSource (http://cksource.com/)",
20
19
  "license": "GPL-2.0-or-later",
@@ -2,6 +2,9 @@
2
2
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
+ /**
6
+ * @module code-block/codeblockediting
7
+ */
5
8
  import { Plugin, type Editor } from 'ckeditor5/src/core.js';
6
9
  import { ShiftEnter } from 'ckeditor5/src/enter.js';
7
10
  /**
@@ -2,13 +2,17 @@
2
2
  * @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
+ /**
6
+ * @module code-block/codeblockediting
7
+ */
5
8
  import { Plugin } from 'ckeditor5/src/core.js';
6
9
  import { ShiftEnter } from 'ckeditor5/src/enter.js';
7
10
  import { UpcastWriter } from 'ckeditor5/src/engine.js';
11
+ import { ClipboardPipeline } from 'ckeditor5/src/clipboard.js';
8
12
  import CodeBlockCommand from './codeblockcommand.js';
9
13
  import IndentCodeBlockCommand from './indentcodeblockcommand.js';
10
14
  import OutdentCodeBlockCommand from './outdentcodeblockcommand.js';
11
- import { getNormalizedAndLocalizedLanguageDefinitions, getLeadingWhiteSpaces, rawSnippetTextToViewDocumentFragment, getCodeBlockAriaAnnouncement } from './utils.js';
15
+ import { getNormalizedAndLocalizedLanguageDefinitions, getLeadingWhiteSpaces, rawSnippetTextToViewDocumentFragment, getCodeBlockAriaAnnouncement, getTextNodeAtLineStart } from './utils.js';
12
16
  import { modelToViewCodeBlockInsertion, modelToDataViewSoftBreakInsertion, dataViewToModelCodeBlockInsertion, dataViewToModelTextNewlinesInsertion, dataViewToModelOrphanNodeConsumer } from './converters.js';
13
17
  const DEFAULT_ELEMENT = 'paragraph';
14
18
  /**
@@ -120,6 +124,27 @@ export default class CodeBlockEditing extends Plugin {
120
124
  // Pass the view fragment to the default clipboardInput handler.
121
125
  data.content = rawSnippetTextToViewDocumentFragment(writer, text);
122
126
  });
127
+ if (editor.plugins.has('ClipboardPipeline')) {
128
+ // Elements may have a plain textual representation (hence be present in the 'text/plain' data transfer),
129
+ // but not be allowed in the code block.
130
+ // Filter them out before inserting the content to the model.
131
+ editor.plugins.get(ClipboardPipeline).on('contentInsertion', (evt, data) => {
132
+ const model = editor.model;
133
+ const selection = model.document.selection;
134
+ if (!selection.anchor.parent.is('element', 'codeBlock')) {
135
+ return;
136
+ }
137
+ model.change(writer => {
138
+ const contentRange = writer.createRangeIn(data.content);
139
+ for (const item of [...contentRange.getItems()]) {
140
+ // Remove all nodes disallowed in the code block.
141
+ if (item.is('node') && !schema.checkChild(selection.anchor, item)) {
142
+ writer.remove(item);
143
+ }
144
+ }
145
+ });
146
+ });
147
+ }
123
148
  // Make sure multi–line selection is always wrapped in a code block when `getSelectedContent()`
124
149
  // is used (e.g. clipboard copy). Otherwise, only the raw text will be copied to the clipboard and,
125
150
  // upon next paste, this bare text will not be inserted as a code block, which is not the best UX.
@@ -231,9 +256,10 @@ export default class CodeBlockEditing extends Plugin {
231
256
  function breakLineOnEnter(editor) {
232
257
  const model = editor.model;
233
258
  const modelDoc = model.document;
259
+ // Use last position as other mechanisms (e.g. condition deciding whether this function should be called) also check that.
234
260
  const lastSelectionPosition = modelDoc.selection.getLastPosition();
235
- const node = lastSelectionPosition.nodeBefore || lastSelectionPosition.textNode;
236
261
  let leadingWhiteSpaces;
262
+ const node = getTextNodeAtLineStart(lastSelectionPosition, model);
237
263
  // Figure out the indentation (white space chars) at the beginning of the line.
238
264
  if (node && node.is('$text')) {
239
265
  leadingWhiteSpaces = getLeadingWhiteSpaces(node);
@@ -3,7 +3,7 @@
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
4
4
  */
5
5
  import { Command } from 'ckeditor5/src/core.js';
6
- import { getLeadingWhiteSpaces, getIndentOutdentPositions, isModelSelectionInCodeBlock } from './utils.js';
6
+ import { getLeadingWhiteSpaces, getIndentOutdentPositions, isModelSelectionInCodeBlock, getTextNodeAtLineStart } from './utils.js';
7
7
  /**
8
8
  * The code block indentation decrease command plugin.
9
9
  */
@@ -101,7 +101,7 @@ export default class OutdentCodeBlockCommand extends Command {
101
101
  // @returns {<module:engine/model/range~Range>|null}
102
102
  function getLastOutdentableSequenceRange(model, position, sequence) {
103
103
  // Positions start before each text node (code line). Get the node corresponding to the position.
104
- const nodeAtPosition = getCodeLineTextNodeAtPosition(position);
104
+ const nodeAtPosition = getTextNodeAtLineStart(position, model);
105
105
  if (!nodeAtPosition) {
106
106
  return null;
107
107
  }
@@ -131,18 +131,3 @@ function getLastOutdentableSequenceRange(model, position, sequence) {
131
131
  //
132
132
  return model.createRange(model.createPositionAt(parent, startOffset + lastIndexOfSequence), model.createPositionAt(parent, startOffset + lastIndexOfSequence + sequence.length));
133
133
  }
134
- function getCodeLineTextNodeAtPosition(position) {
135
- // Positions start before each text node (code line). Get the node corresponding to the position.
136
- let nodeAtPosition = position.parent.getChild(position.index);
137
- // <codeBlock>foo^</codeBlock>
138
- // <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
139
- if (!nodeAtPosition || nodeAtPosition.is('element', 'softBreak')) {
140
- nodeAtPosition = position.nodeBefore;
141
- }
142
- // <codeBlock>^</codeBlock>
143
- // <codeBlock>foo^<softBreak></softBreak>bar</codeBlock>
144
- if (!nodeAtPosition || nodeAtPosition.is('element', 'softBreak')) {
145
- return null;
146
- }
147
- return nodeAtPosition;
148
- }
package/src/utils.d.ts CHANGED
@@ -141,3 +141,37 @@ export declare function canBeCodeBlock(schema: Schema, element: Element): boolea
141
141
  * Get the translated message read by the screen reader when you enter or exit an element with your cursor.
142
142
  */
143
143
  export declare function getCodeBlockAriaAnnouncement(t: LocaleTranslate, languageDefs: Array<CodeBlockLanguageDefinition>, element: Element, direction: 'enter' | 'leave'): string;
144
+ /**
145
+ * 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
146
+ * beginning of the line (or `null` if there's no text node at the beginning of a given line).
147
+ *
148
+ * Line beings at the start of a code block element and after each `softBreak` element.
149
+ *
150
+ * Note: even though code block doesn't allow inline elements other than `<softBreak>` by default, some features may overwrite this rule,
151
+ * so such inline elements are taken into account.
152
+ *
153
+ * Some examples of expected results:
154
+ *
155
+ * ```
156
+ * <codeBlock>^</codeBlock> -> null
157
+ * <codeBlock>^foobar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
158
+ * <codeBlock>foobar^</codeBlock> -> <codeBlock>[foobar]</codeBlock>
159
+ * <codeBlock>foo^bar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
160
+ * <codeBlock>foo^<softBreak />bar</codeBlock> -> <codeBlock>[foo]<softBreak />bar</codeBlock>
161
+ * <codeBlock>foo<softBreak />bar^</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
162
+ * <codeBlock>foo<softBreak />b^ar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
163
+ * <codeBlock>foo<softBreak />^bar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
164
+ * <codeBlock>^<element /></codeBlock> -> null
165
+ * <codeBlock><element />^</codeBlock> -> null
166
+ * <codeBlock>foo^<element /></codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
167
+ * <codeBlock>foo<element />^</codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
168
+ * <codeBlock>foo<element />bar^</codeBlock> -> <codeBlock>[foo]<element />bar</codeBlock>
169
+ * <codeBlock><element />bar^</codeBlock> -> null
170
+ * <codeBlock>foo<softBreak />^<softBreak /></codeBlock> -> null
171
+ * <codeBlock>foo<softBreak />^<element /></codeBlock> -> null
172
+ * <codeBlock>foo<softBreak /><element />^</codeBlock> -> null
173
+ * <codeBlock>foo<softBreak />bar<element />^</codeBlock> -> <codeBlock>foo<softBreak />[bar]<element /></codeBlock>
174
+ * <codeBlock>foo<softBreak /><element />ba^r</codeBlock> -> null
175
+ * ```
176
+ */
177
+ export declare function getTextNodeAtLineStart(position: Position, model: Model): Text | null;
package/src/utils.js CHANGED
@@ -173,17 +173,23 @@ export function getIndentOutdentPositions(model) {
173
173
  direction: 'backward'
174
174
  });
175
175
  for (const { item } of walker) {
176
- if (!item.is('$textProxy')) {
176
+ let node = item.is('$textProxy') ? item.textNode : item;
177
+ const parent = node.parent;
178
+ if (!parent.is('element', 'codeBlock') || node.is('element', 'softBreak')) {
177
179
  continue;
178
180
  }
179
- const { parent, startOffset } = item.textNode;
180
- if (!parent.is('element', 'codeBlock')) {
181
- continue;
181
+ // For each item in code block, move backwards until the beginning of the line it is in is found.
182
+ while (node.previousSibling && !node.previousSibling.is('element', 'softBreak')) {
183
+ node = node.previousSibling;
184
+ }
185
+ // Take the leading white spaces into account (only for text nodes).
186
+ const startOffset = !node.is('$text') ? node.startOffset : node.startOffset + getLeadingWhiteSpaces(node).length;
187
+ const position = model.createPositionAt(parent, startOffset);
188
+ // Do not add the same position twice. Unfortunately using set doesn't deduplicate positions because
189
+ // they are different objects.
190
+ if (positions.every(pos => !pos.isEqual(position))) {
191
+ positions.push(position);
182
192
  }
183
- const leadingWhiteSpaces = getLeadingWhiteSpaces(item.textNode);
184
- // Make sure the position is after all leading whitespaces in the text node.
185
- const position = model.createPositionAt(parent, startOffset + leadingWhiteSpaces.length);
186
- positions.push(position);
187
193
  }
188
194
  return positions;
189
195
  }
@@ -225,3 +231,50 @@ export function getCodeBlockAriaAnnouncement(t, languageDefs, element, direction
225
231
  }
226
232
  return t('Leaving code snippet');
227
233
  }
234
+ /**
235
+ * 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
236
+ * beginning of the line (or `null` if there's no text node at the beginning of a given line).
237
+ *
238
+ * Line beings at the start of a code block element and after each `softBreak` element.
239
+ *
240
+ * Note: even though code block doesn't allow inline elements other than `<softBreak>` by default, some features may overwrite this rule,
241
+ * so such inline elements are taken into account.
242
+ *
243
+ * Some examples of expected results:
244
+ *
245
+ * ```
246
+ * <codeBlock>^</codeBlock> -> null
247
+ * <codeBlock>^foobar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
248
+ * <codeBlock>foobar^</codeBlock> -> <codeBlock>[foobar]</codeBlock>
249
+ * <codeBlock>foo^bar</codeBlock> -> <codeBlock>[foobar]</codeBlock>
250
+ * <codeBlock>foo^<softBreak />bar</codeBlock> -> <codeBlock>[foo]<softBreak />bar</codeBlock>
251
+ * <codeBlock>foo<softBreak />bar^</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
252
+ * <codeBlock>foo<softBreak />b^ar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
253
+ * <codeBlock>foo<softBreak />^bar</codeBlock> -> <codeBlock>foo<softBreak />[bar]</codeBlock>
254
+ * <codeBlock>^<element /></codeBlock> -> null
255
+ * <codeBlock><element />^</codeBlock> -> null
256
+ * <codeBlock>foo^<element /></codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
257
+ * <codeBlock>foo<element />^</codeBlock> -> <codeBlock>[foo]<element /></codeBlock>
258
+ * <codeBlock>foo<element />bar^</codeBlock> -> <codeBlock>[foo]<element />bar</codeBlock>
259
+ * <codeBlock><element />bar^</codeBlock> -> null
260
+ * <codeBlock>foo<softBreak />^<softBreak /></codeBlock> -> null
261
+ * <codeBlock>foo<softBreak />^<element /></codeBlock> -> null
262
+ * <codeBlock>foo<softBreak /><element />^</codeBlock> -> null
263
+ * <codeBlock>foo<softBreak />bar<element />^</codeBlock> -> <codeBlock>foo<softBreak />[bar]<element /></codeBlock>
264
+ * <codeBlock>foo<softBreak /><element />ba^r</codeBlock> -> null
265
+ * ```
266
+ */
267
+ export function getTextNodeAtLineStart(position, model) {
268
+ // First, move position before a text node, if it is inside a text node.
269
+ if (position.textNode) {
270
+ position = model.createPositionBefore(position.textNode);
271
+ }
272
+ // Then, jump-back the position until it is before a `softBreak` or at the beginning of the `codeBlock`.
273
+ while (position.nodeBefore && !position.nodeBefore.is('element', 'softBreak')) {
274
+ position = model.createPositionBefore(position.nodeBefore);
275
+ }
276
+ // Now, the position is at the beginning of a line.
277
+ // Return a text node after the position, if there is one.
278
+ const nodeAtStart = position.nodeAfter;
279
+ return nodeAtStart && nodeAtStart.is('$text') ? nodeAtStart : null;
280
+ }