From fe78ca0b5afd756d81be515ec5731b9822acd377 Mon Sep 17 00:00:00 2001 From: Szymon Cofalik Date: Fri, 20 Jan 2023 14:04:24 +0100 Subject: [PATCH] Markers should not be lost on list items and in table cells in the data pipeline. --- .../ckeditor5-clipboard/tests/dragdrop.js | 4 +- .../ckeditor5-engine/src/view/domconverter.ts | 6 +- .../tests/view/domconverter/view-to-dom.js | 62 +++ .../tests/htmlcomment-integration.js | 2 +- .../src/documentlist/converters.ts | 11 +- .../documentlist/integrations/markers.js | 352 ++++++++++++++++++ .../src/converters/downcast.js | 11 +- .../src/converters/upcasttable.js | 23 +- packages/ckeditor5-table/src/tableediting.js | 39 -- .../tests/converters/upcasttable.js | 2 +- .../tests/table-integration.js | 309 ++++++++++++++- 11 files changed, 763 insertions(+), 58 deletions(-) create mode 100644 packages/ckeditor5-list/tests/documentlist/integrations/markers.js diff --git a/packages/ckeditor5-clipboard/tests/dragdrop.js b/packages/ckeditor5-clipboard/tests/dragdrop.js index bb087cf86a3..13e66d56e09 100644 --- a/packages/ckeditor5-clipboard/tests/dragdrop.js +++ b/packages/ckeditor5-clipboard/tests/dragdrop.js @@ -863,7 +863,7 @@ describe( 'Drag and Drop', () => { expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( - '
abc
' + '

abc

' ); dataTransferMock.dropEffect = 'move'; @@ -929,7 +929,7 @@ describe( 'Drag and Drop', () => { expect( spyClipboardOutput.firstCall.firstArg.method ).to.equal( 'dragstart' ); expect( spyClipboardOutput.firstCall.firstArg.dataTransfer ).to.equal( dataTransferMock ); expect( stringifyView( spyClipboardOutput.firstCall.firstArg.content ) ).to.equal( - '
abc
' + '

abc

' ); } ); diff --git a/packages/ckeditor5-engine/src/view/domconverter.ts b/packages/ckeditor5-engine/src/view/domconverter.ts index f791fbc6bf7..b89895a57f1 100644 --- a/packages/ckeditor5-engine/src/view/domconverter.ts +++ b/packages/ckeditor5-engine/src/view/domconverter.ts @@ -29,7 +29,8 @@ import { indexOf, getAncestors, isText, - isComment + isComment, + first } from '@ckeditor/ckeditor5-utils'; import type ViewNode from './node'; @@ -558,7 +559,8 @@ export default class DomConverter { } const transparentRendering = childView.is( 'element' ) && - ( childView.getCustomProperty( 'dataPipeline:transparentRendering' ) as boolean | undefined ); + !!childView.getCustomProperty( 'dataPipeline:transparentRendering' ) && + !first( childView.getAttributes() ); if ( transparentRendering && this.renderingMode == 'data' ) { yield* this.viewChildrenToDom( childView, options ); diff --git a/packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js b/packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js index bd18797f854..fe5f0bde2bc 100644 --- a/packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js +++ b/packages/ckeditor5-engine/tests/view/domconverter/view-to-dom.js @@ -1291,6 +1291,68 @@ describe( 'DomConverter', () => { sinon.match.string // Link to the documentation ); } ); + + it( 'should not be transparent in the data pipeline if has any attribute', () => { + converter.renderingMode = 'data'; + converter.blockFillerMode = 'nbsp'; + + const warnStub = testUtils.sinon.stub( console, 'warn' ); + + const viewList = parse( + '' + + '' + + '' + + 'foo' + + '' + + '' + + 'bar' + + '' + + '' + + 'baz' + + '' + + '' + + '' + ); + + const bogusParagraph1 = viewList.getChild( 0 ).getChild( 0 ).getChild( 0 ); + const bogusParagraph2 = viewList.getChild( 0 ).getChild( 1 ).getChild( 0 ); + const bogusParagraph3 = viewList.getChild( 0 ).getChild( 2 ).getChild( 0 ); + + bogusParagraph1._setCustomProperty( 'dataPipeline:transparentRendering', true ); + bogusParagraph2._setCustomProperty( 'dataPipeline:transparentRendering', true ); + bogusParagraph3._setCustomProperty( 'dataPipeline:transparentRendering', true ); + + const domDivChildren = Array.from( converter.viewChildrenToDom( viewList ) ); + + expect( domDivChildren.length ).to.equal( 1 ); + expect( domDivChildren[ 0 ].tagName.toLowerCase() ).to.equal( 'ul' ); + + const domUlChildren = Array.from( domDivChildren[ 0 ].childNodes ); + + expect( domUlChildren.length ).to.equal( 3 ); + expect( domUlChildren[ 0 ].tagName.toLowerCase() ).to.equal( 'li' ); + expect( domUlChildren[ 1 ].tagName.toLowerCase() ).to.equal( 'li' ); + expect( domUlChildren[ 2 ].tagName.toLowerCase() ).to.equal( 'li' ); + + const domUl1Children = Array.from( domUlChildren[ 0 ].childNodes ); + const domUl2Children = Array.from( domUlChildren[ 1 ].childNodes ); + const domUl3Children = Array.from( domUlChildren[ 2 ].childNodes ); + + expect( domUl1Children.length ).to.equal( 1 ); + expect( domUl1Children[ 0 ].tagName.toLowerCase() ).to.equal( 'p' ); + expect( domUl1Children[ 0 ].getAttribute( 'class' ) ).to.equal( 'style' ); + expect( domUl1Children[ 0 ].firstChild.data ).to.equal( 'foo' ); + + expect( domUl2Children.length ).to.equal( 1 ); + expect( domUl2Children[ 0 ].tagName.toLowerCase() ).to.equal( 'p' ); + expect( domUl2Children[ 0 ].getAttribute( 'data-foo' ) ).to.equal( '123' ); + expect( domUl2Children[ 0 ].firstChild.data ).to.equal( 'bar' ); + + expect( domUl3Children.length ).to.equal( 1 ); + expect( domUl3Children[ 0 ].data ).to.equal( 'baz' ); + + sinon.assert.notCalled( warnStub ); + } ); } ); } ); diff --git a/packages/ckeditor5-html-support/tests/htmlcomment-integration.js b/packages/ckeditor5-html-support/tests/htmlcomment-integration.js index 9de645bf5d0..46c9d81b07b 100644 --- a/packages/ckeditor5-html-support/tests/htmlcomment-integration.js +++ b/packages/ckeditor5-html-support/tests/htmlcomment-integration.js @@ -1157,8 +1157,8 @@ describe( 'HtmlComment integration', () => { '' + '' + '' + - '' + ' ' + + '' + '' + '' + '' + diff --git a/packages/ckeditor5-list/src/documentlist/converters.ts b/packages/ckeditor5-list/src/documentlist/converters.ts index ea272451d13..7a484fd9eae 100644 --- a/packages/ckeditor5-list/src/documentlist/converters.ts +++ b/packages/ckeditor5-list/src/documentlist/converters.ts @@ -369,12 +369,15 @@ export function bogusParagraphCreator( return null; } - const viewElement = writer.createContainerElement( 'span', { class: 'ck-list-bogus-paragraph' } ); - - if ( dataPipeline ) { - writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement ); + if ( !dataPipeline ) { + return writer.createContainerElement( 'span', { class: 'ck-list-bogus-paragraph' } ); } + // Using `

` in case there are some markers on it and transparentRendering will render it anyway. + const viewElement = writer.createContainerElement( 'p' ); + + writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement ); + return viewElement; }; } diff --git a/packages/ckeditor5-list/tests/documentlist/integrations/markers.js b/packages/ckeditor5-list/tests/documentlist/integrations/markers.js new file mode 100644 index 00000000000..0a74addc3c0 --- /dev/null +++ b/packages/ckeditor5-list/tests/documentlist/integrations/markers.js @@ -0,0 +1,352 @@ +/** + * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved. + * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license + */ + +/* global document */ + +import DocumentListEditing from '../../../src/documentlist/documentlistediting'; + +import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; +import ImageBlockEditing from '@ckeditor/ckeditor5-image/src/image/imageblockediting'; +import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; + +import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; +import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; + +import stubUid from '../_utils/uid'; + +describe( 'DocumentListEditing integrations: markers', () => { + let element, editor, model, root; + + testUtils.createSinonSandbox(); + + beforeEach( async () => { + element = document.createElement( 'div' ); + document.body.appendChild( element ); + + editor = await ClassicTestEditor.create( element, { + plugins: [ Paragraph, DocumentListEditing, ImageBlockEditing ] + } ); + + model = editor.model; + root = model.document.getRoot(); + + editor.conversion.for( 'upcast' ).dataToMarker( { view: 'foo' } ); + editor.conversion.for( 'dataDowncast' ).markerToData( { model: 'foo' } ); + + stubUid(); + } ); + + afterEach( async () => { + element.remove(); + + await editor.destroy(); + } ); + + function addMarker( range ) { + model.change( writer => { + writer.addMarker( 'foo:bar', { + usingOperation: true, + affectsData: true, + range + } ); + } ); + } + + function checkMarker( range ) { + const marker = model.markers.get( 'foo:bar' ); + + expect( marker ).to.not.be.null; + expect( marker.getRange().isEqual( range ) ).to.be.true; + } + + describe( 'list item with a single paragraph', () => { + beforeEach( () => { + setModelData( model, + 'A' + ); + } ); + + it( 'marker beginning before a paragraph and ending inside', () => { + const range = model.createRange( + model.createPositionBefore( root.getChild( 0 ) ), + model.createPositionAt( root.getChild( 0 ), 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning before an empty paragraph and ending inside', () => { + model.change( writer => writer.remove( root.getChild( 0 ).getChild( 0 ) ) ); + + const range = model.createRange( + model.createPositionBefore( root.getChild( 0 ) ), + model.createPositionAt( root.getChild( 0 ), 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning before a paragraph and ending after it', () => { + const range = model.createRangeOn( root.getChild( 0 ) ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker inside a paragraph', () => { + const range = model.createRangeIn( root.getChild( 0 ) ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker inside an empty paragraph', () => { + model.change( writer => writer.remove( root.getChild( 0 ).getChild( 0 ) ) ); + + const range = model.createRangeIn( root.getChild( 0 ) ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + } ); + + describe( 'list item with multiple paragraphs', () => { + beforeEach( () => { + setModelData( model, + 'A' + + 'B' + ); + } ); + + it( 'marker beginning before a paragraph and ending inside', () => { + const range = model.createRange( + model.createPositionBefore( root.getChild( 0 ) ), + model.createPositionAt( root.getChild( 0 ), 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning before a paragraph and ending inside the next paragraph', () => { + const range = model.createRange( + model.createPositionBefore( root.getChild( 0 ) ), + model.createPositionAt( root.getChild( 1 ), 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning before an empty paragraph and ending inside the next paragraph', () => { + model.change( writer => { + writer.remove( root.getChild( 0 ).getChild( 0 ) ); + writer.remove( root.getChild( 1 ).getChild( 0 ) ); + } ); + + const range = model.createRange( + model.createPositionBefore( root.getChild( 0 ) ), + model.createPositionAt( root.getChild( 1 ), 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning before a paragraph and ending after the next paragraph', () => { + const range = model.createRangeIn( root ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker starting in a paragraph and ending in next paragraph', () => { + const range = model.createRange( + model.createPositionAt( root.getChild( 0 ), 'end' ), + model.createPositionAt( root.getChild( 1 ), 0 ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker starting in a empty paragraph and ending in next empty paragraph', () => { + model.change( writer => { + writer.remove( root.getChild( 0 ).getChild( 0 ) ); + writer.remove( root.getChild( 1 ).getChild( 0 ) ); + } ); + + const range = model.createRange( + model.createPositionAt( root.getChild( 0 ), 'end' ), + model.createPositionAt( root.getChild( 1 ), 0 ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + } ); +} ); diff --git a/packages/ckeditor5-table/src/converters/downcast.js b/packages/ckeditor5-table/src/converters/downcast.js index c0b69f7fef6..9543670d223 100644 --- a/packages/ckeditor5-table/src/converters/downcast.js +++ b/packages/ckeditor5-table/src/converters/downcast.js @@ -114,7 +114,7 @@ export function downcastCell( options = {} ) { * @returns {Function} Element creator. */ export function convertParagraphInTableCell( options = {} ) { - return ( modelElement, { writer, consumable, mapper } ) => { + return ( modelElement, { writer } ) => { if ( !modelElement.parent.is( 'element', 'tableCell' ) ) { return; } @@ -126,9 +126,12 @@ export function convertParagraphInTableCell( options = {} ) { if ( options.asWidget ) { return writer.createContainerElement( 'span', { class: 'ck-table-bogus-paragraph' } ); } else { - // Additional requirement for data pipeline to have backward compatible data tables. - consumable.consume( modelElement, 'insert' ); - mapper.bindElements( modelElement, mapper.toViewElement( modelElement.parent ) ); + // Using `

` in case there are some markers on it and transparentRendering will render it anyway. + const viewElement = writer.createContainerElement( 'p' ); + + writer.setCustomProperty( 'dataPipeline:transparentRendering', true, viewElement ); + + return viewElement; } }; } diff --git a/packages/ckeditor5-table/src/converters/upcasttable.js b/packages/ckeditor5-table/src/converters/upcasttable.js index 47f23a49029..e6a7032cfb9 100644 --- a/packages/ckeditor5-table/src/converters/upcasttable.js +++ b/packages/ckeditor5-table/src/converters/upcasttable.js @@ -146,18 +146,33 @@ export function skipEmptyTableRow() { */ export function ensureParagraphInTableCell( elementName ) { return dispatcher => { - dispatcher.on( `element:${ elementName }`, ( evt, data, conversionApi ) => { + dispatcher.on( `element:${ elementName }`, ( evt, data, { writer } ) => { // The default converter will create a model range on converted table cell. if ( !data.modelRange ) { return; } + const tableCell = data.modelRange.start.nodeAfter; + const modelCursor = writer.createPositionAt( tableCell, 0 ); + // Ensure a paragraph in the model for empty table cells for converted table cells. if ( data.viewItem.isEmpty ) { - const tableCell = data.modelRange.start.nodeAfter; - const modelCursor = conversionApi.writer.createPositionAt( tableCell, 0 ); + writer.insertElement( 'paragraph', modelCursor ); + + return; + } - conversionApi.writer.insertElement( 'paragraph', modelCursor ); + const childNodes = Array.from( tableCell.getChildren() ); + + // In case there are only markers inside the table cell then move them to the paragraph. + if ( childNodes.every( node => node.is( 'element', '$marker' ) ) ) { + const paragraph = writer.createElement( 'paragraph' ); + + writer.insert( paragraph, writer.createPositionAt( tableCell, 0 ) ); + + for ( const node of childNodes ) { + writer.move( writer.createRangeOn( node ), writer.createPositionAt( paragraph, 'end' ) ); + } } }, { priority: 'low' } ); }; diff --git a/packages/ckeditor5-table/src/tableediting.js b/packages/ckeditor5-table/src/tableediting.js index f8377501d9f..c4dc7f98f41 100644 --- a/packages/ckeditor5-table/src/tableediting.js +++ b/packages/ckeditor5-table/src/tableediting.js @@ -152,11 +152,6 @@ export default class TableEditing extends Plugin { view: 'rowspan' } ); - // Manually adjust model position mappings in a special case, when a table cell contains a paragraph, which is bound - // to its parent (to the table cell). This custom model-to-view position mapping is necessary in data pipeline only, - // because only during this conversion a paragraph can be bound to its parent. - editor.data.mapper.on( 'modelToViewPosition', mapTableCellModelPositionToView() ); - // Define the config. editor.config.define( 'table.defaultHeadings.rows', 0 ); editor.config.define( 'table.defaultHeadings.columns', 0 ); @@ -197,40 +192,6 @@ export default class TableEditing extends Plugin { } } -// Creates a mapper callback to adjust model position mappings in a table cell containing a paragraph, which is bound to its parent -// (to the table cell). Only positions after this paragraph have to be adjusted, because after binding this paragraph to the table cell, -// elements located after this paragraph would point either to a non-existent offset inside `tableCell` (if paragraph is empty), or after -// the first character of the paragraph's text. See https://github.com/ckeditor/ckeditor5/issues/10116. -// -// ^ -> ^  -// -// foobar^ -> foobar^ -// -// @returns {Function} -function mapTableCellModelPositionToView() { - return ( evt, data ) => { - const modelParent = data.modelPosition.parent; - const modelNodeBefore = data.modelPosition.nodeBefore; - - if ( !modelParent.is( 'element', 'tableCell' ) ) { - return; - } - - if ( !modelNodeBefore || !modelNodeBefore.is( 'element', 'paragraph' ) ) { - return; - } - - const viewNodeBefore = data.mapper.toViewElement( modelNodeBefore ); - const viewParent = data.mapper.toViewElement( modelParent ); - - if ( viewNodeBefore === viewParent ) { - // Since the paragraph has already been bound to its parent, update the current position in the model with paragraph's - // max offset, so it points to the place which should normally (in all other cases) be the end position of this paragraph. - data.viewPosition = data.mapper.findPositionIn( viewParent, modelNodeBefore.maxOffset ); - } - }; -} - // Returns fixed colspan and rowspan attrbutes values. // // @private diff --git a/packages/ckeditor5-table/tests/converters/upcasttable.js b/packages/ckeditor5-table/tests/converters/upcasttable.js index 029a4e63561..24f575dcfb2 100644 --- a/packages/ckeditor5-table/tests/converters/upcasttable.js +++ b/packages/ckeditor5-table/tests/converters/upcasttable.js @@ -333,7 +333,7 @@ describe( 'upcastTable()', () => { ); expectModel( - '' + '' ); } ); diff --git a/packages/ckeditor5-table/tests/table-integration.js b/packages/ckeditor5-table/tests/table-integration.js index f86ec0a8a14..6835da9140e 100644 --- a/packages/ckeditor5-table/tests/table-integration.js +++ b/packages/ckeditor5-table/tests/table-integration.js @@ -253,7 +253,7 @@ describe( 'Table feature – integration with markers', () => { editor.setData( '

' ); expect( editor.getData() ).to.equal( - '

 
' + '
 
' ); } ); @@ -281,4 +281,311 @@ describe( 'Table feature – integration with markers', () => { ); } ); } ); + + describe( 'markers converted to data and vice versa', () => { + beforeEach( async () => { + editor = await ClassicTestEditor.create( '', { plugins: [ Paragraph, TableEditing ] } ); + + editor.conversion.for( 'upcast' ).dataToMarker( { view: 'foo' } ); + editor.conversion.for( 'dataDowncast' ).markerToData( { model: 'foo' } ); + } ); + + function addMarker( range ) { + editor.model.change( writer => { + writer.addMarker( 'foo:bar', { + usingOperation: true, + affectsData: true, + range + } ); + } ); + } + + function checkMarker( range ) { + const marker = editor.model.markers.get( 'foo:bar' ); + + expect( marker ).to.not.be.null; + expect( marker.getRange().isEqual( range ) ).to.be.true; + } + + describe( 'single empty paragraph', () => { + let paragraph; + + beforeEach( async () => { + setModelData( editor.model, modelTable( [ [ '' ] ] ) ); + + paragraph = editor.model.document.getRoot().getNodeByPath( [ 0, 0, 0, 0 ] ); + } ); + + it( 'marker beginning before a paragraph and ending inside', async () => { + const range = editor.model.createRange( + editor.model.createPositionBefore( paragraph ), + editor.model.createPositionAt( paragraph, 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '

 

' + + '
' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning in a paragraph and ending after it', async () => { + const range = editor.model.createRange( + editor.model.createPositionAt( paragraph, 0 ), + editor.model.createPositionAfter( paragraph ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

 

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker on the paragraph', async () => { + const range = editor.model.createRangeOn( paragraph ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

 

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker inside a paragraph', async () => { + const range = editor.model.createRangeIn( paragraph ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '
 
' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + } ); + + describe( 'single paragraph', () => { + let paragraph; + + beforeEach( async () => { + setModelData( editor.model, modelTable( [ [ 'text' ] ] ) ); + + paragraph = editor.model.document.getRoot().getNodeByPath( [ 0, 0, 0, 0 ] ); + } ); + + it( 'marker beginning before a paragraph and ending inside', async () => { + const range = editor.model.createRange( + editor.model.createPositionBefore( paragraph ), + editor.model.createPositionAt( paragraph, 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '

text

' + + '
' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning in a paragraph and ending after it', async () => { + const range = editor.model.createRange( + editor.model.createPositionAt( paragraph, 0 ), + editor.model.createPositionAfter( paragraph ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

text

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker on the paragraph', async () => { + const range = editor.model.createRangeOn( paragraph ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

text

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker inside a paragraph', async () => { + const range = editor.model.createRangeIn( paragraph ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '
text
' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + } ); + + describe( 'multiple paragraphs', () => { + let paragraphA, paragraphB, tableCell; + + beforeEach( async () => { + setModelData( editor.model, modelTable( [ [ 'ab' ] ] ) ); + + tableCell = editor.model.document.getRoot().getNodeByPath( [ 0, 0, 0 ] ); + paragraphA = tableCell.getChild( 0 ); + paragraphB = tableCell.getChild( 1 ); + } ); + + it( 'marker beginning before a paragraph and ending inside another paragraph', async () => { + const range = editor.model.createRange( + editor.model.createPositionBefore( paragraphA ), + editor.model.createPositionAt( paragraphB, 'end' ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

a

b

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker beginning in a paragraph and ending after another paragraph', async () => { + const range = editor.model.createRange( + editor.model.createPositionAt( paragraphA, 0 ), + editor.model.createPositionAfter( paragraphB ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

a

b

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker on multiple paragraphs', async () => { + const range = editor.model.createRangeIn( tableCell ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

a

b

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + + it( 'marker inside starting in a paragraph and ending in an other paragraph', async () => { + const range = editor.model.createRange( + editor.model.createPositionAt( paragraphA, 'end' ), + editor.model.createPositionAt( paragraphB, 0 ) + ); + + addMarker( range ); + + const data = editor.getData(); + + expect( data ).to.equal( + '
' + + '' + + '

a

b

' + ); + + editor.setData( data ); + + checkMarker( range ); + expect( editor.getData() ).to.equal( data ); + } ); + } ); + } ); } );