@ckeditor/ckeditor5-engine 48.0.1 → 48.1.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.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
+ */
5
+ /**
6
+ * @module engine/conversion/comparemarkers
7
+ */
8
+ import type { ModelRange } from '../model/range.js';
9
+ /**
10
+ * Sorts markers so the downcast result is deterministic regardless of the order
11
+ * markers were added to the marker collection.
12
+ *
13
+ * The sort key is the marker's range, ordered "right-to-left" through the document so that
14
+ * a marker's opening boundary is processed *after* any markers nested inside it. This way
15
+ * the outer marker wraps the inner ones at conversion time.
16
+ *
17
+ * Cases (positions shown as `0123456789`, sort result top-to-bottom):
18
+ *
19
+ * 1. Non-overlapping ranges — sorted by position, last range first:
20
+ *
21
+ * a: [--] → c, b, a
22
+ * b: [--]
23
+ * c: [--]
24
+ *
25
+ * 2. Adjacent ranges (end === start) — treated as non-overlapping:
26
+ *
27
+ * first: [---] → third, second, first
28
+ * second: [---]
29
+ * third: [---]
30
+ *
31
+ * 3. Nested ranges (same start, different ends) — inner first, outer last:
32
+ *
33
+ * shorter: [-] → shorter, longer
34
+ * longer: [---]
35
+ *
36
+ * 4. Partially overlapping ranges — sorted by start position:
37
+ *
38
+ * earlier: [---] → later, earlier
39
+ * later: [---]
40
+ *
41
+ * 5. Identical ranges — fall back to reverse name comparison:
42
+ *
43
+ * alpha: [---] → charlie, bravo, alpha
44
+ * bravo: [---]
45
+ * charlie: [---]
46
+ *
47
+ * @internal
48
+ */
49
+ export declare function compareMarkersForDowncast([name1, range1]: readonly [string, ModelRange], [name2, range2]: readonly [string, ModelRange]): number;
@@ -2,6 +2,12 @@
2
2
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
3
  * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
4
4
  */
5
+ /**
6
+ * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
7
+ * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
8
+ *
9
+ * @module engine/conversion/upcasthelpers
10
+ */
5
11
  import { type MatchClassPatterns, type MatcherPattern, type MatchPropertyPatterns } from '../view/matcher.js';
6
12
  import { ConversionHelpers } from './conversionhelpers.js';
7
13
  import type { UpcastDispatcher, UpcastConversionApi, UpcastConversionData } from './upcastdispatcher.js';
@@ -12,12 +18,6 @@ import { type Model } from '../model/model.js';
12
18
  import { type ViewSelection } from '../view/selection.js';
13
19
  import { type ViewDocumentSelection } from '../view/documentselection.js';
14
20
  import { type EventInfo, type PriorityString } from '@ckeditor/ckeditor5-utils';
15
- /**
16
- * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
17
- * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
18
- *
19
- * @module engine/conversion/upcasthelpers
20
- */
21
21
  /**
22
22
  * Upcast conversion helper functions.
23
23
  *
package/dist/index.d.ts CHANGED
@@ -11,19 +11,19 @@ export { EditingController } from './controller/editingcontroller.js';
11
11
  export { DataController, type DataControllerInitEvent, type DataControllerSetEvent, type DataControllerToModelEvent, type DataControllerToViewEvent, type DataControllerReadyEvent, type DataControllerGetEvent } from './controller/datacontroller.js';
12
12
  export { Conversion, type ConversionType } from './conversion/conversion.js';
13
13
  export { ConversionHelpers } from './conversion/conversionhelpers.js';
14
- export type { DowncastDispatcher, DowncastDispatcherEventMap, DowncastAddMarkerEvent, DowncastAttributeEvent, DowncastConversionApi, DowncastInsertEvent, DowncastRemoveEvent, DowncastRemoveMarkerEvent, DowncastSelectionEvent, DowncastReduceChangesEvent, DowncastReduceChangesEventData, DowncastEvent, DowncastCleanSelectionEvent } from './conversion/downcastdispatcher.js';
15
- export type { UpcastDispatcher, UpcastConversionApi, UpcastConversionData, UpcastElementEvent, UpcastTextEvent, UpcastViewCleanupEvent, UpcastEvent, UpcastDocumentFragmentEvent } from './conversion/upcastdispatcher.js';
14
+ export { DowncastDispatcher, type DowncastDispatcherEventMap, type DowncastAddMarkerEvent, type DowncastAttributeEvent, type DowncastConversionApi, type DowncastInsertEvent, type DowncastRemoveEvent, type DowncastRemoveMarkerEvent, type DowncastSelectionEvent, type DowncastReduceChangesEvent, type DowncastReduceChangesEventData, type DowncastEvent, type DowncastCleanSelectionEvent } from './conversion/downcastdispatcher.js';
15
+ export { UpcastDispatcher, type UpcastConversionApi, type UpcastConversionData, type UpcastElementEvent, type UpcastTextEvent, type UpcastViewCleanupEvent, type UpcastEvent, type UpcastDocumentFragmentEvent } from './conversion/upcastdispatcher.js';
16
16
  export { UpcastHelpers } from './conversion/upcasthelpers.js';
17
17
  export { DowncastHelpers, type DowncastStructureCreatorFunction, type DowncastAttributeElementCreatorFunction, type DowncastElementCreatorFunction, type DowncastHighlightDescriptor, type DowncastSlotFilter, type DowncastAttributeDescriptor, type DowncastMarkerElementCreatorFunction, type DowncastAddHighlightCallback, type DowncastHighlightDescriptorCreatorFunction, type DowncastRemoveHighlightCallback, type DowncastMarkerDataCreatorFunction, type DowncastAttributeCreatorFunction } from './conversion/downcasthelpers.js';
18
18
  export type { UpcastElementCreatorFunction, UpcastAttributeCreatorFunction, UpcastMarkerFromElementCreatorFunction, UpcastMarkerFromAttributeCreatorFunction } from './conversion/upcasthelpers.js';
19
19
  export { Mapper, type MapperModelToViewPositionEvent, type MapperViewToModelPositionEvent, type MapperModelToViewPositionEventData, type MapperViewToModelPositionEventData } from './conversion/mapper.js';
20
- export type { ModelConsumable } from './conversion/modelconsumable.js';
21
- export type { Consumables, ViewConsumable } from './conversion/viewconsumable.js';
20
+ export { ModelConsumable } from './conversion/modelconsumable.js';
21
+ export { type Consumables, ViewConsumable } from './conversion/viewconsumable.js';
22
22
  export type { DataProcessor } from './dataprocessor/dataprocessor.js';
23
23
  export type { DataProcessorHtmlWriter } from './dataprocessor/htmlwriter.js';
24
24
  export { HtmlDataProcessor } from './dataprocessor/htmldataprocessor.js';
25
25
  export { XmlDataProcessor } from './dataprocessor/xmldataprocessor.js';
26
- export type { Operation } from './model/operation/operation.js';
26
+ export { Operation } from './model/operation/operation.js';
27
27
  export { InsertOperation } from './model/operation/insertoperation.js';
28
28
  export { MoveOperation } from './model/operation/moveoperation.js';
29
29
  export { MergeOperation } from './model/operation/mergeoperation.js';
@@ -49,14 +49,14 @@ export { ModelDocument, type ModelPostFixer } from './model/document.js';
49
49
  export { History } from './model/history.js';
50
50
  export { ModelText } from './model/text.js';
51
51
  export { ModelTextProxy } from './model/textproxy.js';
52
- export { MarkerCollection, type Marker, type MarkerData, type MarkerChangeRangeEvent, type MarkerCollectionChangeContentEvent, type MarkerChangeEvent, type MarkerCollectionUpdateEvent } from './model/markercollection.js';
52
+ export { MarkerCollection, Marker, type MarkerData, type MarkerChangeRangeEvent, type MarkerCollectionChangeContentEvent, type MarkerChangeEvent, type MarkerCollectionUpdateEvent } from './model/markercollection.js';
53
53
  export { Batch, type BatchType } from './model/batch.js';
54
54
  export { Differ, type DifferItem, type DifferItemAttribute, type DifferItemInsert, type DifferItemRemove, type DifferItemAction, type DifferItemReinsert, type DifferItemRoot } from './model/differ.js';
55
55
  export type { ModelItem } from './model/item.js';
56
56
  export { ModelNode, type ModelNodeAttributes } from './model/node.js';
57
57
  export { ModelNodeList } from './model/nodelist.js';
58
58
  export { ModelRootElement } from './model/rootelement.js';
59
- export { ModelSchema, ModelSchemaContext, type ModelSchemaCheckChildEvent, type ModelSchemaCheckAttributeEvent, type ModelSchemaAttributeCheckCallback, type ModelSchemaChildCheckCallback, type ModelAttributeProperties, type ModelSchemaItemDefinition, type ModelSchemaCompiledItemDefinition, type ModelSchemaContextDefinition, type ModelSchemaContextItem } from './model/schema.js';
59
+ export { ModelSchema, ModelSchemaContext, type ModelSchemaCheckChildEvent, type ModelSchemaCheckAttributeEvent, type ModelSchemaAttributeCheckCallback, type ModelSchemaChildCheckCallback, type ModelAttributeProperties, type ModelSchemaItemDefinition, type ModelSchemaCompiledItemDefinition, type ModelSchemaContextDefinition, type ModelSchemaContextItem, type ModelBlockAlignmentAttributesMapping } from './model/schema.js';
60
60
  export { ModelSelection, type ModelSelectionChangeEvent, type ModelSelectionChangeRangeEvent, type ModelSelectionChangeAttributeEvent, type ModelSelectable, type ModelPlaceOrOffset } from './model/selection.js';
61
61
  export { ModelTypeCheckable } from './model/typecheckable.js';
62
62
  export { ModelWriter } from './model/writer.js';
package/dist/index.js CHANGED
@@ -3773,11 +3773,18 @@ ViewContainerElement.prototype.is = function(type, name) {
3773
3773
  const children = [
3774
3774
  ...this.getChildren()
3775
3775
  ];
3776
- const lastChild = children[this.childCount - 1];
3776
+ let lastChild = children[this.childCount - 1];
3777
3777
  // Block filler is required after a `<br>` if it's the last element in its container. See #1422.
3778
3778
  if (lastChild && lastChild.is('element', 'br')) {
3779
3779
  return this.childCount;
3780
3780
  }
3781
+ // Check if there is a `<br>` inside an attribute element at the end of container.
3782
+ while(lastChild && lastChild.is('attributeElement')){
3783
+ lastChild = lastChild.getChild(lastChild.childCount - 1);
3784
+ if (lastChild && lastChild.is('element', 'br')) {
3785
+ return this.childCount;
3786
+ }
3787
+ }
3781
3788
  for (const child of children){
3782
3789
  // If there's any non-UI element – don't render the bogus.
3783
3790
  if (!child.is('uiElement')) {
@@ -6060,6 +6067,11 @@ const contextsSymbol = Symbol('bubblingContexts');
6060
6067
  */ getRoot(name = 'main') {
6061
6068
  return this.roots.get(name);
6062
6069
  }
6070
+ /**
6071
+ * Returns an array with all roots added to the document.
6072
+ */ getRoots() {
6073
+ return Array.from(this.roots);
6074
+ }
6063
6075
  /**
6064
6076
  * Allows registering post-fixer callbacks. A post-fixers mechanism allows to update the view tree just before it is rendered
6065
6077
  * to the DOM.
@@ -8367,33 +8379,6 @@ const validNodesToInsert = [
8367
8379
  }
8368
8380
 
8369
8381
  /**
8370
- * Set of utilities related to handling block and inline fillers.
8371
- *
8372
- * Browsers do not allow to put caret in elements which does not have height. Because of it, we need to fill all
8373
- * empty elements which should be selectable with elements or characters called "fillers". Unfortunately there is no one
8374
- * universal filler, this is why two types are uses:
8375
- *
8376
- * * Block filler is an element which fill block elements, like `<p>`. CKEditor uses `<br>` as a block filler during the editing,
8377
- * as browsers do natively. So instead of an empty `<p>` there will be `<p><br></p>`. The advantage of block filler is that
8378
- * it is transparent for the selection, so when the caret is before the `<br>` and user presses right arrow he will be
8379
- * moved to the next paragraph, not after the `<br>`. The disadvantage is that it breaks a block, so it cannot be used
8380
- * in the middle of a line of text. The {@link module:engine/view/filler~BR_FILLER `<br>` filler} can be replaced with any other
8381
- * character in the data output, for instance {@link module:engine/view/filler~NBSP_FILLER non-breaking space} or
8382
- * {@link module:engine/view/filler~MARKED_NBSP_FILLER marked non-breaking space}.
8383
- *
8384
- * * Inline filler is a filler which does not break a line of text, so it can be used inside the text, for instance in the empty
8385
- * `<b>` surrendered by text: `foo<b></b>bar`, if we want to put the caret there. CKEditor uses a sequence of the zero-width
8386
- * spaces as an {@link module:engine/view/filler~INLINE_FILLER inline filler} having the predetermined
8387
- * {@link module:engine/view/filler~INLINE_FILLER_LENGTH length}. A sequence is used, instead of a single character to
8388
- * avoid treating random zero-width spaces as the inline filler. Disadvantage of the inline filler is that it is not
8389
- * transparent for the selection. The arrow key moves the caret between zero-width spaces characters, so the additional
8390
- * code is needed to handle the caret.
8391
- *
8392
- * Both inline and block fillers are handled by the {@link module:engine/view/renderer~ViewRenderer renderer} and are not present in the
8393
- * view.
8394
- *
8395
- * @module engine/view/filler
8396
- */ /**
8397
8382
  * Non-breaking space filler creator. This function creates the `&nbsp;` text node.
8398
8383
  * It defines how the filler is created.
8399
8384
  *
@@ -16699,6 +16684,78 @@ ModelRange.prototype.is = function(type) {
16699
16684
  return parts.length > 1 ? parts[0] + ':' + parts[1] : parts[0];
16700
16685
  }
16701
16686
 
16687
+ /**
16688
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
16689
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options
16690
+ */ /**
16691
+ * @module engine/conversion/comparemarkers
16692
+ */ /**
16693
+ * Sorts markers so the downcast result is deterministic regardless of the order
16694
+ * markers were added to the marker collection.
16695
+ *
16696
+ * The sort key is the marker's range, ordered "right-to-left" through the document so that
16697
+ * a marker's opening boundary is processed *after* any markers nested inside it. This way
16698
+ * the outer marker wraps the inner ones at conversion time.
16699
+ *
16700
+ * Cases (positions shown as `0123456789`, sort result top-to-bottom):
16701
+ *
16702
+ * 1. Non-overlapping ranges — sorted by position, last range first:
16703
+ *
16704
+ * a: [--] → c, b, a
16705
+ * b: [--]
16706
+ * c: [--]
16707
+ *
16708
+ * 2. Adjacent ranges (end === start) — treated as non-overlapping:
16709
+ *
16710
+ * first: [---] → third, second, first
16711
+ * second: [---]
16712
+ * third: [---]
16713
+ *
16714
+ * 3. Nested ranges (same start, different ends) — inner first, outer last:
16715
+ *
16716
+ * shorter: [-] → shorter, longer
16717
+ * longer: [---]
16718
+ *
16719
+ * 4. Partially overlapping ranges — sorted by start position:
16720
+ *
16721
+ * earlier: [---] → later, earlier
16722
+ * later: [---]
16723
+ *
16724
+ * 5. Identical ranges — fall back to reverse name comparison:
16725
+ *
16726
+ * alpha: [---] → charlie, bravo, alpha
16727
+ * bravo: [---]
16728
+ * charlie: [---]
16729
+ *
16730
+ * @internal
16731
+ */ function compareMarkersForDowncast([name1, range1], [name2, range2]) {
16732
+ if (range1.end.compareWith(range2.start) !== 'after') {
16733
+ // m1.end <= m2.start -- m1 is entirely <= m2.
16734
+ return 1;
16735
+ } else if (range1.start.compareWith(range2.end) !== 'before') {
16736
+ // m1.start >= m2.end -- m1 is entirely >= m2.
16737
+ return -1;
16738
+ } else {
16739
+ // They overlap, so use their start positions as the primary sort key and
16740
+ // end positions as the secondary sort key.
16741
+ switch(range1.start.compareWith(range2.start)){
16742
+ case 'before':
16743
+ return 1;
16744
+ case 'after':
16745
+ return -1;
16746
+ default:
16747
+ switch(range1.end.compareWith(range2.end)){
16748
+ case 'before':
16749
+ return -1;
16750
+ case 'after':
16751
+ return 1;
16752
+ default:
16753
+ return name2.localeCompare(name1);
16754
+ }
16755
+ }
16756
+ }
16757
+ }
16758
+
16702
16759
  /**
16703
16760
  * The downcast dispatcher is a central point of downcasting (conversion from the model to the view), which is a process of reacting
16704
16761
  * to changes in the model and firing a set of events. The callbacks listening to these events are called converters. The
@@ -16856,8 +16913,28 @@ ModelRange.prototype.is = function(type) {
16856
16913
  this._convertMarkerRemove(markerName, markerRange, conversionApi);
16857
16914
  this._convertMarkerAdd(markerName, markerRange, conversionApi);
16858
16915
  }
16916
+ // Sort markers so the downcast result is deterministic regardless of the order
16917
+ // markers were added to the collection.
16918
+ //
16919
+ // "Reverse DOM order" = markers ending later in the document come first, so each
16920
+ // marker's opening boundary is processed after any markers nested inside it.
16921
+ // For overlapping ranges this is best-effort (start position wins, then end position).
16922
+ //
16923
+ // Example: replacing "old" with "new" creates two adjacent markers (delete + insert).
16924
+ // With `markerToElement`, each boundary is a self-closing tag, so the processing
16925
+ // order directly controls where they land at the shared boundary point:
16926
+ //
16927
+ // Sorted (reverse DOM order): <DEL-START/>old<DEL-END/><INS-START/>new<INS-END/>
16928
+ // Insertion order (legacy): <DEL-START/>old<INS-START/><DEL-END/>new<INS-END/>
16929
+ const markersToAdd = differ.getMarkersToAdd().sort((a, b)=>compareMarkersForDowncast([
16930
+ a.name,
16931
+ a.range
16932
+ ], [
16933
+ b.name,
16934
+ b.range
16935
+ ]));
16859
16936
  // After the view is updated, convert markers which have changed.
16860
- for (const change of differ.getMarkersToAdd()){
16937
+ for (const change of markersToAdd){
16861
16938
  this._convertMarkerAdd(change.name, change.range, conversionApi);
16862
16939
  }
16863
16940
  // Verify if all insert consumables were consumed.
@@ -16876,7 +16953,9 @@ ModelRange.prototype.is = function(type) {
16876
16953
  */ convert(range, markers, writer, options = {}) {
16877
16954
  const conversionApi = this._createConversionApi(writer, undefined, options);
16878
16955
  this._convertInsert(range, conversionApi);
16879
- for (const [name, range] of markers){
16956
+ // Sort markers in reverse DOM order for deterministic downcast output.
16957
+ // See the analogous sort in `convertChanges()` for a detailed rationale and examples.
16958
+ for (const [name, range] of Array.from(markers).sort(compareMarkersForDowncast)){
16880
16959
  this._convertMarkerAdd(name, range, conversionApi);
16881
16960
  }
16882
16961
  // Verify if all insert consumables were consumed.
@@ -19444,19 +19523,11 @@ ModelDocumentSelection.prototype.is = function(type) {
19444
19523
  // 4. If not, try to find the first character on the left, that is in the same node.
19445
19524
  // When gravity is overridden then don't take node before into consideration.
19446
19525
  if (!this.isGravityOverridden && !attrs) {
19447
- let node = nodeBefore;
19448
- while(node && !attrs){
19449
- node = node.previousSibling;
19450
- attrs = getTextAttributes(node, schema);
19451
- }
19526
+ attrs = getTextAttributes(nodeBefore, schema, 'backward');
19452
19527
  }
19453
19528
  // 5. If not found, try to find the first character on the right, that is in the same node.
19454
19529
  if (!attrs) {
19455
- let node = nodeAfter;
19456
- while(node && !attrs){
19457
- node = node.nextSibling;
19458
- attrs = getTextAttributes(node, schema);
19459
- }
19530
+ attrs = getTextAttributes(nodeAfter, schema, 'forward');
19460
19531
  }
19461
19532
  // 6. If not found, selection should retrieve attributes from parent.
19462
19533
  if (!attrs) {
@@ -19482,33 +19553,50 @@ ModelDocumentSelection.prototype.is = function(type) {
19482
19553
  /**
19483
19554
  * Helper function for {@link module:engine/model/liveselection~LiveSelection#_updateAttributes}.
19484
19555
  *
19485
- * It checks if the passed model item is a text node (or text proxy) and, if so, returns it's attributes.
19486
- * If not, it checks if item is an inline object and does the same. Otherwise it returns `null`.
19487
- */ function getTextAttributes(node, schema) {
19488
- if (!node) {
19489
- return null;
19490
- }
19491
- if (node instanceof ModelTextProxy || node instanceof ModelText) {
19492
- return node.getAttributes();
19493
- }
19494
- if (!schema.isInline(node)) {
19556
+ * It checks if the passed model node is a text node and, if so, returns its attributes.
19557
+ * If not, it checks if item is an inline element and does the same. Otherwise, it returns `null`.
19558
+ */ function getTextAttributes(startNode, schema, scan = 'self') {
19559
+ if (!startNode) {
19495
19560
  return null;
19496
19561
  }
19497
- // Stop on inline elements (such as `<softBreak>`) that are not objects (such as `<imageInline>` or `<mathml>`).
19498
- if (!schema.isObject(node)) {
19499
- return [];
19562
+ for (const node of siblingNodes(startNode, scan)){
19563
+ if (!node) {
19564
+ return null;
19565
+ }
19566
+ if (node instanceof ModelText) {
19567
+ return node.getAttributes();
19568
+ }
19569
+ if (!schema.isInline(node)) {
19570
+ continue;
19571
+ }
19572
+ const isObject = schema.isObject(node);
19573
+ const attributes = [];
19574
+ // Collect all attributes that can be applied to the text node.
19575
+ for (const [key, value] of node.getAttributes()){
19576
+ if (schema.checkAttribute('$text', key) && (!isObject || schema.getAttributeProperties(key).copyFromObject !== false)) {
19577
+ attributes.push([
19578
+ key,
19579
+ value
19580
+ ]);
19581
+ }
19582
+ }
19583
+ return attributes;
19500
19584
  }
19501
- const attributes = [];
19502
- // Collect all attributes that can be applied to the text node.
19503
- for (const [key, value] of node.getAttributes()){
19504
- if (schema.checkAttribute('$text', key) && schema.getAttributeProperties(key).copyFromObject !== false) {
19505
- attributes.push([
19506
- key,
19507
- value
19508
- ]);
19585
+ return null;
19586
+ }
19587
+ /**
19588
+ * Returns sibling nodes from the given start node.
19589
+ */ function* siblingNodes(startNode, scan) {
19590
+ if (scan == 'self') {
19591
+ yield startNode;
19592
+ } else {
19593
+ let node = startNode;
19594
+ while(node){
19595
+ node = scan == 'backward' ? node.previousSibling : node.nextSibling;
19596
+ yield node;
19509
19597
  }
19510
19598
  }
19511
- return attributes;
19599
+ return null;
19512
19600
  }
19513
19601
  /**
19514
19602
  * Removes selection attributes from element which is not empty anymore.
@@ -21146,14 +21234,18 @@ function cloneNodes(nodes) {
21146
21234
  }
21147
21235
  const mapper = conversionApi.mapper;
21148
21236
  const viewWriter = conversionApi.writer;
21149
- // Add "opening" element.
21150
- viewWriter.insert(mapper.toViewPosition(markerRange.start), viewStartElement);
21151
- conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
21152
- // Add "closing" element only if range is not collapsed.
21237
+ viewWriter.setCustomProperty('markerBoundaryType', 'start', viewStartElement);
21238
+ viewWriter.setCustomProperty('markerBoundaryType', 'end', viewEndElement);
21239
+ // Add "end" element only if range is not collapsed.
21153
21240
  if (!markerRange.isCollapsed) {
21154
21241
  viewWriter.insert(mapper.toViewPosition(markerRange.end), viewEndElement);
21155
21242
  conversionApi.mapper.bindElementToMarker(viewEndElement, data.markerName);
21156
21243
  }
21244
+ // Jump over end UI elements to find a proper position for "start" element.
21245
+ // It should be after all marker "end" UI elements as markers conversion should be triggered in reverse DOM order.
21246
+ const startViewPosition = mapper.toViewPosition(markerRange.start).getLastMatchingPosition(({ item })=>item.is('uiElement') && item.getCustomProperty('markerBoundaryType') === 'end');
21247
+ viewWriter.insert(startViewPosition, viewStartElement);
21248
+ conversionApi.mapper.bindElementToMarker(viewStartElement, data.markerName);
21157
21249
  evt.stop();
21158
21250
  };
21159
21251
  }
@@ -22246,11 +22338,6 @@ function getFromAttributeCreator(config) {
22246
22338
  }
22247
22339
 
22248
22340
  /**
22249
- * Contains the {@link module:engine/view/view view} to {@link module:engine/model/model model} converters for
22250
- * {@link module:engine/conversion/upcastdispatcher~UpcastDispatcher}.
22251
- *
22252
- * @module engine/conversion/upcasthelpers
22253
- */ /**
22254
22341
  * Upcast conversion helper functions.
22255
22342
  *
22256
22343
  * Learn more about {@glink framework/deep-dive/conversion/upcast upcast helpers}.
@@ -25876,45 +25963,6 @@ function removeDisallowedAttributeFromNode(schema, node, writer) {
25876
25963
  }
25877
25964
  }
25878
25965
  }
25879
- // Sort the markers in a stable fashion to ensure that the order in which they are
25880
- // added to the model's marker collection does not affect how they are
25881
- // downcast. One particular use case that we are targeting here, is one where
25882
- // two markers are adjacent but not overlapping, such as an insertion/deletion
25883
- // suggestion pair representing the replacement of a range of text. In this
25884
- // case, putting the markers in DOM order causes the first marker's end to be
25885
- // serialized right after the second marker's start, while putting the markers
25886
- // in reverse DOM order causes it to be right before the second marker's
25887
- // start. So, we sort these in a way that ensures non-intersecting ranges are in
25888
- // reverse DOM order, and intersecting ranges are in something approximating
25889
- // reverse DOM order (since reverse DOM order doesn't have a precise meaning
25890
- // when working with intersecting ranges).
25891
- result.sort(([n1, r1], [n2, r2])=>{
25892
- if (r1.end.compareWith(r2.start) !== 'after') {
25893
- // m1.end <= m2.start -- m1 is entirely <= m2
25894
- return 1;
25895
- } else if (r1.start.compareWith(r2.end) !== 'before') {
25896
- // m1.start >= m2.end -- m1 is entirely >= m2
25897
- return -1;
25898
- } else {
25899
- // they overlap, so use their start positions as the primary sort key and
25900
- // end positions as the secondary sort key
25901
- switch(r1.start.compareWith(r2.start)){
25902
- case 'before':
25903
- return 1;
25904
- case 'after':
25905
- return -1;
25906
- default:
25907
- switch(r1.end.compareWith(r2.end)){
25908
- case 'before':
25909
- return 1;
25910
- case 'after':
25911
- return -1;
25912
- default:
25913
- return n2.localeCompare(n1);
25914
- }
25915
- }
25916
- }
25917
- });
25918
25966
  return new Map(result);
25919
25967
  }
25920
25968
 
@@ -31834,8 +31882,6 @@ ModelLivePosition.prototype.is = function(type) {
31834
31882
  }
31835
31883
 
31836
31884
  /**
31837
- * @module engine/model/history
31838
- */ /**
31839
31885
  * `History` keeps the track of all the operations applied to the {@link module:engine/model/document~ModelDocument document}.
31840
31886
  */ class History {
31841
31887
  /**
@@ -34733,6 +34779,10 @@ ModelDocumentFragment.prototype.is = function(type) {
34733
34779
  return;
34734
34780
  }
34735
34781
  const schema = model.schema;
34782
+ const documentSelection = model.document.selection;
34783
+ const selectionIsDocumentSelection = isRelatedToDocumentSelection(selection, documentSelection, selRange);
34784
+ const selectionAttributes = Array.from(documentSelection.getAttributes());
34785
+ const selectionParentWasEmpty = !!documentSelection.getFirstRange()?.start.parent.isEmpty;
34736
34786
  model.change((writer)=>{
34737
34787
  // 1. Replace the entire content with paragraph.
34738
34788
  // See: https://github.com/ckeditor/ckeditor5-engine/issues/1012#issuecomment-315017594.
@@ -34785,6 +34835,9 @@ ModelDocumentFragment.prototype.is = function(type) {
34785
34835
  if (!options.doNotAutoparagraph && shouldAutoparagraph(schema, startPosition)) {
34786
34836
  insertParagraph(writer, startPosition, selection, attributesForAutoparagraph);
34787
34837
  }
34838
+ if (selectionIsDocumentSelection) {
34839
+ restoreSelectionAttributesOnEmptyParent(writer, selectionAttributes, selectionParentWasEmpty);
34840
+ }
34788
34841
  startPosition.detach();
34789
34842
  endPosition.detach();
34790
34843
  });
@@ -35167,6 +35220,56 @@ function replaceEntireContentWithParagraph(writer, selection) {
35167
35220
  selection.setTo(positionOrRange);
35168
35221
  }
35169
35222
  }
35223
+ /**
35224
+ * Restores the document selection attributes after a deletion that leaves the selection in an empty parent block.
35225
+ * This preserves the pre-delete formatting (e.g. bold, italic) so that subsequent typing continues in the same style.
35226
+ *
35227
+ * Attributes are only restored when:
35228
+ * - There were attributes on the selection before the deletion.
35229
+ * - The deletion left the document selection's parent block empty.
35230
+ * - The parent block was **not** already empty before the deletion — this ensures that attributes are not
35231
+ * re-applied when `deleteContent()` was called on a completely unrelated block.
35232
+ */ function restoreSelectionAttributesOnEmptyParent(writer, selectionAttributes, selectionParentWasEmpty) {
35233
+ if (!selectionAttributes.length) {
35234
+ return;
35235
+ }
35236
+ const documentSelection = writer.model.document.selection;
35237
+ const selectionParent = documentSelection.anchor.parent;
35238
+ if (!selectionParent.isEmpty) {
35239
+ return;
35240
+ }
35241
+ // Preserve attributes only when the delete operation leaves the live selection in an empty parent
35242
+ // that was not empty before the change. This avoids reasserting attributes on unrelated empty blocks
35243
+ // when deleteContent() operates on a synthetic selection somewhere else in the document.
35244
+ if (selectionParentWasEmpty) {
35245
+ return;
35246
+ }
35247
+ // Setting document selection attributes here also persists them as `selection:*`
35248
+ // on the empty parent, so future typing keeps the pre-delete formatting.
35249
+ for (const [key, value] of selectionAttributes){
35250
+ if (writer.model.schema.getAttributeProperties(key).isFormatting && writer.model.schema.checkAttributeInSelection(documentSelection, key)) {
35251
+ writer.setSelectionAttribute(key, value);
35252
+ }
35253
+ }
35254
+ }
35255
+ /**
35256
+ * Checks whether the provided selection is related to the document selection.
35257
+ *
35258
+ * Returns `true` when:
35259
+ * - the selection is a `DocumentSelection` instance, or
35260
+ * - the document selection is collapsed and sits at the start or end of the given range, or
35261
+ * - the document selection is not collapsed and its range intersects the given range.
35262
+ */ function isRelatedToDocumentSelection(selection, documentSelection, selRange) {
35263
+ if (selection instanceof ModelDocumentSelection) {
35264
+ return true;
35265
+ }
35266
+ if (documentSelection.isCollapsed) {
35267
+ const position = documentSelection.getFirstPosition();
35268
+ return position.isEqual(selRange.start) || position.isEqual(selRange.end);
35269
+ }
35270
+ const docSelRange = documentSelection.getFirstRange();
35271
+ return !!docSelRange?.isIntersecting(selRange);
35272
+ }
35170
35273
 
35171
35274
  /**
35172
35275
  * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
@@ -36337,6 +36440,11 @@ function getSearchRange(start, isForward) {
36337
36440
  this.schema.register('$root', {
36338
36441
  isLimit: true
36339
36442
  });
36443
+ this.schema.register('$inlineRoot', {
36444
+ allowContentOf: '$block',
36445
+ allowAttributesOf: '$root',
36446
+ isLimit: true
36447
+ });
36340
36448
  this.schema.register('$container', {
36341
36449
  allowIn: [
36342
36450
  '$root',
@@ -36367,12 +36475,18 @@ function getSearchRange(start, isForward) {
36367
36475
  isContent: true
36368
36476
  });
36369
36477
  this.schema.register('$clipboardHolder', {
36370
- allowContentOf: '$root',
36478
+ allowContentOf: [
36479
+ '$root',
36480
+ '$inlineRoot'
36481
+ ],
36371
36482
  allowChildren: '$text',
36372
36483
  isLimit: true
36373
36484
  });
36374
36485
  this.schema.register('$documentFragment', {
36375
- allowContentOf: '$root',
36486
+ allowContentOf: [
36487
+ '$root',
36488
+ '$inlineRoot'
36489
+ ],
36376
36490
  allowChildren: '$text',
36377
36491
  isLimit: true
36378
36492
  });
@@ -40477,5 +40591,5 @@ const maxTreeDumpLength = 20;
40477
40591
  }
40478
40592
  }
40479
40593
 
40480
- export { AttributeOperation, Batch, BubblingEmitterMixin, BubblingEventInfo, ClickObserver, CompositionObserver, Conversion, ConversionHelpers, DataController, Differ, DomEventObserver, DowncastHelpers, EditingController, EditingView, FakeSelectionObserver, FocusObserver, History, HtmlDataProcessor, InputObserver, InsertOperation, KeyObserver, Mapper, MarkerCollection, MarkerOperation, Matcher, MergeOperation, Model, ModelDocument, ModelDocumentFragment, ModelDocumentSelection, ModelElement, ModelLivePosition, ModelLiveRange, ModelNode, ModelNodeList, ModelPosition, ModelRange, ModelRootElement, ModelSchema, ModelSchemaContext, ModelSelection, ModelText, ModelTextProxy, ModelTreeWalker, ModelTypeCheckable, ModelWriter, MouseObserver, MoveOperation, MutationObserver, NoOperation, Observer, OperationFactory, PointerObserver, RenameOperation, RootAttributeOperation, RootOperation, SelectionObserver, SplitOperation, StylesMap, StylesProcessor, TabObserver, TouchObserver, UpcastHelpers, ViewUpcastWriter as UpcastWriter, ViewAttributeElement, ViewContainerElement, ViewDataTransfer, ViewDocument, ViewDocumentDomEventData, ViewDocumentFragment, ViewDocumentSelection, ViewDomConverter, ViewDowncastWriter, ViewEditableElement, ViewElement, ViewEmptyElement, ViewNode, ViewPosition, ViewRange, ViewRawElement, ViewRenderer, ViewRootEditableElement, ViewSelection, ViewText, ViewTextProxy, ViewTokenList, ViewTreeWalker, ViewTypeCheckable, ViewUIElement, ViewUpcastWriter, XmlDataProcessor, BasicHtmlWriter as _DataProcessorBasicHtmlWriter, DetachOperation as _DetachOperation, MapperCache as _MapperCache, OperationReplayer as _OperationReplayer, BR_FILLER as _VIEW_BR_FILLER, INLINE_FILLER as _VIEW_INLINE_FILLER, INLINE_FILLER_LENGTH as _VIEW_INLINE_FILLER_LENGTH, MARKED_NBSP_FILLER as _VIEW_MARKED_NBSP_FILLER, NBSP_FILLER as _VIEW_NBSP_FILLER, ViewElementConsumables as _ViewElementConversionConsumables, autoParagraphEmptyRoots as _autoParagraphEmptyModelRoots, convertMapToStringifiedObject as _convertMapToStringifiedObject, convertMapToTags as _convertMapToTags, deleteContent as _deleteModelContent, cleanSelection as _downcastCleanSelection, convertCollapsedSelection as _downcastConvertCollapsedSelection, convertRangeSelection as _downcastConvertRangeSelection, createViewElementFromDowncastHighlightDescriptor as _downcastCreateViewElementFromDowncastHighlightDescriptor, insertAttributesAndChildren as _downcastInsertAttributesAndChildren, insertElement as _downcastInsertElement, insertStructure as _downcastInsertStructure, insertText as _downcastInsertText, insertUIElement as _downcastInsertUIElement, remove as _downcastRemove, wrap as _downcastWrap, dumpTrees as _dumpTrees, getDataWithoutFiller as _getDataWithoutViewFiller, _getModelData, getNodeAfterPosition as _getModelNodeAfterPosition, getNodeBeforePosition as _getModelNodeBeforePosition, getTextNodeAtPosition as _getModelTextNodeAtPosition, getSelectedContent as _getSelectedModelContent, _getViewData, initDocumentDumping as _initDocumentDumping, injectSelectionPostFixer as _injectModelSelectionPostFixer, injectQuirksHandling as _injectViewQuirksHandling, injectUiElementHandling as _injectViewUIElementHandling, _insert as _insertIntoModelNodeList, insertContent as _insertModelContent, insertObject as _insertModelObject, isInlineFiller as _isInlineViewFiller, isParagraphable as _isParagraphableModelNode, isPatternMatched as _isViewPatternMatched, logDocument as _logDocument, mergeIntersectingRanges as _mergeIntersectingModelRanges, modifySelection as _modifyModelSelection, _move as _moveInModelNodeList, normalizeConsumables as _normalizeConversionConsumables, _normalizeNodes as _normalizeInModelNodeList, transform$1 as _operationTransform, _parseModel, _parseView, _remove as _removeFromModelNodeList, _setAttribute as _setAttributeInModelNodeList, _setModelData, _setViewData, startsWithFiller as _startsWithViewFiller, _stringifyModel, _stringifyView, tryFixingRange as _tryFixingModelRange, convertSelectionChange as _upcastConvertSelectionChange, convertText as _upcastConvertText, convertToModelFragment$1 as _upcastConvertToModelFragment, wrapInParagraph as _wrapInModelParagraph, addBackgroundStylesRules, addBorderStylesRules, addMarginStylesRules, addPaddingStylesRules, disableViewPlaceholder, enableViewPlaceholder, getBoxSidesStyleShorthandValue, getBoxSidesStyleValueReducer, getBoxSidesStyleValues, getPositionStyleShorthandNormalizer, getShorthandStylesValues, getViewFillerOffset, hideViewPlaceholder, isAttachmentStyleValue, isColorStyleValue, isLengthStyleValue, isLineStyleValue, isPercentageStyleValue, isPositionStyleValue, isRepeatStyleValue, isURLStyleValue, needsViewPlaceholder, showViewPlaceholder, transformOperationSets };
40594
+ export { AttributeOperation, Batch, BubblingEmitterMixin, BubblingEventInfo, ClickObserver, CompositionObserver, Conversion, ConversionHelpers, DataController, Differ, DomEventObserver, DowncastDispatcher, DowncastHelpers, EditingController, EditingView, FakeSelectionObserver, FocusObserver, History, HtmlDataProcessor, InputObserver, InsertOperation, KeyObserver, Mapper, Marker, MarkerCollection, MarkerOperation, Matcher, MergeOperation, Model, ModelConsumable, ModelDocument, ModelDocumentFragment, ModelDocumentSelection, ModelElement, ModelLivePosition, ModelLiveRange, ModelNode, ModelNodeList, ModelPosition, ModelRange, ModelRootElement, ModelSchema, ModelSchemaContext, ModelSelection, ModelText, ModelTextProxy, ModelTreeWalker, ModelTypeCheckable, ModelWriter, MouseObserver, MoveOperation, MutationObserver, NoOperation, Observer, Operation, OperationFactory, PointerObserver, RenameOperation, RootAttributeOperation, RootOperation, SelectionObserver, SplitOperation, StylesMap, StylesProcessor, TabObserver, TouchObserver, UpcastDispatcher, UpcastHelpers, ViewUpcastWriter as UpcastWriter, ViewAttributeElement, ViewConsumable, ViewContainerElement, ViewDataTransfer, ViewDocument, ViewDocumentDomEventData, ViewDocumentFragment, ViewDocumentSelection, ViewDomConverter, ViewDowncastWriter, ViewEditableElement, ViewElement, ViewEmptyElement, ViewNode, ViewPosition, ViewRange, ViewRawElement, ViewRenderer, ViewRootEditableElement, ViewSelection, ViewText, ViewTextProxy, ViewTokenList, ViewTreeWalker, ViewTypeCheckable, ViewUIElement, ViewUpcastWriter, XmlDataProcessor, BasicHtmlWriter as _DataProcessorBasicHtmlWriter, DetachOperation as _DetachOperation, MapperCache as _MapperCache, OperationReplayer as _OperationReplayer, BR_FILLER as _VIEW_BR_FILLER, INLINE_FILLER as _VIEW_INLINE_FILLER, INLINE_FILLER_LENGTH as _VIEW_INLINE_FILLER_LENGTH, MARKED_NBSP_FILLER as _VIEW_MARKED_NBSP_FILLER, NBSP_FILLER as _VIEW_NBSP_FILLER, ViewElementConsumables as _ViewElementConversionConsumables, autoParagraphEmptyRoots as _autoParagraphEmptyModelRoots, convertMapToStringifiedObject as _convertMapToStringifiedObject, convertMapToTags as _convertMapToTags, deleteContent as _deleteModelContent, cleanSelection as _downcastCleanSelection, convertCollapsedSelection as _downcastConvertCollapsedSelection, convertRangeSelection as _downcastConvertRangeSelection, createViewElementFromDowncastHighlightDescriptor as _downcastCreateViewElementFromDowncastHighlightDescriptor, insertAttributesAndChildren as _downcastInsertAttributesAndChildren, insertElement as _downcastInsertElement, insertStructure as _downcastInsertStructure, insertText as _downcastInsertText, insertUIElement as _downcastInsertUIElement, remove as _downcastRemove, wrap as _downcastWrap, dumpTrees as _dumpTrees, getDataWithoutFiller as _getDataWithoutViewFiller, _getModelData, getNodeAfterPosition as _getModelNodeAfterPosition, getNodeBeforePosition as _getModelNodeBeforePosition, getTextNodeAtPosition as _getModelTextNodeAtPosition, getSelectedContent as _getSelectedModelContent, _getViewData, initDocumentDumping as _initDocumentDumping, injectSelectionPostFixer as _injectModelSelectionPostFixer, injectQuirksHandling as _injectViewQuirksHandling, injectUiElementHandling as _injectViewUIElementHandling, _insert as _insertIntoModelNodeList, insertContent as _insertModelContent, insertObject as _insertModelObject, isInlineFiller as _isInlineViewFiller, isParagraphable as _isParagraphableModelNode, isPatternMatched as _isViewPatternMatched, logDocument as _logDocument, mergeIntersectingRanges as _mergeIntersectingModelRanges, modifySelection as _modifyModelSelection, _move as _moveInModelNodeList, normalizeConsumables as _normalizeConversionConsumables, _normalizeNodes as _normalizeInModelNodeList, transform$1 as _operationTransform, _parseModel, _parseView, _remove as _removeFromModelNodeList, _setAttribute as _setAttributeInModelNodeList, _setModelData, _setViewData, startsWithFiller as _startsWithViewFiller, _stringifyModel, _stringifyView, tryFixingRange as _tryFixingModelRange, convertSelectionChange as _upcastConvertSelectionChange, convertText as _upcastConvertText, convertToModelFragment$1 as _upcastConvertToModelFragment, wrapInParagraph as _wrapInModelParagraph, addBackgroundStylesRules, addBorderStylesRules, addMarginStylesRules, addPaddingStylesRules, disableViewPlaceholder, enableViewPlaceholder, getBoxSidesStyleShorthandValue, getBoxSidesStyleValueReducer, getBoxSidesStyleValues, getPositionStyleShorthandNormalizer, getShorthandStylesValues, getViewFillerOffset, hideViewPlaceholder, isAttachmentStyleValue, isColorStyleValue, isLengthStyleValue, isLineStyleValue, isPercentageStyleValue, isPositionStyleValue, isRepeatStyleValue, isURLStyleValue, needsViewPlaceholder, showViewPlaceholder, transformOperationSets };
40481
40595
  //# sourceMappingURL=index.js.map