From 4a4726773a08ea6ac57240e8500e0ce97926ab72 Mon Sep 17 00:00:00 2001 From: James Nylen Date: Wed, 18 Nov 2015 15:33:30 -0600 Subject: [PATCH 1/3] Editor: Add word count --- client/lib/posts/post-edit-store.js | 1 + client/lib/text-utils/Makefile | 12 +++ client/lib/text-utils/index.js | 26 +++++++ client/lib/text-utils/test/index.js | 64 +++++++++++++++ .../post-editor/editor-word-count/index.jsx | 78 +++++++++++++++++++ client/post-editor/post-editor.jsx | 4 + client/post-editor/style.scss | 33 ++++++++ 7 files changed, 218 insertions(+) create mode 100644 client/lib/text-utils/Makefile create mode 100644 client/lib/text-utils/index.js create mode 100644 client/lib/text-utils/test/index.js create mode 100644 client/post-editor/editor-word-count/index.jsx diff --git a/client/lib/posts/post-edit-store.js b/client/lib/posts/post-edit-store.js index 8bb22b49b503..2750f7da8f15 100644 --- a/client/lib/posts/post-edit-store.js +++ b/client/lib/posts/post-edit-store.js @@ -229,6 +229,7 @@ function setRawContent( content ) { if ( PostEditStore.isDirty() !== isDirty || PostEditStore.hasContent() !== hasContent ) { PostEditStore.emit( 'change' ); } + PostEditStore.emit( 'rawContentChange' ); } } diff --git a/client/lib/text-utils/Makefile b/client/lib/text-utils/Makefile new file mode 100644 index 000000000000..c51b1c60d0d5 --- /dev/null +++ b/client/lib/text-utils/Makefile @@ -0,0 +1,12 @@ +NODE_BIN := $(shell npm bin) +MOCHA ?= $(NODE_BIN)/mocha +BASE_DIR := $(NODE_BIN)/../.. +NODE_PATH := test:$(BASE_DIR)/client:$(BASE_DIR)/shared +COMPILERS ?= js:babel/register +REPORTER ?= spec +UI ?= bdd + +test: + @NODE_ENV=test NODE_PATH=$(NODE_PATH) $(MOCHA) --compilers $(COMPILERS) --reporter $(REPORTER) --ui $(UI) + +.PHONY: test diff --git a/client/lib/text-utils/index.js b/client/lib/text-utils/index.js new file mode 100644 index 000000000000..f16e471eb20c --- /dev/null +++ b/client/lib/text-utils/index.js @@ -0,0 +1,26 @@ +function countWords( content ) { + // Adapted from TinyMCE wordcount plugin: + // https://github.com/tinymce/tinymce/blob/4.2.6/js/tinymce/plugins/wordcount/plugin.js + + if ( content && typeof content === 'string' ) { + content = content.replace( /\.\.\./g, ' ' ); // convert ellipses to spaces + content = content.replace( /<.[^<>]*?>/g, ' ' ); // remove HTML tags + content = content.replace( / | /gi, ' ' ); // remove space chars + + // deal with HTML entities + content = content.replace( /(\w+)(&#?[a-z0-9]+;)+(\w+)/i, '$1$3' ); // strip entities inside words + content = content.replace( /&.+?;/g, ' ' ); // turn all other entities into spaces + + // remove numbers and punctuation + content = content.replace( /[0-9.(),;:!?%#$?\x27\x22_+=\\\/\-]*/g, '' ); + + const words = content.match( /[\w\u2019\x27\-\u00C0-\u1FFF]+/g ); + if ( words ) { + return words.length; + } + } + + return 0; +} + +export default { countWords }; diff --git a/client/lib/text-utils/test/index.js b/client/lib/text-utils/test/index.js new file mode 100644 index 000000000000..949d2e6d8020 --- /dev/null +++ b/client/lib/text-utils/test/index.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { expect } from 'chai'; + +/** + * Internal dependencies + */ +import textUtils from '../'; + +// Adapted from TinyMCE word count tests: +// https://github.com/tinymce/tinymce/blob/4.2.6/tests/plugins/wordcount.js + +describe( 'textUtils', () => { + describe( 'wordCount', () => { + it( 'should return 0 for blank content', () => { + expect( textUtils.countWords( + '' + ) ).to.equal( 0 ); + } ); + + it( 'should strip HTML tags and count words for a simple sentence', () => { + expect( textUtils.countWords( + '

My sentence is this.

' + ) ).to.equal( 4 ); + } ); + + it( 'should not count dashes', () => { + expect( textUtils.countWords( + '

Something -- ok

' + ) ).to.equal( 2 ); + } ); + + it( 'should not count asterisks or other non-word characters', () => { + expect( textUtils.countWords( + '

* something\n\u00b7 something else

' + ) ).to.equal( 3 ); + } ); + + it( 'should not count numbers', () => { + expect( textUtils.countWords( + '

Something 123 ok

' + ) ).to.equal( 2 ); + } ); + + it( 'should not count HTML entities', () => { + expect( textUtils.countWords( + '

It’s my life – – – don\'t you forget.

' + ) ).to.equal( 6 ); + } ); + + it( 'should count hyphenated words as one word', () => { + expect( textUtils.countWords( + '

Hello some-word here.

' + ) ).to.equal( 3 ); + } ); + + it( 'should count words between blocks as two words', () => { + expect( textUtils.countWords( + '

Hello

world

' + ) ).to.equal( 2 ); + } ); + } ); +} ); diff --git a/client/post-editor/editor-word-count/index.jsx b/client/post-editor/editor-word-count/index.jsx new file mode 100644 index 000000000000..ccce8a5812db --- /dev/null +++ b/client/post-editor/editor-word-count/index.jsx @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import React from 'react/addons'; + +/** + * Internal dependencies + */ +import PostEditStore from 'lib/posts/post-edit-store'; +import userModule from 'lib/user'; +import Count from 'components/count'; +import textUtils from 'lib/text-utils'; + +/** + * Module variables + */ +const user = userModule(); + +export default React.createClass( { + displayName: 'EditorWordCount', + + mixins: [ React.addons.PureRenderMixin ], + + getInitialState() { + return { + rawContent: '' + }; + }, + + componentWillMount() { + PostEditStore.on( 'rawContentChange', this.onRawContentChange ); + }, + + componentDidMount() { + this.onRawContentChange(); + }, + + componentWillUnmount() { + PostEditStore.removeListener( 'rawContentChange', this.onRawContentChange ); + }, + + onRawContentChange() { + this.setState( { + rawContent: PostEditStore.getRawContent() + } ); + }, + + render() { + const currentUser = user.get(); + const localeSlug = currentUser && currentUser.localeSlug || 'en'; + + switch ( localeSlug ) { + case 'ja': + case 'th': + case 'zh-cn': + case 'zh-hk': + case 'zh-sg': + case 'zh-tw': + // TODO these are character-based languages - count characters instead + return null; + + case 'ko': + // TODO Korean is not supported by our current word count regex + return null; + } + + return ( +
+ { this.translate( 'Word Count' ) } + +
+ ); + }, + + getCount() { + return textUtils.countWords( this.state.rawContent ); + } +} ); diff --git a/client/post-editor/post-editor.jsx b/client/post-editor/post-editor.jsx index 15148b06d76e..a9dc7d536604 100644 --- a/client/post-editor/post-editor.jsx +++ b/client/post-editor/post-editor.jsx @@ -28,6 +28,7 @@ var actions = require( 'lib/posts/actions' ), SimpleNotice = require( 'notices/simple-notice' ), protectForm = require( 'lib/mixins/protect-form' ), TinyMCE = require( 'components/tinymce' ), + EditorWordCount = require( 'post-editor/editor-word-count' ), SegmentedControl = require( 'components/segmented-control' ), SegmentedControlItem = require( 'components/segmented-control/item' ), EditorMobileNavigation = require( 'post-editor/editor-mobile-navigation' ), @@ -390,6 +391,9 @@ var PostEditor = React.createClass( { onTextEditorChange={ this.onEditorContentChange } onTogglePin={ this.onTogglePin } /> +
+ +
{ this.iframePreviewEnabled() ? 660px" ) { + width: 683px; // TODO ??? + } + + & .editor-word-count { + float: right; + border-top: 1px solid lighten( $gray, 20% ); + border-left: 1px solid lighten( $gray, 20% ); + border-radius: 4px 0; + background-color: rgba( $white, 0.92 ); + color: $gray; + text-transform: uppercase; + padding: 8px; + + & .count { + margin-left: 4px; + color: darken( $gray, 10% ); + } + } +} From fa276d014acdfdb282c3e8e9e0693d4857408f68 Mon Sep 17 00:00:00 2001 From: James Nylen Date: Fri, 20 Nov 2015 14:00:48 -0600 Subject: [PATCH 2/3] Remove extra spaces --- client/lib/text-utils/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/lib/text-utils/index.js b/client/lib/text-utils/index.js index f16e471eb20c..5fe806b8c500 100644 --- a/client/lib/text-utils/index.js +++ b/client/lib/text-utils/index.js @@ -3,9 +3,10 @@ function countWords( content ) { // https://github.com/tinymce/tinymce/blob/4.2.6/js/tinymce/plugins/wordcount/plugin.js if ( content && typeof content === 'string' ) { - content = content.replace( /\.\.\./g, ' ' ); // convert ellipses to spaces - content = content.replace( /<.[^<>]*?>/g, ' ' ); // remove HTML tags - content = content.replace( / | /gi, ' ' ); // remove space chars + // convert ellipses to spaces, remove HTML tags, and remove space chars + content = content.replace( /\.\.\./g, ' ' ); + content = content.replace( /<.[^<>]*?>/g, ' ' ); + content = content.replace( / | /gi, ' ' ); // deal with HTML entities content = content.replace( /(\w+)(&#?[a-z0-9]+;)+(\w+)/i, '$1$3' ); // strip entities inside words From 85b4fafde0e6c60f0d2a05a29135b31800ddbd45 Mon Sep 17 00:00:00 2001 From: MichaelArestad Date: Fri, 20 Nov 2015 14:10:27 -0700 Subject: [PATCH 3/3] Editor - moved word count to be static and full-width --- client/post-editor/style.scss | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/client/post-editor/style.scss b/client/post-editor/style.scss index 1335cabbfe1c..d2aafd158147 100644 --- a/client/post-editor/style.scss +++ b/client/post-editor/style.scss @@ -1,6 +1,7 @@ .is-section-post .wp-primary { // We don't need the space for a sidebar margin-left: 0; + padding-bottom: 0; // Lets go full width on smaller screens @include breakpoint( "<660px" ) { @@ -334,27 +335,14 @@ } .post-editor__word-count-wrapper { - position: fixed; - bottom: 0; - left: auto; - right: auto; - - box-sizing: border-box; - width: 100%; - + @include clear-fix; + border-top: 1px solid lighten( $gray, 30% ); + background-color: $white; font-size: 11px; line-height: 18px; - @include breakpoint( ">660px" ) { - width: 683px; // TODO ??? - } - & .editor-word-count { float: right; - border-top: 1px solid lighten( $gray, 20% ); - border-left: 1px solid lighten( $gray, 20% ); - border-radius: 4px 0; - background-color: rgba( $white, 0.92 ); color: $gray; text-transform: uppercase; padding: 8px;