Explorar o código

Merge pull request #10 from ckeditor/t/5

Fix: Improve words recognition, remove enter from counted characters. Closes #4. Closes #5.
Maciej %!s(int64=6) %!d(string=hai) anos
pai
achega
35e4751f73

+ 25 - 6
packages/ckeditor5-word-count/src/wordcount.js

@@ -12,6 +12,7 @@ import { modelElementToPlainText } from './utils';
 import { throttle, isElement } from 'lodash-es';
 import { throttle, isElement } from 'lodash-es';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import View from '@ckeditor/ckeditor5-ui/src/view';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
 import Template from '@ckeditor/ckeditor5-ui/src/template';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 
 /**
 /**
  * The word count plugin.
  * The word count plugin.
@@ -104,6 +105,24 @@ export default class WordCount extends Plugin {
 		 * @type {module:ui/view~View}
 		 * @type {module:ui/view~View}
 		 */
 		 */
 		this._outputView;
 		this._outputView;
+
+		/**
+		 * A regular expression used to recognize words in the editor's content.
+		 *
+		 * @readonly
+		 * @private
+		 * @type {RegExp}
+		 */
+		this._wordsMatchRegExp = env.features.isRegExpUnicodePropertySupported ?
+			// Usage of regular expression literal cause error during build (ckeditor/ckeditor5-dev#534).
+			// Groups:
+			// {L} - Any kind of letter from any language.
+			// {N} - Any kind of numeric character in any script.
+			// {M} - A character intended to be combined with another character (e.g. accents, umlauts, enclosing boxes, etc.).
+			// {Pd} - Any kind of hyphen or dash.
+			// {Pc} - A punctuation character such as an underscore that connects words.
+			new RegExp( '[\\p{L}\\p{N}\\p{M}\\p{Pd}\\p{Pc}]+', 'gu' ) :
+			/[_\-a-zA-Z0-9À-ž]+/gu;
 	}
 	}
 
 
 	/**
 	/**
@@ -119,7 +138,7 @@ export default class WordCount extends Plugin {
 	init() {
 	init() {
 		const editor = this.editor;
 		const editor = this.editor;
 
 
-		editor.model.document.on( 'change:data', throttle( this._calculateWordsAndCharacters.bind( this ), 250 ) );
+		editor.model.document.on( 'change:data', throttle( this._refreshStats.bind( this ), 250 ) );
 
 
 		if ( typeof this._config.onUpdate == 'function' ) {
 		if ( typeof this._config.onUpdate == 'function' ) {
 			this.on( 'update', ( evt, data ) => {
 			this.on( 'update', ( evt, data ) => {
@@ -221,17 +240,17 @@ export default class WordCount extends Plugin {
 
 
 	/**
 	/**
 	 * Determines the number of words and characters in the current editor's model and assigns it to {@link #characters} and {@link #words}.
 	 * Determines the number of words and characters in the current editor's model and assigns it to {@link #characters} and {@link #words}.
-	 * It also fires {@link #event:update}.
+	 * It also fires the {@link #event:update}.
 	 *
 	 *
 	 * @private
 	 * @private
 	 * @fires update
 	 * @fires update
 	 */
 	 */
-	_calculateWordsAndCharacters() {
+	_refreshStats() {
 		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
 		const txt = modelElementToPlainText( this.editor.model.document.getRoot() );
+		const detectedWords = txt.match( this._wordsMatchRegExp ) || [];
 
 
-		this.characters = txt.length;
-
-		this.words = ( txt.match( /[_a-zA-Z0-9À-ž]+/gu ) || [] ).length;
+		this.characters = txt.replace( /\n/g, '' ).length;
+		this.words = detectedWords.length;
 
 
 		this.fire( 'update', {
 		this.fire( 'update', {
 			words: this.words,
 			words: this.words,

+ 69 - 11
packages/ckeditor5-word-count/tests/wordcount.js

@@ -13,6 +13,9 @@ import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model';
 import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
 import { add as addTranslations, _clear as clearTranslations } from '@ckeditor/ckeditor5-utils/src/translation-service';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
 import Position from '@ckeditor/ckeditor5-engine/src/model/position';
+import ShiftEnter from '@ckeditor/ckeditor5-enter/src/shiftenter';
+import TableEditing from '@ckeditor/ckeditor5-table/src/tableediting';
+import env from '@ckeditor/ckeditor5-utils/src/env';
 
 
 // Delay related to word-count throttling.
 // Delay related to word-count throttling.
 const DELAY = 255;
 const DELAY = 255;
@@ -24,7 +27,7 @@ describe( 'WordCount', () => {
 
 
 	beforeEach( () => {
 	beforeEach( () => {
 		return VirtualTestEditor.create( {
 		return VirtualTestEditor.create( {
-			plugins: [ WordCount, Paragraph ]
+			plugins: [ WordCount, Paragraph, ShiftEnter, TableEditing ]
 		} )
 		} )
 			.then( _editor => {
 			.then( _editor => {
 				editor = _editor;
 				editor = _editor;
@@ -56,9 +59,9 @@ describe( 'WordCount', () => {
 			setModelData( model, '<paragraph>Foo(bar)baz</paragraph>' +
 			setModelData( model, '<paragraph>Foo(bar)baz</paragraph>' +
 				'<paragraph><$text foo="true">Hello</$text> world.</paragraph>' +
 				'<paragraph><$text foo="true">Hello</$text> world.</paragraph>' +
 				'<paragraph>1234</paragraph>' +
 				'<paragraph>1234</paragraph>' +
-				'<paragraph>(-@#$%^*())</paragraph>' );
+				'<paragraph>(@#$%^*())</paragraph>' );
 
 
-			wordCountPlugin._calculateWordsAndCharacters();
+			wordCountPlugin._refreshStats();
 
 
 			expect( wordCountPlugin.words ).to.equal( 6 );
 			expect( wordCountPlugin.words ).to.equal( 6 );
 		} );
 		} );
@@ -66,24 +69,80 @@ describe( 'WordCount', () => {
 		it( 'counts characters', () => {
 		it( 'counts characters', () => {
 			setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
 			setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
 
 
-			wordCountPlugin._calculateWordsAndCharacters();
+			wordCountPlugin._refreshStats();
 
 
 			expect( wordCountPlugin.characters ).to.equal( 12 );
 			expect( wordCountPlugin.characters ).to.equal( 12 );
 		} );
 		} );
 
 
+		it( 'should not count enter as a character', () => {
+			expect( wordCountPlugin.characters ).to.equal( 0 );
+
+			setModelData( model, '<paragraph>Fo<softBreak></softBreak>o</paragraph>' +
+				'<paragraph>Foo</paragraph>' +
+				'<table>' +
+				'<tableRow>' +
+					'<tableCell></tableCell><tableCell></tableCell><tableCell></tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+					'<tableCell></tableCell><tableCell><paragraph>foo</paragraph></tableCell><tableCell></tableCell>' +
+				'</tableRow>' +
+				'<tableRow>' +
+					'<tableCell></tableCell><tableCell></tableCell><tableCell></tableCell>' +
+				'</tableRow>' +
+				'</table>' );
+
+			wordCountPlugin._refreshStats();
+
+			expect( wordCountPlugin.characters ).to.equal( 9 );
+		} );
+
+		it( 'should count international words', function() {
+			if ( !env.features.isRegExpUnicodePropertySupported ) {
+				this.skip();
+			}
+
+			expect( wordCountPlugin.words ).to.equal( 0 );
+
+			setModelData( model, '<paragraph>שמש 太陽 ดวงอาทิตย์ شمس ਸੂਰਜ słońce</paragraph>' );
+			wordCountPlugin._refreshStats();
+
+			expect( wordCountPlugin.words ).to.equal( 6 );
+		} );
+
+		describe( 'ES2018 RegExp Unicode property fallback', () => {
+			const originalPropertiesSupport = env.features.isRegExpUnicodePropertySupported;
+
+			before( () => {
+				env.features.isRegExpUnicodePropertySupported = false;
+			} );
+
+			after( () => {
+				env.features.isRegExpUnicodePropertySupported = originalPropertiesSupport;
+			} );
+
+			it( 'should use different regexp when unicode properties are not supported', () => {
+				expect( wordCountPlugin.words ).to.equal( 0 );
+
+				setModelData( model, '<paragraph>hello world.</paragraph>' );
+				wordCountPlugin._refreshStats();
+
+				expect( wordCountPlugin.words ).to.equal( 2 );
+			} );
+		} );
+
 		describe( 'update event', () => {
 		describe( 'update event', () => {
 			it( 'fires update event with actual amount of characters and words', () => {
 			it( 'fires update event with actual amount of characters and words', () => {
 				const fake = sinon.fake();
 				const fake = sinon.fake();
 				wordCountPlugin.on( 'update', fake );
 				wordCountPlugin.on( 'update', fake );
 
 
-				wordCountPlugin._calculateWordsAndCharacters();
+				wordCountPlugin._refreshStats();
 
 
 				sinon.assert.calledOnce( fake );
 				sinon.assert.calledOnce( fake );
 				sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 0, characters: 0 } );
 				sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 0, characters: 0 } );
 
 
-				// _calculateWordsAndCharacters is throttled, so for this test case is run manually
+				// _refreshStats is throttled, so for this test case is run manually
 				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
 				setModelData( model, '<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
-				wordCountPlugin._calculateWordsAndCharacters();
+				wordCountPlugin._refreshStats();
 
 
 				sinon.assert.calledTwice( fake );
 				sinon.assert.calledTwice( fake );
 				sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 2, characters: 12 } );
 				sinon.assert.calledWithExactly( fake, sinon.match.any, { words: 2, characters: 12 } );
@@ -120,10 +179,9 @@ describe( 'WordCount', () => {
 			setModelData( model, '<paragraph>Foo(bar)baz</paragraph>' +
 			setModelData( model, '<paragraph>Foo(bar)baz</paragraph>' +
 				'<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
 				'<paragraph><$text foo="true">Hello</$text> world.</paragraph>' );
 
 
-			wordCountPlugin._calculateWordsAndCharacters();
+			wordCountPlugin._refreshStats();
 
 
-			// There is \n between paragraph which has to be included into calculations
-			expect( container.innerText ).to.equal( 'Words: 5Characters: 24' );
+			expect( container.innerText ).to.equal( 'Words: 5Characters: 23' );
 		} );
 		} );
 
 
 		it( 'subsequent calls provides the same element', () => {
 		it( 'subsequent calls provides the same element', () => {
@@ -170,7 +228,7 @@ describe( 'WordCount', () => {
 		} );
 		} );
 	} );
 	} );
 
 
-	describe( '_calculateWordsAndCharacters and throttle', () => {
+	describe( '_refreshStats and throttle', () => {
 		beforeEach( done => {
 		beforeEach( done => {
 			// We need to flush initial throttle value after editor's initialization
 			// We need to flush initial throttle value after editor's initialization
 			setTimeout( done, DELAY );
 			setTimeout( done, DELAY );