Index: src/wp-includes/class-wp-editor.php
===================================================================
--- src/wp-includes/class-wp-editor.php	(revision 32659)
+++ src/wp-includes/class-wp-editor.php	(working copy)
@@ -368,6 +368,7 @@
 						'wplink',
 						'wpdialogs',
 						'wpview',
+						'wptextpattern'
 					);
 
 					if ( ! self::$has_medialib ) {
Index: src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
===================================================================
--- src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(revision 0)
+++ src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(working copy)
@@ -0,0 +1,107 @@
+( function( tinymce, setTimeout ) {
+	tinymce.PluginManager.add( 'wptextpattern', function( editor ) {
+		var $$ = editor.$,
+			patterns = [],
+			canUndo = false;
+
+		function add( regExp, callback ) {
+			patterns.push( {
+				regExp: regExp,
+				callback: callback
+			} );
+		}
+
+		add( /^[*-]\s/, function() {
+			this.execCommand( 'InsertUnorderedList' );
+		} );
+
+		add( /^1[.)]\s/, function() {
+			this.execCommand( 'InsertOrderedList' );
+		} );
+
+		editor.on( 'selectionchange', function() {
+			canUndo = false;
+		} );
+
+		editor.on( 'keydown', function( event ) {
+			if ( canUndo && event.keyCode === tinymce.util.VK.BACKSPACE ) {
+				editor.undoManager.undo();
+				event.preventDefault();
+			}
+		} );
+
+		editor.on( 'keyup', function( event ) {
+			var rng, node, parent, child, text,
+				replaced = false;
+
+			if ( event.keyCode !== tinymce.util.VK.SPACEBAR ) {
+				return;
+			}
+
+			rng = editor.selection.getRng();
+			node = rng.startContainer;
+
+			if ( ! node || node.nodeType !== 3 ) {
+				return;
+			}
+
+			parent = editor.dom.getParent( node, 'p' );
+
+			if ( ! parent ) {
+				return;
+			}
+
+			while ( child = parent.firstChild ) {
+				if ( child.nodeType !== 3 ) {
+					parent = child;
+				} else {
+					break;
+				}
+			}
+
+			if ( child !== node ) {
+				return;
+			}
+
+			text = node.nodeValue;
+
+			if ( text ) {
+				tinymce.each( patterns, function( pattern ) {
+					var replace = text.replace( pattern.regExp, '' );
+
+					if ( text === replace ) {
+						return;
+					}
+
+					if ( rng.startOffset !== text.length - replace.length ) {
+						return;
+					}
+
+					editor.undoManager.add();
+
+					editor.undoManager.transact( function() {
+						editor.selection.setCursorLocation( node, 0 );
+
+						if ( replace ) {
+							$$( node ).replaceWith( document.createTextNode( replace ) );
+						} else  {
+							$$( node.parentNode ).empty().append( '<br>' );
+						}
+
+						pattern.callback.apply( editor );
+					} );
+
+					replaced = true;
+					return false;
+				} );
+			}
+
+			// We need to wait for native events to be triggered.
+			if ( replaced ) {
+				setTimeout( function() {
+					canUndo = true;
+				}, 0 );
+			}
+		} );
+	} );
+} )( window.tinymce, window.setTimeout );

Property changes on: src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: src/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
===================================================================

--- tests/qunit/editor/js/utils.js	(revision 32659)
+++ tests/qunit/editor/js/utils.js	(working copy)
@@ -131,8 +131,19 @@
 
 	// TODO: Replace this with the new event logic in 3.5
 	function type(chr) {
-		var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng;
+		var editor = tinymce.activeEditor, keyCode, charCode, evt, startElm, rng, textNode;
 
+		function charCodeToKeyCode(charCode) {
+			var lookup = {
+				'0': 48, '1': 49, '2': 50, '3': 51, '4': 52, '5': 53, '6': 54, '7': 55, '8': 56, '9': 57,'a': 65, 'b': 66, 'c': 67,
+				'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81,
+				'r': 82, 's': 83, 't': 84, 'u': 85,	'v': 86, 'w': 87, 'x': 88, 'y': 89, ' ': 32, ',': 188, '-': 189, '.': 190, '/': 191, '\\': 220,
+				'[': 219, ']': 221, '\'': 222, ';': 186, '=': 187, ')': 41
+			};
+
+			return lookup[String.fromCharCode(charCode)];
+		}
+
 		function fakeEvent(target, type, evt) {
 			editor.dom.fire(target, type, evt);
 		}
@@ -139,7 +150,8 @@
 
 		// Numeric keyCode
 		if (typeof(chr) == "number") {
-			charCode = keyCode = chr;
+			charCode = chr;
+			keyCode = charCodeToKeyCode(charCode);
 		} else if (typeof(chr) == "string") {
 			// String value
 			if (chr == '\b') {
@@ -150,10 +162,18 @@
 				charCode = chr.charCodeAt(0);
 			} else {
 				charCode = chr.charCodeAt(0);
-				keyCode = charCode;
+				keyCode = charCodeToKeyCode(charCode);
 			}
 		} else {
 			evt = chr;
+
+			if (evt.charCode) {
+				chr = String.fromCharCode(evt.charCode);
+			}
+
+			if (evt.keyCode) {
+				keyCode = evt.keyCode;
+			}
 		}
 
 		evt = evt || {keyCode: keyCode, charCode: charCode};
@@ -196,11 +216,14 @@
 				if (rng.startContainer.nodeType == 3 && rng.collapsed) {
 					rng.startContainer.insertData(rng.startOffset, chr);
 					rng.setStart(rng.startContainer, rng.startOffset + 1);
-					rng.collapse(true);
-					editor.selection.setRng(rng);
 				} else {
-					rng.insertNode(editor.getDoc().createTextNode(chr));
+					textNode = editor.getDoc().createTextNode(chr);
+					rng.insertNode(textNode);
+					rng.setStart(textNode, 1);
 				}
+
+				rng.collapse(true);
+				editor.selection.setRng(rng);
 			}
 		}
 
Index: tests/qunit/index.html
===================================================================
--- tests/qunit/index.html	(revision 32659)
+++ tests/qunit/index.html	(working copy)
@@ -108,5 +108,10 @@
 				<# } #>
 			</li>
 		</script>
+		
+		<!-- TinyMCE -->
+		<script src="../../src/wp-includes/js/tinymce/tinymce.js"></script>
+		<script src="editor/js/utils.js"></script>
+		<script src="wp-includes/js/tinymce/plugins/wptextpattern/plugin.js"></script>
 	</body>
 </html>
Index: tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
===================================================================
--- tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(revision 0)
+++ tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js	(working copy)
@@ -0,0 +1,103 @@
+( function( $, module, test, _type, setTimeout ) {
+
+	var editor;
+
+	function type( chr ) {
+		typeof chr === 'string' ? tinymce.each( chr.split( '' ), _type ) : _type( chr );
+	}
+
+	module( 'tinymce.plugins.wptextpattern', {
+		beforeEach: function() {
+			stop();
+
+			$( '#qunit-fixture' ).append( '<textarea id="editor">' );
+
+			tinymce.init( {
+				selector: '#editor',
+				plugins: 'wptextpattern',
+				init_instance_callback: function() {
+					editor = arguments[0];
+					editor.focus();
+					start();
+				}
+			} );
+		},
+		afterEach: function( assert ) {
+			editor.remove();
+		}
+	} );
+
+	test( 'Unordered list.', function( assert ) {
+		type( '* test' );
+		assert.equal( editor.getContent(), '<ul>\n<li>test</li>\n</ul>' );
+	} );
+
+	test( 'Ordered list.', function( assert ) {
+		type( '1. test' );
+		assert.equal( editor.getContent(), '<ol>\n<li>test</li>\n</ol>' );
+	} );
+
+	test( 'Ordered list with content.', function( assert ) {
+		editor.setContent( '<p><strong>test</strong></p>' );
+		editor.selection.setCursorLocation();
+		type( '* ' );
+		assert.equal( editor.getContent(), '<ul>\n<li><strong>test</strong></li>\n</ul>' );
+	} );
+
+	test( 'Only transform inside a P tag.', function( assert ) {
+		editor.setContent( '<h1>test</h1>' );
+		editor.selection.setCursorLocation();
+		type( '* ' );
+		assert.equal( editor.getContent(), '<h1>* test</h1>' );
+	} );
+
+	test( 'Only transform at the start of a P tag.', function( assert ) {
+		editor.setContent( '<p>test <strong>test</strong></p>' );
+		editor.selection.setCursorLocation( editor.$( 'strong' )[0].firstChild, 0 );
+		type( '* ' );
+		assert.equal( editor.getContent(), '<p>test <strong>* test</strong></p>' );
+	} );
+
+	test( 'Only transform when at the cursor is at the start.', function( assert ) {
+		editor.setContent( '<p>* test</p>' );
+		editor.selection.setCursorLocation( editor.$( 'p' )[0].firstChild, 6 );
+		type( ' test' );
+		assert.equal( editor.getContent(), '<p>* test test</p>' );
+	} );
+
+	test( 'Backspace should undo the transformation.', function( assert ) {
+		var done = assert.async();
+
+		editor.setContent( '<p>test</p>' );
+		editor.selection.setCursorLocation();
+		type( '* ' );
+
+		setTimeout( function() {
+			type( '\b' );
+			assert.equal( editor.getContent(), '<p>* test</p>' );
+			assert.equal( editor.selection.getRng().startOffset, 2 );
+			done();
+		}, 0 );
+	} );
+
+	test( 'Backspace should undo the transformation only right after it happened.', function( assert ) {
+		var done = assert.async();
+
+		editor.setContent( '<p>test</p>' );
+		editor.selection.setCursorLocation();
+		type( '* ' );
+
+		// Wait until we can undo.
+		setTimeout( function() {
+			editor.selection.setCursorLocation( editor.$( 'li' )[0].firstChild, 4 );
+
+			// Wait for the `selectionchange` event.
+			setTimeout( function() {
+				type( '\b' );
+				assert.equal( editor.getContent(), '<ul>\n<li>tes</li>\n</ul>' );
+				done();
+			}, 0 );
+		}, 0 );
+	} );
+
+} )( window.jQuery, window.QUnit.module, window.QUnit.test, window.Utils.type, window.setTimeout );

Property changes on: tests/qunit/wp-includes/js/tinymce/plugins/wptextpattern/plugin.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
