diff --git a/packages/ckeditor5-engine/src/conversion/upcasthelpers.js b/packages/ckeditor5-engine/src/conversion/upcasthelpers.js
index c51c2789584..b67d64e7611 100644
--- a/packages/ckeditor5-engine/src/conversion/upcasthelpers.js
+++ b/packages/ckeditor5-engine/src/conversion/upcasthelpers.js
@@ -867,6 +867,13 @@ function prepareToAttributeConverter( config, shallow ) {
const matcher = new Matcher( config.view );
return ( evt, data, conversionApi ) => {
+ // Converting an attribute of an element that has not been converted to anything does not make sense
+ // because there will be nowhere to set that attribute on. At this stage, the element should've already
+ // been converted (https://github.com/ckeditor/ckeditor5/issues/11000).
+ if ( !data.modelRange && shallow ) {
+ return;
+ }
+
const match = matcher.match( data.viewItem );
// If there is no match, this callback should not do anything.
diff --git a/packages/ckeditor5-engine/tests/conversion/upcasthelpers.js b/packages/ckeditor5-engine/tests/conversion/upcasthelpers.js
index 16dc3da1784..f658308d9b7 100644
--- a/packages/ckeditor5-engine/tests/conversion/upcasthelpers.js
+++ b/packages/ckeditor5-engine/tests/conversion/upcasthelpers.js
@@ -735,6 +735,30 @@ describe( 'UpcastHelpers', () => {
);
} );
+ // https://github.com/ckeditor/ckeditor5/issues/11000
+ it( 'should not set an attribute on child nodes if parent was not converted', () => {
+ upcastHelpers.elementToElement( { view: 'p', model: 'paragraph' } );
+ upcastHelpers.attributeToAttribute( { view: { key: 'foo' }, model: 'foo' } );
+
+ schema.extend( 'paragraph', {
+ allowAttributes: [ 'foo' ]
+ } );
+
+ schema.extend( '$text', {
+ allowAttributes: [ 'foo' ]
+ } );
+
+ const viewElement = viewParse(
+ '
abc
' +
+ '
def
'
+ );
+
+ expectResult(
+ viewElement,
+ 'abcdef'
+ );
+ } );
+
// #9536.
describe( 'calling the `model.value()` callback', () => {
it( 'should not call the `model.view()` callback if the attribute was already consumed', () => {
diff --git a/packages/ckeditor5-html-support/src/converters.js b/packages/ckeditor5-html-support/src/converters.js
index f7365d13968..b11e747c192 100644
--- a/packages/ckeditor5-html-support/src/converters.js
+++ b/packages/ckeditor5-html-support/src/converters.js
@@ -159,7 +159,11 @@ export function attributeToViewInlineConverter( { priority, view: viewName } ) {
export function viewToModelBlockAttributeConverter( { view: viewName }, dataFilter ) {
return dispatcher => {
dispatcher.on( `element:${ viewName }`, ( evt, data, conversionApi ) => {
- if ( !data.modelRange ) {
+ // Converting an attribute of an element that has not been converted to anything does not make sense
+ // because there will be nowhere to set that attribute on. At this stage, the element should've already
+ // been converted. A collapsed range can show up in to-do lists () or complex widgets (e.g. table).
+ // (https://github.com/ckeditor/ckeditor5/issues/11000).
+ if ( !data.modelRange || data.modelRange.isCollapsed ) {
return;
}
diff --git a/packages/ckeditor5-html-support/tests/datafilter.js b/packages/ckeditor5-html-support/tests/datafilter.js
index b1b085be66a..006485b88d4 100644
--- a/packages/ckeditor5-html-support/tests/datafilter.js
+++ b/packages/ckeditor5-html-support/tests/datafilter.js
@@ -722,6 +722,26 @@ describe( 'DataFilter', () => {
expect( editor.getData() ).to.equal( '