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; - } } );