Skip to content

Commit

Permalink
Merge pull request #300 from Automattic/add/editor/word-count
Browse files Browse the repository at this point in the history
Editor: Add word count
  • Loading branch information
nylen committed Nov 20, 2015
2 parents d0af5b6 + 85b4faf commit e85dcd6
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions client/lib/posts/post-edit-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ function setRawContent( content ) {
if ( PostEditStore.isDirty() !== isDirty || PostEditStore.hasContent() !== hasContent ) {
PostEditStore.emit( 'change' );
}
PostEditStore.emit( 'rawContentChange' );
}
}

Expand Down
12 changes: 12 additions & 0 deletions client/lib/text-utils/Makefile
Original file line number Diff line number Diff line change
@@ -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
27 changes: 27 additions & 0 deletions client/lib/text-utils/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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' ) {
// convert ellipses to spaces, remove HTML tags, and remove space chars
content = content.replace( /\.\.\./g, ' ' );
content = content.replace( /<.[^<>]*?>/g, ' ' );
content = content.replace( /&nbsp;|&#160;/gi, ' ' );

// 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 };
64 changes: 64 additions & 0 deletions client/lib/text-utils/test/index.js
Original file line number Diff line number Diff line change
@@ -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(
'<p>My sentence is this.</p>'
) ).to.equal( 4 );
} );

it( 'should not count dashes', () => {
expect( textUtils.countWords(
'<p>Something -- ok</p>'
) ).to.equal( 2 );
} );

it( 'should not count asterisks or other non-word characters', () => {
expect( textUtils.countWords(
'<p>* something\n\u00b7 something else</p>'
) ).to.equal( 3 );
} );

it( 'should not count numbers', () => {
expect( textUtils.countWords(
'<p>Something 123 ok</p>'
) ).to.equal( 2 );
} );

it( 'should not count HTML entities', () => {
expect( textUtils.countWords(
'<p>It&rsquo;s my life &ndash; &#8211; &#x2013; don\'t you forget.</p>'
) ).to.equal( 6 );
} );

it( 'should count hyphenated words as one word', () => {
expect( textUtils.countWords(
'<p>Hello some-word here.</p>'
) ).to.equal( 3 );
} );

it( 'should count words between blocks as two words', () => {
expect( textUtils.countWords(
'<p>Hello</p><p>world</p>'
) ).to.equal( 2 );
} );
} );
} );
78 changes: 78 additions & 0 deletions client/post-editor/editor-word-count/index.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="editor-word-count">
{ this.translate( 'Word Count' ) }
<Count count={ this.getCount() } />
</div>
);
},

getCount() {
return textUtils.countWords( this.state.rawContent );
}
} );
4 changes: 4 additions & 0 deletions client/post-editor/post-editor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' ),
Expand Down Expand Up @@ -390,6 +391,9 @@ var PostEditor = React.createClass( {
onTextEditorChange={ this.onEditorContentChange }
onTogglePin={ this.onTogglePin } />
</div>
<div className="post-editor__word-count-wrapper">
<EditorWordCount />
</div>
{ this.iframePreviewEnabled() ?
<EditorPreview
showPreview={ this.state.showPreview }
Expand Down
21 changes: 21 additions & 0 deletions client/post-editor/style.scss
Original file line number Diff line number Diff line change
@@ -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" ) {
Expand Down Expand Up @@ -332,3 +333,23 @@
max-height: 400px;
}
}

.post-editor__word-count-wrapper {
@include clear-fix;
border-top: 1px solid lighten( $gray, 30% );
background-color: $white;
font-size: 11px;
line-height: 18px;

& .editor-word-count {
float: right;
color: $gray;
text-transform: uppercase;
padding: 8px;

& .count {
margin-left: 4px;
color: darken( $gray, 10% );
}
}
}

0 comments on commit e85dcd6

Please sign in to comment.