Index: Gruntfile.js
===================================================================
--- Gruntfile.js	(revision 32833)
+++ Gruntfile.js	(working copy)
@@ -394,6 +394,9 @@
 			}
 		},
 		uglify: {
+			options: {
+				ASCIIOnly: true
+			},
 			core: {
 				expand: true,
 				cwd: SOURCE_DIR,
Index: src/wp-admin/js/post.js
===================================================================
--- src/wp-admin/js/post.js	(revision 32833)
+++ src/wp-admin/js/post.js	(working copy)
@@ -203,7 +203,6 @@
 jQuery(document).ready( function($) {
 	var stamp, visibility, $submitButtons, updateVisibility, updateText,
 		sticky = '',
-		last = 0,
 		$textarea = $('#content'),
 		$document = $(document),
 		$editSlugWrap = $('#edit-slug-box'),
@@ -788,24 +787,6 @@
 		});
 	}
 
-	// word count
-	if ( typeof(wpWordCount) != 'undefined' ) {
-		$document.triggerHandler('wpcountwords', [ $textarea.val() ]);
-
-		$textarea.keyup( function(e) {
-			var k = e.keyCode || e.charCode;
-
-			if ( k == last )
-				return true;
-
-			if ( 13 == k || 8 == last || 46 == last )
-				$document.triggerHandler('wpcountwords', [ $textarea.val() ]);
-
-			last = k;
-			return true;
-		});
-	}
-
 	wptitlehint = function(id) {
 		id = id || 'title';
 
@@ -935,3 +916,44 @@
 		}
 	});
 });
+
+( function( $, counter ) {
+	$( function() {
+		var $content = $( '#content' ),
+			$count = $( '#wp-word-count' ).find( '.word-count' ),
+			prevCount = 0,
+			contentEditor;
+
+		function update() {
+			var text, count;
+
+			if ( ! contentEditor || contentEditor.isHidden() ) {
+				text = $content.val();
+			} else {
+				text = contentEditor.getContent( { format: 'raw' } );
+			}
+
+			count = counter.count( text );
+
+			if ( count !== prevCount ) {
+				$count.text( count );
+			}
+
+			prevCount = count;
+		}
+
+		$( document ).on( 'tinymce-editor-init', function( event, editor ) {
+			if ( editor.id !== 'content' ) {
+				return;
+			}
+
+			contentEditor = editor;
+
+			editor.on( 'nodechange keyup', _.debounce( update, 500 ) );
+		} );
+
+		$content.on( 'input keyup', _.debounce( update, 500 ) );
+
+		update();
+	} );
+} )( jQuery, new wp.utils.WordCounter() );
Index: src/wp-admin/js/word-count.js
===================================================================
--- src/wp-admin/js/word-count.js	(revision 32833)
+++ src/wp-admin/js/word-count.js	(working copy)
@@ -1,44 +1,77 @@
-/* global wordCountL10n */
-var wpWordCount;
-(function($,undefined) {
-	wpWordCount = {
-
-		settings : {
-			strip : /<[a-zA-Z\/][^<>]*>/g, // strip HTML tags
-			clean : /[0-9.(),;:!?%#$¿'"_+=\\/-]+/g, // regexp to remove punctuation, etc.
-			w : /\S\s+/g, // word-counting regexp
-			c : /\S/g // char-counting regexp for asian languages
-		},
-
-		block : 0,
-
-		wc : function(tx, type) {
-			var t = this, w = $('.word-count'), tc = 0;
-
-			if ( type === undefined )
-				type = wordCountL10n.type;
-			if ( type !== 'w' && type !== 'c' )
-				type = 'w';
-
-			if ( t.block )
-				return;
-
-			t.block = 1;
-
-			setTimeout( function() {
-				if ( tx ) {
-					tx = tx.replace( t.settings.strip, ' ' ).replace( /&nbsp;|&#160;/gi, ' ' );
-					tx = tx.replace( t.settings.clean, '' );
-					tx.replace( t.settings[type], function(){tc++;} );
+( function() {
+	function WordCounter( settings ) {
+		var key;
+
+		if ( settings ) {
+			for ( key in settings ) {
+				if ( settings.hasOwnProperty( key ) ) {
+					this.settings[ key ] = settings[ key ];
 				}
-				w.html(tc.toString());
+			}
+		}
 
-				setTimeout( function() { t.block = 0; }, 2000 );
-			}, 1 );
+		if ( this.settings.l10n.shortcodes ) {
+			this.settings.shortcodeRegExp = new RegExp( '\\[\\/?(?:' + this.settings.l10n.shortcodes.join( '|' ) + ')[^\\]]*?\\]', 'gi' );
 		}
+
+		this.settings.excludeHTMLRegExp = new RegExp( '<(' + this.settings.excludeHTML.join( '|' ) + ')[^>]*?>[\\s\\S]*?<\\/\\1>', 'gi' );
+		this.settings.contractRegExp = new RegExp( '[' + this.settings.contract + ']', 'g' );
+		this.settings.expandRegExp = new RegExp( '[' + this.settings.expand + ']', 'g' );
+	}
+
+	WordCounter.prototype.settings = {
+		excludeHTML: [ 'code', 'form', 'noscript', 'script' ],
+		HTMLRegExp: /<\/?[a-z][^>]*?>/gi,
+		spaceRegExp: /&nbsp;|&#160;/gi,
+		HTMLEntitiesRegExp: /&#?[a-z0-9]+?;/gi,
+		wordsRegExp: /\S\s+/g,
+		charactersRegExp: /\S/g,
+		// Just the apostrophe and hyphen.
+		contract: '\'\u2019\\-\u2010\u2011',
+		expand: [
+			// Extract form "Basic Latin".
+			'\u0021-\u0040\u005B-\u0060\u007B-\u007E',
+			// Extract from "Latin-1 Supplement".
+			'\u00A1-\u00BF\u00D7\u00F7',
+			// "Combining Diacritical Marks".
+			'\u0300-\u036F',
+			// Punctuation, symbols, operators...
+			'\u2000-\u2BFF',
+			// "Supplemental Punctuation".
+			'\u2E00-\u2E7F'
+		].join( '' ),
+		l10n: window.wordCountL10n || {}
+	};
+
+	WordCounter.prototype.count = function( text, type ) {
+		var count = 0;
+
+		type = type || this.settings.l10n.type || 'words';
+
+		if ( text ) {
+			text = ' ' + text + ' ';
+
+			text = text.replace( this.settings.excludeHTMLRegExp, ' ' );
+			text = text.replace( this.settings.HTMLRegExp, ' ' );
+			text = text.replace( this.settings.shortcodeRegExp, ' ' );
+			text = text.replace( this.settings.spaceRegExp, ' ' );
+			text = text.replace( this.settings.HTMLEntitiesRegExp, '' );
+			// en/em dash shorthand is an exception
+			text = text.replace( /--/g, ' ' );
+			text = text.replace( this.settings.contractRegExp, '' );
+			text = text.replace( this.settings.expandRegExp, ' ' );
+
+			text = text.match( this.settings[ type + 'RegExp' ] );
+
+			if ( text ) {
+				count = text.length;
+			}
+		}
+
+		return count;
 	};
 
-	$(document).bind( 'wpcountwords', function(e, txt) {
-		wpWordCount.wc(txt);
-	});
-}(jQuery));
+	window.wp = window.wp || {};
+	window.wp.utils = window.wp.utils || {};
+	window.wp.utils.WordCounter = WordCounter;
+} )();
Index: src/wp-includes/js/tinymce/plugins/wordpress/plugin.js
===================================================================
--- src/wp-includes/js/tinymce/plugins/wordpress/plugin.js	(revision 32833)
+++ src/wp-includes/js/tinymce/plugins/wordpress/plugin.js	(working copy)
@@ -7,8 +7,7 @@
 	var DOM = tinymce.DOM,
 		each = tinymce.each,
 		__ = editor.editorManager.i18n.translate,
-		wpAdvButton, style,
-		last = 0;
+		wpAdvButton, style;
 
 	if ( typeof window.jQuery !== 'undefined' ) {
 		window.jQuery( document ).triggerHandler( 'tinymce-editor-setup', [ editor ] );
@@ -363,23 +362,6 @@
 		}
 	});
 
-	// Word count
-	if ( typeof window.jQuery !== 'undefined' ) {
-		editor.on( 'keyup', function( e ) {
-			var key = e.keyCode || e.charCode;
-
-			if ( key === last ) {
-				return;
-			}
-
-			if ( 13 === key || 8 === last || 46 === last ) {
-				window.jQuery( document ).triggerHandler( 'wpcountwords', [ editor.getContent({ format : 'raw' }) ] );
-			}
-
-			last = key;
-		});
-	}
-
 	editor.on( 'SaveContent', function( e ) {
 		// If editor is hidden, we just want the textarea's value to be saved
 		if ( ! editor.inline && editor.isHidden() ) {
Index: src/wp-includes/script-loader.php
===================================================================
--- src/wp-includes/script-loader.php	(revision 32833)
+++ src/wp-includes/script-loader.php	(working copy)
@@ -371,11 +371,12 @@
 
 	$scripts->add( 'wpdialogs', "/wp-includes/js/wpdialog$suffix.js", array( 'jquery-ui-dialog' ), false, 1 );
 
-	$scripts->add( 'word-count', "/wp-admin/js/word-count$suffix.js", array( 'jquery' ), false, 1 );
+	$scripts->add( 'word-count', "/wp-admin/js/word-count$suffix.js", array(), false, 1 );
 	did_action( 'init' ) && $scripts->localize( 'word-count', 'wordCountL10n', array(
 		/* translators: If your word count is based on single characters (East Asian characters),
 		   enter 'characters'. Otherwise, enter 'words'. Do not translate into your own language. */
-		'type' => 'characters' == _x( 'words', 'word count: words or characters?' ) ? 'c' : 'w',
+		'type' => _x( 'words', 'word count: words or characters?' ),
+		'shortcodes' => array_keys( $GLOBALS['shortcode_tags'] )
 	) );
 
 	$scripts->add( 'media-upload', "/wp-admin/js/media-upload$suffix.js", array( 'thickbox', 'shortcode' ), false, 1 );
@@ -451,7 +452,7 @@
 			'tagDelimiter' => _x( ',', 'tag delimiter' ),
 		) );
 
-		$scripts->add( 'post', "/wp-admin/js/post$suffix.js", array( 'suggest', 'wp-lists', 'postbox', 'tags-box' ), false, 1 );
+		$scripts->add( 'post', "/wp-admin/js/post$suffix.js", array( 'suggest', 'wp-lists', 'postbox', 'tags-box', 'underscore', 'word-count' ), false, 1 );
 		did_action( 'init' ) && $scripts->localize( 'post', 'postL10n', array(
 			'ok' => __('OK'),
 			'cancel' => __('Cancel'),
Index: tests/qunit/index.html
===================================================================
--- tests/qunit/index.html	(revision 32833)
+++ tests/qunit/index.html	(working copy)
@@ -32,6 +32,7 @@
 		<script src="../../src/wp-includes/js/customize-models.js"></script>
 		<script src="../../src/wp-includes/js/shortcode.js"></script>
 		<script src="../../src/wp-admin/js/customize-controls.js"></script>
+		<script src="../../src/wp-admin/js/word-count.js"></script>
 
 		<!-- Unit tests -->
 		<script src="wp-admin/js/password-strength-meter.js"></script>
@@ -40,6 +41,7 @@
 		<script src="wp-includes/js/shortcode.js"></script>
 		<script src="wp-admin/js/customize-controls.js"></script>
 		<script src="wp-admin/js/customize-controls-utils.js"></script>
+		<script src="wp-admin/js/word-count.js"></script>
 
 		<!-- Customizer templates for sections -->
 		<script type="text/html" id="tmpl-customize-section-default">
Index: tests/qunit/wp-admin/js/word-count.js
===================================================================
--- tests/qunit/wp-admin/js/word-count.js	(revision 0)
+++ tests/qunit/wp-admin/js/word-count.js	(working copy)
@@ -0,0 +1,81 @@
+( function( QUnit ) {
+	var wordCounter = new window.wp.utils.WordCounter( {
+		l10n: {
+			shortcodes: [ 'shortcode' ]
+		}
+	} );
+
+	QUnit.module( 'word-count' );
+
+	QUnit.test( 'All.', function( assert ) {
+		var tests = [
+			{
+				string: 'one two three',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one <I> two </I> three',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: '<p class="class"> one two three </p>',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one\ntwo\nthree',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one&nbsp;two&#160;three',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one two three ... 4 ?',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: '\u03BC\u1FC6\u03BD\u03B9\u03BD \u1F04\u03B5\u03B9\u03B4\u03B5 \u03B8\u03B5\u1F70',
+				wordCount: 3,
+				charCount: 13
+			},
+			{
+				string: 'one two--three!',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one two\u2013three!',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one [shortcode attr="value"] two [/shortcode] three',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'It\'s two three',
+				wordCount: 3,
+				charCount: 11
+			},
+			{
+				string: 'one two three<script>function script() {}</script>',
+				wordCount: 3,
+				charCount: 11
+			}
+		];
+
+		var i = tests.length;
+
+		while ( i-- ) {
+			assert.equal( wordCounter.count( tests[ i ].string ), tests[ i ].wordCount );
+			assert.equal( wordCounter.count( tests[ i ].string, 'characters' ), tests[ i ].charCount );
+		}
+	} );
+} )( window.QUnit );
