@ckeditor/ckeditor5-code-block 42.0.2 → 43.0.0-alpha.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.
@@ -2,4 +2,4 @@
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
- export default {"sr-latn":{"dictionary":{"Insert code block":"Dodaj blok koda","Plain text":"Običan tekst","Leaving %0 code snippet":"","Entering %0 code snippet":"","Entering code snippet":"","Leaving code snippet":"","Code block":""},getPluralForm(n){return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}}}
5
+ export default {"sr-latn":{"dictionary":{"Insert code block":"Dodaj blok koda","Plain text":"Običan tekst","Leaving %0 code snippet":"Ostavljate %0 isečak koda","Entering %0 code snippet":"Unosite %0 isečak koda","Entering code snippet":"Unošenje isečka koda","Leaving code snippet":"Ostavljanje fragmenta koda  ","Code block":"Blok koda"},getPluralForm(n){return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}}}
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  ( e => {
7
- const { [ 'sr-latn' ]: { dictionary, getPluralForm } } = {"sr-latn":{"dictionary":{"Insert code block":"Dodaj blok koda","Plain text":"Običan tekst","Leaving %0 code snippet":"","Entering %0 code snippet":"","Entering code snippet":"","Leaving code snippet":"","Code block":""},getPluralForm(n){return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}}};
7
+ const { [ 'sr-latn' ]: { dictionary, getPluralForm } } = {"sr-latn":{"dictionary":{"Insert code block":"Dodaj blok koda","Plain text":"Običan tekst","Leaving %0 code snippet":"Ostavljate %0 isečak koda","Entering %0 code snippet":"Unosite %0 isečak koda","Entering code snippet":"Unošenje isečka koda","Leaving code snippet":"Ostavljanje fragmenta koda  ","Code block":"Blok koda"},getPluralForm(n){return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}}};
8
8
  e[ 'sr-latn' ] ||= { dictionary: {}, getPluralForm: null };
9
9
  e[ 'sr-latn' ].dictionary = Object.assign( e[ 'sr-latn' ].dictionary, dictionary );
10
10
  e[ 'sr-latn' ].getPluralForm = getPluralForm;
package/dist/utils.d.ts CHANGED
@@ -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;
@@ -27,20 +27,20 @@ msgstr "Običan tekst"
27
27
 
28
28
  msgctxt "Assistive technologies label for leaving the code block with a specified programming language. Example: 'Leaving JavaScript code snippet'"
29
29
  msgid "Leaving %0 code snippet"
30
- msgstr ""
30
+ msgstr "Ostavljate %0 isečak koda"
31
31
 
32
32
  msgctxt "Assistive technologies label for entering the code block with a specified programming language. Example: 'Entering JavaScript code snippet'"
33
33
  msgid "Entering %0 code snippet"
34
- msgstr ""
34
+ msgstr "Unosite %0 isečak koda"
35
35
 
36
36
  msgctxt "Assistive technologies label for entering the code block with unspecified programming language."
37
37
  msgid "Entering code snippet"
38
- msgstr ""
38
+ msgstr "Unošenje isečka koda"
39
39
 
40
40
  msgctxt "Assistive technologies label for leaving the code block with unspecified programming language."
41
41
  msgid "Leaving code snippet"
42
- msgstr ""
42
+ msgstr "Ostavljanje fragmenta koda  "
43
43
 
44
44
  msgctxt "The accessible label of the menu bar button that inserts a code block into editor content."
45
45
  msgid "Code block"
46
- msgstr ""
46
+ msgstr "Blok koda"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ckeditor/ckeditor5-code-block",
3
- "version": "42.0.2",
3
+ "version": "43.0.0-alpha.0",
4
4
  "description": "Code block feature for CKEditor 5.",
5
5
  "keywords": [
6
6
  "ckeditor",
@@ -13,13 +13,13 @@
13
13
  "type": "module",
14
14
  "main": "src/index.js",
15
15
  "dependencies": {
16
- "@ckeditor/ckeditor5-core": "42.0.2",
17
- "@ckeditor/ckeditor5-engine": "42.0.2",
18
- "@ckeditor/ckeditor5-enter": "42.0.2",
19
- "@ckeditor/ckeditor5-ui": "42.0.2",
20
- "@ckeditor/ckeditor5-utils": "42.0.2",
21
- "ckeditor5": "42.0.2",
22
- "lodash-es": "4.17.21"
16
+ "@ckeditor/ckeditor5-core": "43.0.0-alpha.0",
17
+ "@ckeditor/ckeditor5-clipboard": "43.0.0-alpha.0",
18
+ "@ckeditor/ckeditor5-engine": "43.0.0-alpha.0",
19
+ "@ckeditor/ckeditor5-enter": "43.0.0-alpha.0",
20
+ "@ckeditor/ckeditor5-ui": "43.0.0-alpha.0",
21
+ "@ckeditor/ckeditor5-utils": "43.0.0-alpha.0",
22
+ "ckeditor5": "43.0.0-alpha.0"
23
23
  },
24
24
  "author": "CKSource (http://cksource.com/)",
25
25
  "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
  /**
@@ -90,9 +94,11 @@ export default class CodeBlockEditing extends Plugin {
90
94
  allowAttributesOf: '$listItem',
91
95
  isBlock: true
92
96
  });
93
- // Disallow all attributes on `$text` inside `codeBlock`.
94
- schema.addAttributeCheck(context => {
95
- if (context.endsWith('codeBlock $text')) {
97
+ // Disallow formatting attributes on `codeBlock` children.
98
+ schema.addAttributeCheck((context, attributeName) => {
99
+ const parent = context.getItem(context.length - 2);
100
+ const isFormatting = schema.getAttributeProperties(attributeName).isFormatting;
101
+ if (isFormatting && parent && parent.name == 'codeBlock') {
96
102
  return false;
97
103
  }
98
104
  });
@@ -120,6 +126,27 @@ export default class CodeBlockEditing extends Plugin {
120
126
  // Pass the view fragment to the default clipboardInput handler.
121
127
  data.content = rawSnippetTextToViewDocumentFragment(writer, text);
122
128
  });
129
+ if (editor.plugins.has('ClipboardPipeline')) {
130
+ // Elements may have a plain textual representation (hence be present in the 'text/plain' data transfer),
131
+ // but not be allowed in the code block.
132
+ // Filter them out before inserting the content to the model.
133
+ editor.plugins.get(ClipboardPipeline).on('contentInsertion', (evt, data) => {
134
+ const model = editor.model;
135
+ const selection = model.document.selection;
136
+ if (!selection.anchor.parent.is('element', 'codeBlock')) {
137
+ return;
138
+ }
139
+ model.change(writer => {
140
+ const contentRange = writer.createRangeIn(data.content);
141
+ for (const item of [...contentRange.getItems()]) {
142
+ // Remove all nodes disallowed in the code block.
143
+ if (item.is('node') && !schema.checkChild(selection.anchor, item)) {
144
+ writer.remove(item);
145
+ }
146
+ }
147
+ });
148
+ });
149
+ }
123
150
  // Make sure multi–line selection is always wrapped in a code block when `getSelectedContent()`
124
151
  // is used (e.g. clipboard copy). Otherwise, only the raw text will be copied to the clipboard and,
125
152
  // upon next paste, this bare text will not be inserted as a code block, which is not the best UX.
@@ -231,9 +258,10 @@ export default class CodeBlockEditing extends Plugin {
231
258
  function breakLineOnEnter(editor) {
232
259
  const model = editor.model;
233
260
  const modelDoc = model.document;
261
+ // Use last position as other mechanisms (e.g. condition deciding whether this function should be called) also check that.
234
262
  const lastSelectionPosition = modelDoc.selection.getLastPosition();
235
- const node = lastSelectionPosition.nodeBefore || lastSelectionPosition.textNode;
236
263
  let leadingWhiteSpaces;
264
+ const node = getTextNodeAtLineStart(lastSelectionPosition, model);
237
265
  // Figure out the indentation (white space chars) at the beginning of the line.
238
266
  if (node && node.is('$text')) {
239
267
  leadingWhiteSpaces = getLeadingWhiteSpaces(node);
@@ -67,6 +67,7 @@ export default class CodeBlockUI extends Plugin {
67
67
  componentFactory.add('menuBar:codeBlock', locale => {
68
68
  const menuView = new MenuBarMenuView(locale);
69
69
  menuView.buttonView.set({
70
+ role: 'menuitem',
70
71
  label: t('Code block'),
71
72
  icon: icons.codeBlock
72
73
  });
@@ -79,7 +80,10 @@ export default class CodeBlockUI extends Plugin {
79
80
  const listItemView = new MenuBarMenuListItemView(locale, menuView);
80
81
  const buttonView = new MenuBarMenuListItemButtonView(locale);
81
82
  buttonView.bind(...Object.keys(definition.model)).to(definition.model);
82
- buttonView.bind('ariaChecked').to(buttonView, 'isOn');
83
+ buttonView.set({
84
+ isToggleable: true,
85
+ role: 'menuitemcheckbox'
86
+ });
83
87
  buttonView.delegate('execute').to(menuView);
84
88
  buttonView.on('execute', () => {
85
89
  editor.execute('codeBlock', {
@@ -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
+ }