From 103685c884b77e3206d7321ff4c53e3c58b6e3a0 Mon Sep 17 00:00:00 2001 From: Kamil Piechaczek Date: Wed, 15 Apr 2026 11:27:40 +0200 Subject: [PATCH] Revert "Fix inconsistent marker boundary order" --- ...23163157_ck_19975_marker_boundary_order.md | 11 - .../src/controller/datacontroller.ts | 40 ++ .../src/conversion/comparemarkers.ts | 49 --- .../src/conversion/downcastdispatcher.ts | 22 +- .../src/conversion/downcasthelpers.ts | 16 +- .../tests/conversion/comparemarkers.js | 159 ------- .../tests/conversion/downcasthelpers.js | 388 +----------------- .../legacytodolist/legacytodolistediting.js | 8 +- 8 files changed, 59 insertions(+), 634 deletions(-) delete mode 100644 .changelog/20260323163157_ck_19975_marker_boundary_order.md delete mode 100644 packages/ckeditor5-engine/src/conversion/comparemarkers.ts delete mode 100644 packages/ckeditor5-engine/tests/conversion/comparemarkers.js diff --git a/.changelog/20260323163157_ck_19975_marker_boundary_order.md b/.changelog/20260323163157_ck_19975_marker_boundary_order.md deleted file mode 100644 index 2cd2c7d389c..00000000000 --- a/.changelog/20260323163157_ck_19975_marker_boundary_order.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -type: Fix -scope: - - ckeditor5-engine -closes: - - 19975 ---- - -Fixed the editing downcast order of adjacent marker UI boundaries so marker ends and starts are rendered consistently with the model and data output. - -The editing pipeline now uses stable marker ordering and preserves the expected boundary order when adjacent markers are added together or when the second adjacent marker is added later. diff --git a/packages/ckeditor5-engine/src/controller/datacontroller.ts b/packages/ckeditor5-engine/src/controller/datacontroller.ts index d98efce848c..26e6a0565a7 100644 --- a/packages/ckeditor5-engine/src/controller/datacontroller.ts +++ b/packages/ckeditor5-engine/src/controller/datacontroller.ts @@ -669,5 +669,45 @@ function _getMarkersRelativeToElement( element: ModelElement ): Map { + if ( r1.end.compareWith( r2.start ) !== 'after' ) { + // m1.end <= m2.start -- m1 is entirely <= m2 + return 1; + } else if ( r1.start.compareWith( r2.end ) !== 'before' ) { + // m1.start >= m2.end -- m1 is entirely >= m2 + return -1; + } else { + // they overlap, so use their start positions as the primary sort key and + // end positions as the secondary sort key + switch ( r1.start.compareWith( r2.start ) ) { + case 'before': + return 1; + case 'after': + return -1; + default: + switch ( r1.end.compareWith( r2.end ) ) { + case 'before': + return 1; + case 'after': + return -1; + default: + return n2.localeCompare( n1 ); + } + } + } + } ); + return new Map( result ); } diff --git a/packages/ckeditor5-engine/src/conversion/comparemarkers.ts b/packages/ckeditor5-engine/src/conversion/comparemarkers.ts deleted file mode 100644 index ab01904fb52..00000000000 --- a/packages/ckeditor5-engine/src/conversion/comparemarkers.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options - */ - -/** - * @module engine/conversion/comparemarkers - */ - -import type { ModelRange } from '../model/range.js'; - -/** - * Sorts markers in a stable fashion so their addition order does not affect downcast output. - * - * Markers are ordered in reverse DOM order for non-intersecting ranges. For intersecting ranges, - * the start position is the primary sort key and the end position is the secondary sort key. - * - * @internal - */ -export function compareMarkersForDowncast( - [ name1, range1 ]: readonly [ string, ModelRange ], - [ name2, range2 ]: readonly [ string, ModelRange ] -): number { - if ( range1.end.compareWith( range2.start ) !== 'after' ) { - // m1.end <= m2.start -- m1 is entirely <= m2. - return 1; - } else if ( range1.start.compareWith( range2.end ) !== 'before' ) { - // m1.start >= m2.end -- m1 is entirely >= m2. - return -1; - } else { - // They overlap, so use their start positions as the primary sort key and - // end positions as the secondary sort key. - switch ( range1.start.compareWith( range2.start ) ) { - case 'before': - return 1; - case 'after': - return -1; - default: - switch ( range1.end.compareWith( range2.end ) ) { - case 'before': - return 1; - case 'after': - return -1; - default: - return name2.localeCompare( name1 ); - } - } - } -} diff --git a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.ts b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.ts index 74d0ed22ae1..db36dfe6c75 100644 --- a/packages/ckeditor5-engine/src/conversion/downcastdispatcher.ts +++ b/packages/ckeditor5-engine/src/conversion/downcastdispatcher.ts @@ -8,7 +8,6 @@ */ import { ModelConsumable } from './modelconsumable.js'; -import { compareMarkersForDowncast } from './comparemarkers.js'; import { ModelRange } from '../model/range.js'; import { EmitterMixin } from '@ckeditor/ckeditor5-utils'; @@ -201,23 +200,8 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() { this._convertMarkerAdd( markerName, markerRange, conversionApi ); } - // Sort markers in reverse DOM order so that the downcast result is deterministic - // regardless of the order markers were added to the collection. - // - // Example: replacing "old" with "new" creates two adjacent markers (delete + insert). - // With `markerToElement`, each boundary is a self-closing tag, so the processing - // order directly controls where they land at the shared boundary point: - // - // Stable (reverse DOM order): oldnew - // Unstable (insertion order): oldnew - // - // Non-intersecting ranges → strict reverse DOM order. - // Intersecting ranges → best-effort reverse DOM order (ambiguous by nature). - const markersToAdd = differ.getMarkersToAdd() - .sort( ( a, b ) => compareMarkersForDowncast( [ a.name, a.range ], [ b.name, b.range ] ) ); - // After the view is updated, convert markers which have changed. - for ( const change of markersToAdd ) { + for ( const change of differ.getMarkersToAdd() ) { this._convertMarkerAdd( change.name, change.range, conversionApi ); } @@ -246,9 +230,7 @@ export class DowncastDispatcher extends /* #__PURE__ */ EmitterMixin() { this._convertInsert( range, conversionApi ); - // Sort markers in reverse DOM order for deterministic downcast output. - // See the analogous sort in `convertChanges()` for a detailed rationale and examples. - for ( const [ name, range ] of Array.from( markers ).sort( compareMarkersForDowncast ) ) { + for ( const [ name, range ] of markers ) { this._convertMarkerAdd( name, range, conversionApi ); } diff --git a/packages/ckeditor5-engine/src/conversion/downcasthelpers.ts b/packages/ckeditor5-engine/src/conversion/downcasthelpers.ts index f1df5b1fc8f..1249a9af667 100644 --- a/packages/ckeditor5-engine/src/conversion/downcasthelpers.ts +++ b/packages/ckeditor5-engine/src/conversion/downcasthelpers.ts @@ -1335,24 +1335,16 @@ export function insertUIElement( elementCreator: DowncastMarkerElementCreatorFun const mapper = conversionApi.mapper; const viewWriter = conversionApi.writer; - viewWriter.setCustomProperty( 'markerBoundaryType', 'start', viewStartElement ); - viewWriter.setCustomProperty( 'markerBoundaryType', 'end', viewEndElement ); + // Add "opening" element. + viewWriter.insert( mapper.toViewPosition( markerRange.start ), viewStartElement ); + conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName ); - // Add "end" element only if range is not collapsed. + // Add "closing" element only if range is not collapsed. if ( !markerRange.isCollapsed ) { viewWriter.insert( mapper.toViewPosition( markerRange.end ), viewEndElement ); conversionApi.mapper.bindElementToMarker( viewEndElement, data.markerName ); } - // Jump over end UI elements to find a proper position for "start" element. - // It should be after all marker "end" UI elements as markers conversion should be triggered in reverse DOM order. - const startViewPosition = mapper.toViewPosition( markerRange.start ).getLastMatchingPosition( ( { item } ) => - item.is( 'uiElement' ) && item.getCustomProperty( 'markerBoundaryType' ) === 'end' - ); - - viewWriter.insert( startViewPosition, viewStartElement ); - conversionApi.mapper.bindElementToMarker( viewStartElement, data.markerName ); - evt.stop(); }; } diff --git a/packages/ckeditor5-engine/tests/conversion/comparemarkers.js b/packages/ckeditor5-engine/tests/conversion/comparemarkers.js deleted file mode 100644 index 20c3ceab4f6..00000000000 --- a/packages/ckeditor5-engine/tests/conversion/comparemarkers.js +++ /dev/null @@ -1,159 +0,0 @@ -/** - * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved. - * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options - */ - -import { Model } from '../../src/model/model.js'; -import { ModelText } from '../../src/model/text.js'; -import { compareMarkersForDowncast } from '../../src/conversion/comparemarkers.js'; - -describe( 'compareMarkersForDowncast()', () => { - let model, root; - - beforeEach( () => { - model = new Model(); - root = model.document.createRoot(); - - root._appendChild( [ - new ModelText( 'abcdefghij' ) - ] ); - } ); - - function range( startOffset, endOffset ) { - return model.createRange( - model.createPositionFromPath( root, [ startOffset ] ), - model.createPositionFromPath( root, [ endOffset ] ) - ); - } - - function sortedNames( markers ) { - return markers.sort( compareMarkersForDowncast ).map( ( [ name ] ) => name ); - } - - describe( 'non-overlapping ranges', () => { - it( 'should sort in reverse DOM order', () => { - expect( sortedNames( [ - [ 'a', range( 0, 2 ) ], - [ 'b', range( 4, 6 ) ], - [ 'c', range( 7, 9 ) ] - ] ) ).to.deep.equal( [ 'c', 'b', 'a' ] ); - } ); - - it( 'should sort in reverse DOM order regardless of initial order', () => { - expect( sortedNames( [ - [ 'c', range( 7, 9 ) ], - [ 'a', range( 0, 2 ) ], - [ 'b', range( 4, 6 ) ] - ] ) ).to.deep.equal( [ 'c', 'b', 'a' ] ); - } ); - - it( 'should treat adjacent ranges (end == start) as non-overlapping', () => { - expect( sortedNames( [ - [ 'first', range( 0, 3 ) ], - [ 'second', range( 3, 6 ) ], - [ 'third', range( 6, 9 ) ] - ] ) ).to.deep.equal( [ 'third', 'second', 'first' ] ); - } ); - } ); - - describe( 'overlapping ranges', () => { - it( 'should sort outer marker after inner marker (outer starts earlier)', () => { - expect( sortedNames( [ - [ 'inner', range( 3, 5 ) ], - [ 'outer', range( 1, 7 ) ] - ] ) ).to.deep.equal( [ 'inner', 'outer' ] ); - } ); - - it( 'should sort by start position first for partially overlapping ranges', () => { - expect( sortedNames( [ - [ 'earlier', range( 1, 5 ) ], - [ 'later', range( 3, 7 ) ] - ] ) ).to.deep.equal( [ 'later', 'earlier' ] ); - } ); - - it( 'should use end position as secondary key when starts are equal', () => { - // Same start — the longer range (ending later) sorts first, shorter after. - expect( sortedNames( [ - [ 'shorter', range( 2, 4 ) ], - [ 'longer', range( 2, 6 ) ] - ] ) ).to.deep.equal( [ 'longer', 'shorter' ] ); - } ); - - it( 'should sort three nested markers from innermost to outermost', () => { - expect( sortedNames( [ - [ 'outer', range( 0, 9 ) ], - [ 'mid', range( 2, 7 ) ], - [ 'inner', range( 4, 5 ) ] - ] ) ).to.deep.equal( [ 'inner', 'mid', 'outer' ] ); - } ); - - it( 'should sort three nested markers from innermost to outermost regardless of initial order', () => { - expect( sortedNames( [ - [ 'inner', range( 4, 5 ) ], - [ 'outer', range( 0, 9 ) ], - [ 'mid', range( 2, 7 ) ] - ] ) ).to.deep.equal( [ 'inner', 'mid', 'outer' ] ); - } ); - } ); - - describe( 'identical ranges', () => { - it( 'should fall back to reverse name comparison for identical ranges', () => { - expect( sortedNames( [ - [ 'alpha', range( 2, 5 ) ], - [ 'charlie', range( 2, 5 ) ], - [ 'bravo', range( 2, 5 ) ] - ] ) ).to.deep.equal( [ 'charlie', 'bravo', 'alpha' ] ); - } ); - - it( 'should preserve order for markers with identical ranges and names', () => { - const markers = [ - [ 'same', range( 2, 5 ) ], - [ 'same', range( 2, 5 ) ] - ]; - - const result = compareMarkersForDowncast( markers[ 0 ], markers[ 1 ] ); - - expect( result ).to.equal( 0 ); - } ); - } ); - - describe( 'mixed scenarios', () => { - it( 'should correctly sort a mix of non-overlapping and overlapping ranges', () => { - expect( sortedNames( [ - [ 'solo', range( 8, 9 ) ], - [ 'outer', range( 0, 6 ) ], - [ 'inner', range( 2, 4 ) ] - ] ) ).to.deep.equal( [ 'solo', 'inner', 'outer' ] ); - } ); - - it( 'should correctly sort overlapping ranges sharing the same start with a non-overlapping range', () => { - expect( sortedNames( [ - [ 'short', range( 0, 3 ) ], - [ 'long', range( 0, 7 ) ], - [ 'separate', range( 8, 9 ) ] - ] ) ).to.deep.equal( [ 'separate', 'long', 'short' ] ); - } ); - - it( 'should sort many markers consistently regardless of initial order', () => { - const expected = [ 'e', 'd', 'c', 'b', 'a' ]; - - // Reversed initial order. - expect( sortedNames( [ - [ 'a', range( 0, 2 ) ], - [ 'b', range( 2, 4 ) ], - [ 'c', range( 4, 6 ) ], - [ 'd', range( 6, 8 ) ], - [ 'e', range( 8, 10 ) ] - ] ) ).to.deep.equal( expected ); - - // Random initial order. - expect( sortedNames( [ - [ 'c', range( 4, 6 ) ], - [ 'e', range( 8, 10 ) ], - [ 'a', range( 0, 2 ) ], - [ 'd', range( 6, 8 ) ], - [ 'b', range( 2, 4 ) ] - ] ) ).to.deep.equal( expected ); - } ); - } ); -} ); diff --git a/packages/ckeditor5-engine/tests/conversion/downcasthelpers.js b/packages/ckeditor5-engine/tests/conversion/downcasthelpers.js index 21958e24866..d17594de76b 100644 --- a/packages/ckeditor5-engine/tests/conversion/downcasthelpers.js +++ b/packages/ckeditor5-engine/tests/conversion/downcasthelpers.js @@ -424,15 +424,15 @@ describe( 'DowncastHelpers', () => { it( 'should properly re-bind mapper mappings and retain markers', () => { downcastHelpers.elementToElement( { - model: { - name: 'simpleBlock', - attributes: [ 'toStyle', 'toClass' ] - }, + model: 'simpleBlock', view: ( modelElement, { writer } ) => { const viewElement = writer.createContainerElement( 'div', getViewAttributes( modelElement ) ); return toWidget( viewElement, writer ); }, + triggerBy: { + attributes: [ 'toStyle', 'toClass' ] + }, converterPriority: 'high' } ); @@ -1287,15 +1287,15 @@ describe( 'DowncastHelpers', () => { it( 'should properly re-bind mapper mappings and retain markers', () => { downcastHelpers.elementToElement( { - model: { - name: 'simpleBlock', - attributes: [ 'toStyle', 'toClass' ] - }, + model: 'simpleBlock', view: ( modelElement, { writer } ) => { const viewElement = writer.createContainerElement( 'div', getViewAttributes( modelElement ) ); return toWidget( viewElement, writer ); }, + triggerBy: { + attributes: [ 'toStyle', 'toClass' ] + }, converterPriority: 'high' } ); @@ -4162,378 +4162,6 @@ describe( 'DowncastHelpers', () => { expect( viewToString( viewRoot ) ).to.equal( '

foobar

' ); } ); - it( 'should keep adjacent marker boundaries in model order when markers are added together', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - model.change( writer => { - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 4 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - } ); - - it( 'should keep adjacent marker boundaries in model order when the second marker is added later', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - model.change( writer => { - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - } ); - - model.change( writer => { - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 4 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - } ); - - it( 'adjacent markers do not overlap regardless of creation order', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - const rangeA = model.createRange( model.createPositionAt( modelElement, 0 ), model.createPositionAt( modelElement, 3 ) ); - const rangeB = model.createRange( model.createPositionAt( modelElement, 3 ), model.createPositionAt( modelElement, 6 ) ); - - model.change( writer => { - writer.addMarker( 'marker:a', { range: rangeA, usingOperation: false } ); - writer.addMarker( 'marker:b', { range: rangeB, usingOperation: false } ); - } ); - - const expected = - '

' + - 'foo' + - 'bar' + - '

'; - - expect( viewToString( viewRoot ) ).to.equal( expected ); - - // Remove all markers. - model.change( writer => { - writer.removeMarker( 'marker:a' ); - writer.removeMarker( 'marker:b' ); - } ); - - // Re-add in reversed order in a fresh change block so the differ - // processes them in the new order. - model.change( writer => { - writer.addMarker( 'marker:b', { range: rangeB, usingOperation: false } ); - writer.addMarker( 'marker:a', { range: rangeA, usingOperation: false } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( expected ); - } ); - - it( 'intersecting markers downcast consistently regardless of creation order', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - function r( start, end ) { - return model.createRange( model.createPositionAt( modelElement, start ), model.createPositionAt( modelElement, end ) ); - } - - // "foobar" — offsets 0..6 - const markerRanges = { - base: r( 1, 5 ), - equal: r( 1, 5 ), - outsideStart: r( 0, 1 ), - overlapStart: r( 0, 2 ), - insideStart: r( 1, 3 ), - inside: r( 2, 4 ), - insideEnd: r( 4, 5 ), - overlapEnd: r( 4, 6 ), - outsideEnd: r( 5, 6 ) - }; - - model.change( writer => { - for ( const [ name, range ] of Object.entries( markerRanges ) ) { - writer.addMarker( `marker:${ name }`, { range, usingOperation: false } ); - } - } ); - - const result = viewToString( viewRoot ); - - // Remove all markers. - model.change( writer => { - for ( const name of Object.keys( markerRanges ) ) { - writer.removeMarker( `marker:${ name }` ); - } - } ); - - // Re-add in reversed order in a fresh change block so the differ - // processes them in the new order. - model.change( writer => { - for ( const [ name, range ] of Object.entries( markerRanges ).reverse() ) { - writer.addMarker( `marker:${ name }`, { range, usingOperation: false } ); - } - } ); - - expect( viewToString( viewRoot ) ).to.equal( result ); - } ); - - it( 'should not change adjacent marker positions when text spanning the boundary is wrapped with bold', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - model.schema.extend( '$text', { allowAttributes: 'bold' } ); - - model.change( writer => { - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 4 ) ), - usingOperation: false - } ); - } ); - - // Wrap text spanning the marker boundary (positions 1-5) with bold. - model.change( writer => { - writer.setAttribute( - 'bold', true, - writer.createRange( writer.createPositionAt( modelElement, 1 ), writer.createPositionAt( modelElement, 5 ) ) - ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - } ); - - it( 'should not change adjacent marker positions when bold is removed from text spanning the boundary', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - model.schema.extend( '$text', { allowAttributes: 'bold' } ); - - // Insert bold text and markers. - model.change( writer => { - writer.setAttribute( - 'bold', true, - writer.createRange( writer.createPositionAt( modelElement, 1 ), writer.createPositionAt( modelElement, 5 ) ) - ); - - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 4 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - - // Remove bold from the same range. - model.change( writer => { - writer.removeAttribute( - 'bold', - writer.createRange( writer.createPositionAt( modelElement, 1 ), writer.createPositionAt( modelElement, 5 ) ) - ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - } ); - - it( 'should keep adjacent marker boundary order when one marker is inside bold and the other is not', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - model.schema.extend( '$text', { allowAttributes: 'bold' } ); - - // Make "foo" bold: <$text bold>foobar - model.change( writer => { - writer.setAttribute( - 'bold', true, - writer.createRange( writer.createPositionAt( modelElement, 0 ), writer.createPositionAt( modelElement, 3 ) ) - ); - } ); - - // marker:1 at [1, 3) — inside the bold text. - // marker:2 at [3, 5) — outside the bold text. - // Adjacent at offset 3 (the bold boundary). - model.change( writer => { - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 1 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 5 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

f' + - 'oo' + - '' + - 'bar

' - ); - } ); - - it( 'should keep adjacent marker boundary order when the second marker is inside bold and the first is not', () => { - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - downcastHelpers.attributeToElement( { model: 'bold', view: 'strong' } ); - model.schema.extend( '$text', { allowAttributes: 'bold' } ); - - // Make "bar" bold: foo<$text bold>bar - model.change( writer => { - writer.setAttribute( - 'bold', true, - writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 6 ) ) - ); - } ); - - // marker:1 at [1, 3) — outside the bold text. - // marker:2 at [3, 5) — inside the bold text. - // Adjacent at offset 3 (the bold boundary). - model.change( writer => { - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 1 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 5 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

f' + - 'oo' + - '' + - 'bar

' - ); - } ); - - it( 'should preserve adjacent marker positions when container element is renamed', () => { - model.schema.register( 'heading1', { inheritAllFrom: '$block' } ); - downcastHelpers.elementToElement( { model: 'heading1', view: 'h1' } ); - - downcastHelpers.markerToElement( { - model: 'marker', - view: ( data, { writer } ) => { - return writer.createUIElement( 'span', { - class: `${ data.markerName }-${ data.isOpening ? 'start' : 'end' }` - } ); - } - } ); - - model.change( writer => { - writer.addMarker( 'marker:1', { - range: writer.createRange( writer.createPositionAt( modelElement, 2 ), writer.createPositionAt( modelElement, 3 ) ), - usingOperation: false - } ); - writer.addMarker( 'marker:2', { - range: writer.createRange( writer.createPositionAt( modelElement, 3 ), writer.createPositionAt( modelElement, 4 ) ), - usingOperation: false - } ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - - // Rename paragraph to heading1 — triggers reconversion of the container element. - model.change( writer => { - writer.rename( modelElement, 'heading1' ); - } ); - - expect( viewToString( viewRoot ) ).to.equal( - '

fo' + - 'ob' + - 'ar

' - ); - } ); - it( 'should not convert if consumable was consumed', () => { sinon.spy( controller.downcastDispatcher, 'fire' ); diff --git a/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js b/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js index cb8b20a3bea..b5e4375d64f 100644 --- a/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js +++ b/packages/ckeditor5-list/tests/legacytodolist/legacytodolistediting.js @@ -603,9 +603,11 @@ describe( 'LegacyTodoListEditing', () => { '
  • ' + '' + '' + - '[]' + - '' + - 'foo' + + '[]' + + '' + + '' + + 'foo' + + '' + '' + '
  • ' + '
  • ' +