Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Add support for letter and number class unicode characters. #71

Merged
merged 9 commits into from
Jun 28, 2019
13 changes: 7 additions & 6 deletions src/featuredetection.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@
*/
export default {
/**
* Indicates whether the current browser supports ES2018 Unicode punctuation groups `\p{P}`.
* Indicates whether the current browser supports ES2018 Unicode groups like `\p{P}` or `\p{L}`.
*
* @type {Boolean}
*/
isPunctuationGroupSupported: ( function() {
let punctuationSupported = false;
// Feature detection for Unicode punctuation groups. It's added in ES2018. Currently Firefox and Edge does not support it.
isUnicodeGroupSupported: ( function() {
let isSupported = false;

// Feature detection for Unicode groups. Added in ES2018. Currently Firefox and Edge do not support it.
// See https://github.com/ckeditor/ckeditor5-mention/issues/44#issuecomment-487002174.

try {
punctuationSupported = '.'.search( new RegExp( '[\\p{P}]', 'u' ) ) === 0;
isSupported = 'ć'.search( new RegExp( '[\\p{L}]', 'u' ) ) === 0;
} catch ( error ) {
// Firefox throws a SyntaxError when the group is unsupported.
}

return punctuationSupported;
return isSupported;
}() )
};
25 changes: 13 additions & 12 deletions src/mentionui.js
Original file line number Diff line number Diff line change
Expand Up @@ -532,19 +532,20 @@ function getBalloonPanelPositions( preferredPosition ) {
// @returns {RegExp}
export function createRegExp( marker, minimumCharacters ) {
const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${ minimumCharacters },}`;
const patternBase = featureDetection.isPunctuationGroupSupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';

return new RegExp( buildPattern( patternBase, marker, numberOfCharacters ), 'u' );
}
const openAfterCharacters = featureDetection.isUnicodeGroupSupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';
jodator marked this conversation as resolved.
Show resolved Hide resolved
const mentionCharacters = featureDetection.isUnicodeGroupSupported ? '\\p{L}\\p{N}' : 'a-zA-ZÀ-ž0-9';

// Helper to build a RegExp pattern string for the marker.
//
// @param {String} whitelistedCharacters
// @param {String} marker
// @param {Number} minimumCharacters
// @returns {String}
function buildPattern( whitelistedCharacters, marker, numberOfCharacters ) {
return `(^|[ ${ whitelistedCharacters }])([${ marker }])([_a-zA-Z0-9À-ž]${ numberOfCharacters }?)$`;
// The pattern consists of 3 groups:
// - 0 (non-capturing): Opening sequence - start of the line, space or an opening punctuation character like "(" or "\"",
// - 1: The marker character,
// - 2: Mention input (taking the minimal length into consideration to trigger the UI),
//
// The pattern matches up to the caret (end of string switch - $).
// (0: opening sequence )(1: marker )(2: typed mention )$
const pattern = `(?:^|[ ${ openAfterCharacters }])([${ marker }])([_${ mentionCharacters }]${ numberOfCharacters })$`;

return new RegExp( pattern, 'u' );
}

// Creates a test callback for the marker to be used in the text watcher instance.
Expand All @@ -567,7 +568,7 @@ function getFeedText( marker, text ) {

const match = text.match( regExp );

return match[ 3 ];
return match[ 2 ];
}

// The default feed callback.
Expand Down
49 changes: 41 additions & 8 deletions tests/mentionui.js
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,10 @@ describe( 'MentionUI', () => {
let regExpStub;

// Cache the original value to restore it after the tests.
const originalPunctuationSupport = featureDetection.isPunctuationGroupSupported;
const originalGroupSupport = featureDetection.isUnicodeGroupSupported;

before( () => {
featureDetection.isPunctuationGroupSupported = false;
featureDetection.isUnicodeGroupSupported = false;
} );

beforeEach( () => {
Expand All @@ -468,21 +468,21 @@ describe( 'MentionUI', () => {
} );

after( () => {
featureDetection.isPunctuationGroupSupported = originalPunctuationSupport;
featureDetection.isUnicodeGroupSupported = originalGroupSupport;
} );

it( 'returns a simplified RegExp for browsers not supporting Unicode punctuation groups', () => {
featureDetection.isPunctuationGroupSupported = false;
featureDetection.isUnicodeGroupSupported = false;
createRegExp( '@', 2 );
sinon.assert.calledOnce( regExpStub );
sinon.assert.calledWithExactly( regExpStub, '(^|[ \\(\\[{"\'])([@])([_a-zA-Z0-9À-ž]{2,}?)$', 'u' );
sinon.assert.calledWithExactly( regExpStub, '(?:^|[ \\(\\[{"\'])([@])([_a-zA-ZÀ-ž0-9]{2,})$', 'u' );
} );

it( 'returns a ES2018 RegExp for browsers supporting Unicode punctuation groups', () => {
featureDetection.isPunctuationGroupSupported = true;
featureDetection.isUnicodeGroupSupported = true;
createRegExp( '@', 2 );
sinon.assert.calledOnce( regExpStub );
sinon.assert.calledWithExactly( regExpStub, '(^|[ \\p{Ps}\\p{Pi}"\'])([@])([_a-zA-Z0-9À-ž]{2,}?)$', 'u' );
sinon.assert.calledWithExactly( regExpStub, '(?:^|[ \\p{Ps}\\p{Pi}"\'])([@])([_\\p{L}\\p{N}]{2,})$', 'u' );
} );
} );

Expand Down Expand Up @@ -567,7 +567,7 @@ describe( 'MentionUI', () => {
// Belongs to Pi (Punctuation, Initial quote) group:
'«', '‹', '⸌', ' ⸂', '⸠'
] ) {
testOpeningPunctuationCharacter( character, !featureDetection.isPunctuationGroupSupported );
testOpeningPunctuationCharacter( character, !featureDetection.isUnicodeGroupSupported );
}

it( 'should not show panel for marker in the middle of other word', () => {
Expand Down Expand Up @@ -823,6 +823,39 @@ describe( 'MentionUI', () => {
} );
} );

describe( 'unicode', () => {
beforeEach( () => {
return createClassicTestEditor( {
feeds: [
{
// Always return 5 items
feed: [ '@תַפּוּחַ', '@אַגָס', '@apple', '@pear' ],
marker: '@'
}
]
} );
} );

it( 'should open panel for unicode character ב', function() {
if ( !featureDetection.isUnicodeGroupSupported ) {
this.skip();
}

setData( model, '<paragraph>foo []</paragraph>' );

model.change( writer => {
writer.insertText( '@ס', doc.selection.getFirstPosition() );
} );

return waitForDebounce()
.then( () => {
expect( panelView.isVisible, 'panel is visible' ).to.be.true;
expect( editor.model.markers.has( 'mention' ), 'marker is inserted' ).to.be.true;
expect( mentionsView.items ).to.have.length( 1 );
} );
} );
} );

describe( 'asynchronous list with custom trigger', () => {
beforeEach( () => {
const issuesNumbers = [ '#100', '#101', '#102', '#103' ];
Expand Down