diff --git a/packages/ckeditor5-mention/src/mentioncommand.ts b/packages/ckeditor5-mention/src/mentioncommand.ts index dbe8cedac42..08609394a75 100644 --- a/packages/ckeditor5-mention/src/mentioncommand.ts +++ b/packages/ckeditor5-mention/src/mentioncommand.ts @@ -13,6 +13,12 @@ import { CKEditorError, toMap } from 'ckeditor5/src/utils.js'; import { _addMentionAttributes } from './mentionediting.js'; +const BRACKET_PAIRS = { + '(': ')', + '[': ']', + '{': '}' +} as const; + /** * The mention command. * @@ -168,8 +174,30 @@ export default class MentionCommand extends Command { attributesWithMention.set( 'mention', mention ); // Replace a range with the text with a mention. - model.insertContent( writer.createText( mentionText, attributesWithMention ), range ); - model.insertContent( writer.createText( ' ', currentAttributes ), range!.start.getShiftedBy( mentionText.length ) ); + const insertionRange = model.insertContent( writer.createText( mentionText, attributesWithMention ), range ); + const nodeBefore = insertionRange.start.nodeBefore; + const nodeAfter = insertionRange.end.nodeAfter; + + const isFollowedByWhiteSpace = nodeAfter && nodeAfter.is( '$text' ) && nodeAfter.data.startsWith( ' ' ); + let isInsertedInBrackets = false; + + if ( nodeBefore && nodeAfter && nodeBefore.is( '$text' ) && nodeAfter.is( '$text' ) ) { + const precedingCharacter = nodeBefore.data.slice( -1 ); + const isPrecededByOpeningBracket = precedingCharacter in BRACKET_PAIRS; + const isFollowedByBracketClosure = isPrecededByOpeningBracket && nodeAfter.data.startsWith( + BRACKET_PAIRS[ precedingCharacter as keyof typeof BRACKET_PAIRS ] + ); + + isInsertedInBrackets = isPrecededByOpeningBracket && isFollowedByBracketClosure; + } + + // Don't add a white space if either of the following is true: + // * there's already one after the mention; + // * the mention was inserted in the empty matching brackets. + // https://github.com/ckeditor/ckeditor5/issues/4651 + if ( !isInsertedInBrackets && !isFollowedByWhiteSpace ) { + model.insertContent( writer.createText( ' ', currentAttributes ), range!.start.getShiftedBy( mentionText.length ) ); + } } ); } } diff --git a/packages/ckeditor5-mention/tests/mentioncommand.js b/packages/ckeditor5-mention/tests/mentioncommand.js index 9a8bec022b4..87e972c1be9 100644 --- a/packages/ckeditor5-mention/tests/mentioncommand.js +++ b/packages/ckeditor5-mention/tests/mentioncommand.js @@ -4,7 +4,7 @@ */ import ModelTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/modeltesteditor.js'; -import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; +import { setData, getData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model.js'; import MentionCommand from '../src/mentioncommand.js'; @@ -177,6 +177,49 @@ describe( 'MentionCommand', () => { expectToThrowCKEditorError( () => command.execute( options ), /mentioncommand-incorrect-id/, editor ); } } ); + + it( 'should not insert a white space if the mention was already followed by one', () => { + setData( model, 'foo [] bar' ); + + command.execute( { + marker: '@', + mention: '@John' + } ); + + assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '@John' ); + + expect( getData( model ) ).to.match( + /foo <\$text mention="{"uid":"[^"]+","_text":"@John","id":"@John"}">@John\[\]<\/\$text> bar<\/paragraph>/ + ); + } ); + + [ [ '(', ')' ], [ '[', ']' ], [ '{', '}' ] ].forEach( ( [ openingBracket, closingBracket ] ) => { + it( `should not insert a white space if the mention injected in "${ openingBracket }${ closingBracket }" brackets`, () => { + model.change( writer => { + const paragraph = writer.createElement( 'paragraph' ); + const text = writer.createText( `${ openingBracket }${ closingBracket }` ); + + writer.append( text, paragraph ); + writer.append( paragraph, model.document.getRoot() ); + + writer.setSelection( paragraph, 1 ); + } ); + + command.execute( { + marker: '@', + mention: '@John' + } ); + + assertMention( doc.getRoot().getChild( 0 ).getChild( 1 ), '@John' ); + + expect( getData( model ) ).to.match( + new RegExp( '\\' + openingBracket + + '<\\$text mention="{"uid":"[^"]+","_text":"@John","id":"@John"}">@John\\[\\]\\' + + closingBracket + '' + ) + ); + } ); + } ); } ); function assertMention( textNode, id ) {