diff --git a/src/featuredetection.js b/src/featuredetection.js
new file mode 100644
index 0000000..234b046
--- /dev/null
+++ b/src/featuredetection.js
@@ -0,0 +1,35 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
+ */
+
+/**
+ * @module mention/featuredetection
+ */
+
+/**
+ * Holds feature detection resolutions used by the mention plugin.
+ *
+ * @protected
+ * @namespace
+ */
+export default {
+ /**
+ * Indicates whether the current browser supports ES2018 Unicode punctuation groups `\p{P}`.
+ *
+ * @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.
+ // See https://github.com/ckeditor/ckeditor5-mention/issues/44#issuecomment-487002174.
+
+ try {
+ punctuationSupported = '.'.search( new RegExp( '[\\p{P}]', 'u' ) ) === 0;
+ } catch ( error ) {
+ // Firefox throws a SyntaxError when the group is unsupported.
+ }
+
+ return punctuationSupported;
+ }() )
+};
diff --git a/src/mentionui.js b/src/mentionui.js
index 0f013dd..0a1ab9f 100644
--- a/src/mentionui.js
+++ b/src/mentionui.js
@@ -12,9 +12,9 @@ import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import Collection from '@ckeditor/ckeditor5-utils/src/collection';
import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
import { keyCodes } from '@ckeditor/ckeditor5-utils/src/keyboard';
+import featureDetection from './featuredetection';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';
import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
-import env from '@ckeditor/ckeditor5-utils/src/env';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
import TextWatcher from './textwatcher';
@@ -536,24 +536,16 @@ function getBalloonPanelPositions( preferredPosition ) {
// Creates a RegExp pattern for the marker.
//
+// Function has to be exported to achieve 100% code coverage.
+//
// @param {String} marker
// @param {Number} minimumCharacters
// @returns {RegExp}
-function createRegExp( marker, minimumCharacters ) {
+export function createRegExp( marker, minimumCharacters ) {
const numberOfCharacters = minimumCharacters == 0 ? '*' : `{${ minimumCharacters },}`;
+ const patternBase = featureDetection.isPunctuationGroupSupported ? '\\p{Ps}\\p{Pi}"\'' : '\\(\\[{"\'';
- if ( !env.isEdge ) {
- // Unfortunately Edge does not throw on `/[\p{Ps}\p{Pi}]/u` as it does on `/\p{Ps}\p{Pi}/u (no square brackets in latter).
- try {
- // Uses the ES2018 syntax. See ckeditor5-mention#44.
- return new RegExp( buildPattern( '\\p{Ps}\\p{Pi}"\'', marker, numberOfCharacters ), 'u' );
- } catch ( error ) {
- // It's OK we're fallback to non ES2018 RegExp later.
- }
- }
-
- // ES2018 RegExp Unicode property escapes are not supported - fallback to save character list.
- return new RegExp( buildPattern( '\\(\\[{"\'', marker, numberOfCharacters ), 'u' );
+ return new RegExp( buildPattern( patternBase, marker, numberOfCharacters ), 'u' );
}
// Helper to build a RegExp pattern string for the marker.
diff --git a/tests/mentionui.js b/tests/mentionui.js
index 8393c2b..bc95448 100644
--- a/tests/mentionui.js
+++ b/tests/mentionui.js
@@ -3,7 +3,7 @@
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
-/* global document, setTimeout, Event, window */
+/* global window, document, setTimeout, Event */
import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor';
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
@@ -18,7 +18,8 @@ import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
import env from '@ckeditor/ckeditor5-utils/src/env';
-import MentionUI from '../src/mentionui';
+import MentionUI, { createRegExp } from '../src/mentionui';
+import featureDetection from '../src/featuredetection';
import MentionEditing from '../src/mentionediting';
import MentionsView from '../src/ui/mentionsview';
@@ -276,8 +277,6 @@ describe( 'MentionUI', () => {
} );
describe( 'typing integration', () => {
- const supportsES2018Escapes = checkES2018RegExpSupport();
-
it( 'should show panel for matched marker after typing minimum characters', () => {
return createClassicTestEditor( { feeds: [ Object.assign( { minimumCharacters: 2 }, staticConfig.feeds[ 0 ] ) ] } )
.then( () => {
@@ -443,94 +442,40 @@ describe( 'MentionUI', () => {
} );
describe( 'ES2018 RegExp Unicode property escapes fallback', () => {
- // Cache the original RegExp to restore it after the tests.
- const RegExp = window.RegExp;
-
- beforeEach( () => {
- // The FakeRegExp throws on first call - it simulates syntax error of ES2018 syntax usage
- // on browsers other then Chrome. See ckeditor5-mention#44.
- function FakeRegExp( pattern, flags ) {
- if ( pattern.includes( '\\p{Ps}' ) ) {
- throw new SyntaxError( 'invalid identity escape in regular expression' );
- }
-
- return new RegExp( pattern, flags );
- }
+ let regExpStub;
- window.RegExp = FakeRegExp;
+ // Cache the original value to restore it after the tests.
+ const originalPunctuationSupport = featureDetection.isPunctuationGroupSupported;
- return createClassicTestEditor( staticConfig );
- } );
-
- afterEach( () => {
- // Restore the original RegExp.
- window.RegExp = RegExp;
+ before( () => {
+ featureDetection.isPunctuationGroupSupported = false;
} );
- it( 'should fallback to old method if browser does not support unicode property escapes', () => {
- setData( model, '[] foo' );
-
- model.change( writer => {
- writer.insertText( '〈', doc.selection.getFirstPosition() );
- } );
-
- model.change( writer => {
- writer.insertText( '@', doc.selection.getFirstPosition() );
- } );
+ beforeEach( () => {
+ return createClassicTestEditor( staticConfig )
+ .then( editor => {
+ regExpStub = sinon.stub( window, 'RegExp' );
- return waitForDebounce()
- .then( () => {
- expect( panelView.isVisible ).to.be.false;
- expect( editor.model.markers.has( 'mention' ) ).to.be.false;
+ return editor;
} );
} );
- it( 'should fallback to old method if browser does not support unicode property escapes (on Edge)', () => {
- // Stub the isEdge for coverage tests in other browsers.
- testUtils.sinon.stub( env, 'isEdge' ).get( () => true );
-
- setData( model, '[] foo' );
-
- model.change( writer => {
- writer.insertText( '〈', doc.selection.getFirstPosition() );
- } );
-
- model.change( writer => {
- writer.insertText( '@', doc.selection.getFirstPosition() );
- } );
-
- return waitForDebounce()
- .then( () => {
- expect( panelView.isVisible ).to.be.false;
- expect( editor.model.markers.has( 'mention' ) ).to.be.false;
- } );
+ after( () => {
+ featureDetection.isPunctuationGroupSupported = originalPunctuationSupport;
} );
- } );
- describe( 'ES2018 RegExp Unicode property escapes fallback on Edge', () => {
- beforeEach( () => {
- // Most tests assume non-edge environment but we do not set `contenteditable=false` on Edge so stub `env.isEdge`.
- testUtils.sinon.stub( env, 'isEdge' ).get( () => true );
-
- return createClassicTestEditor( staticConfig );
+ it( 'returns a simplified RegExp for browsers not supporting Unicode punctuation groups', () => {
+ featureDetection.isPunctuationGroupSupported = false;
+ createRegExp( '@', 2 );
+ sinon.assert.calledOnce( regExpStub );
+ sinon.assert.calledWithExactly( regExpStub, '(^|[ \\(\\[{"\'])([@])([_a-zA-Z0-9À-ž]{2,}?)$', 'u' );
} );
- it( 'should fallback to old method if browser does not support unicode property escapes (on Edge)', () => {
- setData( model, '[] foo' );
-
- model.change( writer => {
- writer.insertText( '〈', doc.selection.getFirstPosition() );
- } );
-
- model.change( writer => {
- writer.insertText( '@', doc.selection.getFirstPosition() );
- } );
-
- return waitForDebounce()
- .then( () => {
- expect( panelView.isVisible ).to.be.false;
- expect( editor.model.markers.has( 'mention' ) ).to.be.false;
- } );
+ it( 'returns a ES2018 RegExp for browsers supporting Unicode punctuation groups', () => {
+ featureDetection.isPunctuationGroupSupported = true;
+ createRegExp( '@', 2 );
+ sinon.assert.calledOnce( regExpStub );
+ sinon.assert.calledWithExactly( regExpStub, '(^|[ \\p{Ps}\\p{Pi}"\'])([@])([_a-zA-Z0-9À-ž]{2,}?)$', 'u' );
} );
} );
@@ -615,7 +560,7 @@ describe( 'MentionUI', () => {
// Belongs to Pi (Punctuation, Initial quote) group:
'«', '‹', '⸌', ' ⸂', '⸠'
] ) {
- testOpeningPunctuationCharacter( character, !supportsES2018Escapes );
+ testOpeningPunctuationCharacter( character, !featureDetection.isPunctuationGroupSupported );
}
it( 'should not show panel for marker in the middle of other word', () => {
@@ -1783,16 +1728,4 @@ describe( 'MentionUI', () => {
expect( mentionForCommand[ key ] ).to.equal( item[ key ] );
}
}
-
- function checkES2018RegExpSupport() {
- let supportsES2018Escapes = false;
-
- try {
- supportsES2018Escapes = new RegExp( '\\p{Ps}', 'u' ).test( '〈' );
- } catch ( error ) {
- // It's Ok we skip test on this browser.
- }
-
- return supportsES2018Escapes;
- }
} );