diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..ea6ea66
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,6 @@
+lib/
+dist/
+docs/
+examples/
+node_modules/
+website/
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 0000000..6153f6c
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,15 @@
+
+module.exports = {
+  parser: 'babel-eslint',
+
+  extends: './node_modules/fbjs-scripts/eslint/.eslintrc.js',
+
+  plugins: [
+    'react',
+  ],
+
+  rules: {
+    'react/jsx-uses-react': 1,
+    'react/react-in-jsx-scope': 1,
+  }
+};
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cbc59d6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+/dist/
+/lib/
+/node_modules/
+/examples/tex/node_modules/
+/website/build/
+/website/node_modules/
+/website/src/draft-js/lib/
+npm-debug.log
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..e1b67c0
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,14 @@
+language: node_js
+node_js:
+- 5
+sudo: false
+cache:
+  directories:
+  - node_modules
+before_install:
+- |
+  npm install -g npm@latest-2
+  npm --version
+script:
+- |
+  npm test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..b66e2a9
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,40 @@
+# Contributing to Draft.js
+We want to make contributing to this project as easy and transparent as
+possible.
+
+## Our Development Process
+We use GitHub to sync code to and from our internal repository. We'll use GitHub
+to track issues and feature requests, as well as accept pull requests.
+
+## Pull Requests
+We actively welcome your pull requests.
+
+1. Fork the repo and create your branch from `master`.
+2. If you've added code that should be tested, add tests.
+3. If you've changed APIs, update the documentation.
+4. Ensure the test suite passes.
+5. Make sure your code lints.
+6. If you haven't already, complete the Contributor License Agreement ("CLA").
+
+## Contributor License Agreement ("CLA")
+In order to accept your pull request, we need you to submit a CLA. You only need
+to do this once to work on any of Facebook's open source projects.
+
+Complete your CLA here: <https://code.facebook.com/cla>
+
+## Issues
+We use GitHub issues to track public bugs. Please ensure your description is
+clear and has sufficient instructions to be able to reproduce the issue.
+
+Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe
+disclosure of security bugs. In those cases, please go through the process
+outlined on that page and do not file a public issue.
+
+## Coding Style  
+* 2 spaces for indentation rather than tabs
+* 80 character line length
+* Run `npm run lint` to conform to our lint rules
+
+## License
+By contributing to Draft.js, you agree that your contributions will be licensed
+under its BSD license.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ae052fb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,31 @@
+BSD License
+
+For Draft.js software
+
+Copyright (c) 2013-present, Facebook, Inc.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+ * Neither the name Facebook nor the names of its contributors may be used to
+   endorse or promote products derived from this software without specific
+   prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/LICENSE-examples b/LICENSE-examples
new file mode 100644
index 0000000..a9060ea
--- /dev/null
+++ b/LICENSE-examples
@@ -0,0 +1,11 @@
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+The examples provided by Facebook are for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/PATENTS b/PATENTS
new file mode 100644
index 0000000..42afbc8
--- /dev/null
+++ b/PATENTS
@@ -0,0 +1,33 @@
+Additional Grant of Patent Rights Version 2
+
+"Software" means the Draft.js software distributed by Facebook, Inc.
+
+Facebook, Inc. ("Facebook") hereby grants to each recipient of the Software
+("you") a perpetual, worldwide, royalty-free, non-exclusive, irrevocable
+(subject to the termination provision below) license under any Necessary
+Claims, to make, have made, use, sell, offer to sell, import, and otherwise
+transfer the Software. For avoidance of doubt, no license is granted under
+Facebook's rights in any patent claims that are infringed by (i) modifications
+to the Software made by you or any third party or (ii) the Software in
+combination with any software or other technology.
+
+The license granted hereunder will terminate, automatically and without notice,
+if you (or any of your subsidiaries, corporate affiliates or agents) initiate
+directly or indirectly, or take a direct financial interest in, any Patent
+Assertion: (i) against Facebook or any of its subsidiaries or corporate
+affiliates, (ii) against any party if such Patent Assertion arises in whole or
+in part from any software, technology, product or service of Facebook or any of
+its subsidiaries or corporate affiliates, or (iii) against any party relating
+to the Software. Notwithstanding the foregoing, if Facebook or any of its
+subsidiaries or corporate affiliates files a lawsuit alleging patent
+infringement against you in the first instance, and you respond by filing a
+patent infringement counterclaim in that lawsuit against that party that is
+unrelated to the Software, the license granted hereunder will not terminate
+under section (i) of this paragraph due to such counterclaim.
+
+A "Necessary Claim" is a claim of a patent owned by Facebook that is
+necessarily infringed by the Software standing alone.
+
+A "Patent Assertion" is any lawsuit or other action alleging direct, indirect,
+or contributory infringement or inducement to infringe any patent, including a
+cross-claim or counterclaim.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9e9d806
--- /dev/null
+++ b/README.md
@@ -0,0 +1,53 @@
+# [Draft.js](https://facebook.github.io/draft-js/)
+
+Draft.js is a JavaScript rich text editor framework, built for React and
+backed by an immutable model.
+
+- **Extensible and Customizable:** We provide the building blocks to enable
+the creation of a broad variety of rich text composition experiences, from
+simple text styles to embedded media.
+- **Declarative Rich Text:** Draft.js fits seamlessly into
+[React](http://facebook.github.io/react/) applications,
+abstracting away the details of rendering, selection, and input behavior with a
+familiar declarative API.
+- **Immutable Editor State:** The Draft.js model is built
+with [immutable-js](https://facebook.github.io/immutable-js/), offering
+an API with functional state updates and aggressively leveraging data persistence
+for scalable memory usage.
+
+[Learn how to use Draft.js in your own project.](https://facebook.github.io/draft-js/docs/quickstart-getting-started.html)
+
+## Examples
+
+Visit https://facebook.github.io/draft-js/ to try out a simple rich editor example.
+
+The repository includes a variety of different editor examples to demonstrate
+some of the features offered by the framework.
+
+To run the examples, first build Draft.js locally:
+
+```
+git clone https://github.com/facebook/draft-js.git
+cd draft-js
+npm install
+npm run build
+```
+
+then open the example HTML files in your browser.
+
+Draft.js is used in production on Facebook, including status and
+comment inputs, [Notes](https://www.facebook.com/notes/), and
+[messenger.com](https://www.messenger.com).
+
+## Contribute
+
+We actively welcome pull requests. Learn how to
+[contribute](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md).
+
+## License
+
+Draft.js is [BSD Licensed](https://github.com/facebook/draft-js/blob/master/LICENSE).
+We also provide an additional [patent grant](https://github.com/facebook/draft-js/blob/master/PATENTS).
+
+Examples provided in this repository and in the documentation are separately
+licensed.
diff --git a/docs/APIReference-CharacterMetadata.md b/docs/APIReference-CharacterMetadata.md
new file mode 100644
index 0000000..1049f53
--- /dev/null
+++ b/docs/APIReference-CharacterMetadata.md
@@ -0,0 +1,157 @@
+---
+id: api-reference-character-metadata
+title: CharacterMetadata
+layout: docs
+category: API Reference
+next: api-reference-entity
+permalink: docs/api-reference-character-metadata.html
+---
+
+`CharacterMetadata` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents inline style and entity information for a single character.
+
+`CharacterMetadata` objects are aggressively pooled and shared. If two characters
+have the same inline style and entity, they are represented with the same
+`CharacterMetadata` object. We therefore need only as many objects as combinations
+of utilized inline style sets with entity keys, keeping our memory footprint
+small even as the contents grow in size and complexity.
+
+To that end, you should create or apply changes to `CharacterMetadata` objects
+via the provided set of static methods, which will ensure that pooling is utilized.
+
+Most Draft use cases are unlikely to use these static methods, since most common edit
+operations are already implemented and available via utility modules. The getter
+methods, however, may come in handy at render time.
+
+See the API reference on
+[ContentBlock](/draft-js/docs/api-reference-content-block.html#representing-styles-and-entities)
+for information on how `CharacterMetadata` is used within `ContentBlock`.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#create">
+      <pre>static create(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applystyle">
+      <pre>static applyStyle(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removestyle">
+      <pre>static removeStyle(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyentity">
+      <pre>static applyEntity(...): CharacterMetadata</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getstyle">
+      <pre>getStyle(): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasstyle">
+      <pre>hasStyle(style: string): boolean</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getentity">
+      <pre>getEntity(): ?string</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+Under the hood, these methods will utilize pooling to return a matching object,
+or return a new object if none exists.
+
+### create
+
+```
+static create(config?: CharacterMetadataConfig): CharacterMetadata
+```
+Generates a `CharacterMetadata` object from the provided configuration. This
+function should be used in lieu of a constructor.
+
+The configuration will be used to check whether a pooled match for this
+configuration already exists. If so, the pooled object will be returned.
+Otherwise, a new `CharacterMetadata` will be pooled for this configuration,
+and returned.
+
+### applyStyle
+
+```
+static applyStyle(
+  record: CharacterMetadata,
+  style: string
+): CharacterMetadata
+```
+Apply an inline style to this `CharacterMetadata`.
+
+### removeStyle
+
+```
+static removeStyle(
+  record: CharacterMetadata,
+  style: string
+): CharacterMetadata
+```
+Remove an inline style from this `CharacterMetadata`.
+
+### applyEntity
+
+```
+static applyEntity(
+  record: CharacterMetadata,
+  entityKey: ?string
+): CharacterMetadata
+```
+
+Apply an entity key -- or provide `null` to remove an entity key -- on this
+`CharacterMetadata`.
+
+## Methods
+
+### getStyle
+
+```
+getStyle(): DraftInlineStyle
+```
+Returns the `DraftInlineStyle` for this character, an `OrderedSet` of strings
+that represents the inline style to apply for the character at render time.
+
+### hasStyle
+
+```
+hasStyle(style: string): boolean
+```
+Returns whether this character has the specified style.
+
+### getEntity
+
+```
+getEntity(): ?string
+```
+Returns the entity key (if any) for this character, as mapped to the global set of
+entities tracked by the [`Entity`](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntity.js)
+module.
+
+By tracking a string key here, we can keep the corresponding metadata separate
+from the character representation.
+
+If null, no entity is applied for this character.
diff --git a/docs/APIReference-ContentBlock.md b/docs/APIReference-ContentBlock.md
new file mode 100644
index 0000000..3a4aee4
--- /dev/null
+++ b/docs/APIReference-ContentBlock.md
@@ -0,0 +1,246 @@
+---
+id: api-reference-content-block
+title: ContentBlock
+layout: docs
+category: API Reference
+next: api-reference-character-metadata
+permalink: docs/api-reference-content-block.html
+---
+
+`ContentBlock` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of a single block of editor content, including:
+
+  - Plain text contents of the block
+  - Type, e.g. paragraph, header, list item
+  - Entity, inline style, and depth information
+
+A `ContentState` object contains an `OrderedMap` of these `ContentBlock` objects,
+which together comprise the full contents of the editor.
+
+`ContentBlock` objects are largely analogous to block-level HTML elements like
+paragraphs and list items.
+
+New `ContentBlock` objects may be created directly using the constructor.
+Expected Record values are detailed below.
+
+### Representing styles and entities
+
+The `characterList` field is an immutable `List` containing a `CharacterMetadata`
+object for every character in the block. This is how we encode styles and
+entities for a given block.
+
+By making heavy use of immutability and data persistence for these lists and
+`CharacterMetadata` objects, edits to the content generally have little impact
+on the memory footprint of the editor.
+
+By encoding inline styles and entities together in this way, a function that
+performs edits on a `ContentBlock` can perform slices, concats, and other List
+methods on a single `List` object.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getkey">
+      <pre>getKey(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettype">
+      <pre>getType(): DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettext">
+      <pre>getText(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcharacterlist">
+      <pre>getCharacterList(): List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlength">
+      <pre>getLength(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getdepth">
+      <pre>getDepth(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getinlinestyleat">
+      <pre>getInlineStyleAt(offset: number): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getentityat">
+      <pre>getEntityAt(offset: number): ?string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findstyleranges">
+      <pre>findStyleRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findentityranges">
+      <pre>findEntityRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the `ContentBlock` constructor or to set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#key">
+      <pre>key: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#type">
+      <pre>type: DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#text">
+      <pre>text: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#characterlist">
+      <pre>characterList: List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#depth">
+      <pre>depth: number</pre>
+    </a>
+  </li>
+</ul>
+
+## Methods
+
+### getKey()
+
+```
+getKey(): string
+```
+Returns the string key for this `ContentBlock`.
+
+### getType()
+
+```
+getType(): DraftBlockType
+```
+Returns the type for this `ContentBlock`. Type values are largely analogous to
+block-level HTML elements.
+
+### getText()
+
+```
+getText(): string
+```
+Returns the full plaintext for this `ContentBlock`. This value does not contain
+any styling, decoration, or HTML information.
+
+### getCharacterList()
+
+```
+getCharacterList(): List<CharacterMetadata>
+```
+Returns an immutable `List` of `CharacterMetadata` objects, one for each
+character in the `ContentBlock`. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+This `List` contains all styling and entity information for the block.
+
+### getLength()
+
+```
+getLength(): number
+```
+Returns the length of the plaintext for the `ContentBlock`.
+
+This value uses the standard JavaScript `length` property for the string, and
+is therefore not Unicode-aware -- surrogate pairs will be counted as two
+characters.
+
+### getDepth()
+
+```
+getDepth(): number
+```
+Returns the depth value for this block, if any. This is currently used only
+for list items.
+
+### getInlineStyleAt()
+
+```
+getInlineStyleAt(offset: number): DraftInlineStyle
+```
+Returns the `DraftInlineStyle` value (an `OrderedSet<string>`) at a given offset
+within this `ContentBlock`.
+
+### getEntityAt()
+
+```
+getEntityAt(offset: number): ?string
+```
+Returns the entity key value (or `null` if none) at a given offset within this
+`ContentBlock`.
+
+### findStyleRanges()
+
+```
+findStyleRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+```
+Executes a callback for each contiguous range of styles within this
+`ContentBlock`.
+
+### findEntityRanges()
+
+```
+findEntityRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+```
+Executes a callback for each contiguous range of entities within this
+`ContentBlock`.
+
+## Properties
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the `ContentBlock` constructor or to set properties.
+
+### key
+See `getKey()`.
+
+### text
+See `getText()`.
+
+### type
+See `getType()`.
+
+### characterList
+See `getCharacterList()`.
+
+### depth
+See `getDepth()`.
diff --git a/docs/APIReference-ContentState.md b/docs/APIReference-ContentState.md
new file mode 100644
index 0000000..43e62a8
--- /dev/null
+++ b/docs/APIReference-ContentState.md
@@ -0,0 +1,265 @@
+---
+id: api-reference-content-state
+title: ContentState
+layout: docs
+category: API Reference
+next: api-reference-content-block
+permalink: docs/api-reference-content-state.html
+---
+
+`ContentState` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the `ContentState` object is via `EditorState.getCurrentContent()`,
+which provides the `ContentState` currently being rendered in the editor.
+
+An `EditorState` object maintains undo and redo stacks comprised of `ContentState`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>static createFromText(text: string): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>static createFromBlockArray(blocks: Array<ContentBlock>): ContentState</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+```
+static createFromText(
+  text: string,
+  delimiter?: string
+): ContentState
+```
+Generates a `ContentState` from a string, with a delimiter to split the string
+into `ContentBlock` objects. If no delimiter is provided, '\n' is used.
+
+### createFromBlockArray
+
+```
+static createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+```
+Generates a `ContentState` from an array of `ContentBlock` objects. The default
+`selectionBefore` and `selectionAfter` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+```
+getBlockMap(): BlockMap
+```
+Returns the full ordered map of `ContentBlock` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific `ContentBlock` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+```
+getSelectionBefore(): SelectionState
+```
+Returns the `SelectionState` displayed in the editor before rendering `blockMap`.
+
+When performing an `undo` action in the editor, the `selectionBefore` of the current
+`ContentState` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+```
+getSelectionAfter(): SelectionState
+```
+Returns the `SelectionState` displayed in the editor after rendering `blockMap`.
+
+When performing any action in the editor that leads to this `blockMap` being rendered,
+the selection range will be placed in the `selectionAfter` position.
+
+### getBlockForKey
+
+```
+getBlockForKey(key: string): ContentBlock
+```
+Returns the `ContentBlock` corresponding to the given block key.
+
+#### Example
+
+```
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+```
+
+### getKeyBefore()
+
+```
+getKeyBefore(key: string): ?string
+```
+Returns the key before the specified key in `blockMap`, or null if this is the first key.
+
+### getKeyAfter()
+
+```
+getKeyAfter(key: string): ?string
+```
+Returns the key after the specified key in `blockMap`, or null if this is the last key.
+
+### getBlockBefore()
+
+```
+getBlockBefore(key: string): ?ContentBlock
+```
+Returns the `ContentBlock` before the specified key in `blockMap`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+```
+getBlockAfter(key: string): ?ContentBlock
+```
+Returns the `ContentBlock` after the specified key in `blockMap`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+```
+getBlocksAsArray(): Array<ContentBlock>
+```
+Returns the values of `blockMap` as an array.
+
+### getPlainText()
+
+```
+getPlainText(delimiter?: string): string
+```
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (`\u000A`) is used.
+
+### hasText()
+
+```
+hasText(): boolean
+```
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See `getBlockMap()`.
+
+### selectionBefore
+See `getSelectionBefore()`.
+
+### selectionAfter
+See `getSelectionAfter()`.
diff --git a/docs/APIReference-Data-Conversion.md b/docs/APIReference-Data-Conversion.md
new file mode 100644
index 0000000..004fd96
--- /dev/null
+++ b/docs/APIReference-Data-Conversion.md
@@ -0,0 +1,46 @@
+---
+id: api-reference-data-conversion
+title: Data Conversion
+layout: docs
+category: API Reference
+next: api-reference-modifier
+permalink: docs/api-reference-data-conversion.html
+---
+
+Because a text editor doesn't exist in a vacuum and it's important to save
+contents for storage or transmission, you will want to be able to
+convert a `ContentState` into raw JS, and vice versa.
+
+To that end, we provide a couple of utility functions that allow you to perform
+these conversions.
+
+Note that the Draft library does not currently provide utilities to convert to
+and from markdown or markup, since different clients may have different requirements
+for these formats. We instead provide JavaScript objects that can be converted
+to other formats as needed.
+
+The Flow type [`RawDraftContentState`](https://github.com/facebook/draft-js/blob/master/src/model/encoding/RawDraftContentState.js)
+denotes the expected structure of the raw format of the contents. The raw state
+contains a list of content blocks, as well as a map of all relevant entity
+objects.
+
+## Functions
+
+### convertFromRaw
+
+```
+convertFromRaw(rawState: RawDraftContentState): ContentState
+```
+
+Given a raw state, convert it to  `ContentState` object. This is useful when
+restoring contents to use within a Draft editor.
+
+### convertToRaw
+
+```
+convertToRaw(contentState: ContentState): RawDraftContentState
+```
+
+Given a `ContentState` object, convert it to a raw JS structure. This is useful
+when saving an editor state for storage, conversion to other formats, or
+other usage within an application.
diff --git a/docs/APIReference-Editor.md b/docs/APIReference-Editor.md
new file mode 100644
index 0000000..999844c
--- /dev/null
+++ b/docs/APIReference-Editor.md
@@ -0,0 +1,193 @@
+---
+id: api-reference-editor
+title: Editor Component
+layout: docs
+category: API Reference
+next: api-reference-editor-state
+permalink: docs/api-reference-editor.html
+---
+
+This article discusses the API and props of the core controlled contentEditable
+component itself, `Editor`. Props are defined within
+[`DraftEditorProps`](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js).
+
+## Props
+
+### Basics
+
+See [API Basics](/draft-js/docs/quickstart-api-basics.html) for an introduction.
+
+#### editorState
+```
+editorState: EditorState
+```
+The `EditorState` object to be rendered by the `Editor`.
+
+#### onChange
+```
+onChange: (editorState: EditorState) => void
+```
+The `onChange` function to be executed by the `Editor` when edits and selection
+changes occur.
+
+### Presentation (Optional)
+
+#### placeholder
+```
+placeholder?: string
+```
+Optional placeholder string to display when the editor is empty.
+
+Note: You can use CSS to style or hide your placeholder as needed. For instance,
+in the [rich editor example](https://github.com/facebook/draft-js/tree/master/examples/rich),
+the placeholder is hidden when the user changes block styling in an empty editor.
+This is because the placeholder may not line up with the cursor when the style
+is changed.
+
+#### textAlignment
+```
+textAlignment?: DraftTextAlignment
+```
+Optionally set the overriding text alignment for this editor. This alignment value will
+apply to the entire contents, regardless of default text direction for input text.
+
+You may use this if you wish to center your text or align it flush in one direction
+to fit it within your UI design.
+
+If this value is not set, text alignment will be based on the characters within
+the editor, on a per-block basis.
+
+#### blockRendererFn
+```
+blockRendererFn?: (block: ContentBlock) => ?Object
+```
+Optionally set a function to define custom block rendering. See
+[Advanced Topics: Block Components](/draft-js/docs/advanced-topics-block-components.html)
+for details on usage.
+
+#### blockStyleFn
+```
+blockStyleFn?: (block: ContentBlock) => string
+```
+Optionally set a function to define class names to apply to the given block
+when it is rendered. See
+[Advanced Topics: Block Styling](/draft-js/docs/advanced-topics-block-styling.html)
+for details on usage.
+
+#### customStyleMap
+```
+customStyleMap?: Object
+```
+Optionally define a map of inline styles to apply to spans of text with the specified
+style. See
+[Advanced Topics: Inline Styles](/draft-js/docs/advanced-topics-inline-styles.html)
+for details on usage.
+
+### Behavior (Optional)
+
+#### readOnly
+```
+readOnly?: boolean
+```
+Set whether the editor should be rendered as static DOM, with all editability
+disabled.
+
+This is useful when supporting interaction within
+[custom block components](/draft-js/docs/advanced-topics-block-components.html)
+or if you just want to display content for a static use case.
+
+Default is `false`.
+
+#### spellCheck
+```
+spellCheck?: boolean
+```
+Set whether spellcheck is turned on for your editor.
+
+Note that in OSX Safari, enabling spellcheck also enables autocorrect, if the user
+has it turned on. Also note that spellcheck is always disabled in IE, since the events
+needed to observe spellcheck events are not fired in IE.
+
+Default is `false`.
+
+#### stripPastedStyles
+```
+stripPastedStyles?: boolean
+```
+Set whether to remove all information except plaintext from pasted content.
+
+This should be used if your editor does not support rich styles.
+
+Default is `false`.
+
+### DOM and Accessibility (Optional)
+
+#### tabIndex
+#### ARIA props
+
+These props allow you to set accessibility properties on your editor. See
+[DraftEditorProps](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js) for the exhaustive list of supported attributes.
+
+### Cancelable Handlers (Optional)
+
+These prop functions are provided to allow custom event handling for a small
+set of useful events. By returning true from your handler, you indicate that
+the event is handled and the Draft core should do nothing more with it. By returning
+false, you defer to Draft to handle the event.
+
+#### handleReturn
+```
+handleReturn?: (e: SyntheticKeyboardEvent) => boolean
+```
+Handle a `RETURN` keydown event. Example usage: Choosing a mention tag from a
+rendered list of results to trigger applying the mention entity to your content.
+
+#### handleKeyCommand
+```
+handleKeyCommand?: (command: string) => boolean
+```
+Handle the named editor command. See
+[Advanced Topics: Key Bindings](/draft-js/docs/advanced-topics-key-bindings.html)
+for details on usage.
+
+#### handleBeforeInput
+```
+handleBeforeInput?: (e: SyntheticInputEvent) => boolean
+```
+Handle a `beforeInput` event before character insertion occurs within the editor.
+Example usage: After a user has typed `- ` at the start of a new block, you might
+convert that `ContentBlock` into an `unordered-list-item`.
+
+At Facebook, we also use this to convert typed ASCII quotes into "smart" quotes,
+and to convert typed emoticons into images.
+
+#### handlePastedFiles
+```
+handlePastedFiles?: (files: Array<Blob>) => boolean
+```
+Handle files that have been pasted directly into the editor.
+
+### Key Handlers (Optional)
+
+These prop functions expose common useful key events. Example: at Facebook, these are
+used to provide keyboard interaction for mention results in inputs.
+
+#### onEscape
+```
+onEscape?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onTab
+```
+onTab?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onUpArrow
+```
+onUpArrow?: (e: SyntheticKeyboardEvent) => void
+```
+
+#### onDownArrow
+```
+onDownArrow?: (e: SyntheticKeyboardEvent) => void
+```
diff --git a/docs/APIReference-EditorState.md b/docs/APIReference-EditorState.md
new file mode 100644
index 0000000..9901a79
--- /dev/null
+++ b/docs/APIReference-EditorState.md
@@ -0,0 +1,481 @@
+---
+id: api-reference-editor-state
+title: EditorState
+layout: docs
+category: API Reference
+next: api-reference-content-state
+permalink: docs/api-reference-editor-state.html
+---
+
+`EditorState` is the top-level state object for the editor.
+
+It is an Immutable [Record](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+that represents the entire state of a Draft editor, including:
+
+  - The current text content state
+  - The current selection state
+  - The fully decorated representation of the contents
+  - Undo/redo stacks
+  - The most recent type of change made to the contents
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods below.
+
+## Overview
+
+*Common instance methods*
+
+The list below includes the most commonly used instance methods for `EditorState` objects.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getcurrentcontent">
+      <pre>getCurrentContent(): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselection">
+      <pre>getSelection(): SelectionState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcurrentinlinestyle">
+      <pre>getCurrentInlineStyle(): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocktree">
+      <pre>getBlockTree(): OrderedMap</pre>
+    </a>
+  </li>
+</ul>
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createwithcontent">
+      <pre>static createWithContent(contentState, ?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#create">
+      <pre>static create(config): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#push">
+      <pre>static push(editorState, contentState, changeType): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undo">
+      <pre>static undo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redo">
+      <pre>static redo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#acceptselection">
+      <pre>static acceptSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>static forceSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use the static `EditorState` methods to set properties, rather than using
+> the Immutable API directly.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#allowundo">
+      <pre>allowUndo</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#currentcontent">
+      <pre>currentContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#decorator">
+      <pre>decorator</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#directionMap">
+      <pre>directionMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>forceSelection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#incompositionmode">
+      <pre>inCompositionMode</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#inlinestyleoverride">
+      <pre>inlineStyleOverride</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#lastchangetype">
+      <pre>lastChangeType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#nativelyrenderedcontent">
+      <pre>nativelyRenderedContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redostack">
+      <pre>redoStack</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selection">
+      <pre>selection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#treemap">
+      <pre>treeMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undostack">
+      <pre>undoStack</pre>
+    </a>
+  </li>
+</ul>
+
+## Common Instance Methods
+
+### getCurrentContent
+
+```
+getCurrentContent(): ContentState
+```
+Returns the current contents of the editor.
+
+### getSelection
+
+```
+getSelection(): SelectionState
+```
+Returns the current cursor/selection state of the editor.
+
+### getCurrentInlineStyle
+
+```
+getCurrentInlineStyle(): DraftInlineStyle
+```
+Returns an `OrderedSet<string>` that represents the "current" inline style
+for the editor.
+
+This is the inline style value that would be used if a character were inserted
+for the current `ContentState` and `SelectionState`, and takes into account
+any inline style overrides that should be applied.
+
+### getBlockTree
+
+```
+getBlockTree(blockKey: string): List;
+```
+Returns an Immutable `List` of decorated and styled ranges. This is used for
+rendering purposes, and is generated based on the `currentContent` and
+`decorator`.
+
+At render time, this object is used to break the contents into the appropriate
+block, decorator, and styled range components.
+
+## Static Methods
+
+### createEmpty
+
+```
+static createEmpty(decorator?: DraftDecoratorType): EditorState
+```
+Returns a new `EditorState` object with an empty `ContentState` and default
+configuration.
+
+### createWithContent
+
+```
+static createWithContent(
+  contentState: ContentState,
+  decorator?: DraftDecoratorType
+): EditorState
+```
+Returns a new `EditorState` object based on the `ContentState` and decorator
+provided.
+
+### create
+
+```
+static create(config: EditorStateCreationConfig): EditorState
+```
+Returns a new `EditorState` object based on a configuration object. Use this
+if you need custom configuration not available via the methods above.
+
+### push
+
+```
+static push(
+  editorState: EditorState,
+  contentState: ContentState,
+  changeType: EditorChangeType
+): EditorState
+```
+Returns a new `EditorState` object with the specified `ContentState` applied
+as the new `currentContent`. Based on the `changeType`, this `ContentState`
+may be regarded as a boundary state for undo/redo behavior. See
+[Undo/Redo](/draft-js/docs/advanced-undo-redo.html) discussion for details.
+
+All content changes must be applied to the EditorState with this method.
+
+_To be renamed._
+
+### undo
+
+```
+static undo(editorState: EditorState): EditorState
+```
+Returns a new `EditorState` object with the top of the undo stack applied
+as the new `currentContent`.
+
+The existing `currentContent` is pushed onto the `redo` stack.
+
+### redo
+
+```
+static redo(editorState: EditorState): EditorState
+```
+Returns a new `EditorState` object with the top of the redo stack applied
+as the new `currentContent`.
+
+The existing `currentContent` is pushed onto the `undo` stack.
+
+### acceptSelection
+
+```
+static acceptSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+```
+Returns a new `EditorState` object with the specified `SelectionState` applied,
+but without requiring the selection to be rendered.
+
+For example, this is useful when the DOM selection has changed outside of our
+control, and no re-renders are necessary.
+
+### forceSelection
+
+```
+static forceSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+```
+Returns a new `EditorState` object with the specified `SelectionState` applied,
+forcing the selection to be rendered.
+
+This is useful when the selection should be manually rendered in the correct
+location to maintain control of the rendered output.
+
+## Properties and Getters
+
+In most cases, the instance and static methods above should be sufficient to
+manage the state of your Draft editor.
+
+Below is the full list of properties tracked by an `EditorState`, as well as
+their corresponding getter methods, to better provide detail on the scope of the
+state tracked in this object.
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods above.
+
+### allowUndo
+
+```
+allowUndo: boolean;
+getAllowUndo()
+```
+Whether to allow undo/redo behavior in this editor. Default is `true`.
+
+Since the undo/redo stack is the major source of memory retention, if you have
+an editor UI that does not require undo/redo behavior, you might consider
+setting this to `false`.
+
+### currentContent
+
+```
+currentContent: ContentState;
+getCurrentContent()
+```
+The currently rendered `ContentState`. See [getCurrentContent()](#getcurrentcontent).
+
+### decorator
+
+```
+decorator: ?DraftDecoratorType;
+getDecorator()
+```
+The current decorator object, if any.
+
+Note that the `ContentState` is independent of your decorator. If a decorator
+is provided, it will be used to decorate ranges of text for rendering.
+
+### directionMap
+
+```
+directionMap: BlockMap;
+getDirectionMap()
+```
+A map of each block and its text direction, as determined by UnicodeBidiService.
+
+You should not manage this manually.
+
+### forceSelection
+
+```
+forceSelection: boolean;
+mustForceSelection()
+```
+Whether to force the current `SelectionState` to be rendered.
+
+You should not set this property manually -- see
+[forceSelection()](#forceselection).
+
+### inCompositionMode
+
+```
+inCompositionMode: boolean;
+isInCompositionMode()
+```
+Whether the user is in IME composition mode. This is useful for rendering the
+appropriate UI for IME users, even when no content changes have been committed
+to the editor. You should not set this property manually.
+
+### inlineStyleOverride
+
+```
+inlineStyleOverride: DraftInlineStyle;
+getInlineStyleOverride()
+```
+An inline style value to be applied to the next inserted characters. This is
+used when keyboard commands or style buttons are used to apply an inline style
+for a collapsed selection range.
+
+`DraftInlineStyle` is a type alias for an immutable `OrderedSet` of strings,
+each of which corresponds to an inline style.
+
+### lastChangeType
+
+```
+lastChangeType: EditorChangeType;
+getLastChangeType()
+```
+The type of content change that took place in order to bring us to our current
+`ContentState`. This is used when determining boundary states for undo/redo.
+
+### nativelyRenderedContent
+
+```
+nativelyRenderedContent: ?ContentState;
+getNativelyRenderedContent()
+```
+During edit behavior, the editor may allow certain actions to render natively.
+For instance, during normal typing behavior in the contentEditable-based component,
+we can typically allow key events to fall through to print characters in the DOM.
+In doing so, we can avoid extra re-renders and preserve spellcheck highlighting.
+
+When allowing native rendering behavior, it is appropriate to use the
+`nativelyRenderedContent` property to indicate that no re-render is necessary
+for this `EditorState`.
+
+### redoStack
+
+```
+redoStack: Stack<ContentState>;
+getRedoStack()
+```
+An immutable stack of `ContentState` objects that can be resurrected for redo
+operations. When performing an undo operation, the current `ContentState` is
+pushed onto the `redoStack`.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the `allowUndo` property.
+
+See also [undoStack](#undostack).
+
+### selection
+
+```
+selection: SelectionState;
+getSelection()
+```
+The currently rendered `SelectionState`. See [acceptSelection()](#acceptselection)
+and [forceSelection()](#forceselection).
+
+You should not manage this property manually.
+
+### treeMap
+
+```
+treeMap: OrderedMap<string, List>;
+```
+The fully decorated and styled tree of ranges to be rendered in the editor
+component. The `treeMap` object is generated based on a `ContentState` and an
+optional decorator (`DraftDecoratorType`).
+
+At render time, components should iterate through the `treeMap` object to
+render decorated ranges and styled ranges, using the [getBlockTree()](#getblocktree)
+method.
+
+You should not manage this property manually.
+
+### undoStack
+
+```
+undoStack: Stack<ContentState>;
+getUndoStack()
+```
+An immutable stack of `ContentState` objects that can be restored for undo
+operations.
+
+When performing operations that modify contents, we determine whether the
+current `ContentState` should be regarded as a "boundary" state that the user
+can reach by performing an undo operation. If so, the `ContentState` is pushed
+onto the `undoStack`. If not, the outgoing `ContentState` is simply discarded.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the `allowUndo` property.
+
+See also [redoStack](#redostack).
diff --git a/docs/APIReference-Entity.md b/docs/APIReference-Entity.md
new file mode 100644
index 0000000..6548ff6
--- /dev/null
+++ b/docs/APIReference-Entity.md
@@ -0,0 +1,117 @@
+---
+id: api-reference-entity
+title: Entity
+layout: docs
+category: API Reference
+next: api-reference-selection-state
+permalink: docs/api-reference-entity.html
+---
+
+`Entity` is a static module containing the API for creating, retrieving, and
+updating entity objects, which are used for annotating text ranges with metadata.
+This module also houses the single store used to maintain entity data.
+
+This article is dedicated to covering the details of the API. See the
+[advanced topics article on entities](/draft-js/docs/advanced-topics-entities.html)
+for more detail on how entities may be used.
+
+Entity objects returned by `Entity` methods are represented as
+[DraftEntityInstance](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntityInstance.js) immutable records. These have a simple set of getter functions and should
+be used only for retrieval.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#create">
+      <pre>create(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#add">
+      <pre>add(instance: DraftEntityInstance): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#get">
+      <pre>get(key: string): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#mergedata">
+      <pre>mergeData(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#replacedata">
+      <pre>replaceData(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+</ul>
+
+## Methods
+
+### create
+
+```
+create(
+  type: DraftEntityType,
+  mutability: DraftEntityMutability,
+  data?: Object
+): string
+```
+The `create` method should be used to generate a new entity object with the
+supplied properties.
+
+Note that a string is returned from this function. This is because entities
+are referenced by their string key in `ContentState`. The string value should
+be used within `CharacterMetadata` objects to track the entity for annotated
+characters.
+
+### add
+
+```
+add(instance: DraftEntityInstance): string
+```
+In most cases, you will use `Entity.create()`. This is a convenience method
+that you probably will not need in typical Draft usage.
+
+The `add` function is useful in cases where the instances have already been
+created, and now need to be added to the `Entity` store. This may occur in cases
+where a vanilla JavaScript representation of a `ContentState` is being revived
+for editing.
+
+### get
+
+```
+get(key: string): DraftEntityInstance
+```
+Returns the `DraftEntityInstance` for the specified key. Throws if no instance
+exists for that key.
+
+### mergeData
+
+```
+mergeData(
+  key: string,
+  toMerge: {[key: string]: any}
+): DraftEntityInstance
+```
+Since `DraftEntityInstance` objects are immutable, you cannot update an entity's
+metadata through typical mutative means.
+
+The `mergeData` method allows you to apply updates to the specified entity.
+
+### replaceData
+
+```
+replaceData(
+  key: string,
+  newData: {[key: string]: any}
+): DraftEntityInstance
+```
+The `replaceData` method is similar to the `mergeData` method, except it will
+totally discard the existing `data` value for the instance and replace it with
+the specified `newData`.
diff --git a/docs/APIReference-Modifier.md b/docs/APIReference-Modifier.md
new file mode 100644
index 0000000..d6c56eb
--- /dev/null
+++ b/docs/APIReference-Modifier.md
@@ -0,0 +1,207 @@
+---
+id: api-reference-modifier
+title: Modifier
+layout: docs
+category: API Reference
+permalink: docs/api-reference-modifier.html
+---
+
+The `Modifier` module is a static set of utility functions that encapsulate common
+edit operations on `ContentState` objects. It is highly recommended that you use
+these methods for edit operations.
+
+These methods also take care of removing or modifying entity ranges appropriately,
+given the mutability types of any affected entities.
+
+In each case, these methods accept `ContentState` objects with relevant
+parameters and return `ContentState` objects. The returned `ContentState`
+will be the same as the input object if no edit was actually performed.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#replacetext">
+      <pre>replaceText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#inserttext">
+      <pre>insertText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#movetext">
+      <pre>moveText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#replacewithfragment">
+      <pre>replaceWithFragment(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removerange">
+      <pre>removeRange(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#splitblock">
+      <pre>splitBlock(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyinlinestyle">
+      <pre>applyInlineStyle(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removeinlinestyle">
+      <pre>removeInlineStyle(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#setblocktype">
+      <pre>setBlockType(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyentity">
+      <pre>applyEntity(...): ContentState</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### replaceText
+
+```
+replaceText(
+  contentState: ContentState,
+  rangeToReplace: SelectionState,
+  text: string,
+  inlineStyle?: DraftInlineStyle,
+  entityKey?: ?string
+): ContentState
+```
+Replaces the specified range of this `ContentState` with the supplied string,
+with the inline style and entity key applied to the entire inserted string.
+
+Example: On Facebook, when replacing `@abraham lincoln` with a mention of
+Abraham Lincoln, the entire old range is the target to replace and the mention
+entity should be applied to the inserted string.
+
+### insertText
+
+```
+insertText(
+  contentState: ContentState,
+  targetRange: SelectionState,
+  text: string,
+  inlineStyle?: DraftInlineStyle,
+  entityKey?: ?string
+): ContentState
+```
+Identical to `replaceText`, but enforces that the target range is collapsed
+so that no characters are replaced. This is just for convenience, since text
+edits are so often insertions rather than replacements.
+
+### moveText
+
+```
+moveText(
+  contentState: ContentState,
+  removalRange: SelectionState,
+  targetRange: SelectionState
+): ContentState
+```
+Moves the "removal" range to the "target" range, replacing the target text.
+
+### replaceWithFragment
+
+```
+replaceWithFragment(
+  contentState: ContentState,
+  targetRange: SelectionState,
+  fragment: BlockMap
+): ContentState
+```
+A "fragment" is a section of a block map, effectively just an
+`OrderedMap<string, ContentBlock>` much the same as the full block map of a
+`ContentState` object.
+
+This method will replace the "target" range with the fragment.
+
+Example: When pasting content, we convert the paste into a fragment to be inserted
+into the editor, then use this method to add it.
+
+### removeRange
+
+```
+removeRange(
+  contentState: ContentState,
+  rangeToRemove: SelectionState,
+  removalDirection: DraftRemovalDirection
+): ContentState
+```
+Remove an entire range of text from the editor. The removal direction is important
+for proper entity deletion behavior.
+
+### splitBlock
+
+```
+splitBlock(
+  contentState: ContentState,
+  selectionState: SelectionState
+): ContentState
+```
+Split the selected block into two blocks. This should only be used if the
+selection is collapsed.
+
+### applyInlineStyle
+
+```
+applyInlineStyle(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  inlineStyle: string
+): ContentState
+```
+Apply the specified inline style to the entire selected range.
+
+### removeInlineStyle
+
+```
+removeInlineStyle(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  inlineStyle: string
+): ContentState
+```
+Remove the specified inline style from the entire selected range.
+
+### setBlockType
+
+```
+setBlockType(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  blockType: DraftBlockType
+): ContentState
+```
+Set the block type for all selected blocks.
+
+### applyEntity
+
+```
+applyEntity(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  entityKey: ?string
+): ContentState
+```
+Apply an entity to the entire selected range, or remove all entities from the
+range if `entityKey` is `null`.
diff --git a/docs/APIReference-SelectionState.md b/docs/APIReference-SelectionState.md
new file mode 100644
index 0000000..ece2156
--- /dev/null
+++ b/docs/APIReference-SelectionState.md
@@ -0,0 +1,324 @@
+---
+id: api-reference-selection-state
+title: SelectionState
+layout: docs
+category: API Reference
+next: api-reference-data-conversion
+permalink: docs/api-reference-selection-state.html
+---
+
+`SelectionState` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents a selection range in the editor.
+
+The most common use for the `SelectionState` object is via `EditorState.getSelection()`,
+which provides the `SelectionState` currently being rendered in the editor.
+
+### Keys and Offsets
+
+A selection range has two points: an **anchor** and a **focus**. (Read more on
+[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary)).
+
+The native DOM approach represents each point as a Node/offset pair, where the offset
+is a number corresponding either to a position within a Node's `childNodes` or, if the
+Node is a text node, a character offset within the text contents.
+
+Since Draft maintains the contents of the editor using `ContentBlock` objects,
+we can use our own model to represent these points. Thus, selection points are
+tracked as key/offset pairs, where the `key` value is the key of the `ContentBlock`
+where the point is positioned and the `offset` value is the character offset
+within the block.
+
+### Start/End vs. Anchor/Focus
+
+The concept of **anchor** and **focus** is very useful when actually rendering
+a selection state in the browser, as it allows us to use forward and backward
+selection as needed. For editing operations, however, the direction of the selection
+doesn't matter. In this case, it is more appropriate to think in terms of
+**start** and **end** points.
+
+The `SelectionState` therefore exposes both anchor/focus values and
+start/end values. When managing selection behavior, we recommend that
+you work with _anchor_ and _focus_ values to maintain selection direction.
+When managing content operations, however, we recommend that you use _start_
+and _end_ values.
+
+For instance, when extracting a slice of text from a block based on a
+`SelectionState`, it is irrelevant whether the selection is backward:
+
+```
+var start = selectionState.getStartOffset();
+var end = selectionState.getEndOffset();
+var selectedText = myContentBlock.getText().slice(start, end);
+```
+
+Note that `SelectionState` itself tracks only _anchor_ and _focus_ values.
+_Start_ and _end_ values are derived.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(blockKey)</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getstartkey">
+      <pre>getStartKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getstartoffset">
+      <pre>getStartOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendkey">
+      <pre>getEndKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendoffset">
+      <pre>getEndOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchorkey">
+      <pre>getAnchorKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchoroffset">
+      <pre>getAnchorOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocuskey">
+      <pre>getFocusKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocusoffset">
+      <pre>getFocusOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getisbackward">
+      <pre>getIsBackward()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gethasfocus">
+      <pre>getHasFocus()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#iscollapsed">
+      <pre>isCollapsed()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasedgewithin">
+      <pre>hasEdgeWithin(blockKey, start, end)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#serialize">
+      <pre>serialize()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#anchorkey">
+      <pre>anchorKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#anchoroffset">
+      <pre>anchorOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focuskey">
+      <pre>focusKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focusoffset">
+      <pre>focusOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#isbackward">
+      <pre>isBackward</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasfocus">
+      <pre>hasFocus</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createEmpty()
+
+```
+createEmpty(blockKey: string): SelectionState
+```
+Create a `SelectionState` object at the zero offset of the provided block key
+and `hasFocus` set to false.
+
+## Methods
+
+### getStartKey()
+
+```
+getStartKey(): string
+```
+Returns the key of the block containing the start position of the selection range.
+
+### getStartOffset()
+
+```
+getStartOffset(): number
+```
+Returns the block-level character offset of the start position of the selection range.
+
+### getEndKey()
+
+```
+getEndKey(): string
+```
+Returns the key of the block containing the end position of the selection range.
+
+### getEndOffset()
+
+```
+getEndOffset(): number
+```
+Returns the block-level character offset of the end position of the selection range.
+
+### getAnchorKey()
+
+```
+getAnchorKey(): string
+```
+Returns the key of the block containing the anchor position of the selection range.
+
+### getAnchorOffset()
+
+```
+getAnchorOffset(): number
+```
+Returns the block-level character offset of the anchor position of the selection range.
+
+### getFocusKey()
+
+```
+getFocusKey(): string
+```
+Returns the key of the block containing the focus position of the selection range.
+
+### getFocusOffset()
+
+```
+getFocusOffset(): number
+```
+Returns the block-level character offset of the focus position of the selection range.
+
+### getIsBackward()
+
+```
+getIsBackward(): boolean
+```
+Returns whether the focus position is before the anchor position in the document.
+
+This must be derived from the key order of the active `ContentState`, or if the selection
+range is entirely within one block, a comparison of the anchor and focus offset values.
+
+### getHasFocus()
+
+```
+getHasFocus(): boolean
+```
+Returns whether the editor has focus.
+
+### isCollapsed()
+
+```
+isCollapsed(): boolean
+```
+Returns whether the selection range is collapsed, i.e. a caret. This is true
+when the anchor and focus keys are the same /and/ the anchor and focus offsets
+are the same.
+
+### hasEdgeWithin()
+
+```
+hasEdgeWithin(blockKey: string, start: number, end: number): boolean
+```
+Returns whether the selection range has an edge that overlaps with the specified
+start/end range within a given block.
+
+This is useful when setting DOM selection within a block after contents are
+rendered.
+
+### serialize()
+
+```
+serialize(): string
+```
+Returns a serialized version of the `SelectionState`. Useful for debugging.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+```
+var selectionState = SelectionState.createEmpty('foo');
+var updatedSelection = selectionState.merge({
+  focusKey: 'bar',
+  focusOffset: 0,
+});
+var anchorKey = updatedSelection.getAnchorKey(); // 'foo'
+var focusKey = updatedSelection.getFocusKey(); // 'bar'
+```
+
+### anchorKey
+The block containing the anchor end of the selection range.
+
+### anchorOffset
+The offset position of the anchor end of the selection range.
+
+### focusKey
+The block containing the focus end of the selection range.
+
+### focusOffset
+The offset position of the focus end of the selection range.
+
+### isBackward
+If the anchor position is lower in the document than the focus position, the
+selection is backward. Note: The `SelectionState` is an object with
+no knowledge of the `ContentState` structure. Therefore, when updating
+`SelectionState` values, you are responsible for updating `isBackward` as well.
+
+### hasFocus
+Whether the editor currently has focus.
diff --git a/docs/Advanced-Topics-Block-Components.md b/docs/Advanced-Topics-Block-Components.md
new file mode 100644
index 0000000..d073636
--- /dev/null
+++ b/docs/Advanced-Topics-Block-Components.md
@@ -0,0 +1,120 @@
+---
+id: advanced-topics-block-components
+title: Custom Block Components
+layout: docs
+category: Advanced Topics
+next: advanced-topics-inline-styles
+permalink: docs/advanced-topics-block-components.html
+---
+
+Draft is designed to solve problems for straightforward rich text interfaces
+like comments and chat messages, but it also powers richer editor experiences
+like [Facebook Notes](https://www.facebook.com/notes/).
+
+Users can embed images within their Notes, either loading from their existing
+Facebook photos or by uploading new images from the desktop. To that end,
+the Draft framework supports custom rendering at the block level, to render
+content like rich media in place of plain text.
+
+The [TeX editor](https://github.com/facebook/draft-js/tree/master/examples/tex)
+in the Draft repository provides a live example of custom block rendering, with
+TeX syntax translated on the fly into editable embedded formula rendering via the
+[KaTeX library](https://khan.github.io/KaTeX/).
+
+By using a custom block renderer, it is possible to introduce complex rich
+interactions within the frame of your editor.
+
+## Custom Block Components
+
+Within the `Editor` component, one may specify the `blockRendererFn` prop.
+This prop function allows a higher-level component to define custom React
+rendering for `ContentBlock` objects, based on block type, text, or other
+criteria.
+
+For instance, we may wish to render `ContentBlock` objects of type `'media'` using
+a custom `MediaComponent`.
+
+```
+function myBlockRenderer(contentBlock) {
+  const type = contentBlock.getType();
+  if (type === 'media') {
+    return {
+      component: MediaComponent,
+      props: {
+        foo: 'bar',
+      },
+    };
+  }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithMedia = React.createClass({
+  ...
+  render() {
+    return <Editor ... blockRendererFn={myBlockRenderer} />;
+  }
+});
+```
+
+If no custom renderer object is returned by the `blockRendererFn` function,
+`Editor` will render the default `DraftEditorBlock` text block component.
+
+The `component` property defines the component to be used, while the optional
+`props` object includes props that will be passed through to the rendered
+custom component.
+
+By defining this function within the context of a higher-level component,
+the props for this custom component may be bound to that component, allowing
+instance methods for custom component props.
+
+## Defining custom block components
+
+Within `MediaComponent`, the most likely use case is that you will want to
+retrieve entity metadata to render your custom block. You may apply an entity
+key to the text within a `'media'` block during `EditorState` management,
+then retrieve the metadata for that key in your custom component `render()`
+code.
+
+```
+import {Entity} from 'draft-js';
+const MediaComponent = React.createClass({
+  render() {
+    const {block, foo} = this.props;
+    const data = Entity.get(block.getEntityAt(0)).getData();
+    // Return a <figure> or some other content using this data.
+  }
+});
+```
+
+The `ContentBlock` object is made available within the custom component, along
+with the props defined at the top level. By extracting entity information from
+the `ContentBlock` and the `Entity` map, you can obtain the metadata required to
+render your custom component.
+
+_Retrieving the entity from the block is admittedly a bit of an awkward API,
+and is worth revisiting._
+
+## Recommendations and other notes
+
+If your custom block renderer requires mouse interaction, it is often wise
+to temporarily set your `Editor` to `readOnly={true}` during this
+interaction. In this way, the user does not trigger any selection changes within
+the editor while interacting with the custom block. This should not be a problem
+with respect to editor behavior, since interacting with your custom block
+component is most likely mutually exclusive from text changes within the editor.
+
+The recommendation above is especially important for custom block renderers
+that involve text input, like the TeX editor example.
+
+It is also worth noting that within the Facebook Notes editor, we have not
+tried to perform any special SelectionState rendering or management on embedded
+media, such as rendering a highlight on an embedded photo when selecting it.
+This is in part because of the rich interaction provided on the media
+itself, with resize handles and other controls exposed to mouse behavior.
+
+Since an engineer using Draft has full awareness of the selection state
+of the editor and full control over native Selection APIs, it would be possible
+to build selection behavior on static embedded media if desired. So far, though,
+we have not tried to solve this at Facebook, so we have not packaged solutions
+for this use case into the Draft project at this time.
diff --git a/docs/Advanced-Topics-Block-Styling.md b/docs/Advanced-Topics-Block-Styling.md
new file mode 100644
index 0000000..0727ef7
--- /dev/null
+++ b/docs/Advanced-Topics-Block-Styling.md
@@ -0,0 +1,59 @@
+---
+id: advanced-topics-block-styling
+title: Block Styling
+layout: docs
+category: Advanced Topics
+next: advanced-topics-block-components
+permalink: docs/advanced-topics-block-styling.html
+---
+
+Within `Editor`, some block types are given default CSS styles to limit the amount
+of basic configuration required to get engineers up and running with custom
+editors.
+
+By defining a `blockStyleFn` prop function for a `Editor`, it is possible
+to specify classes that should be applied to blocks at render time.
+
+## DraftStyleDefault.css
+
+The Draft library includes default block CSS styles within
+[DraftStyleDefault.css](https://github.com/facebook/draft-js/blob/master/src/component/utils/DraftStyleDefault.css). _(Note that the annotations on the CSS class names are
+artifacts of Facebook's internal CSS module management system._
+
+These CSS rules are largely devoted to providing default styles for list items,
+without which callers would be responsible for managing their own default list
+styles.
+
+## blockStyleFn
+
+The `blockStyleFn` prop on `Editor` allows you to define CSS classes to
+style blocks at render time. For instance, you may wish to style `'blockquote'`
+type blocks with fancy italic text.
+
+```
+function myBlockStyleFn(contentBlock) {
+  const type = contentBlock.getType();
+  if (type === 'blockquote') {
+    return 'superFancyBlockquote';
+  }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithFancyBlockquotes = React.createClass({
+  render() {
+    return <Editor ... blockStyleFn={myBlockStyleFn} />;
+  }
+});
+```
+
+Then in your own CSS:
+
+```
+.superFancyBlockquote {
+  color: #999;
+  font-family: 'Hoefler Text', Georgia, serif;
+  font-style: italic;
+  text-align: center;
+}
+```
diff --git a/docs/Advanced-Topics-Decorators.md b/docs/Advanced-Topics-Decorators.md
new file mode 100644
index 0000000..7b5cad9
--- /dev/null
+++ b/docs/Advanced-Topics-Decorators.md
@@ -0,0 +1,152 @@
+---
+id: advanced-topics-decorators
+title: Decorators
+layout: docs
+category: Advanced Topics
+next: advanced-topics-key-bindings
+permalink: docs/advanced-topics-decorators.html
+---
+
+Inline and block styles aren't the only kind of rich styling that we might
+want to add to our editor. The Facebook comment input, for example, provides
+blue background highlights for mentions and hashtags.
+
+To support flexibility for custom rich text, Draft provides a "decorator"
+system. The [tweet example](https://github.com/facebook/draft-js/tree/master/examples/tweet)
+offers a live example of decorators in action.
+
+## CompositeDecorator
+
+The decorator concept is based on scanning the contents of a given
+[ContentBlock](/draft-js/docs/api-reference-content-block.html)
+for ranges of text that match a defined strategy, then rendering them
+with a specified React component.
+
+You can use the `CompositeDecorator` class to define your desired
+decorator behavior. This class allows you to supply multiple `DraftDecorator`
+objects, and will search through a block of text with each strategy in turn.
+
+Decorators are stored within the `EditorState` record. When creating a new
+`EditorState` object, e.g. via `EditorState.createEmpty()`, a decorator may
+optionally be provided.
+
+> Under the hood
+>
+> When contents change in a Draft editor, the resulting `EditorState` object
+> will evaluate the new `ContentState` with its decorator, and identify ranges
+> to be decorated. A complete tree of blocks, decorators, and inline styles is
+> formed at this time, and serves as the basis for our rendered output.
+>
+> In this way, we always ensure that as contents change, rendered decorations
+> are in sync with our `EditorState`.
+
+In the "Tweet" editor example, for instance, we use a `CompositeDecorator` that
+searches for @-handle strings as well as hashtag strings:
+
+```
+const compositeDecorator = new CompositeDecorator([
+  {
+    strategy: handleStrategy,
+    component: HandleSpan,
+  },
+  {
+    strategy: hashtagStrategy,
+    component: HashtagSpan,
+  },
+]);
+```
+
+This composite decorator will first scan a given block of text for @-handle
+matches, then for hashtag matches.
+
+```
+// Note: these aren't very good regexes, don't use them!
+const HANDLE_REGEX = /\@[\w]+/g;
+const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
+
+function handleStrategy(contentBlock, callback) {
+  findWithRegex(HANDLE_REGEX, contentBlock, callback);
+}
+
+function hashtagStrategy(contentBlock, callback) {
+  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+}
+
+function findWithRegex(regex, contentBlock, callback) {
+  const text = contentBlock.getText();
+  let matchArr, start;
+  while ((matchArr = regex.exec(text)) !== null) {
+    start = matchArr.index;
+    callback(start, start + matchArr[0].length);
+  }
+}
+```
+
+The strategy functions execute the provided callback with the `start` and
+`end` values of the matching range of text.
+
+## Decorator Components
+
+For your decorated ranges of text, you must define a React component to use
+to render them. These tend to be simple `span` elements with CSS classes or
+styles applied to them.
+
+In our current example, the `CompositeDecorator` object names `HandleSpan` and
+`HashtagSpan` as the components to use for decoration. These are just basic
+stateless components:
+
+```
+const HandleSpan = (props) => {
+  return <span {...props} style={styles.handle}>{props.children}</span>;
+};
+
+const HashtagSpan = (props) => {
+  return <span {...props} style={styles.hashtag}>{props.children}</span>;
+};
+```
+
+Note that `props.children` is passed through to the rendered output. This is
+done to ensure that the text is rendered within the decorated `span`.
+
+You can use the same approach for links, as demonstrated in our
+[link example](https://github.com/facebook/draft-js/tree/master/examples/link).
+
+### Beyond CompositeDecorator
+
+The decorator object supplied to an `EditorState` need only match the expectations
+of the
+[DraftDecoratorType](https://github.com/facebook/draft-js/blob/master/src/model/decorators/DraftDecoratorType.js)
+Flow type definition, which means that you can create any decorator classes
+you wish, as long as they match the expected type -- you are not bound by
+`CompositeDecorator`.
+
+## Setting new decorators
+
+Further, it is acceptable to set a new `decorator` value on the `EditorState`
+on the fly, during normal state propagation -- through immutable means, of course.
+
+This means that during your app workflow, if your decorator becomes invalid or
+requires a modification, you can create a new decorator object (or use
+`null` to remove all decorations) and `EditorState.set()` to make use of the new
+decorator setting.
+
+For example, if for some reason we wished to disable the creation of @-handle
+decorations while the user interacts with the editor, it would be fine to do the
+following:
+
+```
+function turnOffHandleDecorations(editorState) {
+  const onlyHashtags = new CompositeDecorator([{
+    strategy: hashtagStrategy,
+    component: HashtagSpan,
+  }]);
+  return EditorState.set(editorState, {decorator: onlyHashtags});
+}
+```
+
+The `ContentState` for this `editorState` will be re-evaluated with the new
+decorator, and @-handle decorations will no longer be present in the next
+render pass.
+
+Again, this remains memory-efficient due to data persistence across immutable
+objects.
diff --git a/docs/Advanced-Topics-Entities.md b/docs/Advanced-Topics-Entities.md
new file mode 100644
index 0000000..010db84
--- /dev/null
+++ b/docs/Advanced-Topics-Entities.md
@@ -0,0 +1,136 @@
+---
+id: advanced-topics-entities
+title: Entities
+layout: docs
+category: Advanced Topics
+next: advanced-topics-decorators
+permalink: docs/advanced-topics-entities.html
+---
+
+This article discusses the Entity system, which Draft uses for annotating
+ranges of text with metadata. Entities enable engineers to introduce levels of
+richness beyond styled text to their editors. Links, mentions, and embedded
+content can all be implemented using entities.
+
+In the Draft repository, the
+[link editor](https://github.com/facebook/draft-js/tree/master/examples/link)
+and
+[entity demo](https://github.com/facebook/draft-js/tree/master/examples/entity)
+provide live code examples to help clarify how entities can be used, as well
+as their built-in behavior.
+
+The [Entity API Reference](/draf-js/docs/api-reference-entity.html) provides
+details on the static methods to be used when creating, retrieving, or updating
+entity objects.
+
+## Introduction
+
+An entity is an object that represents metadata for a range of text within a
+Draft editor. It has three properties:
+
+- **type**: A string that indicates what kind of entity it is, e.g. `'LINK'`,
+`'MENTION'`, `'PHOTO'`.
+- **mutability**: Not to be confused with immutability a la `immutable-js`, this
+property denotes the behavior of a range of text annotated with this entity
+object when editing the text range within the editor. This is addressed in
+greater detail below.
+- **data**: An optional object containing metadata for the entity. For instance,
+a `'LINK'` entity might contain a `data` object that contains the `href` value
+for that link.
+
+All entities are stored in a single object store within the `Entity` module,
+and are referenced by key within `ContentState` and React components used to
+decorate annotated ranges. _(We are considering future changes to bring
+the entity store into `EditorState` or `ContentState`.)_
+
+Using [decorators](/draft-js/docs/advanced-topics-decorators.html) or
+[custom block components](docs/advanced-topics-block-components.html), you can
+add rich rendering to your editor based on entity metadata.
+
+## Creating and Retrieving Entities
+
+Entities should be created using `Entity.create`, which accepts the three
+properties above as arguments. This method returns a string key, which can then
+be used to refer to the entity.
+
+This key is the value that should be used when applying entities to your
+content. For instance, the `Modifier` module contains an `applyEntity` method:
+
+```
+const key = Entity.create('LINK', 'MUTABLE', {href: 'http://www.zombo.com'});
+const contentStateWithLink = Modifier.applyEntity(
+  contentState,
+  targetRange,
+  key
+);
+```
+
+For a given range of text, then, you can extract its associated entity key by using
+the `getEntityAt()` method on a `ContentBlock` object, passing in the target
+offset value.
+
+```
+const blockWithLinkAtBeginning = contentState.getBlockForKey('...');
+const linkKey = blockWithLinkAtBeginning.getEntityAt(0);
+const linkInstance = Entity.get(linkKey);
+const {href} = linkInstance.getData();
+```
+
+## "Mutability"
+
+Entities may have one of three "mutability" values. The difference between them
+is the way they behave when the user makes edits to them.
+
+Note that `DraftEntityInstance` objects are always immutable Records, and this
+property is meant only to indicate how the annotated text may be "mutated" within
+the editor. _(Future changes may rename this property to ward off potential
+confusion around naming.)_
+
+### Immutable
+
+This text cannot be altered without removing the entity annotation
+from the text. Entities with this mutability type are effectively atomic.
+
+For instance, in a Facebook input, add a mention for a Page (i.e. Barack Obama).
+Then, either add a character within the mentioned text, or try to delete a character.
+Note that when adding characters, the entity is removed, and when deleting charaters,
+the entire entity is removed.
+
+This mutability value is useful in cases where the text absolutely must match
+its relevant metadata, and may not be altered.
+
+### Mutable
+
+This text may be altered freely. For instance, link text is
+generally intended to be "mutable" since the href and linkified text are not
+tightly coupled.
+
+### Segmented
+
+Entities that are "segmented" are tightly coupled to their text in much the
+same way as "immutable" entities, but allow customization via deletion.
+
+For instance, in a Facebook input, add a mention for a friend. Then, add a
+character to the text. Note that the entity is removed from the entire string,
+since your mentioned friend may not have their name altered in your text.
+
+Next, try deleting a character or word within the mention. Note that only the
+section of the mention that you have deleted is removed. In this way, we can
+allow short names for mentions.
+
+## Modifying Entities
+
+Since `DraftEntityInstance` records are immutable, you may not update the `data`
+property on an instance directly.
+
+Instead, two `Entity` methods are available to modify entities: `mergeData` and
+`replaceData`. The former allows updating data by passing in an object to merge,
+while the latter completely swaps in the new data object.
+
+## Using Entities for Rich Content
+
+The next article in this section covers the usage of decorator objects, which
+can be used to retrieve entities for rendering purposes.
+
+The [link editor example](https://github.com/facebook/draft-js/tree/master/examples/link)
+provides a working example of entity creation and decoration in use.
diff --git a/docs/Advanced-Topics-Inline-Styles.md b/docs/Advanced-Topics-Inline-Styles.md
new file mode 100644
index 0000000..638b2e6
--- /dev/null
+++ b/docs/Advanced-Topics-Inline-Styles.md
@@ -0,0 +1,112 @@
+---
+id: advanced-topics-inline-styles
+title: Complex Inline Styles
+layout: docs
+category: Advanced Topics
+next: advanced-topics-nested-lists
+permalink: docs/advanced-topics-inline-styles.html
+---
+
+Within your editor, you may wish to provide a wide variety of inline style
+behavior that goes well beyond the bold/italic/underline basics. For instance,
+you may want to support variety with color, font families, font sizes, and more.
+Further, your desired styles may overlap or be mutually exclusive.
+
+The [Rich Editor](http://github.com/facebook/draft-js/examples/rich) and
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+examples demonstrate complex inline style behavior in action.
+
+### Model
+
+Within the Draft model, inline styles are represented at the character level,
+using an immutable `OrderedSet` to define the list of styles to be applied to
+each character. These styles are identified by string. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+For example, consider the text "Hello **world**". The first six characters of
+the string are represented by the empty set, `OrderedSet()`. The final five
+characters are represented by `OrderedSet.of('BOLD')`. For convenience, we can
+think of these `OrderedSet` objects as arrays, though in reality we aggressively
+reuse identical immutable objects.
+
+In essence, our styles are:
+
+```
+[
+  [], // H
+  [], // e
+  ...
+  ['BOLD'], // w
+  ['BOLD'], // o
+  // etc.
+]
+```
+
+### Overlapping Styles
+
+Now let's say that we wish to make the middle range of characters italic as well:
+"He_llo **wo**_**rld**". This operation can be performed via the
+[Modifier](/draft-js/docs/api-reference-modifier.html) API.
+
+The end result will accommodate the overlap by including `'ITALIC'` in the
+relevant `OrderedSet` objects as well.
+
+```
+[
+  [], // H
+  [], // e
+  ['ITALIC'], // l
+  ...
+  ['BOLD', 'ITALIC'], // w
+  ['BOLD', 'ITALIC'], // o
+  ['BOLD'], // r
+  // etc.
+]
+```
+
+When determining how to render inline-styled text, Draft will identify
+contiguous ranges of identically styled characters and render those characters
+together in styled `span` nodes.
+
+### Mapping a style string to CSS
+
+By default, `Editor` provides support for a basic list of inline styles:
+`'BOLD'`, `'ITALIC'`, `'UNDERLINE'`, and `'CODE'`. These are mapped to simple CSS
+style objects, which are used to apply styles to the relevant ranges.
+
+For your editor, you may define custom style strings to include with these
+defaults, or you may override the default style objects for the basic styles.
+
+Within your `Editor` use case, you may provide the `customStyleMap` prop
+to define your style objects. (See
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+for a live example.)
+
+For example, you may want to add a `'STRIKETHROUGH'` style. To do so, define a
+custom style map:
+
+```
+import {Editor} from 'draft-js';
+
+const styleMap = {
+  'STRIKETHROUGH': {
+    textDecoration: 'line-through',
+  },
+};
+
+class MyEditor extends React.Component {
+  // ...
+  render() {
+    return (
+      <Editor
+        customStyleMap={styleMap}
+        editorState={this.state.editorState}
+        ...
+      />
+    );
+  }
+}
+```
+
+When rendered, the `textDecoration: line-through` style will be applied to all
+character ranges with the `STRIKETHROUGH` style.
diff --git a/docs/Advanced-Topics-Issues-and-Pitfalls.md b/docs/Advanced-Topics-Issues-and-Pitfalls.md
new file mode 100644
index 0000000..048f62d
--- /dev/null
+++ b/docs/Advanced-Topics-Issues-and-Pitfalls.md
@@ -0,0 +1,75 @@
+---
+id: advanced-topics-issues-and-pitfalls
+title: Issues and Pitfalls
+layout: docs
+category: Advanced Topics
+next: api-reference-editor
+permalink: docs/advanced-topics-issues-and-pitfalls.html
+---
+
+This article addresses some known issues with the Draft editor framework, as
+well as some common pitfalls that we have encountered while using the framework
+at Facebook.
+
+## Common Pitfalls
+
+### Delayed state updates
+
+A common pattern for unidirectional data management is to batch or otherwise
+delay updates to data stores, using a setTimeout or another mechanism. Stores are
+updated, then emit changes to the relevant React components to propagate
+re-rendering.
+
+When delays are introduced to a React application with a Draft editor, however,
+it is possible to cause significant interaction problems. This is because the
+editor expects immediate updates and renders that stay in sync with the user's typing
+behavior. Delays can prevent updates from being propagated through the editor
+component tree, which can cause a disconnect between keystrokes and updates.
+
+To avoid this while still using a delaying or batching mechanism, you should
+separate the delay behavior from your `Editor` state propagation. That is,
+you must always allow your `EditorState` to propagate to your `Editor`
+component without delay, and independently perform batched updates that do
+not affect the state of your `Editor` component.
+
+## Known Issues
+
+### React ContentEditable Warning
+
+Within the React core, a warning is used to ward off engineers who wish to
+use ContentEditable within their components, since by default the
+browser-controlled nature of ContentEditable does not mesh with strict React
+control over the DOM. The Draft editor resolves this issue, so for our case,
+the warning is noise. You can ignore it for now.
+
+We are currently looking into removing or replacing the warning to alleviate
+the irritation it may cause: https://github.com/facebook/react/issues/6081
+
+### Custom OSX Keybindings
+
+Because the browser has no access to OS-level custom keybindings, it is not
+possible to intercept edit intent behaviors that do not map to default system
+key bindings.
+
+The result of this is that users who use custom keybindings may encounter
+issues with Draft editors, since their key commands may not behave as expected.
+
+### Browser plugins/extensions
+
+As with any React application, browser plugins and extensions that modify the
+DOM can cause Draft editors to break.
+
+Grammar checkers, for instance, may modify the DOM within contentEditable
+elements, adding styles like underlines and backgrounds. Since React cannot
+reconcile the DOM if the browser does not match its expectations,
+the editor state may fail to remain in sync with the DOM.
+
+Certain old ad blockers are also known to break the native DOM Selection
+API -- a bad idea no matter what! -- and since Draft depends on this API to
+maintain controlled selection state, this can cause trouble for editor
+interaction.
+
+### IME and Internet Explorer
+
+As of IE11, Internet Explorer demonstrates notable issues with certain international
+input methods, most significantly Korean input.
diff --git a/docs/Advanced-Topics-Key-Bindings.md b/docs/Advanced-Topics-Key-Bindings.md
new file mode 100644
index 0000000..d9c02e3
--- /dev/null
+++ b/docs/Advanced-Topics-Key-Bindings.md
@@ -0,0 +1,100 @@
+---
+id: advanced-topics-key-bindings
+title: Key Bindings
+layout: docs
+category: Advanced Topics
+next: advanced-topics-managing-focus
+permalink: docs/advanced-topics-key-bindings.html
+---
+
+The `Editor` component offers flexibility to define custom key bindings
+for your editor, via the `keyBindingFn` prop. This allows you to match key
+commands to behaviors in your editor component.
+
+### Defaults
+
+The default key binding function is `getDefaultKeyBinding`.
+
+Since the Draft framework maintains tight control over DOM rendering and
+behavior, basic editing commands must be captured and routed through the key
+binding system.
+
+`getDefaultKeyBinding` maps known OS-level editor commands to `DraftEditorCommand`
+strings, which then correspond to behaviors within component handlers.
+
+For instance, `Ctrl+Z` (Win) and `Cmd+Z` (OSX) map to the `'undo'` command,
+which then routes our handler to perform an `EditorState.undo()`.
+
+### Customization
+
+You may provide your own key binding function to supply custom command strings.
+
+It is recommended that your function use `getDefaultKeyBinding` as a
+fall-through case, so that your editor may benefit from default commands.
+
+With your custom command string, you may then implement the `handleKeyCommand`
+prop function, which allows you to map that command string to your desired
+behavior. If `handleKeyCommand` returns `true`, the command is considered
+handled. If it returns `false`, the command will fall through
+
+### Example
+
+Let's say we have an editor that should have a "Save" mechanism to periodically
+write your contents to the server as a draft copy.
+
+First, let's define our key binding function.
+
+```
+import {KeyBindingUtil} from 'draft-js';
+const {hasCommandModifier} = KeyBindingUtil;
+
+function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
+  if (e.keyCode === 83 /* `S` key */ && hasCommandModifier(e)) {
+    return 'myeditor-save';
+  }
+  return getDefaultKeyBinding(e);
+}
+```
+
+Our function receives a key event, and we check whether it matches our criteria:
+it must be an `S` key, and it must have a command modifier, i.e. the command
+key for OSX, or the control key otherwise.
+
+If the command is a match, return a string that names the command. Otherwise,
+fall through to the default key bindings.
+
+In our editor component, we can then make use of the command via the
+`handleKeyCommand` prop:
+
+```
+import {Editor} from 'draft-js';
+class MyEditor extends React.Component {
+  // ...
+
+  handleKeyCommand(command: string): boolean {
+    if (command === 'myeditor-save') {
+      // Perform a request to save your contents, set
+      // a new `editorState`, etc.
+      return true;
+    }
+    return false;
+  }
+
+  render() {
+    return (
+      <Editor
+        editorState={this.state.editorState}
+        handleKeyCommand={this.handleKeyCommand.bind(this)}
+        ...
+      />
+    );
+  }
+}
+```
+
+The `'myeditor-save'` command can be used for our custom behavior, and returning
+true instructs the editor that the command has been handled and no more work
+is required.
+
+By returning false in all other cases, default commands are able to fall
+through to default handler behavior.
diff --git a/docs/Advanced-Topics-Managing-Focus.md b/docs/Advanced-Topics-Managing-Focus.md
new file mode 100644
index 0000000..a0e2ec2
--- /dev/null
+++ b/docs/Advanced-Topics-Managing-Focus.md
@@ -0,0 +1,40 @@
+---
+id: advanced-topics-managing-focus
+title: Managing Focus
+layout: docs
+category: Advanced Topics
+next: advanced-topics-block-styling
+permalink: docs/advanced-topics-managing-focus.html
+---
+
+Managing text input focus can be a tricky task within React components. The browser
+focus/blur API is imperative, so setting or removing focus via declarative means
+purely through `render()` tends to feel awkward and incorrect, and it requires
+challenging attempts at controlling focus state.
+
+With that in mind, at Facebook we often choose to expose `focus()` methods
+on components that wrap text inputs. This breaks the declarative paradigm,
+but it also simplifies the work needed for engineers to successfully manage
+focus behavior within their apps.
+
+The `Editor` component follows this pattern, so there is a public `focus()`
+method available on the component. This allows you to use a ref within your
+higher-level component to call `focus()` directly on the component when needed.
+
+The event listeners within the component will observe focus changes and
+propagate them through `onChange` as expected, so state and DOM will remain
+correctly in sync.
+
+## Translating container clicks to focus
+
+Your higher-level component will most likely wrap the `Editor` component in a
+container of some kind, perhaps with padding to style it to match your app.
+
+By default, if a user clicks within this container but outside of the rendered
+`Editor` while attempting to focus the editor, the editor will have no awareness
+of the click event. It is therefore recommended that you use a click listener
+on your container component, and use the `focus()` method described above to
+apply focus to your editor.
+
+The [plaintext editor example](https://github.com/facebook/draft-js/tree/master/examples/plaintext),
+for instance, uses this pattern.
diff --git a/docs/Advanced-Topics-Nested-Lists.md b/docs/Advanced-Topics-Nested-Lists.md
new file mode 100644
index 0000000..d28feaf
--- /dev/null
+++ b/docs/Advanced-Topics-Nested-Lists.md
@@ -0,0 +1,22 @@
+---
+id: advanced-topics-nested-lists
+title: Nested Lists
+layout: docs
+category: Advanced Topics
+next: advanced-topics-text-direction
+permalink: docs/advanced-topics-nested-lists.html
+---
+
+The Draft framework provides support for nested lists, as demonstrated in the
+Facebook Notes editor. There, you can use `Tab` and `Shift+Tab` to add or remove
+depth to a list item.
+
+The `RichUtils` module provides a handy `onTab` method that manages this
+behavior, and should be sufficient for most nested list needs. You can use
+the `onTab` prop on your `Editor` to make use of this utility.
+
+By default, styling is applied to list items to set appropriate spacing and
+list style behavior, via `DraftStyleDefault.css`.
+
+Note that there is currently no support for handling depth for blocks of any type
+except `'ordered-list-item'` and `'unordered-list-item'`.
diff --git a/docs/Advanced-Topics-Text-Direction.md b/docs/Advanced-Topics-Text-Direction.md
new file mode 100644
index 0000000..f262933
--- /dev/null
+++ b/docs/Advanced-Topics-Text-Direction.md
@@ -0,0 +1,35 @@
+---
+id: advanced-topics-text-direction
+title: Text Direction
+layout: docs
+category: Advanced Topics
+next: advanced-topics-issues-and-pitfalls
+permalink: docs/advanced-topics-text-direction.html
+---
+
+Facebook supports dozens of languages, which means that our text inputs need
+to be flexible enough to handle considerable variety.
+
+For example, we want input behavior for RTL languages such as Arabic and Hebrew
+to meet users' expectations. We also want to be able to support editor contents
+with a mixture of LTR and RTL text.
+
+To that end, Draft uses a bidi direction algorithm to determine appropriate
+text alignment and direction on a per-block basis.
+
+Text is rendered with an LTR or RTL direction automatically as the user types.
+You should not need to do anything to set direction yourself.
+
+## Text Alignment
+
+While languages are automatically aligned to the left or right during composition,
+as defined by the content characters, it is also possible for engineers to
+manually set the text alignment for an editor's contents.
+
+This may be useful, for instance, if an editor requires strictly centered
+contents, or needs to keep text aligned flush against another UI element.
+
+The `Editor` component therefore provides a `textAlignment` prop, with a
+simple set of values: `'left'`, `'center'`, and `'right'`. Using these values,
+the contents of your editor will be aligned to the specified direction regardless
+of language and character set.
diff --git a/docs/Overview.md b/docs/Overview.md
new file mode 100644
index 0000000..e5d98dc
--- /dev/null
+++ b/docs/Overview.md
@@ -0,0 +1,45 @@
+---
+id: getting-started
+title: Overview
+layout: docs
+category: Quick Start
+next: quickstart-api-basics
+permalink: docs/overview.html
+---
+
+Draft.js is a framework for building rich text editors in React, powered by an immutable model and abstracting over cross-browser differences.
+
+Draft.js makes it easy to build any type of rich text input, whether you're just looking to support a few inline text styles or building a complex text editor for composing long-form articles.
+
+### Installation
+
+Currently Draft.js is distributed via npm. It depends on React and React DOM which must also be installed.
+
+```sh
+npm install --save draft-js react react-dom
+```
+
+### Usage
+
+```js
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Editor} from 'draft-js';
+
+class MyEditor extends React.Component {
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <Editor editorState={editorState} onChange={this.onChange} />;
+  }
+}
+
+ReactDOM.render(
+  <MyEditor />,
+  document.getElementById('container')
+);
+```
+
+Next, let's go into the basics of the API and learn what else you can do with Draft.js.
diff --git a/docs/QuickStart-API-Basics.md b/docs/QuickStart-API-Basics.md
new file mode 100644
index 0000000..0ec845e
--- /dev/null
+++ b/docs/QuickStart-API-Basics.md
@@ -0,0 +1,75 @@
+---
+id: quickstart-api-basics
+title: API Basics
+layout: docs
+category: Quick Start
+next: quickstart-rich-styling
+permalink: docs/quickstart-api-basics.html
+---
+
+This document provides an overview of the basics of the `Draft` API. A
+[working example](https://github.com/facebook/draft-js/tree/master/examples/plaintext)
+is also available to follow along.
+
+## Controlled Inputs
+
+The `Editor` React component is built as a controlled ContentEditable component,
+with the goal of providing a top-level API modeled on the familiar React
+*controlled input* API.
+
+As a brief refresher, controlled inputs involve two key pieces:
+
+1. A _value_ to represent the state of the input
+2. An _onChange_ prop function to receive updates to the input
+
+This approach allows the component that composes the input to have strict
+control over the state of the input, while still allowing updates to the DOM
+to provide information about the text that the user has written.
+
+```
+const MyInput = React.createClass({
+  onChange(evt) {
+    this.setState({value: evt.target.value});
+  },
+  render() {
+    return <input value={this.state.value} onChange={this.onChange} />;
+  }
+});
+```
+
+The top-level component can maintain control over the input state via this
+`value` state property.
+
+## Controlling Rich Text
+
+In a React rich text scenario, however, there are two clear problems:
+
+1. A string of plaintext is insufficient to represent the complex state of
+a rich editor.
+2. There is no such `onChange` event available for a ContentEditable element.
+
+State is therefore represented as a single immutable
+[EditorState](/draft-js/docs/api-reference-editor-state.html) object, and
+`onChange` is implemented within the `Editor` core to provide this state
+value to the top level.
+
+The `EditorState` object is a complete snapshot of the state of the editor,
+including contents, cursor, and undo/redo history. All changes to content and
+selection within the editor will create new `EditorState` objects. Note that
+this remains efficient due to data persistence across immutable objects.
+
+```
+import {Editor} from 'draft-js';
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <Editor editorState={editorState} onChange={this.onChange} />;
+  }
+});
+```
+
+For any edits or selection changes that occur in the editor DOM, your `onChange`
+handler will execute with the latest `EditorState` object based on those changes.
diff --git a/docs/QuickStart-Rich-Styling.md b/docs/QuickStart-Rich-Styling.md
new file mode 100644
index 0000000..6fd2489
--- /dev/null
+++ b/docs/QuickStart-Rich-Styling.md
@@ -0,0 +1,112 @@
+---
+id: quickstart-rich-styling
+title: Rich Styling
+layout: docs
+category: Quick Start
+next: advanced-topics-entities
+permalink: docs/quickstart-rich-styling.html
+---
+
+Now that we have established the basics of the top-level API, we can go a step
+further and examine how basic rich styling can be added to a `Draft` editor.
+
+A [rich text example](https://github.com/facebook/draft-js/tree/master/examples/rich)
+is also available to follow along.
+
+## EditorState: Yours to Command
+
+The previous article introduced the `EditorState` object as a snapshot of the
+full state of the editor, as provided by the `Editor` core via the
+`onChange` prop.
+
+However, since your top-level React component is responsible for maintaining the
+state, you also have the freedom to apply changes to that `EditorState` object
+in any way you see fit.
+
+For inline and block style behavior, for example, the `RichUtils` module
+provides a number of useful functions to help manipulate state.
+
+Similarly, the [Modifier](/draft-js/docs/api-reference-modifier.html) module also provides a
+number of common operations that allow you to apply edits, including changes
+to text, styles, and more. This module is a suite of edit functions that
+compose simpler, smaller edit functions to return the desired `EditorState`
+object.
+
+For this example, we'll stick with `RichUtils` to demonstrate how to apply basic
+rich styling within the top-level component.
+
+## RichUtils and Key Commands
+
+`RichUtils` has information about the core key commands available to web editors,
+such as Cmd+B (bold), Cmd+I (italic), and so on.
+
+We can observe and handle key commands via the `handleKeyCommand` prop, and
+hook these into `RichUtils` to apply or remove the desired style.
+
+```
+import {Editor, RichUtils} from 'draft-js';
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  handleKeyCommand(command) {
+    const {editorState} = this.state;
+    const newState = RichUtils.handleKeyCommand(editorState, command);
+    if (newState) {
+      this.onChange(newState);
+      return true;
+    }
+    return false;
+  },
+  render() {
+    const {editorState} = this.state;
+    return (
+      <Editor
+        editorState={editorState}
+        handleKeyCommand={this.handleKeyCommand}
+        onChange={this.onChange}
+      />
+    );
+  }
+});
+```
+
+> handleKeyCommand
+>
+> The `command` argument supplied to `handleKeyCommand` is a string value, the
+> name of the command to be executed. This is mapped from a DOM key event. See
+> [Advanced Topics - Key Binding](/draft-js/docs/advanced-topics-key-bindings.html) for more
+> on this, as well as details on why the function returns a boolean.
+
+## Styling Controls in UI
+
+Within your React component, you can add buttons or other controls to allow
+the user to modify styles within the editor. In the example above, we are using
+known key commands, but we can add more complex UI to provide these rich
+features.
+
+Here's a super-basic example with a "Bold" button to toggle the `BOLD` style.
+
+```
+const MyEditor = React.createClass({
+  ...
+
+  _onBoldClick() {
+    this.onChange(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
+  }
+
+  render() {
+    const {editorState} = this.state;
+    return (
+      <div>
+        <button onClick={this._onBoldClick}>Bold</button>
+        <Editor
+          editorState={editorState}
+          handleKeyCommand={this.handleKeyCommand}
+          onChange={this.onChange}
+        />
+      </div>
+    );
+  }
+});
+```
diff --git a/examples/color/color.html b/examples/color/color.html
new file mode 100644
index 0000000..f7a63a6
--- /dev/null
+++ b/examples/color/color.html
@@ -0,0 +1,219 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Color</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+  </head>
+  <body>
+    <div style="font-family: Georgia, serif; font-size: 15px; padding: 20px; width: 600px; line-height: 24px;">
+      This example demonstrates how custom inline styles can be used to create
+      editors with an unlimited range of style combinations. Note also that
+      the state of the editor can be manipulated to enforce that only one
+      color may be active at any time.
+    </div>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.min.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {Editor, EditorState, Modifier, RichUtils} = Draft;
+
+      class ColorfulEditorExample extends React.Component {
+        constructor(props) {
+          super(props);
+          this.state = {editorState: EditorState.createEmpty()};
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+          this.toggleColor = (toggledColor) => this._toggleColor(toggledColor);
+        }
+
+        _toggleColor(toggledColor) {
+          const {editorState} = this.state;
+          const selection = editorState.getSelection();
+
+          // Let's just allow one color at a time. Turn off all active colors.
+          const nextContentState = Object.keys(colorStyleMap)
+            .reduce((contentState, color) => {
+              return Modifier.removeInlineStyle(contentState, selection, color)
+            }, editorState.getCurrentContent());
+
+          let nextEditorState = EditorState.push(
+            editorState,
+            nextContentState,
+            'change-inline-style'
+          );
+
+          const currentStyle = editorState.getCurrentInlineStyle();
+
+          // Unset style override for current color.
+          if (selection.isCollapsed()) {
+            nextEditorState = currentStyle.reduce((state, color) => {
+              return RichUtils.toggleInlineStyle(state, color);
+            }, nextEditorState);
+          }
+
+          // If the color is being toggled on, apply it.
+          if (!currentStyle.has(toggledColor)) {
+            nextEditorState = RichUtils.toggleInlineStyle(
+              nextEditorState,
+              toggledColor
+            );
+          }
+
+          this.onChange(nextEditorState);
+        }
+
+        render() {
+          const {editorState} = this.state;
+          return (
+            <div style={styles.root}>
+              <ColorControls
+                editorState={editorState}
+                onToggle={this.toggleColor}
+              />
+              <div style={styles.editor} onClick={this.focus}>
+                <Editor
+                  customStyleMap={colorStyleMap}
+                  editorState={editorState}
+                  onChange={this.onChange}
+                  placeholder="Write something colorful..."
+                  ref="editor"
+                />
+              </div>
+            </div>
+          );
+        }
+      }
+
+      class StyleButton extends React.Component {
+        constructor(props) {
+          super(props);
+          this.onToggle = (e) => {
+            e.preventDefault();
+            this.props.onToggle(this.props.style);
+          };
+        }
+
+        render() {
+          let style;
+          if (this.props.active) {
+            style = {...styles.styleButton, ...colorStyleMap[this.props.style]};
+          } else {
+            style = styles.styleButton;
+          }
+
+          return (
+            <span style={style} onMouseDown={this.onToggle}>
+              {this.props.label}
+            </span>
+          );
+        }
+      }
+
+      var COLORS = [
+        {label: 'Red', style: 'red'},
+        {label: 'Orange', style: 'orange'},
+        {label: 'Yellow', style: 'yellow'},
+        {label: 'Green', style: 'green'},
+        {label: 'Blue', style: 'blue'},
+        {label: 'Indigo', style: 'indigo'},
+        {label: 'Violet', style: 'violet'},
+      ];
+
+      const ColorControls = (props) => {
+        var currentStyle = props.editorState.getCurrentInlineStyle();
+        return (
+          <div style={styles.controls}>
+            {COLORS.map(type =>
+              <StyleButton
+                active={currentStyle.has(type.style)}
+                label={type.label}
+                onToggle={props.onToggle}
+                style={type.style}
+              />
+            )}
+          </div>
+        );
+      };
+
+      // This object provides the styling information for our custom color
+      // styles.
+      const colorStyleMap = {
+        red: {
+          color: 'rgba(255, 0, 0, 1.0)',
+        },
+        orange: {
+          color: 'rgba(255, 127, 0, 1.0)',
+        },
+        yellow: {
+          color: 'rgba(180, 180, 0, 1.0)',
+        },
+        green: {
+          color: 'rgba(0, 180, 0, 1.0)',
+        },
+        blue: {
+          color: 'rgba(0, 0, 255, 1.0)',
+        },
+        indigo: {
+          color: 'rgba(75, 0, 130, 1.0)',
+        },
+        violet: {
+          color: 'rgba(127, 0, 255, 1.0)',
+        },
+      };
+
+      const styles = {
+        root: {
+          fontFamily: '\'Georgia\', serif',
+          fontSize: 14,
+          padding: 20,
+          width: 600,
+        },
+        editor: {
+          borderTop: '1px solid #ddd',
+          cursor: 'text',
+          fontSize: 16,
+          marginTop: 20,
+          minHeight: 400,
+          paddingTop: 20,
+        },
+        controls: {
+          fontFamily: '\'Helvetica\', sans-serif',
+          fontSize: 14,
+          marginBottom: 10,
+          userSelect: 'none',
+        },
+        styleButton: {
+          color: '#999',
+          cursor: 'pointer',
+          marginRight: 16,
+          padding: '2px 0',
+        },
+      };
+
+      ReactDOM.render(
+        <ColorfulEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/examples/entity/entity.html b/examples/entity/entity.html
new file mode 100644
index 0000000..777fc75
--- /dev/null
+++ b/examples/entity/entity.html
@@ -0,0 +1,223 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Plain Text Editor</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {
+        convertFromRaw,
+        convertToRaw,
+        CompositeDecorator,
+        ContentState,
+        Editor,
+        EditorState,
+        Entity,
+      } = Draft;
+
+      const rawContent = {
+        blocks: [
+          {
+            text: (
+              'This is an "immutable" entity: Superman. Deleting any ' +
+              'characters will delete the entire entity. Adding characters ' +
+              'will remove the entity from the range.'
+            ),
+            type: 'unstyled',
+            entityRanges: [{offset: 31, length: 8, key: 'first'}],
+          },
+          {
+            text: '',
+            type: 'unstyled',
+          },
+          {
+            text: (
+              'This is a "mutable" entity: Batman. Characters may be added ' +
+              'and removed.'
+            ),
+            type: 'unstyled',
+            entityRanges: [{offset: 28, length: 6, key: 'second'}],
+          },
+          {
+            text: '',
+            type: 'unstyled',
+          },
+          {
+            text: (
+              'This is a "segmented" entity: Green Lantern. Deleting any ' +
+              'characters will delete the current "segment" from the range. ' +
+              'Adding characters will remove the entire entity from the range.'
+            ),
+            type: 'unstyled',
+            entityRanges: [{offset: 30, length: 13, key: 'third'}],
+          },
+        ],
+
+        entityMap: {
+          first: {
+            type: 'TOKEN',
+            mutability: 'IMMUTABLE',
+          },
+          second: {
+            type: 'TOKEN',
+            mutability: 'MUTABLE',
+          },
+          third: {
+            type: 'TOKEN',
+            mutability: 'SEGMENTED',
+          },
+        },
+      };
+
+      class EntityEditorExample extends React.Component {
+        constructor(props) {
+          super(props);
+
+          const decorator = new CompositeDecorator([
+            {
+              strategy: getEntityStrategy('IMMUTABLE'),
+              component: TokenSpan,
+            },
+            {
+              strategy: getEntityStrategy('MUTABLE'),
+              component: TokenSpan,
+            },
+            {
+              strategy: getEntityStrategy('SEGMENTED'),
+              component: TokenSpan,
+            },
+          ]);
+
+          const blocks = convertFromRaw(rawContent);
+
+          this.state = {
+            editorState: EditorState.createWithContent(
+              ContentState.createFromBlockArray(blocks),
+              decorator
+            ),
+          };
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+          this.logState = () => {
+            console.log(convertToRaw(this.state.editorState.toJS()));
+          };
+        }
+
+        render() {
+          return (
+            <div style={styles.root}>
+              <div style={styles.editor} onClick={this.focus}>
+                <Editor
+                  editorState={this.state.editorState}
+                  onChange={this.onChange}
+                  placeholder="Enter some text..."
+                  ref="editor"
+                />
+              </div>
+              <input
+                onClick={this.logState}
+                style={styles.button}
+                type="button"
+                value="Log State"
+              />
+            </div>
+          );
+        }
+      }
+
+      function getEntityStrategy(mutability) {
+        return function(contentBlock, callback) {
+          contentBlock.findEntityRanges(
+            (character) => {
+              const entityKey = character.getEntity();
+              if (entityKey === null) {
+                return false;
+              }
+              return Entity.get(entityKey).getMutability() === mutability;
+            },
+            callback
+          );
+        };
+      }
+
+      function getDecoratedStyle(mutability) {
+        switch (mutability) {
+          case 'IMMUTABLE': return styles.immutable;
+          case 'MUTABLE': return styles.mutable;
+          case 'SEGMENTED': return styles.segmented;
+          default: return null;
+        }
+      }
+
+      const TokenSpan = (props) => {
+        const style = getDecoratedStyle(
+          Entity.get(props.entityKey).getMutability()
+        );
+        return (
+          <span {...props} style={style}>
+            {props.children}
+          </span>
+        );
+      };
+
+      const styles = {
+        root: {
+          fontFamily: '\'Helvetica\', sans-serif',
+          padding: 20,
+          width: 600,
+        },
+        editor: {
+          border: '1px solid #ccc',
+          cursor: 'text',
+          minHeight: 80,
+          padding: 10,
+        },
+        button: {
+          marginTop: 10,
+          textAlign: 'center',
+        },
+        immutable: {
+          backgroundColor: 'rgba(0, 0, 0, 0.2)',
+          padding: '2px 0',
+        },
+        mutable: {
+          backgroundColor: 'rgba(204, 204, 255, 1.0)',
+          padding: '2px 0',
+        },
+        segmented: {
+          backgroundColor: 'rgba(248, 222, 126, 1.0)',
+          padding: '2px 0',
+        },
+      };
+
+      ReactDOM.render(
+        <EntityEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/examples/link/link.html b/examples/link/link.html
new file mode 100644
index 0000000..4865f8a
--- /dev/null
+++ b/examples/link/link.html
@@ -0,0 +1,178 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Link Editor</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {
+        CompositeDecorator,
+        ContentState,
+        Editor,
+        EditorState,
+        Entity,
+        RichUtils,
+      } = Draft;
+
+      class LinkEditorExample extends React.Component {
+        constructor(props) {
+          super(props);
+
+          const decorator = new CompositeDecorator([
+            {
+              strategy: findLinkEntities,
+              component: Link,
+            },
+          ]);
+
+          this.state = {
+            editorState: EditorState.createEmpty(decorator),
+          };
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+          this.logState = () => {
+            console.log(convertToRaw(this.state.editorState.toJS()));
+          };
+
+          this.addLink = this._addLink.bind(this);
+          this.removeLink = this._removeLink.bind(this);
+        }
+
+        _addLink(e) {
+          const {editorState} = this.state;
+          const selection = editorState.getSelection();
+          if (selection.isCollapsed()) {
+            return;
+          }
+          const href = window.prompt('Enter a URL');
+          const entityKey = Entity.create('link', 'MUTABLE', {href});
+          const content = editorState.getCurrentContent();
+          this.setState({
+            editorState: RichUtils.toggleLink(editorState, selection, entityKey),
+          });
+        }
+
+        _removeLink(e) {
+          const {editorState} = this.state;
+          const selection = editorState.getSelection();
+          if (selection.isCollapsed()) {
+            return;
+          }
+          const content = editorState.getCurrentContent();
+          this.setState({
+            editorState: RichUtils.toggleLink(editorState, selection, null),
+          });
+        }
+
+        render() {
+          return (
+            <div style={styles.root}>
+              <div style={{marginBottom: 10}}>
+                Select some text, then use the buttons to add or remove links
+                on the selected text.
+              </div>
+              <div style={styles.buttons}>
+                <button onMouseDown={this.addLink} style={{marginRight: 10}}>
+                  Add Link
+                </button>
+                <button onMouseDown={this.removeLink}>
+                  Remove Link
+                </button>
+              </div>
+              <div style={styles.editor} onClick={this.focus}>
+                <Editor
+                  editorState={this.state.editorState}
+                  onChange={this.onChange}
+                  placeholder="Enter some text..."
+                  ref="editor"
+                />
+              </div>
+              <input
+                onClick={this.logState}
+                style={styles.button}
+                type="button"
+                value="Log State"
+              />
+            </div>
+          );
+        }
+      }
+
+      function findLinkEntities(contentBlock, callback) {
+        contentBlock.findEntityRanges(
+          (character) => {
+            const entityKey = character.getEntity();
+            return (
+              entityKey !== null &&
+              Entity.get(entityKey).getType() === 'link'
+            );
+          },
+          callback
+        );
+      }
+
+      const Link = (props) => {
+        const {href} = Entity.get(props.entityKey).getData();
+        return (
+          <a href={href} style={styles.link}>
+            {props.children}
+          </a>
+        );
+      };
+
+      const styles = {
+        root: {
+          fontFamily: '\'Georgia\', serif',
+          padding: 20,
+          width: 600,
+        },
+        buttons: {
+          marginBottom: 10,
+        },
+        editor: {
+          border: '1px solid #ccc',
+          cursor: 'text',
+          minHeight: 80,
+          padding: 10,
+        },
+        button: {
+          marginTop: 10,
+          textAlign: 'center',
+        },
+        link: {
+          color: '#3b5998',
+          textDecoration: 'underline',
+        },
+      };
+
+      ReactDOM.render(
+        <LinkEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/examples/plaintext/plaintext.html b/examples/plaintext/plaintext.html
new file mode 100644
index 0000000..49e5036
--- /dev/null
+++ b/examples/plaintext/plaintext.html
@@ -0,0 +1,89 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Plain Text Editor</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {Editor, EditorState} = Draft;
+
+      class PlainTextEditorExample extends React.Component {
+        constructor(props) {
+          super(props);
+          this.state = {editorState: EditorState.createEmpty()};
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+          this.logState = () => console.log(this.state.editorState.toJS());
+        }
+
+        render() {
+          return (
+            <div style={styles.root}>
+              <div style={styles.editor} onClick={this.focus}>
+                <Editor
+                  editorState={this.state.editorState}
+                  onChange={this.onChange}
+                  placeholder="Enter some text..."
+                  ref="editor"
+                />
+              </div>
+              <input
+                onClick={this.logState}
+                style={styles.button}
+                type="button"
+                value="Log State"
+              />
+            </div>
+          );
+        }
+      }
+
+      const styles = {
+        root: {
+          fontFamily: '\'Helvetica\', sans-serif',
+          padding: 20,
+          width: 600,
+        },
+        editor: {
+          border: '1px solid #ccc',
+          cursor: 'text',
+          minHeight: 80,
+          padding: 10,
+        },
+        button: {
+          marginTop: 10,
+          textAlign: 'center',
+        },
+      };
+
+      ReactDOM.render(
+        <PlainTextEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/examples/rich/RichEditor.css b/examples/rich/RichEditor.css
new file mode 100644
index 0000000..2cfaf5d
--- /dev/null
+++ b/examples/rich/RichEditor.css
@@ -0,0 +1,62 @@
+.RichEditor-root {
+  background: #fff;
+  border: 1px solid #ddd;
+  font-family: 'Georgia', serif;
+  font-size: 14px;
+  padding: 15px;
+}
+
+.RichEditor-editor {
+  border-top: 1px solid #ddd;
+  cursor: text;
+  font-size: 16px;
+  margin-top: 10px;
+}
+
+.RichEditor-editor .public-DraftEditorPlaceholder-root,
+.RichEditor-editor .public-DraftEditor-content {
+  margin: 0 -15px -15px;
+  padding: 15px;
+}
+
+.RichEditor-editor .public-DraftEditor-content {
+  min-height: 100px;
+}
+
+.RichEditor-hidePlaceholder .public-DraftEditorPlaceholder-root {
+  display: none;
+}
+
+.RichEditor-editor .RichEditor-blockquote {
+  border-left: 5px solid #eee;
+  color: #666;
+  font-family: 'Hoefler Text', 'Georgia', serif;
+  font-style: italic;
+  margin: 16px 0;
+  padding: 10px 20px;
+}
+
+.RichEditor-editor .public-DraftStyleDefault-pre {
+  background-color: rgba(0, 0, 0, 0.05);
+  font-family: 'Inconsolata', 'Menlo', 'Consolas', monospace;
+  font-size: 16px;
+  padding: 20px;
+}
+
+.RichEditor-controls {
+  font-family: 'Helvetica', sans-serif;
+  font-size: 14px;
+  margin-bottom: 5px;
+  user-select: none;
+}
+
+.RichEditor-styleButton {
+  color: #999;
+  cursor: pointer;
+  margin-right: 16px;
+  padding: 2px 0;
+}
+
+.RichEditor-activeButton {
+  color: #5890ff;
+}
diff --git a/examples/rich/rich.html b/examples/rich/rich.html
new file mode 100644
index 0000000..85df09e
--- /dev/null
+++ b/examples/rich/rich.html
@@ -0,0 +1,218 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Rich Text</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+    <link rel="stylesheet" href="RichEditor.css" />
+    <style>
+      #target { width: 600px; }
+    </style>
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.min.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {Editor, EditorState, RichUtils} = Draft;
+
+      class RichEditorExample extends React.Component {
+        constructor(props) {
+          super(props);
+          this.state = {editorState: EditorState.createEmpty()};
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+
+          this.handleKeyCommand = (command) => this._handleKeyCommand(command);
+          this.toggleBlockType = (type) => this._toggleBlockType(type);
+          this.toggleInlineStyle = (style) => this._toggleInlineStyle(style);
+        }
+
+        _handleKeyCommand(command) {
+          const {editorState} = this.state;
+          const newState = RichUtils.handleKeyCommand(editorState, command);
+          if (newState) {
+            this.onChange(newState);
+            return true;
+          }
+          return false;
+        }
+
+        _toggleBlockType(blockType) {
+          this.onChange(
+            RichUtils.toggleBlockType(
+              this.state.editorState,
+              blockType
+            )
+          );
+        }
+
+        _toggleInlineStyle(inlineStyle) {
+          this.onChange(
+            RichUtils.toggleInlineStyle(
+              this.state.editorState,
+              inlineStyle
+            )
+          );
+        }
+
+        render() {
+          const {editorState} = this.state;
+
+          // If the user changes block type before entering any text, we can
+          // either style the placeholder or hide it. Let's just hide it now.
+          let className = 'RichEditor-editor';
+          var contentState = editorState.getCurrentContent();
+          if (!contentState.hasText()) {
+            if (contentState.getBlockMap().first().getType() !== 'unstyled') {
+              className += ' RichEditor-hidePlaceholder';
+            }
+          }
+
+          return (
+            <div className="RichEditor-root">
+              <BlockStyleControls
+                editorState={editorState}
+                onToggle={this.toggleBlockType}
+              />
+              <InlineStyleControls
+                editorState={editorState}
+                onToggle={this.toggleInlineStyle}
+              />
+              <div className={className} onClick={this.focus}>
+                <Editor
+                  blockStyleFn={getBlockStyle}
+                  customStyleMap={styleMap}
+                  editorState={editorState}
+                  handleKeyCommand={this.handleKeyCommand}
+                  onChange={this.onChange}
+                  placeholder="Tell a story..."
+                  ref="editor"
+                  spellCheck={true}
+                />
+              </div>
+            </div>
+          );
+        }
+      }
+
+      // Custom overrides for "code" style.
+      const styleMap = {
+        CODE: {
+          backgroundColor: 'rgba(0, 0, 0, 0.05)',
+          fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
+          fontSize: 16,
+          padding: 2,
+        },
+      };
+
+      function getBlockStyle(block) {
+        switch (block.getType()) {
+          case 'blockquote': return 'RichEditor-blockquote';
+          default: return null;
+        }
+      }
+
+      class StyleButton extends React.Component {
+        constructor() {
+          super();
+          this.onToggle = (e) => {
+            e.preventDefault();
+            this.props.onToggle(this.props.style);
+          };
+        }
+
+        render() {
+          let className = 'RichEditor-styleButton';
+          if (this.props.active) {
+            className += ' RichEditor-activeButton';
+          }
+
+          return (
+            <span className={className} onMouseDown={this.onToggle}>
+              {this.props.label}
+            </span>
+          );
+        }
+      }
+
+      const BLOCK_TYPES = [
+        {label: 'H1', style: 'header-one'},
+        {label: 'H2', style: 'header-two'},
+        {label: 'Blockquote', style: 'blockquote'},
+        {label: 'UL', style: 'unordered-list-item'},
+        {label: 'OL', style: 'ordered-list-item'},
+        {label: 'Code Block', style: 'code-block'},
+      ];
+
+      const BlockStyleControls = (props) => {
+        const {editorState} = props;
+        const selection = editorState.getSelection();
+        const blockType = editorState
+          .getCurrentContent()
+          .getBlockForKey(selection.getStartKey())
+          .getType();
+
+        return (
+          <div className="RichEditor-controls">
+            {BLOCK_TYPES.map((type) =>
+              <StyleButton
+                active={type.style === blockType}
+                label={type.label}
+                onToggle={props.onToggle}
+                style={type.style}
+              />
+            )}
+          </div>
+        );
+      };
+
+      var INLINE_STYLES = [
+        {label: 'Bold', style: 'BOLD'},
+        {label: 'Italic', style: 'ITALIC'},
+        {label: 'Underline', style: 'UNDERLINE'},
+        {label: 'Monospace', style: 'CODE'},
+      ];
+
+      const InlineStyleControls = (props) => {
+        var currentStyle = props.editorState.getCurrentInlineStyle();
+        return (
+          <div className="RichEditor-controls">
+            {INLINE_STYLES.map(type =>
+              <StyleButton
+                active={currentStyle.has(type.style)}
+                label={type.label}
+                onToggle={props.onToggle}
+                style={type.style}
+              />
+            )}
+          </div>
+        );
+      };
+
+      ReactDOM.render(
+        <RichEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/examples/tex/.eslintrc b/examples/tex/.eslintrc
new file mode 100644
index 0000000..eab3a26
--- /dev/null
+++ b/examples/tex/.eslintrc
@@ -0,0 +1,172 @@
+---
+parser: babel-eslint
+
+plugins:
+  - react
+
+env:
+  node: true
+  es6: true
+
+globals:
+  document: false
+  Immutable: false
+  React: false
+
+arrowFunctions: true
+blockBindings: true
+classes: true
+defaultParams: true
+destructuring: true
+forOf: true
+generators: true
+modules: true
+objectLiteralComputedProperties: true
+objectLiteralShorthandMethods: true
+objectLiteralShorthandProperties: true
+spread: true
+templateStrings: true
+
+rules:
+  # ERRORS
+  brace-style: [2, 1tbs, allowSingleLine: true]
+  camelcase: [2, properties: always]
+  comma-style: [2, last]
+  curly: [2, all]
+  eol-last: 2
+  eqeqeq: 2
+  guard-for-in: 2
+  handle-callback-err: [2, error]
+  indent: [2, 2, SwitchCase: 1]
+  key-spacing: [2, {beforeColon: false, afterColon: true}]
+  max-len: [2, 80, 4, ignorePattern: "^(\\s*var\\s.+=\\s*require\\s*\\(|import )"]
+  new-parens: 2
+  no-alert: 2
+  no-array-constructor: 2
+  no-caller: 2
+  no-cond-assign: 2
+  no-constant-condition: 2
+  no-delete-var: 2
+  no-div-regex: 2
+  no-dupe-args: 2
+  no-dupe-keys: 2
+  no-duplicate-case: 2
+  no-empty-character-class: 2
+  no-empty-label: 2
+  no-empty: 2
+  no-eval: 2
+  no-ex-assign: 2
+  no-extend-native: 2
+  no-extra-bind: 2
+  no-extra-boolean-cast: 2
+  no-extra-semi: 2
+  no-fallthrough: 2
+  no-floating-decimal: 2
+  no-func-assign: 2
+  no-implied-eval: 2
+  no-inner-declarations: [2, functions]
+  no-invalid-regexp: 2
+  no-irregular-whitespace: 2
+  no-iterator: 2
+  no-label-var: 2
+  no-lonely-if: 2
+  no-mixed-requires: [2, true]
+  no-mixed-spaces-and-tabs: 2
+  no-multi-spaces: 2
+  no-multi-str: 2
+  no-negated-in-lhs: 2
+  no-new-object: 2
+  no-new-require: 2
+  no-new-wrappers: 2
+  no-new: 2
+  no-obj-calls: 2
+  no-octal-escape: 2
+  no-octal: 2
+  no-param-reassign: 2
+  no-path-concat: 2
+  no-proto: 2
+  no-redeclare: 2
+  no-regex-spaces: 2
+  no-return-assign: 2
+  no-script-url: 2
+  no-sequences: 2
+  no-shadow-restricted-names: 2
+  no-shadow: 2
+  no-spaced-func: 2
+  no-sparse-arrays: 2
+  no-sync: 2
+  no-throw-literal: 2
+  no-trailing-spaces: 2
+  no-undef-init: 2
+  no-undef: 2
+  no-unreachable: 2
+  no-unused-expressions: 2
+  no-unused-vars: [2, {vars: all, args: after-used}]
+  no-void: 2
+  no-with: 2
+  one-var: [2, never]
+  operator-assignment: [2, always]
+  quote-props: [2, as-needed]
+  quotes: [2, single]
+  radix: 2
+  semi-spacing: [2, {before: false, after: true}]
+  semi: [2, always]
+  space-after-keywords: [2, always]
+  space-before-blocks: [2, always]
+  space-before-function-paren: [2, {anonymous: always, named: never}]
+  space-infix-ops: [2, int32Hint: false]
+  space-return-throw-case: 2
+  space-unary-ops: [2, {words: true, nonwords: false}]
+  spaced-comment: [2, always]
+  use-isnan: 2
+  valid-typeof: 2
+  wrap-iife: 2
+  yoda: [2, never, exceptRange: true]
+
+  # WARNINGS
+
+  # DISABLED
+  block-scoped-var: 0
+  comma-dangle: 0
+  complexity: 0
+  consistent-return: 0
+  consistent-this: 0
+  default-case: 0
+  dot-notation: 0
+  func-names: 0
+  func-style: 0
+  max-nested-callbacks: 0
+  new-cap: 0
+  newline-after-var: 0
+  no-catch-shadow: 0
+  no-console: 0
+  no-control-regex: 0
+  no-debugger: 0
+  no-eq-null: 0
+  no-inline-comments: 0
+  no-labels: 0
+  no-lone-blocks: 0
+  no-loop-func: 0
+  no-multiple-empty-lines: 0
+  no-native-reassign: 0
+  no-nested-ternary: 0
+  no-new-func: 0
+  no-process-env: 0
+  no-process-exit: 0
+  no-reserved-keys: 0
+  no-restricted-modules: 0
+  no-self-compare: 0
+  no-ternary: 0
+  no-undefined: 0
+  no-underscore-dangle: 0
+  no-use-before-define: 0
+  no-var: 0
+  no-warning-comments: 0
+  padded-blocks: 0
+  sort-vars: 0
+  space-in-brackets: 0
+  space-in-parens: 0
+  strict: 0
+  valid-jsdoc: 0
+  vars-on-top: 0
+  wrap-regex: 0
diff --git a/examples/tex/js/app.js b/examples/tex/js/app.js
new file mode 100644
index 0000000..2ddb511
--- /dev/null
+++ b/examples/tex/js/app.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import 'babel/polyfill';
+import TeXEditorExample from './components/TeXEditorExample';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+ReactDOM.render(
+  <TeXEditorExample />,
+  document.getElementById('target')
+);
diff --git a/examples/tex/js/components/TeXBlock.js b/examples/tex/js/components/TeXBlock.js
new file mode 100644
index 0000000..9747c94
--- /dev/null
+++ b/examples/tex/js/components/TeXBlock.js
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import katex from 'katex';
+import React from 'react';
+import {Entity} from 'draft-js';
+
+class KatexOutput extends React.Component {
+  constructor(props) {
+    super(props);
+    this._timer = null;
+  }
+
+  _update() {
+    if (this._timer) {
+      clearTimeout(this._timer);
+    }
+
+    this._timer = setTimeout(() => {
+      katex.render(
+        this.props.content,
+        this.refs.container,
+        {displayMode: true}
+      );
+    }, 0);
+  }
+
+  componentDidMount() {
+    this._update();
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.content !== this.props.content) {
+      this._update();
+    }
+  }
+
+  componentWillUnmount() {
+    clearTimeout(this._timer);
+    this._timer = null;
+  }
+
+  render() {
+    return <div ref="container" onClick={this.props.onClick} />;
+  }
+}
+
+export default class TeXBlock extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {editMode: false};
+
+    this._onClick = () => {
+      if (this.state.editMode) {
+        return;
+      }
+
+      this.setState({
+        editMode: true,
+        texValue: this._getValue(),
+      }, () => {
+        this._startEdit();
+      });
+    };
+
+    this._onValueChange = evt => {
+      var value = evt.target.value;
+      var invalid = false;
+      try {
+        katex.__parse(value);
+      } catch (e) {
+        invalid = true;
+      } finally {
+        this.setState({
+          invalidTeX: invalid,
+          texValue: value,
+        });
+      }
+    };
+
+    this._save = () => {
+      var entityKey = this.props.block.getEntityAt(0);
+      Entity.mergeData(entityKey, {content: this.state.texValue});
+      this.setState({
+        invalidTeX: false,
+        editMode: false,
+        texValue: null,
+      }, this._finishEdit);
+    };
+
+    this._remove = () => {
+      this.props.blockProps.onRemove(this.props.block.getKey());
+    };
+    this._startEdit = () => {
+      this.props.blockProps.onStartEdit(this.props.block.getKey());
+    };
+    this._finishEdit = () => {
+      this.props.blockProps.onFinishEdit(this.props.block.getKey());
+    };
+  }
+
+  _getValue() {
+    return Entity
+      .get(this.props.block.getEntityAt(0))
+      .getData()['content'];
+  }
+
+  render() {
+    var texContent = null;
+    if (this.state.editMode) {
+      if (this.state.invalidTeX) {
+        texContent = '';
+      } else {
+        texContent = this.state.texValue;
+      }
+    } else {
+      texContent = this._getValue();
+    }
+
+    var className = 'TeXEditor-tex';
+    if (this.state.editMode) {
+      className += ' TeXEditor-activeTeX';
+    }
+
+    var editPanel = null;
+    if (this.state.editMode) {
+      var buttonClass = 'TeXEditor-saveButton';
+      if (this.state.invalidTeX) {
+        buttonClass += ' TeXEditor-invalidButton';
+      }
+
+      editPanel =
+        <div className="TeXEditor-panel">
+          <textarea
+            className="TeXEditor-texValue"
+            onChange={this._onValueChange}
+            ref="textarea"
+            value={this.state.texValue}
+          />
+          <div className="TeXEditor-buttons">
+            <button
+              className={buttonClass}
+              disabled={this.state.invalidTeX}
+              onClick={this._save}>
+              {this.state.invalidTeX ? 'Invalid TeX' : 'Done'}
+            </button>
+            <button className="TeXEditor-removeButton" onClick={this._remove}>
+              Remove
+            </button>
+          </div>
+        </div>;
+    }
+
+    return (
+      <figure
+        contentEditable={false}
+        className={className}>
+        <KatexOutput content={texContent} onClick={this._onClick} />
+        {editPanel}
+      </figure>
+    );
+  }
+}
diff --git a/examples/tex/js/components/TeXEditorExample.js b/examples/tex/js/components/TeXEditorExample.js
new file mode 100644
index 0000000..3ee990f
--- /dev/null
+++ b/examples/tex/js/components/TeXEditorExample.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import Draft from 'draft-js';
+import {Map} from 'immutable';
+import React from 'react';
+
+import TeXBlock from './TeXBlock';
+import {content} from '../data/content';
+import {insertTeXBlock} from '../modifiers/insertTeXBlock';
+import {removeTeXBlock} from '../modifiers/removeTeXBlock';
+
+var {ContentState, Editor, EditorState, RichUtils} = Draft;
+
+export default class TeXEditorExample extends React.Component {
+  constructor(props) {
+    super(props);
+    const contentState = ContentState.createFromBlockArray(content);
+    this.state = {
+      editorState: EditorState.createWithContent(contentState),
+      liveTeXEdits: Map(),
+    };
+
+    this._blockRenderer = (block) => {
+      if (block.getType() === 'media') {
+        return {
+          component: TeXBlock,
+          props: {
+            onStartEdit: (blockKey) => {
+              var {liveTeXEdits} = this.state;
+              this.setState({liveTeXEdits: liveTeXEdits.set(blockKey, true)});
+            },
+            onFinishEdit: (blockKey) => {
+              var {liveTeXEdits} = this.state;
+              this.setState({liveTeXEdits: liveTeXEdits.remove(blockKey)});
+            },
+            onRemove: (blockKey) => this._removeTeX(blockKey),
+          },
+        };
+      }
+      return null;
+    };
+
+    this._focus = () => this.refs.editor.focus();
+    this._onChange = (editorState) => this.setState({editorState});
+
+    this._handleKeyCommand = command => {
+      var {editorState} = this.state;
+      var newState = RichUtils.handleKeyCommand(editorState, command);
+      if (newState) {
+        this._onChange(newState);
+        return true;
+      }
+      return false;
+    };
+
+    this._removeTeX = (blockKey) => {
+      var {editorState, liveTeXEdits} = this.state;
+      this.setState({
+        liveTeXEdits: liveTeXEdits.remove(blockKey),
+        editorState: removeTeXBlock(editorState, blockKey),
+      });
+    };
+
+    this._insertTeX = () => {
+      this.setState({
+        liveTeXEdits: Map(),
+        editorState: insertTeXBlock(this.state.editorState),
+      });
+    };
+  }
+
+  /**
+   * While editing TeX, set the Draft editor to read-only. This allows us to
+   * have a textarea within the DOM.
+   */
+  render() {
+    return (
+      <div className="TexEditor-container">
+        <div className="TeXEditor-root">
+          <div className="TeXEditor-editor" onClick={this._focus}>
+            <Editor
+              blockRendererFn={this._blockRenderer}
+              editorState={this.state.editorState}
+              handleKeyCommand={this._handleKeyCommand}
+              onChange={this._onChange}
+              placeholder="Start a document..."
+              readOnly={this.state.liveTeXEdits.count()}
+              ref="editor"
+              spellCheck={true}
+            />
+          </div>
+        </div>
+        <button onClick={this._insertTeX} className="TeXEditor-insert">
+          {'Insert new TeX'}
+        </button>
+      </div>
+    );
+  }
+}
diff --git a/examples/tex/js/data/content.js b/examples/tex/js/data/content.js
new file mode 100644
index 0000000..1dccca7
--- /dev/null
+++ b/examples/tex/js/data/content.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import {convertFromRaw} from 'draft-js';
+
+var rawContent = {
+  blocks: [
+    {
+      text: 'This is a Draft-based editor that supports TeX rendering.',
+      type: 'unstyled',
+    },
+    {
+      text: '',
+      type: 'unstyled',
+    },
+    {
+      text: (
+        'Each TeX block below is represented as a DraftEntity object and ' +
+        'rendered using Khan Academy\'s KaTeX library.'
+      ),
+      type: 'unstyled',
+    },
+    {
+      text: '',
+      type: 'unstyled',
+    },
+    {
+      text: 'Click any TeX block to edit.',
+      type: 'unstyled',
+    },
+    {
+      text: ' ',
+      type: 'media',
+      entityRanges: [{offset: 0, length: 1, key: 'first'}],
+    },
+    {
+      text: 'You can also insert a new TeX block at the cursor location.',
+      type: 'unstyled',
+    },
+  ],
+
+  entityMap: {
+    first: {
+      type: 'TOKEN',
+      mutability: 'IMMUTABLE',
+      data: {
+        content: (
+          '\\left( \\sum_{k=1}^n a_k b_k \\right)^{\\!\\!2} \\leq\n' +
+          '\\left( \\sum_{k=1}^n a_k^2 \\right)\n' +
+          '\\left( \\sum_{k=1}^n b_k^2 \\right)'
+        ),
+      },
+    }
+  }
+};
+
+export var content = convertFromRaw(rawContent);
diff --git a/examples/tex/js/modifiers/insertTeXBlock.js b/examples/tex/js/modifiers/insertTeXBlock.js
new file mode 100644
index 0000000..367a29a
--- /dev/null
+++ b/examples/tex/js/modifiers/insertTeXBlock.js
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import {List, Repeat} from 'immutable';
+import {
+  BlockMapBuilder,
+  CharacterMetadata,
+  ContentBlock,
+  EditorState,
+  Entity,
+  Modifier,
+  genKey,
+} from 'draft-js';
+
+var count = 0;
+var examples = [
+  '\\int_a^bu\\frac{d^2v}{dx^2}\\,dx\n' +
+  '=\\left.u\\frac{dv}{dx}\\right|_a^b\n' +
+  '-\\int_a^b\\frac{du}{dx}\\frac{dv}{dx}\\,dx',
+
+  'P(E) = {n \\choose k} p^k (1-p)^{ n-k} ',
+
+  '\\tilde f(\\omega)=\\frac{1}{2\\pi}\n' +
+  '\\int_{-\\infty}^\\infty f(x)e^{-i\\omega x}\\,dx',
+
+  '\\frac{1}{(\\sqrt{\\phi \\sqrt{5}}-\\phi) e^{\\frac25 \\pi}} =\n' +
+  '1+\\frac{e^{-2\\pi}} {1+\\frac{e^{-4\\pi}} {1+\\frac{e^{-6\\pi}}\n' +
+  '{1+\\frac{e^{-8\\pi}} {1+\\ldots} } } }',
+];
+
+export function insertTeXBlock(editorState) {
+  var contentState = editorState.getCurrentContent();
+  var selectionState = editorState.getSelection();
+
+  var afterRemoval = Modifier.removeRange(
+    contentState,
+    selectionState,
+    'backward'
+  );
+
+  var targetSelection = afterRemoval.getSelectionAfter();
+  var afterSplit = Modifier.splitBlock(afterRemoval, targetSelection);
+  var insertionTarget = afterSplit.getSelectionAfter();
+
+  var asMedia = Modifier.setBlockType(afterSplit, insertionTarget, 'media');
+  var nextFormula = count++ % examples.length;
+
+  var entityKey = Entity.create(
+    'TOKEN',
+    'IMMUTABLE',
+    {content: examples[nextFormula]}
+  );
+
+  var charData = CharacterMetadata.create({entity: entityKey});
+
+  var fragmentArray = [
+    new ContentBlock({
+      key: genKey(),
+      type: 'media',
+      text: ' ',
+      characterList: List(Repeat(charData, 1)),
+    }),
+    new ContentBlock({
+      key: genKey(),
+      type: 'unstyled',
+      text: '',
+      characterList: List(),
+    }),
+  ];
+
+  var fragment = BlockMapBuilder.createFromArray(fragmentArray);
+
+  var withMedia = Modifier.replaceWithFragment(
+    asMedia,
+    insertionTarget,
+    fragment
+  );
+
+  var newContent = withMedia.merge({
+    selectionBefore: selectionState,
+    selectionAfter: withMedia.getSelectionAfter().set('hasFocus', true),
+  });
+
+  return EditorState.push(editorState, newContent, 'insert-fragment');
+}
diff --git a/examples/tex/js/modifiers/removeTeXBlock.js b/examples/tex/js/modifiers/removeTeXBlock.js
new file mode 100644
index 0000000..63ae24b
--- /dev/null
+++ b/examples/tex/js/modifiers/removeTeXBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+'use strict';
+
+import {EditorState, Modifier, SelectionState} from 'draft-js';
+
+export function removeTeXBlock(editorState, blockKey) {
+  var content = editorState.getCurrentContent();
+  var block = content.getBlockForKey(blockKey);
+
+  var targetRange = new SelectionState({
+    anchorKey: blockKey,
+    anchorOffset: 0,
+    focusKey: blockKey,
+    focusOffset: block.getLength(),
+  });
+
+  var withoutTeX = Modifier.removeRange(content, targetRange, 'backward');
+  var resetBlock = Modifier.setBlockType(
+    withoutTeX,
+    withoutTeX.getSelectionAfter(),
+    'unstyled'
+  );
+
+  var newState = EditorState.push(editorState, resetBlock, 'remove-range');
+  return EditorState.forceSelection(newState, resetBlock.getSelectionAfter());
+}
diff --git a/examples/tex/package.json b/examples/tex/package.json
new file mode 100644
index 0000000..dcadb3a
--- /dev/null
+++ b/examples/tex/package.json
@@ -0,0 +1,22 @@
+{
+  "private": true,
+  "scripts": {
+    "start": "babel-node ./server.js",
+    "postinstall": "npm install ../.."
+  },
+  "dependencies": {
+    "babel": "5.8.23",
+    "babel-eslint": "^4.1.3",
+    "babel-loader": "5.3.2",
+    "classnames": "^2.1.3",
+    "eslint": "^1.0.0",
+    "eslint-loader": "^1.0.0",
+    "eslint-plugin-react": "^3.2.0",
+    "express": "^4.13.1",
+    "immutable": "^3.7.4",
+    "katex": "^0.5.1",
+    "react": "^0.14.0",
+    "webpack": "^1.10.5",
+    "webpack-dev-server": "^1.10.1"
+  }
+}
diff --git a/examples/tex/public/TeXEditor.css b/examples/tex/public/TeXEditor.css
new file mode 100644
index 0000000..e332f2f
--- /dev/null
+++ b/examples/tex/public/TeXEditor.css
@@ -0,0 +1,100 @@
+.TeXEditor-root {
+  font-family: 'Century Schoolbook', serif;
+  -webkit-font-smoothing: antialiased;
+  margin: 40px auto;
+  width: 900px;
+}
+
+.TeXEditor-editor {
+  cursor: text;
+  font-size: 18px;
+  min-height: 40px;
+  padding: 30px;
+}
+
+.TeXEditor-button {
+  margin-top: 10px;
+  text-align: center;
+}
+
+.TeXEditor-handle {
+  color: rgba(98, 177, 254, 1.0);
+  direction: ltr;
+  unicode-bidi: bidi-override;
+}
+
+.TeXEditor-hashtag {
+  color: rgba(95, 184, 138, 1.0);
+}
+
+.TeXEditor-tex {
+  background-color: #fff;
+  cursor: pointer;
+  margin: 20px auto;
+  padding: 20px;
+  -webkit-transition: background-color 0.2s fade-in-out;
+  user-select: none;
+  -webkit-user-select: none;
+}
+
+.TeXEditor-activeTeX {
+  color: #888;
+}
+
+.TeXEditor-panel {
+  font-family: 'Helvetica', sans-serif;
+  font-weight: 200;
+}
+
+.TeXEditor-panel .TeXEditor-texValue {
+  border: 1px solid #e1e1e1;
+  display: block;
+  font-family: 'Inconsolata', 'Menlo', monospace;
+  font-size: 14px;
+  height: 110px;
+  margin: 20px auto 10px;
+  outline: none;
+  padding: 14px;
+  resize: none;
+  -webkit-box-sizing: border-box;
+  width: 500px;
+}
+
+.TeXEditor-buttons {
+  text-align: center;
+}
+
+.TeXEditor-saveButton,
+.TeXEditor-removeButton {
+  background-color: #fff;
+  border: 1px solid #0a0;
+  cursor: pointer;
+  font-family: 'Helvetica', 'Arial', sans-serif;
+  font-size: 16px;
+  font-weight: 200;
+  margin: 10px auto;
+  padding: 6px;
+  -webkit-border-radius: 3px;
+  width: 100px;
+}
+
+.TeXEditor-removeButton {
+  border-color: #aaa;
+  color: #999;
+  margin-left: 8px;
+}
+
+.TeXEditor-invalidButton {
+  background-color: #eee;
+  border-color: #a00;
+  color: #666;
+}
+
+.TeXEditor-insert {
+  background-color: #f1f1f1;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  bottom: 30px;
+  position: fixed;
+  right: 30px;
+}
diff --git a/examples/tex/public/index.html b/examples/tex/public/index.html
new file mode 100644
index 0000000..a5a28e4
--- /dev/null
+++ b/examples/tex/public/index.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • TeX</title>
+    <link rel="stylesheet" href="TeXEditor.css" />
+    <link rel="stylesheet" href="node_modules/draft-js/dist/Draft.css" />
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.3.0/katex.min.css">
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="http://localhost:3000/webpack-dev-server.js"></script>
+    <script src="js/app.js"></script>
+  </body>
+</html>
diff --git a/examples/tex/server.js b/examples/tex/server.js
new file mode 100644
index 0000000..141d659
--- /dev/null
+++ b/examples/tex/server.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+ *
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+import express from 'express';
+import path from 'path';
+import webpack from 'webpack';
+import WebpackDevServer from 'webpack-dev-server';
+
+const APP_PORT = 3000;
+
+// Serve the TeX Editor app
+var compiler = webpack({
+  entry: path.resolve(__dirname, 'js', 'app.js'),
+  eslint: {
+    configFile: '.eslintrc'
+  },
+  module: {
+    loaders: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        loader: 'babel',
+      },
+      {
+        test: /\.js$/,
+        loader: 'eslint'
+      }
+    ]
+  },
+  output: {filename: 'app.js', path: '/'}
+});
+var app = new WebpackDevServer(compiler, {
+  contentBase: '/public/',
+  publicPath: '/js/',
+  stats: {colors: true}
+});
+// Serve static resources
+app.use('/', express.static('public'));
+app.use('/node_modules', express.static('node_modules'));
+app.listen(APP_PORT, () => {
+  console.log(`TeX Editor is now running on http://localhost:${APP_PORT}`);
+});
diff --git a/examples/tweet/tweet.html b/examples/tweet/tweet.html
new file mode 100644
index 0000000..e7e12ca
--- /dev/null
+++ b/examples/tweet/tweet.html
@@ -0,0 +1,144 @@
+<!--
+Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+
+This file provided by Facebook is for non-commercial testing and evaluation
+purposes only. Facebook reserves all rights not expressly granted.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+-->
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <title>Draft • Decorators</title>
+    <link rel="stylesheet" href="../../dist/Draft.css" />
+  </head>
+  <body>
+    <div id="target"></div>
+    <script src="../../node_modules/react/dist/react.js"></script>
+    <script src="../../node_modules/react-dom/dist/react-dom.js"></script>
+    <script src="../../node_modules/immutable/dist/immutable.js"></script>
+    <script src="../../node_modules/babel-core/browser.js"></script>
+    <script src="../../dist/Draft.js"></script>
+    <script type="text/babel">
+      'use strict';
+
+      const {CompositeDecorator, Editor, EditorState} = Draft;
+
+      class TweetEditorExample extends React.Component {
+        constructor() {
+          super();
+          const compositeDecorator = new CompositeDecorator([
+            {
+              strategy: handleStrategy,
+              component: HandleSpan,
+            },
+            {
+              strategy: hashtagStrategy,
+              component: HashtagSpan,
+            },
+          ]);
+
+          this.state = {
+            editorState: EditorState.createEmpty(compositeDecorator),
+          };
+
+          this.focus = () => this.refs.editor.focus();
+          this.onChange = (editorState) => this.setState({editorState});
+          this.logState = () => console.log(this.state.editorState.toJS());
+        }
+
+        render() {
+          return (
+            <div style={styles.root}>
+              <div style={styles.editor} onClick={this.focus}>
+                <Editor
+                  editorState={this.state.editorState}
+                  onChange={this.onChange}
+                  placeholder="Write a tweet..."
+                  ref="editor"
+                  spellCheck={true}
+                />
+              </div>
+              <input
+                onClick={this.logState}
+                style={styles.button}
+                type="button"
+                value="Log State"
+              />
+            </div>
+          );
+        }
+      }
+
+      /**
+       * Super simple decorators for handles and hashtags, for demonstration
+       * purposes only. Don't reuse these regexes.
+       */
+      const HANDLE_REGEX = /\@[\w]+/g;
+      const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
+
+      function handleStrategy(contentBlock, callback) {
+        findWithRegex(HANDLE_REGEX, contentBlock, callback);
+      }
+
+      function hashtagStrategy(contentBlock, callback) {
+        findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+      }
+
+      function findWithRegex(regex, contentBlock, callback) {
+        const text = contentBlock.getText();
+        let matchArr, start;
+        while ((matchArr = regex.exec(text)) !== null) {
+          start = matchArr.index;
+          callback(start, start + matchArr[0].length);
+        }
+      }
+
+      const HandleSpan = (props) => {
+        return <span {...props} style={styles.handle}>{props.children}</span>;
+      };
+
+      const HashtagSpan = (props) => {
+        return <span {...props} style={styles.hashtag}>{props.children}</span>;
+      };
+
+      const styles = {
+        root: {
+          fontFamily: '\'Helvetica\', sans-serif',
+          padding: 20,
+          width: 600,
+        },
+        editor: {
+          border: '1px solid #ddd',
+          cursor: 'text',
+          fontSize: 16,
+          minHeight: 40,
+          padding: 10,
+        },
+        button: {
+          marginTop: 10,
+          textAlign: 'center',
+        },
+        handle: {
+          color: 'rgba(98, 177, 254, 1.0)',
+          direction: 'ltr',
+          unicodeBidi: 'bidi-override',
+        },
+        hashtag: {
+          color: 'rgba(95, 184, 138, 1.0)',
+        },
+      };
+
+      ReactDOM.render(
+        <TweetEditorExample />,
+        document.getElementById('target')
+      );
+    </script>
+  </body>
+</html>
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..9d2b6e0
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+'use strict';
+
+var babel = require('gulp-babel');
+var del = require('del');
+var concatCSS = require('gulp-concat-css');
+var derequire = require('gulp-derequire');
+var flatten = require('gulp-flatten');
+var gulp = require('gulp');
+var gulpUtil = require('gulp-util');
+var runSequence = require('run-sequence');
+var webpackStream = require('webpack-stream');
+
+var babelOpts = require('./scripts/babel/default-options');
+var babelPluginDEV = require('fbjs-scripts/babel/dev-expression');
+var gulpCheckDependencies = require('fbjs-scripts/gulp/check-dependencies');
+
+var paths = {
+  dist: 'dist',
+  lib: 'lib',
+  src: [
+    'src/**/*.js',
+    '!src/**/__tests__/**/*.js',
+    '!src/**/__mocks__/**/*.js'
+  ],
+  css: [
+    'src/**/*.css'
+  ]
+};
+
+// Ensure that we use another plugin that isn't specified in the default Babel
+// options, converting __DEV__.
+babelOpts.plugins.push(babelPluginDEV);
+
+var buildDist = function(opts) {
+  var webpackOpts = {
+    debug: opts.debug,
+    externals: {
+      immutable: 'Immutable',
+      react: 'React',
+      'react-dom': 'ReactDOM',
+    },
+    output: {
+      filename: opts.output,
+      libraryTarget: 'var',
+      library: 'Draft'
+    },
+    plugins: [
+      new webpackStream.webpack.optimize.OccurenceOrderPlugin(),
+      new webpackStream.webpack.optimize.DedupePlugin()
+    ]
+  };
+  if (!opts.debug) {
+    webpackOpts.plugins.push(
+      new webpackStream.webpack.optimize.UglifyJsPlugin({
+        compress: {
+          hoist_vars: true,
+          screw_ie8: true,
+          warnings: false
+        }
+      })
+    );
+  }
+  return webpackStream(webpackOpts, null, function(err, stats) {
+    if (err) {
+      throw new gulpUtil.PluginError('webpack', err);
+    }
+    if (stats.compilation.errors.length) {
+      gulpUtil.log('webpack', '\n' + stats.toString({colors: true}));
+    }
+  });
+};
+
+gulp.task('clean', function() {
+  return del([paths.dist, paths.lib]);
+});
+
+gulp.task('modules', function() {
+  return gulp
+    .src(paths.src)
+    .pipe(babel(babelOpts))
+    .pipe(flatten())
+    .pipe(gulp.dest(paths.lib));
+});
+
+gulp.task('css', function() {
+  return gulp
+    .src(paths.css)
+    .pipe(concatCSS('Draft.css'))
+    .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('dist', ['modules', 'css'], function() {
+  var opts = {
+    debug: true,
+    output: 'Draft.js'
+  };
+  return gulp.src('./lib/Draft.js')
+    .pipe(buildDist(opts))
+    .pipe(derequire())
+    .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('dist:min', ['modules'], function() {
+  var opts = {
+    debug: false,
+    output: 'Draft.min.js',
+  };
+  return gulp.src('./lib/Draft.js')
+    .pipe(buildDist(opts))
+    .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('check-dependencies', function() {
+  return gulp
+    .src('package.json')
+    .pipe(gulpCheckDependencies());
+});
+
+gulp.task('watch', function() {
+  gulp.watch(paths.src, ['modules']);
+});
+
+gulp.task('default', function(cb) {
+  runSequence('check-dependencies', 'clean', 'modules', ['dist', 'dist:min'], cb);
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..257820c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,85 @@
+{
+  "name": "draft-js",
+  "private": true,
+  "description": "A React framework for building text editors.",
+  "version": "0.1.0",
+  "keywords": [
+    "draftjs",
+    "editor",
+    "react",
+    "richtext"
+  ],
+  "homepage": "https://facebook.github.io/draft-js",
+  "bugs": "https://github.com/facebook/draft-js/issues",
+  "files": [
+    "dist/",
+    "lib/",
+    "LICENSE",
+    "PATENTS"
+  ],
+  "main": "lib/Draft.js",
+  "repository": "facebook/draft-js",
+  "license": "BSD-3-Clause",
+  "scripts": {
+    "build": "gulp",
+    "lint": "eslint .",
+    "test": "NODE_ENV=test jest"
+  },
+  "dependencies": {
+    "fbjs": "^0.8.0-alpha.1",
+    "immutable": "^3.7.4"
+  },
+  "peerDependencies": {
+    "react": "^0.14.0",
+    "react-dom": "^0.14.0"
+  },
+  "devDependencies": {
+    "babel-core": "^5.8.35",
+    "babel-eslint": "^4.1.3",
+    "del": "^2.2.0",
+    "envify": "^3.4.0",
+    "eslint": "^1.5.1",
+    "eslint-plugin-react": "^3.2.2",
+    "fbjs-scripts": "^0.6.0-alpha.1",
+    "gulp": "^3.9.0",
+    "gulp-babel": "^5.1.0",
+    "gulp-browserify-thin": "^0.1.5",
+    "gulp-concat-css": "^2.2.0",
+    "gulp-derequire": "^2.1.0",
+    "gulp-flatten": "^0.2.0",
+    "gulp-uglify": "^1.2.0",
+    "gulp-util": "^3.0.6",
+    "jest-cli": "^0.9.0-fb1",
+    "object-assign": "^4.0.1",
+    "react": "^0.14.0",
+    "react-dom": "^0.14.0",
+    "run-sequence": "^1.1.2",
+    "vinyl-buffer": "^1.0.0",
+    "webpack-stream": "^3.0.0"
+  },
+  "jest": {
+    "rootDir": "",
+    "scriptPreprocessor": "scripts/jest/preprocessor.js",
+    "setupEnvScriptFile": "node_modules/fbjs-scripts/jest/environment.js",
+    "persistModuleRegistryBetweenSpecs": true,
+    "modulePathIgnorePatterns": [
+      "<rootDir>/lib/",
+      "<rootDir>/node_modules/"
+    ],
+    "preprocessorIgnorePatterns": [
+      "<rootDir>/node_modules/"
+    ],
+    "testPathDirs": [
+      "<rootDir>/src/"
+    ],
+    "testRunner": "node_modules/jest-cli/src/testRunners/jasmine/jasmine2.js",
+    "unmockedModulePathPatterns": [
+      "<rootDir>/node_modules/fbjs/node_modules/",
+      "<rootDir>/node_modules/fbjs/lib/(?!(UserAgent.js$|UserAgentData.js$))",
+      "<rootDir>/node_modules/fbjs-scripts/",
+      "<rootDir>/node_modules/immutable/",
+      "<rootDir>/node_modules/react/",
+      "<rootDir>/node_modules/react-dom/"
+    ]
+  }
+}
diff --git a/scripts/babel/default-options.js b/scripts/babel/default-options.js
new file mode 100644
index 0000000..ad5b804
--- /dev/null
+++ b/scripts/babel/default-options.js
@@ -0,0 +1,20 @@
+var assign = require('object-assign');
+var babelPluginModules = require('fbjs-scripts/babel/rewrite-modules');
+
+module.exports = {
+  blacklist: [
+    'es6.regex.unicode',
+  ],
+  nonStandard: true,
+  optional: [
+    'es7.trailingFunctionCommas',
+    'es7.classProperties',
+  ],
+  stage: 1,
+  plugins: [babelPluginModules],
+  _moduleMap: assign({}, require('fbjs/module-map'), {
+    immutable: 'immutable',
+    React: 'react',
+    ReactDOM: 'react-dom',
+  }),
+};
diff --git a/scripts/jest/preprocessor.js b/scripts/jest/preprocessor.js
new file mode 100644
index 0000000..5c571f5
--- /dev/null
+++ b/scripts/jest/preprocessor.js
@@ -0,0 +1,31 @@
+var babel = require('babel-core');
+var assign = require('object-assign');
+var babelOpts = require('../babel/default-options');
+var babelInlineRequires = require('fbjs-scripts/babel/inline-requires');
+var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
+var path = require('path');
+
+// Modify babelOpts to account for our needs. Namely we want to retain line
+// numbers for better stack traces in tests.
+babelOpts.retainLines = true;
+babelOpts._moduleMap = assign(babelOpts._moduleMap, {
+  ReactTestUtils: 'react/lib/ReactTestUtils',
+  reactComponentExpect: 'react/lib/reactComponentExpect',
+});
+babelOpts.plugins.push({
+  position: 'after',
+  transformer: babelInlineRequires,
+});
+
+var cacheKeyFunction = createCacheKeyFunction([
+  __filename,
+  path.join(__dirname, '..', '..', 'node_modules', 'fbjs', 'package.json'),
+  path.join(__dirname, '..', '..', 'node_modules', 'fbjs-scripts', 'package.json'),
+]);
+
+module.exports = {
+  process: function(src, path) {
+    return babel.transform(src, assign({filename: path}, babelOpts)).code;
+  },
+  getCacheKey: cacheKeyFunction,
+};
diff --git a/src/.flowconfig b/src/.flowconfig
new file mode 100644
index 0000000..dfd77fa
--- /dev/null
+++ b/src/.flowconfig
@@ -0,0 +1,17 @@
+[ignore]
+.*/__tests__.*
+.*/react/node_modules/.*
+.*/fbjs/node_modules/.*
+
+[include]
+../node_modules/fbjs/lib/
+../node_modules/immutable
+../node_modules/react
+
+[libs]
+../node_modules/fbjs/flow/lib
+
+[options]
+module.system=haste
+esproposal.class_static_fields=enable
+suppress_type=$FlowIssue
diff --git a/src/Draft.js b/src/Draft.js
new file mode 100644
index 0000000..80b9422
--- /dev/null
+++ b/src/Draft.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule Draft
+ */
+
+'use strict';
+
+const BlockMapBuilder = require('BlockMapBuilder');
+const CharacterMetadata = require('CharacterMetadata');
+const CompositeDraftDecorator = require('CompositeDraftDecorator');
+const ContentBlock = require('ContentBlock');
+const ContentState = require('ContentState');
+const DraftEditor = require('DraftEditor.react');
+const DraftModifier = require('DraftModifier');
+const DraftEntity = require('DraftEntity');
+const DraftEntityInstance = require('DraftEntityInstance');
+const EditorState = require('EditorState');
+const RichTextEditorUtil = require('RichTextEditorUtil');
+const SelectionState = require('SelectionState');
+
+const convertFromDraftStateToRaw = require('convertFromDraftStateToRaw');
+const convertFromRawToDraftState = require('convertFromRawToDraftState');
+const generateBlockKey = require('generateBlockKey');
+
+var DraftPublic = {
+  Editor: DraftEditor,
+  EditorState,
+
+  CompositeDecorator: CompositeDraftDecorator,
+  Entity: DraftEntity,
+  EntityInstance: DraftEntityInstance,
+
+  BlockMapBuilder,
+  CharacterMetadata,
+  ContentBlock,
+  ContentState,
+  SelectionState,
+
+  Modifier: DraftModifier,
+  RichUtils: RichTextEditorUtil,
+
+  convertFromRaw: convertFromRawToDraftState,
+  convertToRaw: convertFromDraftStateToRaw,
+  genKey: generateBlockKey,
+};
+
+module.exports = DraftPublic;
diff --git a/src/component/base/DraftEditor.css b/src/component/base/DraftEditor.css
new file mode 100644
index 0000000..552a585
--- /dev/null
+++ b/src/component/base/DraftEditor.css
@@ -0,0 +1,67 @@
+/**
+ * @providesModule DraftEditor
+ * @permanent
+ */
+
+/**
+ * We inherit the height of the container by default
+ */
+.DraftEditor-root,
+.DraftEditor-editorContainer,
+.public-DraftEditor-content {
+  height: inherit;
+  text-align: initial;
+}
+
+.DraftEditor-root {
+  position: relative;
+}
+
+/**
+ * Zero-opacity background used to allow focus in IE. Otherwise, clicks
+ * fall through to the placeholder.
+ */
+.DraftEditor-editorContainer {
+  background-color: rgba(255, 255, 255, 0);
+  /* Repair mysterious missing Safari cursor */
+  border-left: 0.1px solid transparent;
+  position: relative;
+  z-index: 1;
+}
+
+.public-DraftEditor-content {
+  outline: none;
+  white-space: pre-wrap;
+}
+
+.public-DraftEditor-block {
+  position: relative;
+}
+
+.DraftEditor-alignLeft .public-DraftEditor-block {
+  text-align: left;
+}
+
+.DraftEditor-alignLeft .public-DraftEditorPlaceholder/root {
+  left: 0;
+  text-align: left;
+}
+
+.DraftEditor-alignCenter .public-DraftEditor-block {
+  text-align: center;
+}
+
+.DraftEditor-alignCenter .public-DraftEditorPlaceholder/root {
+  margin: 0 auto;
+  text-align: center;
+  width: 100%;
+}
+
+.DraftEditor-alignRight .public-DraftEditor-block {
+  text-align: right;
+}
+
+.DraftEditor-alignRight .public-DraftEditorPlaceholder/root {
+  right: 0;
+  text-align: right;
+}
diff --git a/src/component/base/DraftEditor.react.js b/src/component/base/DraftEditor.react.js
new file mode 100644
index 0000000..bed6a54
--- /dev/null
+++ b/src/component/base/DraftEditor.react.js
@@ -0,0 +1,448 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditor.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DefaultDraftInlineStyle = require('DefaultDraftInlineStyle');
+const DraftEditorCompositionHandler = require('DraftEditorCompositionHandler');
+const DraftEditorContents = require('DraftEditorContents.react');
+const DraftEditorDragHandler = require('DraftEditorDragHandler');
+const DraftEditorEditHandler = require('DraftEditorEditHandler');
+const DraftEditorPlaceholder = require('DraftEditorPlaceholder.react');
+const EditorState = require('EditorState');
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const Scroll = require('Scroll');
+const Style = require('Style');
+const UserAgent = require('UserAgent');
+
+const cx = require('cx');
+const emptyFunction = require('emptyFunction');
+const getDefaultKeyBinding = require('getDefaultKeyBinding');
+const nullthrows = require('nullthrows');
+const getScrollPosition = require('getScrollPosition');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentBlock from 'ContentBlock';
+import type {DraftEditorModes} from 'DraftEditorModes';
+import type {DraftEditorProps} from 'DraftEditorProps';
+import type {DraftScrollPosition} from 'DraftScrollPosition';
+
+const isIE = UserAgent.isBrowser('IE');
+
+// IE does not support the `input` event on contentEditable, so we can't
+// observe spellcheck behavior.
+const allowSpellCheck = !isIE;
+
+// Define a set of handler objects to correspond to each possible `mode`
+// of editor behavior.
+const handlerMap = {
+  'edit': DraftEditorEditHandler,
+  'composite': DraftEditorCompositionHandler,
+  'drag': DraftEditorDragHandler,
+  'cut': null,
+  'render': null,
+};
+
+type DefaultProps = {
+  blockRendererFn: (block: ContentBlock) => ?Object;
+  blockStyleFn: (type: number) => string,
+  keyBindingFn: (e: SyntheticKeyboardEvent) => ?string,
+  readOnly: boolean,
+  spellCheck: boolean,
+  stripPastedStyles: boolean,
+};
+
+type State = {
+  containerKey: number,
+};
+
+/**
+ * `DraftEditor` is the root editor component. It composes a `contentEditable`
+ * div, and provides a wide variety of useful function props for managing the
+ * state of the editor. See `DraftEditorProps` for details.
+ */
+class DraftEditor
+  extends React.Component<DefaultProps, DraftEditorProps, State> {
+  state: State;
+
+  static defaultProps = {
+    blockRendererFn: emptyFunction.thatReturnsNull,
+    blockStyleFn: emptyFunction.thatReturns(''),
+    keyBindingFn: getDefaultKeyBinding,
+    readOnly: false,
+    spellCheck: false,
+    stripPastedStyles: false,
+  };
+
+  _blockSelectEvents: boolean;
+  _clipboard: ?BlockMap;
+  _guardAgainstRender: boolean;
+  _handler: ?Object;
+  _dragCount: number;
+
+  /**
+   * Define proxies that can route events to the current handler.
+   */
+  _onBeforeInput: Function;
+  _onBlur: Function;
+  _onCharacterData: Function;
+  _onCompositionEnd: Function;
+  _onCompositionStart: Function;
+  _onCopy: Function;
+  _onCut: Function;
+  _onDragEnd: Function;
+  _onDragOver: Function;
+  _onDragStart: Function;
+  _onDrop: Function;
+  _onInput: Function;
+  _onFocus: Function;
+  _onKeyDown: Function;
+  _onKeyPress: Function;
+  _onKeyUp: Function;
+  _onMouseDown: Function;
+  _onMouseUp: Function;
+  _onPaste: Function;
+  _onSelect: Function;
+
+  focus: () => void;
+  blur: () => void;
+  setMode: (mode: DraftEditorModes) => void;
+  exitCurrentMode: () => void;
+  restoreEditorDOM: (scrollPosition: DraftScrollPosition) => void;
+  setRenderGuard: () => void;
+  removeRenderGuard: () => void;
+  setClipboard: (clipboard?: BlockMap) => void;
+  getClipboard: () => ?BlockMap;
+  update: (editorState: EditorState) => void;
+  onDragEnter: () => void;
+  onDragLeave: () => void;
+
+  constructor(props: DraftEditorProps) {
+    super(props);
+
+    this._blockSelectEvents = false;
+    this._clipboard = null;
+    this._guardAgainstRender = false;
+    this._handler = null;
+    this._dragCount = 0;
+
+    this._onBeforeInput = this._buildHandler('onBeforeInput');
+    this._onBlur = this._buildHandler('onBlur');
+    this._onCharacterData = this._buildHandler('onCharacterData');
+    this._onCompositionEnd = this._buildHandler('onCompositionEnd');
+    this._onCompositionStart = this._buildHandler('onCompositionStart');
+    this._onCopy = this._buildHandler('onCopy');
+    this._onCut = this._buildHandler('onCut');
+    this._onDragEnd = this._buildHandler('onDragEnd');
+    this._onDragOver = this._buildHandler('onDragOver');
+    this._onDragStart = this._buildHandler('onDragStart');
+    this._onDrop = this._buildHandler('onDrop');
+    this._onInput = this._buildHandler('onInput');
+    this._onFocus = this._buildHandler('onFocus');
+    this._onKeyDown = this._buildHandler('onKeyDown');
+    this._onKeyPress = this._buildHandler('onKeyPress');
+    this._onKeyUp = this._buildHandler('onKeyUp');
+    this._onMouseDown = this._buildHandler('onMouseDown');
+    this._onMouseUp = this._buildHandler('onMouseUp');
+    this._onPaste = this._buildHandler('onPaste');
+    this._onSelect = this._buildHandler('onSelect');
+
+    // Manual binding for public and internal methods.
+    this.focus = this._focus.bind(this);
+    this.blur = this._blur.bind(this);
+    this.setMode = this._setMode.bind(this);
+    this.exitCurrentMode = this._exitCurrentMode.bind(this);
+    this.restoreEditorDOM = this._restoreEditorDOM.bind(this);
+    this.setRenderGuard = this._setRenderGuard.bind(this);
+    this.removeRenderGuard = this._removeRenderGuard.bind(this);
+    this.setClipboard = this._setClipboard.bind(this);
+    this.getClipboard = this._getClipboard.bind(this);
+    this.update = this._update.bind(this);
+    this.onDragEnter = this._onDragEnter.bind(this);
+    this.onDragLeave = this._onDragLeave.bind(this);
+
+    // See `_restoreEditorDOM()`.
+    this.state = {containerKey: 0};
+  }
+
+  /**
+   * Build a method that will pass the event to the specified handler method.
+   * This allows us to look up the correct handler function for the current
+   * editor mode, if any has been specified.
+   */
+  _buildHandler(eventName: string): Function {
+    return (e) => {
+      if (!this.props.readOnly) {
+        const method = this._handler && this._handler[eventName];
+        method && method.call(this, e);
+      }
+    };
+  }
+
+  _renderPlaceholder(): ?ReactElement {
+    const content = this.props.editorState.getCurrentContent();
+    const showPlaceholder = (
+      this.props.placeholder &&
+      !this.props.editorState.isInCompositionMode() &&
+      !content.hasText()
+    );
+
+    if (showPlaceholder) {
+      return (
+        <DraftEditorPlaceholder
+          text={nullthrows(this.props.placeholder)}
+          editorState={this.props.editorState}
+          textAlignment={this.props.textAlignment}
+        />
+      );
+    }
+  }
+
+  render(): ReactElement {
+    const {readOnly, textAlignment} = this.props;
+    const rootClass = cx({
+      'DraftEditor/root': true,
+      'DraftEditor/alignLeft': textAlignment === 'left',
+      'DraftEditor/alignRight': textAlignment === 'right',
+      'DraftEditor/alignCenter': textAlignment === 'center',
+    });
+    const hasContent = this.props.editorState.getCurrentContent().hasText();
+
+    return (
+      <div className={rootClass}>
+        {this._renderPlaceholder()}
+        <div
+          className={cx('DraftEditor/editorContainer')}
+          key={'editor' + this.state.containerKey}
+          ref="editorContainer">
+          <div
+            aria-activedescendant={
+              readOnly ? null : this.props.ariaActiveDescendantID
+            }
+            aria-autocomplete={readOnly ? null : this.props.ariaAutoComplete}
+            aria-describedby={this.props.ariaDescribedBy}
+            aria-expanded={readOnly ? null : this.props.ariaExpanded}
+            aria-haspopup={readOnly ? null : this.props.ariaHasPopup}
+            aria-label={this.props.ariaLabel}
+            aria-owns={readOnly ? null : this.props.ariaOwneeID}
+            className={cx('public/DraftEditor/content')}
+            contentEditable={!readOnly}
+            data-testid={this.props.webDriverTestID}
+            onBeforeInput={this._onBeforeInput}
+            onBlur={this._onBlur}
+            onCompositionEnd={this._onCompositionEnd}
+            onCompositionStart={this._onCompositionStart}
+            onCopy={this._onCopy}
+            onCut={this._onCut}
+            onDragEnd={this._onDragEnd}
+            onDragEnter={this.onDragEnter}
+            onDragLeave={this.onDragLeave}
+            onDragOver={this._onDragOver}
+            onDragStart={this._onDragStart}
+            onDrop={this._onDrop}
+            onFocus={this._onFocus}
+            onInput={this._onInput}
+            onKeyDown={this._onKeyDown}
+            onKeyPress={this._onKeyPress}
+            onKeyUp={this._onKeyUp}
+            onMouseUp={this._onMouseUp}
+            onPaste={this._onPaste}
+            onSelect={this._onSelect}
+            ref="editor"
+            role={readOnly ? null : (this.props.role || 'textbox')}
+            spellCheck={allowSpellCheck && this.props.spellCheck}
+            tabIndex={this.props.tabIndex}
+            title={hasContent ? null : this.props.placeholder}>
+            <DraftEditorContents
+              blockRendererFn={nullthrows(this.props.blockRendererFn)}
+              blockStyleFn={nullthrows(this.props.blockStyleFn)}
+              customStyleMap={
+                {...DefaultDraftInlineStyle, ...this.props.customStyleMap}
+              }
+              editorState={this.props.editorState}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  componentDidMount(): void {
+    this.setMode('edit');
+
+    /**
+     * IE has a hardcoded "feature" that attempts to convert link text into
+     * anchors in contentEditable DOM. This breaks the editor's expectations of
+     * the DOM, and control is lost. Disable it to make IE behave.
+     * See: http://blogs.msdn.com/b/ieinternals/archive/2010/09/15/
+     * ie9-beta-minor-change-list.aspx
+     */
+    if (isIE) {
+      document.execCommand('AutoUrlDetect', false, false);
+    }
+  }
+
+  /**
+   * Prevent selection events from affecting the current editor state. This
+   * is mostly intended to defend against IE, which fires off `selectionchange`
+   * events regardless of whether the selection is set via the browser or
+   * programmatically. We only care about selection events that occur because
+   * of browser interaction, not re-renders and forced selections.
+   */
+  componentWillUpdate(): void {
+    this._blockSelectEvents = true;
+  }
+
+  componentDidUpdate(): void {
+    this._blockSelectEvents = false;
+  }
+
+  /**
+   * Used via `this.focus()`.
+   *
+   * Force focus back onto the editor node.
+   *
+   * Forcing focus causes the browser to scroll to the top of the editor, which
+   * may be undesirable when the editor is taller than the viewport. To solve
+   * this, either use a specified scroll position (in cases like `cut` behavior
+   * where it should be restored to a known position) or store the current
+   * scroll state and put it back in place after focus has been forced.
+   */
+  _focus(scrollPosition?: DraftScrollPosition): void {
+    const {editorState} = this.props;
+    const alreadyHasFocus = editorState.getSelection().getHasFocus();
+    const editorNode = ReactDOM.findDOMNode(this.refs.editor);
+
+    const scrollParent = Style.getScrollParent(editorNode);
+    const {x, y} = scrollPosition || getScrollPosition(scrollParent);
+
+    editorNode.focus();
+    if (scrollParent === window) {
+      window.scrollTo(x, y);
+    } else {
+      Scroll.setTop(scrollParent, y);
+    }
+
+    // On Chrome and Safari, calling focus on contenteditable focuses the
+    // cursor at the first character. This is something you don't expect when
+    // you're clicking on an input element but not directly on a character.
+    // Put the cursor back where it was before the blur.
+    if (!alreadyHasFocus) {
+      this.update(
+        EditorState.forceSelection(
+          editorState,
+          editorState.getSelection()
+        )
+      );
+    }
+  }
+
+  _blur(): void {
+    ReactDOM.findDOMNode(this.refs.editor).blur();
+  }
+
+  /**
+   * Used via `this.setMode(...)`.
+   *
+   * Set the behavior mode for the editor component. This switches the current
+   * handler module to ensure that DOM events are managed appropriately for
+   * the active mode.
+   */
+  _setMode(mode: DraftEditorModes): void {
+    this._handler = handlerMap[mode];
+  }
+
+  _exitCurrentMode(): void {
+    this.setMode('edit');
+  }
+
+  /**
+   * Used via `this.restoreEditorDOM()`.
+   *
+   * Force a complete re-render of the editor based on the current EditorState.
+   * This is useful when we know we are going to lose control of the DOM
+   * state (cut command, IME) and we want to make sure that reconciliation
+   * occurs on a version of the DOM that is synchronized with our EditorState.
+   */
+  _restoreEditorDOM(scrollPosition?: DraftScrollPosition): void {
+    this.setState({containerKey: this.state.containerKey + 1}, () => {
+      this._focus(scrollPosition);
+    });
+  }
+
+  /**
+   * Guard against rendering. Intended for use when we need to manually
+   * reset editor contents, to ensure that no outside influences lead to
+   * React reconciliation when we are in an uncertain state.
+   */
+  _setRenderGuard(): void {
+    this._guardAgainstRender = true;
+  }
+
+  _removeRenderGuard(): void {
+    this._guardAgainstRender = false;
+  }
+
+  /**
+   * Used via `this.setClipboard(...)`.
+   *
+   * Set the clipboard state for a cut/copy event.
+   */
+  _setClipboard(clipboard: ?BlockMap): void {
+    this._clipboard = clipboard;
+  }
+
+  /**
+   * Used via `this.getClipboard()`.
+   *
+   * Retrieve the clipboard state for a cut/copy event.
+   */
+  _getClipboard(): ?BlockMap {
+    return this._clipboard;
+  }
+
+  /**
+   * Used via `this.update(...)`.
+   *
+   * Propagate a new `EditorState` object to higher-level components. This is
+   * the method by which event handlers inform the `DraftEditor` component of
+   * state changes. A component that composes a `DraftEditor` **must** provide
+   * an `onChange` prop to receive state updates passed along from this
+   * function.
+   */
+  _update(editorState: EditorState): void {
+    this.props.onChange(editorState);
+  }
+
+  /**
+   * Used in conjunction with `_onDragLeave()`, by counting the number of times
+   * a dragged element enters and leaves the editor (or any of its children),
+   * to determine when the dragged element absolutely leaves the editor.
+   */
+  _onDragEnter(): void {
+    this._dragCount++;
+  }
+
+  /**
+   * See `_onDragEnter()`.
+   */
+  _onDragLeave(): void {
+    this._dragCount--;
+    if (this._dragCount === 0) {
+      this.exitCurrentMode();
+    }
+  }
+}
+
+module.exports = DraftEditor;
diff --git a/src/component/base/DraftEditorPlaceholder.css b/src/component/base/DraftEditorPlaceholder.css
new file mode 100644
index 0000000..9c218f9
--- /dev/null
+++ b/src/component/base/DraftEditorPlaceholder.css
@@ -0,0 +1,17 @@
+/**
+ * @providesModule DraftEditorPlaceholder
+ */
+
+.public-DraftEditorPlaceholder-root {
+  color: #9197a3;
+  position: absolute;
+  z-index: 0;
+}
+
+.public-DraftEditorPlaceholder-hasFocus {
+  color: #bdc1c9;
+}
+
+.DraftEditorPlaceholder-hidden {
+  display: none;
+}
diff --git a/src/component/base/DraftEditorPlaceholder.react.js b/src/component/base/DraftEditorPlaceholder.react.js
new file mode 100644
index 0000000..8364252
--- /dev/null
+++ b/src/component/base/DraftEditorPlaceholder.react.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorPlaceholder.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const React = require('React');
+
+const cx = require('cx');
+
+import type {DraftTextAlignment} from 'DraftTextAlignment';
+import type EditorState from 'EditorState';
+
+type Props = {
+  editorState: EditorState,
+  text: string,
+  textAlignment: DraftTextAlignment,
+};
+
+/**
+ * This component is responsible for rendering placeholder text for the
+ * `DraftEditor` component.
+ *
+ * Override placeholder style via CSS.
+ */
+class DraftEditorPlaceholder extends React.Component {
+  shouldComponentUpdate(nextProps: Props): boolean {
+    return (
+      this.props.text !== nextProps.text ||
+      (
+        this.props.editorState.getSelection().getHasFocus() !==
+        nextProps.editorState.getSelection().getHasFocus()
+      )
+    );
+  }
+
+  render(): ReactElement {
+    const hasFocus = this.props.editorState.getSelection().getHasFocus();
+
+    const className = cx({
+      'public/DraftEditorPlaceholder/root': true,
+      'public/DraftEditorPlaceholder/hasFocus': hasFocus,
+    });
+
+    return (
+      <div className={className}>
+        <div className={cx('public/DraftEditorPlaceholder/inner')}>
+          {this.props.text}
+        </div>
+      </div>
+    );
+  }
+}
+
+module.exports = DraftEditorPlaceholder;
diff --git a/src/component/base/DraftEditorProps.js b/src/component/base/DraftEditorProps.js
new file mode 100644
index 0000000..6825716
--- /dev/null
+++ b/src/component/base/DraftEditorProps.js
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorProps
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+import type {DraftTextAlignment} from 'DraftTextAlignment';
+import type EditorState from 'EditorState';
+
+export type DraftEditorProps = {
+  /**
+   * The two most critical props are `editorState` and `onChange`.
+   *
+   * The `editorState` prop defines the entire state of the editor, while the
+   * `onChange` prop is the method in which all state changes are propagated
+   * upward to higher-level components.
+   *
+   * These props are analagous to `value` and `onChange` in controlled React
+   * text inputs.
+   */
+  editorState: EditorState,
+  onChange: (editorState: EditorState) => void,
+
+  placeholder?: string,
+
+  // Specify whether text alignment should be forced in a direction
+  // regardless of input characters.
+  textAlignment?: DraftTextAlignment,
+
+  // For a given `ContentBlock` object, return an object that specifies
+  // a custom block component and/or props. If no object is returned,
+  // the default `TextEditorBlock` is used.
+  blockRendererFn?: (block: ContentBlock) => ?Object,
+
+  // Function that returns a cx map corresponding to block-level styles.
+  blockStyleFn?: (type: number) => string,
+
+  // A function that accepts a synthetic key event and returns
+  // the matching DraftEditorCommand constant, or null if no command should
+  // be invoked.
+  keyBindingFn?: (e: SyntheticKeyboardEvent) => ?string,
+
+  // Set whether the `DraftEditor` component should be editable. Useful for
+  // temporarily disabling edit behavior or allowing `DraftEditor` rendering
+  // to be used for consumption purposes.
+  readOnly?: boolean,
+
+  // Note: spellcheck is always disabled for IE. If enabled in Safari, OSX
+  // autocorrect is enabled as well.
+  spellCheck?: boolean,
+
+  // Set whether to remove all style information from pasted content. If your
+  // use case should not have any block or inline styles, it is recommended
+  // that you set this to `true`.
+  stripPastedStyles?: boolean,
+
+  tabIndex?: number,
+
+  ariaActiveDescendantID?: string,
+  ariaAutoComplete?: string,
+  ariaDescribedBy?: string,
+  ariaExpanded?: boolean,
+  ariaHasPopup?: boolean,
+  ariaLabel?: string,
+  ariaOwneeID?: string,
+
+  webDriverTestID?: string,
+
+  /**
+   * Cancelable event handlers, handled from the top level down. A handler
+   * that returns true will be the last handler to execute for that event.
+   */
+
+  // Useful for managing special behavior for pressing the `Return` key. E.g.
+  // removing the style from an empty list item.
+  handleReturn?: (e: SyntheticKeyboardEvent) => boolean,
+
+  // Map a key command string provided by your key binding function to a
+  // specified behavior.
+  handleKeyCommand?: (command: DraftEditorCommand) => boolean,
+
+  // Handle intended text insertion before the insertion occurs. This may be
+  // useful in cases where the user has entered characters that you would like
+  // to trigger some special behavior. E.g. immediately converting `:)` to an
+  // emoji Unicode character, or replacing ASCII quote characters with smart
+  // quotes.
+  handleBeforeInput?: (e: SyntheticInputEvent) => boolean,
+
+  handlePastedFiles?: (files: Array<Blob>) => boolean,
+
+  /**
+   * Non-cancelable event triggers.
+   */
+  onEscape?: (e: SyntheticKeyboardEvent) => void,
+  onTab?: (e: SyntheticKeyboardEvent) => void,
+  onUpArrow?: (e: SyntheticKeyboardEvent) => void,
+  onDownArrow?: (e: SyntheticKeyboardEvent) => void,
+  onPasteRawText?: (text: string) => void,
+
+  onBlur?: (e: SyntheticEvent) => void,
+  onFocus?: (e: SyntheticEvent) => void,
+
+  // Provide a map of inline style names corresponding to CSS style objects
+  // that will be rendered for matching ranges.
+  customStyleMap?: Object,
+};
diff --git a/src/component/base/DraftScrollPosition.js b/src/component/base/DraftScrollPosition.js
new file mode 100644
index 0000000..45f1038
--- /dev/null
+++ b/src/component/base/DraftScrollPosition.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftScrollPosition
+ * @flow
+ */
+
+'use strict';
+
+export type DraftScrollPosition = {
+  x: number,
+  y: number,
+};
diff --git a/src/component/base/DraftTextAlignment.js b/src/component/base/DraftTextAlignment.js
new file mode 100644
index 0000000..c24d7ed
--- /dev/null
+++ b/src/component/base/DraftTextAlignment.js
@@ -0,0 +1,15 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftTextAlignment
+ * @flow
+ */
+
+'use strict';
+
+export type DraftTextAlignment = 'left' | 'center' | 'right';
diff --git a/src/component/contents/DraftEditorBlock.react.js b/src/component/contents/DraftEditorBlock.react.js
new file mode 100644
index 0000000..0fda395
--- /dev/null
+++ b/src/component/contents/DraftEditorBlock.react.js
@@ -0,0 +1,224 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorBlock.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const ContentBlock = require('ContentBlock');
+const DraftEditorLeaf = require('DraftEditorLeaf.react');
+const DraftOffsetKey = require('DraftOffsetKey');
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const Scroll = require('Scroll');
+const SelectionState = require('SelectionState');
+const Style = require('Style');
+const UnicodeBidi = require('UnicodeBidi');
+const UnicodeBidiDirection = require('UnicodeBidiDirection');
+
+const cx = require('cx');
+const getElementPosition = require('getElementPosition');
+const getScrollPosition = require('getScrollPosition');
+const getViewportDimensions = require('getViewportDimensions');
+const nullthrows = require('nullthrows');
+
+import type {BidiDirection} from 'UnicodeBidiDirection';
+import type {DraftDecoratorType} from 'DraftDecoratorType';
+import type {List} from 'immutable';
+
+const SCROLL_BUFFER = 10;
+
+type Props = {
+  block: ContentBlock,
+  customStyleMap: Object,
+  tree: List,
+  selection: SelectionState,
+  decorator: DraftDecoratorType,
+  forceSelection: boolean,
+  direction: BidiDirection,
+  blockProps?: Object,
+  startIndent?: boolean,
+  blockStyleFn: Function,
+};
+
+/**
+ * The default block renderer for a `DraftEditor` component.
+ *
+ * A `DraftEditorBlock` is able to render a given `ContentBlock` to its
+ * appropriate decorator and inline style components.
+ */
+class DraftEditorBlock extends React.Component {
+  shouldComponentUpdate(nextProps: Props): boolean {
+    return (
+      this.props.block !== nextProps.block ||
+      this.props.tree !== nextProps.tree ||
+      this.props.direction !== nextProps.direction ||
+      (
+        isBlockOnSelectionEdge(
+          nextProps.selection,
+          nextProps.block.getKey()
+        ) &&
+        nextProps.forceSelection
+      )
+    );
+  }
+
+  /**
+   * When a block is mounted and overlaps the selection state, we need to make
+   * sure that the cursor is visible to match native behavior. This may not
+   * be the case if the user has pressed `RETURN` or pasted some content, since
+   * programatically creating these new blocks and setting the DOM selection
+   * will miss out on the browser natively scrolling to that position.
+   *
+   * To replicate native behavior, if the block overlaps the selection state
+   * on mount, force the scroll position. Check the scroll state of the scroll
+   * parent, and adjust it to align the entire block to the bottom of the
+   * scroll parent.
+   */
+  componentDidMount(): void {
+    var selection = this.props.selection;
+    var endKey = selection.getEndKey();
+    if (!selection.getHasFocus() || endKey !== this.props.block.getKey()) {
+      return;
+    }
+
+    var blockNode = ReactDOM.findDOMNode(this);
+    var scrollParent = Style.getScrollParent(blockNode);
+    var scrollPosition = getScrollPosition(scrollParent);
+    var scrollDelta;
+
+    if (scrollParent === window) {
+      var nodePosition = getElementPosition(blockNode);
+      var nodeBottom = nodePosition.y + nodePosition.height;
+      var viewportHeight = getViewportDimensions().height;
+      scrollDelta = nodeBottom - viewportHeight;
+      if (scrollDelta > 0) {
+        window.scrollTo(
+          scrollPosition.x,
+          scrollPosition.y + scrollDelta + SCROLL_BUFFER
+        );
+      }
+    } else {
+      var blockBottom = blockNode.offsetHeight + blockNode.offsetTop;
+      var scrollBottom = scrollParent.offsetHeight + scrollPosition.y;
+      scrollDelta = blockBottom - scrollBottom;
+      if (scrollDelta > 0) {
+        Scroll.setTop(
+          scrollParent,
+          Scroll.getTop(scrollParent) + scrollDelta + SCROLL_BUFFER
+        );
+      }
+    }
+  }
+
+  _renderChildren(): Array<ReactElement> {
+    var block = this.props.block;
+    var blockKey = block.getKey();
+    var text = block.getText();
+    var lastLeafSet = this.props.tree.size - 1;
+    var hasSelection = isBlockOnSelectionEdge(this.props.selection, blockKey);
+
+    return this.props.tree.map((leafSet, ii) => {
+      var leavesForLeafSet = leafSet.get('leaves');
+      var lastLeaf = leavesForLeafSet.size - 1;
+      var leaves = leavesForLeafSet.map((leaf, jj) => {
+        var offsetKey = DraftOffsetKey.encode(blockKey, ii, jj);
+        var start = leaf.get('start');
+        var end = leaf.get('end');
+        return (
+          <DraftEditorLeaf
+            key={offsetKey}
+            offsetKey={offsetKey}
+            blockKey={blockKey}
+            start={start}
+            selection={hasSelection ? this.props.selection : undefined}
+            forceSelection={this.props.forceSelection}
+            text={text.slice(start, end)}
+            styleSet={block.getInlineStyleAt(start)}
+            customStyleMap={this.props.customStyleMap}
+            isLast={ii === lastLeafSet && jj === lastLeaf}
+          />
+        );
+      }).toArray();
+
+      var decoratorKey = leafSet.get('decoratorKey');
+      if (decoratorKey == null) {
+        return leaves;
+      }
+
+      if (!this.props.decorator) {
+        return leaves;
+      }
+
+      var decorator = nullthrows(this.props.decorator);
+
+      var DecoratorComponent = decorator.getComponentForKey(decoratorKey);
+      if (!DecoratorComponent) {
+        return leaves;
+      }
+
+      var decoratorProps = decorator.getPropsForKey(decoratorKey);
+      var decoratorOffsetKey = DraftOffsetKey.encode(blockKey, ii, 0);
+      var decoratedText = text.slice(
+        leavesForLeafSet.first().get('start'),
+        leavesForLeafSet.last().get('end')
+      );
+
+      // Resetting dir to the same value on a child node makes Chrome/Firefox
+      // confused on cursor movement. See http://jsfiddle.net/d157kLck/3/
+      var dir = UnicodeBidiDirection.getHTMLDirIfDifferent(
+        UnicodeBidi.getDirection(decoratedText),
+        this.props.direction,
+      );
+
+      return (
+        <DecoratorComponent
+          {...decoratorProps}
+          dir={dir}
+          key={decoratorOffsetKey}
+          entityKey={block.getEntityAt(leafSet.get('start'))}
+          offsetKey={decoratorOffsetKey}>
+          {leaves}
+        </DecoratorComponent>
+      );
+    }).toArray();
+  }
+
+  render(): ReactElement {
+    const {direction, offsetKey} = this.props;
+    const className = cx({
+      'public/DraftStyleDefault/block': true,
+      'public/DraftStyleDefault/ltr': direction === 'LTR',
+      'public/DraftStyleDefault/rtl': direction === 'RTL',
+    });
+
+    return (
+      <div data-offset-key={offsetKey} className={className}>
+        {this._renderChildren()}
+      </div>
+    );
+  }
+}
+
+/**
+ * Return whether a block overlaps with either edge of the `SelectionState`.
+ */
+function isBlockOnSelectionEdge(
+  selection: SelectionState,
+  key: string
+): boolean {
+  return (
+    selection.getAnchorKey() === key ||
+    selection.getFocusKey() === key
+  );
+}
+
+module.exports = DraftEditorBlock;
diff --git a/src/component/contents/DraftEditorContents.react.js b/src/component/contents/DraftEditorContents.react.js
new file mode 100644
index 0000000..b4d1a95
--- /dev/null
+++ b/src/component/contents/DraftEditorContents.react.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorContents.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DraftEditorBlock = require('DraftEditorBlock.react');
+const DraftOffsetKey = require('DraftOffsetKey');
+const EditorState = require('EditorState');
+const React = require('React');
+
+const cx = require('cx');
+const getElementForBlockType = require('getElementForBlockType');
+const getWrapperTemplateForBlockType = require('getWrapperTemplateForBlockType');
+const joinClasses = require('joinClasses');
+const nullthrows = require('nullthrows');
+
+import type {BidiDirection} from 'UnicodeBidiDirection';
+import type ContentBlock from 'ContentBlock';
+
+type Props = {
+  blockRendererFn: Function,
+  blockStyleFn: (block: ContentBlock) => string,
+  editorState: EditorState,
+};
+
+/**
+ * `DraftEditorContents` is the container component for all block components
+ * rendered for a `DraftEditor`. It is optimized to aggressively avoid
+ * re-rendering blocks whenever possible.
+ *
+ * This component is separate from `DraftEditor` because certain props
+ * (for instance, ARIA props) must be allowed to update without affecting
+ * the contents of the editor.
+ */
+class DraftEditorContents extends React.Component {
+  shouldComponentUpdate(nextProps: Props): boolean {
+    const prevEditorState = this.props.editorState;
+    const nextEditorState = nextProps.editorState;
+
+    const prevDirectionMap = prevEditorState.getDirectionMap();
+    const nextDirectionMap = nextEditorState.getDirectionMap();
+
+    // Text direction has changed for one or more blocks. We must re-render.
+    if (prevDirectionMap !== nextDirectionMap) {
+      return true;
+    }
+
+    const didHaveFocus = prevEditorState.getSelection().getHasFocus();
+    const nowHasFocus = nextEditorState.getSelection().getHasFocus();
+
+    if (didHaveFocus !== nowHasFocus) {
+      return true;
+    }
+
+    const nextNativeContent = nextEditorState.getNativelyRenderedContent();
+
+    const wasComposing = prevEditorState.isInCompositionMode();
+    const nowComposing = nextEditorState.isInCompositionMode();
+
+    // If the state is unchanged or we're currently rendering a natively
+    // rendered state, there's nothing new to be done.
+    if (
+      prevEditorState === nextEditorState ||
+      (
+        nextNativeContent !== null &&
+        nextEditorState.getCurrentContent() === nextNativeContent
+      ) ||
+      (wasComposing && nowComposing)
+    ) {
+      return false;
+    }
+
+    const prevContent = prevEditorState.getCurrentContent();
+    const nextContent = nextEditorState.getCurrentContent();
+    const prevDecorator = prevEditorState.getDecorator();
+    const nextDecorator = nextEditorState.getDecorator();
+    return (
+      wasComposing !== nowComposing ||
+      prevContent !== nextContent ||
+      prevDecorator !== nextDecorator ||
+      nextEditorState.mustForceSelection()
+    );
+  }
+
+  render(): ReactElement {
+    const {blockRendererFn, customStyleMap, editorState} = this.props;
+    const content = editorState.getCurrentContent();
+    const selection = editorState.getSelection();
+    const forceSelection = editorState.mustForceSelection();
+    const decorator = editorState.getDecorator();
+    const directionMap = nullthrows(editorState.getDirectionMap());
+
+    const blocksAsArray = content.getBlocksAsArray();
+    const blocks = [];
+    let currentWrapperElement = null;
+    let currentWrapperTemplate = null;
+    let currentDepth = null;
+    let currentWrappedBlocks;
+    let block, key, blockType, child, wrapperTemplate;
+
+    for (let ii = 0; ii < blocksAsArray.length; ii++) {
+      block = blocksAsArray[ii];
+      key = block.getKey();
+      blockType = block.getType();
+
+      const customRenderer = blockRendererFn(block);
+      let CustomComponent, customProps;
+      if (customRenderer) {
+        CustomComponent = customRenderer.component;
+        customProps = customRenderer.props;
+      }
+
+      const direction = directionMap.get(key);
+      const offsetKey = DraftOffsetKey.encode(key, 0, 0);
+      const componentProps = {
+        block,
+        blockProps: customProps,
+        customStyleMap,
+        decorator,
+        direction,
+        forceSelection,
+        key,
+        offsetKey,
+        selection,
+        tree: editorState.getBlockTree(key),
+      };
+
+      wrapperTemplate = getWrapperTemplateForBlockType(blockType);
+      const useNewWrapper = wrapperTemplate !== currentWrapperTemplate;
+
+      if (CustomComponent) {
+        child = <CustomComponent {...componentProps} />;
+      } else {
+        const Element = getElementForBlockType(blockType);
+        const depth = block.getDepth();
+        let className = this.props.blockStyleFn(block);
+
+        // List items are special snowflakes, since we handle nesting and
+        // counters manually.
+        if (Element === 'li') {
+          const shouldResetCount = (
+            useNewWrapper ||
+            currentDepth === null ||
+            depth > currentDepth
+          );
+          className = joinClasses(
+            className,
+            getListItemClasses(blockType, depth, shouldResetCount, direction)
+          );
+        }
+
+        /* $FlowFixMe - Support DOM elements in React.createElement */
+        child = React.createElement(
+          Element,
+          {
+            className,
+            'data-block': true,
+            'data-offset-key': offsetKey,
+            key,
+          },
+          <DraftEditorBlock {...componentProps} />,
+        );
+      }
+
+      if (wrapperTemplate) {
+        if (useNewWrapper) {
+          currentWrappedBlocks = [];
+          currentWrapperElement = React.cloneElement(
+            wrapperTemplate,
+            {
+              key: key + '-wrap',
+              'data-offset-key': offsetKey,
+            },
+            currentWrappedBlocks
+          );
+          currentWrapperTemplate = wrapperTemplate;
+          blocks.push(currentWrapperElement);
+        }
+        currentDepth = block.getDepth();
+        nullthrows(currentWrappedBlocks).push(child);
+      } else {
+        currentWrappedBlocks = null;
+        currentWrapperElement = null;
+        currentWrapperTemplate = null;
+        currentDepth = null;
+        blocks.push(child);
+      }
+    }
+
+    return <div data-contents="true">{blocks}</div>;
+  }
+}
+
+/**
+ * Provide default styling for list items. This way, lists will be styled with
+ * proper counters and indentation even if the caller does not specify
+ * their own styling at all. If more than five levels of nesting are needed,
+ * the necessary CSS classes can be provided via `blockStyleFn` configuration.
+ */
+function getListItemClasses(
+  type: string,
+  depth: number,
+  shouldResetCount: boolean,
+  direction: BidiDirection
+): string {
+  return cx({
+    'public/DraftStyleDefault/unorderedListItem':
+      type === 'unordered-list-item',
+    'public/DraftStyleDefault/orderedListItem':
+      type === 'ordered-list-item',
+    'public/DraftStyleDefault/reset': shouldResetCount,
+    'public/DraftStyleDefault/depth0': depth === 0,
+    'public/DraftStyleDefault/depth1': depth === 1,
+    'public/DraftStyleDefault/depth2': depth === 2,
+    'public/DraftStyleDefault/depth3': depth === 3,
+    'public/DraftStyleDefault/depth4': depth === 4,
+    'public/DraftStyleDefault/listLTR': direction === 'LTR',
+    'public/DraftStyleDefault/listRTL': direction === 'RTL',
+  });
+}
+
+module.exports = DraftEditorContents;
diff --git a/src/component/contents/DraftEditorLeaf.react.js b/src/component/contents/DraftEditorLeaf.react.js
new file mode 100644
index 0000000..cc907d6
--- /dev/null
+++ b/src/component/contents/DraftEditorLeaf.react.js
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorLeaf.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftEditorTextNode = require('DraftEditorTextNode.react');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var SelectionState = require('SelectionState');
+
+var setDraftEditorSelection = require('setDraftEditorSelection');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+type Props = {
+  // A function passed through from the the top level to define a cx
+  // style map for the provided style value.
+  blockKey: string,
+
+  // Mapping of style names to CSS declarations.
+  customStyleMap: Object,
+
+  // Whether to force the DOM selection after render.
+  forceSelection: boolean,
+
+  // Whether this leaf is the last in its block. Used for a DOM hack.
+  isLast: boolean,
+
+  offsetKey: string,
+
+  // The current `SelectionState`, used to
+  selection: SelectionState,
+
+  // The offset of this string within its block.
+  start: number,
+
+  // The set of style(s) names to apply to the node.
+  styleSet: DraftInlineStyle,
+
+  // The full text to be rendered within this node.
+  text: string,
+};
+
+/**
+ * All leaf nodes in the editor are spans with single text nodes. Leaf
+ * elements are styled based on the merging of an optional custom style map
+ * and a default style map.
+ *
+ * `DraftEditorLeaf` also provides a wrapper for calling into the imperative
+ * DOM Selection API. In this way, top-level components can declaratively
+ * maintain the selection state.
+ */
+class DraftEditorLeaf extends React.Component {
+  /**
+   * By making individual leaf instances aware of their context within
+   * the text of the editor, we can set our selection range more
+   * easily than we could in the non-React world.
+   *
+   * Note that this depends on our maintaining tight control over the
+   * DOM structure of the TextEditor component. If leaves had multiple
+   * text nodes, this would be harder.
+   */
+  _setSelection(): void {
+    const {selection} = this.props;
+
+    // If selection state is irrelevant to the parent block, no-op.
+    if (selection == null || !selection.getHasFocus()) {
+      return;
+    }
+
+    const {blockKey, start, text} = this.props;
+    const end = start + text.length;
+    if (!selection.hasEdgeWithin(blockKey, start, end)) {
+      return;
+    }
+
+    // Determine the appropriate target node for selection. If the child
+    // is not a text node, it is a <br /> spacer. In this case, use the
+    // <span> itself as the selection target.
+    const node = ReactDOM.findDOMNode(this);
+    const child = node.firstChild;
+    let targetNode;
+
+    if (child.nodeType === Node.TEXT_NODE) {
+      targetNode = child;
+    } else if (child.tagName === 'BR') {
+      targetNode = node;
+    } else {
+      targetNode = child.firstChild;
+    }
+
+    setDraftEditorSelection(selection, targetNode, blockKey, start, end);
+  }
+
+  shouldComponentUpdate(nextProps: Props): boolean {
+    return (
+      ReactDOM.findDOMNode(this.refs.leaf).textContent !== nextProps.text ||
+      nextProps.forceSelection
+    );
+  }
+
+  componentDidUpdate(): void {
+    this._setSelection();
+  }
+
+  componentDidMount(): void {
+    this._setSelection();
+  }
+
+  render(): ReactElement {
+    let {text} = this.props;
+
+    // If the leaf is at the end of its block and ends in a soft newline, append
+    // an extra line feed character. Browsers collapse trailing newline
+    // characters, which leaves the cursor in the wrong place after a
+    // shift+enter. The extra character repairs this.
+    if (text.endsWith('\n') && this.props.isLast) {
+      text += '\n';
+    }
+
+    const {customStyleMap, offsetKey, styleSet} = this.props;
+    const styleObj = styleSet.reduce((map, styleName) => {
+      const mergedStyles = {};
+      const style = customStyleMap[styleName];
+
+      if (
+        style !== undefined &&
+        map.textDecoration !== style.textDecoration
+      ) {
+        mergedStyles.textDecoration =
+          [map.textDecoration, style.textDecoration].join(' ');
+      }
+
+      return Object.assign(map, style, mergedStyles);
+    }, {});
+
+    return (
+      <span
+        data-offset-key={offsetKey}
+        ref="leaf"
+        style={styleObj}>
+        <DraftEditorTextNode>{text}</DraftEditorTextNode>
+      </span>
+    );
+  }
+}
+
+module.exports = DraftEditorLeaf;
diff --git a/src/component/contents/DraftEditorTextNode.react.js b/src/component/contents/DraftEditorTextNode.react.js
new file mode 100644
index 0000000..695150a
--- /dev/null
+++ b/src/component/contents/DraftEditorTextNode.react.js
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorTextNode.react
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const React = require('React');
+const ReactDOM = require('ReactDOM');
+const UserAgent = require('UserAgent');
+
+// In IE, spans with <br> tags render as two newlines. By rendering a span
+// with only a newline character, we can be sure to render a single line.
+const useNewlineChar = UserAgent.isBrowser('IE <= 11');
+
+/**
+ * Check whether the node should be considered a newline.
+ */
+function isNewline(node: Element): boolean {
+  return useNewlineChar ? node.textContent === '\n' : node.tagName === 'BR';
+}
+
+/**
+ * Placeholder elements for empty text content.
+ *
+ * What is this `data-text` attribute, anyway? It turns out that we need to
+ * put an attribute on the lowest-level text node in order to preserve correct
+ * spellcheck handling. If the <span> is naked, Chrome and Safari may do
+ * bizarre things to do the DOM -- split text nodes, create extra spans, etc.
+ * If the <span> has an attribute, this appears not to happen.
+ * See http://jsfiddle.net/9khdavod/ for the failure case, and
+ * http://jsfiddle.net/7pg143f7/ for the fixed case.
+ */
+const NEWLINE_A = useNewlineChar ?
+  <span key="A" data-text="true">{'\n'}</span> :
+  <br key="A" data-text="true" />;
+
+const NEWLINE_B = useNewlineChar ?
+  <span key="B" data-text="true">{'\n'}</span> :
+  <br key="B" data-text="true" />;
+
+type Props = {
+  children: string,
+};
+
+/**
+ * The lowest-level component in a `DraftEditor`, the text node component
+ * replaces the default React text node implementation. This allows us to
+ * perform custom handling of newline behavior and avoid re-rendering text
+ * nodes with DOM state that already matches the expectations of our immutable
+ * editor state.
+ */
+class DraftEditorTextNode extends React.Component {
+  _forceFlag: boolean;
+
+  constructor(props: Props) {
+    super(props);
+    this._forceFlag = false;
+  }
+
+  shouldComponentUpdate(nextProps: Props): boolean {
+    const node = ReactDOM.findDOMNode(this);
+    const shouldBeNewline = nextProps.children === '';
+    if (shouldBeNewline) {
+      return !isNewline(node);
+    }
+    return node.textContent !== nextProps.children;
+  }
+
+  componentWillUpdate(): void {
+    // By flipping this flag, we also keep flipping keys which forces
+    // React to remount this node every time it rerenders.
+    this._forceFlag = !this._forceFlag;
+  }
+
+  render(): ReactElement {
+    if (this.props.children === '') {
+      return this._forceFlag ? NEWLINE_A : NEWLINE_B;
+    }
+    return (
+      <span key={this._forceFlag ? 'A' : 'B'} data-text="true">
+        {this.props.children}
+      </span>
+    );
+  }
+}
+
+module.exports = DraftEditorTextNode;
diff --git a/src/component/contents/__tests__/DraftEditorBlock.react-test.js b/src/component/contents/__tests__/DraftEditorBlock.react-test.js
new file mode 100644
index 0000000..aee5e9f
--- /dev/null
+++ b/src/component/contents/__tests__/DraftEditorBlock.react-test.js
@@ -0,0 +1,555 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ * @typechecks
+ */
+
+'use strict';
+
+jest.autoMockOff()
+  .mock('Style')
+  .mock('getElementPosition')
+  .mock('getScrollPosition')
+  .mock('getViewportDimensions');
+
+var BlockTree = require('BlockTree');
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var ReactTestUtils = require('ReactTestUtils');
+var SampleDraftInlineStyle = require('SampleDraftInlineStyle');
+var SelectionState = require('SelectionState');
+var Style = require('Style');
+var UnicodeBidiDirection = require('UnicodeBidiDirection');
+
+var getElementPosition = require('getElementPosition');
+var getScrollPosition = require('getScrollPosition');
+var getViewportDimensions = require('getViewportDimensions');
+var reactComponentExpect = require('reactComponentExpect');
+var {BOLD, NONE, ITALIC} = SampleDraftInlineStyle;
+
+var mockGetDecorations = jest.genMockFn();
+
+var DecoratorSpan = React.createClass({
+  render() {
+    return <span>{this.props.children}</span>;
+  },
+});
+
+var DraftEditorBlock = require('DraftEditorBlock.react');
+
+// Define a class to satisfy typechecks.
+class Decorator {
+  getDecorations() {
+    return mockGetDecorations();
+  }
+  getComponentForKey() {
+    return DecoratorSpan;
+  }
+  getPropsForKey() {
+    return {};
+  }
+}
+
+var mockLeafRender = jest.genMockFn().mockReturnValue(<span />);
+jest.setMock(
+  'DraftEditorLeaf.react',
+  React.createClass({
+    render: function() {
+      return mockLeafRender();
+    },
+  })
+);
+
+var DraftEditorLeaf = require('DraftEditorLeaf.react');
+
+function returnEmptyString() {
+  return '';
+}
+
+function getHelloBlock() {
+  return new ContentBlock({
+    key: 'a',
+    type: 'unstyled',
+    text: 'hello',
+    characterList: Immutable.List(
+      Immutable.Repeat(CharacterMetadata.EMPTY, 5)
+    ),
+  });
+}
+
+function getSelection() {
+  return new SelectionState({
+    anchorKey: 'a',
+    anchorOffset: 0,
+    focusKey: 'a',
+    focusOffset: 0,
+    isBackward: false,
+    hasFocus: true,
+  });
+}
+
+function getProps(block, decorator) {
+  return {
+    block,
+    tree: BlockTree.generate(block, decorator),
+    selection: getSelection(),
+    decorator: decorator || null,
+    forceSelection: false,
+    direction: UnicodeBidiDirection.LTR,
+    blockStyleFn: returnEmptyString,
+    styleSet: NONE,
+  };
+}
+
+function arePropsEqual(renderedChild, leafPropSet) {
+  Object.keys(leafPropSet).forEach(key => {
+    expect(
+      Immutable.is(leafPropSet[key], renderedChild.instance().props[key])
+    ).toBeTruthy();
+  });
+}
+
+function assertLeaves(renderedBlock, leafProps) {
+  leafProps.forEach((leafPropSet, ii) => {
+    const child = renderedBlock.expectRenderedChildAt(ii);
+    child.toBeComponentOfType(DraftEditorLeaf);
+    arePropsEqual(child, leafPropSet);
+  });
+}
+
+describe('DraftEditorBlock.react', () => {
+  Style.getScrollParent.mockReturnValue(window);
+  window.scrollTo = jest.genMockFn();
+  getElementPosition.mockReturnValue({
+    x: 0,
+    y: 600,
+    width: 500,
+    height: 16,
+  });
+  getScrollPosition.mockReturnValue({x: 0, y: 0});
+  getViewportDimensions.mockReturnValue({width: 1200, height: 800});
+
+  beforeEach(() => {
+    window.scrollTo.mockClear();
+    mockGetDecorations.mockClear();
+    mockLeafRender.mockClear();
+  });
+
+  describe('Basic rendering', () => {
+    it('must render a leaf node', () => {
+      var props = getProps(getHelloBlock());
+      var block = ReactTestUtils.renderIntoDocument(
+        <DraftEditorBlock {...props} />
+      );
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      assertLeaves(rendered, [
+        {
+          text: 'hello',
+          offsetKey: 'a-0-0',
+          start: 0,
+          styleSet: NONE,
+          isLast: true,
+        },
+      ]);
+    });
+
+    it('must render multiple leaf nodes', () => {
+      var boldLength = 2;
+      var helloBlock = getHelloBlock();
+      var characters = helloBlock.getCharacterList();
+      characters = characters
+        .slice(0, boldLength)
+        .map(c => CharacterMetadata.applyStyle(c, 'BOLD'))
+        .concat(characters.slice(boldLength));
+
+      helloBlock = helloBlock.set('characterList', characters.toList());
+
+      var props = getProps(helloBlock);
+      var block = ReactTestUtils.renderIntoDocument(
+        <DraftEditorBlock {...props} />
+      );
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      assertLeaves(rendered, [
+        {
+          text: 'he',
+          offsetKey: 'a-0-0',
+          start: 0,
+          styleSet: BOLD,
+          isLast: false,
+        },
+        {
+          text: 'llo',
+          offsetKey: 'a-0-1',
+          start: 2,
+          styleSet: NONE,
+          isLast: true,
+        },
+      ]);
+    });
+  });
+
+  describe('Allow/reject component updates', () => {
+    it('must allow update when `block` has changed', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      var updatedHelloBlock = helloBlock.set('text', 'hxllo');
+      var nextProps = getProps(updatedHelloBlock);
+
+      expect(updatedHelloBlock).not.toBe(helloBlock);
+      expect(props.block).not.toBe(nextProps.block);
+
+      ReactDOM.render(
+        <DraftEditorBlock {...nextProps} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+    });
+
+    it('must allow update when `tree` has changed', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      mockGetDecorations.mockReturnValue(
+        Immutable.List.of('x', 'x', null, null, null)
+      );
+      var decorator = new Decorator();
+
+      var newTree = BlockTree.generate(helloBlock, decorator);
+      var nextProps = {...props, tree: newTree, decorator};
+
+      expect(props.tree).not.toBe(nextProps.tree);
+
+      ReactDOM.render(
+        <DraftEditorBlock {...nextProps} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(3);
+    });
+
+    it('must allow update when `direction` has changed', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      var nextProps = {...props, direction: UnicodeBidiDirection.RTL};
+      expect(props.direction).not.toBe(nextProps.direction);
+
+      ReactDOM.render(
+        <DraftEditorBlock {...nextProps} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+    });
+
+    it('must allow update when forcing selection', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      // The default selection state in this test is on a selection edge.
+      var nextProps = {
+        ...props,
+        forceSelection: true,
+      };
+
+      ReactDOM.render(
+        <DraftEditorBlock {...nextProps} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+    });
+
+    it('must reject update if conditions are not met', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      // Render again with the exact same props as before.
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      // No new leaf renders.
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+    });
+
+    it('must reject update if selection is not on an edge', () => {
+      var helloBlock = getHelloBlock();
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+
+      // Move selection state to some other block.
+      var nonEdgeSelection = props.selection.merge({
+        anchorKey: 'z',
+        focusKey: 'z',
+      });
+
+      var newProps = {...props, selection: nonEdgeSelection};
+
+      // Render again with selection now moved elsewhere and the contents
+      // unchanged.
+      ReactDOM.render(
+        <DraftEditorBlock {...newProps} />,
+        container
+      );
+
+      // No new leaf renders.
+      expect(mockLeafRender.mock.calls.length).toBe(1);
+    });
+  });
+
+  describe('Complex rendering with a decorator', () => {
+    it('must split apart two decorated and undecorated', () => {
+      var helloBlock = getHelloBlock();
+
+      mockGetDecorations.mockReturnValue(
+        Immutable.List.of('x', 'x', null, null, null)
+      );
+      var decorator = new Decorator();
+      var props = getProps(helloBlock, decorator);
+
+      var container = document.createElement('div');
+      var block = ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      rendered
+        .expectRenderedChildAt(0)
+        .scalarPropsEqual({offsetKey: 'a-0-0'})
+        .toBeComponentOfType(DecoratorSpan)
+          .expectRenderedChild()
+          .toBeComponentOfType('span');
+
+      rendered
+        .expectRenderedChildAt(1)
+        .scalarPropsEqual({offsetKey: 'a-1-0'})
+        .toBeComponentOfType(DraftEditorLeaf);
+    });
+
+    it('must split apart two decorators', () => {
+      var helloBlock = getHelloBlock();
+
+      mockGetDecorations.mockReturnValue(
+        Immutable.List.of('x', 'x', 'y', 'y', 'y')
+      );
+
+      var decorator = new Decorator();
+      var props = getProps(helloBlock, decorator);
+
+      var container = document.createElement('div');
+      var block = ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      rendered
+        .expectRenderedChildAt(0)
+        .scalarPropsEqual({offsetKey: 'a-0-0'})
+        .toBeComponentOfType(DecoratorSpan);
+
+      rendered
+        .expectRenderedChildAt(1)
+        .scalarPropsEqual({offsetKey: 'a-1-0'})
+        .toBeComponentOfType(DecoratorSpan);
+    });
+  });
+
+  describe('Complex rendering with inline styles', () => {
+    it('must split apart styled spans', () => {
+      var helloBlock = getHelloBlock();
+      var characters = helloBlock.getCharacterList();
+      var newChars = characters.slice(0, 2).map(ch => {
+        return CharacterMetadata.applyStyle(ch, 'BOLD');
+      }).concat(characters.slice(2));
+
+      helloBlock = helloBlock.set('characterList', Immutable.List(newChars));
+      var props = getProps(helloBlock);
+
+      var container = document.createElement('div');
+      var block = ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(2);
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      let child = rendered.expectRenderedChildAt(0);
+      child.toBeComponentOfType(DraftEditorLeaf);
+      arePropsEqual(child, {offsetKey: 'a-0-0', styleSet: BOLD});
+
+      child = rendered.expectRenderedChildAt(1);
+      child.toBeComponentOfType(DraftEditorLeaf);
+      arePropsEqual(child, {offsetKey: 'a-0-1', styleSet: NONE});
+    });
+
+    it('must split styled spans apart within decorator', () => {
+      var helloBlock = getHelloBlock();
+      var characters = helloBlock.getCharacterList();
+      var newChars = Immutable.List([
+        CharacterMetadata.applyStyle(characters.get(0), 'BOLD'),
+        CharacterMetadata.applyStyle(characters.get(1), 'ITALIC'),
+      ]).concat(characters.slice(2));
+
+      helloBlock = helloBlock.set('characterList', Immutable.List(newChars));
+
+      mockGetDecorations.mockReturnValue(
+        Immutable.List.of('x', 'x', null, null, null)
+      );
+      var decorator = new Decorator();
+      var props = getProps(helloBlock, decorator);
+
+      var container = document.createElement('div');
+      var block = ReactDOM.render(
+        <DraftEditorBlock {...props} />,
+        container
+      );
+
+      expect(mockLeafRender.mock.calls.length).toBe(3);
+
+      var rendered = reactComponentExpect(block)
+        .expectRenderedChild()
+        .toBeComponentOfType('div');
+
+      var decoratedSpan = rendered
+        .expectRenderedChildAt(0)
+        .scalarPropsEqual({offsetKey: 'a-0-0'})
+        .toBeComponentOfType(DecoratorSpan)
+          .expectRenderedChild();
+
+      let child = decoratedSpan.expectRenderedChildAt(0);
+      child.toBeComponentOfType(DraftEditorLeaf);
+      arePropsEqual(child, {offsetKey: 'a-0-0', styleSet: BOLD});
+
+      child = decoratedSpan.expectRenderedChildAt(1);
+      child.toBeComponentOfType(DraftEditorLeaf);
+      arePropsEqual(child, {offsetKey: 'a-0-1', styleSet: ITALIC});
+
+      child = rendered.expectRenderedChildAt(1);
+      child.toBeComponentOfType(DraftEditorLeaf);
+      arePropsEqual(child, {offsetKey: 'a-1-0', styleSet: NONE});
+    });
+  });
+
+  describe('Scroll-to-cursor on mount', () => {
+    var props = getProps(getHelloBlock());
+
+    describe('Scroll parent is `window`', () => {
+      it('must scroll the window if needed', () => {
+        getElementPosition.mockReturnValueOnce({
+          x: 0,
+          y: 800,
+          width: 500,
+          height: 16,
+        });
+
+        var container = document.createElement('div');
+        ReactDOM.render(
+          <DraftEditorBlock {...props} />,
+          container
+        );
+
+        var scrollCalls = window.scrollTo.mock.calls;
+        expect(scrollCalls.length).toBe(1);
+        expect(scrollCalls[0][0]).toBe(0);
+
+        // (current scroll position) + (block height) + (buffer)
+        expect(scrollCalls[0][1]).toBe(26);
+      });
+
+      it('must not scroll the window if unnecessary', () => {
+        var container = document.createElement('div');
+        ReactDOM.render(
+          <DraftEditorBlock {...props} />,
+          container
+        );
+
+        var scrollCalls = window.scrollTo.mock.calls;
+        expect(scrollCalls.length).toBe(0);
+      });
+    });
+  });
+});
diff --git a/src/component/contents/__tests__/DraftEditorTextNode-test.js b/src/component/contents/__tests__/DraftEditorTextNode-test.js
new file mode 100644
index 0000000..8d0571e
--- /dev/null
+++ b/src/component/contents/__tests__/DraftEditorTextNode-test.js
@@ -0,0 +1,212 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.dontMock('DraftEditorTextNode.react');
+
+var BLOCK_DELIMITER_CHAR = '\n';
+var TEST_A = 'Hello';
+var TEST_B = ' World!';
+
+var DraftEditorTextNode = require('DraftEditorTextNode.react');
+var React = require('React');
+var ReactDOM = require('ReactDOM');
+var UserAgent = require('UserAgent');
+
+describe('DraftEditorTextNode', function() {
+  var container;
+
+  beforeEach(function() {
+    jest.resetModuleRegistry();
+    container = document.createElement('div');
+  });
+
+  function renderIntoContainer(element) {
+    return ReactDOM.render(element, container);
+  }
+
+  function initializeAsIE() {
+    UserAgent.isBrowser.mockImplementation(() => true);
+  }
+
+  function initializeAsNonIE() {
+    UserAgent.isBrowser.mockImplementation(() => false);
+  }
+
+  function expectPopulatedSpan(stub, testString) {
+    var node = ReactDOM.findDOMNode(stub);
+    expect(node.tagName).toBe('SPAN');
+    expect(node.childNodes.length).toBe(1);
+    expect(node.firstChild.textContent).toBe(testString);
+  }
+
+  it('must initialize correctly with an empty string, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{''}</DraftEditorTextNode>
+    );
+    expect(ReactDOM.findDOMNode(stub).tagName).toBe('BR');
+  });
+
+  it('must initialize correctly with an empty string, IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{''}</DraftEditorTextNode>
+    );
+    expectPopulatedSpan(stub, BLOCK_DELIMITER_CHAR);
+  });
+
+  it('must initialize correctly with a string, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+    expectPopulatedSpan(stub, TEST_A);
+  });
+
+  it('must initialize correctly with a string, IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+    expectPopulatedSpan(stub, TEST_A);
+  });
+
+  it('must update from empty to non-empty, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{''}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+    expectPopulatedSpan(stub, TEST_A);
+  });
+
+  it('must update from empty to non-empty, IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{''}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+    expectPopulatedSpan(stub, TEST_A);
+  });
+
+  it('must update from non-empty to non-empty, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A + TEST_B}</DraftEditorTextNode>
+    );
+
+    expectPopulatedSpan(stub, TEST_A + TEST_B);
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+    expectPopulatedSpan(stub, TEST_B);
+  });
+
+  it('must update from non-empty to non-empty, non-IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A + TEST_B}</DraftEditorTextNode>
+    );
+    expectPopulatedSpan(stub, TEST_A + TEST_B);
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+    expectPopulatedSpan(stub, TEST_B);
+  });
+
+  it('must skip updates if text already matches DOM, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    spyOn(stub, 'render').and.callThrough();
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+    expect(stub.render.calls.count()).toBe(0);
+
+    // Sanity check that updating is performed when appropriate.
+    renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+
+    expect(stub.render.calls.count()).toBe(1);
+  });
+
+  it('must skip updates if text already matches DOM, IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    spyOn(stub, 'render').and.callThrough();
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+    expect(stub.render.calls.count()).toBe(0);
+
+    // Sanity check that updating is performed when appropriate.
+    renderIntoContainer(<DraftEditorTextNode>{TEST_B}</DraftEditorTextNode>);
+
+    expect(stub.render.calls.count()).toBe(1);
+  });
+
+  it('must update from non-empty to empty, non-IE', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(<DraftEditorTextNode>{''}</DraftEditorTextNode>);
+
+    expect(ReactDOM.findDOMNode(stub).tagName).toBe('BR');
+  });
+
+  it('must update from non-empty to empty, IE', function() {
+    initializeAsIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    renderIntoContainer(<DraftEditorTextNode>{''}</DraftEditorTextNode>);
+
+    expectPopulatedSpan(stub, BLOCK_DELIMITER_CHAR);
+  });
+
+  it('must render properly into a parent DOM node', function() {
+    initializeAsNonIE();
+    renderIntoContainer(
+      <div><DraftEditorTextNode>{TEST_A}</DraftEditorTextNode></div>
+    );
+  });
+
+  it('must force unchanged text back into the DOM', function() {
+    initializeAsNonIE();
+    var stub = renderIntoContainer(
+      <DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>
+    );
+
+    ReactDOM.findDOMNode(stub).textContent = TEST_B;
+
+    renderIntoContainer(<DraftEditorTextNode>{TEST_A}</DraftEditorTextNode>);
+
+    expect(ReactDOM.findDOMNode(stub).textContent).toBe(TEST_A);
+  });
+});
diff --git a/src/component/handlers/DraftEditorModes.js b/src/component/handlers/DraftEditorModes.js
new file mode 100644
index 0000000..ca17966
--- /dev/null
+++ b/src/component/handlers/DraftEditorModes.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorModes
+ * @flow
+ */
+
+'use strict';
+
+export type DraftEditorModes = (
+  /**
+   * `edit` is the most common mode for text entry. This includes most typing,
+   * deletion, cut/copy/paste, and other behaviors.
+   */
+  'edit' |
+
+  /**
+   * `composite` mode handles IME text entry.
+   */
+  'composite' |
+
+  /**
+   * `drag` mode handles editor behavior while a drag event is occurring.
+   */
+  'drag' |
+
+  /**
+   * `cut` mode allows us to effectively ignore all edit behaviors while the`
+   * browser performs a native `cut` operation on the DOM.
+   */
+  'cut' |
+
+  /**
+   * `render` mode is the normal "null" mode, during which no edit behavior is
+   * expected or observed.
+   */
+  'render'
+);
diff --git a/src/component/handlers/composition/DraftEditorCompositionHandler.js b/src/component/handlers/composition/DraftEditorCompositionHandler.js
new file mode 100644
index 0000000..9749073
--- /dev/null
+++ b/src/component/handlers/composition/DraftEditorCompositionHandler.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorCompositionHandler
+ * @flow
+ */
+
+'use strict';
+
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+const Keys = require('Keys');
+
+const getEntityKeyForSelection = require('getEntityKeyForSelection');
+const isSelectionAtLeafStart = require('isSelectionAtLeafStart');
+
+/**
+ * Millisecond delay to allow `compositionstart` to fire again upon
+ * `compositionend`.
+ *
+ * This is used for Korean input to ensure that typing can continue without
+ * the editor trying to render too quickly. More specifically, Safari 7.1+
+ * triggers `compositionstart` a little slower than Chrome/FF, which
+ * leads to composed characters being resolved and re-render occurring
+ * sooner than we want.
+ */
+const RESOLVE_DELAY = 20;
+
+/**
+ * A handful of variables used to track the current composition and its
+ * resolution status. These exist at the module level because it is not
+ * possible to have compositions occurring in multiple editors simultaneously,
+ * and it simplifies state management with respect to the DraftEditor component.
+ */
+let resolved = false;
+let stillComposing = false;
+let textInputData = '';
+
+var DraftEditorCompositionHandler = {
+  onBeforeInput: function(e: SyntheticInputEvent): void {
+    textInputData = (textInputData || '') + e.data;
+  },
+
+  /**
+   * A `compositionstart` event has fired while we're still in composition
+   * mode. Continue the current composition session to prevent a re-render.
+   */
+  onCompositionStart: function(): void {
+    stillComposing = true;
+  },
+
+  /**
+   * Attempt to end the current composition session.
+   *
+   * Defer handling because browser will still insert the chars into active
+   * element after `compositionend`. If a `compositionstart` event fires
+   * before `resolveComposition` executes, our composition session will
+   * continue.
+   *
+   * The `resolved` flag is useful because certain IME interfaces fire the
+   * `compositionend` event multiple times, thus queueing up multiple attempts
+   * at handling the composition. Since handling the same composition event
+   * twice could break the DOM, we only use the first event. Example: Arabic
+   * Google Input Tools on Windows 8.1 fires `compositionend` three times.
+   */
+  onCompositionEnd: function(): void {
+    resolved = false;
+    stillComposing = false;
+    setTimeout(() => {
+      if (!resolved) {
+        DraftEditorCompositionHandler.resolveComposition.call(this);
+      }
+    }, RESOLVE_DELAY);
+  },
+
+  /**
+   * In Safari, keydown events may fire when committing compositions. If
+   * the arrow keys are used to commit, prevent default so that the cursor
+   * doesn't move, otherwise it will jump back noticeably on re-render.
+   */
+  onKeyDown: function(e: SyntheticKeyboardEvent): void {
+    if (e.which === Keys.RIGHT || e.which === Keys.LEFT) {
+      e.preventDefault();
+    }
+  },
+
+  /**
+   * Keypress events may fire when committing compositions. In Firefox,
+   * pressing RETURN commits the composition and inserts extra newline
+   * characters that we do not want. `preventDefault` allows the composition
+   * to be committed while preventing the extra characters.
+   */
+  onKeyPress: function(e: SyntheticKeyboardEvent): void {
+    if (e.which === Keys.RETURN) {
+      e.preventDefault();
+    }
+  },
+
+  /**
+   * Attempt to insert composed characters into the document.
+   *
+   * If we are still in a composition session, do nothing. Otherwise, insert
+   * the characters into the document and terminate the composition session.
+   *
+   * If no characters were composed -- for instance, the user
+   * deleted all composed characters and committed nothing new --
+   * force a re-render. We also re-render when the composition occurs
+   * at the beginning of a leaf, to ensure that if the browser has
+   * created a new text node for the composition, we will discard it.
+   *
+   * Resetting innerHTML will move focus to the beginning of the editor,
+   * so we update to force it back to the correct place.
+   */
+  resolveComposition: function(): void {
+    if (stillComposing) {
+      return;
+    }
+
+    resolved = true;
+    const composedChars = textInputData;
+    textInputData = '';
+
+    const editorState = EditorState.set(this.props.editorState, {
+      inCompositionMode: false,
+    });
+
+    const currentStyle = editorState.getCurrentInlineStyle();
+    const entityKey = getEntityKeyForSelection(
+      editorState.getCurrentContent(),
+      editorState.getSelection()
+    );
+
+    const mustReset = (
+      !composedChars ||
+      isSelectionAtLeafStart(editorState) ||
+      currentStyle.size > 0 ||
+      entityKey !== null
+    );
+
+    if (mustReset) {
+      this.restoreEditorDOM();
+    }
+
+    this.exitCurrentMode();
+    this.removeRenderGuard();
+
+    if (composedChars) {
+      // If characters have been composed, re-rendering with the update
+      // is sufficient to reset the editor.
+      const contentState = DraftModifier.replaceText(
+        editorState.getCurrentContent(),
+        editorState.getSelection(),
+        composedChars,
+        currentStyle,
+        entityKey
+      );
+      this.update(
+        EditorState.push(
+          editorState,
+          contentState,
+          'insert-characters'
+        )
+      );
+      return;
+    }
+
+    if (mustReset) {
+      this.update(
+        EditorState.set(editorState, {
+          nativelyRenderedContent: null,
+          forceSelection: true,
+        })
+      );
+    }
+  },
+};
+
+module.exports = DraftEditorCompositionHandler;
diff --git a/src/component/handlers/drag/DraftEditorDragHandler.js b/src/component/handlers/drag/DraftEditorDragHandler.js
new file mode 100644
index 0000000..f27e1f8
--- /dev/null
+++ b/src/component/handlers/drag/DraftEditorDragHandler.js
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorDragHandler
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const DataTransfer = require('DataTransfer');
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+
+const findAncestorOffsetKey = require('findAncestorOffsetKey');
+const getTextContentFromFiles = require('getTextContentFromFiles');
+const getUpdatedSelectionState = require('getUpdatedSelectionState');
+const nullthrows = require('nullthrows');
+
+import type SelectionState from 'SelectionState';
+
+/**
+ * Get a SelectionState for the supplied mouse event.
+ */
+function getSelectionForEvent(
+  event: Object,
+  editorState: EditorState
+): ?SelectionState {
+  let node: ?Node = null;
+  let offset: ?number = null;
+
+  if (document.caretRangeFromPoint) {
+    var dropRange = document.caretRangeFromPoint(event.x, event.y);
+    node = dropRange.startContainer;
+    offset = dropRange.startOffset;
+  } else if (event.rangeParent) {
+    node = event.rangeParent;
+    offset = event.rangeOffset;
+  } else {
+    return null;
+  }
+
+  node = nullthrows(node);
+  offset = nullthrows(offset);
+  const offsetKey = nullthrows(findAncestorOffsetKey(node));
+
+  return getUpdatedSelectionState(
+    editorState,
+    offsetKey,
+    offset,
+    offsetKey,
+    offset
+  );
+}
+
+var DraftEditorDragHandler = {
+  /**
+   * Drag originating from input terminated.
+   */
+  onDragEnd: function(): void {
+    this.exitCurrentMode();
+  },
+
+  /**
+   * Handle data being dropped.
+   */
+  onDrop: function(e: Object): void {
+    const data = new DataTransfer(e.nativeEvent.dataTransfer);
+
+    const editorState: EditorState = this.props.editorState;
+    const dropSelection: ?SelectionState = getSelectionForEvent(
+      e.nativeEvent,
+      editorState
+    );
+
+    e.preventDefault();
+    this.exitCurrentMode();
+
+    if (dropSelection == null) {
+      return;
+    }
+
+    const files = data.getFiles();
+    if (files.length > 0) {
+      if (this.props.handleDroppedFiles &&
+          this.props.handleDroppedFiles(dropSelection, files)) {
+        return;
+      }
+
+      getTextContentFromFiles(files, fileText => {
+        fileText && this.update(
+          insertTextAtSelection(
+            editorState,
+            nullthrows(dropSelection), // flow wtf
+            fileText
+          )
+        );
+      });
+      return;
+    }
+
+    if (this._internalDrag) {
+      this.update(moveText(editorState, dropSelection));
+      return;
+    }
+
+    this.update(
+      insertTextAtSelection(editorState, dropSelection, data.getText())
+    );
+  },
+
+};
+
+function moveText(
+  editorState: EditorState,
+  targetSelection: SelectionState
+): EditorState {
+  const newContentState = DraftModifier.moveText(
+    editorState.getCurrentContent(),
+    editorState.getSelection(),
+    targetSelection
+  );
+  return EditorState.push(
+    editorState,
+    newContentState,
+    'insert-fragment'
+  );
+}
+
+/**
+ * Insert text at a specified selection.
+ */
+function insertTextAtSelection(
+  editorState: EditorState,
+  selection: SelectionState,
+  text: string
+): EditorState {
+  const newContentState = DraftModifier.insertText(
+    editorState.getCurrentContent(),
+    selection,
+    text,
+    editorState.getCurrentInlineStyle()
+  );
+  return EditorState.push(
+    editorState,
+    newContentState,
+    'insert-fragment'
+  );
+}
+
+module.exports = DraftEditorDragHandler;
diff --git a/src/component/handlers/edit/DraftEditorEditHandler.js b/src/component/handlers/edit/DraftEditorEditHandler.js
new file mode 100644
index 0000000..da719d6
--- /dev/null
+++ b/src/component/handlers/edit/DraftEditorEditHandler.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorEditHandler
+ * @flow
+ */
+
+'use strict';
+
+const onBeforeInput = require('editOnBeforeInput');
+const onBlur = require('editOnBlur');
+const onCompositionStart = require('editOnCompositionStart');
+const onCopy = require('editOnCopy');
+const onCut = require('editOnCut');
+const onDragOver = require('editOnDragOver');
+const onDragStart = require('editOnDragStart');
+const onFocus = require('editOnFocus');
+const onInput = require('editOnInput');
+const onKeyDown = require('editOnKeyDown');
+const onPaste = require('editOnPaste');
+const onSelect = require('editOnSelect');
+
+const DraftEditorEditHandler = {
+  onBeforeInput,
+  onBlur,
+  onCompositionStart,
+  onCopy,
+  onCut,
+  onDragOver,
+  onDragStart,
+  onFocus,
+  onInput,
+  onKeyDown,
+  onPaste,
+  onSelect,
+};
+
+module.exports = DraftEditorEditHandler;
diff --git a/src/component/handlers/edit/commands/SecondaryClipboard.js b/src/component/handlers/edit/commands/SecondaryClipboard.js
new file mode 100644
index 0000000..4a06bac
--- /dev/null
+++ b/src/component/handlers/edit/commands/SecondaryClipboard.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SecondaryClipboard
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+var getContentStateFragment = require('getContentStateFragment');
+var nullthrows = require('nullthrows');
+
+import type {BlockMap} from 'BlockMap';
+import type SelectionState from 'SelectionState';
+
+var clipboard: ?BlockMap = null;
+
+/**
+ * Some systems offer a "secondary" clipboard to allow quick internal cut
+ * and paste behavior. For instance, Ctrl+K (cut) and Ctrl+Y (paste).
+ */
+var SecondaryClipboard = {
+  cut: function(editorState: EditorState): EditorState {
+    var content = editorState.getCurrentContent();
+    var selection = editorState.getSelection();
+    var targetRange: ?SelectionState = null;
+
+    if (selection.isCollapsed()) {
+      var anchorKey = selection.getAnchorKey();
+      var blockEnd = content.getBlockForKey(anchorKey).getLength();
+
+      if (blockEnd === selection.getAnchorOffset()) {
+        return editorState;
+      }
+
+      targetRange = selection.set('focusOffset', blockEnd);
+    } else {
+      targetRange = selection;
+    }
+
+    targetRange = nullthrows(targetRange);
+    clipboard = getContentStateFragment(content, targetRange);
+
+    var afterRemoval = DraftModifier.removeRange(
+      content,
+      targetRange,
+      'forward'
+    );
+
+    if (afterRemoval === content) {
+      return editorState;
+    }
+
+    return EditorState.push(editorState, afterRemoval, 'remove-range');
+  },
+
+  paste: function(editorState: EditorState): EditorState {
+    if (!clipboard) {
+      return editorState;
+    }
+
+    var newContent = DraftModifier.replaceWithFragment(
+      editorState.getCurrentContent(),
+      editorState.getSelection(),
+      clipboard
+    );
+
+    return EditorState.push(editorState, newContent, 'insert-fragment');
+  },
+};
+
+module.exports = SecondaryClipboard;
diff --git a/src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js b/src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js
new file mode 100644
index 0000000..81f1962
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandBackspaceToStartOfLine.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandBackspaceToStartOfLine
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+var expandRangeToStartOfLine = require('expandRangeToStartOfLine');
+var getDraftEditorSelectionWithNodes = require('getDraftEditorSelectionWithNodes');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+function keyCommandBackspaceToStartOfLine(
+  editorState: EditorState
+): EditorState {
+  var afterRemoval = removeTextWithStrategy(
+    editorState,
+    strategyState => {
+      var domSelection = global.getSelection();
+      var range = domSelection.getRangeAt(0);
+      range = expandRangeToStartOfLine(range);
+
+      var selection = getDraftEditorSelectionWithNodes(
+        strategyState,
+        null,
+        range.endContainer,
+        range.endOffset,
+        range.startContainer,
+        range.startOffset
+      ).selectionState;
+      return selection;
+    },
+    'backward'
+  );
+
+  if (afterRemoval === editorState.getCurrentContent()) {
+    return editorState;
+  }
+
+  return EditorState.push(
+    editorState,
+    afterRemoval,
+    'remove-range'
+  );
+}
+
+module.exports = keyCommandBackspaceToStartOfLine;
diff --git a/src/component/handlers/edit/commands/keyCommandBackspaceWord.js b/src/component/handlers/edit/commands/keyCommandBackspaceWord.js
new file mode 100644
index 0000000..4a635ff
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandBackspaceWord.js
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandBackspaceWord
+ * @flow
+ */
+
+'use strict';
+
+var DraftRemovableWord = require('DraftRemovableWord');
+var EditorState = require('EditorState');
+
+var moveSelectionBackward = require('moveSelectionBackward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Delete the word that is left of the cursor, as well as any spaces or
+ * punctuation after the word.
+ */
+function keyCommandBackspaceWord(editorState: EditorState): EditorState {
+  var afterRemoval = removeTextWithStrategy(
+    editorState,
+    strategyState => {
+      var selection = strategyState.getSelection();
+      var offset = selection.getStartOffset();
+      // If there are no words before the cursor, remove the preceding newline.
+      if (offset === 0) {
+        return moveSelectionBackward(strategyState, 1);
+      }
+      var key = selection.getStartKey();
+      var content = strategyState.getCurrentContent();
+      var text = content.getBlockForKey(key).getText().slice(0, offset);
+      var toRemove = DraftRemovableWord.getBackward(text);
+      return moveSelectionBackward(
+        strategyState,
+        toRemove.length || 1
+      );
+    },
+    'backward'
+  );
+
+  if (afterRemoval === editorState.getCurrentContent()) {
+    return editorState;
+  }
+
+  return EditorState.push(editorState, afterRemoval, 'remove-range');
+}
+
+module.exports = keyCommandBackspaceWord;
diff --git a/src/component/handlers/edit/commands/keyCommandDeleteWord.js b/src/component/handlers/edit/commands/keyCommandDeleteWord.js
new file mode 100644
index 0000000..e57ddc6
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandDeleteWord.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandDeleteWord
+ * @flow
+ */
+
+'use strict';
+
+var DraftRemovableWord = require('DraftRemovableWord');
+var EditorState = require('EditorState');
+
+var moveSelectionForward = require('moveSelectionForward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Delete the word that is right of the cursor, as well as any spaces or
+ * punctuation before the word.
+ */
+function keyCommandDeleteWord(editorState: EditorState): EditorState {
+  var afterRemoval = removeTextWithStrategy(
+    editorState,
+    strategyState => {
+      var selection = strategyState.getSelection();
+      var offset = selection.getStartOffset();
+      var key = selection.getStartKey();
+      var content = strategyState.getCurrentContent();
+      var text = content.getBlockForKey(key).getText().slice(offset);
+      var toRemove = DraftRemovableWord.getForward(text);
+
+      // If there are no words in front of the cursor, remove the newline.
+      return moveSelectionForward(
+        strategyState,
+        toRemove.length || 1
+      );
+    },
+    'forward'
+  );
+
+  if (afterRemoval === editorState.getCurrentContent()) {
+    return editorState;
+  }
+
+  return EditorState.push(editorState, afterRemoval, 'remove-range');
+}
+
+module.exports = keyCommandDeleteWord;
diff --git a/src/component/handlers/edit/commands/keyCommandInsertNewline.js b/src/component/handlers/edit/commands/keyCommandInsertNewline.js
new file mode 100644
index 0000000..fbed82d
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandInsertNewline.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandInsertNewline
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+function keyCommandInsertNewline(editorState: EditorState): EditorState {
+  var contentState = DraftModifier.splitBlock(
+    editorState.getCurrentContent(),
+    editorState.getSelection()
+  );
+  return EditorState.push(editorState, contentState, 'split-block');
+}
+
+module.exports = keyCommandInsertNewline;
diff --git a/src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js b/src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js
new file mode 100644
index 0000000..a2cc876
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandMoveSelectionToEndOfBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandMoveSelectionToEndOfBlock
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * See comment for `moveSelectionToStartOfBlock`.
+ */
+function keyCommandMoveSelectionToEndOfBlock(
+  editorState: EditorState
+): EditorState {
+  var selection = editorState.getSelection();
+  var endKey = selection.getEndKey();
+  var content = editorState.getCurrentContent();
+  var textLength = content.getBlockForKey(endKey).getLength();
+  return EditorState.set(editorState, {
+    selection: selection.merge({
+      anchorKey: endKey,
+      anchorOffset: textLength,
+      focusKey: endKey,
+      focusOffset: textLength,
+      isBackward: false,
+    }),
+    forceSelection: true,
+  });
+}
+
+module.exports = keyCommandMoveSelectionToEndOfBlock;
diff --git a/src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js b/src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
new file mode 100644
index 0000000..e3b7904
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandMoveSelectionToStartOfBlock.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandMoveSelectionToStartOfBlock
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * Collapse selection at the start of the first selected block. This is used
+ * for Firefox versions that attempt to navigate forward/backward instead of
+ * moving the cursor. Other browsers are able to move the cursor natively.
+ */
+function keyCommandMoveSelectionToStartOfBlock(
+  editorState: EditorState
+): EditorState {
+  var selection = editorState.getSelection();
+  var startKey = selection.getStartKey();
+  return EditorState.set(editorState, {
+    selection: selection.merge({
+      anchorKey: startKey,
+      anchorOffset: 0,
+      focusKey: startKey,
+      focusOffset: 0,
+      isBackward: false,
+    }),
+    forceSelection: true,
+  });
+}
+
+module.exports = keyCommandMoveSelectionToStartOfBlock;
diff --git a/src/component/handlers/edit/commands/keyCommandPlainBackspace.js b/src/component/handlers/edit/commands/keyCommandPlainBackspace.js
new file mode 100644
index 0000000..c4e62ab
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandPlainBackspace.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandPlainBackspace
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UnicodeUtils = require('UnicodeUtils');
+
+var moveSelectionBackward = require('moveSelectionBackward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Remove the selected range. If the cursor is collapsed, remove the preceding
+ * character. This operation is Unicode-aware, so removing a single character
+ * will remove a surrogate pair properly as well.
+ */
+function keyCommandPlainBackspace(editorState: EditorState): EditorState {
+  var afterRemoval = removeTextWithStrategy(
+    editorState,
+    strategyState => {
+      var selection = strategyState.getSelection();
+      var content = strategyState.getCurrentContent();
+      var key = selection.getAnchorKey();
+      var offset = selection.getAnchorOffset();
+      var charBehind = content.getBlockForKey(key).getText()[offset - 1];
+      return moveSelectionBackward(
+        strategyState,
+        charBehind ? UnicodeUtils.getUTF16Length(charBehind, 0) : 1
+      );
+    },
+    'backward'
+  );
+
+  if (afterRemoval === editorState.getCurrentContent()) {
+    return editorState;
+  }
+
+  var selection = editorState.getSelection();
+  return EditorState.push(
+    editorState,
+    afterRemoval.set('selectionBefore', selection),
+    selection.isCollapsed() ? 'backspace-character' : 'remove-range'
+  );
+}
+
+module.exports = keyCommandPlainBackspace;
diff --git a/src/component/handlers/edit/commands/keyCommandPlainDelete.js b/src/component/handlers/edit/commands/keyCommandPlainDelete.js
new file mode 100644
index 0000000..398fb71
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandPlainDelete.js
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandPlainDelete
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UnicodeUtils = require('UnicodeUtils');
+
+var moveSelectionForward = require('moveSelectionForward');
+var removeTextWithStrategy = require('removeTextWithStrategy');
+
+/**
+ * Remove the selected range. If the cursor is collapsed, remove the following
+ * character. This operation is Unicode-aware, so removing a single character
+ * will remove a surrogate pair properly as well.
+ */
+function keyCommandPlainDelete(editorState: EditorState): EditorState {
+  var afterRemoval = removeTextWithStrategy(
+    editorState,
+    strategyState => {
+      var selection = strategyState.getSelection();
+      var content = strategyState.getCurrentContent();
+      var key = selection.getAnchorKey();
+      var offset = selection.getAnchorOffset();
+      var charAhead = content.getBlockForKey(key).getText()[offset];
+      return moveSelectionForward(
+        strategyState,
+        charAhead ? UnicodeUtils.getUTF16Length(charAhead, 0) : 1
+      );
+    },
+    'forward'
+  );
+
+  if (afterRemoval === editorState.getCurrentContent()) {
+    return editorState;
+  }
+
+  var selection = editorState.getSelection();
+
+  return EditorState.push(
+    editorState,
+    afterRemoval.set('selectionBefore', selection),
+    selection.isCollapsed() ? 'delete-character' : 'remove-range'
+  );
+}
+
+module.exports = keyCommandPlainDelete;
diff --git a/src/component/handlers/edit/commands/keyCommandTransposeCharacters.js b/src/component/handlers/edit/commands/keyCommandTransposeCharacters.js
new file mode 100644
index 0000000..71c4e97
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandTransposeCharacters.js
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandTransposeCharacters
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+var getContentStateFragment = require('getContentStateFragment');
+
+/**
+ * Transpose the characters on either side of a collapsed cursor, or
+ * if the cursor is at the end of the block, transpose the last two
+ * characters.
+ */
+function keyCommandTransposeCharacters(editorState: EditorState): EditorState {
+  var selection = editorState.getSelection();
+  if (!selection.isCollapsed()) {
+    return editorState;
+  }
+
+  var offset = selection.getAnchorOffset();
+  if (offset === 0) {
+    return editorState;
+  }
+
+  var blockKey = selection.getAnchorKey();
+  var content = editorState.getCurrentContent();
+  var block = content.getBlockForKey(blockKey);
+  var length = block.getLength();
+
+  // Nothing to transpose if there aren't two characters.
+  if (length <= 1) {
+    return editorState;
+  }
+
+  var removalRange;
+  var finalSelection;
+
+  if (offset === length) {
+    // The cursor is at the end of the block. Swap the last two characters.
+    removalRange = selection.set('anchorOffset', offset - 1);
+    finalSelection = selection;
+  } else {
+    removalRange = selection.set('focusOffset', offset + 1);
+    finalSelection = removalRange.set('anchorOffset', offset + 1);
+  }
+
+  // Extract the character to move as a fragment. This preserves its
+  // styling and entity, if any.
+  var movedFragment = getContentStateFragment(content, removalRange);
+  var afterRemoval = DraftModifier.removeRange(
+    content,
+    removalRange,
+    'backward'
+  );
+
+  // After the removal, the insertion target is one character back.
+  var selectionAfter = afterRemoval.getSelectionAfter();
+  var targetOffset = selectionAfter.getAnchorOffset() - 1;
+  var targetRange = selectionAfter.merge({
+    anchorOffset: targetOffset,
+    focusOffset: targetOffset,
+  });
+
+  var afterInsert = DraftModifier.replaceWithFragment(
+    afterRemoval,
+    targetRange,
+    movedFragment
+  );
+
+  var newEditorState = EditorState.push(
+    editorState,
+    afterInsert,
+    'insert-fragment'
+  );
+
+  return EditorState.acceptSelection(newEditorState, finalSelection);
+}
+
+module.exports = keyCommandTransposeCharacters;
diff --git a/src/component/handlers/edit/commands/keyCommandUndo.js b/src/component/handlers/edit/commands/keyCommandUndo.js
new file mode 100644
index 0000000..55b2559
--- /dev/null
+++ b/src/component/handlers/edit/commands/keyCommandUndo.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule keyCommandUndo
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+function keyCommandUndo(
+  e: SyntheticKeyboardEvent,
+  editorState: EditorState,
+  updateFn: (editorState: EditorState) => void
+): void {
+  var undoneState = EditorState.undo(editorState);
+
+  // If the last change to occur was a spellcheck change, allow the undo
+  // event to fall through to the browser. This allows the browser to record
+  // the unwanted change, which should soon lead it to learn not to suggest
+  // the correction again.
+  if (editorState.getLastChangeType() === 'spellcheck-change') {
+    var nativelyRenderedContent = undoneState.getCurrentContent();
+    updateFn(EditorState.set(undoneState, {nativelyRenderedContent}));
+    return;
+  }
+
+  // Otheriwse, manage the undo behavior manually.
+  e.preventDefault();
+  if (!editorState.getNativelyRenderedContent()) {
+    updateFn(undoneState);
+    return;
+  }
+
+  // Trigger a re-render with the current content state to ensure that the
+  // component tree has up-to-date props for comparison.
+  updateFn(EditorState.set(editorState, {nativelyRenderedContent: null}));
+
+  // Wait to ensure that the re-render has occurred before performing
+  // the undo action.
+  setTimeout(() => {
+    updateFn(undoneState);
+  }, 0);
+}
+
+module.exports = keyCommandUndo;
diff --git a/src/component/handlers/edit/commands/moveSelectionBackward.js b/src/component/handlers/edit/commands/moveSelectionBackward.js
new file mode 100644
index 0000000..1e3f08d
--- /dev/null
+++ b/src/component/handlers/edit/commands/moveSelectionBackward.js
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule moveSelectionBackward
+ * @flow
+ */
+
+'use strict';
+
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Given a collapsed selection, move the focus `maxDistance` backward within
+ * the selected block. If the selection will go beyond the start of the block,
+ * move focus to the end of the previous block, but no further.
+ *
+ * This function is not Unicode-aware, so surrogate pairs will be treated
+ * as having length 2.
+ */
+function moveSelectionBackward(
+  editorState: EditorState,
+  maxDistance: number
+): SelectionState {
+  var selection = editorState.getSelection();
+  var content = editorState.getCurrentContent();
+  var key = selection.getStartKey();
+  var offset = selection.getStartOffset();
+
+  var focusKey = key;
+  var focusOffset = 0;
+
+  if (maxDistance > offset) {
+    var keyBefore = content.getKeyBefore(key);
+    if (keyBefore == null) {
+      focusKey = key;
+    } else {
+      focusKey = keyBefore;
+      var blockBefore = content.getBlockForKey(keyBefore);
+      focusOffset = blockBefore.getText().length;
+    }
+  } else {
+    focusOffset = offset - maxDistance;
+  }
+
+  return selection.merge({
+    focusKey,
+    focusOffset,
+    isBackward: true,
+  });
+}
+
+module.exports = moveSelectionBackward;
diff --git a/src/component/handlers/edit/commands/moveSelectionForward.js b/src/component/handlers/edit/commands/moveSelectionForward.js
new file mode 100644
index 0000000..8a4717a
--- /dev/null
+++ b/src/component/handlers/edit/commands/moveSelectionForward.js
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule moveSelectionForward
+ * @flow
+ */
+
+'use strict';
+
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Given a collapsed selection, move the focus `maxDistance` forward within
+ * the selected block. If the selection will go beyond the end of the block,
+ * move focus to the start of the next block, but no further.
+ *
+ * This function is not Unicode-aware, so surrogate pairs will be treated
+ * as having length 2.
+ */
+function moveSelectionForward(
+  editorState: EditorState,
+  maxDistance: number
+): SelectionState {
+  var selection = editorState.getSelection();
+  var key = selection.getStartKey();
+  var offset = selection.getStartOffset();
+  var content = editorState.getCurrentContent();
+
+  var focusKey = key;
+  var focusOffset;
+
+  var block = content.getBlockForKey(key);
+
+  if (maxDistance > (block.getText().length - offset)) {
+    focusKey = content.getKeyAfter(key);
+    focusOffset = 0;
+  } else {
+    focusOffset = offset + maxDistance;
+  }
+
+  return selection.merge({focusKey, focusOffset});
+}
+
+module.exports = moveSelectionForward;
diff --git a/src/component/handlers/edit/commands/removeTextWithStrategy.js b/src/component/handlers/edit/commands/removeTextWithStrategy.js
new file mode 100644
index 0000000..d2d9857
--- /dev/null
+++ b/src/component/handlers/edit/commands/removeTextWithStrategy.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule removeTextWithStrategy
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+
+import type ContentState from 'ContentState';
+import type {DraftRemovalDirection} from 'DraftRemovalDirection';
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * For a collapsed selection state, remove text based on the specified strategy.
+ * If the selection state is not collapsed, remove the entire selected range.
+ */
+function removeTextWithStrategy(
+  editorState: EditorState,
+  strategy: (editorState: EditorState) => SelectionState,
+  direction: DraftRemovalDirection
+): ContentState {
+  var selection = editorState.getSelection();
+  var content = editorState.getCurrentContent();
+  var target = selection;
+  if (selection.isCollapsed()) {
+    if (direction === 'forward') {
+      if (editorState.isSelectionAtEndOfContent()) {
+        return content;
+      }
+    } else if (editorState.isSelectionAtStartOfContent()) {
+      return content;
+    }
+
+    target = strategy(editorState);
+    if (target === selection) {
+      return content;
+    }
+  }
+  return DraftModifier.removeRange(content, target, direction);
+}
+
+module.exports = removeTextWithStrategy;
diff --git a/src/component/handlers/edit/editOnBeforeInput.js b/src/component/handlers/edit/editOnBeforeInput.js
new file mode 100644
index 0000000..516882a
--- /dev/null
+++ b/src/component/handlers/edit/editOnBeforeInput.js
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnBeforeInput
+ * @flow
+ */
+
+'use strict';
+
+var BlockTree = require('BlockTree');
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var getEntityKeyForSelection = require('getEntityKeyForSelection');
+var isSelectionAtLeafStart = require('isSelectionAtLeafStart');
+var nullthrows = require('nullthrows');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+// When nothing is focused, Firefox regards two characters, `'` and `/`, as
+// commands that should open and focus the "quickfind" search bar. This should
+// *never* happen while a contenteditable is focused, but as of v28, it
+// sometimes does, even when the keypress event target is the contenteditable.
+// This breaks the input. Special case these characters to ensure that when
+// they are typed, we prevent default on the event to make sure not to
+// trigger quickfind.
+var FF_QUICKFIND_CHAR = '\'';
+var FF_QUICKFIND_LINK_CHAR = '\/';
+var isFirefox = UserAgent.isBrowser('Firefox');
+
+function mustPreventDefaultForCharacter(character: string): boolean {
+  return (
+    isFirefox && (
+      character == FF_QUICKFIND_CHAR ||
+      character == FF_QUICKFIND_LINK_CHAR
+    )
+  );
+}
+
+/**
+ * Replace the current selection with the specified text string, with the
+ * inline style and entity key applied to the newly inserted text.
+ */
+function replaceText(
+  editorState: EditorState,
+  text: string,
+  inlineStyle: DraftInlineStyle,
+  entityKey: ?string
+): EditorState {
+  var contentState = DraftModifier.replaceText(
+    editorState.getCurrentContent(),
+    editorState.getSelection(),
+    text,
+    inlineStyle,
+    entityKey
+  );
+  return EditorState.push(editorState, contentState, 'insert-characters');
+}
+
+/**
+ * When `onBeforeInput` executes, the browser is attempting to insert a
+ * character into the editor. Apply this character data to the document,
+ * allowing native insertion if possible.
+ *
+ * Native insertion is encouraged in order to limit re-rendering and to
+ * preserve spellcheck highlighting, which disappears or flashes if re-render
+ * occurs on the relevant text nodes.
+ */
+function editOnBeforeInput(e: SyntheticInputEvent): void {
+  var chars = e.data;
+
+  // In some cases (ex: IE ideographic space insertion) no character data
+  // is provided. There's nothing to do when this happens.
+  if (!chars) {
+    return;
+  }
+
+  // Allow the top-level component to handle the insertion manually. This is
+  // useful when triggering interesting behaviors for a character insertion,
+  // Simple examples: replacing a raw text ':)' with a smile emoji or image
+  // decorator, or setting a block to be a list item after typing '- ' at the
+  // start of the block.
+  if (this.props.handleBeforeInput && this.props.handleBeforeInput(chars)) {
+    e.preventDefault();
+    return;
+  }
+
+  // If selection is collapsed, conditionally allow native behavior. This
+  // reduces re-renders and preserves spellcheck highlighting. If the selection
+  // is not collapsed, we will re-render.
+  var editorState = this.props.editorState;
+  var selection = editorState.getSelection();
+
+  if (!selection.isCollapsed()) {
+    e.preventDefault();
+    this.update(
+      replaceText(
+        editorState,
+        chars,
+        editorState.getCurrentInlineStyle(),
+        getEntityKeyForSelection(
+          editorState.getCurrentContent(),
+          editorState.getSelection()
+        )
+      )
+    );
+    return;
+  }
+
+  var mayAllowNative = !isSelectionAtLeafStart(editorState);
+  var newEditorState = replaceText(
+    editorState,
+    chars,
+    editorState.getCurrentInlineStyle(),
+    getEntityKeyForSelection(
+      editorState.getCurrentContent(),
+      editorState.getSelection()
+    )
+  );
+
+  if (!mayAllowNative) {
+    e.preventDefault();
+    this.update(newEditorState);
+    return;
+  }
+
+  var anchorKey = selection.getAnchorKey();
+  var anchorTree = editorState.getBlockTree(anchorKey);
+
+  // Check the old and new "fingerprints" of the current block to determine
+  // whether this insertion requires any addition or removal of text nodes,
+  // in which case we would prevent the native character insertion.
+  var originalFingerprint = BlockTree.getFingerprint(anchorTree);
+  var newFingerprint = BlockTree.getFingerprint(
+    newEditorState.getBlockTree(anchorKey)
+  );
+
+  if (
+    mustPreventDefaultForCharacter(chars) ||
+    originalFingerprint !== newFingerprint ||
+    (
+      nullthrows(newEditorState.getDirectionMap()).get(anchorKey) !==
+      nullthrows(editorState.getDirectionMap()).get(anchorKey)
+    )
+  ) {
+    e.preventDefault();
+  } else {
+    // The native event is allowed to occur.
+    newEditorState = EditorState.set(newEditorState, {
+      nativelyRenderedContent: newEditorState.getCurrentContent(),
+    });
+  }
+
+  this.update(newEditorState);
+}
+
+module.exports = editOnBeforeInput;
diff --git a/src/component/handlers/edit/editOnBlur.js b/src/component/handlers/edit/editOnBlur.js
new file mode 100644
index 0000000..9234c09
--- /dev/null
+++ b/src/component/handlers/edit/editOnBlur.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnBlur
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var getActiveElement = require('getActiveElement');
+
+var isWebKit = UserAgent.isEngine('WebKit');
+
+function editOnBlur(e: SyntheticEvent): void {
+  // Webkit has a bug in which blurring a contenteditable by clicking on
+  // other active elements will trigger the `blur` event but will not remove
+  // the DOM selection from the contenteditable. We therefore force the
+  // issue to be certain, checking whether the active element is `body`
+  // to force it when blurring occurs within the window (as opposed to
+  // clicking to another tab or window).
+  if (isWebKit && getActiveElement() === document.body) {
+    global.getSelection().removeAllRanges();
+  }
+
+  var editorState = this.props.editorState;
+  var currentSelection = editorState.getSelection();
+  if (!currentSelection.getHasFocus()) {
+    return;
+  }
+
+  var selection = currentSelection.set('hasFocus', false);
+  this.props.onBlur && this.props.onBlur(e);
+  this.update(EditorState.acceptSelection(editorState, selection));
+}
+
+module.exports = editOnBlur;
diff --git a/src/component/handlers/edit/editOnCompositionStart.js b/src/component/handlers/edit/editOnCompositionStart.js
new file mode 100644
index 0000000..18d1578
--- /dev/null
+++ b/src/component/handlers/edit/editOnCompositionStart.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCompositionStart
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+/**
+ * The user has begun using an IME input system. Switching to `composite` mode
+ * allows handling composition input and disables other edit behavior.
+ */
+function editOnCompositionStart(): void {
+  this.setRenderGuard();
+  this.setMode('composite');
+  this.update(
+    EditorState.set(this.props.editorState, {inCompositionMode: true})
+  );
+}
+
+module.exports = editOnCompositionStart;
diff --git a/src/component/handlers/edit/editOnCopy.js b/src/component/handlers/edit/editOnCopy.js
new file mode 100644
index 0000000..3178802
--- /dev/null
+++ b/src/component/handlers/edit/editOnCopy.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCopy
+ * @flow
+ */
+
+'use strict';
+
+var getFragmentFromSelection = require('getFragmentFromSelection');
+
+/**
+ * If we have a selection, create a ContentState fragment and store
+ * it in our internal clipboard. Subsequent paste events will use this
+ * fragment if no external clipboard data is supplied.
+ */
+function editOnCopy(e: SyntheticClipboardEvent): void {
+  var editorState = this.props.editorState;
+  var selection = editorState.getSelection();
+
+  // No selection, so there's nothing to copy.
+  if (selection.isCollapsed()) {
+    e.preventDefault();
+    return;
+  }
+
+  this.setClipboard(getFragmentFromSelection(this.props.editorState));
+}
+
+module.exports = editOnCopy;
diff --git a/src/component/handlers/edit/editOnCut.js b/src/component/handlers/edit/editOnCut.js
new file mode 100644
index 0000000..e5431eb
--- /dev/null
+++ b/src/component/handlers/edit/editOnCut.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnCut
+ * @flow
+ */
+
+'use strict';
+
+const DraftModifier = require('DraftModifier');
+const EditorState = require('EditorState');
+const Style = require('Style');
+
+const getFragmentFromSelection = require('getFragmentFromSelection');
+const getScrollPosition = require('getScrollPosition');
+
+/**
+ * On `cut` events, native behavior is allowed to occur so that the system
+ * clipboard is set properly. This means that we need to take steps to recover
+ * the editor DOM state after the `cut` has occurred in order to maintain
+ * control of the component.
+ *
+ * In addition, we can keep a copy of the removed fragment, including all
+ * styles and entities, for use as an internal paste.
+ */
+function editOnCut(e: SyntheticClipboardEvent): void {
+  const editorState = this.props.editorState;
+  const selection = editorState.getSelection();
+
+  // No selection, so there's nothing to cut.
+  if (selection.isCollapsed()) {
+    e.preventDefault();
+    return;
+  }
+
+  // Track the current scroll position so that it can be forced back in place
+  // after the editor regains control of the DOM.
+  const scrollParent = Style.getScrollParent(e.target);
+  const {x, y} = getScrollPosition(scrollParent);
+
+  const fragment = getFragmentFromSelection(editorState);
+  this.setClipboard(fragment);
+
+  // Set `cut` mode to disable all event handling temporarily.
+  this.setRenderGuard();
+  this.setMode('cut');
+
+  // Let native `cut` behavior occur, then recover control.
+  setTimeout(() => {
+    this.restoreEditorDOM({x, y});
+    this.removeRenderGuard();
+    this.exitCurrentMode();
+    this.update(removeFragment(editorState));
+  }, 0);
+}
+
+function removeFragment(editorState: EditorState): EditorState {
+  const newContent = DraftModifier.removeRange(
+    editorState.getCurrentContent(),
+    editorState.getSelection(),
+    'forward'
+  );
+  return EditorState.push(editorState, newContent, 'remove-range');
+}
+
+module.exports = editOnCut;
diff --git a/src/component/handlers/edit/editOnDragOver.js b/src/component/handlers/edit/editOnDragOver.js
new file mode 100644
index 0000000..37fd32f
--- /dev/null
+++ b/src/component/handlers/edit/editOnDragOver.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnDragOver
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * Drag behavior has begun from outside the editor element.
+ */
+function editOnDragOver(e: SyntheticDragEvent): void {
+  this._internalDrag = false;
+  this.setMode('drag');
+  e.preventDefault();
+}
+
+module.exports = editOnDragOver;
diff --git a/src/component/handlers/edit/editOnDragStart.js b/src/component/handlers/edit/editOnDragStart.js
new file mode 100644
index 0000000..44be4e1
--- /dev/null
+++ b/src/component/handlers/edit/editOnDragStart.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnDragStart
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * A `dragstart` event has begun within the text editor component.
+ */
+function editOnDragStart(): void {
+  this._internalDrag = true;
+  this.setMode('drag');
+}
+
+module.exports = editOnDragStart;
diff --git a/src/component/handlers/edit/editOnFocus.js b/src/component/handlers/edit/editOnFocus.js
new file mode 100644
index 0000000..cc3f726
--- /dev/null
+++ b/src/component/handlers/edit/editOnFocus.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnFocus
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+
+function editOnFocus(e: SyntheticFocusEvent): void {
+  var editorState = this.props.editorState;
+  var currentSelection = editorState.getSelection();
+  if (currentSelection.getHasFocus()) {
+    return;
+  }
+
+  var selection = currentSelection.set('hasFocus', true);
+  this.props.onFocus && this.props.onFocus(e);
+
+  // When the tab containing this text editor is hidden and the user does a
+  // find-in-page in a _different_ tab, Chrome on Mac likes to forget what the
+  // selection was right after sending this focus event and (if you let it)
+  // moves the cursor back to the beginning of the editor, so we force the
+  // selection here instead of simply accepting it in order to preserve the
+  // old cursor position. See https://crbug.com/540004.
+  this.update(EditorState.forceSelection(editorState, selection));
+}
+
+module.exports = editOnFocus;
diff --git a/src/component/handlers/edit/editOnInput.js b/src/component/handlers/edit/editOnInput.js
new file mode 100644
index 0000000..1878647
--- /dev/null
+++ b/src/component/handlers/edit/editOnInput.js
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnInput
+ * @flow
+ */
+
+'use strict';
+
+var DraftModifier = require('DraftModifier');
+var DraftOffsetKey = require('DraftOffsetKey');
+var EditorState = require('EditorState');
+var UserAgent = require('UserAgent');
+
+var findAncestorOffsetKey = require('findAncestorOffsetKey');
+var nullthrows = require('nullthrows');
+
+var isGecko = UserAgent.isEngine('Gecko');
+
+var DOUBLE_NEWLINE = '\n\n';
+
+/**
+ * This function is intended to handle spellcheck and autocorrect changes,
+ * which occur in the DOM natively without any opportunity to observe or
+ * interpret the changes before they occur.
+ *
+ * The `input` event fires in contentEditable elements reliably for non-IE
+ * browsers, immediately after changes occur to the editor DOM. Since our other
+ * handlers override or otherwise handle cover other varieties of text input,
+ * the DOM state should match the model in all controlled input cases. Thus,
+ * when an `input` change leads to a DOM/model mismatch, the change should be
+ * due to a spellcheck change, and we can incorporate it into our model.
+ */
+function editOnInput(): void {
+  var domSelection = global.getSelection();
+
+  var {anchorNode, isCollapsed} = domSelection;
+  if (anchorNode.nodeType !== Node.TEXT_NODE) {
+    return;
+  }
+
+  var domText = anchorNode.textContent;
+  var {editorState} = this.props;
+  var offsetKey = nullthrows(findAncestorOffsetKey(anchorNode));
+  var {blockKey, decoratorKey, leafKey} = DraftOffsetKey.decode(offsetKey);
+
+  var {start, end} = editorState
+    .getBlockTree(blockKey)
+    .getIn([decoratorKey, 'leaves', leafKey]);
+
+  var content = editorState.getCurrentContent();
+  var block = content.getBlockForKey(blockKey);
+  var modelText = block.getText().slice(start, end);
+
+  // Special-case soft newlines here. If the DOM text ends in a soft newline,
+  // we will have manually inserted an extra soft newline in DraftEditorLeaf.
+  // We want to remove this extra newline for the purpose of our comparison
+  // of DOM and model text.
+  if (domText.endsWith(DOUBLE_NEWLINE)) {
+    domText = domText.slice(0, -1);
+  }
+
+  // No change -- the DOM is up to date. Nothing to do here.
+  if (domText === modelText) {
+    return;
+  }
+
+  var selection = editorState.getSelection();
+
+  // We'll replace the entire leaf with the text content of the target.
+  var targetRange = selection.merge({
+    anchorOffset: start,
+    focusOffset: end,
+    isBackward: false,
+  });
+
+  var newContent = DraftModifier.replaceText(
+    content,
+    targetRange,
+    domText,
+    block.getInlineStyleAt(start)
+  );
+
+  var anchorOffset, focusOffset, startOffset, endOffset;
+
+  if (isGecko) {
+    // Firefox selection does not change while the context menu is open, so
+    // we preserve the anchor and focus values of the DOM selection.
+    anchorOffset = domSelection.anchorOffset;
+    focusOffset = domSelection.focusOffset;
+    startOffset = start + Math.min(anchorOffset, focusOffset);
+    endOffset = startOffset + Math.abs(anchorOffset - focusOffset);
+    anchorOffset = startOffset;
+    focusOffset = endOffset;
+  } else {
+    // Browsers other than Firefox may adjust DOM selection while the context
+    // menu is open, and Safari autocorrect is prone to providing an inaccurate
+    // DOM selection. Don't trust it. Instead, use our existing SelectionState
+    // and adjust it based on the number of characters changed during the
+    // mutation.
+    var charDelta = domText.length - modelText.length;
+    startOffset = selection.getStartOffset();
+    endOffset = selection.getEndOffset();
+
+    anchorOffset = isCollapsed ? endOffset + charDelta : startOffset;
+    focusOffset = endOffset + charDelta;
+  }
+
+  var contentWithAdjustedDOMSelection = newContent.merge({
+    selectionBefore: content.getSelectionAfter(),
+    selectionAfter: selection.merge({anchorOffset, focusOffset}),
+  });
+
+  this.update(
+    EditorState.push(
+      editorState,
+      contentWithAdjustedDOMSelection,
+      'spellcheck-change'
+    )
+  );
+}
+
+module.exports = editOnInput;
diff --git a/src/component/handlers/edit/editOnKeyDown.js b/src/component/handlers/edit/editOnKeyDown.js
new file mode 100644
index 0000000..f3f40ed
--- /dev/null
+++ b/src/component/handlers/edit/editOnKeyDown.js
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnKeyDown
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var Keys = require('Keys');
+var SecondaryClipboard = require('SecondaryClipboard');
+
+var keyCommandBackspaceToStartOfLine = require('keyCommandBackspaceToStartOfLine');
+var keyCommandBackspaceWord = require('keyCommandBackspaceWord');
+var keyCommandDeleteWord = require('keyCommandDeleteWord');
+var keyCommandInsertNewline = require('keyCommandInsertNewline');
+var keyCommandPlainBackspace = require('keyCommandPlainBackspace');
+var keyCommandPlainDelete = require('keyCommandPlainDelete');
+var keyCommandMoveSelectionToEndOfBlock = require('keyCommandMoveSelectionToEndOfBlock');
+var keyCommandMoveSelectionToStartOfBlock = require('keyCommandMoveSelectionToStartOfBlock');
+var keyCommandTransposeCharacters = require('keyCommandTransposeCharacters');
+var keyCommandUndo = require('keyCommandUndo');
+
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+
+/**
+ * Map a `DraftEditorCommand` command value to a corresponding function.
+ */
+function onKeyCommand(
+  command: DraftEditorCommand,
+  editorState: EditorState
+): EditorState {
+  switch (command) {
+    case 'redo':
+      return EditorState.redo(editorState);
+    case 'delete':
+      return keyCommandPlainDelete(editorState);
+    case 'delete-word':
+      return keyCommandDeleteWord(editorState);
+    case 'backspace':
+      return keyCommandPlainBackspace(editorState);
+    case 'backspace-word':
+      return keyCommandBackspaceWord(editorState);
+    case 'backspace-to-start-of-line':
+      return keyCommandBackspaceToStartOfLine(editorState);
+    case 'split-block':
+      return keyCommandInsertNewline(editorState);
+    case 'transpose-characters':
+      return keyCommandTransposeCharacters(editorState);
+    case 'move-selection-to-start-of-block':
+      return keyCommandMoveSelectionToStartOfBlock(editorState);
+    case 'move-selection-to-end-of-block':
+      return keyCommandMoveSelectionToEndOfBlock(editorState);
+    case 'secondary-cut':
+      return SecondaryClipboard.cut(editorState);
+    case 'secondary-paste':
+      return SecondaryClipboard.paste(editorState);
+    default:
+      return editorState;
+  }
+}
+
+/**
+ * Intercept keydown behavior to handle keys and commands manually, if desired.
+ *
+ * Keydown combinations may be mapped to `DraftCommand` values, which may
+ * correspond to command functions that modify the editor or its contents.
+ *
+ * See `getDefaultKeyBinding` for defaults. Alternatively, the top-level
+ * component may provide a custom mapping via the `keyBindingFn` prop.
+ */
+function editOnKeyDown(e: SyntheticKeyboardEvent): void {
+  var keyCode = e.which;
+  var editorState = this.props.editorState;
+
+  switch (keyCode) {
+    case Keys.RETURN:
+      e.preventDefault();
+      // The top-level component may manually handle newline insertion. If
+      // no special handling is performed, insert a newline.
+      if (!this.props.handleReturn || !this.props.handleReturn(e)) {
+        this.update(keyCommandInsertNewline(editorState));
+      }
+      return;
+    case Keys.ESC:
+      e.preventDefault();
+      this.props.onEscape && this.props.onEscape(e);
+      return;
+    case Keys.TAB:
+      this.props.onTab && this.props.onTab(e);
+      return;
+    case Keys.UP:
+      this.props.onUpArrow && this.props.onUpArrow(e);
+      return;
+    case Keys.DOWN:
+      this.props.onDownArrow && this.props.onDownArrow(e);
+      return;
+  }
+
+  var command = this.props.keyBindingFn(e);
+
+  // If no command is specified, allow keydown event to continue.
+  if (!command) {
+    return;
+  }
+
+  if (command === 'undo') {
+    // Since undo requires some special updating behavior to keep the editor
+    // in sync, handle it separately.
+    keyCommandUndo(e, editorState, this.update);
+    return;
+  }
+
+  // At this point, we know that we're handling a command of some kind, so
+  // we don't want to insert a character following the keydown.
+  e.preventDefault();
+
+  // Allow components higher up the tree to handle the command first.
+  if (this.props.handleKeyCommand && this.props.handleKeyCommand(command)) {
+    return;
+  }
+
+  var newState = onKeyCommand(command, editorState);
+  if (newState !== editorState) {
+    this.update(newState);
+  }
+}
+
+module.exports = editOnKeyDown;
diff --git a/src/component/handlers/edit/editOnPaste.js b/src/component/handlers/edit/editOnPaste.js
new file mode 100644
index 0000000..14f3044
--- /dev/null
+++ b/src/component/handlers/edit/editOnPaste.js
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnPaste
+ * @flow
+ */
+
+'use strict';
+
+var BlockMapBuilder = require('BlockMapBuilder');
+var CharacterMetadata = require('CharacterMetadata');
+var DataTransfer = require('DataTransfer');
+var DraftModifier = require('DraftModifier');
+var DraftPasteProcessor = require('DraftPasteProcessor');
+var EditorState = require('EditorState');
+
+var getEntityKeyForSelection = require('getEntityKeyForSelection');
+var getTextContentFromFiles = require('getTextContentFromFiles');
+var nullthrows = require('nullthrows');
+var splitTextIntoTextBlocks = require('splitTextIntoTextBlocks');
+
+import type {BlockMap} from 'BlockMap';
+
+/**
+ * Paste content.
+ */
+function editOnPaste(e: SyntheticClipboardEvent): void {
+  e.preventDefault();
+  var data = new DataTransfer(e.clipboardData);
+
+  // Get files, unless this is likely to be a string the user wants inline.
+  if (!data.isRichText()) {
+    var files = data.getFiles();
+    var defaultFileText = data.getText();
+    if (files.length > 0) {
+      // Allow customized paste handling for images, etc. Otherwise, fall
+      // through to insert text contents into the editor.
+      if (
+        this.props.handlePastedFiles &&
+        this.props.handlePastedFiles(files)
+      ) {
+        return;
+      }
+
+      getTextContentFromFiles(files, (/*string*/ fileText) => {
+        fileText = fileText || defaultFileText;
+        if (!fileText) {
+          return;
+        }
+
+        var {editorState} = this.props;
+        var blocks = splitTextIntoTextBlocks(fileText);
+        var character = CharacterMetadata.create({
+          style: editorState.getCurrentInlineStyle(),
+          entity: getEntityKeyForSelection(
+            editorState.getCurrentContent(),
+            editorState.getSelection()
+          ),
+        });
+
+        var text = DraftPasteProcessor.processText(blocks, character);
+        var fragment = BlockMapBuilder.createFromArray(text);
+
+        var withInsertedText = DraftModifier.replaceWithFragment(
+          editorState.getCurrentContent(),
+          editorState.getSelection(),
+          fragment
+        );
+
+        this.update(
+          EditorState.push(
+            editorState,
+            withInsertedText,
+            'insert-fragment'
+          )
+        );
+      });
+
+      return;
+    }
+  }
+
+  var textBlocks: ?Array<string> = null;
+  var text = data.getText();
+  this.props.onPasteRawText && this.props.onPasteRawText(text);
+  if (text) {
+    textBlocks = splitTextIntoTextBlocks(text);
+  }
+
+  if (!this.props.stripPastedStyles) {
+    // If the text from the paste event is rich content that matches what we
+    // already have on the internal clipboard, assume that we should just use
+    // the clipboard fragment for the paste. This will allow us to preserve
+    // styling and entities, if any are present. Note that newlines are
+    // stripped during comparison -- this is because copy/paste within the
+    // editor in Firefox and IE will not include empty lines. The resulting
+    // paste will preserve the newlines correctly.
+    if (data.isRichText() && this.getClipboard()) {
+      textBlocks = nullthrows(textBlocks);
+      var textBlocksWithoutNewlines = textBlocks.filter(filterOutNewlines);
+      var currentClipboard = this.getClipboard();
+      var clipboardWithoutNewlines = currentClipboard
+        .toSeq()
+        .map(clipBlock => clipBlock.getText())
+        .filter(filterOutNewlines)
+        .toArray();
+
+      var clipboardMatch = textBlocksWithoutNewlines.every(
+        (line, ii) => line === clipboardWithoutNewlines[ii]
+      );
+
+      if (clipboardMatch) {
+        this.update(
+          insertFragment(this.props.editorState, currentClipboard)
+        );
+        return;
+      }
+    }
+
+    // If there is html paste data, try to parse that.
+    var htmlData = data.getHTML();
+    if (htmlData) {
+      var htmlFragment = DraftPasteProcessor.processHTML(
+        htmlData,
+      );
+
+      if (htmlFragment) {
+        var htmlMap = BlockMapBuilder.createFromArray(htmlFragment);
+        this.update(insertFragment(this.props.editorState, htmlMap));
+        return;
+      }
+    }
+    // Otherwise, create a new fragment from our pasted text. Also
+    // empty the internal clipboard, since it's no longer valid.
+    this.setClipboard(null);
+  }
+
+  if (textBlocks) {
+    var {editorState} = this.props;
+    var character = CharacterMetadata.create({
+      style: editorState.getCurrentInlineStyle(),
+      entity: getEntityKeyForSelection(
+        editorState.getCurrentContent(),
+        editorState.getSelection()
+      ),
+    });
+
+    var textFragment = DraftPasteProcessor.processText(
+      textBlocks,
+      character
+    );
+
+    var textMap = BlockMapBuilder.createFromArray(textFragment);
+    this.update(insertFragment(this.props.editorState, textMap));
+  }
+}
+
+function filterOutNewlines(str: string): boolean {
+  return str.length > 0;
+}
+
+function insertFragment(
+  editorState: EditorState,
+  fragment: BlockMap
+): EditorState {
+  var newContent = DraftModifier.replaceWithFragment(
+    editorState.getCurrentContent(),
+    editorState.getSelection(),
+    fragment
+  );
+  return EditorState.push(
+    editorState,
+    newContent,
+    'insert-fragment'
+  );
+}
+
+module.exports = editOnPaste;
diff --git a/src/component/handlers/edit/editOnSelect.js b/src/component/handlers/edit/editOnSelect.js
new file mode 100644
index 0000000..c942f06
--- /dev/null
+++ b/src/component/handlers/edit/editOnSelect.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule editOnSelect
+ * @flow
+ */
+
+'use strict';
+
+var EditorState = require('EditorState');
+var ReactDOM = require('ReactDOM');
+
+var getDraftEditorSelection = require('getDraftEditorSelection');
+
+function editOnSelect(): void {
+  if (this._blockSelectEvents) {
+    return;
+  }
+
+  var editorState = this.props.editorState;
+  var documentSelection = getDraftEditorSelection(
+    editorState,
+    ReactDOM.findDOMNode(this.refs.editorContainer).firstChild
+  );
+  var updatedSelectionState = documentSelection.selectionState;
+
+  if (updatedSelectionState !== editorState.getSelection()) {
+    if (documentSelection.needsRecovery) {
+      editorState = EditorState.forceSelection(
+        editorState,
+        updatedSelectionState
+      );
+    } else {
+      editorState = EditorState.acceptSelection(
+        editorState,
+        updatedSelectionState
+      );
+    }
+    this.update(editorState);
+  }
+}
+
+module.exports = editOnSelect;
diff --git a/src/component/handlers/edit/getFragmentFromSelection.js b/src/component/handlers/edit/getFragmentFromSelection.js
new file mode 100644
index 0000000..0c54d5a
--- /dev/null
+++ b/src/component/handlers/edit/getFragmentFromSelection.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getFragmentFromSelection
+ * @flow
+ */
+
+'use strict';
+
+var getContentStateFragment = require('getContentStateFragment');
+
+import type {BlockMap} from 'BlockMap';
+import type EditorState from 'EditorState';
+
+function getFragmentFromSelection(editorState: EditorState): ?BlockMap {
+  var selectionState = editorState.getSelection();
+  if (!selectionState.isCollapsed()) {
+    return getContentStateFragment(
+      editorState.getCurrentContent(),
+      selectionState
+    );
+  }
+}
+
+module.exports = getFragmentFromSelection;
diff --git a/src/component/selection/DOMDerivedSelection.js b/src/component/selection/DOMDerivedSelection.js
new file mode 100644
index 0000000..40f9ccf
--- /dev/null
+++ b/src/component/selection/DOMDerivedSelection.js
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DOMDerivedSelection
+ * @flow
+ */
+
+'use strict';
+
+import type SelectionState from 'SelectionState';
+
+export type DOMDerivedSelection = {
+  selectionState: SelectionState;
+  needsRecovery: boolean;
+};
diff --git a/src/component/selection/DraftOffsetKey.js b/src/component/selection/DraftOffsetKey.js
new file mode 100644
index 0000000..626c683
--- /dev/null
+++ b/src/component/selection/DraftOffsetKey.js
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftOffsetKey
+ * @flow
+ */
+
+'use strict';
+
+import type {DraftOffsetKeyPath} from 'DraftOffsetKeyPath';
+
+var KEY_DELIMITER = '-';
+
+var DraftOffsetKey = {
+  encode: function(
+    blockKey: string,
+    decoratorKey: number,
+    leafKey: number
+  ): string {
+    return blockKey + KEY_DELIMITER + decoratorKey + KEY_DELIMITER + leafKey;
+  },
+
+  decode: function(offsetKey: string): DraftOffsetKeyPath {
+    var [blockKey, decoratorKey, leafKey] = offsetKey.split(KEY_DELIMITER);
+    return {
+      blockKey,
+      decoratorKey: parseInt(decoratorKey, 10),
+      leafKey: parseInt(leafKey, 10),
+    };
+  },
+};
+
+module.exports = DraftOffsetKey;
diff --git a/src/component/selection/DraftOffsetKeyPath.js b/src/component/selection/DraftOffsetKeyPath.js
new file mode 100644
index 0000000..60e17cf
--- /dev/null
+++ b/src/component/selection/DraftOffsetKeyPath.js
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftOffsetKeyPath
+ * @flow
+ */
+
+'use strict';
+
+export type DraftOffsetKeyPath = {
+  blockKey: string;
+  decoratorKey: number;
+  leafKey: number;
+};
diff --git a/src/component/selection/__tests__/getDraftEditorSelection-test.js b/src/component/selection/__tests__/getDraftEditorSelection-test.js
new file mode 100644
index 0000000..0a5ab03
--- /dev/null
+++ b/src/component/selection/__tests__/getDraftEditorSelection-test.js
@@ -0,0 +1,1277 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var ContentState = require('ContentState');
+var EditorState = require('EditorState');
+var Immutable = require('immutable');
+var SelectionState = require('SelectionState');
+
+var {BOLD} = require('SampleDraftInlineStyle');
+var {EMPTY} = CharacterMetadata;
+
+var getDraftEditorSelection = require('getDraftEditorSelection');
+
+/**
+ * Test possible selection states for the text editor. This is based on
+ * far too many hours of manual testing and bug fixes, and still may not be
+ * a completely accurate representation of all subtle and bizarre differences
+ * in implementations and APIs across browsers and operating systems.
+ *
+ * Welcome to the jungle.
+ */
+describe('getDraftEditorSelection', function() {
+  var editorState;
+  var root;
+  var contents;
+  var blocks;
+  var decorators;
+  var leafs;
+  var leafChildren;
+  var textNodes;
+
+  beforeEach(function() {
+    window.getSelection = jest.genMockFunction();
+    root = document.createElement('div');
+    contents = document.createElement('div');
+    contents.setAttribute('data-contents', 'true');
+    root.appendChild(contents);
+
+    var text = [
+      'Washington',
+      'Jefferson',
+      'Lincoln',
+      'Roosevelt',
+      'Kennedy',
+      'Obama',
+    ];
+
+    var textA = text[0] + text[1];
+    var textB = text[2] + text[3];
+    var textC = text[4] + text[5];
+
+    var boldChar = CharacterMetadata.create({style: BOLD});
+    var aChars = Immutable.List(
+      Immutable.Repeat(EMPTY, text[0].length).concat(
+        Immutable.Repeat(boldChar, text[1].length)
+      )
+    );
+    var bChars = Immutable.List(
+      Immutable.Repeat(EMPTY, text[2].length).concat(
+        Immutable.Repeat(boldChar, text[3].length)
+      )
+    );
+    var cChars = Immutable.List(
+      Immutable.Repeat(EMPTY, text[4].length).concat(
+        Immutable.Repeat(boldChar, text[5].length)
+      )
+    );
+
+    var contentBlocks = [
+      new ContentBlock({
+        key: 'a',
+        type: 'unstyled',
+        text: textA,
+        characterList: aChars,
+      }),
+      new ContentBlock({
+        key: 'b',
+        type: 'unstyled',
+        text: textB,
+        characterList: bChars,
+      }),
+      new ContentBlock({
+        key: 'c',
+        type: 'unstyled',
+        text: textC,
+        characterList: cChars,
+      }),
+    ];
+
+    var contentState = ContentState.createFromBlockArray(contentBlocks);
+    editorState = EditorState.createWithContent(contentState);
+
+    textNodes = text
+      .map(
+        function(text) {
+          return document.createTextNode(text);
+        }
+      );
+    leafChildren = textNodes
+      .map(
+        function(textNode) {
+          var span = document.createElement('span');
+          span.appendChild(textNode);
+          return span;
+        }
+      );
+    leafs = ['a-0-0', 'a-0-1', 'b-0-0', 'b-0-1', 'c-0-0', 'c-0-1']
+      .map(
+        function(blockKey, ii) {
+          var span = document.createElement('span');
+          span.setAttribute('data-offset-key', '' + blockKey);
+          span.appendChild(leafChildren[ii]);
+          return span;
+        }
+      );
+    decorators = ['a-0-0', 'b-0-0', 'c-0-0']
+      .map(
+        function(decoratorKey, ii) {
+          var span = document.createElement('span');
+          span.setAttribute('data-offset-key', '' + decoratorKey);
+          span.appendChild(leafs[(ii * 2)]);
+          span.appendChild(leafs[(ii * 2) + 1]);
+          return span;
+        }
+      );
+    blocks = ['a-0-0', 'b-0-0', 'c-0-0']
+      .map(
+        function(blockKey, ii) {
+          var blockElement = document.createElement('div');
+          blockElement.setAttribute('data-offset-key', '' + blockKey);
+          blockElement.appendChild(decorators[ii]);
+          return blockElement;
+        }
+      );
+    blocks.forEach(
+      function(blockElem) {
+        contents.appendChild(blockElem);
+      }
+    );
+  });
+
+  function assertEquals(result, assert) {
+    var resultSelection = result.selectionState;
+    var resultRecovery = result.needsRecovery;
+    var assertSelection = assert.selectionState;
+    var assertRecovery = assert.needsRecovery;
+    expect(resultSelection.getAnchorKey())
+      .toBe(assertSelection.getAnchorKey());
+    expect(resultSelection.getAnchorOffset())
+      .toBe(assertSelection.getAnchorOffset());
+    expect(resultSelection.getFocusKey())
+      .toBe(assertSelection.getFocusKey());
+    expect(resultSelection.getFocusOffset())
+      .toBe(assertSelection.getFocusOffset());
+    expect(resultSelection.getIsBackward())
+      .toBe(assertSelection.getIsBackward());
+    expect(resultRecovery).toBe(assertRecovery);
+  }
+
+  describe('Modern Selection', function() {
+    beforeEach(function() {
+      document.selection = null;
+    });
+
+    describe('Selection is entirely within a single text node', function() {
+
+      it('must find offsets when collapsed at start', function() {
+        var textNode = textNodes[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 0,
+          focusNode: textNode,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('must find offsets when collapsed at end', function() {
+        var textNode = textNodes[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: textNode.length,
+          focusNode: textNode,
+          focusOffset: textNode.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: textNode.length,
+            focusKey: 'a',
+            focusOffset: textNode.length,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('must find offsets for non-collapsed selection', function() {
+        var textNode = textNodes[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 1,
+          focusNode: textNode,
+          focusOffset: 6,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 1,
+            focusKey: 'a',
+            focusOffset: 6,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('must find offsets for reversed selection', function() {
+        var textNode = textNodes[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 6,
+          focusNode: textNode,
+          focusOffset: 1,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 6,
+            focusKey: 'a',
+            focusOffset: 1,
+            isBackward: true,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('must find offsets for selection on entire text node', function() {
+        var textNode = textNodes[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 0,
+          focusNode: textNode,
+          focusOffset: textNode.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: textNode.length,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+    });
+
+    describe('Selection spans multiple text nodes', function() {
+
+      it('starts at head of one node and ends at head of another', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: textNodes[4],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('extends from head of one node to end of another', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: textNodes[2],
+          focusOffset: textNodes[2].textContent.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'b',
+            focusOffset: textNodes[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('starts within one text node and ends within another', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 4,
+          focusNode: textNodes[4],
+          focusOffset: 6,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 4,
+            focusKey: 'c',
+            focusOffset: 6,
+            isBackward: false,
+          }),
+          needsRecovery: false,
+        });
+      });
+
+      it('is a reversed selection across multiple text nodes', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[4],
+          anchorOffset: 4,
+          focusNode: textNodes[0],
+          focusOffset: 6,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: 4,
+            focusKey: 'a',
+            focusOffset: 6,
+            isBackward: true,
+          }),
+          needsRecovery: false,
+        });
+      });
+    });
+
+    // I'm not even certain this is possible, but let's handle it anyway.
+    describe('One end is a text node, the other is a leaf child', function() {
+      it('starts at head of text node, ends at head of leaf child', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: leafChildren[4],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('starts at head of text node, ends at end of leaf child', function() {
+        var leaf = leafChildren[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+
+      it('starts within text node, ends at start of leaf child', function() {
+        var leaf = leafChildren[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 4,
+          focusNode: leaf,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 4,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('starts within text node, ends at end of leaf child', function() {
+        var leaf = leafChildren[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 4,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 4,
+            focusKey: 'c',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is a reversed text-to-leaf-child selection', function() {
+        var leaf = leafChildren[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: 0,
+          focusNode: textNodes[0],
+          focusOffset: 4,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 4,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('One end is a text node, the other is a leaf span', function() {
+      it('starts at head of text node, ends at head of leaf span', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: leafs[4],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('starts at head of text node, ends at end of leaf span', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+
+      it('starts within text node, ends at start of leaf span', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 4,
+          focusNode: leaf,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 4,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('starts within text node, ends at end of leaf span', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 4,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 4,
+            focusKey: 'c',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is a reversed text-to-leaf selection', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: 0,
+          focusNode: textNodes[0],
+          focusOffset: 4,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 4,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('A single leaf span is selected', function() {
+      it('is collapsed at start', function() {
+        var leaf = leafs[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: 0,
+          focusNode: leaf,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is collapsed at end', function() {
+        var leaf = leafs[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: leaf.childNodes.length,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: leaf.textContent.length,
+            focusKey: 'a',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+
+      it('contains an entire leaf', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: 0,
+          focusNode: leaf,
+          focusOffset: leaf.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: leaf.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is reversed on entire leaf', function() {
+        var leaf = leafs[4];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leaf,
+          anchorOffset: leaf.childNodes.length,
+          focusNode: leaf,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: leaf.textContent.length,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('Multiple leaf spans are selected', function() {
+      it('from start of one to start of another', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leafs[0],
+          anchorOffset: 0,
+          focusNode: leafs[4],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('from start of one to end of other', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leafs[0],
+          anchorOffset: 0,
+          focusNode: leafs[4],
+          focusOffset: leafs[4].childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: leafs[4].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+
+      it('reversed leaf to leaf', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: leafs[4],
+          anchorOffset: leafs[4].childNodes.length,
+          focusNode: leafs[0],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: leafs[4].textContent.length,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('A single block is selected', function() {
+      it('is collapsed at start', function() {
+        var block = blocks[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: block,
+          anchorOffset: 0,
+          focusNode: block,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is collapsed at end', function() {
+        var block = blocks[0];
+        var decorators = block.childNodes;
+        var leafs = decorators[0].childNodes;
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: block,
+          anchorOffset: decorators.length,
+          focusNode: block,
+          focusOffset: decorators.length,
+        });
+
+        var textLength = 0;
+        for (var ii = 0; ii < leafs.length; ii++) {
+          textLength += leafs[ii].textContent.length;
+        }
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: textLength,
+            focusKey: 'a',
+            focusOffset: textLength,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is entirely selected', function() {
+        var block = blocks[0];
+        var decorators = block.childNodes;
+        var leafs = decorators[0].childNodes;
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: block,
+          anchorOffset: 0,
+          focusNode: block,
+          focusOffset: decorators.length,
+        });
+
+        var textLength = 0;
+        for (var ii = 0; ii < leafs.length; ii++) {
+          textLength += leafs[ii].textContent.length;
+        }
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: textLength,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    /**
+     * FF: Triple-clicking a block leads to an entire block being selected,
+     * with the first text node as the anchor (0 offset) and the block element
+     * as the focus (childNodes.length offset)
+     */
+    describe('Selection with text node and block', function() {
+      it('begins at text node zero, ends at end of block', function() {
+        var textNode = textNodes[0];
+        var block = blocks[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 0,
+          focusNode: block,
+          focusOffset: block.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: block.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      // No idea if this is possible.
+      it('begins within text node, ends at end of block', function() {
+        var textNode = textNodes[0];
+        var block = blocks[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNode,
+          anchorOffset: 5,
+          focusNode: block,
+          focusOffset: block.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 5,
+            focusKey: 'a',
+            focusOffset: block.textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      // No idea if this is possible.
+      it('is reversed from the first case', function() {
+        var textNode = textNodes[0];
+        var block = blocks[0];
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: block,
+          anchorOffset: block.childNodes.length,
+          focusNode: textNode,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: block.textContent.length,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('Multiple blocks are selected', function() {
+      it('goes from start of one to end of other', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: blocks[0],
+          anchorOffset: 0,
+          focusNode: blocks[2],
+          focusOffset: blocks[2].childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('goes from start of one to start of other', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: blocks[0],
+          anchorOffset: 0,
+          focusNode: blocks[2],
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('goes from end of one to end of other', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: blocks[0],
+          anchorOffset: blocks[0].childNodes.length,
+          focusNode: blocks[2],
+          focusOffset: blocks[2].childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: blocks[0].textContent.length,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('goes from within one to within another', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: blocks[0],
+          anchorOffset: 1,
+          focusNode: blocks[2].firstChild,
+          focusOffset: 1,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: blocks[0].textContent.length,
+            focusKey: 'c',
+            focusOffset: textNodes[4].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is the same as above but reversed', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: blocks[2].firstChild,
+          anchorOffset: 1,
+          focusNode: blocks[0],
+          focusOffset: 1,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: textNodes[4].textContent.length,
+            focusKey: 'a',
+            focusOffset: blocks[0].textContent.length,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    describe('The content wrapper is selected', () => {
+      it('is collapsed at the start of the contents', () => {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: contents,
+          anchorOffset: 0,
+          focusNode: contents,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('occupies a single child of the contents', () => {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: contents,
+          anchorOffset: 0,
+          focusNode: contents,
+          focusOffset: 1,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: blocks[0].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is collapsed at the end of a child', () => {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: contents,
+          anchorOffset: 1,
+          focusNode: contents,
+          focusOffset: 1,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: blocks[0].textContent.length,
+            focusKey: 'a',
+            focusOffset: blocks[0].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is contains multiple children', () => {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: contents,
+          anchorOffset: 0,
+          focusNode: contents,
+          focusOffset: 3,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    /**
+     * In some scenarios, the entire editor may be selected by command-A.
+     */
+    describe('The entire editor is selected.', function() {
+      it('is collapsed at start', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: root,
+          anchorOffset: 0,
+          focusNode: root,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is collapsed at end', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: root,
+          anchorOffset: root.childNodes.length,
+          focusNode: root,
+          focusOffset: root.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: blocks[2].textContent.length,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is completely selected', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: root,
+          anchorOffset: 0,
+          focusNode: root,
+          focusOffset: root.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+
+      it('is reversed from above', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: root,
+          anchorOffset: root.childNodes.length,
+          focusNode: root,
+          focusOffset: 0,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'c',
+            anchorOffset: blocks[2].textContent.length,
+            focusKey: 'a',
+            focusOffset: 0,
+            isBackward: true,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+
+    /**
+     * A selection possibility that defies logic. In IE11, triple clicking a
+     * block leads to the text node being selected as the anchor, and the
+     * **entire editor** being selected as the focus. Ludicrous.
+     */
+    describe('One end is text node, the other is the whole editor', function() {
+      it('does the crazy stuff described above', function() {
+        window.getSelection.mockReturnValueOnce({
+          rangeCount: 1,
+          anchorNode: textNodes[0],
+          anchorOffset: 0,
+          focusNode: root,
+          focusOffset: root.childNodes.length,
+        });
+
+        var selection = getDraftEditorSelection(editorState, root);
+        assertEquals(selection, {
+          selectionState: new SelectionState({
+            anchorKey: 'a',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: blocks[2].textContent.length,
+            isBackward: false,
+          }),
+          needsRecovery: true,
+        });
+      });
+    });
+  });
+});
diff --git a/src/component/selection/expandRangeToStartOfLine.js b/src/component/selection/expandRangeToStartOfLine.js
new file mode 100644
index 0000000..60b3a3c
--- /dev/null
+++ b/src/component/selection/expandRangeToStartOfLine.js
@@ -0,0 +1,201 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule expandRangeToStartOfLine
+ * @typechecks
+ * @flow
+ */
+
+var UnicodeUtils = require('UnicodeUtils');
+
+var getRangeClientRects = require('getRangeClientRects');
+var invariant = require('invariant');
+
+/**
+ * Return the computed line height, in pixels, for the provided element.
+ */
+function getLineHeightPx(element: Element): number {
+  var computed = getComputedStyle(element);
+  var div = document.createElement('div');
+  div.style.fontFamily = computed.fontFamily;
+  div.style.fontSize = computed.fontSize;
+  div.style.fontStyle = computed.fontStyle;
+  div.style.fontWeight = computed.fontWeight;
+  div.style.lineHeight = computed.lineHeight;
+  div.style.position = 'absolute';
+  div.textContent = 'M';
+
+  // forced layout here
+  document.body.appendChild(div);
+  var rect = div.getBoundingClientRect();
+  document.body.removeChild(div);
+
+  return rect.height;
+}
+
+/**
+ * Return whether every ClientRect in the provided list lies on the same line.
+ *
+ * We assume that the rects on the same line all contain the baseline, so the
+ * lowest top line needs to be above the highest bottom line (i.e., if you were
+ * to project the rects onto the y-axis, their intersection would be nonempty).
+ *
+ * In addition, we require that no two boxes are lineHeight (or more) apart at
+ * either top or bottom, which helps protect against false positives for fonts
+ * with extremely large glyph heights (e.g., with a font size of 17px, Zapfino
+ * produces rects of height 58px!).
+ */
+function areRectsOnOneLine(
+  rects: Array<ClientRect>,
+  lineHeight: number
+): boolean {
+  var minTop = Infinity;
+  var minBottom = Infinity;
+  var maxTop = -Infinity;
+  var maxBottom = -Infinity;
+
+  for (var ii = 0; ii < rects.length; ii++) {
+    var rect = rects[ii];
+    if (rect.width === 0 || rect.width === 1) {
+      // When a range starts or ends a soft wrap, many browsers (Chrome, IE,
+      // Safari) include an empty rect on the previous or next line. When the
+      // text lies in a container whose position is not integral (e.g., from
+      // margin: auto), Safari makes these empty rects have width 1 (instead of
+      // 0). Having one-pixel-wide characters seems unlikely (and most browsers
+      // report widths in subpixel precision anyway) so it's relatively safe to
+      // skip over them.
+      continue;
+    }
+    minTop = Math.min(minTop, rect.top);
+    minBottom = Math.min(minBottom, rect.bottom);
+    maxTop = Math.max(maxTop, rect.top);
+    maxBottom = Math.max(maxBottom, rect.bottom);
+  }
+
+  return (
+    maxTop <= minBottom &&
+    maxTop - minTop < lineHeight && maxBottom - minBottom < lineHeight
+  );
+}
+
+/**
+ * Return the length of a node, as used by Range offsets.
+ */
+function getNodeLength(node: Node): number {
+  // http://www.w3.org/TR/dom/#concept-node-length
+  switch (node.nodeType) {
+    case Node.DOCUMENT_TYPE_NODE:
+      return 0;
+    case Node.TEXT_NODE:
+    case Node.PROCESSING_INSTRUCTION_NODE:
+    case Node.COMMENT_NODE:
+      return (node: any).length;
+    default:
+      return node.childNodes.length;
+  }
+}
+
+/**
+ * Given a collapsed range, move the start position backwards as far as
+ * possible while the range still spans only a single line.
+ */
+function expandRangeToStartOfLine(range: Range): Range {
+  invariant(
+    range.collapsed,
+    'expandRangeToStartOfLine: Provided range is not collapsed.'
+  );
+  range = range.cloneRange();
+
+  var containingElement = range.startContainer;
+  if (containingElement.nodeType !== 1) {
+    containingElement = containingElement.parentNode;
+  }
+  var lineHeight = getLineHeightPx((containingElement: any));
+
+  // Imagine our text looks like:
+  //   <div><span>once upon a time, there was a <em>boy
+  //   who lived</em> </span><q><strong>under^ the
+  //   stairs</strong> in a small closet.</q></div>
+  // where the caret represents the cursor. First, we crawl up the tree until
+  // the range spans multiple lines (setting the start point to before
+  // "<strong>", then before "<div>"), then at each level we do a search to
+  // find the latest point which is still on a previous line. We'll find that
+  // the break point is inside the span, then inside the <em>, then in its text
+  // node child, the actual break point before "who".
+
+  var bestContainer = range.endContainer;
+  var bestOffset = range.endOffset;
+  range.setStart(range.startContainer, 0);
+
+  while (areRectsOnOneLine(getRangeClientRects(range), lineHeight)) {
+    bestContainer = range.startContainer;
+    bestOffset = range.startOffset;
+    invariant(
+      bestContainer.parentNode,
+      'Found unexpected detached subtree when traversing.'
+    );
+    range.setStartBefore(bestContainer);
+    if (bestContainer.nodeType === 1 &&
+        getComputedStyle((bestContainer: any)).display !== 'inline') {
+      // The start of the line is never in a different block-level container.
+      break;
+    }
+  }
+
+  // In the above example, range now spans from "<div>" to "under",
+  // bestContainer is <div>, and bestOffset is 1 (index of <q> inside <div>)].
+  // Picking out which child to recurse into here is a special case since we
+  // don't want to check past <q> -- once we find that the final range starts
+  // in <span>, we can look at all of its children (and all of their children)
+  // to find the break point.
+
+  // At all times, (bestContainer, bestOffset) is the latest single-line start
+  // point that we know of.
+  var currentContainer = bestContainer;
+  var maxIndexToConsider = bestOffset - 1;
+
+  do {
+    var nodeValue = currentContainer.nodeValue;
+
+    for (var ii = maxIndexToConsider; ii >= 0; ii--) {
+      if (nodeValue != null && ii > 0 &&
+          UnicodeUtils.isSurrogatePair(nodeValue, ii - 1)) {
+        // We're in the middle of a surrogate pair -- skip over so we never
+        // return a range with an endpoint in the middle of a code point.
+        continue;
+      }
+
+      range.setStart(currentContainer, ii);
+      if (areRectsOnOneLine(getRangeClientRects(range), lineHeight)) {
+        bestContainer = currentContainer;
+        bestOffset = ii;
+      } else {
+        break;
+      }
+    }
+
+    if (ii === -1 || currentContainer.childNodes.length === 0) {
+      // If ii === -1, then (bestContainer, bestOffset), which is equal to
+      // (currentContainer, 0), was a single-line start point but a start
+      // point before currentContainer wasn't, so the line break seems to
+      // have occurred immediately after currentContainer's start tag
+      //
+      // If currentContainer.childNodes.length === 0, we're already at a
+      // terminal node (e.g., text node) and should return our current best.
+      break;
+    }
+
+    currentContainer = currentContainer.childNodes[ii];
+    maxIndexToConsider = getNodeLength(currentContainer);
+  } while (true);
+
+  range.setStart(bestContainer, bestOffset);
+  return range;
+}
+
+module.exports = expandRangeToStartOfLine;
diff --git a/src/component/selection/findAncestorOffsetKey.js b/src/component/selection/findAncestorOffsetKey.js
new file mode 100644
index 0000000..0dd26cd
--- /dev/null
+++ b/src/component/selection/findAncestorOffsetKey.js
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findAncestorOffsetKey
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var getSelectionOffsetKeyForNode = require('getSelectionOffsetKeyForNode');
+
+/**
+ * Get the key from the node's nearest offset-aware ancestor.
+ */
+function findAncestorOffsetKey(node: Node): ?string {
+  while (node && node !== document.documentElement) {
+    var key = getSelectionOffsetKeyForNode(node);
+    if (key != null) {
+      return key;
+    }
+    node = node.parentNode;
+  }
+  return null;
+}
+
+module.exports = findAncestorOffsetKey;
diff --git a/src/component/selection/getDraftEditorSelection.js b/src/component/selection/getDraftEditorSelection.js
new file mode 100644
index 0000000..47b9103
--- /dev/null
+++ b/src/component/selection/getDraftEditorSelection.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getDraftEditorSelection
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var getDraftEditorSelectionWithNodes = require('getDraftEditorSelectionWithNodes');
+
+import type {DOMDerivedSelection} from 'DOMDerivedSelection';
+import type EditorState from 'EditorState';
+
+/**
+ * Convert the current selection range to an anchor/focus pair of offset keys
+ * and values that can be interpreted by components.
+ */
+function getDraftEditorSelection(
+  editorState: EditorState,
+  root: HTMLElement
+): DOMDerivedSelection {
+  var selection = global.getSelection();
+
+  // No active selection.
+  if (selection.rangeCount === 0) {
+    return {
+      selectionState: editorState.getSelection().set('hasFocus', false),
+      needsRecovery: false,
+    };
+  }
+
+  return getDraftEditorSelectionWithNodes(
+    editorState,
+    root,
+    selection.anchorNode,
+    selection.anchorOffset,
+    selection.focusNode,
+    selection.focusOffset
+  );
+}
+
+module.exports = getDraftEditorSelection;
diff --git a/src/component/selection/getDraftEditorSelectionWithNodes.js b/src/component/selection/getDraftEditorSelectionWithNodes.js
new file mode 100644
index 0000000..140c7eb
--- /dev/null
+++ b/src/component/selection/getDraftEditorSelectionWithNodes.js
@@ -0,0 +1,216 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getDraftEditorSelectionWithNodes
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var findAncestorOffsetKey = require('findAncestorOffsetKey');
+var getSelectionOffsetKeyForNode = require('getSelectionOffsetKeyForNode');
+var getUpdatedSelectionState = require('getUpdatedSelectionState');
+var invariant = require('invariant');
+var nullthrows = require('nullthrows');
+
+import type {DOMDerivedSelection} from 'DOMDerivedSelection';
+import type EditorState from 'EditorState';
+
+type SelectionPoint = {
+  key: string;
+  offset: number;
+};
+
+/**
+ * Convert the current selection range to an anchor/focus pair of offset keys
+ * and values that can be interpreted by components.
+ */
+function getDraftEditorSelectionWithNodes(
+  editorState: EditorState,
+  root: ?HTMLElement,
+  anchorNode: Node,
+  anchorOffset: number,
+  focusNode: Node,
+  focusOffset: number
+): DOMDerivedSelection {
+  var anchorIsTextNode = anchorNode.nodeType === Node.TEXT_NODE;
+  var focusIsTextNode = focusNode.nodeType === Node.TEXT_NODE;
+
+  // If the selection range lies only on text nodes, the task is simple.
+  // Find the nearest offset-aware elements and use the
+  // offset values supplied by the selection range.
+  if (anchorIsTextNode && focusIsTextNode) {
+    return {
+      selectionState: getUpdatedSelectionState(
+        editorState,
+        nullthrows(findAncestorOffsetKey(anchorNode)),
+        anchorOffset,
+        nullthrows(findAncestorOffsetKey(focusNode)),
+        focusOffset
+      ),
+      needsRecovery: false,
+    };
+  }
+
+  var anchorPoint = null;
+  var focusPoint = null;
+  var needsRecovery = true;
+
+  // An element is selected. Convert this selection range into leaf offset
+  // keys and offset values for consumption at the component level. This
+  // is common in Firefox, where select-all and triple click behavior leads
+  // to entire elements being selected.
+  //
+  // Note that we use the `needsRecovery` parameter in the callback here. This
+  // is because when certain elements are selected, the behavior for subsequent
+  // cursor movement (e.g. via arrow keys) is uncertain and may not match
+  // expectations at the component level. For example, if an entire <div> is
+  // selected and the user presses the right arrow, Firefox keeps the selection
+  // on the <div>. If we allow subsequent keypresses to insert characters
+  // natively, they will be inserted into a browser-created text node to the
+  // right of that <div>. This is obviously undesirable.
+  //
+  // With the `needsRecovery` flag, we inform the caller that it is responsible
+  // for manually setting the selection state on the rendered document to
+  // ensure proper selection state maintenance.
+
+  if (anchorIsTextNode) {
+    anchorPoint = {
+      key: nullthrows(findAncestorOffsetKey(anchorNode)),
+      offset: anchorOffset,
+    };
+    focusPoint = getPointForNonTextNode(root, focusNode, focusOffset);
+  } else if (focusIsTextNode) {
+    focusPoint = {
+      key: nullthrows(findAncestorOffsetKey(focusNode)),
+      offset: focusOffset,
+    };
+    anchorPoint = getPointForNonTextNode(root, anchorNode, anchorOffset);
+  } else {
+    anchorPoint = getPointForNonTextNode(root, anchorNode, anchorOffset);
+    focusPoint = getPointForNonTextNode(root, focusNode, focusOffset);
+
+    // If the selection is collapsed on an empty block, don't force recovery.
+    // This way, on arrow key selection changes, the browser can move the
+    // cursor from a non-zero offset on one block, through empty blocks,
+    // to a matching non-zero offset on other text blocks.
+    if (anchorNode === focusNode && anchorOffset === focusOffset) {
+      needsRecovery = anchorNode.firstChild.nodeName !== 'BR';
+    }
+  }
+
+  return {
+    selectionState: getUpdatedSelectionState(
+      editorState,
+      anchorPoint.key,
+      anchorPoint.offset,
+      focusPoint.key,
+      focusPoint.offset
+    ),
+    needsRecovery,
+  };
+}
+
+/**
+ * Identify the first leaf descendant for the given node.
+ */
+function getFirstLeaf(node: Node): Node {
+  while (node.firstChild && getSelectionOffsetKeyForNode(node.firstChild)) {
+    node = node.firstChild;
+  }
+  return node;
+}
+
+/**
+ * Identify the last leaf descendant for the given node.
+ */
+function getLastLeaf(node: Node): Node {
+  while (node.lastChild && getSelectionOffsetKeyForNode(node.lastChild)) {
+    node = node.lastChild;
+  }
+  return node;
+}
+
+function getPointForNonTextNode(
+  editorRoot: ?HTMLElement,
+  node: Node,
+  childOffset: number
+): SelectionPoint {
+  var offsetKey: ?string = findAncestorOffsetKey(node);
+
+  invariant(
+    offsetKey != null ||
+    editorRoot && (editorRoot === node || editorRoot.firstChild === node),
+    'Unknown node in selection range.'
+  );
+
+  // If the editorRoot is the selection, step downward into the content
+  // wrapper.
+  if (editorRoot === node) {
+    node = node.firstChild;
+    invariant(
+      node instanceof Element && node.getAttribute('data-contents') === 'true',
+      'Invalid DraftEditorContents structure.'
+    );
+    if (childOffset > 0) {
+      childOffset = node.childNodes.length;
+    }
+  }
+
+  // If the child offset is zero and we have an offset key, we're done.
+  // If there's no offset key because the entire editor is selected,
+  // find the leftmost ("first") leaf in the tree and use that as the offset
+  // key.
+  if (childOffset === 0) {
+    var key: ?string = null;
+    if (offsetKey != null) {
+      key = offsetKey;
+    } else {
+      var firstLeaf = getFirstLeaf(node);
+      key = nullthrows(getSelectionOffsetKeyForNode(firstLeaf));
+    }
+    return {key, offset: 0};
+  }
+
+  var nodeBeforeCursor = node.childNodes[childOffset - 1];
+  var leafKey: ?string = null;
+  var textLength: ?number = null;
+
+  if (!getSelectionOffsetKeyForNode(nodeBeforeCursor)) {
+    // Our target node may be a leaf or a text node, in which case we're
+    // already where we want to be and can just use the child's length as
+    // our offset.
+    leafKey = nullthrows(offsetKey);
+    textLength = getTextContentLength(nodeBeforeCursor);
+  } else {
+    // Otherwise, we'll look at the child to the left of the cursor and find
+    // the last leaf node in its subtree.
+    var lastLeaf = getLastLeaf(nodeBeforeCursor);
+    leafKey = nullthrows(getSelectionOffsetKeyForNode(lastLeaf));
+    textLength = getTextContentLength(lastLeaf);
+  }
+
+  return {
+    key: leafKey,
+    offset: textLength,
+  };
+}
+
+/**
+ * Return the length of a node's textContent, regarding single newline
+ * characters as zero-length. This allows us to avoid problems with identifying
+ * the correct selection offset for empty blocks in IE, in which we
+ * render newlines instead of break tags.
+ */
+function getTextContentLength(node: Node): number {
+  var textContent = node.textContent;
+  return textContent === '\n' ? 0 : textContent.length;
+}
+
+module.exports = getDraftEditorSelectionWithNodes;
diff --git a/src/component/selection/getRangeBoundingClientRect.js b/src/component/selection/getRangeBoundingClientRect.js
new file mode 100644
index 0000000..e0a161e
--- /dev/null
+++ b/src/component/selection/getRangeBoundingClientRect.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getRangeBoundingClientRect
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var getRangeClientRects = require('getRangeClientRects');
+
+/**
+ * Like range.getBoundingClientRect() but normalizes for browser bugs.
+ */
+function getRangeBoundingClientRect(range: Range): ClientRect {
+  // "Return a DOMRect object describing the smallest rectangle that includes
+  // the first rectangle in list and all of the remaining rectangles of which
+  // the height or width is not zero."
+  // http://www.w3.org/TR/cssom-view/#dom-range-getboundingclientrect
+  var rects = getRangeClientRects(range);
+  var top = 0;
+  var right = 0;
+  var bottom = 0;
+  var left = 0;
+
+  if (rects.length) {
+    ({top, right, bottom, left} = rects[0]);
+    for (var ii = 1; ii < rects.length; ii++) {
+      var rect = rects[ii];
+      if (rect.height !== 0 || rect.width !== 0) {
+        top = Math.min(top, rect.top);
+        right = Math.max(right, rect.right);
+        bottom = Math.max(bottom, rect.bottom);
+        left = Math.min(left, rect.left);
+      }
+    }
+  }
+
+  return {
+    top,
+    right,
+    bottom,
+    left,
+    width: right - left,
+    height: bottom - top,
+  };
+}
+
+module.exports = getRangeBoundingClientRect;
diff --git a/src/component/selection/getRangeClientRects.js b/src/component/selection/getRangeClientRects.js
new file mode 100644
index 0000000..e8e9eb4
--- /dev/null
+++ b/src/component/selection/getRangeClientRects.js
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getRangeClientRects
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var UserAgent = require('UserAgent');
+
+var invariant = require('invariant');
+
+var isChrome = UserAgent.isBrowser('Chrome');
+
+// In Chrome, the client rects will include the entire bounds of all nodes that
+// begin (have a start tag) within the selection, even if the selection does
+// not overlap the entire node. To resolve this, we split the range at each
+// start tag and join the client rects together.
+// https://code.google.com/p/chromium/issues/detail?id=324437
+function getRangeClientRectsChrome(range: Range): Array<ClientRect> {
+  var tempRange = range.cloneRange();
+  var clientRects = [];
+
+  for (
+    var ancestor = range.endContainer;
+    ancestor != null;
+    ancestor = ancestor.parentNode
+  ) {
+    // If we've climbed up to the common ancestor, we can now use the
+    // original start point and stop climbing the tree.
+    var atCommonAncestor = ancestor === range.commonAncestorContainer;
+    if (atCommonAncestor) {
+      tempRange.setStart(range.startContainer, range.startOffset);
+    } else {
+      tempRange.setStart(tempRange.endContainer, 0);
+    }
+    var rects = Array.from(tempRange.getClientRects());
+    clientRects.push(rects);
+    if (atCommonAncestor) {
+      clientRects.reverse();
+      return [].concat(...clientRects);
+    }
+    tempRange.setEndBefore(ancestor);
+  }
+
+  invariant(
+    false,
+    'Found an unexpected detached subtree when getting range client rects.'
+  );
+}
+
+/**
+ * Like range.getClientRects() but normalizes for browser bugs.
+ */
+var getRangeClientRects = isChrome ?
+  getRangeClientRectsChrome :
+  function(range: Range): Array<ClientRect> {
+    return Array.from(range.getClientRects());
+  };
+
+module.exports = getRangeClientRects;
diff --git a/src/component/selection/getSelectionOffsetKeyForNode.js b/src/component/selection/getSelectionOffsetKeyForNode.js
new file mode 100644
index 0000000..4e8ec05
--- /dev/null
+++ b/src/component/selection/getSelectionOffsetKeyForNode.js
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getSelectionOffsetKeyForNode
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * Get offset key from a node.
+ */
+function getSelectionOffsetKeyForNode(node: Node): ?string {
+  return node instanceof Element ? node.getAttribute('data-offset-key') : null;
+}
+
+module.exports = getSelectionOffsetKeyForNode;
diff --git a/src/component/selection/getUpdatedSelectionState.js b/src/component/selection/getUpdatedSelectionState.js
new file mode 100644
index 0000000..942a568
--- /dev/null
+++ b/src/component/selection/getUpdatedSelectionState.js
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getUpdatedSelectionState
+ * @flow
+ */
+
+'use strict';
+
+var DraftOffsetKey = require('DraftOffsetKey');
+
+var nullthrows = require('nullthrows');
+
+import type EditorState from 'EditorState';
+import type SelectionState from 'SelectionState';
+
+function getUpdatedSelectionState(
+  editorState: EditorState,
+  anchorKey: string,
+  anchorOffset: number,
+  focusKey: string,
+  focusOffset: number
+): SelectionState {
+  var selection: SelectionState = nullthrows(editorState.getSelection());
+  if (__DEV__) {
+    if (!anchorKey || !focusKey) {
+      /*eslint-disable no-console */
+      console.warn(
+        'Invalid selection state.',
+        arguments,
+        editorState.toJS()
+      );
+      /*eslint-enable no-console */
+      return selection;
+
+    }
+  }
+
+  var anchorPath = DraftOffsetKey.decode(anchorKey);
+  var anchorBlockKey = anchorPath.blockKey;
+  var anchorLeaf = editorState
+    .getBlockTree(anchorBlockKey)
+    .getIn([
+      anchorPath.decoratorKey,
+      'leaves',
+      anchorPath.leafKey,
+    ]);
+
+  var focusPath = DraftOffsetKey.decode(focusKey);
+  var focusBlockKey = focusPath.blockKey;
+  var focusLeaf = editorState
+    .getBlockTree(focusBlockKey)
+    .getIn([
+      focusPath.decoratorKey,
+      'leaves',
+      focusPath.leafKey,
+    ]);
+
+  var anchorLeafStart: number = anchorLeaf.get('start');
+  var focusLeafStart: number = focusLeaf.get('start');
+
+  var anchorBlockOffset = anchorLeaf ? anchorLeafStart + anchorOffset : null;
+  var focusBlockOffset = focusLeaf ? focusLeafStart + focusOffset : null;
+
+  var areEqual = (
+    selection.getAnchorKey() === anchorBlockKey &&
+    selection.getAnchorOffset() === anchorBlockOffset &&
+    selection.getFocusKey() === focusBlockKey &&
+    selection.getFocusOffset() === focusBlockOffset
+  );
+
+  if (areEqual) {
+    return selection;
+  }
+
+  var isBackward = false;
+  if (anchorBlockKey === focusBlockKey) {
+    var anchorLeafEnd: number = anchorLeaf.get('end');
+    var focusLeafEnd: number = focusLeaf.get('end');
+    if (
+      focusLeafStart === anchorLeafStart &&
+      focusLeafEnd === anchorLeafEnd
+    ) {
+      isBackward = focusOffset < anchorOffset;
+    } else {
+      isBackward = focusLeafStart < anchorLeafStart;
+    }
+  } else {
+    var startKey = editorState
+      .getCurrentContent()
+      .getBlockMap()
+      .keySeq()
+      .skipUntil(v => v === anchorBlockKey || v === focusBlockKey)
+      .first();
+    isBackward = startKey === focusBlockKey;
+  }
+
+  return selection.merge({
+    anchorKey: anchorBlockKey,
+    anchorOffset: anchorBlockOffset,
+    focusKey: focusBlockKey,
+    focusOffset: focusBlockOffset,
+    isBackward,
+  });
+}
+
+module.exports = getUpdatedSelectionState;
diff --git a/src/component/selection/isSelectionAtLeafStart.js b/src/component/selection/isSelectionAtLeafStart.js
new file mode 100644
index 0000000..0d22e9e
--- /dev/null
+++ b/src/component/selection/isSelectionAtLeafStart.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isSelectionAtLeafStart
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+import type EditorState from 'EditorState';
+
+function isSelectionAtLeafStart(editorState: EditorState): boolean {
+  var selection = editorState.getSelection();
+  var anchorKey = selection.getAnchorKey();
+  var blockTree = editorState.getBlockTree(anchorKey);
+  var offset = selection.getStartOffset();
+
+  var isAtStart = false;
+
+  blockTree.some(leafSet => {
+    if (offset === leafSet.get('start')) {
+      isAtStart = true;
+      return true;
+    }
+
+    if (offset < leafSet.get('end')) {
+      return leafSet.get('leaves').some(leaf => {
+        var leafStart = leaf.get('start');
+        if (offset === leafStart) {
+          isAtStart = true;
+          return true;
+        }
+        if (offset < leafStart) {
+          return false;
+        }
+      });
+    }
+
+    return false;
+  });
+
+  return isAtStart;
+}
+
+module.exports = isSelectionAtLeafStart;
diff --git a/src/component/selection/setDraftEditorSelection.js b/src/component/selection/setDraftEditorSelection.js
new file mode 100644
index 0000000..f6f4109
--- /dev/null
+++ b/src/component/selection/setDraftEditorSelection.js
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setDraftEditorSelection
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var containsNode = require('containsNode');
+var getActiveElement = require('getActiveElement');
+
+import type SelectionState from 'SelectionState';
+
+/**
+ * In modern non-IE browsers, we can support both forward and backward
+ * selections.
+ *
+ * Note: IE10+ supports the Selection object, but it does not support
+ * the `extend` method, which means that even in modern IE, it's not possible
+ * to programatically create a backward selection. Thus, for all IE
+ * versions, we use the old IE API to create our selections.
+ */
+function setDraftEditorSelection(
+  selectionState: SelectionState,
+  node: Node,
+  blockKey: string,
+  nodeStart: number,
+  nodeEnd: number
+): void {
+  // It's possible that the editor has been removed from the DOM but
+  // our selection code doesn't know it yet. Forcing selection in
+  // this case may lead to errors, so just bail now.
+  if (!containsNode(document.documentElement, node)) {
+    return;
+  }
+
+  var selection = global.getSelection();
+  var anchorKey = selectionState.getAnchorKey();
+  var anchorOffset = selectionState.getAnchorOffset();
+  var focusKey = selectionState.getFocusKey();
+  var focusOffset = selectionState.getFocusOffset();
+  var isBackward = selectionState.getIsBackward();
+
+  // IE doesn't support backward selection. Swap key/offset pairs.
+  if (!selection.extend && isBackward) {
+    var tempKey = anchorKey;
+    var tempOffset = anchorOffset;
+    anchorKey = focusKey;
+    anchorOffset = focusOffset;
+    focusKey = tempKey;
+    focusOffset = tempOffset;
+    isBackward = false;
+  }
+
+  var hasAnchor = (
+    anchorKey === blockKey &&
+    nodeStart <= anchorOffset &&
+    nodeEnd >= anchorOffset
+  );
+
+  var hasFocus = (
+    focusKey === blockKey &&
+    nodeStart <= focusOffset &&
+    nodeEnd >= focusOffset
+  );
+
+  // If the selection is entirely bound within this node, set the selection
+  // and be done.
+  if (hasAnchor && hasFocus) {
+    selection.removeAllRanges();
+    addPointToSelection(selection, node, anchorOffset - nodeStart);
+    addFocusToSelection(selection, node, focusOffset - nodeStart);
+    return;
+  }
+
+  if (!isBackward) {
+    // If the anchor is within this node, set the range start.
+    if (hasAnchor) {
+      selection.removeAllRanges();
+      addPointToSelection(selection, node, anchorOffset - nodeStart);
+    }
+
+    // If the focus is within this node, we can assume that we have
+    // already set the appropriate start range on the selection, and
+    // can simply extend the selection.
+    if (hasFocus) {
+      addFocusToSelection(selection, node, focusOffset - nodeStart);
+    }
+  } else {
+    // If this node has the focus, set the selection range to be a
+    // collapsed range beginning here. Later, when we encounter the anchor,
+    // we'll use this information to extend the selection.
+    if (hasFocus) {
+      selection.removeAllRanges();
+      addPointToSelection(selection, node, focusOffset - nodeStart);
+    }
+
+    // If this node has the anchor, we may assume that the correct
+    // focus information is already stored on the selection object.
+    // We keep track of it, reset the selection range, and extend it
+    // back to the focus point.
+    if (hasAnchor) {
+      var storedFocusNode = selection.focusNode;
+      var storedFocusOffset = selection.focusOffset;
+
+      selection.removeAllRanges();
+      addPointToSelection(selection, node, anchorOffset - nodeStart);
+      addFocusToSelection(selection, storedFocusNode, storedFocusOffset);
+    }
+  }
+}
+
+/**
+ * Extend selection towards focus point.
+ */
+function addFocusToSelection(
+  selection: Object,
+  node: Node,
+  offset: number
+): void {
+  if (selection.extend && containsNode(getActiveElement(), node)) {
+    // If `extend` is called while another element has focus, an error is
+    // thrown. We therefore disable `extend` if the active element is somewhere
+    // other than the node we are selecting. This should only occur in Firefox,
+    // since it is the only browser to support multiple selections.
+    // See https://bugzilla.mozilla.org/show_bug.cgi?id=921444.
+    selection.extend(node, offset);
+  } else {
+    // IE doesn't support extend. This will mean no backward selection.
+    // Extract the existing selection range and add focus to it.
+    // Additionally, clone the selection range. IE11 throws an
+    // InvalidStateError when attempting to access selection properties
+    // after the range is detached.
+    var range = selection.getRangeAt(0);
+    range.setEnd(node, offset);
+    selection.addRange(range.cloneRange());
+  }
+}
+
+function addPointToSelection(
+  selection: Object,
+  node: Node,
+  offset: number
+): void {
+  var range = document.createRange();
+  range.setStart(node, offset);
+  selection.addRange(range);
+}
+
+module.exports = setDraftEditorSelection;
diff --git a/src/component/utils/DraftStyleDefault.css b/src/component/utils/DraftStyleDefault.css
new file mode 100644
index 0000000..87ff790
--- /dev/null
+++ b/src/component/utils/DraftStyleDefault.css
@@ -0,0 +1,190 @@
+/**
+ * @providesModule DraftStyleDefault
+ */
+
+.public-DraftStyleDefault-block {
+  position: relative;
+  white-space: pre-wrap;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-ltr {
+  direction: ltr;
+  text-align: left;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-rtl {
+  direction: rtl;
+  text-align: right;
+}
+
+/**
+ * These rules provide appropriate text direction for counter pseudo-elements.
+ */
+/* @noflip */
+.public-DraftStyleDefault-listLTR {
+  direction: ltr;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-listRTL {
+  direction: rtl;
+}
+
+/**
+ * Default spacing for list container elements. Override with CSS as needed.
+ */
+.public-DraftStyleDefault-ul,
+.public-DraftStyleDefault-ol {
+  margin: 16px 0;
+  padding: 0;
+}
+
+/**
+ * Default counters and styles are provided for five levels of nesting.
+ * If you require nesting beyond that level, you should use your own CSS
+ * classes to do so. If you care about handling RTL languages, the rules you
+ * create should look a lot like these.
+ */
+
+/* @noflip */
+.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR {
+  margin-left: 1.5em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL {
+  margin-right: 1.5em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR {
+  margin-left: 3em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL {
+  margin-right: 3em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR {
+  margin-left: 4.5em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL {
+  margin-right: 4.5em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR {
+  margin-left: 6em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL {
+  margin-right: 6em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR {
+  margin-left: 7.5em;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL {
+  margin-right: 7.5em;
+}
+
+/**
+ * Only use `square` list-style after the first two levels.
+ */
+.public-DraftStyleDefault-unorderedListItem {
+  list-style-type: square;
+  position: relative;
+}
+
+.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0 {
+  list-style-type: disc;
+}
+
+.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1 {
+  list-style-type: circle;
+}
+
+/**
+ * Ordered list item counters are managed with CSS, since all list nesting is
+ * purely visual.
+ */
+.public-DraftStyleDefault-orderedListItem {
+  list-style-type: none;
+  position: relative;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before {
+  left: -36px;
+  position: absolute;
+  text-align: right;
+  width: 30px;
+}
+
+/* @noflip */
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before {
+  position: absolute;
+  right: -36px;
+  text-align: left;
+  width: 30px;
+}
+
+/**
+ * Counters are reset in JavaScript. If you need different counter styles,
+ * override these rules. If you need more nesting, create your own rules to
+ * do so.
+ */
+.public-DraftStyleDefault-orderedListItem:before {
+  content: counter(ol0) ". ";
+  counter-increment: ol0;
+}
+
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before {
+  content: counter(ol1) ". ";
+  counter-increment: ol1;
+}
+
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before {
+  content: counter(ol2) ". ";
+  counter-increment: ol2;
+}
+
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before {
+  content: counter(ol3) ". ";
+  counter-increment: ol3;
+}
+
+.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before {
+  content: counter(ol4) ". ";
+  counter-increment: ol4;
+}
+
+.public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset {
+  counter-reset: ol0;
+}
+
+.public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset {
+  counter-reset: ol1;
+}
+
+.public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset {
+  counter-reset: ol2;
+}
+
+.public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset {
+  counter-reset: ol3;
+}
+
+.public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset {
+  counter-reset: ol4;
+}
diff --git a/src/component/utils/KeyBindingUtil.js b/src/component/utils/KeyBindingUtil.js
new file mode 100644
index 0000000..09e60a0
--- /dev/null
+++ b/src/component/utils/KeyBindingUtil.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule KeyBindingUtil
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var UserAgent = require('UserAgent');
+
+var isOSX = UserAgent.isPlatform('Mac OS X');
+
+var KeyBindingUtil = {
+  /**
+   * Check whether the ctrlKey modifier is *not* being used in conjunction with
+   * the altKey modifier. If they are combined, the result is an `altGraph`
+   * key modifier, which should not be handled by this set of key bindings.
+   */
+  isCtrlKeyCommand: function(e: SyntheticKeyboardEvent): boolean {
+    return !!e.ctrlKey && !e.altKey;
+  },
+
+  hasCommandModifier: function(e: SyntheticKeyboardEvent): boolean {
+    return isOSX ?
+      (!!e.metaKey && !e.altKey) :
+      KeyBindingUtil.isCtrlKeyCommand(e);
+  },
+};
+
+module.exports = KeyBindingUtil;
diff --git a/src/component/utils/getDefaultKeyBinding.js b/src/component/utils/getDefaultKeyBinding.js
new file mode 100644
index 0000000..68bfcc6
--- /dev/null
+++ b/src/component/utils/getDefaultKeyBinding.js
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getDefaultKeyBinding
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var KeyBindingUtil = require('KeyBindingUtil');
+var Keys = require('Keys');
+var UserAgent = require('UserAgent');
+
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+
+var isOSX = UserAgent.isPlatform('Mac OS X');
+var isWindows = UserAgent.isPlatform('Windows');
+
+// Firefox on OSX had a bug resulting in navigation instead of cursor movement.
+// This bug was fixed in Firefox 29. Feature detection is virtually impossible
+// so we just check the version number. See #342765.
+var shouldFixFirefoxMovement = isOSX && UserAgent.isBrowser('Firefox < 29');
+
+var {
+  hasCommandModifier,
+  isCtrlKeyCommand,
+} = KeyBindingUtil;
+
+function shouldRemoveWord(e: SyntheticKeyboardEvent): boolean {
+  return (isOSX && e.altKey) || isCtrlKeyCommand(e);
+}
+
+/**
+ * Get the appropriate undo/redo command for a Z key command.
+ */
+function getZCommand(
+  e: SyntheticKeyboardEvent
+): ?DraftEditorCommand {
+  if (hasCommandModifier(e)) {
+    return e.shiftKey ? 'redo' : 'undo';
+  }
+}
+
+function getDeleteCommand(
+  e: SyntheticKeyboardEvent
+): ?DraftEditorCommand {
+  // Allow default "cut" behavior for Windows on Shift + Delete.
+  if (isWindows && e.shiftKey) {
+    return null;
+  }
+  return shouldRemoveWord(e) ? 'delete-word' : 'delete';
+}
+
+function getBackspaceCommand(
+  e: SyntheticKeyboardEvent
+): ?DraftEditorCommand {
+  if (hasCommandModifier(e) && isOSX) {
+    return 'backspace-to-start-of-line';
+  }
+  return shouldRemoveWord(e) ? 'backspace-word' : 'backspace';
+}
+
+/**
+ * Retrieve a bound key command for the given event.
+ */
+function getDefaultKeyBinding(
+  e: SyntheticKeyboardEvent
+): ?DraftEditorCommand {
+  switch (e.keyCode) {
+    case 66: // B
+      return hasCommandModifier(e) ? 'bold' : null;
+    case 68: // D
+      return isCtrlKeyCommand(e) ? 'delete' : null;
+    case 72: // H
+      return isCtrlKeyCommand(e) ? 'backspace' : null;
+    case 73: // I
+      return hasCommandModifier(e) ? 'italic' : null;
+    case 74: // J
+      return hasCommandModifier(e) ? 'code' : null;
+    case 75: // K
+      return (!isWindows && isCtrlKeyCommand(e)) ? 'secondary-cut' : null;
+    case 79: // O
+      return isCtrlKeyCommand(e) ? 'split-block' : null;
+    case 84: // T
+      return (isOSX && isCtrlKeyCommand(e)) ? 'transpose-characters' : null;
+    case 85: // U
+      return hasCommandModifier(e) ? 'underline' : null;
+    case 87: // W
+      return (isOSX && isCtrlKeyCommand(e)) ? 'backspace-word' : null;
+    case 89: // Y
+      if (isCtrlKeyCommand(e)) {
+        return isWindows ? 'redo' : 'secondary-paste';
+      }
+      return null;
+    case 90: // Z
+      return getZCommand(e) || null;
+    case Keys.DELETE:
+      return getDeleteCommand(e);
+    case Keys.BACKSPACE:
+      return getBackspaceCommand(e);
+    // LEFT/RIGHT handlers serve as a workaround for a Firefox bug.
+    case Keys.LEFT:
+      return (
+        shouldFixFirefoxMovement && hasCommandModifier(e) ?
+          'move-selection-to-start-of-block' :
+          null
+      );
+    case Keys.RIGHT:
+      return (
+        shouldFixFirefoxMovement && hasCommandModifier(e) ?
+          'move-selection-to-end-of-block' :
+          null
+      );
+    default:
+      return null;
+  }
+}
+
+module.exports = getDefaultKeyBinding;
diff --git a/src/component/utils/getElementForBlockType.js b/src/component/utils/getElementForBlockType.js
new file mode 100644
index 0000000..635cca8
--- /dev/null
+++ b/src/component/utils/getElementForBlockType.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getElementForBlockType
+ * @flow
+ */
+
+'use strict';
+
+import type {DraftBlockType} from 'DraftBlockType';
+
+function getElementForBlockType(blockType: DraftBlockType): string {
+  switch (blockType) {
+    case 'header-one':
+      return 'h2';
+    case 'header-two':
+      return 'h3';
+    case 'unordered-list-item':
+    case 'ordered-list-item':
+      return 'li';
+    case 'blockquote':
+      return 'blockquote';
+    case 'media':
+      return 'figure';
+    default:
+      return 'div';
+  }
+}
+
+module.exports = getElementForBlockType;
diff --git a/src/component/utils/getTextContentFromFiles.js b/src/component/utils/getTextContentFromFiles.js
new file mode 100644
index 0000000..4664d44
--- /dev/null
+++ b/src/component/utils/getTextContentFromFiles.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextContentFromFiles
+ * @flow
+ */
+
+'use strict';
+
+var TEXT_CLIPPING_REGEX = /\.textClipping$/;
+
+var TEXT_TYPES = {
+  'text/plain': true,
+  'text/html': true,
+  'text/rtf': true,
+};
+
+// Somewhat arbitrary upper bound on text size. Let's not lock up the browser.
+var TEXT_SIZE_UPPER_BOUND = 5000;
+
+/**
+ * Extract the text content from a file list.
+ */
+function getTextContentFromFiles(
+  files: Array<File>,
+  callback: (contents: string) => void
+): void {
+  var readCount = 0;
+  var results = [];
+  files.forEach(function(/*blob*/ file) {
+    readFile(file, function(/*string*/ text) {
+      readCount++;
+      text && results.push(text.slice(0, TEXT_SIZE_UPPER_BOUND));
+      if (readCount == files.length) {
+        callback(results.join('\r'));
+      }
+    });
+  });
+}
+
+/**
+ * todo isaac: Do work to turn html/rtf into a content fragment.
+ */
+function readFile(
+  file: File,
+  callback: (contents: string) => void
+): void {
+  if (!global.FileReader || (file.type && !(file.type in TEXT_TYPES))) {
+    callback('');
+    return;
+  }
+
+  if (file.type === '') {
+    var contents = '';
+    // Special-case text clippings, which have an empty type but include
+    // `.textClipping` in the file name. `readAsText` results in an empty
+    // string for text clippings, so we force the file name to serve
+    // as the text value for the file.
+    if (TEXT_CLIPPING_REGEX.test(file.name)) {
+      contents = file.name.replace(TEXT_CLIPPING_REGEX, '');
+    }
+    callback(contents);
+    return;
+  }
+
+  var reader = new FileReader();
+  reader.onload = function() {
+    callback(reader.result);
+  };
+  reader.onerror = function() {
+    callback('');
+  };
+  reader.readAsText(file);
+}
+
+module.exports = getTextContentFromFiles;
diff --git a/src/component/utils/getWrapperTemplateForBlockType.js b/src/component/utils/getWrapperTemplateForBlockType.js
new file mode 100644
index 0000000..70957c1
--- /dev/null
+++ b/src/component/utils/getWrapperTemplateForBlockType.js
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getWrapperTemplateForBlockType
+ * @flow
+ */
+
+'use strict';
+
+const React = require('React');
+
+const cx = require('cx');
+
+import type {DraftBlockType} from 'DraftBlockType';
+
+/**
+ * Create these elements once and cache them so they're reference-equal.
+ */
+const UL_WRAP = <ul className={cx('public/DraftStyleDefault/ul')} />;
+const OL_WRAP = <ol className={cx('public/DraftStyleDefault/ol')} />;
+const PRE_WRAP = <pre className={cx('public/DraftStyleDefault/pre')} />;
+
+function getWrapperTemplateForBlockType(
+  blockType: DraftBlockType
+): ?ReactElement {
+  switch (blockType) {
+    case 'unordered-list-item':
+      return UL_WRAP;
+    case 'ordered-list-item':
+      return OL_WRAP;
+    case 'code-block':
+      return PRE_WRAP;
+    default:
+      return null;
+  }
+}
+
+module.exports = getWrapperTemplateForBlockType;
diff --git a/src/component/utils/isSoftNewlineEvent.js b/src/component/utils/isSoftNewlineEvent.js
new file mode 100644
index 0000000..1ef6692
--- /dev/null
+++ b/src/component/utils/isSoftNewlineEvent.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule isSoftNewlineEvent
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Keys = require('Keys');
+
+function isSoftNewlineEvent(e: SyntheticKeyboardEvent): boolean {
+  return e.which === Keys.RETURN && (
+    e.getModifierState('Shift') ||
+    e.getModifierState('Alt') ||
+    e.getModifierState('Control')
+  );
+}
+
+module.exports = isSoftNewlineEvent;
diff --git a/src/component/utils/splitTextIntoTextBlocks.js b/src/component/utils/splitTextIntoTextBlocks.js
new file mode 100644
index 0000000..1b2f4c5
--- /dev/null
+++ b/src/component/utils/splitTextIntoTextBlocks.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule splitTextIntoTextBlocks
+ * @flow
+ */
+
+'use strict';
+
+var NEWLINE_REGEX = /\r\n?|\n/g;
+
+function splitTextIntoTextBlocks(text: string): Array<string> {
+  return text.split(NEWLINE_REGEX);
+}
+
+module.exports = splitTextIntoTextBlocks;
diff --git a/src/model/constants/ComposedEntityMutability.js b/src/model/constants/ComposedEntityMutability.js
new file mode 100644
index 0000000..2ecbf3c
--- /dev/null
+++ b/src/model/constants/ComposedEntityMutability.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ComposedEntityMutability
+ * @flow
+ */
+
+'use strict';
+
+var ComposedEntityMutability = {
+  'MUTABLE': true,
+  'IMMUTABLE': true,
+  'SEGMENTED': true,
+};
+
+module.exports = ComposedEntityMutability;
diff --git a/src/model/constants/ComposedEntityType.js b/src/model/constants/ComposedEntityType.js
new file mode 100644
index 0000000..4af8e6d
--- /dev/null
+++ b/src/model/constants/ComposedEntityType.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ComposedEntityType
+ * @flow
+ */
+
+'use strict';
+
+var ComposedEntityType = {
+  'LINK': true,
+  'TOKEN': true,
+  'PHOTO': true,
+};
+
+module.exports = ComposedEntityType;
diff --git a/src/model/constants/DraftBlockType.js b/src/model/constants/DraftBlockType.js
new file mode 100644
index 0000000..0b70d77
--- /dev/null
+++ b/src/model/constants/DraftBlockType.js
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftBlockType
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * The list of default valid block types.
+ */
+export type DraftBlockType = (
+  'unstyled' |
+  'paragraph' |
+  'header-one' |
+  'header-two' |
+  'unordered-list-item' |
+  'ordered-list-item' |
+  'blockquote' |
+  'pullquote' |
+  'code-block' |
+  'media'
+);
diff --git a/src/model/constants/DraftEditorCommand.js b/src/model/constants/DraftEditorCommand.js
new file mode 100644
index 0000000..828e17d
--- /dev/null
+++ b/src/model/constants/DraftEditorCommand.js
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEditorCommand
+ * @flow
+ */
+
+/*eslint-disable no-bitwise*/
+
+'use strict';
+
+/**
+ * A set of editor commands that may be invoked by keyboard commands or UI
+ * controls. These commands should map to operations that modify content or
+ * selection state and update the editor state accordingly.
+ */
+export type DraftEditorCommand = (
+  /**
+   * Self-explanatory.
+   */
+  'undo' |
+  'redo' |
+
+  /**
+   * Perform a forward deletion.
+   */
+  'delete' |
+
+  /**
+   * Perform a forward deletion to the next word boundary after the selection.
+   */
+  'delete-word' |
+
+  /**
+   * Perform a backward deletion.
+   */
+  'backspace' |
+
+  /**
+   * Perform a backward deletion to the previous word boundary before the
+   * selection.
+   */
+  'backspace-word' |
+
+  /**
+   * Perform a backward deletion to the beginning of the current line.
+   */
+  'backspace-to-start-of-line' |
+
+  /**
+   * Toggle styles. Commands may be intepreted to modify inline text ranges
+   * or block types.
+   */
+  'bold' |
+  'italic' |
+  'underline' |
+  'code' |
+
+  /**
+   * Split a block in two.
+   */
+  'split-block' |
+
+  /**
+   * Self-explanatory.
+   */
+  'transpose-characters' |
+  'move-selection-to-start-of-block' |
+  'move-selection-to-end-of-block' |
+
+  /**
+   * Commands to support the "secondary" clipboard provided by certain
+   * browsers and operating systems.
+   */
+  'secondary-cut' |
+  'secondary-paste'
+);
diff --git a/src/model/constants/DraftRemovalDirection.js b/src/model/constants/DraftRemovalDirection.js
new file mode 100644
index 0000000..17769f8
--- /dev/null
+++ b/src/model/constants/DraftRemovalDirection.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftRemovalDirection
+ * @flow
+ */
+
+/*eslint-disable no-bitwise*/
+
+'use strict';
+
+/**
+ * A type that allows us to avoid passing boolean arguments
+ * around to indicate whether a deletion is forward or backward.
+ */
+export type DraftRemovalDirection = 'backward' | 'forward';
diff --git a/src/model/decorators/CompositeDraftDecorator.js b/src/model/decorators/CompositeDraftDecorator.js
new file mode 100644
index 0000000..6dc2f4c
--- /dev/null
+++ b/src/model/decorators/CompositeDraftDecorator.js
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CompositeDraftDecorator
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftDecorator} from 'DraftDecorator';
+
+var {List} = Immutable;
+
+var DELIMITER = '.';
+
+/**
+ * A CompositeDraftDecorator traverses through a list of DraftDecorator
+ * instances to identify sections of a ContentBlock that should be rendered
+ * in a "decorated" manner. For example, hashtags, mentions, and links may
+ * be intended to stand out visually, be rendered as anchors, etc.
+ *
+ * The list of decorators supplied to the constructor will be used in the
+ * order they are provided. This allows the caller to specify a priority for
+ * string matching, in case of match collisions among decorators.
+ *
+ * For instance, I may have a link with a `#` in its text. Though this section
+ * of text may match our hashtag decorator, it should not be treated as a
+ * hashtag. I should therefore list my link DraftDecorator
+ * before my hashtag DraftDecorator when constructing this composite
+ * decorator instance.
+ *
+ * Thus, when a collision like this is encountered, the earlier match is
+ * preserved and the new match is discarded.
+ */
+class CompositeDraftDecorator {
+  _decorators: Array<DraftDecorator>;
+
+  constructor(decorators: Array<DraftDecorator>) {
+    // Copy the decorator array, since we use this array order to determine
+    // precedence of decoration matching. If the array is mutated externally,
+    // we don't want to be affected here.
+    this._decorators = decorators.slice();
+  }
+
+  getDecorations(block: ContentBlock): List<?string> {
+    var decorations = Array(block.getText().length).fill(null);
+
+    this._decorators.forEach(
+      (/*object*/ decorator, /*number*/ ii) => {
+        var counter = 0;
+        var strategy = decorator.strategy;
+        strategy(block, (/*number*/ start, /*number*/ end) => {
+          // Find out if any of our matching range is already occupied
+          // by another decorator. If so, discard the match. Otherwise, store
+          // the component key for rendering.
+          if (canOccupySlice(decorations, start, end)) {
+            occupySlice(decorations, start, end, ii + DELIMITER + counter);
+            counter++;
+          }
+        });
+      }
+    );
+
+    return List(decorations);
+  }
+
+  getComponentForKey(key: string): Function {
+    var componentKey = parseInt(key.split(DELIMITER)[0], 10);
+    return this._decorators[componentKey].component;
+  }
+
+  getPropsForKey(key: string): ?Object {
+    var componentKey = parseInt(key.split(DELIMITER)[0], 10);
+    return this._decorators[componentKey].props;
+  }
+}
+
+/**
+ * Determine whether we can occupy the specified slice of the decorations
+ * array.
+ */
+function canOccupySlice(
+  decorations: Array<?string>,
+  start: number,
+  end: number
+): boolean {
+  for (var ii = start; ii < end; ii++) {
+    if (decorations[ii] != null) {
+      return false;
+    }
+  }
+  return true;
+}
+
+/**
+ * Splice the specified component into our decoration array at the desired
+ * range.
+ */
+function occupySlice(
+  targetArr: Array<?string>,
+  start: number,
+  end: number,
+  componentKey: string
+): void {
+  for (var ii = start; ii < end; ii++) {
+    targetArr[ii] = componentKey;
+  }
+}
+
+module.exports = CompositeDraftDecorator;
diff --git a/src/model/decorators/DraftDecorator.js b/src/model/decorators/DraftDecorator.js
new file mode 100644
index 0000000..4eb3515
--- /dev/null
+++ b/src/model/decorators/DraftDecorator.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftDecorator
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+
+/**
+ * A DraftDecorator is a strategy-component pair intended for use when
+ * rendering content.
+ *
+ *   - A "strategy": A function that accepts a ContentBlock object and
+ *     continuously executes a callback with start/end values corresponding to
+ *     relevant matches in the document text. For example, getHashtagMatches
+ *     uses a hashtag regex to find hashtag strings in the block, and
+ *     for each hashtag match, executes the callback with start/end pairs.
+ *
+ *   - A "component": A React component that will be used to render the
+ *     "decorated" section of text.
+ *
+ *   - "props": Props to be passed into the React component that will be used.
+ */
+export type DraftDecorator = {
+  strategy: (
+    block: ContentBlock,
+    callback: (start: number, end: number) => void
+  ) => void,
+  component: Function,
+  props?: Object,
+};
diff --git a/src/model/decorators/DraftDecoratorType.js b/src/model/decorators/DraftDecoratorType.js
new file mode 100644
index 0000000..9e7fc5e
--- /dev/null
+++ b/src/model/decorators/DraftDecoratorType.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftDecoratorType
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+import type {List} from 'immutable';
+
+/**
+ * An interface for document decorator classes, allowing the creation of
+ * custom decorator classes.
+ *
+ * See `CompositeDraftDecorator` for the most common use case.
+ */
+export type DraftDecoratorType = {
+  /**
+   * Given a `ContentBlock`, return an immutable List of decorator keys.
+   */
+  getDecorations(block: ContentBlock): List<?string>,
+
+  /**
+   * Given a decorator key, return the component to use when rendering
+   * this decorated range.
+   */
+  getComponentForKey(key: string): Function,
+
+  /**
+   * Given a decorator key, optionally return the props to use when rendering
+   * this decorated range.
+   */
+  getPropsForKey(key: string): ?Object,
+};
diff --git a/src/model/decorators/__tests__/CompositeDraftDecorator-test.js b/src/model/decorators/__tests__/CompositeDraftDecorator-test.js
new file mode 100644
index 0000000..24af2cf
--- /dev/null
+++ b/src/model/decorators/__tests__/CompositeDraftDecorator-test.js
@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ * @typechecks
+ */
+
+jest.dontMock('CompositeDraftDecorator');
+
+var CompositeDraftDecorator = require('CompositeDraftDecorator');
+
+describe('CompositeDraftDecorator', () => {
+  class ContentBlock {
+    constructor(/*string*/ text) {
+      this._text = text;
+    }
+    getText() /*string*/ {
+      return this._text;
+    }
+  }
+
+  function searchWith(regex) {
+    return function(block, callback) {
+      var text = block.getText();
+      text.replace(
+        regex,
+        function(/*string*/ match, /*number*/ offset) {
+          callback(offset, offset + match.length);
+        }
+      );
+    };
+  }
+
+  var FooSpan = jest.genMockFn();
+  var FooDecorator = {
+    strategy: searchWith(/foo/gi),
+    component: FooSpan,
+  };
+
+  var BarSpan = jest.genMockFn();
+  var BarDecorator = {
+    strategy: searchWith(/bar/gi),
+    component: BarSpan,
+  };
+
+  var BartSpan = jest.genMockFn();
+  var BartDecorator = {
+    strategy: searchWith(/bart/gi),
+    component: BartSpan,
+  };
+
+  function getCompositeDecorator() {
+    var decorators = [FooDecorator, BarDecorator];
+    return new CompositeDraftDecorator(decorators);
+  }
+
+  function isOccupied(arr) {
+    for (var ii = 0; ii < arr.length; ii++) {
+      if (arr[ii] == null) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  function isEntirelyNull(arr) {
+    for (var ii = 0; ii < arr.length; ii++) {
+      if (arr[ii] != null) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  it('must behave correctly if there are no matches', () => {
+    var composite = getCompositeDecorator();
+    var text = 'take a sad song and make it better';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+    expect(decorations.length).toBe(text.length);
+    expect(decorations).toEqual(Array(text.length).fill(null));
+  });
+
+  it('must find decoration matches', () => {
+    var composite = getCompositeDecorator();
+    var text = 'a footballing fool';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+    expect(decorations.length).toBe(text.length);
+
+    expect(isOccupied(decorations.slice(2, 5))).toBe(true);
+    expect(decorations[2]).toEqual(decorations[4]);
+    expect(composite.getComponentForKey(decorations[2])).toBe(FooSpan);
+
+    expect(isEntirelyNull(decorations.slice(5, 14))).toBe(true);
+
+    expect(isOccupied(decorations.slice(14, 17))).toBe(true);
+    expect(decorations[14]).toEqual(decorations[16]);
+    expect(composite.getComponentForKey(decorations[14])).toBe(FooSpan);
+  });
+
+  it('must find matches for multiple decorators', () => {
+    var composite = getCompositeDecorator();
+    var text = 'a foosball bar';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+
+    // Match the "Foo" decorator.
+    expect(isOccupied(decorations.slice(2, 5))).toBe(true);
+    expect(decorations[2]).toEqual(decorations[4]);
+    expect(composite.getComponentForKey(decorations[2])).toBe(FooSpan);
+
+    expect(isEntirelyNull(decorations.slice(5, 11))).toBe(true);
+
+    // Match the "Bar" decorator.
+    expect(isOccupied(decorations.slice(11, 14))).toBe(true);
+    expect(decorations[11]).toEqual(decorations[13]);
+    expect(composite.getComponentForKey(decorations[11])).toBe(BarSpan);
+  });
+
+  // Reverse the order of the matches from above. "foo" comes after "bar" in
+  // the document text.
+  it('must find matches regardless of text location', () => {
+    var composite = getCompositeDecorator();
+    var text = 'some bar food';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+
+    // Match the "Foo" decorator.
+    expect(isOccupied(decorations.slice(9, 12))).toBe(true);
+    expect(decorations[9]).toEqual(decorations[11]);
+    expect(composite.getComponentForKey(decorations[9])).toBe(FooSpan);
+
+    // Match the "Bar" decorator.
+    expect(isOccupied(decorations.slice(5, 8))).toBe(true);
+    expect(decorations[5]).toEqual(decorations[7]);
+    expect(composite.getComponentForKey(decorations[5])).toBe(BarSpan);
+  });
+
+  it('must throw out overlaps with existing decorations', () => {
+    var decorators = [BartDecorator, BarDecorator];
+    var composite = new CompositeDraftDecorator(decorators);
+
+    var text = 'bart has a bar';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+
+    // Even though "bart" matches our "bar" strategy, "bart" comes first
+    // in our decoration order and will claim those letters first.
+    expect(isOccupied(decorations.slice(0, 4))).toBe(true);
+    expect(decorations[0]).toEqual(decorations[3]);
+    expect(composite.getComponentForKey(decorations[0])).toBe(BartSpan);
+
+    // Verify empty space between matches.
+    expect(isEntirelyNull(decorations.slice(4, 11))).toBe(true);
+
+    // "bar" match
+    expect(isOccupied(decorations.slice(11, 14))).toBe(true);
+    expect(decorations[11]).toEqual(decorations[13]);
+    expect(composite.getComponentForKey(decorations[11])).toBe(BarSpan);
+  });
+
+  // Swap the order of "bar" and "bart".
+  it('must throw out matches if earlier match is shorter', () => {
+    var decorators = [BarDecorator, BartDecorator];
+    var composite = new CompositeDraftDecorator(decorators);
+
+    var text = 'bart has a bar';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+
+    // "bar" comes first and claims two strings.
+    expect(isOccupied(decorations.slice(0, 3))).toBe(true);
+    expect(decorations[0]).toEqual(decorations[2]);
+    expect(composite.getComponentForKey(decorations[0])).toBe(BarSpan);
+
+    expect(isOccupied(decorations.slice(11, 14))).toBe(true);
+    expect(decorations[11]).toEqual(decorations[13]);
+    expect(composite.getComponentForKey(decorations[11])).toBe(BarSpan);
+
+    // There are no "bart" matches, since "bar" has claimed the relevant
+    // strings.
+    expect(isEntirelyNull(decorations.slice(3, 11))).toBe(true);
+  });
+
+  it('must separate adjacent ranges that have the same decorator', () => {
+    var decorators = [BarDecorator];
+    var composite = new CompositeDraftDecorator(decorators);
+
+    var text = 'barbarbar';
+    var content = new ContentBlock(text);
+    var decorations = composite.getDecorations(content).toArray();
+
+    expect(isOccupied(decorations.slice(0, 3))).toBe(true);
+    expect(decorations[0]).toEqual(decorations[2]);
+    expect(composite.getComponentForKey(decorations[0])).toBe(BarSpan);
+
+    expect(isOccupied(decorations.slice(3, 6))).toBe(true);
+    expect(decorations[3]).toEqual(decorations[5]);
+    expect(composite.getComponentForKey(decorations[3])).toBe(BarSpan);
+
+    expect(isOccupied(decorations.slice(6, 9))).toBe(true);
+    expect(decorations[6]).toEqual(decorations[8]);
+    expect(composite.getComponentForKey(decorations[6])).toBe(BarSpan);
+
+    expect(decorations[2]).not.toEqual(decorations[3]);
+    expect(decorations[5]).not.toEqual(decorations[6]);
+    expect(decorations[8]).not.toEqual(decorations[9]);
+  });
+});
diff --git a/src/model/encoding/DraftStringKey.js b/src/model/encoding/DraftStringKey.js
new file mode 100644
index 0000000..dc89eee
--- /dev/null
+++ b/src/model/encoding/DraftStringKey.js
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftStringKey
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftStringKey = {
+  stringify: function(key: mixed): string {
+    return '_' + String(key);
+  },
+
+  unstringify: function(key: string): string {
+    return key.slice(1);
+  },
+};
+
+module.exports = DraftStringKey;
diff --git a/src/model/encoding/EntityRange.js b/src/model/encoding/EntityRange.js
new file mode 100644
index 0000000..af24d3d
--- /dev/null
+++ b/src/model/encoding/EntityRange.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EntityRange
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * A plain object representation of an entity attribution.
+ *
+ * The `key` value corresponds to the key of the entity in the `entityMap` of
+ * a `ComposedText` object, not for use with `DraftEntity.get()`.
+ */
+export type EntityRange = {
+  key: number;
+  offset: number;
+  length: number;
+};
diff --git a/src/model/encoding/InlineStyleRange.js b/src/model/encoding/InlineStyleRange.js
new file mode 100644
index 0000000..5a77031
--- /dev/null
+++ b/src/model/encoding/InlineStyleRange.js
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule InlineStyleRange
+ * @flow
+ */
+
+'use strict';
+
+/**
+ * A plain object representation of an inline style range.
+ */
+export type InlineStyleRange = {
+  style: string;
+  offset: number;
+  length: number;
+};
diff --git a/src/model/encoding/RawDraftContentBlock.js b/src/model/encoding/RawDraftContentBlock.js
new file mode 100644
index 0000000..acc721a
--- /dev/null
+++ b/src/model/encoding/RawDraftContentBlock.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule RawDraftContentBlock
+ * @flow
+ */
+
+'use strict';
+
+import type {DraftBlockType} from 'DraftBlockType';
+import type {EntityRange} from 'EntityRange';
+import type {InlineStyleRange} from 'InlineStyleRange';
+
+/**
+ * A plain object representation of a ContentBlock, with all style and entity
+ * attribution repackaged as range objects.
+ */
+export type RawDraftContentBlock = {
+  key: ?string;
+  type: DraftBlockType;
+  text: string;
+  depth: ?number;
+  inlineStyleRanges: ?Array<InlineStyleRange>;
+  entityRanges: ?Array<EntityRange>;
+};
diff --git a/src/model/encoding/RawDraftContentState.js b/src/model/encoding/RawDraftContentState.js
new file mode 100644
index 0000000..7e44508
--- /dev/null
+++ b/src/model/encoding/RawDraftContentState.js
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule RawDraftContentState
+ * @flow
+ */
+
+'use strict';
+
+import type {RawDraftContentBlock} from 'RawDraftContentBlock';
+import type {RawDraftEntity} from 'RawDraftEntity';
+
+/**
+ * A type that represents a composed document as vanilla JavaScript objects,
+ * with all styles and entities represented as ranges. Corresponding entity
+ * objects are packaged as objects as well.
+ *
+ * This object is especially useful when sending the document state to the
+ * server for storage, as its representation is more concise than our
+ * immutable objects.
+ */
+export type RawDraftContentState = {
+  blocks: Array<RawDraftContentBlock>,
+  entityMap: {[key: string]: RawDraftEntity},
+};
diff --git a/src/model/encoding/RawDraftEntity.js b/src/model/encoding/RawDraftEntity.js
new file mode 100644
index 0000000..b474e24
--- /dev/null
+++ b/src/model/encoding/RawDraftEntity.js
@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule RawDraftEntity
+ * @flow
+ */
+
+'use strict';
+
+import type {DraftEntityMutability} from 'DraftEntityMutability';
+import type {DraftEntityType} from 'DraftEntityType';
+
+/**
+ * A plain object representation of an EntityInstance.
+ */
+export type RawDraftEntity = {
+  type: DraftEntityType;
+  mutability: DraftEntityMutability;
+  data: ?{[key: string]: any};
+};
diff --git a/src/model/encoding/__tests__/decodeEntityRanges-test.js b/src/model/encoding/__tests__/decodeEntityRanges-test.js
new file mode 100644
index 0000000..32f94ad
--- /dev/null
+++ b/src/model/encoding/__tests__/decodeEntityRanges-test.js
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var decodeEntityRanges = require('decodeEntityRanges');
+
+describe('decodeEntityRanges', () => {
+  it('must decode when no entities present', () => {
+    var decoded = decodeEntityRanges(' '.repeat(20), []);
+    expect(decoded).toEqual(Array(20).fill(null));
+  });
+
+  it('must decode when an entity is present', () => {
+    var decoded = decodeEntityRanges(
+      ' '.repeat(5),
+      [{
+        offset: 2,
+        length: 2,
+        key: '6',
+      }]
+    );
+    expect(decoded).toEqual([null, null, '6', '6', null]);
+  });
+
+  it('must decode when multiple entities present', () => {
+    var decoded = decodeEntityRanges(
+      ' '.repeat(8),
+      [
+        {
+          offset: 2,
+          length: 2,
+          key: '6',
+        },
+        {
+          offset: 5,
+          length: 2,
+          key: '8',
+        },
+      ]
+    );
+    expect(decoded).toEqual([null, null, '6', '6', null, '8', '8', null]);
+  });
+
+  it('must decode when an entity is present more than once', () => {
+    var decoded = decodeEntityRanges(
+      ' '.repeat(8),
+      [
+        {
+          offset: 2,
+          length: 2,
+          key: '6',
+        },
+        {
+          offset: 5,
+          length: 2,
+          key: '6',
+        },
+      ]
+    );
+    expect(decoded).toEqual([null, null, '6', '6', null, '6', '6', null]);
+  });
+
+  it('must handle ranges that include surrogate pairs', () => {
+    var decoded = decodeEntityRanges(
+      'Take a \uD83D\uDCF7 #selfie',
+      [
+        {
+          offset: 6,
+          length: 5,
+          key: '6',
+        },
+        {
+          offset: 13,
+          length: 2,
+          key: '8',
+        },
+      ]
+    );
+
+    var entities = [
+      null, null, null, null, null, null, // `Take a`
+      '6', '6', '6', '6', '6', '6',       // ` [camera] #s`
+      null, null, '8', '8', null,         // `elfie`
+    ];
+
+    expect(decoded).toEqual(entities);
+  });
+});
diff --git a/src/model/encoding/__tests__/decodeInlineStyleRanges-test.js b/src/model/encoding/__tests__/decodeInlineStyleRanges-test.js
new file mode 100644
index 0000000..a7d0d54
--- /dev/null
+++ b/src/model/encoding/__tests__/decodeInlineStyleRanges-test.js
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var decodeInlineStyleRanges = require('decodeInlineStyleRanges');
+
+var {
+  BOLD,
+  BOLD_ITALIC,
+  BOLD_ITALIC_UNDERLINE,
+  BOLD_UNDERLINE,
+  ITALIC,
+  NONE,
+} = require('SampleDraftInlineStyle');
+
+var {List} = require('immutable');
+
+function areEqual(a, b) {
+  expect(List(a).equals(List(b))).toBeTruthy();
+}
+
+describe('decodeInlineStyleRanges', function() {
+  it('must decode for an unstyled block', function() {
+    var block = {text: 'Hello'};
+    var decoded = decodeInlineStyleRanges(block.text);
+    areEqual(decoded, Array(block.text.length).fill(NONE));
+  });
+
+  it('must decode for a flat styled block', function() {
+    var block = {
+      text: 'Hello',
+      inlineStyleRanges: [
+        {style: 'BOLD', offset: 0, length: 5},
+      ],
+    };
+    var decoded = decodeInlineStyleRanges(
+      block.text,
+      block.inlineStyleRanges
+    );
+    areEqual(decoded, Array(block.text.length).fill(BOLD));
+  });
+
+  it('must decode for a mixed-style block', function() {
+    var block = {
+      text: 'Hello',
+      inlineStyleRanges: [
+        {style: 'BOLD', offset: 0, length: 3},
+        {style: 'ITALIC', offset: 1, length: 3},
+        {style: 'UNDERLINE', offset: 0, length: 2},
+        {style: 'BOLD', offset: 4, length: 1},
+      ],
+    };
+    var decoded = decodeInlineStyleRanges(
+      block.text,
+      block.inlineStyleRanges
+    );
+    areEqual(decoded, [
+      BOLD_UNDERLINE,
+      BOLD_ITALIC_UNDERLINE,
+      BOLD_ITALIC,
+      ITALIC,
+      BOLD,
+    ]);
+  });
+
+  it('must decode for strings that contain surrogate pairs in UTF-16', () => {
+    var block = {
+      text: 'Take a \uD83D\uDCF7 #selfie',
+      inlineStyleRanges: [
+        {offset: 4, length: 4, style: 'BOLD'},
+        {offset: 6, length: 8, style: 'ITALIC'},
+      ],
+    };
+
+    var decoded = decodeInlineStyleRanges(
+      block.text,
+      block.inlineStyleRanges
+    );
+
+    areEqual(decoded, [
+      NONE, NONE, NONE, NONE,
+      BOLD, BOLD, BOLD_ITALIC, BOLD_ITALIC, BOLD_ITALIC,
+      ITALIC, ITALIC, ITALIC, ITALIC, ITALIC, ITALIC,
+      NONE, NONE,
+    ]);
+  });
+
+});
diff --git a/src/model/encoding/__tests__/encodeEntityRanges-test.js b/src/model/encoding/__tests__/encodeEntityRanges-test.js
new file mode 100644
index 0000000..704bf89
--- /dev/null
+++ b/src/model/encoding/__tests__/encodeEntityRanges-test.js
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+
+var createCharacterList = require('createCharacterList');
+var encodeEntityRanges = require('encodeEntityRanges');
+
+var {OrderedSet, Repeat} = Immutable;
+
+describe('encodeEntityRanges', () => {
+  function createBlock(text, entities) {
+    const style = OrderedSet();
+    return new ContentBlock({
+      key: 'a',
+      text,
+      type: 'unstyled',
+      characterList: createCharacterList(
+        Repeat(style, text.length).toArray(),
+        entities,
+      ),
+    });
+  }
+
+  it('must return an empty array if no entities present', () => {
+    var block = createBlock(' '.repeat(20), Repeat(null, 20).toArray());
+    var encoded = encodeEntityRanges(block, {});
+    expect(encoded).toEqual([]);
+
+    encoded = encodeEntityRanges(block, {'0': '0'});
+    expect(encoded).toEqual([]);
+  });
+
+  it('must return ranges with the storage-mapped key', () => {
+    var entities = [null, null, '6', '6', null];
+    var block = createBlock(' '.repeat(entities.length), entities);
+    var encoded = encodeEntityRanges(block, {'_6': '0'});
+    expect(encoded).toEqual([
+      {
+        offset: 2,
+        length: 2,
+        key: 0,
+      },
+    ]);
+  });
+
+  it('must return ranges with multiple entities present', () => {
+    var entities = [null, null, '6', '6', null, '8', '8', null];
+    var block = createBlock(' '.repeat(entities.length), entities);
+    var encoded = encodeEntityRanges(block, {'_6': '0', '_8': '1'});
+    expect(encoded).toEqual([
+      {
+        offset: 2,
+        length: 2,
+        key: 0,
+      },
+      {
+        offset: 5,
+        length: 2,
+        key: 1,
+      },
+    ]);
+  });
+
+  it('must return ranges with an entity present more than once', () => {
+    var entities = [null, null, '6', '6', null, '6', '6', null];
+    var block = createBlock(' '.repeat(entities.length), entities);
+    var encoded = encodeEntityRanges(block, {'_6': '0', '_8': '1'});
+    expect(encoded).toEqual([
+      {
+        offset: 2,
+        length: 2,
+        key: 0,
+      },
+      {
+        offset: 5,
+        length: 2,
+        key: 0,
+      },
+    ]);
+  });
+
+  it('must handle ranges that include surrogate pairs', () => {
+    var str = 'Take a \uD83D\uDCF7 #selfie';
+    var entities = [
+      null, null, null, null, null, null, // `Take a`
+      '6', '6', '6', '6', '6', '6',       // ` [camera] #s`
+      null, null, '8', '8', null,         // `elfie`
+    ];
+
+    var block = createBlock(str, entities);
+    var encoded = encodeEntityRanges(block, {'_6': '0', '_8': '1'});
+    expect(encoded).toEqual([
+      {
+        offset: 6,
+        length: 5,
+        key: 0,
+      },
+      {
+        offset: 13,
+        length: 2,
+        key: 1,
+      },
+    ]);
+  });
+});
diff --git a/src/model/encoding/__tests__/encodeInlineStyleRanges-test.js b/src/model/encoding/__tests__/encodeInlineStyleRanges-test.js
new file mode 100644
index 0000000..c982452
--- /dev/null
+++ b/src/model/encoding/__tests__/encodeInlineStyleRanges-test.js
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+var SampleDraftInlineStyle = require('SampleDraftInlineStyle');
+
+var createCharacterList = require('createCharacterList');
+var encodeInlineStyleRanges = require('encodeInlineStyleRanges');
+
+var {
+  BOLD,
+  BOLD_ITALIC,
+  BOLD_UNDERLINE,
+  BOLD_ITALIC_UNDERLINE,
+  ITALIC,
+  ITALIC_UNDERLINE,
+  NONE,
+} = SampleDraftInlineStyle;
+
+var {
+  List,
+  Repeat,
+} = Immutable;
+
+describe('encodeInlineStyleRanges', () => {
+  function createBlock(text, inlineStyles) {
+    return new ContentBlock({
+      key: 'a',
+      text,
+      type: 'unstyled',
+      characterList: createCharacterList(
+        inlineStyles.toArray(),
+        Repeat(null, text.length).toArray(),
+      ),
+    });
+  }
+
+  it('must encode for an unstyled document', () => {
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(2), Repeat(NONE, 2)))
+    ).toEqual([]);
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(20), Repeat(NONE, 20)))
+    ).toEqual([]);
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(200), Repeat(NONE, 200)))
+    ).toEqual([]);
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(2000), Repeat(NONE, 2000)))
+    ).toEqual([]);
+  });
+
+  it('must encode for a flat styled document', () => {
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(20), Repeat(BOLD, 20)))
+    ).toEqual([
+      {offset: 0, length: 20, style: 'BOLD'},
+    ]);
+    expect(
+      encodeInlineStyleRanges(
+        createBlock(
+          ' '.repeat(20),
+          Repeat(BOLD_ITALIC, 20)
+        )
+      )
+    ).toEqual([
+      {offset: 0, length: 20, style: 'BOLD'},
+      {offset: 0, length: 20, style: 'ITALIC'},
+    ]);
+
+    var all = BOLD_ITALIC_UNDERLINE;
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(20), Repeat(all, 20)))
+    ).toEqual([
+      {offset: 0, length: 20, style: 'BOLD'},
+      {offset: 0, length: 20, style: 'ITALIC'},
+      {offset: 0, length: 20, style: 'UNDERLINE'},
+    ]);
+  });
+
+  it('must encode for a complex styled document', () => {
+    var complex = List([
+      BOLD, BOLD, BOLD, BOLD, NONE,         // "four "
+      BOLD_ITALIC, BOLD_ITALIC,         // "sc"
+      ITALIC_UNDERLINE, BOLD_UNDERLINE, // "or"
+      BOLD_ITALIC_UNDERLINE,            // "e"
+    ]);
+
+    expect(
+      encodeInlineStyleRanges(createBlock(' '.repeat(10), complex))
+    ).toEqual([
+      {offset: 0, length: 4, style: 'BOLD'},
+      {offset: 5, length: 2, style: 'BOLD'},
+      {offset: 8, length: 2, style: 'BOLD'},
+      {offset: 5, length: 3, style: 'ITALIC'},
+      {offset: 9, length: 1, style: 'ITALIC'},
+      {offset: 7, length: 3, style: 'UNDERLINE'},
+    ]);
+  });
+
+  it('must encode for strings with surrogate pairs', () => {
+    var str = 'Take a \uD83D\uDCF7 #selfie';
+    var styles = List([
+      NONE, NONE, NONE, NONE, // `Take`
+      BOLD, BOLD, BOLD_ITALIC, BOLD_ITALIC, BOLD_ITALIC, // ` a [camera]`
+      ITALIC, ITALIC, ITALIC, ITALIC, ITALIC, ITALIC,  // ` #self`
+      NONE, NONE, // `ie`
+    ]);
+
+    expect(
+      encodeInlineStyleRanges(createBlock(str, styles))
+    ).toEqual([
+      {offset: 4, length: 4, style: 'BOLD'},
+      {offset: 6, length: 8, style: 'ITALIC'},
+    ]);
+  });
+});
diff --git a/src/model/encoding/__tests__/sanitizeDraftText-test.js b/src/model/encoding/__tests__/sanitizeDraftText-test.js
new file mode 100644
index 0000000..34ba98b
--- /dev/null
+++ b/src/model/encoding/__tests__/sanitizeDraftText-test.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+jest.autoMockOff();
+
+describe('sanitizeDraftText', () => {
+  var sanitizeDraftText = require('sanitizeDraftText');
+  it('must strip carriage returns', () => {
+    var trailing = 'test\u000d';
+    expect(sanitizeDraftText(trailing)).toBe('test');
+    var twoTrailing = 'test\u000d\u000d';
+    expect(sanitizeDraftText(twoTrailing)).toBe('test');
+    var within = 'te\u000dst';
+    expect(sanitizeDraftText(within)).toBe('test');
+    var leading = '\u000dtest';
+    expect(sanitizeDraftText(leading)).toBe('test');
+    var all = '\u000dt\u000des\u000dt\u000d\u000d';
+    expect(sanitizeDraftText(all)).toBe('test');
+  });
+});
diff --git a/src/model/encoding/convertFromDraftStateToRaw.js b/src/model/encoding/convertFromDraftStateToRaw.js
new file mode 100644
index 0000000..441bbe6
--- /dev/null
+++ b/src/model/encoding/convertFromDraftStateToRaw.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule convertFromDraftStateToRaw
+ * @flow
+ */
+
+'use strict';
+
+var DraftEntity = require('DraftEntity');
+var DraftStringKey = require('DraftStringKey');
+
+var encodeEntityRanges = require('encodeEntityRanges');
+var encodeInlineStyleRanges = require('encodeInlineStyleRanges');
+
+import type ContentBlock from 'ContentBlock';
+import type ContentState from 'ContentState';
+import type {RawDraftContentState} from 'RawDraftContentState';
+
+function convertFromDraftStateToRaw(
+  contentState: ContentState
+): RawDraftContentState {
+  var entityStorageKey = 0;
+  var entityStorageMap = {};
+  var rawBlocks = [];
+
+  contentState.getBlockMap().forEach((block, blockKey) => {
+    block.findEntityRanges(
+      character => character.getEntity() !== null,
+      start => {
+        // Stringify to maintain order of otherwise numeric keys.
+        var stringifiedEntityKey = DraftStringKey.stringify(
+          block.getEntityAt(start)
+        );
+        if (!entityStorageMap.hasOwnProperty(stringifiedEntityKey)) {
+          entityStorageMap[stringifiedEntityKey] = '' + (entityStorageKey++);
+        }
+      }
+    );
+
+    rawBlocks.push({
+      key: blockKey,
+      text: block.getText(),
+      type: block.getType(),
+      depth: canHaveDepth(block) ? block.getDepth() : 0,
+      inlineStyleRanges: encodeInlineStyleRanges(block),
+      entityRanges: encodeEntityRanges(block, entityStorageMap),
+    });
+  });
+
+  // Flip storage map so that our storage keys map to global
+  // DraftEntity keys.
+  var entityKeys = Object.keys(entityStorageMap);
+  var flippedStorageMap = {};
+  entityKeys.forEach((key, jj) => {
+    var entity = DraftEntity.get(DraftStringKey.unstringify(key));
+    flippedStorageMap[jj] = {
+      type: entity.getType(),
+      mutability: entity.getMutability(),
+      data: entity.getData(),
+    };
+  });
+
+  return {
+    entityMap: flippedStorageMap,
+    blocks: rawBlocks,
+  };
+}
+
+function canHaveDepth(block: ContentBlock): boolean {
+  var type = block.getType();
+  return type === 'ordered-list-item' || type === 'unordered-list-item';
+}
+
+module.exports = convertFromDraftStateToRaw;
diff --git a/src/model/encoding/convertFromRawToDraftState.js b/src/model/encoding/convertFromRawToDraftState.js
new file mode 100644
index 0000000..21039f9
--- /dev/null
+++ b/src/model/encoding/convertFromRawToDraftState.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule convertFromRawToDraftState
+ * @flow
+ */
+
+'use strict';
+
+var ContentBlock = require('ContentBlock');
+var DraftEntity = require('DraftEntity');
+
+var createCharacterList = require('createCharacterList');
+var decodeEntityRanges = require('decodeEntityRanges');
+var decodeInlineStyleRanges = require('decodeInlineStyleRanges');
+var generateBlockKey = require('generateBlockKey');
+
+import type {RawDraftContentState} from 'RawDraftContentState';
+
+function convertFromRawToDraftState(
+  rawState: RawDraftContentState
+): Array<ContentBlock> {
+  var {blocks, entityMap} = rawState;
+
+  var fromStorageToLocal = {};
+  Object.keys(entityMap).forEach(
+    storageKey => {
+      var encodedEntity = entityMap[storageKey];
+      var {type, mutability, data} = encodedEntity;
+      var newKey = DraftEntity.create(type, mutability, data || {});
+      fromStorageToLocal[storageKey] = newKey;
+    }
+  );
+
+  return blocks.map(
+    block => {
+      var {key, type, text, depth, inlineStyleRanges, entityRanges} = block;
+      key = key || generateBlockKey();
+      depth = depth || 0;
+      inlineStyleRanges = inlineStyleRanges || [];
+      entityRanges = entityRanges || [];
+
+      var inlineStyles = decodeInlineStyleRanges(text, inlineStyleRanges);
+
+      // Translate entity range keys to the DraftEntity map.
+      var filteredEntityRanges = entityRanges
+        .filter(range => fromStorageToLocal.hasOwnProperty(range.key))
+        .map(range => {
+          return {...range, key: fromStorageToLocal[range.key]};
+        });
+
+      var entities = decodeEntityRanges(text, filteredEntityRanges);
+      var characterList = createCharacterList(inlineStyles, entities);
+
+      return new ContentBlock({key, type, text, depth, characterList});
+    }
+  );
+}
+
+module.exports = convertFromRawToDraftState;
diff --git a/src/model/encoding/createCharacterList.js b/src/model/encoding/createCharacterList.js
new file mode 100644
index 0000000..a60a324
--- /dev/null
+++ b/src/model/encoding/createCharacterList.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule createCharacterList
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+var Immutable = require('immutable');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+var {List} = Immutable;
+
+function createCharacterList(
+  inlineStyles: Array<DraftInlineStyle>,
+  entities: Array<?string>
+): List<CharacterMetadata> {
+  var characterArray = inlineStyles.map((style, ii) => {
+    var entity = entities[ii];
+    return CharacterMetadata.create({style, entity});
+  });
+  return List(characterArray);
+}
+
+module.exports = createCharacterList;
diff --git a/src/model/encoding/decodeEntityRanges.js b/src/model/encoding/decodeEntityRanges.js
new file mode 100644
index 0000000..ca2b1ab
--- /dev/null
+++ b/src/model/encoding/decodeEntityRanges.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule decodeEntityRanges
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var UnicodeUtils = require('UnicodeUtils');
+
+var {substr} = UnicodeUtils;
+
+/**
+ * Convert to native JavaScript string lengths to determine ranges.
+ */
+function decodeEntityRanges(
+  text: string,
+  ranges: Array<Object>
+): Array<?string> {
+  var entities = Array(text.length).fill(null);
+  if (ranges) {
+    ranges.forEach(
+      range => {
+        // Using Unicode-enabled substrings converted to JavaScript lengths,
+        // fill the output array with entity keys.
+        var start = substr(text, 0, range.offset).length;
+        var end = start + substr(text, range.offset, range.length).length;
+        for (var ii = start; ii < end; ii++) {
+          entities[ii] = range.key;
+        }
+      }
+    );
+  }
+  return entities;
+}
+
+module.exports = decodeEntityRanges;
diff --git a/src/model/encoding/decodeInlineStyleRanges.js b/src/model/encoding/decodeInlineStyleRanges.js
new file mode 100644
index 0000000..78c2709
--- /dev/null
+++ b/src/model/encoding/decodeInlineStyleRanges.js
@@ -0,0 +1,47 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule decodeInlineStyleRanges
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var UnicodeUtils = require('UnicodeUtils');
+
+var {OrderedSet} = require('immutable');
+
+var {substr} = UnicodeUtils;
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+const EMPTY_SET = OrderedSet();
+
+/**
+ * Convert to native JavaScript string lengths to determine ranges.
+ */
+function decodeInlineStyleRanges(
+  text: string,
+  ranges?: Array<Object>
+): Array<DraftInlineStyle> {
+  var styles = Array(text.length).fill(EMPTY_SET);
+  if (ranges) {
+    ranges.forEach((/*object*/ range) => {
+      var cursor = substr(text, 0, range.offset).length;
+      var end = cursor + substr(text, range.offset, range.length).length;
+      while (cursor < end) {
+        styles[cursor] = styles[cursor].add(range.style);
+        cursor++;
+      }
+    });
+  }
+  return styles;
+}
+
+module.exports = decodeInlineStyleRanges;
diff --git a/src/model/encoding/encodeEntityRanges.js b/src/model/encoding/encodeEntityRanges.js
new file mode 100644
index 0000000..adedcca
--- /dev/null
+++ b/src/model/encoding/encodeEntityRanges.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule encodeEntityRanges
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftStringKey = require('DraftStringKey');
+var UnicodeUtils = require('UnicodeUtils');
+
+import type ContentBlock from 'ContentBlock';
+import type {EntityRange} from 'EntityRange';
+
+var {strlen} = UnicodeUtils;
+
+/**
+ * Convert to UTF-8 character counts for storage.
+ */
+function encodeEntityRanges(
+  block: ContentBlock,
+  storageMap: Object
+): Array<EntityRange> {
+  var encoded = [];
+  block.findEntityRanges(
+    character => !!character.getEntity(),
+    (/*number*/ start, /*number*/ end) => {
+      var text = block.getText();
+      var key = block.getEntityAt(start);
+      encoded.push({
+        offset: strlen(text.slice(0, start)),
+        length: strlen(text.slice(start, end)),
+        // Encode the key as a number for range storage.
+        key: Number(storageMap[DraftStringKey.stringify(key)]),
+      });
+    }
+  );
+  return encoded;
+}
+
+module.exports = encodeEntityRanges;
diff --git a/src/model/encoding/encodeInlineStyleRanges.js b/src/model/encoding/encodeInlineStyleRanges.js
new file mode 100644
index 0000000..98462a2
--- /dev/null
+++ b/src/model/encoding/encodeInlineStyleRanges.js
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule encodeInlineStyleRanges
+ * @flow
+ */
+
+'use strict';
+
+var DefaultDraftInlineStyle = require('DefaultDraftInlineStyle');
+var UnicodeUtils = require('UnicodeUtils');
+
+var findRangesImmutable = require('findRangesImmutable');
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+import type {InlineStyleRange} from 'InlineStyleRange';
+import type {List} from 'immutable';
+
+var areEqual = (a, b) => a === b;
+var isTruthy = a => !!a;
+var EMPTY_ARRAY = [];
+
+/**
+ * Helper function for getting encoded styles for each inline style. Convert
+ * to UTF-8 character counts for storage.
+ */
+function getEncodedInlinesForType(
+  block: ContentBlock,
+  styleList: List<DraftInlineStyle>,
+  styleToEncode: string
+): Array<InlineStyleRange> {
+  var ranges = [];
+
+  // Obtain an array with ranges for only the specified style.
+  var filteredInlines = styleList
+    .map(style => style.has(styleToEncode))
+    .toList();
+
+  findRangesImmutable(
+    filteredInlines,
+    areEqual,
+    // We only want to keep ranges with nonzero style values.
+    isTruthy,
+    (start, end) => {
+      var text = block.getText();
+      ranges.push({
+        offset: UnicodeUtils.strlen(text.slice(0, start)),
+        length: UnicodeUtils.strlen(text.slice(start, end)),
+        style: styleToEncode,
+      });
+    }
+  );
+
+  return ranges;
+}
+
+/*
+ * Retrieve the encoded arrays of inline styles, with each individual style
+ * treated separately.
+ */
+function encodeInlineStyleRanges(
+  block: ContentBlock
+): Array<InlineStyleRange> {
+  var styleList = block.getCharacterList().map(c => c.getStyle()).toList();
+  var styles = Object.keys(DefaultDraftInlineStyle);
+  var ranges: Array<Array<InlineStyleRange>> = styles
+    .map(style => getEncodedInlinesForType(block, styleList, style));
+
+  return Array.prototype.concat.apply(EMPTY_ARRAY, ranges);
+}
+
+module.exports = encodeInlineStyleRanges;
diff --git a/src/model/encoding/sanitizeDraftText.js b/src/model/encoding/sanitizeDraftText.js
new file mode 100644
index 0000000..c94cd88
--- /dev/null
+++ b/src/model/encoding/sanitizeDraftText.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule sanitizeDraftText
+ * @flow
+ */
+
+'use strict';
+
+var REGEX_BLOCK_DELIMITER = new RegExp('\r', 'g');
+
+function sanitizeDraftText(input: string): string {
+  return input.replace(REGEX_BLOCK_DELIMITER, '');
+}
+
+module.exports = sanitizeDraftText;
diff --git a/src/model/entity/DraftEntity.js b/src/model/entity/DraftEntity.js
new file mode 100644
index 0000000..e996832
--- /dev/null
+++ b/src/model/entity/DraftEntity.js
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEntity
+ * @typechecks
+ * @flow
+ */
+
+var DraftEntityInstance = require('DraftEntityInstance');
+var Immutable = require('immutable');
+
+var invariant = require('invariant');
+
+import type {DraftEntityMutability} from 'DraftEntityMutability';
+import type {DraftEntityType} from 'DraftEntityType';
+
+var {Map} = Immutable;
+
+var instances: Map<string, DraftEntityInstance> = Map();
+var instanceKey = 0;
+
+/**
+ * A "document entity" is an object containing metadata associated with a
+ * piece of text in a ContentBlock.
+ *
+ * For example, a `link` entity might include a `uri` property. When a
+ * ContentBlock is rendered in the browser, text that refers to that link
+ * entity may be rendered as an anchor, with the `uri` as the href value.
+ *
+ * In a ContentBlock, every position in the text may correspond to zero
+ * or one entities. This correspondence is tracked using a key string,
+ * generated via DraftEntity.create() and used to obtain entity metadata
+ * via DraftEntity.get().
+ */
+var DraftEntity = {
+  /**
+   * Create a DraftEntityInstance and store it for later retrieval.
+   *
+   * A random key string will be generated and returned. This key may
+   * be used to track the entity's usage in a ContentBlock, and for
+   * retrieving data about the entity at render time.
+   */
+  create: function(
+    type: DraftEntityType,
+    mutability: DraftEntityMutability,
+    data?: Object
+  ): string {
+    return DraftEntity.add(
+      new DraftEntityInstance({type, mutability, data: data || {}})
+    );
+  },
+
+  /**
+   * Add an existing DraftEntityInstance to the DraftEntity map. This is
+   * useful when restoring instances from the server.
+   */
+  add: function(instance: DraftEntityInstance): string {
+    var key = '' + (++instanceKey);
+    instances = instances.set(key, instance);
+    return key;
+  },
+
+  /**
+   * Retrieve the entity corresponding to the supplied key string.
+   */
+  get: function(key: string): DraftEntityInstance {
+    var instance = instances.get(key);
+    invariant(!!instance, 'Unknown DraftEntity key.');
+    return instance;
+  },
+
+  /**
+   * Entity instances are immutable. If you need to update the data for an
+   * instance, this method will merge your data updates and return a new
+   * instance.
+   */
+  mergeData: function(
+    key: string,
+    toMerge: {[key: string]: any}
+  ): DraftEntityInstance {
+    var instance = DraftEntity.get(key);
+    var newData = {...instance.getData(), ...toMerge};
+    var newInstance = instance.set('data', newData);
+    instances = instances.set(key, newInstance);
+    return newInstance;
+  },
+
+  /**
+   * Completely replace the data for a given instance.
+   */
+  replaceData: function(
+    key: string,
+    newData: {[key: string]: any}
+  ): DraftEntityInstance {
+    const instance = DraftEntity.get(key);
+    const newInstance = instance.set('data', newData);
+    instances = instances.set(key, newInstance);
+    return newInstance;
+  },
+};
+
+module.exports = DraftEntity;
diff --git a/src/model/entity/DraftEntityInstance.js b/src/model/entity/DraftEntityInstance.js
new file mode 100644
index 0000000..7cee07c
--- /dev/null
+++ b/src/model/entity/DraftEntityInstance.js
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEntityInstance
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+import type {DraftEntityType} from 'DraftEntityType';
+import type {DraftEntityMutability} from 'DraftEntityMutability';
+
+var {Record} = Immutable;
+
+var DraftEntityInstanceRecord = Record({
+  type: 'TOKEN',
+  mutability: 'IMMUTABLE',
+  data: Object,
+});
+
+/**
+ * An instance of a document entity, consisting of a `type` and relevant
+ * `data`, metadata about the entity.
+ *
+ * For instance, a "link" entity might provide a URI, and a "mention"
+ * entity might provide the mentioned user's ID. These pieces of data
+ * may be used when rendering the entity as part of a ContentBlock DOM
+ * representation. For a link, the data would be used as an href for
+ * the rendered anchor. For a mention, the ID could be used to retrieve
+ * a hovercard.
+ */
+class DraftEntityInstance extends DraftEntityInstanceRecord {
+  getType(): DraftEntityType {
+    return this.get('type');
+  }
+
+  getMutability(): DraftEntityMutability {
+    return this.get('mutability');
+  }
+
+  getData(): Object {
+    return this.get('data');
+  }
+}
+
+module.exports = DraftEntityInstance;
diff --git a/src/model/entity/DraftEntityMutability.js b/src/model/entity/DraftEntityMutability.js
new file mode 100644
index 0000000..46c9576
--- /dev/null
+++ b/src/model/entity/DraftEntityMutability.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEntityMutability
+ * @flow
+ */
+
+'use strict';
+
+var ComposedEntityMutability = require('ComposedEntityMutability');
+
+/**
+ * An enum representing the possible "mutability" options for an entity.
+ * This refers to the behavior that should occur when inserting or removing
+ * characters in a text range with an entity applied to it.
+ *
+ * `MUTABLE`:
+ *   The text range can be modified freely. Generally used in cases where
+ *   the text content and the entity do not necessarily have a direct
+ *   relationship. For instance, the text and URI for a link may be completely
+ *   different. The user is allowed to edit the text as needed, and the entity
+ *   is preserved and applied to any characters added within the range.
+ *
+ * `IMMUTABLE`:
+ *   Not to be confused with immutable data structures used to represent the
+ *   state of the editor. Immutable entity ranges cannot be modified in any
+ *   way. Adding characters within the range will remove the entity from the
+ *   entire range. Deleting characters will delete the entire range. Example:
+ *   Facebook Page mentions.
+ *
+ * `SEGMENTED`:
+ *   Segmented entities allow the removal of partial ranges of text, as
+ *   separated by a delimiter. Adding characters wihin the range will remove
+ *   the entity from the entire range. Deleting characters within a segmented
+ *   entity will delete only the segments affected by the deletion. Example:
+ *   Facebook User mentions.
+ */
+
+export type DraftEntityMutability = $Keys<typeof ComposedEntityMutability>;
diff --git a/src/model/entity/DraftEntityType.js b/src/model/entity/DraftEntityType.js
new file mode 100644
index 0000000..6f6477c
--- /dev/null
+++ b/src/model/entity/DraftEntityType.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEntityType
+ * @flow
+ */
+
+'use strict';
+
+import typeof ComposedEntityType from 'ComposedEntityType';
+
+export type DraftEntityType = $Keys<ComposedEntityType>;
diff --git a/src/model/entity/__mocks__/DraftEntity.js b/src/model/entity/__mocks__/DraftEntity.js
new file mode 100644
index 0000000..dce8e1b
--- /dev/null
+++ b/src/model/entity/__mocks__/DraftEntity.js
@@ -0,0 +1,22 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+var DraftEntity = jest.genMockFromModule('DraftEntity');
+
+var DraftEntityInstance = {
+  getType: jest.genMockFn().mockReturnValue(''),
+  getMutability: jest.genMockFn().mockReturnValue(''),
+  getData: jest.genMockFn().mockReturnValue({}),
+};
+
+var count = 0;
+
+DraftEntity.create = jest.genMockFn().mockImplementation(function() {
+  count++;
+  return '' + count;
+});
+
+DraftEntity.get = jest.genMockFn().mockImplementation(function() {
+  return DraftEntityInstance;
+});
+
+module.exports = DraftEntity;
diff --git a/src/model/entity/__tests__/DraftEntity-test.js b/src/model/entity/__tests__/DraftEntity-test.js
new file mode 100644
index 0000000..adda450
--- /dev/null
+++ b/src/model/entity/__tests__/DraftEntity-test.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var DraftEntity = require('DraftEntity');
+
+describe('DraftEntity', () => {
+  beforeEach(() => {
+    jest.resetModuleRegistry();
+  });
+
+  function createLink() {
+    return DraftEntity.create('LINK', 'MUTABLE', {uri: 'zombo.com'});
+  }
+
+  it('must create instances', () => {
+    var key = createLink();
+    expect(typeof key).toBe('string');
+  });
+
+  it('must retrieve an instance given a key', () => {
+    var key = createLink();
+    var retrieved = DraftEntity.get(key);
+    expect(retrieved.getType()).toBe('LINK');
+    expect(retrieved.getMutability()).toBe('MUTABLE');
+    expect(retrieved.getData()).toEqual({uri: 'zombo.com'});
+  });
+
+  it('must throw when retrieving for an invalid key', () => {
+    createLink();
+    expect(() => DraftEntity.get('asdfzxcvqweriuop')).toThrow();
+    expect(() => DraftEntity.get(null)).toThrow();
+  });
+
+  it('must merge data', () => {
+    var key = createLink();
+
+    // Merge new property.
+    var newData = {foo: 'bar'};
+    DraftEntity.mergeData(key, newData);
+    var newEntity = DraftEntity.get(key);
+    expect(newEntity.getData()).toEqual({
+      uri: 'zombo.com',
+      foo: 'bar',
+    });
+
+    // Replace existing property.
+    var withNewURI = {uri: 'homestarrunner.com'};
+    DraftEntity.mergeData(key, withNewURI);
+    var entityWithNewURI = DraftEntity.get(key);
+    expect(entityWithNewURI.getData()).toEqual({
+      uri: 'homestarrunner.com',
+      foo: 'bar',
+    });
+  });
+});
diff --git a/src/model/entity/__tests__/getEntityKeyForSelection-test.js b/src/model/entity/__tests__/getEntityKeyForSelection-test.js
new file mode 100644
index 0000000..9c277bd
--- /dev/null
+++ b/src/model/entity/__tests__/getEntityKeyForSelection-test.js
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+jest.autoMockOff();
+
+jest.mock('DraftEntity');
+
+var getEntityKeyForSelection = require('getEntityKeyForSelection');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+
+var {
+  contentState,
+  selectionState,
+} = getSampleStateForTesting();
+
+selectionState = selectionState.merge({
+  anchorKey: 'b',
+  focusKey: 'b',
+});
+
+var DraftEntity = require('DraftEntity');
+
+describe('getEntityKeyForSelection', () => {
+  describe('collapsed selection', () => {
+    var collapsed = selectionState.merge({
+      anchorOffset: 2,
+      focusOffset: 2,
+    });
+
+    beforeEach(() => {
+      DraftEntity.get.mockClear();
+    });
+
+    it('must return null at start of block', () => {
+      var key = getEntityKeyForSelection(contentState, selectionState);
+      expect(key).toBe(null);
+    });
+
+    it('must return key if mutable', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'MUTABLE'};
+      });
+      var key = getEntityKeyForSelection(contentState, collapsed);
+      expect(key).toBe('123');
+    });
+
+    it('must not return key if immutable', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'IMMUTABLE'};
+      });
+      var key = getEntityKeyForSelection(contentState, collapsed);
+      expect(key).toBe(null);
+    });
+
+    it('must not return key if segmented', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'SEGMENTED'};
+      });
+      var key = getEntityKeyForSelection(contentState, collapsed);
+      expect(key).toBe(null);
+    });
+  });
+
+  describe('non-collapsed selection', () => {
+    var nonCollapsed = selectionState.merge({
+      anchorOffset: 2,
+      focusKey: 'c',
+      focusOffset: 2,
+    });
+
+    beforeEach(() => {
+      DraftEntity.get.mockClear();
+    });
+
+    it('must return null if start is at end of block', () => {
+      var startsAtEnd = nonCollapsed.merge({
+        anchorOffset: contentState.getBlockForKey('b').getLength(),
+      });
+      var key = getEntityKeyForSelection(contentState, startsAtEnd);
+      expect(key).toBe(null);
+    });
+
+    it('must return key if mutable', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'MUTABLE'};
+      });
+      var key = getEntityKeyForSelection(contentState, nonCollapsed);
+      expect(key).toBe('123');
+    });
+
+    it('must not return key if immutable', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'IMMUTABLE'};
+      });
+      var key = getEntityKeyForSelection(contentState, nonCollapsed);
+      expect(key).toBe(null);
+    });
+
+    it('must not return key if segmented', () => {
+      DraftEntity.get.mockImplementation(() => {
+        return {getMutability: () => 'SEGMENTED'};
+      });
+      var key = getEntityKeyForSelection(contentState, nonCollapsed);
+      expect(key).toBe(null);
+    });
+  });
+});
diff --git a/src/model/entity/getEntityKeyForSelection.js b/src/model/entity/getEntityKeyForSelection.js
new file mode 100644
index 0000000..0b52684
--- /dev/null
+++ b/src/model/entity/getEntityKeyForSelection.js
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getEntityKeyForSelection
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftEntity = require('DraftEntity');
+
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Return the entity key that should be used when inserting text for the
+ * specified target selection, only if the entity is `MUTABLE`. `IMMUTABLE`
+ * and `SEGMENTED` entities should not be used for insertion behavior.
+ */
+function getEntityKeyForSelection(
+  contentState: ContentState,
+  targetSelection: SelectionState
+): ?string {
+  var entityKey;
+
+  if (targetSelection.isCollapsed()) {
+    var key = targetSelection.getAnchorKey();
+    var offset = targetSelection.getAnchorOffset();
+    if (offset > 0) {
+      entityKey = contentState.getBlockForKey(key).getEntityAt(offset - 1);
+      return filterKey(entityKey);
+    }
+    return null;
+  }
+
+  var startKey = targetSelection.getStartKey();
+  var startOffset = targetSelection.getStartOffset();
+  var startBlock = contentState.getBlockForKey(startKey);
+
+  entityKey = startOffset === startBlock.getLength() ?
+    null :
+    startBlock.getEntityAt(startOffset);
+
+  return filterKey(entityKey);
+}
+
+/**
+ * Determine whether an entity key corresponds to a `MUTABLE` entity. If so,
+ * return it. If not, return null.
+ */
+function filterKey(
+  entityKey: ?string
+): ?string {
+  if (entityKey) {
+    var entity = DraftEntity.get(entityKey);
+    return entity.getMutability() === 'MUTABLE' ? entityKey : null;
+  }
+  return null;
+}
+
+module.exports = getEntityKeyForSelection;
diff --git a/src/model/entity/getTextAfterNearestEntity.js b/src/model/entity/getTextAfterNearestEntity.js
new file mode 100644
index 0000000..a1dd5f3
--- /dev/null
+++ b/src/model/entity/getTextAfterNearestEntity.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getTextAfterNearestEntity
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+
+/**
+ * Find the string of text between the previous entity and the specified
+ * offset. This allows us to narrow down search areas for regex matching.
+ */
+function getTextAfterNearestEntity(
+  block: ContentBlock,
+  offset: number
+): string {
+  var start = offset;
+
+  // Get start based on where the last entity ended.
+  while (start > 0 && block.getEntityAt(start - 1) === null) {
+    start--;
+  }
+
+  return block.getText().slice(start, offset);
+}
+
+module.exports = getTextAfterNearestEntity;
diff --git a/src/model/immutable/BlockMap.js b/src/model/immutable/BlockMap.js
new file mode 100644
index 0000000..6b78451
--- /dev/null
+++ b/src/model/immutable/BlockMap.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BlockMap
+ * @flow
+ */
+
+'use strict';
+
+import type ContentBlock from 'ContentBlock';
+import type {OrderedMap} from 'immutable';
+
+export type BlockMap = OrderedMap<string, ContentBlock>;
diff --git a/src/model/immutable/BlockMapBuilder.js b/src/model/immutable/BlockMapBuilder.js
new file mode 100644
index 0000000..e65a3bd
--- /dev/null
+++ b/src/model/immutable/BlockMapBuilder.js
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BlockMapBuilder
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentBlock from 'ContentBlock';
+
+var {OrderedMap} = Immutable;
+
+var BlockMapBuilder = {
+  createFromArray: function(
+    blocks: Array<ContentBlock>
+  ): BlockMap {
+    return OrderedMap(
+      blocks.map(
+        block => [block.getKey(), block]
+      )
+    );
+  },
+};
+
+module.exports = BlockMapBuilder;
diff --git a/src/model/immutable/BlockTree.js b/src/model/immutable/BlockTree.js
new file mode 100644
index 0000000..aa84b21
--- /dev/null
+++ b/src/model/immutable/BlockTree.js
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule BlockTree
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+var emptyFunction = require('emptyFunction');
+var findRangesImmutable = require('findRangesImmutable');
+
+import type CharacterMetadata from 'CharacterMetadata';
+import type ContentBlock from 'ContentBlock';
+import type {DraftDecoratorType} from 'DraftDecoratorType';
+
+var {
+  List,
+  Repeat,
+  Record,
+} = Immutable;
+
+var returnTrue = emptyFunction.thatReturnsTrue;
+
+var FINGERPRINT_DELIMITER = '-';
+
+var defaultLeafRange: {
+  start: ?number;
+  end: ?number;
+} = {
+  start: null,
+  end: null,
+};
+
+var LeafRange = Record(defaultLeafRange);
+
+var defaultDecoratorRange: {
+  start: ?number;
+  end: ?number;
+  decoratorKey: ?string;
+  leaves: ?List<LeafRange>;
+} = {
+  start: null,
+  end: null,
+  decoratorKey: null,
+  leaves: null,
+};
+
+var DecoratorRange = Record(defaultDecoratorRange);
+
+var BlockTree = {
+  /**
+   * Generate a block tree for a given ContentBlock/decorator pair.
+   */
+  generate: function(
+    block: ContentBlock,
+    decorator: ?DraftDecoratorType
+  ): List<DecoratorRange> {
+    var textLength = block.getLength();
+    if (!textLength) {
+      return List.of(
+        new DecoratorRange({
+          start: 0,
+          end: 0,
+          decoratorKey: null,
+          leaves: List.of(
+            new LeafRange({start: 0, end: 0})
+          ),
+        })
+      );
+    }
+
+    var leafSets = [];
+    var decorations = decorator ?
+      decorator.getDecorations(block) :
+      List(Repeat(null, textLength));
+
+    var chars = block.getCharacterList();
+
+    findRangesImmutable(
+      decorations,
+      areEqual,
+      returnTrue,
+      (start, end) => {
+        leafSets.push(
+          new DecoratorRange({
+            start,
+            end,
+            decoratorKey: decorations.get(start),
+            leaves: generateLeaves(
+              chars.slice(start, end).toList(),
+              start
+            ),
+          })
+        );
+      }
+    );
+
+    return List(leafSets);
+  },
+
+  /**
+   * Create a string representation of the given tree map. This allows us
+   * to rapidly determine whether a tree has undergone a significant
+   * structural change.
+   */
+  getFingerprint: function(tree: List<DecoratorRange>): string {
+    return tree.map(
+      leafSet => {
+        var decoratorKey = leafSet.get('decoratorKey');
+        var fingerprintString = decoratorKey !== null ?
+          decoratorKey + '.' + (leafSet.get('end') - leafSet.get('start')) :
+          '';
+        return '' + fingerprintString + '.' + leafSet.get('leaves').size;
+      }
+    ).join(FINGERPRINT_DELIMITER);
+  },
+};
+
+/**
+ * Generate LeafRange records for a given character list.
+ */
+function generateLeaves(
+  characters: List<CharacterMetadata>,
+  offset: number
+): List<LeafRange> {
+  var leaves = [];
+  var inlineStyles = characters.map(c => c.getStyle()).toList();
+  findRangesImmutable(
+    inlineStyles,
+    areEqual,
+    returnTrue,
+    (start, end) => {
+      leaves.push(
+        new LeafRange({
+          start: start + offset,
+          end: end + offset,
+        })
+      );
+    }
+  );
+  return List(leaves);
+}
+
+function areEqual(a: any, b: any): boolean {
+  return a === b;
+}
+
+module.exports = BlockTree;
diff --git a/src/model/immutable/CharacterMetadata.js b/src/model/immutable/CharacterMetadata.js
new file mode 100644
index 0000000..d1c52fe
--- /dev/null
+++ b/src/model/immutable/CharacterMetadata.js
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule CharacterMetadata
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var {
+  Map,
+  OrderedSet,
+  Record,
+} = require('immutable');
+
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+type CharacterMetadataConfig = {
+  style?: DraftInlineStyle;
+  entity?: ?string;
+};
+
+const EMPTY_SET = OrderedSet();
+
+var defaultRecord: CharacterMetadataConfig = {
+  style: EMPTY_SET,
+  entity: null,
+};
+
+var CharacterMetadataRecord = Record(defaultRecord);
+
+class CharacterMetadata extends CharacterMetadataRecord {
+  getStyle(): DraftInlineStyle {
+    return this.get('style');
+  }
+
+  getEntity(): ?string {
+    return this.get('entity');
+  }
+
+  hasStyle(style: string): boolean {
+    return this.getStyle().has(style);
+  }
+
+  static applyStyle(
+    record: CharacterMetadata,
+    style: string
+  ): CharacterMetadata {
+    var withStyle = record.set('style', record.getStyle().add(style));
+    return CharacterMetadata.create(withStyle);
+  }
+
+  static removeStyle(
+    record: CharacterMetadata,
+    style: string
+  ): CharacterMetadata {
+    var withoutStyle = record.set('style', record.getStyle().remove(style));
+    return CharacterMetadata.create(withoutStyle);
+  }
+
+  static applyEntity(
+    record: CharacterMetadata,
+    entityKey: ?string
+  ): CharacterMetadata {
+    var withEntity = record.getEntity() === entityKey ?
+      record :
+      record.set('entity', entityKey);
+    return CharacterMetadata.create(withEntity);
+  }
+
+  /**
+   * Use this function instead of the `CharacterMetadata` constructor.
+   * Since most content generally uses only a very small number of
+   * style/entity permutations, we can reuse these objects as often as
+   * possible.
+   */
+  static create(config?: CharacterMetadataConfig): CharacterMetadata {
+    if (!config) {
+      return EMPTY;
+    }
+
+    // Fill in unspecified properties, if necessary.
+    var configMap =
+      Map({style: EMPTY_SET, entity: (null: ?string)}).merge(config);
+
+    var existing: ?CharacterMetadata = pool.get(configMap);
+    if (existing) {
+      return existing;
+    }
+
+    var newCharacter = new CharacterMetadata(configMap);
+    pool = pool.set(configMap, newCharacter);
+    return newCharacter;
+  }
+}
+
+var EMPTY = new CharacterMetadata();
+var pool: Map<Map, CharacterMetadata> = Map([[Map(defaultRecord), EMPTY]]);
+
+CharacterMetadata.EMPTY = EMPTY;
+
+module.exports = CharacterMetadata;
diff --git a/src/model/immutable/ContentBlock.js b/src/model/immutable/ContentBlock.js
new file mode 100644
index 0000000..1303597
--- /dev/null
+++ b/src/model/immutable/ContentBlock.js
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ContentBlock
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+var findRangesImmutable = require('findRangesImmutable');
+
+import type CharacterMetadata from 'CharacterMetadata';
+import type {DraftBlockType} from 'DraftBlockType';
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+var {
+  List,
+  OrderedSet,
+  Record,
+} = Immutable;
+
+const EMPTY_SET = OrderedSet();
+
+var defaultRecord: {
+  key: string;
+  type: DraftBlockType;
+  text: string;
+  characterList: List<CharacterMetadata>;
+  depth: number;
+} = {
+  key: '',
+  type: 'unstyled',
+  text: '',
+  characterList: List(),
+  depth: 0,
+};
+
+var ContentBlockRecord = Record(defaultRecord);
+
+class ContentBlock extends ContentBlockRecord {
+  getKey(): string {
+    return this.get('key');
+  }
+
+  getType(): DraftBlockType {
+    return this.get('type');
+  }
+
+  getText(): string {
+    return this.get('text');
+  }
+
+  getCharacterList(): List<CharacterMetadata> {
+    return this.get('characterList');
+  }
+
+  getLength(): number {
+    return this.getText().length;
+  }
+
+  getDepth(): number {
+    return this.get('depth');
+  }
+
+  getInlineStyleAt(offset: number): DraftInlineStyle {
+    var character = this.getCharacterList().get(offset);
+    return character ? character.getStyle() : EMPTY_SET;
+  }
+
+  getEntityAt(offset: number): ?string {
+    var character = this.getCharacterList().get(offset);
+    return character ? character.getEntity() : null;
+  }
+
+  /**
+   * Execute a callback for every contiguous range of styles within the block.
+   */
+  findStyleRanges(
+    filterFn: (value: CharacterMetadata) => boolean,
+    callback: (start: number, end: number) => void
+  ): void {
+    findRangesImmutable(
+      this.getCharacterList(),
+      haveEqualStyle,
+      filterFn,
+      callback
+    );
+  }
+
+  /**
+   * Execute a callback for every contiguous range of entities within the block.
+   */
+  findEntityRanges(
+    filterFn: (value: CharacterMetadata) => boolean,
+    callback: (start: number, end: number) => void
+  ): void {
+    findRangesImmutable(
+      this.getCharacterList(),
+      haveEqualEntity,
+      filterFn,
+      callback
+    );
+  }
+}
+
+function haveEqualStyle(
+  charA: CharacterMetadata,
+  charB: CharacterMetadata
+): boolean {
+  return charA.getStyle() === charB.getStyle();
+}
+
+function haveEqualEntity(
+  charA: CharacterMetadata,
+  charB: CharacterMetadata
+): boolean {
+  return charA.getEntity() === charB.getEntity();
+}
+
+module.exports = ContentBlock;
diff --git a/src/model/immutable/ContentState.js b/src/model/immutable/ContentState.js
new file mode 100644
index 0000000..860ad15
--- /dev/null
+++ b/src/model/immutable/ContentState.js
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+const BlockMapBuilder = require('BlockMapBuilder');
+const CharacterMetadata = require('CharacterMetadata');
+const ContentBlock = require('ContentBlock');
+const Immutable = require('immutable');
+const SelectionState = require('SelectionState');
+
+const generateBlockKey = require('generateBlockKey');
+const sanitizeDraftText = require('sanitizeDraftText');
+
+import type {BlockMap} from 'BlockMap';
+
+const {List, Record, Repeat} = Immutable;
+
+const defaultRecord: {
+  blockMap: ?BlockMap;
+  selectionBefore: ?SelectionState;
+  selectionAfter: ?SelectionState;
+} = {
+  blockMap: null,
+  selectionBefore: null,
+  selectionAfter: null,
+};
+
+const ContentStateRecord = Record(defaultRecord);
+
+class ContentState extends ContentStateRecord {
+  getBlockMap(): BlockMap {
+    return this.get('blockMap');
+  }
+
+  getSelectionBefore(): SelectionState {
+    return this.get('selectionBefore');
+  }
+
+  getSelectionAfter(): SelectionState {
+    return this.get('selectionAfter');
+  }
+
+  getBlockForKey(key: string): ContentBlock {
+    var block: ContentBlock = this.getBlockMap().get(key);
+    return block;
+  }
+
+  getKeyBefore(key: string): ?string {
+    return this.getBlockMap()
+      .reverse()
+      .keySeq()
+      .skipUntil(v => v === key)
+      .skip(1)
+      .first();
+  }
+
+  getKeyAfter(key: string): ?string {
+    return this.getBlockMap()
+      .keySeq()
+      .skipUntil(v => v === key)
+      .skip(1)
+      .first();
+  }
+
+  getBlockAfter(key: string): ?ContentBlock {
+    return this.getBlockMap()
+      .skipUntil((_, k) => k === key)
+      .skip(1)
+      .first();
+  }
+
+  getBlockBefore(key: string): ?ContentBlock {
+    return this.getBlockMap()
+      .reverse()
+      .skipUntil((_, k) => k === key)
+      .skip(1)
+      .first();
+  }
+
+  getBlocksAsArray(): Array<ContentBlock> {
+    return this.getBlockMap().toArray();
+  }
+
+  getLastBlock(): ContentBlock {
+    return this.getBlockMap().last();
+  }
+
+  getPlainText(delimiter?: string): string {
+    return this.getBlockMap()
+      .map(block => block ? block.getText() : '')
+      .join(delimiter || '\n');
+  }
+
+  hasText(): boolean {
+    var blockMap = this.getBlockMap();
+    return (
+      blockMap.size > 1 ||
+      blockMap.first().getLength() > 0
+    );
+  }
+
+  static createFromBlockArray(
+    blocks: Array<ContentBlock>
+  ): ContentState {
+    var blockMap = BlockMapBuilder.createFromArray(blocks);
+    var selectionState = SelectionState.createEmpty(blockMap.first().getKey());
+    return new ContentState({
+      blockMap,
+      selectionBefore: selectionState,
+      selectionAfter: selectionState,
+    });
+  }
+
+  static createFromText(
+    text: string,
+    delimiter: string | RegExp = /\r\n?|\n/g,
+  ): ContentState {
+    const strings = text.split(delimiter);
+    const blocks = strings.map(
+      block => {
+        block = sanitizeDraftText(block);
+        return new ContentBlock({
+          key: generateBlockKey(),
+          text: block,
+          type: 'unstyled',
+          characterList: List(Repeat(CharacterMetadata.EMPTY, block.length)),
+        });
+      }
+    );
+    return ContentState.createFromBlockArray(blocks);
+  }
+}
+
+module.exports = ContentState;
diff --git a/src/model/immutable/DefaultDraftInlineStyle.js b/src/model/immutable/DefaultDraftInlineStyle.js
new file mode 100644
index 0000000..d4ba324
--- /dev/null
+++ b/src/model/immutable/DefaultDraftInlineStyle.js
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DefaultDraftInlineStyle
+ * @flow
+ */
+
+'use strict';
+
+module.exports = {
+  BOLD: {
+    fontWeight: 'bold',
+  },
+
+  CODE: {
+    fontFamily: 'monospace',
+    wordWrap: 'break-word',
+  },
+
+  ITALIC: {
+    fontStyle: 'italic',
+  },
+
+  STRIKETHROUGH: {
+    textDecoration: 'line-through',
+  },
+
+  UNDERLINE: {
+    textDecoration: 'underline',
+  },
+};
diff --git a/src/model/immutable/DraftInlineStyle.js b/src/model/immutable/DraftInlineStyle.js
new file mode 100644
index 0000000..7a630a7
--- /dev/null
+++ b/src/model/immutable/DraftInlineStyle.js
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftInlineStyle
+ * @flow
+ */
+
+'use strict';
+
+import type Immutable from 'immutable';
+
+export type DraftInlineStyle = Immutable.OrderedSet<string>;
diff --git a/src/model/immutable/EditorBidiService.js b/src/model/immutable/EditorBidiService.js
new file mode 100644
index 0000000..f7ae351
--- /dev/null
+++ b/src/model/immutable/EditorBidiService.js
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EditorBidiService
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+var UnicodeBidiService = require('UnicodeBidiService');
+
+var nullthrows = require('nullthrows');
+
+import type ContentState from 'ContentState';
+
+var {OrderedMap} = Immutable;
+
+var bidiService;
+
+var EditorBidiService = {
+  getDirectionMap: function(
+    content: ContentState,
+    prevBidiMap: ?OrderedMap
+  ): OrderedMap {
+    if (!bidiService) {
+      bidiService = new UnicodeBidiService();
+    } else {
+      bidiService.reset();
+    }
+
+    var blockMap = content.getBlockMap();
+    var nextBidi = blockMap
+      .valueSeq()
+      .map(block => nullthrows(bidiService).getDirection(block.getText()));
+    var bidiMap = OrderedMap(blockMap.keySeq().zip(nextBidi));
+
+    if (prevBidiMap != null && Immutable.is(prevBidiMap, bidiMap)) {
+      return prevBidiMap;
+    }
+
+    return bidiMap;
+  },
+};
+
+module.exports = EditorBidiService;
diff --git a/src/model/immutable/EditorChangeType.js b/src/model/immutable/EditorChangeType.js
new file mode 100644
index 0000000..155ecde
--- /dev/null
+++ b/src/model/immutable/EditorChangeType.js
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EditorChangeType
+ * @flow
+ */
+
+/*eslint-disable no-bitwise*/
+
+'use strict';
+
+export type EditorChangeType = (
+  'undo' |
+  'redo' |
+  'change-selection' |
+  'insert-characters' |
+  'backspace-character' |
+  'delete-character' |
+  'remove-range' |
+  'split-block' |
+  'insert-fragment' |
+  'change-inline-style' |
+  'change-block-type' |
+  'apply-entity' |
+  'reset-block' |
+  'adjust-depth' |
+  'spellcheck-change'
+);
diff --git a/src/model/immutable/EditorState.js b/src/model/immutable/EditorState.js
new file mode 100644
index 0000000..69e260b
--- /dev/null
+++ b/src/model/immutable/EditorState.js
@@ -0,0 +1,638 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EditorState
+ * @flow
+ */
+
+'use strict';
+
+var BlockTree = require('BlockTree');
+var ContentState = require('ContentState');
+var EditorBidiService = require('EditorBidiService');
+var Immutable = require('immutable');
+var SelectionState = require('SelectionState');
+
+import type {BlockMap} from 'BlockMap';
+import type {DraftDecoratorType} from 'DraftDecoratorType';
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+import type {List, OrderedMap} from 'immutable';
+import type {EditorChangeType} from 'EditorChangeType';
+
+var {
+  OrderedSet,
+  Record,
+  Stack,
+} = Immutable;
+
+type EditorStateRecordType = {
+  allowUndo: boolean;
+  currentContent: ?ContentState;
+  decorator: ?DraftDecoratorType;
+  directionMap: ?OrderedMap<string, string>;
+  forceSelection: boolean;
+  inCompositionMode: boolean;
+  inlineStyleOverride: ?DraftInlineStyle;
+  lastChangeType: ?EditorChangeType;
+  nativelyRenderedContent: ?ContentState;
+  redoStack: Stack<ContentState>;
+  selection: ?SelectionState;
+  treeMap: ?OrderedMap<string, List>;
+  undoStack: Stack<ContentState>;
+};
+
+var defaultRecord: EditorStateRecordType = {
+  allowUndo: true,
+  currentContent: null,
+  decorator: null,
+  directionMap: null,
+  forceSelection: false,
+  inCompositionMode: false,
+  inlineStyleOverride: null,
+  lastChangeType: null,
+  nativelyRenderedContent: null,
+  redoStack: Stack(),
+  selection: null,
+  treeMap: null,
+  undoStack: Stack(),
+};
+
+var EditorStateRecord = Record(defaultRecord);
+
+class EditorState {
+  _immutable: EditorStateRecord;
+
+  static createEmpty(
+    decorator?: DraftDecoratorType
+  ): EditorState {
+    return EditorState.createWithContent(
+      ContentState.createFromText(''),
+      decorator
+    );
+  }
+
+  static createWithContent(
+    contentState: ContentState,
+    decorator?: DraftDecoratorType
+  ): EditorState {
+    var firstKey = contentState.getBlockMap().first().getKey();
+    return EditorState.create({
+      currentContent: contentState,
+      undoStack: Stack(),
+      redoStack: Stack(),
+      decorator: decorator || null,
+      selection: SelectionState.createEmpty(firstKey),
+    });
+  }
+
+  static create(config: Object): EditorState {
+    var {currentContent, decorator} = config;
+    var recordConfig = {
+      ...config,
+      treeMap: generateNewTreeMap(currentContent, decorator),
+      directionMap: EditorBidiService.getDirectionMap(currentContent),
+    };
+    return new EditorState(
+      new EditorStateRecord(recordConfig)
+    );
+  }
+
+  static set(editorState: EditorState, put: Object): EditorState {
+    var map = editorState.getImmutable().withMutations(state => {
+      var existingDecorator = state.get('decorator');
+      var decorator = existingDecorator;
+      if (put.decorator === null) {
+        decorator = null;
+      } else if (put.decorator) {
+        decorator = put.decorator;
+      }
+
+      var newContent = put.currentContent || editorState.getCurrentContent();
+
+      if (decorator !== existingDecorator) {
+        var treeMap: OrderedMap = state.get('treeMap');
+        var newTreeMap;
+        if (decorator && existingDecorator) {
+          newTreeMap = regenerateTreeForNewDecorator(
+            newContent.getBlockMap(),
+            treeMap,
+            decorator,
+            existingDecorator
+          );
+        } else {
+          newTreeMap = generateNewTreeMap(newContent, decorator);
+        }
+
+        state.merge({
+          decorator,
+          treeMap: newTreeMap,
+          nativelyRenderedContent: null,
+        });
+        return;
+      }
+
+      var existingContent = editorState.getCurrentContent();
+      if (newContent !== existingContent) {
+        state.set(
+          'treeMap',
+          regenerateTreeForNewBlocks(
+            editorState,
+            newContent.getBlockMap(),
+            decorator
+          )
+        );
+      }
+
+      state.merge(put);
+    });
+
+    return new EditorState(map);
+  }
+
+  toJS(): Object {
+    return this.getImmutable().toJS();
+  }
+
+  getAllowUndo(): boolean {
+    return this.getImmutable().get('allowUndo');
+  }
+
+  getCurrentContent(): ContentState {
+    return this.getImmutable().get('currentContent');
+  }
+
+  getUndoStack(): Stack<ContentState> {
+    return this.getImmutable().get('undoStack');
+  }
+
+  getRedoStack(): Stack<ContentState> {
+    return this.getImmutable().get('redoStack');
+  }
+
+  getSelection(): SelectionState {
+    return this.getImmutable().get('selection');
+  }
+
+  getDecorator(): ?DraftDecoratorType {
+    return this.getImmutable().get('decorator');
+  }
+
+  isInCompositionMode(): boolean {
+    return this.getImmutable().get('inCompositionMode');
+  }
+
+  mustForceSelection(): boolean {
+    return this.getImmutable().get('forceSelection');
+  }
+
+  getNativelyRenderedContent(): ?ContentState {
+    return this.getImmutable().get('nativelyRenderedContent');
+  }
+
+  getLastChangeType(): ?EditorChangeType {
+    return this.getImmutable().get('lastChangeType');
+  }
+
+  /**
+   * While editing, the user may apply inline style commands with a collapsed
+   * cursor, intending to type text that adopts the specified style. In this
+   * case, we track the specified style as an "override" that takes precedence
+   * over the inline style of the text adjacent to the cursor.
+   *
+   * If null, there is no override in place.
+   */
+  getInlineStyleOverride(): ?DraftInlineStyle {
+    return this.getImmutable().get('inlineStyleOverride');
+  }
+
+  /**
+   * Get the appropriate inline style for the editor state. If an
+   * override is in place, use it. Otherwise, the current style is
+   * based on the location of the selection state.
+   */
+  getCurrentInlineStyle(): DraftInlineStyle {
+    var override = this.getInlineStyleOverride();
+    if (override != null) {
+      return override;
+    }
+
+    var content = this.getCurrentContent();
+    var selection = this.getSelection();
+
+    if (selection.isCollapsed()) {
+      return getInlineStyleForCollapsedSelection(content, selection);
+    }
+
+    return getInlineStyleForNonCollapsedSelection(content, selection);
+  }
+
+  getBlockTree(blockKey: string): List {
+    return this.getImmutable().getIn(['treeMap', blockKey]);
+  }
+
+  isSelectionAtStartOfContent(): boolean {
+    var firstKey = this.getCurrentContent().getBlockMap().first().getKey();
+    return this.getSelection().hasEdgeWithin(firstKey, 0, 0);
+  }
+
+  isSelectionAtEndOfContent(): boolean {
+    var content = this.getCurrentContent();
+    var blockMap = content.getBlockMap();
+    var last = blockMap.last();
+    var end = last.getLength();
+    return this.getSelection().hasEdgeWithin(last.getKey(), end, end);
+  }
+
+  getDirectionMap(): ?OrderedMap {
+    return this.getImmutable().get('directionMap');
+  }
+
+  /**
+   * Incorporate native DOM selection changes into the EditorState. This
+   * method can be used when we simply want to accept whatever the DOM
+   * has given us to represent selection, and we do not need to re-render
+   * the editor.
+   *
+   * To forcibly move the DOM selection, see `EditorState.forceSelection`.
+   */
+  static acceptSelection(
+    editorState: EditorState,
+    selection: SelectionState
+  ): EditorState {
+    return updateSelection(editorState, selection, false);
+  }
+
+  /**
+   * At times, we need to force the DOM selection to be where we
+   * need it to be. This can occur when the anchor or focus nodes
+   * are non-text nodes, for instance. In this case, we want to trigger
+   * a re-render of the editor, which in turn forces selection into
+   * the correct place in the DOM. The `forceSelection` method
+   * accomplishes this.
+   *
+   * This method should be used in cases where you need to explicitly
+   * move the DOM selection from one place to another without a change
+   * in ContentState.
+   */
+  static forceSelection(
+    editorState: EditorState,
+    selection: SelectionState
+  ): EditorState {
+    if (!selection.getHasFocus()) {
+      selection = selection.set('hasFocus', true);
+    }
+    return updateSelection(editorState, selection, true);
+  }
+
+  /**
+   * Move selection to the end of the editor without forcing focus.
+   */
+  static moveSelectionToEnd(editorState: EditorState): EditorState {
+    var content = editorState.getCurrentContent();
+    var lastBlock = content.getLastBlock();
+    var lastKey = lastBlock.getKey();
+    var length = lastBlock.getLength();
+
+    return EditorState.acceptSelection(
+      editorState,
+      new SelectionState({
+        anchorKey: lastKey,
+        anchorOffset: length,
+        focusKey: lastKey,
+        focusOffset: length,
+        isBackward: false,
+      })
+    );
+  }
+
+  /**
+   * Force focus to the end of the editor. This is useful in scenarios
+   * where we want to programatically focus the input and it makes sense
+   * to allow the user to continue working seamlessly.
+   */
+  static moveFocusToEnd(editorState: EditorState): EditorState {
+    var afterSelectionMove = EditorState.moveSelectionToEnd(editorState);
+    return EditorState.forceSelection(
+      afterSelectionMove,
+      afterSelectionMove.getSelection()
+    );
+  }
+
+  /**
+   * Push the current ContentState onto the undo stack if it should be
+   * considered a boundary state, and set the provided ContentState as the
+   * new current content.
+   */
+  static push(
+    editorState: EditorState,
+    contentState: ContentState,
+    changeType: EditorChangeType
+  ): EditorState {
+    if (editorState.getCurrentContent() === contentState) {
+      return editorState;
+    }
+
+    var forceSelection = changeType !== 'insert-characters';
+    var directionMap = EditorBidiService.getDirectionMap(
+      contentState,
+      editorState.getDirectionMap()
+    );
+
+    if (!editorState.getAllowUndo()) {
+      return EditorState.set(editorState, {
+        currentContent: contentState,
+        directionMap,
+        lastChangeType: changeType,
+        selection: contentState.getSelectionAfter(),
+        forceSelection,
+        inlineStyleOverride: null,
+      });
+    }
+
+    var selection = editorState.getSelection();
+    var currentContent = editorState.getCurrentContent();
+    var undoStack = editorState.getUndoStack();
+    var newContent = contentState;
+
+    if (
+      selection !== currentContent.getSelectionAfter() ||
+      mustBecomeBoundary(editorState, changeType)
+    ) {
+      undoStack = undoStack.push(currentContent);
+      newContent = newContent.set('selectionBefore', selection);
+    } else if (
+      changeType === 'insert-characters' ||
+      changeType === 'backspace-character' ||
+      changeType === 'delete-character'
+    ) {
+      // Preserve the previous selection.
+      newContent = newContent.set(
+        'selectionBefore',
+        currentContent.getSelectionBefore()
+      );
+    }
+
+    return EditorState.set(editorState, {
+      currentContent: newContent,
+      directionMap,
+      undoStack,
+      redoStack: Stack(),
+      lastChangeType: changeType,
+      selection: contentState.getSelectionAfter(),
+      forceSelection,
+      inlineStyleOverride: null,
+    });
+  }
+
+  /**
+   * Make the top ContentState in the undo stack the new current content and
+   * push the current content onto the redo stack.
+   */
+  static undo(editorState: EditorState): EditorState {
+    if (!editorState.getAllowUndo()) {
+      return editorState;
+    }
+
+    var undoStack = editorState.getUndoStack();
+    var newCurrentContent = undoStack.peek();
+    if (!newCurrentContent) {
+      return editorState;
+    }
+
+    var currentContent = editorState.getCurrentContent();
+    var directionMap = EditorBidiService.getDirectionMap(
+      newCurrentContent,
+      editorState.getDirectionMap()
+    );
+
+    return EditorState.set(editorState, {
+      currentContent: newCurrentContent,
+      directionMap,
+      undoStack: undoStack.shift(),
+      redoStack: editorState.getRedoStack().push(currentContent),
+      forceSelection: true,
+      inlineStyleOverride: null,
+      lastChangeType: 'undo',
+      nativelyRenderedContent: null,
+      selection: currentContent.getSelectionBefore(),
+    });
+  }
+
+  /**
+   * Make the top ContentState in the redo stack the new current content and
+   * push the current content onto the undo stack.
+   */
+  static redo(editorState: EditorState): EditorState {
+    if (!editorState.getAllowUndo()) {
+      return editorState;
+    }
+
+    var redoStack = editorState.getRedoStack();
+    var newCurrentContent = redoStack.peek();
+    if (!newCurrentContent) {
+      return editorState;
+    }
+
+    var currentContent = editorState.getCurrentContent();
+    var directionMap = EditorBidiService.getDirectionMap(
+      newCurrentContent,
+      editorState.getDirectionMap()
+    );
+
+    return EditorState.set(editorState, {
+      currentContent: newCurrentContent,
+      directionMap,
+      undoStack: editorState.getUndoStack().push(currentContent),
+      redoStack: redoStack.shift(),
+      forceSelection: true,
+      inlineStyleOverride: null,
+      lastChangeType: 'redo',
+      nativelyRenderedContent: null,
+      selection: newCurrentContent.getSelectionAfter(),
+    });
+  }
+
+  /**
+   * Not for public consumption.
+   */
+  constructor(immutable: EditorStateRecord) {
+    this._immutable = immutable;
+  }
+
+  /**
+   * Not for public consumption.
+   */
+  getImmutable(): EditorStateRecord {
+    return this._immutable;
+  }
+}
+
+/**
+ * Set the supplied SelectionState as the new current selection, and set
+ * the `force` flag to trigger manual selection placement by the view.
+ */
+function updateSelection(
+  editorState: EditorState,
+  selection: SelectionState,
+  forceSelection: boolean
+): EditorState {
+  return EditorState.set(editorState, {
+    selection,
+    forceSelection,
+    nativelyRenderedContent: null,
+    inlineStyleOverride: null,
+  });
+}
+
+/**
+ * Regenerate the entire tree map for a given ContentState and decorator.
+ * Returns an OrderedMap that maps all available ContentBlock objects.
+ */
+function generateNewTreeMap(
+  contentState: ContentState,
+  decorator: ?DraftDecoratorType
+): OrderedMap<string, List> {
+  return contentState
+    .getBlockMap()
+    .map(block => BlockTree.generate(block, decorator))
+    .toOrderedMap();
+}
+
+/**
+ * Regenerate tree map objects for all ContentBlocks that have changed
+ * between the current editorState and newContent. Returns an OrderedMap
+ * with only changed regenerated tree map objects.
+ */
+function regenerateTreeForNewBlocks(
+  editorState: EditorState,
+  newBlockMap: BlockMap,
+  decorator: ?DraftDecoratorType
+): OrderedMap<string, List> {
+  var prevBlockMap = editorState.getCurrentContent().getBlockMap();
+  var prevTreeMap = editorState.getImmutable().get('treeMap');
+  return prevTreeMap.merge(
+    newBlockMap
+      .toSeq()
+      .filter((block, key) => block !== prevBlockMap.get(key))
+      .map(block => BlockTree.generate(block, decorator))
+  );
+}
+
+/**
+ * Generate tree map objects for a new decorator object, preserving any
+ * decorations that are unchanged from the previous decorator.
+ *
+ * Note that in order for this to perform optimally, decoration Lists for
+ * decorators should be preserved when possible to allow for direct immutable
+ * List comparison.
+ */
+function regenerateTreeForNewDecorator(
+  blockMap: BlockMap,
+  previousTreeMap: OrderedMap<string, List>,
+  decorator: DraftDecoratorType,
+  existingDecorator: DraftDecoratorType
+): OrderedMap<string, List> {
+  return previousTreeMap.merge(
+    blockMap
+      .toSeq()
+      .filter(block => {
+        return (
+          decorator.getDecorations(block) !==
+          existingDecorator.getDecorations(block)
+        );
+      })
+      .map(block => BlockTree.generate(block, decorator))
+  );
+}
+
+/**
+ * Return whether a change should be considered a boundary state, given
+ * the previous change type. Allows us to discard potential boundary states
+ * during standard typing or deletion behavior.
+ */
+function mustBecomeBoundary(
+  editorState: EditorState,
+  changeType: EditorChangeType
+): boolean {
+  var lastChangeType = editorState.getLastChangeType();
+  return (
+    changeType !== lastChangeType ||
+    (
+      changeType !== 'insert-characters' &&
+      changeType !== 'backspace-character' &&
+      changeType !== 'delete-character'
+    )
+  );
+}
+
+function getInlineStyleForCollapsedSelection(
+  content: ContentState,
+  selection: SelectionState
+): DraftInlineStyle {
+  var startKey = selection.getStartKey();
+  var startOffset = selection.getStartOffset();
+  var startBlock = content.getBlockForKey(startKey);
+
+  // If the cursor is not at the start of the block, look backward to
+  // preserve the style of the preceding character.
+  if (startOffset > 0) {
+    return startBlock.getInlineStyleAt(startOffset - 1);
+  }
+
+  // The caret is at position zero in this block. If the block has any
+  // text at all, use the style of the first character.
+  if (startBlock.getLength()) {
+    return startBlock.getInlineStyleAt(0);
+  }
+
+  // Otherwise, look upward in the document to find the closest character.
+  return lookUpwardForInlineStyle(content, startKey);
+}
+
+function getInlineStyleForNonCollapsedSelection(
+  content: ContentState,
+  selection: SelectionState
+): DraftInlineStyle {
+  var startKey = selection.getStartKey();
+  var startOffset = selection.getStartOffset();
+  var startBlock = content.getBlockForKey(startKey);
+
+  // If there is a character just inside the selection, use its style.
+  if (startOffset < startBlock.getLength()) {
+    return startBlock.getInlineStyleAt(startOffset);
+  }
+
+  // Check if the selection at the end of a non-empty block. Use the last
+  // style in the block.
+  if (startOffset > 0) {
+    return startBlock.getInlineStyleAt(startOffset - 1);
+  }
+
+  // Otherwise, look upward in the document to find the closest character.
+  return lookUpwardForInlineStyle(content, startKey);
+}
+
+function lookUpwardForInlineStyle(
+  content: ContentState,
+  fromKey: string
+): DraftInlineStyle {
+  var previousBlock = content.getBlockBefore(fromKey);
+  var previousLength;
+
+  while (previousBlock) {
+    previousLength = previousBlock.getLength();
+    if (previousLength) {
+      return previousBlock.getInlineStyleAt(previousLength - 1);
+    }
+    previousBlock = content.getBlockBefore(previousBlock.getKey());
+  }
+
+  return OrderedSet();
+}
+
+module.exports = EditorState;
diff --git a/src/model/immutable/EditorStateCreationConfig.js b/src/model/immutable/EditorStateCreationConfig.js
new file mode 100644
index 0000000..52c6a10
--- /dev/null
+++ b/src/model/immutable/EditorStateCreationConfig.js
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule EditorStateCreationConfig
+ * @flow
+ */
+
+'use strict';
+
+import type ContentState from 'ContentState';
+import type {DraftDecoratorType} from 'DraftDecoratorType';
+import type SelectionState from 'SelectionState';
+
+export type EditorStateCreationConfig = {
+  allowUndo: boolean;
+  currentContent: ContentState;
+  decorator: ?DraftDecoratorType;
+  selection: SelectionState;
+};
diff --git a/src/model/immutable/SampleDraftInlineStyle.js b/src/model/immutable/SampleDraftInlineStyle.js
new file mode 100644
index 0000000..e9f1278
--- /dev/null
+++ b/src/model/immutable/SampleDraftInlineStyle.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SampleDraftInlineStyle
+ * @flow
+ */
+
+'use strict';
+
+const {OrderedSet} = require('immutable');
+
+module.exports = {
+  BOLD: OrderedSet.of('BOLD'),
+  BOLD_ITALIC: OrderedSet.of('BOLD', 'ITALIC'),
+  BOLD_ITALIC_UNDERLINE: OrderedSet.of('BOLD', 'ITALIC', 'UNDERLINE'),
+  BOLD_UNDERLINE: OrderedSet.of('BOLD', 'UNDERLINE'),
+  CODE: OrderedSet.of('CODE'),
+  ITALIC: OrderedSet.of('ITALIC'),
+  ITALIC_UNDERLINE: OrderedSet.of('ITALIC', 'UNDERLINE'),
+  NONE: OrderedSet(),
+  STRIKETHROUGH: OrderedSet.of('STRIKETHROUGH'),
+  UNDERLINE: OrderedSet.of('UNDERLINE'),
+};
diff --git a/src/model/immutable/SelectionState.js b/src/model/immutable/SelectionState.js
new file mode 100644
index 0000000..eeaef2e
--- /dev/null
+++ b/src/model/immutable/SelectionState.js
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule SelectionState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+var {Record} = Immutable;
+
+var defaultRecord: {
+  anchorKey: string;
+  anchorOffset: number;
+  focusKey: string;
+  focusOffset: number;
+  isBackward: boolean;
+  hasFocus: boolean;
+} = {
+  anchorKey: '',
+  anchorOffset: 0,
+  focusKey: '',
+  focusOffset: 0,
+  isBackward: false,
+  hasFocus: false,
+};
+
+var SelectionStateRecord = Record(defaultRecord);
+
+class SelectionState extends SelectionStateRecord {
+  serialize(): string {
+    return (
+      'Anchor: ' + this.getAnchorKey() + ':' + this.getAnchorOffset() + ', ' +
+      'Focus: ' + this.getFocusKey() + ':' + this.getFocusOffset() + ', ' +
+      'Is Backward: ' + String(this.getIsBackward()) + ', ' +
+      'Has Focus: ' + String(this.getHasFocus())
+    );
+  }
+
+  getAnchorKey(): string {
+    return this.get('anchorKey');
+  }
+
+  getAnchorOffset(): number {
+    return this.get('anchorOffset');
+  }
+
+  getFocusKey(): string {
+    return this.get('focusKey');
+  }
+
+  getFocusOffset(): number {
+    return this.get('focusOffset');
+  }
+
+  getIsBackward(): boolean {
+    return this.get('isBackward');
+  }
+
+  getHasFocus(): boolean {
+    return this.get('hasFocus');
+  }
+
+  /**
+   * Return whether the specified range overlaps with an edge of the
+   * SelectionState.
+   */
+  hasEdgeWithin(
+    blockKey: string,
+    start: number,
+    end: number
+  ): boolean {
+    var anchorKey = this.getAnchorKey();
+    var focusKey = this.getFocusKey();
+
+    if (anchorKey === focusKey && anchorKey === blockKey) {
+      var selectionStart = this.getStartOffset();
+      var selectionEnd = this.getEndOffset();
+      return start <= selectionEnd && selectionStart <= end;
+    }
+
+    if (blockKey !== anchorKey && blockKey !== focusKey) {
+      return false;
+    }
+
+    var offsetToCheck = blockKey === anchorKey ?
+      this.getAnchorOffset() :
+      this.getFocusOffset();
+
+    return start <= offsetToCheck && end >= offsetToCheck;
+  }
+
+  isCollapsed(): boolean {
+    return (
+      this.getAnchorKey() === this.getFocusKey() &&
+      this.getAnchorOffset() === this.getFocusOffset()
+    );
+  }
+
+  getStartKey(): string {
+    return this.getIsBackward() ?
+      this.getFocusKey() :
+      this.getAnchorKey();
+  }
+
+  getStartOffset(): number {
+    return this.getIsBackward() ?
+      this.getFocusOffset() :
+      this.getAnchorOffset();
+  }
+
+  getEndKey(): string {
+    return this.getIsBackward() ?
+      this.getAnchorKey() :
+      this.getFocusKey();
+  }
+
+  getEndOffset(): number {
+    return this.getIsBackward() ?
+      this.getAnchorOffset() :
+      this.getFocusOffset();
+  }
+
+  static createEmpty(
+    key: string
+  ): SelectionState {
+    return new SelectionState({
+      anchorKey: key,
+      anchorOffset: 0,
+      focusKey: key,
+      focusOffset: 0,
+      isBackward: false,
+      hasFocus: false,
+    });
+  }
+}
+
+module.exports = SelectionState;
diff --git a/src/model/immutable/__tests__/BlockTree-test.js b/src/model/immutable/__tests__/BlockTree-test.js
new file mode 100644
index 0000000..66af6ba
--- /dev/null
+++ b/src/model/immutable/__tests__/BlockTree-test.js
@@ -0,0 +1,305 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var BlockTree = require('BlockTree');
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+var {BOLD} = require('SampleDraftInlineStyle');
+
+var {EMPTY} = CharacterMetadata;
+
+var {
+  Repeat,
+} = Immutable;
+
+var PLAIN_BLOCK = {
+  key: 'a',
+  text: 'Lincoln',
+  characterList: Repeat(EMPTY, 7).toList(),
+};
+
+var boldChar = CharacterMetadata.create({style: BOLD});
+var styledChars = Repeat(EMPTY, 4).concat(
+  Repeat(boldChar, 4),
+  Repeat(EMPTY, 2)
+);
+
+var STYLED_BLOCK = {
+  key: 'b',
+  text: 'Washington',
+  characterList: styledChars,
+};
+
+function assertLeafSetValues(leafSet, values) {
+  var {start, end, decoratorKey} = values;
+  expect(leafSet.get('start')).toBe(start);
+  expect(leafSet.get('end')).toBe(end);
+  expect(leafSet.get('decoratorKey')).toBe(decoratorKey);
+}
+
+function assertLeafValues(leaf, values) {
+  expect(leaf.get('start')).toBe(values.start);
+  expect(leaf.get('end')).toBe(values.end);
+}
+
+describe('BlockTree', () => {
+  class Decorator {}
+  Decorator.prototype.getDecorations = jest.genMockFn();
+
+  beforeEach(() => {
+    jest.resetModuleRegistry();
+  });
+
+  describe('generate tree with zero decorations', () => {
+    function getDecorator(length) {
+      Decorator.prototype.getDecorations.mockImplementation(
+        () => Repeat(null, length).toList()
+      );
+      return new Decorator();
+    }
+
+    it('must generate for unstyled block', () => {
+      var block = new ContentBlock(PLAIN_BLOCK);
+      var length = PLAIN_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // No decorations, so only one leaf set.
+      expect(tree.size).toBe(1);
+      var leafSet = tree.get(0);
+      assertLeafSetValues(leafSet, {
+        start: 0,
+        end: length,
+        decoratorKey: null,
+      });
+
+      var leaves = leafSet.get('leaves');
+      expect(leaves.size).toBe(1);
+
+      var leaf = leaves.get(0);
+      assertLeafValues(leaf, {start: 0, end: length});
+
+      expect(BlockTree.getFingerprint(tree)).toBe('.1');
+    });
+
+    it('must generate for styled block', () => {
+      var block = new ContentBlock(STYLED_BLOCK);
+      var length = STYLED_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // No decorations, so only one leaf set.
+      expect(tree.size).toBe(1);
+      var leafSet = tree.get(0);
+      assertLeafSetValues(leafSet, {
+        start: 0,
+        end: length,
+        decoratorKey: null,
+      });
+
+      // Three styled sections, so three leaves in this leaf set.
+      var leaves = leafSet.get('leaves');
+      expect(leaves.size).toBe(3);
+
+      assertLeafValues(leaves.get(0), {start: 0, end: 4});
+      assertLeafValues(leaves.get(1), {start: 4, end: 8});
+      assertLeafValues(leaves.get(2), {start: 8, end: 10});
+
+      expect(BlockTree.getFingerprint(tree)).toBe('.3');
+    });
+  });
+
+  describe('generate tree with single decoration', () => {
+    var DECORATOR_KEY = 'x';
+    var RANGE_LENGTH = 3;
+
+    function getDecorator(length) {
+      Decorator.prototype.getDecorations.mockImplementation(
+        () => {
+          return Repeat(null, RANGE_LENGTH).concat(
+            Repeat(DECORATOR_KEY, RANGE_LENGTH),
+            Repeat(null, (length - (2 * RANGE_LENGTH)))
+          ).toList();
+        }
+      );
+      return new Decorator();
+    }
+
+    it('must generate for unstyled block', () => {
+      var block = new ContentBlock(PLAIN_BLOCK);
+      var length = PLAIN_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // One leaf set for each decoration range.
+      expect(tree.size).toBe(3);
+      assertLeafSetValues(tree.get(0), {
+        start: 0,
+        end: RANGE_LENGTH,
+        decoratorKey: null,
+      });
+      assertLeafSetValues(tree.get(1), {
+        start: RANGE_LENGTH,
+        end: RANGE_LENGTH * 2,
+        decoratorKey: DECORATOR_KEY,
+      });
+      assertLeafSetValues(tree.get(2), {
+        start: RANGE_LENGTH * 2,
+        end: length,
+        decoratorKey: null,
+      });
+
+      expect(tree.get(0).get('leaves').size).toBe(1);
+      expect(tree.get(1).get('leaves').size).toBe(1);
+      expect(tree.get(2).get('leaves').size).toBe(1);
+
+      expect(BlockTree.getFingerprint(tree)).toBe('.1-x.3.1-.1');
+    });
+
+    it('must generate for styled block', () => {
+      var block = new ContentBlock(STYLED_BLOCK);
+      var length = STYLED_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // Leaf Sets: ['Was', 'hin', 'gton']
+      // Set 0 leaves (null entity): ['Was'] with NONE
+      // Set 1 leaves ('x' entity): ['h', 'in'] with NONE, BOLD
+      // Set 2 leaves (null entity): ['gt', 'on'] with BOLD, NONE
+
+      // One leaf set for each decoration range.
+      expect(tree.size).toBe(3);
+      assertLeafSetValues(tree.get(0), {
+        start: 0,
+        end: RANGE_LENGTH,
+        decoratorKey: null,
+      });
+      assertLeafSetValues(tree.get(1), {
+        start: RANGE_LENGTH,
+        end: RANGE_LENGTH * 2,
+        decoratorKey: DECORATOR_KEY,
+      });
+      assertLeafSetValues(tree.get(2), {
+        start: RANGE_LENGTH * 2,
+        end: length,
+        decoratorKey: null,
+      });
+
+      expect(tree.get(0).get('leaves').size).toBe(1);
+      expect(tree.get(1).get('leaves').size).toBe(2);
+      expect(tree.get(2).get('leaves').size).toBe(2);
+
+      expect(BlockTree.getFingerprint(tree)).toBe('.1-x.3.2-.2');
+    });
+  });
+
+  describe('generate tree with multiple decorations', () => {
+    var DECORATOR_KEY_A = 'y';
+    var DECORATOR_KEY_B = 'z';
+    var RANGE_LENGTH = 3;
+
+    function getDecorator(length) {
+      Decorator.prototype.getDecorations.mockImplementation(
+        () => {
+          return Repeat(DECORATOR_KEY_A, RANGE_LENGTH).concat(
+            Repeat(null, RANGE_LENGTH),
+            Repeat(DECORATOR_KEY_B, (length - (2 * RANGE_LENGTH)))
+          ).toList();
+        }
+      );
+      return new Decorator();
+    }
+
+    it('must generate for unstyled block', () => {
+      var block = new ContentBlock(PLAIN_BLOCK);
+      var length = PLAIN_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // One leaf set for each decoration range.
+      expect(tree.size).toBe(3);
+      assertLeafSetValues(tree.get(0), {
+        start: 0,
+        end: RANGE_LENGTH,
+        decoratorKey: DECORATOR_KEY_A,
+      });
+      assertLeafSetValues(tree.get(1), {
+        start: RANGE_LENGTH,
+        end: RANGE_LENGTH * 2,
+        decoratorKey: null,
+      });
+      assertLeafSetValues(tree.get(2), {
+        start: RANGE_LENGTH * 2,
+        end: length,
+        decoratorKey: DECORATOR_KEY_B,
+      });
+
+      expect(tree.get(0).get('leaves').size).toBe(1);
+      expect(tree.get(1).get('leaves').size).toBe(1);
+      expect(tree.get(2).get('leaves').size).toBe(1);
+
+      expect(BlockTree.getFingerprint(tree)).toBe('y.3.1-.1-z.1.1');
+    });
+
+    it('must generate for styled block', () => {
+      var block = new ContentBlock(STYLED_BLOCK);
+      var length = STYLED_BLOCK.text.length;
+      var decorator = getDecorator(length);
+      var tree = BlockTree.generate(block, decorator);
+
+      // Leaf Sets: ['Was', 'hin', 'gton']
+      // Set 0 leaves ('y' entity): ['Was'] with NONE
+      // Set 1 leaves (null entity): ['h', 'in'] with NONE, BOLD
+      // Set 2 leaves ('z' entity): ['gt', 'on'] with BOLD, NONE
+
+      // One leaf set for each decoration range.
+      expect(tree.size).toBe(3);
+      assertLeafSetValues(tree.get(0), {
+        start: 0,
+        end: RANGE_LENGTH,
+        decoratorKey: DECORATOR_KEY_A,
+      });
+      assertLeafSetValues(tree.get(1), {
+        start: RANGE_LENGTH,
+        end: RANGE_LENGTH * 2,
+        decoratorKey: null,
+      });
+      assertLeafSetValues(tree.get(2), {
+        start: RANGE_LENGTH * 2,
+        end: length,
+        decoratorKey: DECORATOR_KEY_B,
+      });
+
+      expect(tree.get(0).get('leaves').size).toBe(1);
+      expect(tree.get(0).get('leaves').get(0).toJS())
+        .toEqual({start: 0, end: 3});
+
+      expect(tree.get(1).get('leaves').size).toBe(2);
+      expect(tree.get(1).get('leaves').get(0).toJS())
+        .toEqual({start: 3, end: 4});
+      expect(tree.get(1).get('leaves').get(1).toJS())
+        .toEqual({start: 4, end: 6});
+
+      expect(tree.get(2).get('leaves').size).toBe(2);
+      expect(tree.get(2).get('leaves').get(0).toJS())
+        .toEqual({start: 6, end: 8});
+      expect(tree.get(2).get('leaves').get(1).toJS())
+        .toEqual({start: 8, end: 10});
+
+      expect(BlockTree.getFingerprint(tree)).toBe('y.3.1-.2-z.4.2');
+    });
+  });
+});
diff --git a/src/model/immutable/__tests__/CharacterMetadata-test.js b/src/model/immutable/__tests__/CharacterMetadata-test.js
new file mode 100644
index 0000000..533b503
--- /dev/null
+++ b/src/model/immutable/__tests__/CharacterMetadata-test.js
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var {
+  BOLD,
+  BOLD_ITALIC,
+  NONE,
+  UNDERLINE,
+} = require('SampleDraftInlineStyle');
+
+describe('CharacterMetadata', () => {
+  it('must have appropriate default values', () => {
+    var character = CharacterMetadata.create();
+    expect(character.getStyle().size).toBe(0);
+    expect(character.getEntity()).toBe(null);
+  });
+
+  describe('Style handling', () => {
+    var plain = CharacterMetadata.create();
+    var bold = CharacterMetadata.create({style: BOLD});
+    var fancy = CharacterMetadata.create({style: BOLD_ITALIC});
+
+    it('must run `hasStyle` correctly', () => {
+      expect(plain.hasStyle('BOLD')).toBe(false);
+      expect(bold.hasStyle('BOLD')).toBe(true);
+      expect(fancy.hasStyle('BOLD')).toBe(true);
+      expect(plain.hasStyle('ITALIC')).toBe(false);
+      expect(bold.hasStyle('ITALIC')).toBe(false);
+      expect(fancy.hasStyle('ITALIC')).toBe(true);
+    });
+
+    it('must apply style', () => {
+      var newlyBold = CharacterMetadata.applyStyle(plain, 'BOLD');
+      expect(newlyBold.hasStyle('BOLD')).toBe(true);
+      var alsoItalic = CharacterMetadata.applyStyle(newlyBold, 'ITALIC');
+      expect(alsoItalic.hasStyle('BOLD')).toBe(true);
+      expect(alsoItalic.hasStyle('ITALIC')).toBe(true);
+    });
+
+    it('must remove style', () => {
+      var justBold = CharacterMetadata.removeStyle(fancy, 'ITALIC');
+      expect(justBold.hasStyle('BOLD')).toBe(true);
+      expect(justBold.hasStyle('ITALIC')).toBe(false);
+      var justPlain = CharacterMetadata.removeStyle(justBold, 'BOLD');
+      expect(justPlain.hasStyle('BOLD')).toBe(false);
+      expect(justPlain.hasStyle('ITALIC')).toBe(false);
+    });
+  });
+
+  describe('Entity handling', () => {
+    var withoutEntity = CharacterMetadata.create();
+    var withEntity = CharacterMetadata.create({entity: 'a'});
+
+    it('must apply entity correctly', () => {
+      var newKey = 'x';
+      var modifiedA = CharacterMetadata.applyEntity(withoutEntity, newKey);
+      var modifiedB = CharacterMetadata.applyEntity(withEntity, newKey);
+      expect(modifiedA.getEntity()).toBe(newKey);
+      expect(modifiedB.getEntity()).toBe(newKey);
+    });
+
+    it('must remove entity correctly', () => {
+      var modifiedA = CharacterMetadata.applyEntity(withoutEntity, null);
+      var modifiedB = CharacterMetadata.applyEntity(withEntity, null);
+      expect(modifiedA.getEntity()).toBe(null);
+      expect(modifiedB.getEntity()).toBe(null);
+    });
+  });
+
+  describe('Metadata Reuse', () => {
+    var empty = CharacterMetadata.create();
+    var withStyle = CharacterMetadata.create({style: BOLD});
+    var withTwoStyles = CharacterMetadata.create({style: BOLD_ITALIC});
+    var withEntity = CharacterMetadata.create({entity: '1234'});
+    var withStyleAndEntity = CharacterMetadata.create({
+      entity: '1234',
+      style: BOLD,
+    });
+
+    it('must reuse the same objects', () => {
+      expect(CharacterMetadata.create()).toBe(empty);
+      expect(CharacterMetadata.create({style: BOLD})).toBe(withStyle);
+      expect(
+        CharacterMetadata.create({style: BOLD_ITALIC})
+      ).toBe(
+        withTwoStyles
+      );
+      expect(CharacterMetadata.create({entity: '1234'})).toBe(withEntity);
+      expect(
+        CharacterMetadata.create({entity: '1234', style: BOLD})
+      ).toBe(
+        withStyleAndEntity
+      );
+    });
+
+    it('must reuse objects by defaulting config properties', () => {
+      expect(
+        CharacterMetadata.create({style: BOLD, entity: null})
+      ).toBe(
+        withStyle
+      );
+      expect(
+        CharacterMetadata.create({style: NONE, entity: '1234'})
+      ).toBe(
+        withEntity
+      );
+
+      var underlined = CharacterMetadata.create({
+        style: UNDERLINE,
+        entity: null,
+      });
+
+      expect(
+        CharacterMetadata.create({style: UNDERLINE})
+      ).toBe(
+        underlined
+      );
+    });
+  });
+});
diff --git a/src/model/immutable/__tests__/ContentBlock-test.js b/src/model/immutable/__tests__/ContentBlock-test.js
new file mode 100644
index 0000000..2e86825
--- /dev/null
+++ b/src/model/immutable/__tests__/ContentBlock-test.js
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var Immutable = require('immutable');
+
+var {
+  NONE,
+  BOLD,
+} = require('SampleDraftInlineStyle');
+
+describe('ContentBlock', () => {
+  var ENTITY_KEY = 'x';
+
+  function getSampleBlock() {
+    return new ContentBlock({
+      key: 'a',
+      type: 'unstyled',
+      text: 'Alpha',
+      characterList: Immutable.List.of(
+        CharacterMetadata.create({style: BOLD, entity: ENTITY_KEY}),
+        CharacterMetadata.EMPTY,
+        CharacterMetadata.EMPTY,
+        CharacterMetadata.create({style: BOLD}),
+        CharacterMetadata.create({entity: ENTITY_KEY})
+      ),
+    });
+  }
+
+  describe('basic retrieval', () => {
+    it('must provide default values', () => {
+      var block = new ContentBlock();
+      expect(block.getType()).toBe('unstyled');
+      expect(block.getText()).toBe('');
+      expect(
+        Immutable.is(block.getCharacterList(), Immutable.List())
+      ).toBe(true);
+    });
+
+    it('must retrieve properties', () => {
+      var block = getSampleBlock();
+      expect(block.getKey()).toBe('a');
+      expect(block.getText()).toBe('Alpha');
+      expect(block.getType()).toBe('unstyled');
+      expect(block.getLength()).toBe(5);
+      expect(block.getCharacterList().count()).toBe(5);
+    });
+  });
+
+  describe('style retrieval', () => {
+    it('must properly retrieve style at offset', () => {
+      var block = getSampleBlock();
+      expect(block.getInlineStyleAt(0)).toBe(BOLD);
+      expect(block.getInlineStyleAt(1)).toBe(NONE);
+      expect(block.getInlineStyleAt(2)).toBe(NONE);
+      expect(block.getInlineStyleAt(3)).toBe(BOLD);
+      expect(block.getInlineStyleAt(4)).toBe(NONE);
+    });
+
+    it('must correctly identify ranges of styles', () => {
+      var block = getSampleBlock();
+      var cb = jest.genMockFn();
+      block.findStyleRanges(() => true, cb);
+
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(4);
+      expect(calls[0]).toEqual([0, 1]);
+      expect(calls[1]).toEqual([1, 3]);
+      expect(calls[2]).toEqual([3, 4]);
+      expect(calls[3]).toEqual([4, 5]);
+    });
+  });
+
+  describe('entity retrieval', () => {
+    it('must properly retrieve entity at offset', () => {
+      var block = getSampleBlock();
+      expect(block.getEntityAt(0)).toBe(ENTITY_KEY);
+      expect(block.getEntityAt(1)).toBe(null);
+      expect(block.getEntityAt(2)).toBe(null);
+      expect(block.getEntityAt(3)).toBe(null);
+      expect(block.getEntityAt(4)).toBe(ENTITY_KEY);
+    });
+
+    it('must correctly identify ranges of entities', () => {
+      var block = getSampleBlock();
+      var cb = jest.genMockFn();
+      block.findEntityRanges(() => true, cb);
+
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(3);
+      expect(calls[0]).toEqual([0, 1]);
+      expect(calls[1]).toEqual([1, 4]);
+      expect(calls[2]).toEqual([4, 5]);
+    });
+  });
+});
diff --git a/src/model/immutable/__tests__/ContentState-test.js b/src/model/immutable/__tests__/ContentState-test.js
new file mode 100644
index 0000000..05f01b2
--- /dev/null
+++ b/src/model/immutable/__tests__/ContentState-test.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest
+  .autoMockOff()
+  .mock('SelectionState');
+
+var BlockMapBuilder = require('BlockMapBuilder');
+var ContentBlock = require('ContentBlock');
+var ContentState = require('ContentState');
+
+var SINGLE_BLOCK = [
+  {text: 'Lorem ipsum', key: 'a'},
+];
+var MULTI_BLOCK = [
+  {text: 'Four score', key: 'b'},
+  {text: 'and seven', key: 'c'},
+];
+
+var SelectionState = require('SelectionState');
+
+describe('ContentState', () => {
+  function getContentBlocks(textBlocks) {
+    return textBlocks.map(block => new ContentBlock(block));
+  }
+
+  function getConfigForText(textBlocks) {
+    var contentBlocks = getContentBlocks(textBlocks);
+    var blockMap = BlockMapBuilder.createFromArray(contentBlocks);
+    return {
+      blockMap,
+      selectionBefore: new SelectionState(),
+      selectionAfter: new SelectionState(),
+    };
+  }
+
+  function getSampleFromConfig(config) {
+    return new ContentState(config);
+  }
+
+  function getSample(textBlocks) {
+    return getSampleFromConfig(
+      getConfigForText(textBlocks)
+    );
+  }
+
+  describe('creation and retrieval', () => {
+    it('must create a new instance', () => {
+      var state = getSample(SINGLE_BLOCK);
+      expect(state instanceof ContentState).toBe(true);
+    });
+  });
+
+  describe('key fetching', () => {
+    it('must succeed or fail properly', () => {
+      var singleBlock = getSample(SINGLE_BLOCK);
+      var key = SINGLE_BLOCK[0].key;
+      expect(singleBlock.getKeyBefore(key)).toBe(undefined);
+      expect(singleBlock.getKeyAfter(key)).toBe(undefined);
+
+      var multiBlock = getSample(MULTI_BLOCK);
+      var firstKey = MULTI_BLOCK[0].key;
+      var secondKey = MULTI_BLOCK[1].key;
+
+      expect(multiBlock.getKeyBefore(firstKey)).toBe(undefined);
+      expect(multiBlock.getKeyAfter(firstKey)).toBe(secondKey);
+      expect(multiBlock.getKeyBefore(secondKey)).toBe(firstKey);
+      expect(multiBlock.getKeyAfter(secondKey)).toBe(undefined);
+    });
+  });
+
+  describe('block fetching', () => {
+    it('must retrieve or fail fetching block for key', () => {
+      var state = getSample(SINGLE_BLOCK);
+      var block = state.getBlockForKey('a');
+      expect(block instanceof ContentBlock).toBe(true);
+      expect(block.getText()).toBe(SINGLE_BLOCK[0].text);
+      expect(state.getBlockForKey('x')).toBe(undefined);
+    });
+  });
+});
diff --git a/src/model/immutable/__tests__/EditorBidiService-test.js b/src/model/immutable/__tests__/EditorBidiService-test.js
new file mode 100644
index 0000000..5a351a2
--- /dev/null
+++ b/src/model/immutable/__tests__/EditorBidiService-test.js
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var ContentBlock = require('ContentBlock');
+var ContentState = require('ContentState');
+var EditorBidiService = require('EditorBidiService');
+var Immutable = require('immutable');
+var UnicodeBidiDirection = require('UnicodeBidiDirection');
+
+var {
+  OrderedMap,
+  Seq,
+} = Immutable;
+
+var {
+  LTR,
+  RTL,
+} = UnicodeBidiDirection;
+
+var ltr = new ContentBlock({
+  key: 'a',
+  text: 'hello',
+});
+var rtl = new ContentBlock({
+  key: 'b',
+  text: '\u05e9\u05d1\u05ea',
+});
+var empty = new ContentBlock({
+  key: 'c',
+  text: '',
+});
+
+describe('EditorBidiService', () => {
+  function getContentState(blocks) {
+    var keys = Seq(blocks.map(b => b.getKey()));
+    var values = Seq(blocks);
+    var blockMap = OrderedMap(keys.zip(values));
+    return new ContentState({blockMap});
+  }
+
+  it('must create a new map', () => {
+    var state = getContentState([ltr]);
+    var directions = EditorBidiService.getDirectionMap(state);
+    expect(
+      directions.keySeq().toArray()
+    ).toEqual(
+      ['a']
+    );
+    expect(
+      directions.valueSeq().toArray()
+    ).toEqual(
+      [LTR]
+    );
+  });
+
+  it('must return the same map if no changes', () => {
+    var state = getContentState([ltr]);
+    var directions = EditorBidiService.getDirectionMap(state);
+
+    var nextState = getContentState([ltr]);
+    var nextDirections = EditorBidiService.getDirectionMap(
+      nextState,
+      directions
+    );
+
+    expect(state).not.toBe(nextState);
+    expect(directions).toBe(nextDirections);
+  });
+
+  it('must return the same map if no text changes', () => {
+    var state = getContentState([ltr]);
+    var directions = EditorBidiService.getDirectionMap(state);
+
+    var newLTR = new ContentBlock({
+      key: 'a',
+      text: 'hello',
+    });
+    expect(newLTR).not.toBe(ltr);
+
+    var nextState = getContentState([newLTR]);
+    var nextDirections = EditorBidiService.getDirectionMap(
+      nextState,
+      directions
+    );
+
+    expect(state).not.toBe(nextState);
+    expect(directions).toBe(nextDirections);
+  });
+
+  it('must return the same map if no directions change', () => {
+    var state = getContentState([ltr]);
+    var directions = EditorBidiService.getDirectionMap(state);
+
+    var newLTR = new ContentBlock({
+      key: 'a',
+      text: 'asdf',
+    });
+    expect(newLTR).not.toBe(ltr);
+
+    var nextState = getContentState([newLTR]);
+    var nextDirections = EditorBidiService.getDirectionMap(
+      nextState,
+      directions
+    );
+
+    expect(state).not.toBe(nextState);
+    expect(directions).toBe(nextDirections);
+  });
+
+  it('must return a new map if block keys change', () => {
+    var state = getContentState([ltr]);
+    var directions = EditorBidiService.getDirectionMap(state);
+
+    var newLTR = new ContentBlock({
+      key: 'asdf',
+      text: 'asdf',
+    });
+
+    var nextState = getContentState([newLTR]);
+    var nextDirections = EditorBidiService.getDirectionMap(
+      nextState,
+      directions
+    );
+
+    expect(state).not.toBe(nextState);
+    expect(directions).not.toBe(nextDirections);
+
+    expect(
+      nextDirections.keySeq().toArray()
+    ).toEqual(
+      ['asdf']
+    );
+    expect(
+      nextDirections.valueSeq().toArray()
+    ).toEqual(
+      [LTR]
+    );
+  });
+
+  it('must return a new map if direction changes', () => {
+    var state = getContentState([ltr, empty]);
+    var directions = EditorBidiService.getDirectionMap(state);
+
+    expect(
+      directions.valueSeq().toArray()
+    ).toEqual(
+      [LTR, LTR]
+    );
+
+    var nextState = getContentState([ltr, rtl]);
+    var nextDirections = EditorBidiService.getDirectionMap(
+      nextState,
+      directions
+    );
+
+    expect(state).not.toBe(nextState);
+    expect(directions).not.toBe(nextDirections);
+    expect(
+      nextDirections.valueSeq().toArray()
+    ).toEqual(
+      [LTR, RTL]
+    );
+  });
+});
diff --git a/src/model/immutable/__tests__/EditorState-test.js b/src/model/immutable/__tests__/EditorState-test.js
new file mode 100644
index 0000000..3b7c817
--- /dev/null
+++ b/src/model/immutable/__tests__/EditorState-test.js
@@ -0,0 +1,275 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var ContentState = require('ContentState');
+var EditorState = require('EditorState');
+var Immutable = require('immutable');
+var SelectionState = require('SelectionState');
+
+var {
+  NONE,
+  BOLD,
+  ITALIC,
+} = require('SampleDraftInlineStyle');
+
+var {EMPTY} = CharacterMetadata;
+
+var {
+  List,
+  Repeat,
+} = Immutable;
+
+var plainBlock = new ContentBlock({
+  key: 'a',
+  text: 'Arsenal',
+  characterList: List(Repeat(EMPTY, 7)),
+});
+
+var boldChar = CharacterMetadata.create({style: BOLD});
+var boldBlock = new ContentBlock({
+  key: 'b',
+  text: 'Burnley',
+  characterList: List(Repeat(boldChar, 7)),
+});
+
+var emptyBlockA = new ContentBlock({
+  key: 'emptyA',
+  text: '',
+  characterList: List(),
+});
+
+var emptyBlockB = new ContentBlock({
+  key: 'emptyB',
+  text: '',
+  characterList: List(),
+});
+
+var italicChar = CharacterMetadata.create({style: ITALIC});
+var italicBlock = new ContentBlock({
+  key: 'c',
+  text: 'Chelsea',
+  characterList: List(Repeat(italicChar, 7)),
+});
+
+function getUndecoratedEditorState() {
+  var contentState = ContentState.createFromBlockArray([
+    plainBlock,
+    boldBlock,
+    emptyBlockA,
+    emptyBlockB,
+    italicBlock,
+  ]);
+  return EditorState.createWithContent(contentState);
+}
+
+function getEmptyLedMultiBlockEditorState() {
+  var contentState = ContentState.createFromBlockArray([
+    emptyBlockA,
+    emptyBlockB,
+    boldBlock,
+  ]);
+  return EditorState.createWithContent(contentState);
+}
+
+function getDecoratedEditorState(decorator) {
+  var contentState = ContentState.createFromBlockArray([
+    boldBlock,
+    italicBlock,
+  ]);
+  return EditorState.createWithContent(contentState, decorator);
+}
+
+describe('EditorState', () => {
+
+  describe('getCurrentInlineStyle', () => {
+    var mainEditor = getUndecoratedEditorState();
+
+    describe('Collapsed selection', () => {
+      var mainSelection = new SelectionState({
+        anchorKey: 'a',
+        anchorOffset: 0,
+        focusKey: 'a',
+        focusOffset: 0,
+        isBackward: false,
+      });
+
+      it('uses right of the caret at document start', () => {
+        var editor = EditorState.acceptSelection(mainEditor, mainSelection);
+        expect(editor.getCurrentInlineStyle()).toBe(NONE);
+      });
+
+      it('uses left of the caret, at position `1+`', () => {
+        var selection = mainSelection.merge({
+          anchorOffset: 1,
+          focusOffset: 1,
+        });
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(NONE);
+      });
+
+      it('uses right of the caret at offset `0` within document', () => {
+        var selection = mainSelection.merge({
+          anchorKey: 'b',
+          focusKey: 'b',
+        });
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(BOLD);
+      });
+
+      it('uses previous block at offset `0` within empty block', () => {
+        var selection = mainSelection.merge({
+          anchorKey: 'emptyA',
+          focusKey: 'emptyA',
+        });
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(BOLD);
+      });
+
+      it('looks upward through empty blocks to find a character', () => {
+        var selection = mainSelection.merge({
+          anchorKey: 'emptyB',
+          focusKey: 'emptyB',
+        });
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(BOLD);
+      });
+    });
+
+    describe('Non-collapsed selection', () => {
+      var selectionState = new SelectionState({
+        anchorKey: 'a',
+        anchorOffset: 0,
+        focusKey: 'a',
+        focusOffset: 1,
+        isBackward: false,
+      });
+
+      it('uses right of the start for blocks with text', () => {
+        var selection = selectionState.set('focusKey', 'b');
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(NONE);
+      });
+
+      it('uses left of the start if starting at end of block', () => {
+        var blockB = mainEditor.getCurrentContent().getBlockForKey('b');
+        var selection = selectionState.merge({
+          anchorKey: 'b',
+          anchorOffset: blockB.getLength(),
+          focusKey: 'c',
+          focusOffset: 3,
+        });
+
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(BOLD);
+      });
+
+      it('looks upward through empty blocks to find a character', () => {
+        var selection = selectionState.merge({
+          anchorKey: 'emptyA',
+          anchorOffset: 0,
+          focusKey: 'c',
+          focusOffset: 3,
+        });
+        var editor = EditorState.acceptSelection(mainEditor, selection);
+        expect(editor.getCurrentInlineStyle()).toBe(BOLD);
+      });
+
+      it('falls back to no style if in empty block at document start', () => {
+        var editor = getEmptyLedMultiBlockEditorState(
+          new SelectionState({
+            anchorKey: 'emptyA',
+            anchorOffset: 0,
+            focusKey: 'c',
+            focusOffset: 3,
+            isBackward: false,
+          })
+        );
+        expect(editor.getCurrentInlineStyle()).toBe(NONE);
+      });
+    });
+  });
+
+  describe('Decorator handling', () => {
+    var boldA = List(Repeat('x', boldBlock.getLength()));
+    var boldB = List(Repeat('y', boldBlock.getLength()));
+
+    function Decorator() {}
+    Decorator.prototype.getDecorations = jest.genMockFn();
+
+    function NextDecorator() {}
+    NextDecorator.prototype.getDecorations = jest.genMockFn();
+
+    beforeEach(() => {
+      Decorator.prototype.getDecorations.mockClear();
+      Decorator.prototype.getDecorations.mockImplementation(
+        v => v === boldBlock ? boldA : List(Repeat(undefined, v.getLength()))
+      );
+    });
+
+    it('must set a new decorator', () => {
+      var decorator = new Decorator();
+      var editorState = getDecoratedEditorState(decorator);
+      expect(decorator.getDecorations.mock.calls.length).toBe(2);
+
+      Decorator.prototype.getDecorations.mockImplementation(
+        v => v === boldBlock ? boldB : List(Repeat(undefined, v.getLength()))
+      );
+      var newDecorator = new NextDecorator();
+      NextDecorator.prototype.getDecorations.mockImplementation(
+        v => v === boldBlock ? boldB : List(Repeat(undefined, v.getLength()))
+      );
+
+      var withNewDecorator = EditorState.set(editorState, {
+        decorator: newDecorator,
+      });
+      expect(withNewDecorator).not.toBe(editorState);
+
+      // Twice for the initial tree map generation, then twice more for
+      // filter comparison.
+      expect(decorator.getDecorations.mock.calls.length).toBe(4);
+
+      // Twice for filter comparison, once for tree generation since one
+      // block has the same decoration list and is filtered out.
+      expect(newDecorator.getDecorations.mock.calls.length).toBe(3);
+
+      expect(withNewDecorator.getDecorator()).toBe(newDecorator);
+
+      // Preserve block trees that had the same decorator list.
+      expect(
+        editorState.getBlockTree(boldBlock.getKey())
+      ).toBe(
+        withNewDecorator.getBlockTree(boldBlock.getKey())
+      );
+
+      expect(
+        editorState.getBlockTree(italicBlock.getKey())
+      ).not.toBe(
+        withNewDecorator.getBlockTree(italicBlock.getKey())
+      );
+    });
+
+    it('must correctly remove a decorator', () => {
+      var decorator = new Decorator();
+      var editorState = getDecoratedEditorState(decorator);
+      expect(decorator.getDecorations.mock.calls.length).toBe(2);
+      var withNewDecorator = EditorState.set(editorState, {decorator: null});
+      expect(withNewDecorator).not.toBe(editorState);
+      expect(withNewDecorator.getDecorator()).toBe(null);
+      expect(decorator.getDecorations.mock.calls.length).toBe(2);
+    });
+  });
+});
diff --git a/src/model/immutable/__tests__/SelectionState-test.js b/src/model/immutable/__tests__/SelectionState-test.js
new file mode 100644
index 0000000..f19da00
--- /dev/null
+++ b/src/model/immutable/__tests__/SelectionState-test.js
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var SelectionState = require('SelectionState');
+
+var COLLAPSED = {
+  anchorKey: 'a',
+  anchorOffset: 0,
+  focusKey: 'a',
+  focusOffset: 0,
+  isBackward: false,
+  hasFocus: true,
+};
+
+var WITHIN_BLOCK = {
+  anchorKey: 'a',
+  anchorOffset: 10,
+  focusKey: 'a',
+  focusOffset: 20,
+  isBackward: false,
+  hasFocus: true,
+};
+
+var MULTIBLOCK = {
+  anchorKey: 'b',
+  anchorOffset: 10,
+  focusKey: 'c',
+  focusOffset: 15,
+  isBackward: false,
+  hasFocus: true,
+};
+
+function getSample(config) {
+  return new SelectionState(config);
+}
+
+function verifyValues(state, values) {
+  var {
+    anchorKey,
+    anchorOffset,
+    focusKey,
+    focusOffset,
+    isBackward,
+    hasFocus,
+  } = values;
+
+  expect(state.getAnchorKey()).toBe(anchorKey);
+  expect(state.getAnchorOffset()).toBe(anchorOffset);
+  expect(state.getFocusKey()).toBe(focusKey);
+  expect(state.getFocusOffset()).toBe(focusOffset);
+  expect(state.getIsBackward()).toBe(isBackward);
+  expect(state.getHasFocus()).toBe(hasFocus);
+}
+
+describe('SelectionState', () => {
+  describe('creation and retrieval', () => {
+    it('must create a new instance', () => {
+      var state = getSample(COLLAPSED);
+      expect(state instanceof SelectionState).toBe(true);
+    });
+
+    it('must retrieve properties correctly', () => {
+      var state = getSample(COLLAPSED);
+      verifyValues(state, COLLAPSED);
+    });
+  });
+
+  describe('hasEdgeWithin', () => {
+    it('is false for non-edge block keys', () => {
+      expect(getSample(COLLAPSED).hasEdgeWithin('b', 0, 0)).toBe(false);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('b', 0, 0)).toBe(false);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('d', 0, 0)).toBe(false);
+    });
+
+    it('is false if offset is outside the selection range', () => {
+      expect(getSample(COLLAPSED).hasEdgeWithin('a', 1, 1)).toBe(false);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 1, 1)).toBe(false);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('b', 1, 1)).toBe(false);
+    });
+
+    it('is true if key match and offset equals selection edge', () => {
+      expect(getSample(COLLAPSED).hasEdgeWithin('a', 0, 1)).toBe(true);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 10, 15)).toBe(true);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 15, 20)).toBe(true);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('b', 10, 20)).toBe(true);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('c', 15, 20)).toBe(true);
+    });
+
+    it('is true if selection range is entirely within test range', () => {
+      expect(
+        getSample({...COLLAPSED, anchorOffset: 5, focusOffset: 5})
+          .hasEdgeWithin('a', 0, 10)
+      ).toBe(true);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 0, 40)).toBe(true);
+    });
+
+    it('is true if selection range edge overlaps test range', () => {
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 5, 15)).toBe(true);
+      expect(getSample(WITHIN_BLOCK).hasEdgeWithin('a', 15, 25)).toBe(true);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('b', 5, 20)).toBe(true);
+      expect(getSample(MULTIBLOCK).hasEdgeWithin('c', 5, 20)).toBe(true);
+    });
+  });
+
+  describe('isCollapsed', () => {
+    it('works properly', () => {
+      expect(getSample(COLLAPSED).isCollapsed()).toBe(true);
+      expect(getSample(WITHIN_BLOCK).isCollapsed()).toBe(false);
+      expect(getSample(MULTIBLOCK).isCollapsed()).toBe(false);
+    });
+  });
+
+  describe('start and end retrieval', () => {
+    function flip(selectionState) {
+      return selectionState.merge({
+        anchorKey: selectionState.getFocusKey(),
+        anchorOffset: selectionState.getFocusOffset(),
+        focusKey: selectionState.getAnchorKey(),
+        focusOffset: selectionState.getAnchorOffset(),
+        isBackward: !selectionState.getIsBackward(),
+      });
+    }
+
+    it('properly identifies start and end keys', () => {
+      expect(getSample(COLLAPSED).getStartKey()).toBe('a');
+      expect(getSample(WITHIN_BLOCK).getStartKey()).toBe('a');
+      expect(getSample(MULTIBLOCK).getStartKey()).toBe('b');
+      expect(getSample(COLLAPSED).getEndKey()).toBe('a');
+      expect(getSample(WITHIN_BLOCK).getEndKey()).toBe('a');
+      expect(getSample(MULTIBLOCK).getEndKey()).toBe('c');
+    });
+
+    it('properly identifies start and end offsets', () => {
+      expect(getSample(COLLAPSED).getStartOffset()).toBe(0);
+      expect(getSample(WITHIN_BLOCK).getStartOffset()).toBe(10);
+      expect(getSample(MULTIBLOCK).getStartOffset()).toBe(10);
+      expect(getSample(COLLAPSED).getEndOffset()).toBe(0);
+      expect(getSample(WITHIN_BLOCK).getEndOffset()).toBe(20);
+      expect(getSample(MULTIBLOCK).getEndOffset()).toBe(15);
+    });
+
+    it('properly identifies start end end keys when backward', () => {
+      var withinBlock = flip(getSample(WITHIN_BLOCK));
+      var multiBlock = getSample({...MULTIBLOCK, isBackward: true});
+      expect(withinBlock.getStartKey()).toBe('a');
+      expect(multiBlock.getStartKey()).toBe('c');
+      expect(withinBlock.getEndKey()).toBe('a');
+      expect(multiBlock.getEndKey()).toBe('b');
+    });
+
+    it('properly identifies start end end offsets when backward', () => {
+      var withinBlock = flip(getSample(WITHIN_BLOCK));
+      var multiBlock = getSample({...MULTIBLOCK, isBackward: true});
+      expect(withinBlock.getStartOffset()).toBe(10);
+      expect(multiBlock.getStartOffset()).toBe(15);
+      expect(withinBlock.getEndOffset()).toBe(20);
+      expect(multiBlock.getEndOffset()).toBe(10);
+    });
+  });
+
+});
diff --git a/src/model/immutable/__tests__/findRangesImmutable-test.js b/src/model/immutable/__tests__/findRangesImmutable-test.js
new file mode 100644
index 0000000..bd43a7e
--- /dev/null
+++ b/src/model/immutable/__tests__/findRangesImmutable-test.js
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var Immutable = require('immutable');
+var findRangesImmutable = require('findRangesImmutable');
+
+describe('findRangesImmutable', () => {
+  var returnTrue = () => true;
+
+  it('must be a no-op for an empty list', () => {
+    var cb = jest.genMockFn();
+    findRangesImmutable(
+      Immutable.List(),
+      returnTrue,
+      returnTrue,
+      cb
+    );
+    expect(cb.mock.calls.length).toBe(0);
+  });
+
+  describe('Function usage', () => {
+    var list = Immutable.List.of(1, 1, 1, 1, 1);
+
+    it('must identify the full list as a single range', () => {
+      var cb = jest.genMockFn();
+      findRangesImmutable(list, returnTrue, returnTrue, cb);
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(1);
+      expect(calls[0]).toEqual([0, 5]);
+    });
+
+    it('must properly use `areEqualFn`', () => {
+      var cb = jest.genMockFn();
+      var areEqual = () => false;
+      findRangesImmutable(
+        list,
+        areEqual, // never equal
+        returnTrue,
+        cb
+      );
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(5);
+      expect(calls[0]).toEqual([0, 1]);
+      expect(calls[1]).toEqual([1, 2]);
+      expect(calls[2]).toEqual([2, 3]);
+      expect(calls[3]).toEqual([3, 4]);
+      expect(calls[4]).toEqual([4, 5]);
+    });
+
+    it('must properly use `filterFn`', () => {
+      var cb = jest.genMockFn();
+      findRangesImmutable(
+        list,
+        returnTrue,
+        () => false, // never an accepted filter result
+        cb
+      );
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(0);
+    });
+  });
+
+  describe('Range finding for multi-value list', () => {
+    var list = Immutable.List.of(0, 0, 1, 1, 0, 0, 2, 2);
+
+    it('must identify each range', () => {
+      var cb = jest.genMockFn();
+      findRangesImmutable(
+        list,
+        (a, b) => a === b,
+        returnTrue,
+        cb
+      );
+      var calls = cb.mock.calls;
+      expect(calls.length).toBe(4);
+      expect(calls[0]).toEqual([0, 2]);
+      expect(calls[1]).toEqual([2, 4]);
+      expect(calls[2]).toEqual([4, 6]);
+      expect(calls[3]).toEqual([6, 8]);
+    });
+  });
+});
diff --git a/src/model/immutable/findRangesImmutable.js b/src/model/immutable/findRangesImmutable.js
new file mode 100644
index 0000000..7351130
--- /dev/null
+++ b/src/model/immutable/findRangesImmutable.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule findRangesImmutable
+ * @flow
+ */
+
+'use strict';
+
+import type {List} from 'immutable';
+
+/**
+ * Search through an array to find contiguous stretches of elements that
+ * match a specified filter function.
+ *
+ * When ranges are found, execute a specified `found` function to supply
+ * the values to the caller.
+ */
+function findRangesImmutable<T>(
+  haystack: List<T>,
+  areEqualFn: (a: T, b: T) => boolean,
+  filterFn: (value: T) => boolean,
+  foundFn: (start: number, end: number) => void
+): void {
+  if (!haystack.size) {
+    return;
+  }
+
+  var cursor: number = 0;
+
+  haystack.reduce((value, nextValue, nextIndex) => {
+    if (!areEqualFn(value, nextValue)) {
+      if (filterFn(value)) {
+        foundFn(cursor, nextIndex);
+      }
+      cursor = nextIndex;
+    }
+    return nextValue;
+  });
+
+  filterFn(haystack.last()) && foundFn(cursor, haystack.count());
+}
+
+module.exports = findRangesImmutable;
diff --git a/src/model/keys/generateBlockKey.js b/src/model/keys/generateBlockKey.js
new file mode 100644
index 0000000..0d2fadc
--- /dev/null
+++ b/src/model/keys/generateBlockKey.js
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule generateBlockKey
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var seenKeys = {};
+var MULTIPLIER = Math.pow(2, 24);
+
+function generateBlockKey(): string {
+  var key;
+  while (key === undefined || seenKeys.hasOwnProperty(key) || !isNaN(+key)) {
+    key = Math.floor(Math.random() * MULTIPLIER).toString(32);
+  }
+  seenKeys[key] = true;
+  return key;
+}
+
+module.exports = generateBlockKey;
diff --git a/src/model/modifier/DraftEntitySegments.js b/src/model/modifier/DraftEntitySegments.js
new file mode 100644
index 0000000..2416cc4
--- /dev/null
+++ b/src/model/modifier/DraftEntitySegments.js
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftEntitySegments
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+import type {DraftRemovalDirection} from 'DraftRemovalDirection';
+import type {DraftRange} from 'DraftRange';
+
+/**
+ * Identify the range to delete from a segmented entity.
+ *
+ * Rules:
+ *
+ *  Example: 'John F. Kennedy'
+ *
+ *   - Deletion from within any non-whitespace (i.e. ['John', 'F.', 'Kennedy'])
+ *     will return the range of that text.
+ *
+ *       'John F. Kennedy' -> 'John F.'
+ *                  ^
+ *
+ *   - Forward deletion of whitespace will remove the following section:
+ *
+ *       'John F. Kennedy' -> 'John Kennedy'
+ *            ^
+ *
+ *   - Backward deletion of whitespace will remove the previous section:
+ *
+ *       'John F. Kennedy' -> 'F. Kennedy'
+ *            ^
+ */
+var DraftEntitySegments = {
+  getRemovalRange: function(
+    selectionStart: number,
+    selectionEnd: number,
+    text: string,
+    entityStart: number,
+    direction: DraftRemovalDirection
+  ): DraftRange {
+    var segments = text.split(' ');
+    segments = segments.map((/*string*/ segment, /*number*/ ii) => {
+      if (direction === 'forward') {
+        if (ii > 0) {
+          return ' ' + segment;
+        }
+      } else if (ii < segments.length - 1) {
+        return segment + ' ';
+      }
+      return segment;
+    });
+
+    var segmentStart = entityStart;
+    var segmentEnd;
+    var segment;
+    var removalStart: any = null;
+    var removalEnd: any = null;
+
+    for (var jj = 0; jj < segments.length; jj++) {
+      segment = segments[jj];
+      segmentEnd = segmentStart + segment.length;
+
+      // Our selection overlaps this segment.
+      if (selectionStart < segmentEnd && segmentStart < selectionEnd) {
+        if (removalStart !== null) {
+          removalEnd = segmentEnd;
+        } else {
+          removalStart = segmentStart;
+          removalEnd = segmentEnd;
+        }
+      } else if (removalStart !== null) {
+        break;
+      }
+
+      segmentStart = segmentEnd;
+    }
+
+    var entityEnd = entityStart + text.length;
+    var atStart = removalStart === entityStart;
+    var atEnd = removalEnd === entityEnd;
+
+    if ((!atStart && atEnd) || (atStart && !atEnd)) {
+      if (direction === 'forward') {
+        if (removalEnd !== entityEnd) {
+          removalEnd++;
+        }
+      } else if (removalStart !== entityStart) {
+        removalStart--;
+      }
+    }
+
+    return {
+      start: removalStart,
+      end: removalEnd,
+    };
+  },
+};
+
+module.exports = DraftEntitySegments;
diff --git a/src/model/modifier/DraftModifier.js b/src/model/modifier/DraftModifier.js
new file mode 100644
index 0000000..06c31ad
--- /dev/null
+++ b/src/model/modifier/DraftModifier.js
@@ -0,0 +1,228 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftModifier
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentStateInlineStyle = require('ContentStateInlineStyle');
+var {OrderedSet} = require('immutable');
+
+var applyEntityToContentState = require('applyEntityToContentState');
+var getCharacterRemovalRange = require('getCharacterRemovalRange');
+var getContentStateFragment = require('getContentStateFragment');
+var insertFragmentIntoContentState = require('insertFragmentIntoContentState');
+var insertTextIntoContentState = require('insertTextIntoContentState');
+var invariant = require('invariant');
+var removeEntitiesAtEdges = require('removeEntitiesAtEdges');
+var removeRangeFromContentState = require('removeRangeFromContentState');
+var setBlockTypeForContentState = require('setBlockTypeForContentState');
+var splitBlockInContentState = require('splitBlockInContentState');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentState from 'ContentState';
+import type {DraftBlockType} from 'DraftBlockType';
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+import type {DraftRemovalDirection} from 'DraftRemovalDirection';
+import type SelectionState from 'SelectionState';
+
+/**
+ * `DraftModifier` provides a set of convenience methods that apply
+ * modifications to a `ContentState` object based on a target `SelectionState`.
+ *
+ * Any change to a `ContentState` should be decomposable into a series of
+ * transaction functions that apply the required changes and return output
+ * `ContentState` objects.
+ *
+ * These functions encapsulate some of the most common transaction sequences.
+ */
+var DraftModifier = {
+  replaceText: function(
+    contentState: ContentState,
+    rangeToReplace: SelectionState,
+    text: string,
+    inlineStyle?: DraftInlineStyle,
+    entityKey?: ?string
+  ): ContentState {
+    var withoutEntities = removeEntitiesAtEdges(contentState, rangeToReplace);
+    var withoutText = removeRangeFromContentState(
+      withoutEntities,
+      rangeToReplace
+    );
+
+    var character = CharacterMetadata.create({
+      style: inlineStyle || OrderedSet(),
+      entity: entityKey || null,
+    });
+
+    return insertTextIntoContentState(
+      withoutText,
+      withoutText.getSelectionAfter(),
+      text,
+      character
+    );
+  },
+
+  insertText: function(
+    contentState: ContentState,
+    targetRange: SelectionState,
+    text: string,
+    inlineStyle?: DraftInlineStyle,
+    entityKey?: ?string
+  ): ContentState {
+    invariant(
+      targetRange.isCollapsed(),
+      'Target range must be collapsed for `insertText`.'
+    );
+    return DraftModifier.replaceText(
+      contentState,
+      targetRange,
+      text,
+      inlineStyle,
+      entityKey
+    );
+  },
+
+  moveText: function(
+    contentState: ContentState,
+    removalRange: SelectionState,
+    targetRange: SelectionState
+  ): ContentState {
+    var movedFragment = getContentStateFragment(contentState, removalRange);
+
+    var afterRemoval = DraftModifier.removeRange(
+      contentState,
+      removalRange,
+      'backward'
+    );
+
+    return DraftModifier.replaceWithFragment(
+      afterRemoval,
+      targetRange,
+      movedFragment
+    );
+  },
+
+  replaceWithFragment: function(
+    contentState: ContentState,
+    targetRange: SelectionState,
+    fragment: BlockMap
+  ): ContentState {
+    var withoutEntities = removeEntitiesAtEdges(contentState, targetRange);
+    var withoutText = removeRangeFromContentState(
+      withoutEntities,
+      targetRange
+    );
+
+    return insertFragmentIntoContentState(
+      withoutText,
+      withoutText.getSelectionAfter(),
+      fragment
+    );
+  },
+
+  removeRange: function(
+    contentState: ContentState,
+    rangeToRemove: SelectionState,
+    removalDirection: DraftRemovalDirection
+  ): ContentState {
+    // Check whether the selection state overlaps with a single entity.
+    // If so, try to remove the appropriate substring of the entity text.
+    if (rangeToRemove.getAnchorKey() === rangeToRemove.getFocusKey()) {
+      var key = rangeToRemove.getAnchorKey();
+      var startOffset = rangeToRemove.getStartOffset();
+      var endOffset = rangeToRemove.getEndOffset();
+      var block = contentState.getBlockForKey(key);
+
+      var startEntity = block.getEntityAt(startOffset);
+      var endEntity = block.getEntityAt(endOffset - 1);
+      if (startEntity && startEntity === endEntity) {
+        var adjustedRemovalRange = getCharacterRemovalRange(
+          block,
+          rangeToRemove,
+          removalDirection
+        );
+        return removeRangeFromContentState(contentState, adjustedRemovalRange);
+      }
+    }
+
+    var withoutEntities = removeEntitiesAtEdges(contentState, rangeToRemove);
+    return removeRangeFromContentState(withoutEntities, rangeToRemove);
+  },
+
+  splitBlock: function(
+    contentState: ContentState,
+    selectionState: SelectionState
+  ): ContentState {
+    var withoutEntities = removeEntitiesAtEdges(contentState, selectionState);
+    var withoutText = removeRangeFromContentState(
+      withoutEntities,
+      selectionState
+    );
+
+    return splitBlockInContentState(
+      withoutText,
+      withoutText.getSelectionAfter()
+    );
+  },
+
+  applyInlineStyle: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    inlineStyle: string
+  ): ContentState {
+    return ContentStateInlineStyle.add(
+      contentState,
+      selectionState,
+      inlineStyle
+    );
+  },
+
+  removeInlineStyle: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    inlineStyle: string
+  ): ContentState {
+    return ContentStateInlineStyle.remove(
+      contentState,
+      selectionState,
+      inlineStyle
+    );
+  },
+
+  setBlockType: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    blockType: DraftBlockType
+  ): ContentState {
+    return setBlockTypeForContentState(
+      contentState,
+      selectionState,
+      blockType
+    );
+  },
+
+  applyEntity: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    entityKey: ?string
+  ): ContentState {
+    var withoutEntities = removeEntitiesAtEdges(contentState, selectionState);
+    return applyEntityToContentState(
+      withoutEntities,
+      selectionState,
+      entityKey
+    );
+  },
+};
+
+module.exports = DraftModifier;
diff --git a/src/model/modifier/DraftRange.js b/src/model/modifier/DraftRange.js
new file mode 100644
index 0000000..8bf74a2
--- /dev/null
+++ b/src/model/modifier/DraftRange.js
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftRange
+ * @flow
+ */
+
+'use strict';
+
+export type DraftRange = {
+  start: number;
+  end: number;
+};
diff --git a/src/model/modifier/DraftRemovableWord.js b/src/model/modifier/DraftRemovableWord.js
new file mode 100644
index 0000000..5395a43
--- /dev/null
+++ b/src/model/modifier/DraftRemovableWord.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftRemovableWord
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var TokenizeUtil = require('TokenizeUtil');
+
+var punctuation = TokenizeUtil.getPunctuation();
+
+// The apostrophe and curly single quotes behave in a curious way: when
+// surrounded on both sides by word characters, they behave as word chars; when
+// either neighbor is punctuation or an end of the string, they behave as
+// punctuation.
+var CHAMELEON_CHARS = '[\'\u2018\u2019]';
+
+// Remove the underscore, which should count as part of the removable word. The
+// "chameleon chars" also count as punctuation in this regex.
+var WHITESPACE_AND_PUNCTUATION = '\\s|(?![_])' + punctuation;
+
+var DELETE_STRING =
+  '^' +
+  '(?:' + WHITESPACE_AND_PUNCTUATION + ')*' +
+  '(?:' + CHAMELEON_CHARS + '|(?!' + WHITESPACE_AND_PUNCTUATION + ').)*' +
+  '(?:(?!' + WHITESPACE_AND_PUNCTUATION + ').)';
+var DELETE_REGEX = new RegExp(DELETE_STRING);
+
+var BACKSPACE_STRING =
+  '(?:(?!' + WHITESPACE_AND_PUNCTUATION + ').)' +
+  '(?:' + CHAMELEON_CHARS + '|(?!' + WHITESPACE_AND_PUNCTUATION + ').)*' +
+  '(?:' + WHITESPACE_AND_PUNCTUATION + ')*' +
+  '$';
+var BACKSPACE_REGEX = new RegExp(BACKSPACE_STRING);
+
+function getRemovableWord(
+  text: string,
+  isBackward: boolean
+): string {
+  var matches = isBackward ?
+    BACKSPACE_REGEX.exec(text) :
+    DELETE_REGEX.exec(text);
+  return matches ? matches[0] : text;
+}
+
+var DraftRemovableWord = {
+  getBackward: function(text: string): string {
+    return getRemovableWord(text, true);
+  },
+
+  getForward: function(text: string): string {
+    return getRemovableWord(text, false);
+  },
+};
+
+module.exports = DraftRemovableWord;
diff --git a/src/model/modifier/RichTextEditorUtil.js b/src/model/modifier/RichTextEditorUtil.js
new file mode 100644
index 0000000..8cf6825
--- /dev/null
+++ b/src/model/modifier/RichTextEditorUtil.js
@@ -0,0 +1,426 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule RichTextEditorUtil
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftEntity = require('DraftEntity');
+var DraftModifier = require('DraftModifier');
+var EditorState = require('EditorState');
+
+var adjustBlockDepthForContentState = require('adjustBlockDepthForContentState');
+
+import type ContentState from 'ContentState';
+import type {DraftBlockType} from 'DraftBlockType';
+import type {DraftEditorCommand} from 'DraftEditorCommand';
+import type SelectionState from 'SelectionState';
+import type URI from 'URI';
+
+var RichTextEditorUtil = {
+  currentBlockContainsLink: function(
+    editorState: EditorState
+  ): boolean {
+    var selection = editorState.getSelection();
+    return editorState.getCurrentContent()
+      .getBlockForKey(selection.getAnchorKey())
+      .getCharacterList()
+      .slice(selection.getStartOffset(), selection.getEndOffset())
+      .some(v => {
+        var entity = v.getEntity();
+        return !!entity && DraftEntity.get(entity).getType() === 'LINK';
+      });
+  },
+
+  getCurrentBlockType: function(editorState: EditorState): DraftBlockType {
+    var selection = editorState.getSelection();
+    return editorState.getCurrentContent()
+      .getBlockForKey(selection.getStartKey())
+      .getType();
+  },
+
+  getDataObjectForLinkURL: function(uri: URI): Object {
+    return {url: uri.toString()};
+  },
+
+  handleKeyCommand: function(
+    editorState: EditorState,
+    command: DraftEditorCommand
+  ): ?EditorState {
+    switch (command) {
+      case 'bold':
+        return RichTextEditorUtil.toggleInlineStyle(editorState, 'BOLD');
+      case 'italic':
+        return RichTextEditorUtil.toggleInlineStyle(editorState, 'ITALIC');
+      case 'underline':
+        return RichTextEditorUtil.toggleInlineStyle(editorState, 'UNDERLINE');
+      case 'code':
+        return RichTextEditorUtil.toggleCode(editorState);
+      case 'backspace':
+      case 'backspace-word':
+      case 'backspace-to-start-of-line':
+        return RichTextEditorUtil.onBackspace(editorState);
+      case 'delete':
+      case 'delete-word':
+      case 'delete-to-end-of-block':
+        return RichTextEditorUtil.onDelete(editorState);
+      default:
+        return null;
+    }
+  },
+
+  insertSoftNewline: function(editorState: EditorState): EditorState {
+    var contentState = DraftModifier.insertText(
+      editorState.getCurrentContent(),
+      editorState.getSelection(),
+      '\n',
+      editorState.getCurrentInlineStyle(),
+      null
+    );
+
+    return EditorState.push(
+      editorState,
+      contentState,
+      'insert-characters'
+    );
+  },
+
+  /**
+   * For collapsed selections at the start of styled blocks, backspace should
+   * just remove the existing style.
+   */
+  onBackspace: function(editorState: EditorState): ?EditorState {
+    var selection = editorState.getSelection();
+    if (
+      !selection.isCollapsed() ||
+      selection.getAnchorOffset() ||
+      selection.getFocusOffset()
+    ) {
+      return null;
+    }
+
+    // First, try to remove a preceding media block.
+    var content = editorState.getCurrentContent();
+    var startKey = selection.getStartKey();
+    var blockAfter = content.getBlockAfter(startKey);
+
+    // If the current block is empty, just delete it.
+    if (blockAfter && content.getBlockForKey(startKey).getLength() === 0) {
+      return null;
+    }
+
+    var blockBefore = content.getBlockBefore(startKey);
+
+    if (blockBefore && blockBefore.getType() === 'media') {
+      var mediaBlockTarget = selection.merge({
+        anchorKey: blockBefore.getKey(),
+        anchorOffset: 0,
+      });
+      var asCurrentStyle = DraftModifier.setBlockType(
+        content,
+        mediaBlockTarget,
+        content.getBlockForKey(startKey).getType()
+      );
+      var withoutMedia = DraftModifier.removeRange(
+        asCurrentStyle,
+        mediaBlockTarget,
+        'backward'
+      );
+      if (withoutMedia !== content) {
+        return EditorState.push(
+          editorState,
+          withoutMedia,
+          'remove-range'
+        );
+      }
+    }
+
+    // If that doesn't succeed, try to remove the current block style.
+    var withoutBlockStyle = RichTextEditorUtil.tryToRemoveBlockStyle(
+      editorState
+    );
+
+    if (withoutBlockStyle) {
+      return EditorState.push(
+        editorState,
+        withoutBlockStyle,
+        'change-block-type'
+      );
+    }
+
+    return null;
+  },
+
+  onDelete: function(editorState: EditorState): ?EditorState {
+    var selection = editorState.getSelection();
+    if (!selection.isCollapsed()) {
+      return null;
+    }
+
+    var content = editorState.getCurrentContent();
+    var startKey = selection.getStartKey();
+    var block = content.getBlockForKey(startKey);
+    var length = block.getLength();
+
+    // The cursor is somewhere within the text. Behave normally.
+    if (selection.getStartOffset() < length) {
+      return null;
+    }
+
+    var blockAfter = content.getBlockAfter(startKey);
+
+    if (!blockAfter || blockAfter.getType() !== 'media') {
+      return null;
+    }
+
+    // If the current block is empty, delete it.
+    if (length === 0) {
+      var target = selection.merge({
+        focusKey: blockAfter.getKey(),
+        focusOffset: 0,
+      });
+
+      var withoutEmptyBlock = DraftModifier.removeRange(
+        content,
+        target,
+        'forward'
+      );
+
+      var preserveMedia = DraftModifier.setBlockType(
+        withoutEmptyBlock,
+        withoutEmptyBlock.getSelectionAfter(),
+        'media'
+      );
+
+      return EditorState.push(editorState, preserveMedia, 'remove-range');
+    }
+
+    // Otherwise, delete the media block.
+    var mediaBlockTarget = selection.merge({
+      focusKey: blockAfter.getKey(),
+      focusOffset: blockAfter.getLength(),
+    });
+
+    var withoutMedia = DraftModifier.removeRange(
+      content,
+      mediaBlockTarget,
+      'forward'
+    );
+
+    if (withoutMedia !== content) {
+      return EditorState.push(
+        editorState,
+        withoutMedia,
+        'remove-range'
+      );
+    }
+
+    return null;
+  },
+
+  onTab: function(
+    event: SyntheticKeyboardEvent,
+    editorState: EditorState,
+    maxDepth: number
+  ): EditorState {
+    var selection = editorState.getSelection();
+    var key = selection.getAnchorKey();
+    if (key !== selection.getFocusKey()) {
+      return editorState;
+    }
+
+    var content = editorState.getCurrentContent();
+    var block = content.getBlockForKey(key);
+    var type = block.getType();
+    if (type !== 'unordered-list-item' && type !== 'ordered-list-item') {
+      return editorState;
+    }
+
+    event.preventDefault();
+
+    var blockAbove = content.getBlockBefore(key);
+    var depth = maxDepth;
+
+    // Only allow indenting one level beyond the block above, and only if
+    // the block above is a list item as well. Always allow unindenting
+    // if depth is greater than zero.
+    if (event.shiftKey) {
+      if (block.getDepth() === 0) {
+        return editorState;
+      }
+    } else {
+      if (!blockAbove || block.getDepth() === maxDepth) {
+        return editorState;
+      }
+
+      var typeAbove = blockAbove.getType();
+      if (
+        typeAbove !== 'unordered-list-item' &&
+        typeAbove !== 'ordered-list-item'
+      ) {
+        return editorState;
+      }
+
+      depth = Math.min(blockAbove.getDepth() + 1, maxDepth);
+    }
+
+    var withAdjustment = adjustBlockDepthForContentState(
+      content,
+      selection,
+      event.shiftKey ? -1 : 1,
+      depth
+    );
+
+    return EditorState.push(
+      editorState,
+      withAdjustment,
+      'adjust-depth'
+    );
+  },
+
+  toggleBlockType: function(
+    editorState: EditorState,
+    blockType: DraftBlockType
+  ): EditorState {
+    var selection = editorState.getSelection();
+    var startKey = selection.getStartKey();
+    var endKey = selection.getEndKey();
+    var content = editorState.getCurrentContent();
+
+    var hasMedia = content.getBlockMap()
+      .skipWhile((_, k) => k !== startKey)
+      .takeWhile((_, k) => k !== endKey)
+      .some(v => v.getType() === 'media');
+
+    if (hasMedia) {
+      return editorState;
+    }
+
+    var typeToSet = content.getBlockForKey(startKey).getType() === blockType ?
+      'unstyled' :
+      blockType;
+
+    return EditorState.push(
+      editorState,
+      DraftModifier.setBlockType(content, selection, typeToSet),
+      'change-block-type'
+    );
+  },
+
+  toggleCode: function(editorState: EditorState): EditorState {
+    var selection = editorState.getSelection();
+    var anchorKey = selection.getAnchorKey();
+    var focusKey = selection.getFocusKey();
+
+    if (selection.isCollapsed() || anchorKey !== focusKey) {
+      return RichTextEditorUtil.toggleBlockType(editorState, 'code-block');
+    }
+
+    return RichTextEditorUtil.toggleInlineStyle(editorState, 'CODE');
+  },
+
+  /**
+   * Toggle the specified inline style for the selection. If the
+   * user's selection is collapsed, apply or remove the style for the
+   * internal state. If it is not collapsed, apply the change directly
+   * to the document state.
+   */
+  toggleInlineStyle: function(
+    editorState: EditorState,
+    inlineStyle: string
+  ): EditorState {
+    var selection = editorState.getSelection();
+    var currentStyle = editorState.getCurrentInlineStyle();
+
+    // If the selection is collapsed, toggle the specified style on or off and
+    // set the result as the new inline style override. This will then be
+    // used as the inline style for the next character to be inserted.
+    if (selection.isCollapsed()) {
+      return EditorState.set(editorState, {
+        inlineStyleOverride:
+          currentStyle.has(inlineStyle)
+            ? currentStyle.remove(inlineStyle)
+            : currentStyle.add(inlineStyle),
+      });
+    }
+
+    // If characters are selected, immediately apply or remove the
+    // inline style on the document state itself.
+    var content = editorState.getCurrentContent();
+    var newContent;
+
+    // If the style is already present for the selection range, remove it.
+    // Otherwise, apply it.
+    if (currentStyle.has(inlineStyle)) {
+      newContent = DraftModifier.removeInlineStyle(
+        content,
+        selection,
+        inlineStyle,
+      );
+    } else {
+      newContent = DraftModifier.applyInlineStyle(
+        content,
+        selection,
+        inlineStyle,
+      );
+    }
+
+    return EditorState.push(
+      editorState,
+      newContent,
+      'change-inline-style'
+    );
+  },
+
+  toggleLink: function(
+    editorState: EditorState,
+    targetSelection: SelectionState,
+    entityKey: ?string
+  ): EditorState {
+    var withoutLink = DraftModifier.applyEntity(
+      editorState.getCurrentContent(),
+      targetSelection,
+      entityKey
+    );
+
+    return EditorState.push(
+      editorState,
+      withoutLink,
+      'apply-entity'
+    );
+  },
+
+  /**
+   * When a collapsed cursor is at the start of an empty styled block, allow
+   * certain key commands (newline, backspace) to simply change the
+   * style of the block instead of the default behavior.
+   */
+  tryToRemoveBlockStyle: function(editorState: EditorState): ?ContentState {
+    var selection = editorState.getSelection();
+    var offset = selection.getAnchorOffset();
+    if (selection.isCollapsed() && offset === 0) {
+      var key = selection.getAnchorKey();
+      var content = editorState.getCurrentContent();
+      var block = content.getBlockForKey(key);
+      if (block.getLength() > 0) {
+        return null;
+      }
+
+      var type = block.getType();
+      if (type !== 'unstyled' && type !== 'code-block') {
+        return DraftModifier.setBlockType(content, selection, 'unstyled');
+      }
+    }
+    return null;
+  },
+};
+
+module.exports = RichTextEditorUtil;
diff --git a/src/model/modifier/__tests__/DraftRemovableWord-test.js b/src/model/modifier/__tests__/DraftRemovableWord-test.js
new file mode 100644
index 0000000..95601d2
--- /dev/null
+++ b/src/model/modifier/__tests__/DraftRemovableWord-test.js
@@ -0,0 +1,140 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails isaac, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var DraftRemovableWord = require('DraftRemovableWord');
+
+describe('DraftRemovableWord', function() {
+  var forward;
+  var backward;
+
+  var english = 'the animals';
+  var accents = 'th\u00e9 f\u00e0bregas';
+  var arabic = '\u0637\u0638\u0639 \u063A\u063B\u063C';
+  var japanese = '\u4f1a\u8b70\u4e2d \u30cf\u30c3\u30b7\u30e5';
+  var korean = '\ud2b8\uc704\ud130 \ud2b8\uc704\ud130';
+  var halfWidthHangul = '\uffa3\uffa6\uffb0 \uffa1\uffa2\uffb2';
+  var cyrillic = '\u0430\u0448\u043e\u043a \u0431\u0414\u0416\u0426';
+  var withNumbers = 'f14 tomcat';
+
+  beforeEach(function() {
+    jest.resetModuleRegistry();
+    forward = DraftRemovableWord.getForward;
+    backward = DraftRemovableWord.getBackward;
+  });
+
+  it('must identify words looking forward', function() {
+    expect(forward(english)).toBe(english.split(' ')[0]);
+    expect(forward(accents)).toBe(accents.split(' ')[0]);
+    expect(forward(arabic)).toBe(arabic.split(' ')[0]);
+    expect(forward(japanese)).toBe(japanese.split(' ')[0]);
+    expect(forward(korean)).toBe(korean.split(' ')[0]);
+    expect(forward(halfWidthHangul)).toBe(halfWidthHangul.split(' ')[0]);
+    expect(forward(cyrillic)).toBe(cyrillic.split(' ')[0]);
+    expect(forward(withNumbers)).toBe(withNumbers.split(' ')[0]);
+  });
+
+  it('must identify words with apostrophes looking forward', function() {
+    expect(forward('you\'re correct.')).toBe('you\'re');
+  });
+
+  it('must identify words with curly quotes looking forward', function() {
+    expect(forward('you\u2019re correct.')).toBe('you\u2019re');
+  });
+
+  it('must identify punctuation with apostrophes', function() {
+    expect(forward("'hello'")).toBe("'hello");
+    expect(backward("'hello'")).toBe("hello'");
+    expect(forward('\u2018hello\u2019')).toBe('\u2018hello');
+    expect(backward('\u2018hello\u2019')).toBe('hello\u2019');
+
+    expect(forward("('hello')")).toBe("('hello");
+    expect(backward("('hello')")).toBe("hello')");
+    expect(forward(".'.")).toBe(".'.");
+    expect(backward(".'.")).toBe(".'.");
+  });
+
+  it('must identify words with underscores', () => {
+    expect(forward('under_score hey')).toBe('under_score');
+    expect(backward('hey under_score')).toBe('under_score');
+  });
+
+  it('must identify words led by spaces looking forward', function() {
+    expect(forward('  ' + english)).toBe('  ' + english.split(' ')[0]);
+    expect(forward('    ' + accents)).toBe('    ' + accents.split(' ')[0]);
+    expect(forward('      ' + arabic)).toBe('      ' + arabic.split(' ')[0]);
+  });
+
+  it('must identify words led by punctuation looking forward', function() {
+    var match = english.split(' ')[0];
+    expect(forward('.' + english)).toBe('.' + match);
+    expect(forward('|' + english)).toBe('|' + match);
+    expect(forward('^' + english)).toBe('^' + match);
+    expect(forward('\u060d\uFD3e\uFD3F' + english)).toBe(
+      '\u060d\uFD3e\uFD3F' + match
+    );
+    expect(forward('.. .. ..' + english)).toBe('.. .. ..' + match);
+  });
+
+  it('must identify punct/whitespace strings looking forward', function() {
+    expect(forward('    ')).toBe('    ');
+    expect(forward('\u060d\uFD3e\uFD3F')).toBe('\u060d\uFD3e\uFD3F');
+    expect(forward('. . . . .')).toBe('. . . . .');
+    expect(forward('   !!!???   ')).toBe('   !!!???   ');
+  });
+
+  it('must identify words looking backward', function() {
+    expect(backward(english)).toBe(english.split(' ')[1]);
+    expect(backward(accents)).toBe(accents.split(' ')[1]);
+    expect(backward(arabic)).toBe(arabic.split(' ')[1]);
+    expect(backward(japanese)).toBe(japanese.split(' ')[1]);
+    expect(backward(korean)).toBe(korean.split(' ')[1]);
+    expect(backward(halfWidthHangul)).toBe(halfWidthHangul.split(' ')[1]);
+    expect(backward(cyrillic)).toBe(cyrillic.split(' ')[1]);
+    expect(backward(withNumbers)).toBe(withNumbers.split(' ')[1]);
+  });
+
+  it('must identify words with apostrophes looking backward', function() {
+    expect(backward('you don\'t')).toBe('don\'t');
+  });
+
+  it('must identify words ended by spaces looking backward', function() {
+    expect(backward(english + '  ')).toBe(english.split(' ')[1] + '  ');
+    expect(backward(accents + '    ')).toBe(accents.split(' ')[1] + '    ');
+    expect(backward(arabic + '      ')).toBe(arabic.split(' ')[1] + '      ');
+  });
+
+  it('must identify words ended by punctuation looking backward', function() {
+    var match = english.split(' ')[1];
+    expect(backward(english + '.')).toBe(match + '.');
+    expect(backward(english + '|')).toBe(match + '|');
+    expect(backward(english + '^')).toBe(match + '^');
+    expect(backward(english + '\u060d\uFD3e\uFD3F')).toBe(
+      match + '\u060d\uFD3e\uFD3F'
+    );
+    expect(backward(english + '.. .. ..')).toBe(match + '.. .. ..');
+  });
+
+  it('must identify punct/whitespace strings looking backward', function() {
+    expect(backward('    ')).toBe('    ');
+    expect(backward('\u060d\uFD3e\uFD3F')).toBe('\u060d\uFD3e\uFD3F');
+    expect(backward('. . . . .')).toBe('. . . . .');
+    expect(backward('   !!!???   ')).toBe('   !!!???   ');
+  });
+
+  it('must identify nothing in an empty string', function() {
+    expect(forward('')).toBe('');
+    expect(backward('')).toBe('');
+  });
+});
diff --git a/src/model/modifier/getCharacterRemovalRange.js b/src/model/modifier/getCharacterRemovalRange.js
new file mode 100644
index 0000000..556865f
--- /dev/null
+++ b/src/model/modifier/getCharacterRemovalRange.js
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getCharacterRemovalRange
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var DraftEntity = require('DraftEntity');
+var DraftEntitySegments = require('DraftEntitySegments');
+
+var getRangesForDraftEntity = require('getRangesForDraftEntity');
+var invariant = require('invariant');
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftRemovalDirection} from 'DraftRemovalDirection';
+import type SelectionState from 'SelectionState';
+
+/**
+ * Given a SelectionState and a removal direction, determine the entire range
+ * that should be removed from a ContentState. This is based on any entities
+ * within the target, with their `mutability` values taken into account.
+ *
+ * For instance, if we are attempting to remove part of an "immutable" entity
+ * range, the entire entity must be removed. The returned `SelectionState`
+ * will be adjusted accordingly.
+ */
+function getCharacterRemovalRange(
+  block: ContentBlock,
+  selectionState: SelectionState,
+  direction: DraftRemovalDirection
+): SelectionState {
+  var start = selectionState.getStartOffset();
+  var end = selectionState.getEndOffset();
+  var entityKey = block.getEntityAt(start);
+  if (!entityKey) {
+    return selectionState;
+  }
+
+  var entity = DraftEntity.get(entityKey);
+  var mutability = entity.getMutability();
+
+  // `MUTABLE` entities can just have the specified range of text removed
+  // directly. No adjustments are needed.
+  if (mutability === 'MUTABLE') {
+    return selectionState;
+  }
+
+  // Find the entity range that overlaps with our removal range.
+  var entityRanges = getRangesForDraftEntity(block, entityKey).filter(
+    (range) => start < range.end && end > range.start
+  );
+
+  invariant(
+    entityRanges.length == 1,
+    'There should only be one entity range within this removal range.'
+  );
+
+  var entityRange = entityRanges[0];
+
+  // For `IMMUTABLE` entity types, we will remove the entire entity range.
+  if (mutability === 'IMMUTABLE') {
+    return selectionState.merge({
+      anchorOffset: entityRange.start,
+      focusOffset: entityRange.end,
+      isBackward: false,
+    });
+  }
+
+  // For `SEGMENTED` entity types, determine the appropriate segment to
+  // remove.
+  var removalRange = DraftEntitySegments.getRemovalRange(
+    start,
+    end,
+    block.getText().slice(entityRange.start, entityRange.end),
+    entityRange.start,
+    direction
+  );
+
+  return selectionState.merge({
+    anchorOffset: removalRange.start,
+    focusOffset: removalRange.end,
+    isBackward: false,
+  });
+}
+
+module.exports = getCharacterRemovalRange;
diff --git a/src/model/modifier/getRangesForDraftEntity.js b/src/model/modifier/getRangesForDraftEntity.js
new file mode 100644
index 0000000..4f9937e
--- /dev/null
+++ b/src/model/modifier/getRangesForDraftEntity.js
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getRangesForDraftEntity
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var invariant = require('invariant');
+
+import type ContentBlock from 'ContentBlock';
+import type {DraftRange} from 'DraftRange';
+
+/**
+ * Obtain the start and end positions of the range that has the
+ * specified entity applied to it.
+ *
+ * Entity keys are applied only to contiguous stretches of text, so this
+ * method searches for the first instance of the entity key and returns
+ * the subsequent range.
+ */
+function getRangesForDraftEntity(
+  block: ContentBlock,
+  key: string
+): Array<DraftRange> {
+  var ranges = [];
+  block.findEntityRanges(
+    c => c.getEntity() === key,
+    (start, end) => {
+      ranges.push({start, end});
+    }
+  );
+
+  invariant(
+    !!ranges.length,
+    'Entity key not found in this range.'
+  );
+
+  return ranges;
+}
+
+module.exports = getRangesForDraftEntity;
diff --git a/src/model/paste/DraftPasteProcessor.js b/src/model/paste/DraftPasteProcessor.js
new file mode 100644
index 0000000..4ba6e31
--- /dev/null
+++ b/src/model/paste/DraftPasteProcessor.js
@@ -0,0 +1,472 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule DraftPasteProcessor
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var DraftEntity = require('DraftEntity');
+var Immutable = require('immutable');
+var URI = require('URI');
+
+var generateBlockKey = require('generateBlockKey');
+var getSafeBodyFromHTML = require('getSafeBodyFromHTML');
+var invariant = require('invariant');
+var nullthrows = require('nullthrows');
+var sanitizeDraftText = require('sanitizeDraftText');
+
+import type {DraftBlockType} from 'DraftBlockType';
+import type {DraftInlineStyle} from 'DraftInlineStyle';
+
+var {
+  List,
+  OrderedSet,
+  Repeat,
+} = Immutable;
+
+var NBSP = '&nbsp;';
+var SPACE = ' ';
+
+// Corresponds to max indent in campfire editor
+var MAX_DEPTH = 4;
+
+// used for replacing characters in HTML
+var REGEX_CR = new RegExp('\r', 'g');
+var REGEX_LF = new RegExp('\n', 'g');
+var REGEX_NBSP = new RegExp(NBSP, 'g');
+
+// Block tag flow is different because LIs do not have
+// a deterministic style ;_;
+var blockTags = ['p', 'h1', 'h2', 'h3', 'li', 'blockquote', 'pre'];
+var inlineTags = {
+  b: 'BOLD',
+  code: 'CODE',
+  del: 'STRIKETHROUGH',
+  em: 'ITALIC',
+  i: 'ITALIC',
+  s: 'STRIKETHROUGH',
+  strike: 'STRIKETHROUGH',
+  strong: 'BOLD',
+  u: 'UNDERLINE',
+};
+
+var lastBlock;
+
+type Block = {
+  type: DraftBlockType;
+  depth: number;
+};
+
+type Chunk = {
+  text: string;
+  inlines: Array<DraftInlineStyle>;
+  entities: Array<string>;
+  blocks: Array<Block>;
+};
+
+function getEmptyChunk(): Chunk {
+  return {
+    text: '',
+    inlines: [],
+    entities: [],
+    blocks: [],
+  };
+}
+
+function getWhitespaceChunk(inEntity: ?string): Chunk {
+  var entities = new Array(1);
+  if (inEntity) {
+    entities[0] = inEntity;
+  }
+  return {
+    text: SPACE,
+    inlines: [OrderedSet()],
+    entities,
+    blocks: [],
+  };
+}
+
+function getSoftNewlineChunk(): Chunk {
+  return {
+    text: '\n',
+    inlines: [OrderedSet()],
+    entities: new Array(1),
+    blocks: [],
+  };
+}
+
+function getBlockDividerChunk(block: DraftBlockType, depth: number): Chunk {
+  return {
+    text: '\r',
+    inlines: [OrderedSet()],
+    entities: new Array(1),
+    blocks: [{
+      type: block,
+      depth: Math.max(0, Math.min(MAX_DEPTH, depth)),
+    }],
+  };
+}
+
+function getBlockTypeForTag(tag: string, lastList: ?string): DraftBlockType {
+  switch (tag) {
+    case 'h1':
+      return 'header-one';
+    case 'h2':
+      return 'header-two';
+    case 'li':
+      if (lastList === 'ol') {
+        return 'ordered-list-item';
+      }
+      return 'unordered-list-item';
+    case 'blockquote':
+      return 'blockquote';
+    case 'pre':
+      return 'code-block';
+    default:
+      return 'unstyled';
+  }
+}
+
+function processInlineTag(
+  tag: string,
+  node: Node,
+  currentStyle: DraftInlineStyle
+): DraftInlineStyle {
+  var styleToCheck = inlineTags[tag];
+  if (styleToCheck) {
+    currentStyle = currentStyle.add(styleToCheck).toOrderedSet();
+  } else if (node instanceof HTMLElement) {
+    const htmlElement = node;
+    currentStyle = currentStyle.withMutations(style => {
+      if (htmlElement.style.fontWeight === 'bold') {
+        style.add('BOLD');
+      }
+
+      if (htmlElement.style.fontStyle === 'italic') {
+        style.add('ITALIC');
+      }
+
+      if (htmlElement.style.textDecoration === 'underline') {
+        style.add('UNDERLINE');
+      }
+
+      if (htmlElement.style.textDecoration === 'line-through') {
+        style.add('STRIKETHROUGH');
+      }
+    }).toOrderedSet();
+  }
+  return currentStyle;
+}
+
+function joinChunks(A: Chunk, B: Chunk): Chunk {
+  // Sometimes two blocks will touch in the DOM and we need to strip the
+  // extra delimiter to preserve niceness.
+  var lastInB = B.text.slice(0, 1);
+
+  if (
+    A.text.slice(-1) === '\r' &&
+    lastInB === '\r'
+  ) {
+    A.text = A.text.slice(0, -1);
+    A.inlines.pop();
+    A.entities.pop();
+    A.blocks.pop();
+  }
+
+  // Kill whitespace after blocks
+  if (
+    A.text.slice(-1) === '\r'
+  ) {
+    if (B.text === SPACE || B.text === '\n') {
+      return A;
+    } else if (lastInB === SPACE || lastInB === '\n') {
+      B.text = B.text.slice(1);
+      B.inlines.shift();
+      B.entities.shift();
+    }
+  }
+
+  return {
+    text: A.text + B.text,
+    inlines: A.inlines.concat(B.inlines),
+    entities: A.entities.concat(B.entities),
+    blocks: A.blocks.concat(B.blocks),
+  };
+}
+
+/**
+ * Check to see if we have anything like <p> <blockquote> <h1>... to create
+ * block tags from. If we do, we can use those and ignore <div> tags. If we
+ * don't, we can treat <div> tags as meaningful (unstyled) blocks.
+ */
+function containsSemanticBlockMarkup(html: string): boolean {
+  return blockTags.some(tag => html.indexOf('<' + tag) !== -1);
+}
+
+function hasValidLinkText(link: Node): boolean {
+  invariant(
+    link instanceof HTMLAnchorElement,
+    'Link must be an HTMLAnchorElement.'
+  );
+  var protocol = link.protocol;
+  return protocol === 'http:' || protocol === 'https:';
+}
+
+function genFragment(
+  node: Node,
+  inlineStyle: DraftInlineStyle,
+  lastList: string,
+  inBlock: ?string,
+  blockTags: Array<string>,
+  depth: number,
+  inEntity?: string
+): Chunk {
+  var nodeName = node.nodeName.toLowerCase();
+  var newBlock = false;
+  var nextBlockType = 'unstyled';
+  var lastLastBlock = lastBlock;
+
+  // Base Case
+  if (nodeName === '#text') {
+    var text = node.textContent;
+    if (text.trim() === '' && inBlock !== 'pre') {
+      return getWhitespaceChunk(inEntity);
+    }
+    if (inBlock !== 'pre') {
+      // Can't use empty string because MSWord
+      text = text.replace(REGEX_LF, SPACE);
+    }
+
+    // save the last block so we can use it later
+    lastBlock = nodeName;
+
+    return {
+      text,
+      inlines: Array(text.length).fill(inlineStyle),
+      entities: Array(text.length).fill(inEntity),
+      blocks: [],
+    };
+  }
+
+  // save the last block so we can use it later
+  lastBlock = nodeName;
+
+  // BR tags
+  if (nodeName === 'br') {
+    if (
+        lastLastBlock === 'br' &&
+        (!inBlock || getBlockTypeForTag(inBlock, lastList) === 'unstyled')
+    ) {
+      return getBlockDividerChunk('unstyled', depth);
+    }
+    return getSoftNewlineChunk();
+  }
+
+  var chunk = getEmptyChunk();
+  var newChunk: ?Chunk = null;
+
+  // Inline tags
+  inlineStyle = processInlineTag(nodeName, node, inlineStyle);
+
+  // Handle lists
+  if (nodeName === 'ul' || nodeName === 'ol') {
+    if (lastList) {
+      depth += 1;
+    }
+    lastList = nodeName;
+  }
+
+  // Block Tags
+  if (!inBlock && blockTags.indexOf(nodeName) !== -1) {
+    chunk = getBlockDividerChunk(
+      getBlockTypeForTag(nodeName, lastList),
+      depth
+    );
+    inBlock = nodeName;
+    newBlock = true;
+  } else if (lastList && inBlock === 'li' && nodeName === 'li') {
+    chunk = getBlockDividerChunk(
+      getBlockTypeForTag(nodeName, lastList),
+      depth
+    );
+    inBlock = nodeName;
+    newBlock = true;
+    nextBlockType = lastList === 'ul' ?
+      'unordered-list-item' :
+      'ordered-list-item';
+  }
+
+  // Recurse through children
+  var child: ?Node = node.firstChild;
+  if (child != null) {
+    nodeName = child.nodeName.toLowerCase();
+  }
+
+  var entityId: ?string = null;
+  var href: ?string = null;
+
+  while (child) {
+    if (nodeName === 'a' && child.href && hasValidLinkText(child)) {
+      href = new URI(child.href).toString();
+      entityId = DraftEntity.create('LINK', 'MUTABLE', {url: href});
+    } else {
+      entityId = undefined;
+    }
+
+    newChunk = genFragment(
+      child,
+      inlineStyle,
+      lastList,
+      inBlock,
+      blockTags,
+      depth,
+      entityId || inEntity
+    );
+
+    chunk = joinChunks(chunk, newChunk);
+    var sibling: Node = child.nextSibling;
+
+    // Put in a newline to break up blocks inside blocks
+    if (
+      sibling &&
+      blockTags.indexOf(nodeName) >= 0 &&
+      inBlock
+    ) {
+      chunk = joinChunks(chunk, getSoftNewlineChunk());
+    }
+    if (sibling) {
+      nodeName = sibling.nodeName.toLowerCase();
+    }
+    child = sibling;
+  }
+
+  if (newBlock) {
+    chunk = joinChunks(
+      chunk,
+      getBlockDividerChunk(nextBlockType, depth)
+    );
+  }
+
+  return chunk;
+}
+
+function getChunkForHTML(html: string): ?Chunk {
+  html = html
+    .trim()
+    .replace(REGEX_CR, '')
+    .replace(REGEX_NBSP, SPACE);
+
+  var safeBody = getSafeBodyFromHTML(html);
+  if (!safeBody) {
+    return null;
+  }
+  lastBlock = null;
+
+  // Sometimes we aren't dealing with content that contains nice semantic
+  // tags. In this case, use divs to separate everything out into paragraphs
+  // and hope for the best.
+  var workingBlocks = containsSemanticBlockMarkup(html) ? blockTags : ['div'];
+
+  // Start with -1 block depth to offset the fact that we are passing in a fake
+  // UL block to start with.
+  var chunk =
+    genFragment(safeBody, OrderedSet(), 'ul', null, workingBlocks, -1);
+
+  // join with previous block to prevent weirdness on paste
+  if (chunk.text.indexOf('\r') === 0) {
+    chunk = {
+      text: chunk.text.slice(1),
+      inlines: chunk.inlines.slice(1),
+      entities: chunk.entities.slice(1),
+      blocks: chunk.blocks,
+    };
+  }
+
+  // Kill block delimiter at the end
+  if (chunk.text.slice(-1) === '\r') {
+    chunk.text = chunk.text.slice(0, -1);
+    chunk.inlines = chunk.inlines.slice(0, -1);
+    chunk.entities = chunk.entities.slice(0, -1);
+    chunk.blocks.pop();
+  }
+
+  // If we saw no block tags, put an unstyled one in
+  if (chunk.blocks.length === 0) {
+    chunk.blocks.push({type: 'unstyled', depth: 0});
+  }
+
+  // Sometimes we start with text that isn't in a block, which is then
+  // followed by blocks. Need to fix up the blocks to add in
+  // an unstyled block for this content
+  if (chunk.text.split('\r').length === chunk.blocks.length + 1) {
+    chunk.blocks.unshift({type: 'unstyled', depth: 0});
+  }
+
+  return chunk;
+}
+
+var DraftPasteProcessor = {
+  processHTML(html: string): ?Array<ContentBlock> {
+    var chunk = getChunkForHTML(html);
+    if (chunk == null) {
+      return null;
+    }
+    var start = 0;
+    return chunk.text.split('\r').map(
+      (textBlock, ii) => {
+        // Make absolutely certain that our text is acceptable.
+        textBlock = sanitizeDraftText(textBlock);
+        var end = start + textBlock.length;
+        var inlines = nullthrows(chunk).inlines.slice(start, end);
+        var entities = nullthrows(chunk).entities.slice(start, end);
+        var characterList = List(
+          inlines.map((style, ii) => {
+            var data = {style, entity: (null: ?string)};
+            if (entities[ii]) {
+              data.entity = entities[ii];
+            }
+            return CharacterMetadata.create(data);
+          })
+        );
+        start = end + 1;
+
+        return new ContentBlock({
+          key: generateBlockKey(),
+          type: nullthrows(chunk).blocks[ii].type,
+          depth: nullthrows(chunk).blocks[ii].depth,
+          text: textBlock,
+          characterList,
+        });
+      }
+    );
+  },
+
+  processText(
+    textBlocks: Array<string>,
+    character: CharacterMetadata,
+  ): Array<ContentBlock> {
+    return textBlocks.map(
+      textLine => {
+        textLine = sanitizeDraftText(textLine);
+        return new ContentBlock({
+          key: generateBlockKey(),
+          type: 'unstyled',
+          text: textLine,
+          characterList: List(Repeat(character, textLine.length)),
+        });
+      }
+    );
+  },
+};
+
+module.exports = DraftPasteProcessor;
diff --git a/src/model/paste/__mocks__/getSafeBodyFromHTML.js b/src/model/paste/__mocks__/getSafeBodyFromHTML.js
new file mode 100644
index 0000000..6bc31a0
--- /dev/null
+++ b/src/model/paste/__mocks__/getSafeBodyFromHTML.js
@@ -0,0 +1,12 @@
+// Copyright 2004-present Facebook. All Rights Reserved.
+
+// THIS IS PURELY A MOCK TO GET AROUND THE TEST FRAMEWORK
+// Never use this for anything else ever.
+function getUnsafeBodyFromHTML(html) {
+  var fragment = document.createElement('body');
+  var match = html.match(/<body>(.*?)<\/body>/);
+  fragment.innerHTML = match ? match[1] : html;
+  return fragment;
+}
+
+module.exports = getUnsafeBodyFromHTML;
diff --git a/src/model/paste/__tests__/DraftPasteProcessor-test.js b/src/model/paste/__tests__/DraftPasteProcessor-test.js
new file mode 100644
index 0000000..b9c9091
--- /dev/null
+++ b/src/model/paste/__tests__/DraftPasteProcessor-test.js
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails azelenskiy, oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var DraftEntity = require('DraftEntity');
+
+var DraftPasteProcessor = require('DraftPasteProcessor');
+
+describe('DraftPasteProcessor', function() {
+  function assertInlineStyles(block, comparison) {
+    var styles = block.getCharacterList().map(c => c.getStyle());
+    expect(styles.toJS()).toEqual(comparison);
+  }
+
+  // Don't want to couple this to a specific way of generating entity IDs so
+  // just checking their existance
+  function assertEntities(block, comparison) {
+    var entities = block.getCharacterList().map(c => c.getEntity());
+    entities.toJS().forEach((entity, ii) => {
+      expect(comparison[ii]).toBe(!!entity);
+    });
+  }
+
+  function assertDepths(blocks, comparison) {
+    expect(
+      blocks.map(b => b.getDepth())
+    ).toEqual(
+      comparison
+    );
+  }
+
+  function assertBlockTypes(blocks, comparison) {
+    expect(
+      blocks.map(b => b.getType())
+    ).toEqual(
+      comparison
+    );
+  }
+
+  it('must identify italics text', function() {
+    var html = '<i>hello</i> hi';
+    var output = DraftPasteProcessor.processHTML(html);
+    var block = output[0];
+    expect(block.getType()).toBe('unstyled');
+    assertInlineStyles(block, [
+      ['ITALIC'],
+      ['ITALIC'],
+      ['ITALIC'],
+      ['ITALIC'],
+      ['ITALIC'],
+      [],
+      [],
+      [],
+    ]);
+    expect(block.getText()).toBe('hello hi');
+  });
+
+  it('must identify overlapping inline styles', function() {
+    var html = '<i><b>he</b>hi</i>';
+    var output = DraftPasteProcessor.processHTML(html);
+    var block = output[0];
+    expect(block.getType()).toBe('unstyled');
+    assertInlineStyles(block, [
+      ['ITALIC', 'BOLD'],
+      ['ITALIC', 'BOLD'],
+      ['ITALIC'],
+      ['ITALIC'],
+    ]);
+    expect(block.getText()).toBe('hehi');
+  });
+
+  it('must identify block styles', function() {
+    var html = '<ol><li>hi</li><li>there</li></ol>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'ordered-list-item',
+      'ordered-list-item',
+    ]);
+  });
+
+  it('must collapse nested blocks to the topmost level', function() {
+    var html = '<ul><li><h2>what</h2></li></ul>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unordered-list-item',
+    ]);
+  });
+
+  /**
+   * todo: azelenskiy
+   * Changes to the mocked DOM appear to have broken this.
+   *
+   * it('must suppress blocks nested inside other blocks', function() {
+   *   var html = '<p><h2>Some text here</h2> more text here </p>';
+   *   var output = DraftPasteProcessor.processHTML(html);
+   *   assertBlockTypes(output, [
+   *     'unstyled',
+   *   ]);
+   * });
+   */
+
+  it('must detect two touching blocks', function() {
+    var html = '<h1>hi</h1>        <h2>hi</h2>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'header-one',
+      'header-two',
+    ]);
+  });
+
+  it('must insert a block when needed', function() {
+    var html = ' <h1> hi </h1><h1> </h1><span> whatever </span> <h2>hi </h2> ';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'header-one',
+      'unstyled',
+      'header-two',
+    ]);
+  });
+
+  it('must not generate fake blocks on heavy nesting', function() {
+    var html = '<p><span><span><span>Word</span></span></span>' +
+      '<span><span>,</span></span></p>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['unstyled']);
+  });
+
+  it('must preserve spaces', function() {
+    var html, output;
+
+    html = '<span>hello</span> <span>hi</span>';
+    output = DraftPasteProcessor.processHTML(html);
+    expect(output.length).toEqual(1);
+    assertBlockTypes(output, ['unstyled']);
+    var block = output[0];
+    expect(block.getText()).toBe('hello hi');
+
+    html = '<span>hello </span><span>hi</span>';
+    output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toBe('hello hi');
+
+    html = '<span>hello</span><span> hi</span>';
+    output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toEqual('hello hi');
+  });
+
+  it('must treat divs as Ps when we do not have semantic markup', function() {
+    var html = '<div>hi</div><div>hello</div>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+  });
+
+  it('must NOT treat divs as Ps when we pave Ps', function() {
+    var html = '<div><p>hi</p><p>hello</p></div>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+  });
+
+  it('must replace br tags with soft newlines', function() {
+    var html = 'hi<br>hello';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toBe('hi\nhello');
+  });
+
+  it('must split unstyled blocks on two br tags', function() {
+    var html = 'hi<br><br>hello';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+    html = '<p>hi<br><br>hello</p>';
+    output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+  });
+
+  it('must NOT split unstyled blocks inside a styled block', function() {
+    var html = '<pre>hi<br><br>hello</pre>';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['code-block']);
+  });
+
+  it('must split unstyled blocks on two br tags', function() {
+    var html = 'hi<br><br>hello';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText().length).toBe(3);
+    expect(output[1].getText()).toBe('hello');
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+  });
+
+  it('must replace newlines in regular tags', function() {
+    var html = '<div>hello\nthere</div>';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toBe('hello there');
+  });
+
+  it('must preserve newlines in pre tags', function() {
+    var html = '<pre>hello\nthere</pre>';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toBe('hello\nthere');
+  });
+
+  it('must preserve newlines in whitespace in pre tags', function() {
+    var html = '<pre><span>hello</span>\n<span>there</span></pre>';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[0].getText()).toBe('hello\nthere');
+    assertBlockTypes(output, ['code-block']);
+  });
+
+  it('must parse based on style attribute', function() {
+    var html = '<span style="font-weight: bold;">Bold '
+      + '<span style="font-style: italic;">Italic</span></span>.';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['unstyled']);
+    assertInlineStyles(output[0], [
+      ['BOLD'],
+      ['BOLD'],
+      ['BOLD'],
+      ['BOLD'],
+      ['BOLD'],
+      ['BOLD', 'ITALIC'],
+      ['BOLD', 'ITALIC'],
+      ['BOLD', 'ITALIC'],
+      ['BOLD', 'ITALIC'],
+      ['BOLD', 'ITALIC'],
+      ['BOLD', 'ITALIC'],
+      [],
+    ]);
+    expect(output[0].getText()).toBe('Bold Italic.');
+  });
+
+  it('must detect links in pasted content', function() {
+    var html = 'This is a <a href="http://www.facebook.com">link</a>, yep.';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['unstyled']);
+    assertEntities(
+      output[0],
+      Array(10).fill(false).concat(Array(4).fill(true), Array(6).fill(false))
+    );
+    expect(output[0].getText()).toBe('This is a link, yep.');
+    var entityId = output[0].getCharacterList().get(12).getEntity();
+    var entity = DraftEntity.get(entityId);
+    expect(entity.getData().url).toBe('http://www.facebook.com/');
+  });
+
+  it('must preserve styles inside links in a good way', function() {
+    var html = 'A <a href="http://www.facebook.com"><i>cool</i> link</a>, yep.';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['unstyled']);
+    assertInlineStyles(
+      output[0],
+      Array(2).fill([]).concat(Array(4).fill(['ITALIC']), Array(11).fill([]))
+    );
+    assertEntities(
+      output[0],
+      Array(2).fill(false).concat(Array(9).fill(true), Array(6).fill(false))
+    );
+    expect(output[0].getText()).toBe('A cool link, yep.');
+  });
+
+  it('must ignore links that do not actually link anywhere', function() {
+    var html = 'This is a <a>link</a>, yep.';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, ['unstyled']);
+    assertEntities(output[0], Array(20).fill(false));
+    expect(output[0].getText()).toBe('This is a link, yep.');
+  });
+
+  it('Tolerate doule BR tags separated by whitespace', function() {
+    var html = 'hi<br>  <br>hello';
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+    html = '<p>hi<br> <br>hello</p>';
+    output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unstyled',
+    ]);
+
+    html = '<p>hi<br> good stuff here <br>hello</p>';
+    output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+    ]);
+  });
+
+  it('Strip whitespace after block dividers', function() {
+    var html = '<p>hello</p> <p> what</p>';
+    var output = DraftPasteProcessor.processHTML(html);
+    expect(output[1].getText()).toBe('what');
+  });
+
+  it('must preserve list formatting', function() {
+    var html = `
+      what
+      <ul>
+          <li>what</li>
+          <li>
+              what
+              <ol>
+                  <li>one</li>
+                  <li>two</li>
+              </ol>
+          </li>
+          <li>what</li>
+      </ul>
+    `;
+    var output = DraftPasteProcessor.processHTML(html);
+    assertBlockTypes(output, [
+      'unstyled',
+      'unordered-list-item',
+      'unordered-list-item',
+      'ordered-list-item',
+      'ordered-list-item',
+      'unordered-list-item',
+    ]);
+    assertDepths(output, [0, 0, 0, 1, 1, 0]);
+  });
+});
diff --git a/src/model/paste/getSafeBodyFromHTML.js b/src/model/paste/getSafeBodyFromHTML.js
new file mode 100644
index 0000000..d3ae05b
--- /dev/null
+++ b/src/model/paste/getSafeBodyFromHTML.js
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getSafeBodyFromHTML
+ * @flow
+ */
+
+'use strict';
+
+var UserAgent = require('UserAgent');
+
+var isOldIE = UserAgent.isBrowser('IE <= 9');
+
+// Provides a dom node that will not execute scripts
+// https://developer.mozilla.org/en-US/docs/Web/API/DOMImplementation.createHTMLDocument
+// https://developer.mozilla.org/en-US/Add-ons/Code_snippets/HTML_to_DOM
+
+function getSafeBodyFromHTML(html: string): ?Element {
+  var doc;
+  var root = null;
+  // Provides a safe context
+  if (
+    !isOldIE &&
+    document.implementation &&
+    document.implementation.createHTMLDocument
+  ) {
+    doc = document.implementation.createHTMLDocument('foo');
+    doc.documentElement.innerHTML = html;
+    root = doc.getElementsByTagName('body')[0];
+  }
+  return root;
+}
+
+module.exports = getSafeBodyFromHTML;
diff --git a/src/model/transaction/ContentStateInlineStyle.js b/src/model/transaction/ContentStateInlineStyle.js
new file mode 100644
index 0000000..cebecac
--- /dev/null
+++ b/src/model/transaction/ContentStateInlineStyle.js
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule ContentStateInlineStyle
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+var {Map} = require('immutable');
+
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+var ContentStateInlineStyle = {
+  add: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    inlineStyle: string
+  ): ContentState {
+    return modifyInlineStyle(contentState, selectionState, inlineStyle, true);
+  },
+
+  remove: function(
+    contentState: ContentState,
+    selectionState: SelectionState,
+    inlineStyle: string
+  ): ContentState {
+    return modifyInlineStyle(contentState, selectionState, inlineStyle, false);
+  },
+};
+
+function modifyInlineStyle(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  inlineStyle: string,
+  addOrRemove: boolean
+): ContentState {
+  var blockMap = contentState.getBlockMap();
+  var startKey = selectionState.getStartKey();
+  var startOffset = selectionState.getStartOffset();
+  var endKey = selectionState.getEndKey();
+  var endOffset = selectionState.getEndOffset();
+
+  var newBlocks = blockMap
+    .skipUntil((_, k) => k === startKey)
+    .takeUntil((_, k) => k === endKey)
+    .concat(Map([[endKey, blockMap.get(endKey)]]))
+    .map((block, blockKey) => {
+      var sliceStart;
+      var sliceEnd;
+
+      if (startKey === endKey) {
+        sliceStart = startOffset;
+        sliceEnd = endOffset;
+      } else {
+        sliceStart = blockKey === startKey ? startOffset : 0;
+        sliceEnd = blockKey === endKey ? endOffset : block.getLength();
+      }
+
+      var chars = block.getCharacterList();
+      var current;
+      while (sliceStart < sliceEnd) {
+        current = chars.get(sliceStart);
+        chars = chars.set(
+          sliceStart,
+          addOrRemove ?
+            CharacterMetadata.applyStyle(current, inlineStyle) :
+            CharacterMetadata.removeStyle(current, inlineStyle)
+        );
+        sliceStart++;
+      }
+
+      return block.set('characterList', chars);
+    });
+
+  return contentState.merge({
+    blockMap: blockMap.merge(newBlocks),
+    selectionBefore: selectionState,
+    selectionAfter: selectionState,
+  });
+}
+
+module.exports = ContentStateInlineStyle;
diff --git a/src/model/transaction/__tests__/ContentStateInlineStyle-test.js b/src/model/transaction/__tests__/ContentStateInlineStyle-test.js
new file mode 100644
index 0000000..b16b9b2
--- /dev/null
+++ b/src/model/transaction/__tests__/ContentStateInlineStyle-test.js
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+const ContentStateInlineStyle = require('ContentStateInlineStyle');
+
+const {
+  List,
+  Repeat,
+} = require('immutable');
+
+const getSampleStateForTesting = require('getSampleStateForTesting');
+
+describe('ContentStateInlineStyle', () => {
+  const {
+    contentState,
+    selectionState,
+  } = getSampleStateForTesting();
+
+  function getStyles(block) {
+    return block.getCharacterList().map(c => c.getStyle()).flatten().toJS();
+  }
+
+  describe('Within a single block', () => {
+    const blockKey = selectionState.getStartKey();
+    const length = contentState.getBlockForKey(blockKey).getLength();
+    const target = selectionState.set('focusOffset', length);
+
+    it('must add styles', () => {
+      let modified = ContentStateInlineStyle.add(contentState, target, 'BOLD');
+      expect(
+        getStyles(modified.getBlockForKey(blockKey))
+      ).toEqual(
+        Repeat('BOLD', length).toJS()
+      );
+
+      const nextTarget = target.set('focusOffset', 2);
+      modified = ContentStateInlineStyle.add(modified, nextTarget, 'ITALIC');
+      expect(
+        getStyles(modified.getBlockForKey(blockKey))
+      ).toEqual(
+        List(['BOLD', 'ITALIC', 'BOLD', 'ITALIC'])
+          .concat(List(Repeat('BOLD', 3))).toJS()
+      );
+    });
+
+    it('must remove styles', () => {
+      // Go ahead and add some styles that we'll then remove.
+      let modified = ContentStateInlineStyle.add(
+        contentState,
+        target,
+        'BOLD'
+      );
+      modified = ContentStateInlineStyle.add(modified, target, 'ITALIC');
+      modified = ContentStateInlineStyle.remove(modified, target, 'BOLD');
+      expect(
+        getStyles(modified.getBlockForKey(blockKey))
+      ).toEqual(
+        Repeat('ITALIC', length).toJS()
+      );
+
+      const nextTarget = target.set('focusOffset', 2);
+      modified = ContentStateInlineStyle.remove(modified, nextTarget, 'ITALIC');
+      expect(
+        getStyles(modified.getBlockForKey(blockKey))
+      ).toEqual(
+        List(Repeat('ITALIC', 3)).toJS()
+      );
+    });
+  });
+
+  describe('Across multiple blocks', () => {
+    const nextBlock = contentState.getBlockAfter(selectionState.getStartKey());
+    const target = selectionState.merge({
+      focusKey: nextBlock.getKey(),
+      focusOffset: nextBlock.getLength(),
+    });
+
+    it('must add and remove styles', () => {
+      let modified = ContentStateInlineStyle.add(contentState, target, 'BOLD');
+      const start = contentState.getBlockForKey(target.getStartKey());
+
+      expect(
+        getStyles(modified.getBlockForKey(target.getStartKey()))
+      ).toEqual(
+        Repeat('BOLD', start.getLength()).toJS()
+      );
+
+      expect(
+        getStyles(modified.getBlockForKey(nextBlock.getKey()))
+      ).toEqual(
+        Repeat('BOLD', nextBlock.getLength()).toJS()
+      );
+
+      modified = ContentStateInlineStyle.remove(contentState, target, 'BOLD');
+
+      expect(
+        getStyles(modified.getBlockForKey(target.getStartKey())).length
+      ).toEqual(0);
+
+      expect(
+        getStyles(modified.getBlockForKey(nextBlock.getKey())).length
+      ).toEqual(0);
+    });
+  });
+});
diff --git a/src/model/transaction/__tests__/applyEntityToContentBlock-test.js b/src/model/transaction/__tests__/applyEntityToContentBlock-test.js
new file mode 100644
index 0000000..0ed3ab4
--- /dev/null
+++ b/src/model/transaction/__tests__/applyEntityToContentBlock-test.js
@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var applyEntityToContentBlock = require('applyEntityToContentBlock');
+
+var {
+  List,
+  Repeat,
+} = require('immutable');
+
+describe('applyEntityToContentBlock', () => {
+  var block = new ContentBlock({
+    key: 'a',
+    text: 'Hello',
+    characterList: List(Repeat(CharacterMetadata.EMPTY, 5)),
+  });
+
+  function getEntities(block) {
+    return block.getCharacterList().map(c => c.getEntity()).toJS();
+  }
+
+  it('must apply from the start', () => {
+    var modified = applyEntityToContentBlock(block, 0, 2, 'x');
+    expect(getEntities(modified)).toEqual(['x', 'x', null, null, null]);
+  });
+
+  it('must apply within', () => {
+    var modified = applyEntityToContentBlock(block, 1, 4, 'x');
+    expect(getEntities(modified)).toEqual([null, 'x', 'x', 'x', null]);
+  });
+
+  it('must apply at the end', () => {
+    var modified = applyEntityToContentBlock(block, 3, 5, 'x');
+    expect(getEntities(modified)).toEqual([null, null, null, 'x', 'x']);
+  });
+
+  it('must apply to the entire text', () => {
+    var modified = applyEntityToContentBlock(block, 0, 5, 'x');
+    expect(getEntities(modified)).toEqual(['x', 'x', 'x', 'x', 'x']);
+  });
+});
diff --git a/src/model/transaction/__tests__/applyEntityToContentState-test.js b/src/model/transaction/__tests__/applyEntityToContentState-test.js
new file mode 100644
index 0000000..156f4f4
--- /dev/null
+++ b/src/model/transaction/__tests__/applyEntityToContentState-test.js
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var Immutable = require('immutable');
+var SelectionState = require('SelectionState');
+var applyEntityToContentState = require('applyEntityToContentState');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+
+describe('applyEntityToContentState', () => {
+  var {
+    contentState,
+  } = getSampleStateForTesting();
+
+  function checkForCharacterList(block) {
+    expect(Immutable.List.isList(block.getCharacterList())).toBe(true);
+  }
+
+  function getEntities(block) {
+    return block.getCharacterList().map(c => c.getEntity()).toJS();
+  }
+
+  describe('Apply entity within single block', () => {
+    var target = contentState.getBlockMap().first();
+    var targetSelection = new SelectionState({
+      anchorKey: target.getKey(),
+      anchorOffset: 0,
+      focusKey: target.getKey(),
+      focusOffset: target.getLength(),
+    });
+
+    function applyAndCheck(entityKey) {
+      var withNewEntity = applyEntityToContentState(
+        contentState,
+        targetSelection,
+        entityKey
+      );
+      var first = withNewEntity.getBlockMap().first();
+
+      checkForCharacterList(first);
+      expect(getEntities(first)).toEqual(
+        Immutable.Repeat(entityKey, first.getLength()).toArray()
+      );
+    }
+
+    it('must apply entity key', () => {
+      applyAndCheck('x');
+    });
+
+    it('must apply null entity', () => {
+      applyAndCheck(null);
+    });
+  });
+
+  describe('Apply entity across multiple blocks', () => {
+    var blockMap = contentState.getBlockMap();
+    var first = blockMap.first();
+    var last = contentState.getBlockAfter(first.getKey());
+
+    var targetSelection = new SelectionState({
+      anchorKey: first.getKey(),
+      anchorOffset: 0,
+      focusKey: last.getKey(),
+      focusOffset: last.getLength(),
+    });
+
+    function applyAndCheck(entityKey) {
+      var withNewEntity = applyEntityToContentState(
+        contentState,
+        targetSelection,
+        entityKey
+      );
+      var first = withNewEntity.getBlockMap().first();
+      var last = withNewEntity.getBlockAfter(first.getKey());
+
+      checkForCharacterList(first);
+      expect(getEntities(first)).toEqual(
+        Immutable.Repeat(entityKey, first.getLength()).toArray()
+      );
+      checkForCharacterList(last);
+      expect(getEntities(first)).toEqual(
+        Immutable.Repeat(entityKey, last.getLength()).toArray()
+      );
+    }
+
+    it('must apply entity key', () => {
+      applyAndCheck('x');
+    });
+
+    it('must apply null entity', () => {
+      applyAndCheck(null);
+    });
+  });
+});
diff --git a/src/model/transaction/__tests__/insertIntoList-test.js b/src/model/transaction/__tests__/insertIntoList-test.js
new file mode 100644
index 0000000..88c1ed3
--- /dev/null
+++ b/src/model/transaction/__tests__/insertIntoList-test.js
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var Immutable = require('immutable');
+var insertIntoList = require('insertIntoList');
+
+describe('insertIntoList', () => {
+  var list = Immutable.List.of(0, 1, 2, 3, 4);
+
+  it('must insert at end of list', () => {
+    var result = insertIntoList(
+      list,
+      Immutable.List.of(100, 101, 102),
+      list.size
+    );
+    expect(result.size).toBe(8);
+    expect(result.toJS()).toEqual([0, 1, 2, 3, 4, 100, 101, 102]);
+  });
+
+  it('must insert at beginning of list', () => {
+    var result = insertIntoList(
+      list,
+      Immutable.List.of(100, 101, 102),
+      0
+    );
+    expect(result.size).toBe(8);
+    expect(result.toJS()).toEqual([100, 101, 102, 0, 1, 2, 3, 4]);
+  });
+
+  it('must insert within a list', () => {
+    var result = insertIntoList(
+      list,
+      Immutable.List.of(100, 101, 102),
+      3
+    );
+    expect(result.size).toBe(8);
+    expect(result.toJS()).toEqual([0, 1, 2, 100, 101, 102, 3, 4]);
+  });
+});
diff --git a/src/model/transaction/__tests__/insertTextIntoContentState-test.js b/src/model/transaction/__tests__/insertTextIntoContentState-test.js
new file mode 100644
index 0000000..6a6ee87
--- /dev/null
+++ b/src/model/transaction/__tests__/insertTextIntoContentState-test.js
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var CharacterMetadata = require('CharacterMetadata');
+var Immutable = require('immutable');
+var insertTextIntoContentState = require('insertTextIntoContentState');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+var {BOLD} = require('SampleDraftInlineStyle');
+
+describe('insertTextIntoContentState', () => {
+  var sample = getSampleStateForTesting();
+  var content = sample.contentState;
+  var selection = sample.selectionState;
+
+  var EMPTY = CharacterMetadata.EMPTY;
+
+  var block = content.getBlockMap().first();
+
+  it('must throw if selection is not collapsed', () => {
+    var target = selection.set('focusOffset', 2);
+    expect(() => {
+      insertTextIntoContentState(content, target, 'hey', EMPTY);
+    }).toThrow();
+  });
+
+  it('must return early if no text is provided', () => {
+    var modified = insertTextIntoContentState(content, selection, '', EMPTY);
+    expect(modified).toBe(content);
+  });
+
+  it('must insert at the start', () => {
+    var character = CharacterMetadata.create({style: BOLD});
+    var modified = insertTextIntoContentState(
+      content,
+      selection,
+      'xx',
+      character
+    );
+
+    var newBlock = modified.getBlockMap().first();
+    expect(newBlock.getText().slice(0, 2)).toBe('xx');
+    expect(newBlock.getText().slice(2)).toBe(block.getText());
+
+    expect(
+      Immutable.is(
+        Immutable.List.of(
+          character, character, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY
+        ),
+        newBlock.getCharacterList()
+      )
+    ).toBe(true);
+  });
+
+  it('must insert within block', () => {
+    var target = selection.merge({
+      focusOffset: 2,
+      anchorOffset: 2,
+      isBackward: false,
+    });
+    var character = CharacterMetadata.create({style: BOLD});
+    var modified = insertTextIntoContentState(content, target, 'xx', character);
+    var newBlock = modified.getBlockMap().first();
+
+    expect(newBlock.getText().slice(2, 4)).toBe('xx');
+    expect(newBlock.getText().slice(0, 2)).toBe(block.getText().slice(0, 2));
+    expect(newBlock.getText().slice(4, 7)).toBe(block.getText().slice(2, 5));
+
+    expect(
+      Immutable.is(
+        newBlock.getCharacterList(),
+        Immutable.List([
+          EMPTY, EMPTY, character, character, EMPTY, EMPTY, EMPTY,
+        ])
+      )
+    ).toBe(true);
+  });
+
+  it('must insert at the end', () => {
+    var target = selection.merge({
+      focusOffset: block.getLength(),
+      anchorOffset: block.getLength(),
+      isBackward: false,
+    });
+
+    var character = CharacterMetadata.create({style: BOLD});
+    var modified = insertTextIntoContentState(content, target, 'xx', character);
+
+    var newBlock = modified.getBlockMap().first();
+    expect(newBlock.getText().slice(5, 7)).toBe('xx');
+    expect(newBlock.getText().slice(0, 5)).toBe(block.getText());
+
+    expect(
+      Immutable.is(
+        newBlock.getCharacterList(),
+        Immutable.List([
+          EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, character, character,
+        ])
+      )
+    ).toBe(true);
+  });
+});
diff --git a/src/model/transaction/__tests__/removeEntitiesAtEdges-test.js b/src/model/transaction/__tests__/removeEntitiesAtEdges-test.js
new file mode 100644
index 0000000..05a4b3c
--- /dev/null
+++ b/src/model/transaction/__tests__/removeEntitiesAtEdges-test.js
@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest
+  .autoMockOff()
+  .mock('DraftEntity');
+
+var DraftEntity = require('DraftEntity');
+var Immutable = require('immutable');
+var applyEntityToContentBlock = require('applyEntityToContentBlock');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+var removeEntitiesAtEdges = require('removeEntitiesAtEdges');
+
+describe('removeEntitiesAtEdges', () => {
+  var {
+    List,
+    Repeat,
+  } = Immutable;
+
+  var {
+    contentState,
+    selectionState,
+  } = getSampleStateForTesting();
+
+  var selectionOnEntity = selectionState.merge({
+    anchorKey: 'b',
+    anchorOffset: 2,
+    focusKey: 'b',
+    focusOffset: 2,
+  });
+
+  function getEntities(block) {
+    return block.getCharacterList().map(c => c.getEntity()).toJS();
+  }
+
+  function expectSameBlockMap(before, after) {
+    expect(before.getBlockMap()).toBe(after.getBlockMap());
+  }
+
+  function expectNullEntities(block) {
+    expect(
+      getEntities(block)
+    ).toEqual(
+      List(Repeat(null, block.getLength())).toJS()
+    );
+  }
+
+  function setEntityMutability(mutability) {
+    DraftEntity.get.mockReturnValue({
+      getMutability: () => mutability,
+    });
+  }
+
+  describe('Within a single block', () => {
+    it('must not affect blockMap if there are no entities', () => {
+      var result = removeEntitiesAtEdges(contentState, selectionState);
+      expectSameBlockMap(contentState, result);
+    });
+
+    describe('Handling different mutability types', () => {
+      it('must not remove mutable entities', () => {
+        setEntityMutability('MUTABLE');
+        var result = removeEntitiesAtEdges(contentState, selectionOnEntity);
+        expectSameBlockMap(contentState, result);
+      });
+
+      it('must remove immutable entities', () => {
+        setEntityMutability('IMMUTABLE');
+        var result = removeEntitiesAtEdges(contentState, selectionOnEntity);
+        expect(result.getBlockMap()).not.toBe(contentState.getBlockMap());
+        expectNullEntities(result.getBlockForKey('b'));
+      });
+
+      it('must remove segmented entities', () => {
+        setEntityMutability('SEGMENTED');
+        var result = removeEntitiesAtEdges(contentState, selectionOnEntity);
+        expect(result.getBlockMap()).not.toBe(contentState.getBlockMap());
+        expectNullEntities(result.getBlockForKey('b'));
+      });
+    });
+
+    describe('Removal for a collapsed cursor', () => {
+      setEntityMutability('IMMUTABLE');
+
+      it('must not remove if cursor is at start of entity', () => {
+        var selection = selectionOnEntity.merge({
+          anchorOffset: 0,
+          focusOffset: 0,
+        });
+        var result = removeEntitiesAtEdges(contentState, selection);
+        expectSameBlockMap(contentState, result);
+      });
+
+      it('must remove if cursor is within entity', () => {
+        var result = removeEntitiesAtEdges(contentState, selectionOnEntity);
+        expectNullEntities(result.getBlockForKey('b'));
+      });
+
+      it('must not remove if cursor is at end of entity', () => {
+        var length = contentState.getBlockForKey('b').getLength();
+        var selection = selectionOnEntity.merge({
+          anchorOffset: length,
+          focusOffset: length,
+        });
+        var result = removeEntitiesAtEdges(contentState, selection);
+        expectSameBlockMap(contentState, result);
+      });
+    });
+
+    describe('Removal for a non-collapsed cursor', () => {
+      setEntityMutability('IMMUTABLE');
+
+      it('must remove for non-collapsed cursor within a single entity', () => {
+        setEntityMutability('IMMUTABLE');
+        var selection = selectionOnEntity.set('anchorOffset', 1);
+        var result = removeEntitiesAtEdges(contentState, selection);
+        expectNullEntities(result.getBlockForKey('b'));
+      });
+
+      it('must remove for non-collapsed cursor on multiple entities', () => {
+        var block = contentState.getBlockForKey('b');
+        var newBlock = applyEntityToContentBlock(block, 3, 5, '456');
+        var newBlockMap = contentState.getBlockMap().set('b', newBlock);
+        var newContent = contentState.set('blockMap', newBlockMap);
+        var selection = selectionOnEntity.merge({
+          anchorOffset: 1,
+          focusOffset: 4,
+        });
+        var result = removeEntitiesAtEdges(newContent, selection);
+        expectNullEntities(result.getBlockForKey('b'));
+      });
+
+      it('must ignore an entity that is entirely within the selection', () => {
+        var block = contentState.getBlockForKey('b');
+
+        // Remove entity from beginning and end of block.
+        var newBlock = applyEntityToContentBlock(block, 0, 1, null);
+        newBlock = applyEntityToContentBlock(newBlock, 4, 5, null);
+
+        var newBlockMap = contentState.getBlockMap().set('b', newBlock);
+        var newContent = contentState.set('blockMap', newBlockMap);
+        var selection = selectionOnEntity.merge({
+          anchorOffset: 0,
+          focusOffset: 5,
+        });
+        var result = removeEntitiesAtEdges(newContent, selection);
+        expectSameBlockMap(newContent, result);
+      });
+    });
+  });
+
+  describe('Across multiple blocks', () => {
+    setEntityMutability('IMMUTABLE');
+
+    it('must remove entity at start of selection', () => {
+      var selection = selectionState.merge({
+        anchorKey: 'b',
+        anchorOffset: 3,
+        focusKey: 'c',
+        focusOffset: 3,
+      });
+      var result = removeEntitiesAtEdges(contentState, selection);
+      expectNullEntities(result.getBlockForKey('b'));
+      expectNullEntities(result.getBlockForKey('c'));
+    });
+
+    it('must remove entity at end of selection', () => {
+      var selection = selectionState.merge({
+        anchorKey: 'a',
+        anchorOffset: 3,
+        focusKey: 'b',
+        focusOffset: 3,
+      });
+      var result = removeEntitiesAtEdges(contentState, selection);
+      expectNullEntities(result.getBlockForKey('a'));
+      expectNullEntities(result.getBlockForKey('b'));
+    });
+
+    it('must remove entities at both ends of selection', () => {
+      var cBlock = contentState.getBlockForKey('c');
+      var len = cBlock.getLength();
+      var modifiedC = applyEntityToContentBlock(cBlock, 0, len, '456');
+      var newBlockMap = contentState.getBlockMap().set('c', modifiedC);
+      var newContent = contentState.set('blockMap', newBlockMap);
+      var selection = selectionState.merge({
+        anchorKey: 'b',
+        anchorOffset: 3,
+        focusKey: 'c',
+        focusOffset: 3,
+      });
+      var result = removeEntitiesAtEdges(newContent, selection);
+      expectNullEntities(result.getBlockForKey('b'));
+      expectNullEntities(result.getBlockForKey('c'));
+    });
+  });
+});
diff --git a/src/model/transaction/__tests__/removeRangeFromContentState-test.js b/src/model/transaction/__tests__/removeRangeFromContentState-test.js
new file mode 100644
index 0000000..cb6feb1
--- /dev/null
+++ b/src/model/transaction/__tests__/removeRangeFromContentState-test.js
@@ -0,0 +1,428 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var Immutable = require('immutable');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+var removeRangeFromContentState = require('removeRangeFromContentState');
+var ContentBlock = require('ContentBlock');
+
+describe('removeRangeFromContentState', () => {
+  var {
+    contentState,
+    selectionState,
+  } = getSampleStateForTesting();
+
+  function checkForCharacterList(block) {
+    expect(Immutable.List.isList(block.getCharacterList())).toBe(true);
+  }
+
+  function getInlineStyles(block) {
+    return block.getCharacterList().map(c => c.getStyle()).toJS();
+  }
+
+  function getEntities(block) {
+    return block.getCharacterList().map(c => c.getEntity()).toJS();
+  }
+
+  function expectBlockToBeSlice(block, comparison, start, end) {
+    expect(block.getType()).toBe(comparison.getType());
+    expect(block.getText()).toBe(comparison.getText().slice(start, end));
+    expect(
+      getInlineStyles(block)
+    ).toEqual(
+      getInlineStyles(comparison).slice(start, end)
+    );
+    expect(
+      getEntities(block)
+    ).toEqual(
+      getEntities(comparison).slice(start, end)
+    );
+    checkForCharacterList(block);
+  }
+
+  it('must return the input ContentState if selection is collapsed', () => {
+    expect(
+      removeRangeFromContentState(contentState, selectionState)
+    ).toBe(contentState);
+  });
+
+  describe('Removal within a single block', () => {
+    it('must remove from the beginning of the block', () => {
+      // Remove from 0 to 3.
+      var selection = selectionState.set('focusOffset', 3);
+      var afterRemoval = removeRangeFromContentState(contentState, selection);
+      var afterBlockMap = afterRemoval.getBlockMap();
+      var alteredBlock = afterBlockMap.first();
+      var originalBlock = contentState.getBlockMap().first();
+
+      expectBlockToBeSlice(alteredBlock, originalBlock, 3);
+    });
+
+    it('must remove from within the block', () => {
+      // Remove from 2 to 4.
+      var selection = selectionState.merge({
+        anchorOffset: 2,
+        focusOffset: 4,
+      });
+      var afterRemoval = removeRangeFromContentState(contentState, selection);
+      var afterBlockMap = afterRemoval.getBlockMap();
+      var alteredBlock = afterBlockMap.first();
+      var originalBlock = contentState.getBlockMap().first();
+
+      expect(alteredBlock).not.toBe(originalBlock);
+      expect(alteredBlock.getType()).toBe(originalBlock.getType());
+      expect(alteredBlock.getText()).toBe(
+        originalBlock.getText().slice(0, 2) +
+        originalBlock.getText().slice(4)
+      );
+
+      var stylesToJS = getInlineStyles(originalBlock);
+      expect(getInlineStyles(alteredBlock)).toEqual(
+        stylesToJS.slice(0, 2).concat(stylesToJS.slice(4))
+      );
+      var entitiesToJS = getEntities(originalBlock);
+      expect(getEntities(alteredBlock)).toEqual(
+        entitiesToJS.slice(0, 2).concat(entitiesToJS.slice(4))
+      );
+    });
+
+    it('nust remove to the end of the block', () => {
+      // Remove from 3 to end.
+      var originalBlock = contentState.getBlockMap().first();
+      var selection = selectionState.merge({
+        anchorOffset: 3,
+        focusOffset: originalBlock.getLength(),
+      });
+      var afterRemoval = removeRangeFromContentState(contentState, selection);
+      var afterBlockMap = afterRemoval.getBlockMap();
+      var alteredBlock = afterBlockMap.first();
+
+      expectBlockToBeSlice(alteredBlock, originalBlock, 0, 3);
+    });
+  });
+
+  describe('Removal across two blocks', () => {
+    describe('Removal from the start of A', () => {
+      it('must remove from the start of A to the start of B', () => {
+        var selection = selectionState.set('focusKey', 'b');
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        // Block B is removed. Its contents replace the contents of block A,
+        // while the `type` of block A is preserved.
+        var blockB = contentState.getBlockMap().skip(1).first();
+        var sameContentDifferentType = blockB.set(
+          'type',
+          contentState.getBlockMap().first().getType()
+        );
+
+        expectBlockToBeSlice(alteredBlock, sameContentDifferentType, 0);
+      });
+
+      it('must remove from the start of A to within B', () => {
+        var selection = selectionState.merge({
+          focusKey: 'b',
+          focusOffset: 3,
+        });
+
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        // A slice of block B contents replace the contents of block A,
+        // while the `type` of block A is preserved. Block B is removed.
+        var blockB = contentState.getBlockMap().skip(1).first();
+        var sameContentDifferentType = blockB.set(
+          'type',
+          contentState.getBlockMap().first().getType()
+        );
+
+        expectBlockToBeSlice(alteredBlock, sameContentDifferentType, 3);
+      });
+
+      it('must remove from the start of A to the end of B', () => {
+        var blockB = contentState.getBlockMap().skip(1).first();
+        var selection = selectionState.merge({
+          focusKey: 'b',
+          focusOffset: blockB.getLength(),
+        });
+
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        // Block A is effectively just emptied out, while block B is removed.
+        var emptyBlock = new ContentBlock({
+          text: '',
+          type: contentState.getBlockMap().first().getType(),
+          characterList: Immutable.List(),
+        });
+
+        expectBlockToBeSlice(alteredBlock, emptyBlock);
+      });
+    });
+
+    describe('Removal from within A', () => {
+      var selectionWithinA = selectionState.set('anchorOffset', 3);
+
+      it('must remove from within A to the start of B', () => {
+        var selection = selectionWithinA.set('focusKey', 'b');
+
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        expect(alteredBlock).not.toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+        expect(alteredBlock.getText()).toBe(
+          originalBlockA.getText().slice(0, 3) +
+          originalBlockB.getText()
+        );
+
+        var stylesToJS = getInlineStyles(originalBlockA);
+        expect(getInlineStyles(alteredBlock)).toEqual(
+          stylesToJS.slice(0, 3).concat(getInlineStyles(originalBlockB))
+        );
+        var entitiesToJS = getEntities(originalBlockA);
+        expect(getEntities(alteredBlock)).toEqual(
+          entitiesToJS.slice(0, 3).concat(getEntities(originalBlockB))
+        );
+
+        checkForCharacterList(alteredBlock);
+      });
+
+      it('must remove from within A to within B', () => {
+        var selection = selectionWithinA.merge({
+          focusKey: 'b',
+          focusOffset: 3,
+        });
+
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        expect(alteredBlock).not.toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+        expect(alteredBlock.getText()).toBe(
+          originalBlockA.getText().slice(0, 3) +
+          originalBlockB.getText().slice(3)
+        );
+
+        var stylesToJS = getInlineStyles(originalBlockA);
+        expect(getInlineStyles(alteredBlock)).toEqual(
+          stylesToJS.slice(0, 3).concat(
+            getInlineStyles(originalBlockB).slice(3)
+          )
+        );
+        var entitiesToJS = getEntities(originalBlockA);
+        expect(getEntities(alteredBlock)).toEqual(
+          entitiesToJS.slice(0, 3).concat(
+            getEntities(originalBlockB).slice(3)
+          )
+        );
+
+        checkForCharacterList(alteredBlock);
+      });
+
+      it('must remove from within A to the end of B', () => {
+        var blockB = contentState.getBlockMap().skip(1).first();
+        var selection = selectionWithinA.merge({
+          focusKey: 'b',
+          focusOffset: blockB.getLength(),
+        });
+
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        expect(alteredBlock).not.toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+        expect(alteredBlock.getText()).toBe(
+          originalBlockA.getText().slice(0, 3)
+        );
+
+        var stylesToJS = getInlineStyles(originalBlockA);
+        expect(getInlineStyles(alteredBlock)).toEqual(
+          stylesToJS.slice(0, 3)
+        );
+        var entitiesToJS = getEntities(originalBlockA);
+        expect(getEntities(alteredBlock)).toEqual(
+          entitiesToJS.slice(0, 3)
+        );
+
+        checkForCharacterList(alteredBlock);
+      });
+    });
+
+    describe('Removal from the end of A', () => {
+      var initialBlock = contentState.getBlockMap().first();
+      var selectionFromEndOfA = selectionState.merge({
+        anchorOffset: initialBlock.getLength(),
+        focusOffset: initialBlock.getLength(),
+      });
+
+      it('must remove from the end of A to the start of B', () => {
+        var selection = selectionFromEndOfA.merge({
+          focusKey: 'b',
+          focusOffset: 0,
+        });
+
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        expect(alteredBlock).not.toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+        expect(alteredBlock.getText()).toBe(
+          originalBlockA.getText() +
+          originalBlockB.getText()
+        );
+
+        var stylesToJS = getInlineStyles(originalBlockA);
+        expect(getInlineStyles(alteredBlock)).toEqual(
+          stylesToJS.concat(getInlineStyles(originalBlockB))
+        );
+        var entitiesToJS = getEntities(originalBlockA);
+        expect(getEntities(alteredBlock)).toEqual(
+          entitiesToJS.concat(getEntities(originalBlockB))
+        );
+
+        checkForCharacterList(alteredBlock);
+      });
+
+      it('must remove from the end of A to within B', () => {
+        var selection = selectionFromEndOfA.merge({
+          focusKey: 'b',
+          focusOffset: 3,
+        });
+
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        expect(alteredBlock).not.toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+        expect(alteredBlock.getText()).toBe(
+          originalBlockA.getText() +
+          originalBlockB.getText().slice(3)
+        );
+
+        var stylesToJS = getInlineStyles(originalBlockA);
+        expect(getInlineStyles(alteredBlock)).toEqual(
+          stylesToJS.concat(
+            getInlineStyles(originalBlockB).slice(3)
+          )
+        );
+        var entitiesToJS = getEntities(originalBlockA);
+        expect(getEntities(alteredBlock)).toEqual(
+          entitiesToJS.concat(
+            getEntities(originalBlockB).slice(3)
+          )
+        );
+        checkForCharacterList(alteredBlock);
+      });
+
+      it('must remove from the end of A to the end of B', () => {
+        var originalBlockA = contentState.getBlockMap().first();
+        var originalBlockB = contentState.getBlockMap().skip(1).first();
+
+        var selection = selectionFromEndOfA.merge({
+          focusKey: 'b',
+          focusOffset: originalBlockB.getLength(),
+        });
+
+        var afterRemoval = removeRangeFromContentState(contentState, selection);
+        var afterBlockMap = afterRemoval.getBlockMap();
+        expect(afterBlockMap.size).toBe(2);
+        var alteredBlock = afterBlockMap.first();
+
+        // no-op for the first block, since no new content is appended.
+        expect(alteredBlock).toBe(originalBlockA);
+        expect(alteredBlock).not.toBe(originalBlockB);
+        expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+
+        expectBlockToBeSlice(alteredBlock, originalBlockA);
+        checkForCharacterList(alteredBlock);
+      });
+    });
+  });
+
+  describe('Removal across more than two blocks', () => {
+    var selection = selectionState.merge({
+      anchorOffset: 3,
+      focusKey: 'c',
+      focusOffset: 3,
+    });
+
+    it('must remove blocks entirely within the selection', () => {
+      var originalBlockA = contentState.getBlockMap().first();
+      var originalBlockB = contentState.getBlockMap().skip(1).first();
+      var originalBlockC = contentState.getBlockMap().skip(2).first();
+
+      var afterRemoval = removeRangeFromContentState(contentState, selection);
+      var afterBlockMap = afterRemoval.getBlockMap();
+      expect(afterBlockMap.size).toBe(1);
+      var alteredBlock = afterBlockMap.first();
+
+      expect(alteredBlock).not.toBe(originalBlockA);
+      expect(alteredBlock).not.toBe(originalBlockB);
+      expect(alteredBlock).not.toBe(originalBlockC);
+      expect(alteredBlock.getType()).toBe(originalBlockA.getType());
+      expect(alteredBlock.getText()).toBe(
+        originalBlockA.getText().slice(0, 3) +
+        originalBlockC.getText().slice(3)
+      );
+
+      var stylesToJS = getInlineStyles(originalBlockA);
+      expect(getInlineStyles(alteredBlock)).toEqual(
+        stylesToJS.slice(0, 3).concat(
+          getInlineStyles(originalBlockC).slice(3)
+        )
+      );
+      var entitiesToJS = getEntities(originalBlockA);
+      expect(getEntities(alteredBlock)).toEqual(
+        entitiesToJS.slice(0, 3).concat(
+          getEntities(originalBlockC).slice(3)
+        )
+      );
+
+      checkForCharacterList(alteredBlock);
+    });
+  });
+});
diff --git a/src/model/transaction/__tests__/splitBlockInContentState-test.js b/src/model/transaction/__tests__/splitBlockInContentState-test.js
new file mode 100644
index 0000000..7c49282
--- /dev/null
+++ b/src/model/transaction/__tests__/splitBlockInContentState-test.js
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @emails oncall+ui_infra
+ */
+
+'use strict';
+
+jest.autoMockOff();
+
+var Immutable = require('immutable');
+var getSampleStateForTesting = require('getSampleStateForTesting');
+var splitBlockInContentState = require('splitBlockInContentState');
+
+describe('removeRangeFromContentState', () => {
+  var {
+    contentState,
+    selectionState,
+  } = getSampleStateForTesting();
+
+  function checkForCharacterList(block) {
+    expect(Immutable.List.isList(block.getCharacterList())).toBe(true);
+  }
+
+  function getInlineStyles(block) {
+    return block.getCharacterList().map(c => c.getStyle()).toJS();
+  }
+
+  function getEntities(block) {
+    return block.getCharacterList().map(c => c.getEntity()).toJS();
+  }
+
+  it('must be restricted to collapsed selections', () => {
+    expect(() => {
+      var nonCollapsed = selectionState.set('focusOffset', 1);
+      return splitBlockInContentState(contentState, nonCollapsed);
+    }).toThrow();
+
+    expect(() => {
+      return splitBlockInContentState(contentState, selectionState);
+    }).not.toThrow();
+  });
+
+  it('must split at the beginning of a block', () => {
+    var initialBlock = contentState.getBlockMap().first();
+    var afterSplit = splitBlockInContentState(contentState, selectionState);
+    var afterBlockMap = afterSplit.getBlockMap();
+    expect(afterBlockMap.size).toBe(4);
+
+    var preSplitBlock = afterBlockMap.first();
+
+    expect(preSplitBlock.getKey()).toBe(initialBlock.getKey());
+    expect(preSplitBlock.getText()).toBe('');
+    expect(getInlineStyles(preSplitBlock)).toEqual([]);
+    expect(getEntities(preSplitBlock)).toEqual([]);
+
+    var postSplitBlock = afterBlockMap.skip(1).first();
+    expect(preSplitBlock.getKey()).not.toBe(postSplitBlock.getKey());
+    expect(preSplitBlock.getType()).toBe(postSplitBlock.getType());
+
+    expect(postSplitBlock.getKey()).not.toBe(initialBlock.getKey());
+    expect(postSplitBlock.getType()).toBe(initialBlock.getType());
+    expect(postSplitBlock.getText()).toBe(initialBlock.getText());
+    expect(
+      getInlineStyles(initialBlock)
+    ).toEqual(
+      getInlineStyles(postSplitBlock)
+    );
+    expect(
+      getEntities(postSplitBlock)
+    ).toEqual(
+      getEntities(initialBlock)
+    );
+
+    checkForCharacterList(preSplitBlock);
+    checkForCharacterList(postSplitBlock);
+  });
+
+  it('must split within a block', () => {
+    var initialBlock = contentState.getBlockMap().first();
+    var SPLIT_OFFSET = 3;
+    var selection = selectionState.merge({
+      anchorOffset: SPLIT_OFFSET,
+      focusOffset: SPLIT_OFFSET,
+    });
+
+    var afterSplit = splitBlockInContentState(contentState, selection);
+    var afterBlockMap = afterSplit.getBlockMap();
+    expect(afterBlockMap.size).toBe(4);
+
+    var preSplitBlock = afterBlockMap.first();
+    var postSplitBlock = afterBlockMap.skip(1).first();
+
+    expect(preSplitBlock.getKey()).toBe(initialBlock.getKey());
+    expect(preSplitBlock.getText()).toBe(
+      initialBlock.getText().slice(0, SPLIT_OFFSET)
+    );
+    expect(getInlineStyles(preSplitBlock)).toEqual(
+      getInlineStyles(initialBlock).slice(0, SPLIT_OFFSET)
+    );
+    expect(getEntities(preSplitBlock)).toEqual(
+      getEntities(initialBlock).slice(0, SPLIT_OFFSET)
+    );
+
+    expect(preSplitBlock.getKey()).not.toBe(postSplitBlock.getKey());
+    expect(preSplitBlock.getType()).toBe(postSplitBlock.getType());
+
+    expect(postSplitBlock.getKey()).not.toBe(initialBlock.getKey());
+    expect(postSplitBlock.getType()).toBe(initialBlock.getType());
+    expect(postSplitBlock.getText()).toBe(
+      initialBlock.getText().slice(SPLIT_OFFSET)
+    );
+    expect(getInlineStyles(postSplitBlock)).toEqual(
+      getInlineStyles(initialBlock).slice(SPLIT_OFFSET)
+    );
+    expect(getEntities(postSplitBlock)).toEqual(
+      getEntities(initialBlock).slice(SPLIT_OFFSET)
+    );
+
+    checkForCharacterList(preSplitBlock);
+    checkForCharacterList(postSplitBlock);
+  });
+
+  it('must split at the end of a block', () => {
+    var initialBlock = contentState.getBlockMap().first();
+    var end = initialBlock.getLength();
+    var selection = selectionState.merge({
+      anchorOffset: end,
+      focusOffset: end,
+    });
+
+    var afterSplit = splitBlockInContentState(contentState, selection);
+    var afterBlockMap = afterSplit.getBlockMap();
+    expect(afterBlockMap.size).toBe(4);
+
+    var preSplitBlock = afterBlockMap.first();
+    var postSplitBlock = afterBlockMap.skip(1).first();
+
+    expect(preSplitBlock.getKey()).toBe(initialBlock.getKey());
+
+    expect(preSplitBlock.getKey()).not.toBe(postSplitBlock.getKey());
+    expect(preSplitBlock.getType()).toBe(postSplitBlock.getType());
+    expect(preSplitBlock.getText()).toBe(initialBlock.getText());
+    expect(
+      getInlineStyles(preSplitBlock)
+    ).toEqual(
+      getInlineStyles(initialBlock)
+    );
+    expect(
+      getEntities(preSplitBlock)
+    ).toEqual(
+      getEntities(initialBlock)
+    );
+
+    expect(postSplitBlock.getKey()).not.toBe(initialBlock.getKey());
+    expect(postSplitBlock.getType()).toBe(initialBlock.getType());
+    expect(postSplitBlock.getText()).toBe('');
+    expect(getInlineStyles(postSplitBlock)).toEqual([]);
+    expect(getEntities(postSplitBlock)).toEqual([]);
+
+    checkForCharacterList(preSplitBlock);
+    checkForCharacterList(postSplitBlock);
+  });
+});
diff --git a/src/model/transaction/adjustBlockDepthForContentState.js b/src/model/transaction/adjustBlockDepthForContentState.js
new file mode 100644
index 0000000..d5edc1f
--- /dev/null
+++ b/src/model/transaction/adjustBlockDepthForContentState.js
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule adjustBlockDepthForContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function adjustBlockDepthForContentState(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  adjustment: number,
+  maxDepth: number
+): ContentState {
+  var startKey = selectionState.getStartKey();
+  var endKey = selectionState.getEndKey();
+  var blockMap = contentState.getBlockMap();
+  var blocks = blockMap
+    .toSeq()
+    .skipUntil((_, k) => k === startKey)
+    .takeUntil((_, k) => k === endKey)
+    .concat([[endKey, blockMap.get(endKey)]])
+    .map(block => {
+      var depth = block.getDepth() + adjustment;
+      depth = Math.max(0, Math.min(depth, maxDepth));
+      return block.set('depth', depth);
+    });
+
+  blockMap = blockMap.merge(blocks);
+
+  return contentState.merge({
+    blockMap,
+    selectionBefore: selectionState,
+    selectionAfter: selectionState,
+  });
+}
+
+module.exports = adjustBlockDepthForContentState;
diff --git a/src/model/transaction/applyEntityToContentBlock.js b/src/model/transaction/applyEntityToContentBlock.js
new file mode 100644
index 0000000..79ce27a
--- /dev/null
+++ b/src/model/transaction/applyEntityToContentBlock.js
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule applyEntityToContentBlock
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+
+import type ContentBlock from 'ContentBlock';
+
+function applyEntityToContentBlock(
+  contentBlock: ContentBlock,
+  start: number,
+  end: number,
+  entityKey: ?string
+): ContentBlock {
+  var characterList = contentBlock.getCharacterList();
+  while (start < end) {
+    characterList = characterList.set(
+      start,
+      CharacterMetadata.applyEntity(characterList.get(start), entityKey)
+    );
+    start++;
+  }
+  return contentBlock.set('characterList', characterList);
+}
+
+module.exports = applyEntityToContentBlock;
diff --git a/src/model/transaction/applyEntityToContentState.js b/src/model/transaction/applyEntityToContentState.js
new file mode 100644
index 0000000..a4555be
--- /dev/null
+++ b/src/model/transaction/applyEntityToContentState.js
@@ -0,0 +1,66 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule applyEntityToContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+var applyEntityToContentBlock = require('applyEntityToContentBlock');
+
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function applyEntityToContentState(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  entityKey: ?string
+): ContentState {
+  var blockMap = contentState.getBlockMap();
+  var startKey = selectionState.getStartKey();
+  var startOffset = selectionState.getStartOffset();
+  var endKey = selectionState.getEndKey();
+  var endOffset = selectionState.getEndOffset();
+
+  var newBlocks = blockMap
+    .skipUntil((_, k) => k === startKey)
+    .takeUntil((_, k) => k === endKey)
+    .toOrderedMap()
+    .merge(Immutable.OrderedMap([[endKey, blockMap.get(endKey)]]))
+    .map((block, blockKey) => {
+      var sliceStart;
+      var sliceEnd;
+
+      if (startKey === endKey) {
+        sliceStart = startOffset;
+        sliceEnd = endOffset;
+      } else {
+        sliceStart = blockKey === startKey ? startOffset : 0;
+        sliceEnd = blockKey === endKey ? 0 : block.getLength();
+      }
+
+      return applyEntityToContentBlock(
+        block,
+        sliceStart,
+        sliceEnd,
+        entityKey
+      );
+    });
+
+  return contentState.merge({
+    blockMap: blockMap.merge(newBlocks),
+    selectionBefore: selectionState,
+    selectionAfter: selectionState,
+  });
+}
+
+module.exports = applyEntityToContentState;
diff --git a/src/model/transaction/getContentStateFragment.js b/src/model/transaction/getContentStateFragment.js
new file mode 100644
index 0000000..5afe98b
--- /dev/null
+++ b/src/model/transaction/getContentStateFragment.js
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getContentStateFragment
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var generateBlockKey = require('generateBlockKey');
+var removeEntitiesAtEdges = require('removeEntitiesAtEdges');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function getContentStateFragment(
+  contentState: ContentState,
+  selectionState: SelectionState
+): BlockMap {
+  var startKey = selectionState.getStartKey();
+  var startOffset = selectionState.getStartOffset();
+  var endKey = selectionState.getEndKey();
+  var endOffset = selectionState.getEndOffset();
+
+  // Edge entities should be stripped to ensure that we don't preserve
+  // invalid partial entities when the fragment is reused. We do, however,
+  // preserve entities that are entirely within the selection range.
+  var contentWithoutEdgeEntities = removeEntitiesAtEdges(
+    contentState,
+    selectionState
+  );
+
+  var blockMap = contentWithoutEdgeEntities.getBlockMap();
+  var blockKeys = blockMap.keySeq();
+  var startIndex = blockKeys.indexOf(startKey);
+  var endIndex = blockKeys.indexOf(endKey) + 1;
+
+  var slice = blockMap.slice(startIndex, endIndex).map((block, blockKey) => {
+    var newKey = generateBlockKey();
+
+    var text = block.getText();
+    var chars = block.getCharacterList();
+
+    if (startKey === endKey) {
+      return block.merge({
+        key: newKey,
+        text: text.slice(startOffset, endOffset),
+        characterList: chars.slice(startOffset, endOffset),
+      });
+    }
+
+    if (blockKey === startKey) {
+      return block.merge({
+        key: newKey,
+        text: text.slice(startOffset),
+        characterList: chars.slice(startOffset),
+      });
+    }
+
+    if (blockKey === endKey) {
+      return block.merge({
+        key: newKey,
+        text: text.slice(0, endOffset),
+        characterList: chars.slice(0, endOffset),
+      });
+    }
+
+    return block.set('key', newKey);
+  });
+
+  return slice.toOrderedMap();
+}
+
+module.exports = getContentStateFragment;
diff --git a/src/model/transaction/getSampleStateForTesting.js b/src/model/transaction/getSampleStateForTesting.js
new file mode 100644
index 0000000..33ebbf3
--- /dev/null
+++ b/src/model/transaction/getSampleStateForTesting.js
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule getSampleStateForTesting
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var BlockMapBuilder = require('BlockMapBuilder');
+var CharacterMetadata = require('CharacterMetadata');
+var ContentBlock = require('ContentBlock');
+var ContentState = require('ContentState');
+var Immutable = require('immutable');
+var SampleDraftInlineStyle = require('SampleDraftInlineStyle');
+var SelectionState = require('SelectionState');
+
+var {BOLD, ITALIC} = SampleDraftInlineStyle;
+var ENTITY_KEY = '123';
+
+var BLOCKS = [
+  new ContentBlock({
+    key: 'a',
+    type: 'unstyled',
+    text: 'Alpha',
+    characterList: Immutable.List(
+      Immutable.Repeat(CharacterMetadata.EMPTY, 5)
+    ),
+  }),
+  new ContentBlock({
+    key: 'b',
+    type: 'unordered-list-item',
+    text: 'Bravo',
+    characterList: Immutable.List(
+      Immutable.Repeat(
+        CharacterMetadata.create({style: BOLD, entity: ENTITY_KEY}),
+        5
+      )
+    ),
+  }),
+  new ContentBlock({
+    key: 'c',
+    type: 'blockquote',
+    text: 'Charlie',
+    characterList: Immutable.List(
+      Immutable.Repeat(
+        CharacterMetadata.create({style: ITALIC, entity: null}),
+        7
+      )
+    ),
+  }),
+];
+
+var selectionState = new SelectionState({
+  anchorKey: 'a',
+  anchorOffset: 0,
+  focusKey: 'a',
+  focusOffset: 0,
+  isBackward: false,
+  hasFocus: true,
+});
+
+var blockMap = BlockMapBuilder.createFromArray(BLOCKS);
+var contentState = new ContentState({
+  blockMap,
+  selectionBefore: selectionState,
+  selectionAfter: selectionState,
+});
+
+function getSampleStateForTesting(): Object {
+  return {contentState, selectionState};
+}
+
+module.exports = getSampleStateForTesting;
diff --git a/src/model/transaction/insertFragmentIntoContentState.js b/src/model/transaction/insertFragmentIntoContentState.js
new file mode 100644
index 0000000..0111c9c
--- /dev/null
+++ b/src/model/transaction/insertFragmentIntoContentState.js
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule insertFragmentIntoContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var BlockMapBuilder = require('BlockMapBuilder');
+
+var generateBlockKey = require('generateBlockKey');
+var insertIntoList = require('insertIntoList');
+var invariant = require('invariant');
+
+import type {BlockMap} from 'BlockMap';
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function insertFragmentIntoContentState(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  fragment: BlockMap
+): ContentState {
+  invariant(
+    selectionState.isCollapsed(),
+    '`insertFragment` should only be called with a collapsed selection state.'
+  );
+
+  var targetKey = selectionState.getStartKey();
+  var targetOffset = selectionState.getStartOffset();
+
+  var blockMap = contentState.getBlockMap();
+
+  var fragmentSize = fragment.size;
+  var finalKey;
+  var finalOffset;
+
+  if (fragmentSize === 1) {
+    var targetBlock = blockMap.get(targetKey);
+    var pastedBlock = fragment.first();
+    var text = targetBlock.getText();
+    var chars = targetBlock.getCharacterList();
+
+    var newBlock = targetBlock.merge({
+      text: (
+        text.slice(0, targetOffset) +
+        pastedBlock.getText() +
+        text.slice(targetOffset)
+      ),
+      characterList: insertIntoList(
+        chars,
+        pastedBlock.getCharacterList(),
+        targetOffset
+      ),
+    });
+
+    blockMap = blockMap.set(targetKey, newBlock);
+
+    finalKey = targetKey;
+    finalOffset = targetOffset + pastedBlock.getText().length;
+
+    return contentState.merge({
+      blockMap: blockMap.set(targetKey, newBlock),
+      selectionBefore: selectionState,
+      selectionAfter: selectionState.merge({
+        anchorKey: finalKey,
+        anchorOffset: finalOffset,
+        focusKey: finalKey,
+        focusOffset: finalOffset,
+        isBackward: false,
+      }),
+    });
+  }
+
+  var newBlockArr = [];
+
+  contentState.getBlockMap().forEach(
+    (block, blockKey) => {
+      if (blockKey !== targetKey) {
+        newBlockArr.push(block);
+        return;
+      }
+
+      var text = block.getText();
+      var chars = block.getCharacterList();
+
+      // Modify head portion of block.
+      var blockSize = text.length;
+      var headText = text.slice(0, targetOffset);
+      var headCharacters = chars.slice(0, targetOffset);
+      var appendToHead = fragment.first();
+
+      var modifiedHead = block.merge({
+        text: headText + appendToHead.getText(),
+        characterList: headCharacters.concat(appendToHead.getCharacterList()),
+      });
+
+      newBlockArr.push(modifiedHead);
+
+      // Insert fragment blocks after the head and before the tail.
+      fragment.slice(1, fragmentSize - 1).forEach(
+        fragmentBlock => {
+          newBlockArr.push(fragmentBlock.set('key', generateBlockKey()));
+        }
+      );
+
+      // Modify tail portion of block.
+      var tailText = text.slice(targetOffset, blockSize);
+      var tailCharacters = chars.slice(targetOffset, blockSize);
+      var prependToTail = fragment.last();
+      finalKey = generateBlockKey();
+
+      var modifiedTail = prependToTail.merge({
+        key: finalKey,
+        text: prependToTail.getText() + tailText,
+        characterList: prependToTail
+          .getCharacterList()
+          .concat(tailCharacters),
+      });
+
+      newBlockArr.push(modifiedTail);
+    }
+  );
+
+  finalOffset = fragment.last().getLength();
+
+  return contentState.merge({
+    blockMap: BlockMapBuilder.createFromArray(newBlockArr),
+    selectionBefore: selectionState,
+    selectionAfter: selectionState.merge({
+      anchorKey: finalKey,
+      anchorOffset: finalOffset,
+      focusKey: finalKey,
+      focusOffset: finalOffset,
+      isBackward: false,
+    }),
+  });
+}
+
+module.exports = insertFragmentIntoContentState;
diff --git a/src/model/transaction/insertIntoList.js b/src/model/transaction/insertIntoList.js
new file mode 100644
index 0000000..ea966be
--- /dev/null
+++ b/src/model/transaction/insertIntoList.js
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule insertIntoList
+ * @flow
+ */
+
+'use strict';
+
+import type {List} from 'immutable';
+
+/**
+ * Maintain persistence for target list when appending and prepending.
+ */
+function insertIntoList<T>(
+  targetList: List<T>,
+  toInsert: List<T>,
+  offset: number
+): List<T> {
+  if (offset === targetList.count()) {
+    toInsert.forEach(c => {
+      targetList = targetList.push(c);
+    });
+  } else if (offset === 0) {
+    toInsert.reverse().forEach(c => {
+      targetList = targetList.unshift(c);
+    });
+  } else {
+    var head = targetList.slice(0, offset);
+    var tail = targetList.slice(offset);
+    targetList = head.concat(toInsert, tail).toList();
+  }
+  return targetList;
+}
+
+module.exports = insertIntoList;
diff --git a/src/model/transaction/insertTextIntoContentState.js b/src/model/transaction/insertTextIntoContentState.js
new file mode 100644
index 0000000..462a0a8
--- /dev/null
+++ b/src/model/transaction/insertTextIntoContentState.js
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule insertTextIntoContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+var insertIntoList = require('insertIntoList');
+var invariant = require('invariant');
+
+var {Repeat} = Immutable;
+
+import type CharacterMetadata from 'CharacterMetadata';
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function insertTextIntoContentState(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  text: string,
+  characterMetadata: CharacterMetadata
+): ContentState {
+  invariant(
+    selectionState.isCollapsed(),
+    '`insertText` should only be called with a collapsed range.'
+  );
+
+  var len = text.length;
+  if (!len) {
+    return contentState;
+  }
+
+  var blockMap = contentState.getBlockMap();
+  var key = selectionState.getStartKey();
+  var offset = selectionState.getStartOffset();
+  var block = blockMap.get(key);
+  var blockText = block.getText();
+
+  var newBlock = block.merge({
+    text: (
+      blockText.slice(0, offset) +
+      text +
+      blockText.slice(offset, block.getLength())
+    ),
+    characterList: insertIntoList(
+      block.getCharacterList(),
+      Repeat(characterMetadata, len).toList(),
+      offset
+    ),
+  });
+
+  var newOffset = offset + len;
+
+  return contentState.merge({
+    blockMap: blockMap.set(key, newBlock),
+    selectionAfter: selectionState.merge({
+      anchorOffset: newOffset,
+      focusOffset: newOffset,
+    }),
+  });
+}
+
+module.exports = insertTextIntoContentState;
diff --git a/src/model/transaction/removeEntitiesAtEdges.js b/src/model/transaction/removeEntitiesAtEdges.js
new file mode 100644
index 0000000..50b3b3d
--- /dev/null
+++ b/src/model/transaction/removeEntitiesAtEdges.js
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule removeEntitiesAtEdges
+ * @flow
+ */
+
+'use strict';
+
+var CharacterMetadata = require('CharacterMetadata');
+var DraftEntity = require('DraftEntity');
+
+var findRangesImmutable = require('findRangesImmutable');
+var invariant = require('invariant');
+
+import type ContentBlock from 'ContentBlock';
+import type ContentState from 'ContentState';
+import type {List} from 'immutable';
+import type SelectionState from 'SelectionState';
+
+function removeEntitiesAtEdges(
+  contentState: ContentState,
+  selectionState: SelectionState
+): ContentState {
+  var blockMap = contentState.getBlockMap();
+
+  var updatedBlocks = {};
+
+  var startKey = selectionState.getStartKey();
+  var startOffset = selectionState.getStartOffset();
+  var startBlock = blockMap.get(startKey);
+  var updatedStart = removeForBlock(startBlock, startOffset);
+
+  if (updatedStart !== startBlock) {
+    updatedBlocks[startKey] = updatedStart;
+  }
+
+  var endKey = selectionState.getEndKey();
+  var endOffset = selectionState.getEndOffset();
+  var endBlock = blockMap.get(endKey);
+  if (startKey === endKey) {
+    endBlock = updatedStart;
+  }
+
+  var updatedEnd = removeForBlock(endBlock, endOffset);
+
+  if (updatedEnd !== endBlock) {
+    updatedBlocks[endKey] = updatedEnd;
+  }
+
+  if (!Object.keys(updatedBlocks).length) {
+    return contentState.set('selectionAfter', selectionState);
+  }
+
+  return contentState.merge({
+    blockMap: blockMap.merge(updatedBlocks),
+    selectionAfter: selectionState,
+  });
+}
+
+function getRemovalRange(
+  characters: List<CharacterMetadata>,
+  key: ?string,
+  offset: number
+): Object {
+  var removalRange;
+  findRangesImmutable(
+    characters,
+    (a, b) => a.getEntity() === b.getEntity(),
+    element => element.getEntity() === key,
+    (start, end) => {
+      if (start <= offset && end >= offset) {
+        removalRange = {start, end};
+      }
+    }
+  );
+  invariant(
+    typeof removalRange === 'object',
+    'Removal range must exist within character list.'
+  );
+  return removalRange;
+}
+
+function removeForBlock(
+  block: ContentBlock,
+  offset: number
+): ContentBlock {
+  var chars = block.getCharacterList();
+  var charBefore = offset > 0 ? chars.get(offset - 1) : undefined;
+  var charAfter = offset < chars.count() ? chars.get(offset) : undefined;
+  var entityBeforeCursor = charBefore ? charBefore.getEntity() : undefined;
+  var entityAfterCursor = charAfter ? charAfter.getEntity() : undefined;
+
+  if (entityAfterCursor && entityAfterCursor === entityBeforeCursor) {
+    var entity = DraftEntity.get(entityAfterCursor);
+    if (entity.getMutability() !== 'MUTABLE') {
+      var {start, end} = getRemovalRange(chars, entityAfterCursor, offset);
+      var current;
+      while (start < end) {
+        current = chars.get(start);
+        chars = chars.set(start, CharacterMetadata.applyEntity(current, null));
+        start++;
+      }
+      return block.set('characterList', chars);
+    }
+  }
+
+  return block;
+}
+
+module.exports = removeEntitiesAtEdges;
diff --git a/src/model/transaction/removeRangeFromContentState.js b/src/model/transaction/removeRangeFromContentState.js
new file mode 100644
index 0000000..ce1feb4
--- /dev/null
+++ b/src/model/transaction/removeRangeFromContentState.js
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule removeRangeFromContentState
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+import type CharacterMetadata from 'CharacterMetadata';
+import type ContentState from 'ContentState';
+import type {List} from 'immutable';
+import type SelectionState from 'SelectionState';
+
+function removeRangeFromContentState(
+  contentState: ContentState,
+  selectionState: SelectionState
+): ContentState {
+  if (selectionState.isCollapsed()) {
+    return contentState;
+  }
+
+  var blockMap = contentState.getBlockMap();
+  var startKey = selectionState.getStartKey();
+  var startOffset = selectionState.getStartOffset();
+  var endKey = selectionState.getEndKey();
+  var endOffset = selectionState.getEndOffset();
+
+  var startBlock = blockMap.get(startKey);
+  var endBlock = blockMap.get(endKey);
+  var characterList;
+
+  if (startBlock === endBlock) {
+    characterList = removeFromList(
+      startBlock.getCharacterList(),
+      startOffset,
+      endOffset
+    );
+  } else {
+    characterList = startBlock
+      .getCharacterList()
+      .slice(0, startOffset)
+      .concat(endBlock.getCharacterList().slice(endOffset));
+  }
+
+  var modifiedStart = startBlock.merge({
+    text: (
+      startBlock.getText().slice(0, startOffset) +
+      endBlock.getText().slice(endOffset)
+    ),
+    characterList,
+  });
+
+  var newBlocks = blockMap
+    .toSeq()
+    .skipUntil((_, k) => k === startKey)
+    .takeUntil((_, k) => k === endKey)
+    .concat(Immutable.Map([[endKey, null]]))
+    .map((_, k) => { return k === startKey ? modifiedStart : null; });
+
+  blockMap = blockMap.merge(newBlocks).filter(block => !!block);
+
+  return contentState.merge({
+    blockMap,
+    selectionBefore: selectionState,
+    selectionAfter: selectionState.merge({
+      anchorKey: startKey,
+      anchorOffset: startOffset,
+      focusKey: startKey,
+      focusOffset: startOffset,
+      isBackward: false,
+    }),
+  });
+}
+
+/**
+ * Maintain persistence for target list when removing characters on the
+ * head and tail of the character list.
+ */
+function removeFromList(
+  targetList: List<CharacterMetadata>,
+  startOffset: number,
+  endOffset: number
+): List<CharacterMetadata> {
+  if (startOffset === 0) {
+    while (startOffset < endOffset) {
+      targetList = targetList.shift();
+      startOffset++;
+    }
+  } else if (endOffset === targetList.count()) {
+    while (endOffset > startOffset) {
+      targetList = targetList.pop();
+      endOffset--;
+    }
+  } else {
+    var head = targetList.slice(0, startOffset);
+    var tail = targetList.slice(endOffset);
+    targetList = head.concat(tail).toList();
+  }
+  return targetList;
+}
+
+module.exports = removeRangeFromContentState;
diff --git a/src/model/transaction/setBlockTypeForContentState.js b/src/model/transaction/setBlockTypeForContentState.js
new file mode 100644
index 0000000..2a6c760
--- /dev/null
+++ b/src/model/transaction/setBlockTypeForContentState.js
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule setBlockTypeForContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var Immutable = require('immutable');
+
+import type ContentState from 'ContentState';
+import type {DraftBlockType} from 'DraftBlockType';
+import type SelectionState from 'SelectionState';
+
+function setBlockTypeForContentState(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  blockType: DraftBlockType,
+): ContentState {
+  var startKey = selectionState.getStartKey();
+  var endKey = selectionState.getEndKey();
+  var blockMap = contentState.getBlockMap();
+  var newBlocks = blockMap
+    .toSeq()
+    .skipUntil((_, k) => k === startKey)
+    .takeUntil((_, k) => k === endKey)
+    .concat(Immutable.Map([[endKey, blockMap.get(endKey)]]))
+    .map(block => block.merge({type: blockType, depth: 0}));
+
+  return contentState.merge({
+    blockMap: blockMap.merge(newBlocks),
+    selectionBefore: selectionState,
+    selectionAfter: selectionState,
+  });
+}
+
+module.exports = setBlockTypeForContentState;
diff --git a/src/model/transaction/splitBlockInContentState.js b/src/model/transaction/splitBlockInContentState.js
new file mode 100644
index 0000000..4927cd1
--- /dev/null
+++ b/src/model/transaction/splitBlockInContentState.js
@@ -0,0 +1,71 @@
+/**
+ * Copyright (c) 2013-present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @providesModule splitBlockInContentState
+ * @typechecks
+ * @flow
+ */
+
+'use strict';
+
+var generateBlockKey = require('generateBlockKey');
+var invariant = require('invariant');
+
+import type ContentState from 'ContentState';
+import type SelectionState from 'SelectionState';
+
+function splitBlockInContentState(
+  contentState: ContentState,
+  selectionState: SelectionState
+): ContentState {
+  invariant(
+    selectionState.isCollapsed(),
+    'Selection range must be collapsed.'
+  );
+
+  var key = selectionState.getAnchorKey();
+  var offset = selectionState.getAnchorOffset();
+  var blockMap = contentState.getBlockMap();
+  var blockToSplit = blockMap.get(key);
+
+  var text = blockToSplit.getText();
+  var chars = blockToSplit.getCharacterList();
+
+  var blockAbove = blockToSplit.merge({
+    text: text.slice(0, offset),
+    characterList: chars.slice(0, offset),
+  });
+
+  var keyBelow = generateBlockKey();
+  var blockBelow = blockAbove.merge({
+    key: keyBelow,
+    text: text.slice(offset),
+    characterList: chars.slice(offset),
+  });
+
+  var blocksBefore = blockMap.toSeq().takeUntil(v => v === blockToSplit);
+  var blocksAfter = blockMap.toSeq().skipUntil(v => v === blockToSplit).rest();
+  var newBlocks = blocksBefore.concat(
+      [[blockAbove.getKey(), blockAbove], [blockBelow.getKey(), blockBelow]],
+      blocksAfter
+    ).toOrderedMap();
+
+  return contentState.merge({
+    blockMap: newBlocks,
+    selectionBefore: selectionState,
+    selectionAfter: selectionState.merge({
+      anchorKey: keyBelow,
+      anchorOffset: 0,
+      focusKey: keyBelow,
+      focusOffset: 0,
+      isBackward: false,
+    }),
+  });
+}
+
+module.exports = splitBlockInContentState;
diff --git a/website/README.md b/website/README.md
new file mode 100644
index 0000000..37391a4
--- /dev/null
+++ b/website/README.md
@@ -0,0 +1,36 @@
+# Run the server
+
+The first time, get all the dependencies loaded via
+
+```
+npm install
+```
+
+Then, run the server via
+
+```
+npm start
+Open http://localhost:8080/draft-js/index.html
+```
+
+Anytime you change the contents, just refresh the page and it's going to be updated
+
+# Publish the website
+
+First setup your environment by having two folders, one `draft` and one `draft-gh-pages`. The publish script expects those exact names.
+
+```
+cd ../../
+git clone git@github.com:facebook/draft.git draft-gh-pages
+cd draft-gh-pages
+git checkout origin/gh-pages
+git checkout -b gh-pages
+git push --set-upstream origin gh-pages
+cd ../draft-js/website
+```
+
+Then, after you've done changes, just run the command and it'll automatically build the static version of the site and publish it to gh-pages.
+
+```
+./publish.sh
+```
diff --git a/website/core/DocsSidebar.js b/website/core/DocsSidebar.js
new file mode 100644
index 0000000..cb826d8
--- /dev/null
+++ b/website/core/DocsSidebar.js
@@ -0,0 +1,95 @@
+/**
+ * @providesModule DocsSidebar
+ * @jsx React.DOM
+ */
+
+var Metadata = require('Metadata');
+
+var DocsSidebar = React.createClass({
+  getCategories: function() {
+    var metadatas = Metadata.files.filter(function(metadata) {
+      return metadata.layout === 'docs';
+    });
+
+    // Build a hashmap of article_id -> metadata
+    var articles = {}
+    for (var i = 0; i < metadatas.length; ++i) {
+      var metadata = metadatas[i];
+      articles[metadata.id] = metadata;
+    }
+
+    // Build a hashmap of article_id -> previous_id
+    var previous = {};
+    for (var i = 0; i < metadatas.length; ++i) {
+      var metadata = metadatas[i];
+      if (metadata.next) {
+        if (!articles[metadata.next]) {
+          throw '`next: ' + metadata.next + '` in ' + metadata.id + ' doesn\'t exist';
+        }
+        previous[articles[metadata.next].id] = metadata.id;
+      }
+    }
+
+    // Find the first element which doesn't have any previous
+    var first = null;
+    for (var i = 0; i < metadatas.length; ++i) {
+      var metadata = metadatas[i];
+      if (!previous[metadata.id] && !metadata.lang) {
+        first = metadata;
+        break;
+      }
+    }
+
+    var categories = [];
+    var currentCategory = null;
+
+    var metadata = first;
+    var i = 0;
+    while (metadata && i++ < 1000) {
+      if (!currentCategory || metadata.category !== currentCategory.name) {
+        currentCategory && categories.push(currentCategory);
+        currentCategory = {
+          name: metadata.category,
+          links: []
+        }
+      }
+      currentCategory.links.push(metadata);
+      metadata = articles[metadata.next];
+    }
+    categories.push(currentCategory);
+
+    return categories;
+  },
+
+  getLink: function(metadata) {
+    if (metadata.permalink.match(/^https?:/)) {
+      return metadata.permalink;
+    }
+    return '/draft-js/' + metadata.permalink + '#content';
+  },
+
+  render: function() {
+    return <div className="nav-docs">
+      {this.getCategories().map((category) =>
+        <div className="nav-docs-section" key={category.name}>
+          <h3>{category.name}</h3>
+          <ul>
+            {category.links.map((metadata) =>
+              <li key={metadata.id}>
+                <a
+                  target={metadata.permalink.match(/^https?:/) && '_blank'}
+                  style={{marginLeft: metadata.indent ? 20 : 0}}
+                  className={metadata.id === this.props.metadata.id ? 'active' : ''}
+                  href={this.getLink(metadata)}>
+                  {metadata.title}
+                </a>
+              </li>
+            )}
+          </ul>
+        </div>
+      )}
+    </div>;
+  }
+});
+
+module.exports = DocsSidebar;
diff --git a/website/core/H2.js b/website/core/H2.js
new file mode 100644
index 0000000..457099d
--- /dev/null
+++ b/website/core/H2.js
@@ -0,0 +1,15 @@
+/**
+ * @providesModule H2
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var Header = require('Header');
+
+var H2 = React.createClass({
+  render: function() {
+    return <Header {...this.props} level={2}>{this.props.children}</Header>
+  }
+});
+
+module.exports = H2;
diff --git a/website/core/Header.js b/website/core/Header.js
new file mode 100644
index 0000000..87fdb84
--- /dev/null
+++ b/website/core/Header.js
@@ -0,0 +1,53 @@
+/**
+ * @providesModule Header
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+
+var Header = React.createClass({
+  slug: function(string) {
+    //  var accents = "àáäâèéëêìíïîòóöôùúüûñç";
+    var accents = "\u00e0\u00e1\u00e4\u00e2\u00e8"
+      + "\u00e9\u00eb\u00ea\u00ec\u00ed\u00ef"
+      + "\u00ee\u00f2\u00f3\u00f6\u00f4\u00f9"
+      + "\u00fa\u00fc\u00fb\u00f1\u00e7";
+
+    var without = "aaaaeeeeiiiioooouuuunc";
+
+    return string
+      .toString()
+
+      // Handle uppercase characters
+      .toLowerCase()
+
+      // Handle accentuated characters
+      .replace(
+        new RegExp('[' + accents + ']', 'g'),
+        function (c) { return without.charAt(accents.indexOf(c)); })
+
+      // Dash special characters
+      .replace(/[^a-z0-9]/g, '-')
+
+      // Compress multiple dash
+      .replace(/-+/g, '-')
+
+      // Trim dashes
+      .replace(/^-|-$/g, '');
+  },
+
+  render: function() {
+    var slug = this.slug(this.props.toSlug || this.props.children);
+    var Heading = 'h' + this.props.level;
+
+    return (
+      <Heading {...this.props}>
+        <a className="anchor" name={slug}></a>
+        {this.props.children}
+        {' '}<a className="hash-link" href={'#' + slug}>#</a>
+      </Heading>
+    );
+  }
+});
+
+module.exports = Header;
diff --git a/website/core/HeaderLinks.js b/website/core/HeaderLinks.js
new file mode 100644
index 0000000..db0a703
--- /dev/null
+++ b/website/core/HeaderLinks.js
@@ -0,0 +1,31 @@
+/**
+ * @providesModule HeaderLinks
+ * @jsx React.DOM
+ */
+
+var HeaderLinks = React.createClass({
+  links: [
+    {section: 'docs', href: '/draft-js/docs/overview.html#content', text: 'docs'},
+    {section: 'github', href: 'http://github.com/facebook/draft-js', text: 'github'},
+  ],
+
+  render: function() {
+    return (
+      <ul className="nav-site">
+        {this.links.map(function(link) {
+          return (
+            <li key={link.section}>
+              <a
+                href={link.href}
+                className={link.section === this.props.section ? 'active' : ''}>
+                {link.text}
+              </a>
+            </li>
+          );
+        }, this)}
+      </ul>
+    );
+  }
+});
+
+module.exports = HeaderLinks;
diff --git a/website/core/Marked.js b/website/core/Marked.js
new file mode 100644
index 0000000..abbf549
--- /dev/null
+++ b/website/core/Marked.js
@@ -0,0 +1,1095 @@
+/**
+ * marked - a markdown parser
+ * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
+ * https://github.com/chjj/marked
+ *
+ * @providesModule Marked
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var Prism = require('Prism');
+var Header = require('Header');
+
+/**
+ * Block-Level Grammar
+ */
+
+var block = {
+  newline: /^\n+/,
+  code: /^( {4}[^\n]+\n*)+/,
+  fences: noop,
+  hr: /^( *[-*_]){3,} *(?:\n+|$)/,
+  heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/,
+  nptable: noop,
+  lheading: /^([^\n]+)\n *(=|-){3,} *\n*/,
+  blockquote: /^( *>[^\n]+(\n[^\n]+)*\n*)+/,
+  list: /^( *)(bull) [\s\S]+?(?:hr|\n{2,}(?! )(?!\1bull )\n*|\s*$)/,
+  html: /^ *(?:comment|closed|closing) *(?:\n{2,}|\s*$)/,
+  def: /^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/,
+  table: noop,
+  paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/,
+  text: /^[^\n]+/
+};
+
+block.bullet = /(?:[*+-]|\d+\.)/;
+block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/;
+block.item = replace(block.item, 'gm')
+  (/bull/g, block.bullet)
+  ();
+
+block.list = replace(block.list)
+  (/bull/g, block.bullet)
+  ('hr', /\n+(?=(?: *[-*_]){3,} *(?:\n+|$))/)
+  ();
+
+block._tag = '(?!(?:'
+  + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code'
+  + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo'
+  + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|@)\\b';
+
+block.html = replace(block.html)
+  ('comment', /<!--[\s\S]*?-->/)
+  ('closed', /<(tag)[\s\S]+?<\/\1>/)
+  ('closing', /<tag(?:"[^"]*"|'[^']*'|[^'">])*?>/)
+  (/tag/g, block._tag)
+  ();
+
+block.paragraph = replace(block.paragraph)
+  ('hr', block.hr)
+  ('heading', block.heading)
+  ('lheading', block.lheading)
+  ('blockquote', block.blockquote)
+  ('tag', '<' + block._tag)
+  ('def', block.def)
+  ();
+
+/**
+ * Normal Block Grammar
+ */
+
+block.normal = merge({}, block);
+
+/**
+ * GFM Block Grammar
+ */
+
+block.gfm = merge({}, block.normal, {
+  fences: /^ *(`{3,}|~{3,}) *(\S+)? *\n([\s\S]+?)\s*\1 *(?:\n+|$)/,
+  paragraph: /^/
+});
+
+block.gfm.paragraph = replace(block.paragraph)
+  ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|')
+  ();
+
+/**
+ * GFM + Tables Block Grammar
+ */
+
+block.tables = merge({}, block.gfm, {
+  nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/,
+  table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/
+});
+
+/**
+ * Block Lexer
+ */
+
+function Lexer(options) {
+  this.tokens = [];
+  this.tokens.links = {};
+  this.options = options || marked.defaults;
+  this.rules = block.normal;
+
+  if (this.options.gfm) {
+    if (this.options.tables) {
+      this.rules = block.tables;
+    } else {
+      this.rules = block.gfm;
+    }
+  }
+}
+
+/**
+ * Expose Block Rules
+ */
+
+Lexer.rules = block;
+
+/**
+ * Static Lex Method
+ */
+
+Lexer.lex = function(src, options) {
+  var lexer = new Lexer(options);
+  return lexer.lex(src);
+};
+
+/**
+ * Preprocessing
+ */
+
+Lexer.prototype.lex = function(src) {
+  src = src
+    .replace(/\r\n|\r/g, '\n')
+    .replace(/\t/g, '    ')
+    .replace(/\u00a0/g, ' ')
+    .replace(/\u2424/g, '\n');
+
+  return this.token(src, true);
+};
+
+/**
+ * Lexing
+ */
+
+Lexer.prototype.token = function(src, top) {
+  var src = src.replace(/^ +$/gm, '')
+    , next
+    , loose
+    , cap
+    , bull
+    , b
+    , item
+    , space
+    , i
+    , l;
+
+  while (src) {
+    // newline
+    if (cap = this.rules.newline.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[0].length > 1) {
+        this.tokens.push({
+          type: 'space'
+        });
+      }
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      cap = cap[0].replace(/^ {4}/gm, '');
+      this.tokens.push({
+        type: 'code',
+        text: !this.options.pedantic
+          ? cap.replace(/\n+$/, '')
+          : cap
+      });
+      continue;
+    }
+
+    // fences (gfm)
+    if (cap = this.rules.fences.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'code',
+        lang: cap[2],
+        text: cap[3]
+      });
+      continue;
+    }
+
+    // heading
+    if (cap = this.rules.heading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[1].length,
+        text: cap[2]
+      });
+      continue;
+    }
+
+    // table no leading pipe (gfm)
+    if (top && (cap = this.rules.nptable.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i].split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // lheading
+    if (cap = this.rules.lheading.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'heading',
+        depth: cap[2] === '=' ? 1 : 2,
+        text: cap[1]
+      });
+      continue;
+    }
+
+    // hr
+    if (cap = this.rules.hr.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'hr'
+      });
+      continue;
+    }
+
+    // blockquote
+    if (cap = this.rules.blockquote.exec(src)) {
+      src = src.substring(cap[0].length);
+
+      this.tokens.push({
+        type: 'blockquote_start'
+      });
+
+      cap = cap[0].replace(/^ *> ?/gm, '');
+
+      // Pass `top` to keep the current
+      // "toplevel" state. This is exactly
+      // how markdown.pl works.
+      this.token(cap, top);
+
+      this.tokens.push({
+        type: 'blockquote_end'
+      });
+
+      continue;
+    }
+
+    // list
+    if (cap = this.rules.list.exec(src)) {
+      src = src.substring(cap[0].length);
+      bull = cap[2];
+
+      this.tokens.push({
+        type: 'list_start',
+        ordered: bull.length > 1
+      });
+
+      // Get each top-level item.
+      cap = cap[0].match(this.rules.item);
+
+      next = false;
+      l = cap.length;
+      i = 0;
+
+      for (; i < l; i++) {
+        item = cap[i];
+
+        // Remove the list item's bullet
+        // so it is seen as the next token.
+        space = item.length;
+        item = item.replace(/^ *([*+-]|\d+\.) +/, '');
+
+        // Outdent whatever the
+        // list item contains. Hacky.
+        if (~item.indexOf('\n ')) {
+          space -= item.length;
+          item = !this.options.pedantic
+            ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '')
+            : item.replace(/^ {1,4}/gm, '');
+        }
+
+        // Determine whether the next list item belongs here.
+        // Backpedal if it does not belong in this list.
+        if (this.options.smartLists && i !== l - 1) {
+          b = block.bullet.exec(cap[i+1])[0];
+          if (bull !== b && !(bull.length > 1 && b.length > 1)) {
+            src = cap.slice(i + 1).join('\n') + src;
+            i = l - 1;
+          }
+        }
+
+        // Determine whether item is loose or not.
+        // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/
+        // for discount behavior.
+        loose = next || /\n\n(?!\s*$)/.test(item);
+        if (i !== l - 1) {
+          next = item[item.length-1] === '\n';
+          if (!loose) loose = next;
+        }
+
+        this.tokens.push({
+          type: loose
+            ? 'loose_item_start'
+            : 'list_item_start'
+        });
+
+        // Recurse.
+        this.token(item, false);
+
+        this.tokens.push({
+          type: 'list_item_end'
+        });
+      }
+
+      this.tokens.push({
+        type: 'list_end'
+      });
+
+      continue;
+    }
+
+    // html
+    if (cap = this.rules.html.exec(src)) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: this.options.sanitize
+          ? 'paragraph'
+          : 'html',
+        pre: cap[1] === 'pre' || cap[1] === 'script',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    // def
+    if (top && (cap = this.rules.def.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.links[cap[1].toLowerCase()] = {
+        href: cap[2],
+        title: cap[3]
+      };
+      continue;
+    }
+
+    // table (gfm)
+    if (top && (cap = this.rules.table.exec(src))) {
+      src = src.substring(cap[0].length);
+
+      item = {
+        type: 'table',
+        header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */),
+        align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */),
+        cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n')
+      };
+
+      for (i = 0; i < item.align.length; i++) {
+        if (/^ *-+: *$/.test(item.align[i])) {
+          item.align[i] = 'right';
+        } else if (/^ *:-+: *$/.test(item.align[i])) {
+          item.align[i] = 'center';
+        } else if (/^ *:-+ *$/.test(item.align[i])) {
+          item.align[i] = 'left';
+        } else {
+          item.align[i] = null;
+        }
+      }
+
+      for (i = 0; i < item.cells.length; i++) {
+        item.cells[i] = item.cells[i]
+          .replace(/^ *\| *| *\| *$/g, '')
+          .split(/ *\| */);
+      }
+
+      this.tokens.push(item);
+
+      continue;
+    }
+
+    // top-level paragraph
+    if (top && (cap = this.rules.paragraph.exec(src))) {
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'paragraph',
+        text: cap[1][cap[1].length-1] === '\n'
+          ? cap[1].slice(0, -1)
+          : cap[1]
+      });
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      // Top-level should never reach here.
+      src = src.substring(cap[0].length);
+      this.tokens.push({
+        type: 'text',
+        text: cap[0]
+      });
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return this.tokens;
+};
+
+/**
+ * Inline-Level Grammar
+ */
+
+var inline = {
+  escape: /^\\([\\`*{}\[\]()#+\-.!_>])/,
+  autolink: /^<([^ >]+(@|:\/)[^ >]+)>/,
+  url: noop,
+  tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
+  link: /^!?\[(inside)\]\(href\)/,
+  reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/,
+  nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/,
+  strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/,
+  em: /^\b_((?:__|[\s\S])+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/,
+  code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
+  br: /^ {2,}\n(?!\s*$)/,
+  del: noop,
+  text: /^[\s\S]+?(?=[\\<!\[_*`]| {2,}\n|$)/
+};
+
+inline._inside = /(?:\[[^\]]*\]|[^\]]|\](?=[^\[]*\]))*/;
+inline._href = /\s*<?([^\s]*?)>?(?:\s+['"]([\s\S]*?)['"])?\s*/;
+
+inline.link = replace(inline.link)
+  ('inside', inline._inside)
+  ('href', inline._href)
+  ();
+
+inline.reflink = replace(inline.reflink)
+  ('inside', inline._inside)
+  ();
+
+/**
+ * Normal Inline Grammar
+ */
+
+inline.normal = merge({}, inline);
+
+/**
+ * Pedantic Inline Grammar
+ */
+
+inline.pedantic = merge({}, inline.normal, {
+  strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,
+  em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/
+});
+
+/**
+ * GFM Inline Grammar
+ */
+
+inline.gfm = merge({}, inline.normal, {
+  escape: replace(inline.escape)('])', '~|])')(),
+  url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/,
+  del: /^~~(?=\S)([\s\S]*?\S)~~/,
+  text: replace(inline.text)
+    (']|', '~]|')
+    ('|', '|https?://|')
+    ()
+});
+
+/**
+ * GFM + Line Breaks Inline Grammar
+ */
+
+inline.breaks = merge({}, inline.gfm, {
+  br: replace(inline.br)('{2,}', '*')(),
+  text: replace(inline.gfm.text)('{2,}', '*')()
+});
+
+/**
+ * Inline Lexer & Compiler
+ */
+
+function InlineLexer(links, options) {
+  this.options = options || marked.defaults;
+  this.links = links;
+  this.rules = inline.normal;
+
+  if (!this.links) {
+    throw new
+      Error('Tokens array requires a `links` property.');
+  }
+
+  if (this.options.gfm) {
+    if (this.options.breaks) {
+      this.rules = inline.breaks;
+    } else {
+      this.rules = inline.gfm;
+    }
+  } else if (this.options.pedantic) {
+    this.rules = inline.pedantic;
+  }
+}
+
+/**
+ * Expose Inline Rules
+ */
+
+InlineLexer.rules = inline;
+
+/**
+ * Static Lexing/Compiling Method
+ */
+
+InlineLexer.output = function(src, links, options) {
+  var inline = new InlineLexer(links, options);
+  return inline.output(src);
+};
+
+/**
+ * Lexing/Compiling
+ */
+
+InlineLexer.prototype.output = function(src) {
+  var out = []
+    , link
+    , text
+    , href
+    , cap;
+
+  while (src) {
+    // escape
+    if (cap = this.rules.escape.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(cap[1]);
+      continue;
+    }
+
+    // autolink
+    if (cap = this.rules.autolink.exec(src)) {
+      src = src.substring(cap[0].length);
+      if (cap[2] === '@') {
+        text = cap[1][6] === ':'
+          ? cap[1].substring(7)
+          : cap[1];
+        href = 'mailto:' + text;
+      } else {
+        text = cap[1];
+        href = text;
+      }
+      out.push(React.DOM.a({href: this.sanitizeUrl(href)}, text));
+      continue;
+    }
+
+    // url (gfm)
+    if (cap = this.rules.url.exec(src)) {
+      src = src.substring(cap[0].length);
+      text = cap[1];
+      href = text;
+      out.push(React.DOM.a({href: this.sanitizeUrl(href)}, text));
+      continue;
+    }
+
+    // tag
+    if (cap = this.rules.tag.exec(src)) {
+      src = src.substring(cap[0].length);
+      // TODO(alpert): Don't escape if sanitize is false
+      out.push(cap[0]);
+      continue;
+    }
+
+    // link
+    if (cap = this.rules.link.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(this.outputLink(cap, {
+        href: cap[2],
+        title: cap[3]
+      }));
+      continue;
+    }
+
+    // reflink, nolink
+    if ((cap = this.rules.reflink.exec(src))
+        || (cap = this.rules.nolink.exec(src))) {
+      src = src.substring(cap[0].length);
+      link = (cap[2] || cap[1]).replace(/\s+/g, ' ');
+      link = this.links[link.toLowerCase()];
+      if (!link || !link.href) {
+        out.push.apply(out, this.output(cap[0][0]));
+        src = cap[0].substring(1) + src;
+        continue;
+      }
+      out.push(this.outputLink(cap, link));
+      continue;
+    }
+
+    // strong
+    if (cap = this.rules.strong.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(React.DOM.strong(null, this.output(cap[2] || cap[1])));
+      continue;
+    }
+
+    // em
+    if (cap = this.rules.em.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(React.DOM.em(null, this.output(cap[2] || cap[1])));
+      continue;
+    }
+
+    // code
+    if (cap = this.rules.code.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(React.DOM.code(null, cap[2]));
+      continue;
+    }
+
+    // br
+    if (cap = this.rules.br.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(React.DOM.br(null, null));
+      continue;
+    }
+
+    // del (gfm)
+    if (cap = this.rules.del.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(React.DOM.del(null, this.output(cap[1])));
+      continue;
+    }
+
+    // text
+    if (cap = this.rules.text.exec(src)) {
+      src = src.substring(cap[0].length);
+      out.push(this.smartypants(cap[0]));
+      continue;
+    }
+
+    if (src) {
+      throw new
+        Error('Infinite loop on byte: ' + src.charCodeAt(0));
+    }
+  }
+
+  return out;
+};
+
+/**
+ * Sanitize a URL for a link or image
+ */
+
+InlineLexer.prototype.sanitizeUrl = function(url) {
+  if (this.options.sanitize) {
+    try {
+      var prot = decodeURIComponent(url)
+          .replace(/[^A-Za-z0-9:]/g, '')
+          .toLowerCase();
+      if (prot.indexOf('javascript:') === 0) {
+        return '#';
+      }
+    } catch (e) {
+      return '#';
+    }
+  }
+  return url;
+};
+
+/**
+ * Compile Link
+ */
+
+InlineLexer.prototype.outputLink = function(cap, link) {
+  if (cap[0][0] !== '!') {
+    var shouldOpenInNewWindow =
+      link.href.charAt(0) !== '/'
+      && link.href.charAt(0) !== '#';
+
+    return React.DOM.a({
+      href: this.sanitizeUrl(link.href),
+      title: link.title,
+      target: shouldOpenInNewWindow ? '_blank' : ''
+    }, this.output(cap[1]));
+  } else {
+    return React.DOM.img({
+      src: this.sanitizeUrl(link.href),
+      alt: cap[1],
+      title: link.title
+    }, null);
+  }
+};
+
+/**
+ * Smartypants Transformations
+ */
+
+InlineLexer.prototype.smartypants = function(text) {
+  if (!this.options.smartypants) return text;
+  return text
+    .replace(/--/g, '\u2014')
+    .replace(/'([^']*)'/g, '\u2018$1\u2019')
+    .replace(/"([^"]*)"/g, '\u201C$1\u201D')
+    .replace(/\.{3}/g, '\u2026');
+};
+
+/**
+ * Parsing & Compiling
+ */
+
+function Parser(options) {
+  this.tokens = [];
+  this.token = null;
+  this.options = options || marked.defaults;
+}
+
+/**
+ * Static Parse Method
+ */
+
+Parser.parse = function(src, options) {
+  var parser = new Parser(options);
+  return parser.parse(src);
+};
+
+/**
+ * Parse Loop
+ */
+
+Parser.prototype.parse = function(src) {
+  this.inline = new InlineLexer(src.links, this.options);
+  this.tokens = src.reverse();
+
+  var out = [];
+  while (this.next()) {
+    out.push(this.tok());
+  }
+
+  return out;
+};
+
+/**
+ * Next Token
+ */
+
+Parser.prototype.next = function() {
+  return this.token = this.tokens.pop();
+};
+
+/**
+ * Preview Next Token
+ */
+
+Parser.prototype.peek = function() {
+  return this.tokens[this.tokens.length-1] || 0;
+};
+
+/**
+ * Parse Text Tokens
+ */
+
+Parser.prototype.parseText = function() {
+  var body = this.token.text;
+
+  while (this.peek().type === 'text') {
+    body += '\n' + this.next().text;
+  }
+
+  return this.inline.output(body);
+};
+
+/**
+ * Parse Current Token
+ */
+
+Parser.prototype.tok = function() {
+  switch (this.token.type) {
+    case 'space': {
+      return [];
+    }
+    case 'hr': {
+      return React.DOM.hr(null, null);
+    }
+    case 'heading': {
+      return (
+        <Header level={this.token.depth} toSlug={this.token.text}>
+          {this.inline.output(this.token.text)}
+        </Header>
+      );
+    }
+    case 'code': {
+      return <Prism>{this.token.text}</Prism>;
+    }
+    case 'table': {
+      var table = []
+        , body = []
+        , row = []
+        , heading
+        , i
+        , cells
+        , j;
+
+      // header
+      for (i = 0; i < this.token.header.length; i++) {
+        heading = this.inline.output(this.token.header[i]);
+        row.push(React.DOM.th(
+          this.token.align[i]
+            ? {style: {textAlign: this.token.align[i]}}
+            : null,
+          heading
+        ));
+      }
+      table.push(React.DOM.thead(null, React.DOM.tr(null, row)));
+
+      // body
+      for (i = 0; i < this.token.cells.length; i++) {
+        row = [];
+        cells = this.token.cells[i];
+        for (j = 0; j < cells.length; j++) {
+          row.push(React.DOM.td(
+            this.token.align[j]
+              ? {style: {textAlign: this.token.align[j]}}
+              : null,
+            this.inline.output(cells[j])
+          ));
+        }
+        body.push(React.DOM.tr(null, row));
+      }
+      table.push(React.DOM.thead(null, body));
+
+      return React.DOM.table(null, table);
+    }
+    case 'blockquote_start': {
+      var body = [];
+
+      while (this.next().type !== 'blockquote_end') {
+        body.push(this.tok());
+      }
+
+      return React.DOM.blockquote(null, body);
+    }
+    case 'list_start': {
+      var type = this.token.ordered ? 'ol' : 'ul'
+        , body = [];
+
+      while (this.next().type !== 'list_end') {
+        body.push(this.tok());
+      }
+
+      return React.DOM[type](null, body);
+    }
+    case 'list_item_start': {
+      var body = [];
+
+      while (this.next().type !== 'list_item_end') {
+        body.push(this.token.type === 'text'
+          ? this.parseText()
+          : this.tok());
+      }
+
+      return React.DOM.li(null, body);
+    }
+    case 'loose_item_start': {
+      var body = [];
+
+      while (this.next().type !== 'list_item_end') {
+        body.push(this.tok());
+      }
+
+      return React.DOM.li(null, body);
+    }
+    case 'html': {
+      return React.DOM.div({
+        dangerouslySetInnerHTML: {
+          __html: this.token.text
+        }
+      });
+    }
+    case 'paragraph': {
+      return this.options.paragraphFn
+        ? this.options.paragraphFn.call(null, this.inline.output(this.token.text))
+        : React.DOM.p(null, this.inline.output(this.token.text));
+    }
+    case 'text': {
+      return this.options.paragraphFn
+        ? this.options.paragraphFn.call(null, this.parseText())
+        : React.DOM.p(null, this.parseText());
+    }
+  }
+};
+
+/**
+ * Helpers
+ */
+
+function escape(html, encode) {
+  return html
+    .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&amp;')
+    .replace(/</g, '&lt;')
+    .replace(/>/g, '&gt;')
+    .replace(/"/g, '&quot;')
+    .replace(/'/g, '&#39;');
+}
+
+function replace(regex, opt) {
+  regex = regex.source;
+  opt = opt || '';
+  return function self(name, val) {
+    if (!name) return new RegExp(regex, opt);
+    val = val.source || val;
+    val = val.replace(/(^|[^\[])\^/g, '$1');
+    regex = regex.replace(name, val);
+    return self;
+  };
+}
+
+function noop() {}
+noop.exec = noop;
+
+function merge(obj) {
+  var i = 1
+    , target
+    , key;
+
+  for (; i < arguments.length; i++) {
+    target = arguments[i];
+    for (key in target) {
+      if (Object.prototype.hasOwnProperty.call(target, key)) {
+        obj[key] = target[key];
+      }
+    }
+  }
+
+  return obj;
+}
+
+/**
+ * Marked
+ */
+
+function marked(src, opt, callback) {
+  if (callback || typeof opt === 'function') {
+    if (!callback) {
+      callback = opt;
+      opt = null;
+    }
+
+    if (opt) opt = merge({}, marked.defaults, opt);
+
+    var highlight = opt.highlight
+      , tokens
+      , pending
+      , i = 0;
+
+    try {
+      tokens = Lexer.lex(src, opt)
+    } catch (e) {
+      return callback(e);
+    }
+
+    pending = tokens.length;
+
+    var done = function(hi) {
+      var out, err;
+
+      if (hi !== true) {
+        delete opt.highlight;
+      }
+
+      try {
+        out = Parser.parse(tokens, opt);
+      } catch (e) {
+        err = e;
+      }
+
+      opt.highlight = highlight;
+
+      return err
+        ? callback(err)
+        : callback(null, out);
+    };
+
+    if (!highlight || highlight.length < 3) {
+      return done(true);
+    }
+
+    if (!pending) return done();
+
+    for (; i < tokens.length; i++) {
+      (function(token) {
+        if (token.type !== 'code') {
+          return --pending || done();
+        }
+        return highlight(token.text, token.lang, function(err, code) {
+          if (code == null || code === token.text) {
+            return --pending || done();
+          }
+          token.text = code;
+          token.escaped = true;
+          --pending || done();
+        });
+      })(tokens[i]);
+    }
+
+    return;
+  }
+  try {
+    if (opt) opt = merge({}, marked.defaults, opt);
+    return Parser.parse(Lexer.lex(src, opt), opt);
+  } catch (e) {
+    e.message += '\nPlease report this to https://github.com/chjj/marked.';
+    if ((opt || marked.defaults).silent) {
+      return [React.DOM.p(null, "An error occurred:"),
+        React.DOM.pre(null, e.message)];
+    }
+    throw e;
+  }
+}
+
+/**
+ * Options
+ */
+
+marked.options =
+marked.setOptions = function(opt) {
+  merge(marked.defaults, opt);
+  return marked;
+};
+
+marked.defaults = {
+  gfm: true,
+  tables: true,
+  breaks: false,
+  pedantic: false,
+  sanitize: false,
+  smartLists: false,
+  silent: false,
+  highlight: null,
+  langPrefix: 'lang-',
+  smartypants: false,
+  paragraphFn: null
+};
+
+/**
+ * Expose
+ */
+
+marked.Parser = Parser;
+marked.parser = Parser.parse;
+
+marked.Lexer = Lexer;
+marked.lexer = Lexer.lex;
+
+marked.InlineLexer = InlineLexer;
+marked.inlineLexer = InlineLexer.output;
+
+marked.parse = marked;
+
+var Marked = React.createClass({
+  render: function() {
+    return <div>{marked(this.props.children, this.props)}</div>;
+  }
+});
+
+module.exports = Marked;
diff --git a/website/core/Prism.js b/website/core/Prism.js
new file mode 100644
index 0000000..e56261c
--- /dev/null
+++ b/website/core/Prism.js
@@ -0,0 +1,361 @@
+/**
+ * Prism: Lightweight, robust, elegant syntax highlighting
+ * MIT license http://www.opensource.org/licenses/mit-license.php/
+ * @author Lea Verou http://lea.verou.me
+ *
+ * @providesModule Prism
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var unindent = require('unindent');
+
+// Private helper vars
+var lang = /\blang(?:uage)?-(?!\*)(\w+)\b/i;
+
+var _ = {
+  util: {
+    type: function (o) {
+      return Object.prototype.toString.call(o).match(/\[object (\w+)\]/)[1];
+    },
+
+    // Deep clone a language definition (e.g. to extend it)
+    clone: function (o) {
+      var type = _.util.type(o);
+
+      switch (type) {
+        case 'Object':
+          var clone = {};
+
+          for (var key in o) {
+            if (o.hasOwnProperty(key)) {
+              clone[key] = _.util.clone(o[key]);
+            }
+          }
+
+          return clone;
+
+        case 'Array':
+          return o.slice();
+      }
+
+      return o;
+    }
+  },
+
+  languages: {
+    extend: function (id, redef) {
+      var lang = _.util.clone(_.languages[id]);
+
+      for (var key in redef) {
+        lang[key] = redef[key];
+      }
+
+      return lang;
+    },
+
+    // Insert a token before another token in a language literal
+    insertBefore: function (inside, before, insert, root) {
+      root = root || _.languages;
+      var grammar = root[inside];
+      var ret = {};
+
+      for (var token in grammar) {
+
+        if (grammar.hasOwnProperty(token)) {
+
+          if (token == before) {
+
+            for (var newToken in insert) {
+
+              if (insert.hasOwnProperty(newToken)) {
+                ret[newToken] = insert[newToken];
+              }
+            }
+          }
+
+          ret[token] = grammar[token];
+        }
+      }
+
+      return root[inside] = ret;
+    },
+
+    // Traverse a language definition with Depth First Search
+    DFS: function(o, callback) {
+      for (var i in o) {
+        callback.call(o, i, o[i]);
+
+        if (_.util.type(o) === 'Object') {
+          _.languages.DFS(o[i], callback);
+        }
+      }
+    }
+  },
+
+  tokenize: function(text, grammar, language) {
+    var Token = _.Token;
+
+    var strarr = [text];
+
+    var rest = grammar.rest;
+
+    if (rest) {
+      for (var token in rest) {
+        grammar[token] = rest[token];
+      }
+
+      delete grammar.rest;
+    }
+
+    tokenloop: for (var token in grammar) {
+      if(!grammar.hasOwnProperty(token) || !grammar[token]) {
+        continue;
+      }
+
+      var pattern = grammar[token],
+        inside = pattern.inside,
+        lookbehind = !!pattern.lookbehind,
+        lookbehindLength = 0;
+
+      pattern = pattern.pattern || pattern;
+
+      for (var i=0; i<strarr.length; i++) { // Don’t cache length as it changes during the loop
+
+        var str = strarr[i];
+
+        if (strarr.length > text.length) {
+          // Something went terribly wrong, ABORT, ABORT!
+          break tokenloop;
+        }
+
+        if (str instanceof Token) {
+          continue;
+        }
+
+        pattern.lastIndex = 0;
+
+        var match = pattern.exec(str);
+
+        if (match) {
+          if(lookbehind) {
+            lookbehindLength = match[1].length;
+          }
+
+          var from = match.index - 1 + lookbehindLength,
+              match = match[0].slice(lookbehindLength),
+              len = match.length,
+              to = from + len,
+            before = str.slice(0, from + 1),
+            after = str.slice(to + 1);
+
+          var args = [i, 1];
+
+          if (before) {
+            args.push(before);
+          }
+
+          var wrapped = new Token(token, inside? _.tokenize(match, inside) : match);
+
+          args.push(wrapped);
+
+          if (after) {
+            args.push(after);
+          }
+
+          Array.prototype.splice.apply(strarr, args);
+        }
+      }
+    }
+
+    return strarr;
+  },
+
+  hooks: {
+    all: {},
+
+    add: function (name, callback) {
+      var hooks = _.hooks.all;
+
+      hooks[name] = hooks[name] || [];
+
+      hooks[name].push(callback);
+    },
+
+    run: function (name, env) {
+      var callbacks = _.hooks.all[name];
+
+      if (!callbacks || !callbacks.length) {
+        return;
+      }
+
+      for (var i=0, callback; callback = callbacks[i++];) {
+        callback(env);
+      }
+    }
+  }
+};
+
+var Token = _.Token = function(type, content) {
+  this.type = type;
+  this.content = content;
+};
+
+Token.reactify = function(o, key) {
+  if (typeof o == 'string') {
+    return o;
+  }
+
+  if (Array.isArray(o)) {
+    return o.map(function(element, i) {
+      return Token.reactify(element, i);
+    });
+  }
+
+  var attributes = {
+    className: 'token ' + o.type,
+    key: key
+  }
+  if (o.type == 'comment') {
+    attributes.spellCheck = true;
+  }
+
+  return React.DOM.span(attributes, Token.reactify(o.content));
+};
+
+_.languages.markup = {
+  'comment': /&lt;!--[\w\W]*?-->/g,
+  'prolog': /&lt;\?.+?\?>/,
+  'doctype': /&lt;!DOCTYPE.+?>/,
+  'cdata': /&lt;!\[CDATA\[[\w\W]*?]]>/i,
+  'tag': {
+    pattern: /&lt;\/?[\w:-]+\s*(?:\s+[\w:-]+(?:=(?:("|')(\\?[\w\W])*?\1|[^\s'">=]+))?\s*)*\/?>/gi,
+    inside: {
+      'tag': {
+        pattern: /^&lt;\/?[\w:-]+/i,
+        inside: {
+          'punctuation': /^&lt;\/?/,
+          'namespace': /^[\w-]+?:/
+        }
+      },
+      'attr-value': {
+        pattern: /=(?:('|")[\w\W]*?(\1)|[^\s>]+)/gi,
+        inside: {
+          'punctuation': /=|>|"/g
+        }
+      },
+      'punctuation': /\/?>/g,
+      'attr-name': {
+        pattern: /[\w:-]+/g,
+        inside: {
+          'namespace': /^[\w-]+?:/
+        }
+      }
+
+    }
+  },
+  'entity': /&amp;#?[\da-z]{1,8};/gi
+};
+
+_.languages.css = {
+  'comment': /\/\*[\w\W]*?\*\//g,
+  'atrule': {
+    pattern: /@[\w-]+?.*?(;|(?=\s*{))/gi,
+    inside: {
+      'punctuation': /[;:]/g
+    }
+  },
+  'url': /url\((["']?).*?\1\)/gi,
+  'selector': /[^\{\}\s][^\{\};]*(?=\s*\{)/g,
+  'property': /(\b|\B)[\w-]+(?=\s*:)/ig,
+  'string': /("|')(\\?.)*?\1/g,
+  'important': /\B!important\b/gi,
+  'ignore': /&(lt|gt|amp);/gi,
+  'punctuation': /[\{\};:]/g
+};
+
+_.languages.insertBefore('markup', 'tag', {
+  'style': {
+    pattern: /(&lt;|<)style[\w\W]*?(>|&gt;)[\w\W]*?(&lt;|<)\/style(>|&gt;)/ig,
+    inside: {
+      'tag': {
+        pattern: /(&lt;|<)style[\w\W]*?(>|&gt;)|(&lt;|<)\/style(>|&gt;)/ig,
+        inside: _.languages.markup.tag.inside
+      },
+      rest: _.languages.css
+    }
+  }
+});
+
+_.languages.clike = {
+  'comment': {
+    pattern: /(^|[^\\])(\/\*[\w\W]*?\*\/|(^|[^:])\/\/.*?(\r?\n|$))/g,
+    lookbehind: true
+  },
+  'string': /("|')(\\?.)*?\1/g,
+  'class-name': {
+    pattern: /((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/ig,
+    lookbehind: true,
+    inside: {
+      punctuation: /(\.|\\)/
+    }
+  },
+  'keyword': /\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/g,
+  'boolean': /\b(true|false)\b/g,
+  'function': {
+    pattern: /[a-z0-9_]+\(/ig,
+    inside: {
+      punctuation: /\(/
+    }
+  },
+  'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/g,
+  'operator': /[-+]{1,2}|!|&lt;=?|>=?|={1,3}|(&amp;){1,2}|\|?\||\?|\*|\/|\~|\^|\%/g,
+  'ignore': /&(lt|gt|amp);/gi,
+  'punctuation': /[{}[\];(),.:]/g
+};
+
+_.languages.javascript = _.languages.extend('clike', {
+  'keyword': /\b(var|let|if|else|while|do|for|return|in|instanceof|function|get|set|new|with|typeof|try|throw|catch|finally|null|break|continue|this)\b/g,
+  'number': /\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?|NaN|-?Infinity)\b/g
+});
+
+_.languages.insertBefore('javascript', 'keyword', {
+  'regex': {
+    pattern: /(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/g,
+    lookbehind: true
+  }
+});
+
+_.languages.insertBefore('markup', 'tag', {
+  'script': {
+    pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)[\w\W]*?(&lt;|<)\/script(>|&gt;)/ig,
+    inside: {
+      'tag': {
+        pattern: /(&lt;|<)script[\w\W]*?(>|&gt;)|(&lt;|<)\/script(>|&gt;)/ig,
+        inside: _.languages.markup.tag.inside
+      },
+      rest: _.languages.javascript
+    }
+  }
+});
+
+var Prism = React.createClass({
+  statics: {
+    _: _
+  },
+  getDefaultProps: function() {
+    return {
+      language: 'javascript'
+    };
+  },
+  render: function() {
+    var grammar = _.languages[this.props.language];
+    return (
+      <pre className={'prism language-' + this.props.language}>
+        {Token.reactify(_.tokenize(this.props.children, grammar))}
+      </pre>
+    );
+  }
+})
+
+module.exports = Prism;
diff --git a/website/core/Site.js b/website/core/Site.js
new file mode 100644
index 0000000..bb140d3
--- /dev/null
+++ b/website/core/Site.js
@@ -0,0 +1,66 @@
+/**
+ * @providesModule Site
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var HeaderLinks = require('HeaderLinks');
+
+var Site = React.createClass({
+  render: function() {
+    return (
+      <html>
+        <head>
+          <meta charSet="utf-8" />
+          <meta httpEquiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+          <title>Draft.js | Rich Text Editor Framework for React</title>
+          <meta name="viewport" content="width=device-width" />
+          <meta property="og:title" content="Draft.js | Rich Text Editor Framework for React" />
+          <meta property="og:type" content="website" />
+          <meta property="og:url" content="http://facebook.github.io/draft-js/index.html" />
+          <meta property="og:description" content="Rich Text Editor Framework for React" />
+
+          <link rel="stylesheet" href="/draft-js/css/draft.css" />
+
+          <script type="text/javascript" src="//use.typekit.net/vqa1hcx.js"></script>
+          <script type="text/javascript">{'try{Typekit.load();}catch(e){}'}</script>
+        </head>
+        <body>
+
+          <div className="container">
+            <div className="nav-main">
+              <div className="wrap">
+                <a className="nav-home" href="/draft-js/">
+                  Draft.js
+                </a>
+                <HeaderLinks section={this.props.section} />
+              </div>
+            </div>
+
+            {this.props.children}
+
+            <footer className="wrap">
+              <div className="right">© 2016 Facebook Inc.</div>
+            </footer>
+          </div>
+
+          <div id="fb-root" />
+          <script dangerouslySetInnerHTML={{__html: `
+            (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
+            (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
+            m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
+            })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+            ga('create', 'UA-387204-10', 'facebook.github.io');
+            ga('send', 'pageview');
+
+            !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)
+            ){js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";
+            fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
+          `}} />
+        </body>
+      </html>
+    );
+  }
+});
+
+module.exports = Site;
diff --git a/website/core/center.js b/website/core/center.js
new file mode 100644
index 0000000..9386613
--- /dev/null
+++ b/website/core/center.js
@@ -0,0 +1,21 @@
+/**
+ * @providesModule center
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+
+var assign = require('object-assign');
+
+var center = React.createClass({
+  render: function() {
+    var {style, ...props} = this.props;
+    style = assign({}, style, {textAlign: 'center'});
+
+    return (
+      <div {...props} style={style}>{this.props.children}</div>
+    );
+  }
+});
+
+module.exports = center;
diff --git a/website/core/metadata.js b/website/core/metadata.js
new file mode 100644
index 0000000..85d8adc
--- /dev/null
+++ b/website/core/metadata.js
@@ -0,0 +1,183 @@
+/**
+ * @generated
+ * @providesModule Metadata
+ */
+module.exports = {
+  "files": [
+    {
+      "id": "advanced-topics-block-components",
+      "title": "Custom Block Components",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-inline-styles",
+      "permalink": "docs/advanced-topics-block-components.html"
+    },
+    {
+      "id": "advanced-topics-block-styling",
+      "title": "Block Styling",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-block-components",
+      "permalink": "docs/advanced-topics-block-styling.html"
+    },
+    {
+      "id": "advanced-topics-decorators",
+      "title": "Decorators",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-key-bindings",
+      "permalink": "docs/advanced-topics-decorators.html"
+    },
+    {
+      "id": "advanced-topics-entities",
+      "title": "Entities",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-decorators",
+      "permalink": "docs/advanced-topics-entities.html"
+    },
+    {
+      "id": "advanced-topics-inline-styles",
+      "title": "Complex Inline Styles",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-nested-lists",
+      "permalink": "docs/advanced-topics-inline-styles.html"
+    },
+    {
+      "id": "advanced-topics-issues-and-pitfalls",
+      "title": "Issues and Pitfalls",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "api-reference-editor",
+      "permalink": "docs/advanced-topics-issues-and-pitfalls.html"
+    },
+    {
+      "id": "advanced-topics-key-bindings",
+      "title": "Key Bindings",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-managing-focus",
+      "permalink": "docs/advanced-topics-key-bindings.html"
+    },
+    {
+      "id": "advanced-topics-managing-focus",
+      "title": "Managing Focus",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-block-styling",
+      "permalink": "docs/advanced-topics-managing-focus.html"
+    },
+    {
+      "id": "advanced-topics-nested-lists",
+      "title": "Nested Lists",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-text-direction",
+      "permalink": "docs/advanced-topics-nested-lists.html"
+    },
+    {
+      "id": "advanced-topics-text-direction",
+      "title": "Text Direction",
+      "layout": "docs",
+      "category": "Advanced Topics",
+      "next": "advanced-topics-issues-and-pitfalls",
+      "permalink": "docs/advanced-topics-text-direction.html"
+    },
+    {
+      "id": "api-reference-character-metadata",
+      "title": "CharacterMetadata",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-entity",
+      "permalink": "docs/api-reference-character-metadata.html"
+    },
+    {
+      "id": "api-reference-content-block",
+      "title": "ContentBlock",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-character-metadata",
+      "permalink": "docs/api-reference-content-block.html"
+    },
+    {
+      "id": "api-reference-content-state",
+      "title": "ContentState",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-content-block",
+      "permalink": "docs/api-reference-content-state.html"
+    },
+    {
+      "id": "api-reference-data-conversion",
+      "title": "Data Conversion",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-modifier",
+      "permalink": "docs/api-reference-data-conversion.html"
+    },
+    {
+      "id": "api-reference-editor",
+      "title": "Editor Component",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-editor-state",
+      "permalink": "docs/api-reference-editor.html"
+    },
+    {
+      "id": "api-reference-editor-state",
+      "title": "EditorState",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-content-state",
+      "permalink": "docs/api-reference-editor-state.html"
+    },
+    {
+      "id": "api-reference-entity",
+      "title": "Entity",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-selection-state",
+      "permalink": "docs/api-reference-entity.html"
+    },
+    {
+      "id": "api-reference-modifier",
+      "title": "Modifier",
+      "layout": "docs",
+      "category": "API Reference",
+      "permalink": "docs/api-reference-modifier.html"
+    },
+    {
+      "id": "api-reference-selection-state",
+      "title": "SelectionState",
+      "layout": "docs",
+      "category": "API Reference",
+      "next": "api-reference-data-conversion",
+      "permalink": "docs/api-reference-selection-state.html"
+    },
+    {
+      "id": "getting-started",
+      "title": "Overview",
+      "layout": "docs",
+      "category": "Quick Start",
+      "next": "quickstart-api-basics",
+      "permalink": "docs/overview.html"
+    },
+    {
+      "id": "quickstart-api-basics",
+      "title": "API Basics",
+      "layout": "docs",
+      "category": "Quick Start",
+      "next": "quickstart-rich-styling",
+      "permalink": "docs/quickstart-api-basics.html"
+    },
+    {
+      "id": "quickstart-rich-styling",
+      "title": "Rich Styling",
+      "layout": "docs",
+      "category": "Quick Start",
+      "next": "advanced-topics-entities",
+      "permalink": "docs/quickstart-rich-styling.html"
+    }
+  ]
+};
\ No newline at end of file
diff --git a/website/core/unindent.js b/website/core/unindent.js
new file mode 100644
index 0000000..9913b2e
--- /dev/null
+++ b/website/core/unindent.js
@@ -0,0 +1,22 @@
+/**
+ * @providesModule unindent
+ */
+
+// Remove the indentation introduced by JSX
+function unindent(code) {
+  var lines = code.split('\n');
+  if (lines[0] === '') {
+    lines.shift();
+  }
+  if (lines.length <= 1) {
+    return code;
+  }
+
+  var indent = lines[0].match(/^\s*/)[0];
+  for (var i = 0; i < lines.length; ++i) {
+   lines[i] = lines[i].replace(new RegExp('^' + indent), '');
+  }
+  return lines.join('\n');
+}
+
+module.exports = unindent;
diff --git a/website/layout/DocsLayout.js b/website/layout/DocsLayout.js
new file mode 100644
index 0000000..1463b8d
--- /dev/null
+++ b/website/layout/DocsLayout.js
@@ -0,0 +1,33 @@
+/**
+ * @providesModule DocsLayout
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var Site = require('Site');
+var Marked = require('Marked');
+var DocsSidebar = require('DocsSidebar');
+var DocsLayout = React.createClass({
+  render: function() {
+    var metadata = this.props.metadata;
+    var content = this.props.children;
+    return (
+      <Site section="docs">
+        <section className="content wrap documentationContent">
+          <DocsSidebar metadata={metadata} />
+          <div className="inner-content">
+            <a id="content" />
+            <h1>{metadata.title}</h1>
+            <Marked>{content}</Marked>
+            <div className="docs-prevnext">
+              {metadata.previous && <a className="docs-prev" href={metadata.previous + '.html#content'}>&larr; Prev</a>}
+              {metadata.next && <a className="docs-next" href={metadata.next + '.html#content'}>Next &rarr;</a>}
+            </div>
+          </div>
+        </section>
+      </Site>
+    );
+  }
+});
+
+module.exports = DocsLayout;
diff --git a/website/layout/PageLayout.js b/website/layout/PageLayout.js
new file mode 100644
index 0000000..ef5aba2
--- /dev/null
+++ b/website/layout/PageLayout.js
@@ -0,0 +1,26 @@
+/**
+ * @providesModule PageLayout
+ * @jsx React.DOM
+ */
+
+var React = require('React');
+var Site = require('Site');
+var Marked = require('Marked');
+
+var support = React.createClass({
+  render: function() {
+    var metadata = this.props.metadata;
+    var content = this.props.children;
+    return (
+      <Site section={metadata.section}>
+        <section className="content wrap documentationContent nosidebar">
+          <div className="inner-content">
+            <Marked>{content}</Marked>
+          </div>
+        </section>
+      </Site>
+    );
+  }
+});
+
+module.exports = support;
diff --git a/website/package.json b/website/package.json
new file mode 100644
index 0000000..9e0e534
--- /dev/null
+++ b/website/package.json
@@ -0,0 +1,25 @@
+{
+  "scripts": {
+    "start": "node server/server.js",
+    "postinstall": "npm install .."
+  },
+  "dependencies": {
+    "compression": "1.2.2",
+    "connect": "3.3.3",
+    "errorhandler": "1.3.0",
+    "fbjs": "^0.8.0-alpha.1",
+    "fs.extra": "*",
+    "glob": "*",
+    "immutable": "^3.7.6",
+    "mkdirp": "*",
+    "morgan": "1.5.0",
+    "object-assign": "^2.0.0",
+    "optimist": "0.6.0",
+    "react": "^0.14.7",
+    "react-dom": "^0.14.7",
+    "react-page-middleware": "git://github.com/facebookarchive/react-page-middleware.git",
+    "request": "*",
+    "serve-favicon": "2.2.0",
+    "serve-static": "1.7.1"
+  }
+}
diff --git a/website/publish.sh b/website/publish.sh
new file mode 100755
index 0000000..655d2cd
--- /dev/null
+++ b/website/publish.sh
@@ -0,0 +1,23 @@
+#!/bin/bash
+
+set -e
+
+# Start in website/ even if run from root directory
+cd "$(dirname "$0")"
+
+cd ../../draft-js-gh-pages
+git checkout -- .
+git clean -dfx
+git fetch
+git rebase
+rm -Rf *
+cd ../draft-js/website
+rm -Rf build/
+node server/generate.js
+cp -R build/draft-js/* ../../draft-js-gh-pages/
+rm -Rf build/
+cd ../../draft-js-gh-pages
+git add --all
+git commit -m "update website"
+git push
+cd ../draft-js/website
diff --git a/website/server/convert.js b/website/server/convert.js
new file mode 100644
index 0000000..3a6270b
--- /dev/null
+++ b/website/server/convert.js
@@ -0,0 +1,121 @@
+var fs = require('fs')
+var glob = require('glob');
+var mkdirp = require('mkdirp');
+var optimist = require('optimist');
+var path = require('path');
+var argv = optimist.argv;
+
+function splitHeader(content) {
+  var lines = content.split('\n');
+  for (var i = 1; i < lines.length - 1; ++i) {
+    if (lines[i] === '---') {
+      break;
+    }
+  }
+  return {
+    header: lines.slice(1, i + 1).join('\n'),
+    content: lines.slice(i + 1).join('\n')
+  };
+}
+
+function backtickify(str) {
+  var escaped = '`' + str.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
+  // Replace require( with require\( so node-haste doesn't replace example
+  // require calls in the docs
+  return escaped.replace(/require\(/g, 'require\\(');
+}
+
+function execute() {
+  var MD_DIR = '../docs/';
+
+  glob('src/draft-js/docs/*.*', function(er, files) {
+    files.forEach(function(file) {
+      try {
+        fs.unlinkSync(file);
+      } catch(e) {
+        /* seriously, unlink throws when the file doesn't exist :( */
+      }
+    });
+  });
+
+  var metadatas = {
+    files: [],
+  };
+
+  glob(MD_DIR + '**/*.*', function (er, files) {
+    files.forEach(function(file) {
+      var extension = path.extname(file);
+      if (extension === '.md' || extension === '.markdown') {
+        var content = fs.readFileSync(file, {encoding: 'utf8'});
+        var metadata = {};
+
+        // Extract markdown metadata header
+        var both = splitHeader(content);
+        var lines = both.header.split('\n');
+        for (var i = 0; i < lines.length - 1; ++i) {
+          var keyvalue = lines[i].split(':');
+          var key = keyvalue[0].trim();
+          var value = keyvalue.slice(1).join(':').trim();
+          // Handle the case where you have "Community #10"
+          try { value = JSON.parse(value); } catch(e) { }
+          metadata[key] = value;
+        }
+        metadatas.files.push(metadata);
+
+        if (metadata.permalink.match(/^https?:/)) {
+          return;
+        }
+
+        // Create a dummy .js version that just calls the associated layout
+        var layout = metadata.layout[0].toUpperCase() + metadata.layout.substr(1) + 'Layout';
+
+        var content = (
+          '/**\n' +
+          ' * @generated\n' +
+          ' */\n' +
+          'var React = require("React");\n' +
+          'var Layout = require("' + layout + '");\n' +
+          'var content = ' + backtickify(both.content) + '\n' +
+          'var Post = React.createClass({\n' +
+          '  statics: {\n' +
+          '    content: content\n' +
+          '  },\n' +
+          '  render: function() {\n' +
+          '    return <Layout metadata={' + JSON.stringify(metadata) + '}>{content}</Layout>;\n' +
+          '  }\n' +
+          '});\n' +
+          'module.exports = Post;\n'
+        );
+
+        var targetFile = 'src/draft-js/' + metadata.permalink.replace(/\.html$/, '.js');
+        mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
+        fs.writeFileSync(targetFile, content);
+      }
+
+      if (extension === '.json') {
+        var content = fs.readFileSync(file, {encoding: 'utf8'});
+        metadatas[path.basename(file, '.json')] = JSON.parse(content);
+      }
+    });
+
+    fs.writeFileSync(
+      'core/metadata.js',
+      '/**\n' +
+      ' * @generated\n' +
+      ' * @providesModule Metadata\n' +
+      ' */\n' +
+      'module.exports = ' + JSON.stringify(metadatas, null, 2) + ';'
+    );
+  });
+
+  fs.writeFileSync('src/draft-js/lib/Draft.css', fs.readFileSync('../dist/Draft.css'));
+  fs.writeFileSync('src/draft-js/lib/Draft.js', fs.readFileSync('../dist/Draft.js'));
+  fs.writeFileSync('src/draft-js/lib/RichEditor.css', fs.readFileSync('../examples/rich/RichEditor.css'));
+}
+
+if (argv.convert) {
+  console.log('convert!')
+  execute();
+}
+
+module.exports = execute;
diff --git a/website/server/generate.js b/website/server/generate.js
new file mode 100644
index 0000000..64c0d3a
--- /dev/null
+++ b/website/server/generate.js
@@ -0,0 +1,63 @@
+
+var request = require('request');
+var glob = require('glob');
+var fs = require('fs.extra');
+var mkdirp = require('mkdirp');
+var server = require('./server.js');
+
+// Sadly, our setup fatals when doing multiple concurrent requests
+// I don't have the time to dig into why, it's easier to just serialize
+// requests.
+var queue = (function() {
+  var is_executing = false;
+  var queue = [];
+  function push(fn) {
+    queue.push(fn);
+    execute();
+  }
+  function execute() {
+    if (is_executing) {
+      return;
+    }
+    if (queue.length === 0) {
+      return;
+    }
+    var fn = queue.shift();
+    is_executing = true;
+    fn(function() {
+      is_executing = false;
+      execute()
+    });
+  }
+  return {push: push};
+})();
+
+glob('src/**/*.*', function(er, files) {
+  var count = files.length;
+
+  files.forEach(function(file) {
+    var targetFile = file.replace(/^src/, 'build');
+
+    if (file.match(/\.js$/) && !file.match(/\/lib\//)) {
+      targetFile = targetFile.replace(/\.js$/, '.html');
+      queue.push(function(cb) {
+        request('http://localhost:8079/' + targetFile.replace(/^build\//, ''), function(error, response, body) {
+          mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
+          fs.writeFileSync(targetFile, body);
+          cb();
+        });
+      });
+    } else {
+      queue.push(function(cb) {
+        mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
+        fs.copy(file, targetFile, cb);
+      });
+    }
+  });
+
+  queue.push(function(cb) {
+    server.close();
+    console.log('It is live at: http://facebook.github.io/draft-js/')
+    cb();
+  });
+});
diff --git a/website/server/server.js b/website/server/server.js
new file mode 100644
index 0000000..24a88a5
--- /dev/null
+++ b/website/server/server.js
@@ -0,0 +1,59 @@
+"use strict";
+
+var compression = require('compression');
+var connect = require('connect');
+var convert = require('./convert.js');
+var errorHandler = require('errorhandler');
+var http = require('http');
+var morgan = require('morgan');
+var optimist = require('optimist');
+var path = require('path');
+var reactMiddleware = require('react-page-middleware');
+var serveFavicon = require('serve-favicon');
+var serveStatic = require('serve-static');
+
+var argv = optimist.argv;
+
+var PROJECT_ROOT = path.resolve(__dirname, '..');
+var FILE_SERVE_ROOT = path.join(PROJECT_ROOT, 'src');
+
+var port = argv.port;
+if (process.argv[1] === path.resolve(__dirname, 'generate.js')) {
+  // Using a different port so that you can publish the website
+  // and keeping the server up at the same time.
+  port = 8079;
+}
+
+var buildOptions = {
+  projectRoot: PROJECT_ROOT,
+  pageRouteRoot: FILE_SERVE_ROOT,
+  useBrowserBuiltins: false,
+  logTiming: true,
+  useSourceMaps: true,
+  ignorePaths: function(p) {
+    return p.indexOf('__tests__') !== -1;
+  },
+  serverRender: true,
+  dev: argv.dev !== 'false',
+  static: true
+};
+
+var app = connect()
+  .use(function(req, res, next) {
+    // convert all the md files on every request. This is not optimal
+    // but fast enough that we don't really need to care right now.
+    convert();
+    next();
+  })
+  .use(reactMiddleware.provide(buildOptions))
+  .use(serveStatic(FILE_SERVE_ROOT))
+  // .use(serveFavicon(path.join(FILE_SERVE_ROOT, 'elements', 'favicon', 'favicon.ico')))
+  .use(morgan('combined'))
+  .use(compression())
+  .use(errorHandler());
+
+var portToUse = port || 8080;
+var server = http.createServer(app);
+server.listen(portToUse);
+console.log('Open http://localhost:' + portToUse + '/draft-js/index.html');
+module.exports = server;
diff --git a/website/src/draft-js/css/draft.css b/website/src/draft-js/css/draft.css
new file mode 100644
index 0000000..a503299
--- /dev/null
+++ b/website/src/draft-js/css/draft.css
@@ -0,0 +1,1217 @@
+html {
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-family: proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-weight: 300;
+  color: #484848;
+  line-height: 1.28;
+}
+
+body {
+  background-color: #FFFCFC;
+}
+
+.subHeader {
+  font-size: 21px;
+  font-weight: 200;
+  line-height: 30px;
+  margin-bottom: 10px;
+}
+
+em {
+  font-style: italic;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  margin: 10px 0;
+  font-family: inherit;
+  font-weight: bold;
+  line-height: 20px;
+  color: inherit;
+  text-rendering: optimizelegibility;
+}
+
+h1 small, h2 small, h3 small, h4 small, h5 small, h6 small {
+  font-weight: normal;
+  color: #7b7b7b;
+}
+
+h1, h2, h3 {
+  line-height: 40px;
+}
+
+h1 {
+  font-size: 39px;
+}
+
+h2 {
+  font-size: 31px;
+}
+
+h3 {
+  font-size: 23px;
+}
+
+h4 {
+  font-size: 17px;
+}
+
+h5 {
+  font-size: 14px;
+}
+
+h6 {
+  font-size: 11px;
+}
+
+h1 small {
+  font-size: 24px;
+}
+
+h2 small {
+  font-size: 18px;
+}
+
+h3 small {
+  font-size: 16px;
+}
+
+h4 small {
+  font-size: 14px;
+}
+
+ul, ol {
+  margin: 0 0 10px 25px;
+  padding: 0;
+}
+
+ul ul, ul ol, ol ol, ol ul {
+  margin-bottom: 0;
+}
+
+li {
+  line-height: 20px;
+}
+
+a {
+  color: #C15757;
+  font-weight: 400;
+  text-decoration: none;
+}
+
+a:hover, a:focus {
+  color: #823030;
+  text-decoration: underline;
+}
+
+a:focus {
+  outline: thin dotted #333;
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+.center {
+  text-align: center;
+}
+
+html * {
+  color-profile: sRGB;
+  rendering-intent: auto;
+}
+
+.prism {
+  white-space: pre-wrap;
+  font-family: 'source-code-pro', Menlo, 'Courier New', Consolas, monospace;
+  font-size: 13px;
+  line-height: 20px;
+  border-left: 4px solid #843131;
+  padding: 5px 10px;
+  margin-left: 20px;
+  background-color: rgba(132, 49, 49, 0.05);
+}
+
+.prism + .prism {
+  margin-top: 10px;
+}
+
+.token.keyword {
+  color: #1990B8;
+}
+
+.token.string, .token.regex {
+  color: #2F9C0A;
+}
+
+.token.boolean, .token.number {
+  color: #C92C2C;
+}
+
+.token.comment {
+  color: #7D8B99;
+}
+
+.side-by-side {
+  overflow: hidden;
+}
+
+.side-by-side > div {
+  width: 460;
+  margin-left: 0;
+  float: left;
+}
+
+* {
+  -webkit-box-sizing: border-box;
+  -moz-box-sizing: border-box;
+  box-sizing: border-box;
+  border: none;
+  margin: 0;
+  padding: 0;
+}
+
+html {
+  background: #f9f9f9;
+}
+
+.left {
+  float: left;
+}
+
+.right {
+  float: right;
+}
+
+.container {
+  padding-top: 50px;
+  min-width: 960px;
+}
+
+.wrap {
+  width: 960px;
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+.skinnyWrap {
+  width: 690px;
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: 20px;
+  padding-right: 20px;
+}
+
+hr {
+  height: 0;
+  border-top: 1px solid #ccc;
+  border-bottom: 1px solid #eee;
+}
+
+ul, li {
+  margin-left: 20px;
+}
+
+h1 .anchor, h2 .anchor, h3 .anchor, h4 .anchor, h5 .anchor, h6 .anchor {
+  margin-top: -50px;
+  position: absolute;
+}
+
+h1:hover .hash-link, h2:hover .hash-link, h3:hover .hash-link, h4:hover .hash-link, h5:hover .hash-link, h6:hover .hash-link {
+  visibility: visible;
+}
+
+.hash-link {
+  color: #aaa;
+  visibility: hidden;
+}
+
+.nav-main {
+  *zoom: 1;
+  background: #3B3738;
+  color: #fafafa;
+  position: fixed;
+  top: 0;
+  height: 50px;
+  box-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
+  width: 100%;
+  z-index: 100;
+}
+
+.nav-main:before, .nav-main:after {
+  content: " ";
+  display: table;
+}
+
+.nav-main:after {
+  clear: both;
+}
+
+.nav-main a {
+  color: #e9e9e9;
+  text-decoration: none;
+}
+
+.nav-main .nav-site {
+  float: right;
+  margin: 0;
+}
+
+.nav-main .nav-site li {
+  margin: 0;
+}
+
+.nav-main .nav-site a {
+  padding: 0 8px;
+  text-transform: uppercase;
+  letter-spacing: 1px;
+  line-height: 50px;
+  display: inline-block;
+  height: 50px;
+  color: #aaa;
+}
+
+.nav-main .nav-site a:hover {
+  color: #fafafa;
+}
+
+.nav-main .nav-site a.active {
+  color: #fafafa;
+  border-bottom: 3px solid #C15757;
+  background: #333;
+}
+
+.nav-main .nav-home {
+  color: #fafafa;
+  font-size: 24px;
+  line-height: 50px;
+}
+
+.nav-home a {
+  color: #C15757;
+}
+
+.nav-home img {
+  vertical-align: -17px;
+  margin-right: 4px;
+  width: 50px;
+  height: 50px;
+}
+
+.nav-main ul {
+  display: inline;
+}
+
+.nav-main li {
+  display: inline;
+}
+
+.hero {
+  background: #843131;
+  padding: 50px 0;
+  color: #FDF3E7;
+  font-weight: 300;
+}
+
+.hero .text {
+  font-size: 64px;
+  text-align: center;
+}
+
+.hero .minitext {
+  font-size: 16px;
+  text-align: center;
+  text-transform: uppercase;
+}
+
+.hero strong {
+  font-weight: 400;
+}
+
+.buttons-unit {
+  margin-top: 60px;
+  text-align: center;
+}
+
+.buttons-unit a {
+  color: #FA6900;
+}
+
+.buttons-unit .button {
+  font-size: 24px;
+  background: #C15757;
+  color: #fafafa;
+}
+
+.buttons-unit .button:active {
+  background: #57A85C;
+}
+
+.buttons-unit.downloads {
+  margin: 30px 0;
+}
+
+.nav-docs {
+  color: #2d2d2d;
+  font-size: 14px;
+  float: left;
+  width: 210px;
+}
+
+.nav-docs ul {
+  list-style: none;
+  margin: 0;
+}
+
+.nav-docs ul ul {
+  margin-left: 20px;
+}
+
+.nav-docs li {
+  margin: 0;
+}
+
+.nav-docs h3 {
+  text-transform: uppercase;
+  font-size: 14px;
+}
+
+.nav-docs a {
+  color: #666;
+  display: block;
+}
+
+.nav-docs a:hover {
+  text-decoration: none;
+  color: #34A83B;
+}
+
+.nav-docs a.active {
+  color: #34A83B;
+}
+
+.nav-docs .nav-docs-section {
+  border-bottom: 1px solid #ccc;
+  border-top: 1px solid #eee;
+  padding: 12px 0;
+}
+
+.nav-docs .nav-docs-section:first-child {
+  padding-top: 0;
+  border-top: 0;
+}
+
+.nav-docs .nav-docs-section:last-child {
+  padding-bottom: 0;
+  border-bottom: 0;
+}
+
+.nav-blog li {
+  margin-bottom: 5px;
+}
+
+.home-section {
+  margin: 50px 0;
+}
+
+.home-divider {
+  border-top-color: #bbb;
+  margin: 0 auto;
+  width: 400px;
+}
+
+.marketing-row {
+  *zoom: 1;
+  margin: 50px 0;
+}
+
+.marketing-row:before, .marketing-row:after {
+  content: " ";
+  display: table;
+}
+
+.marketing-row:after {
+  clear: both;
+}
+
+.marketing-col {
+  float: left;
+  margin-left: 40px;
+  width: 280px;
+}
+
+.marketing-col h3 {
+  color: #2d2d2d;
+  font-size: 24px;
+  font-weight: normal;
+  text-transform: uppercase;
+}
+
+.marketing-col p {
+  font-size: 16px;
+}
+
+.marketing-col:first-child {
+  margin-left: 0;
+}
+
+#examples h3, .home-presentation h3 {
+  color: #2d2d2d;
+  font-size: 24px;
+  font-weight: normal;
+  margin-bottom: 5px;
+}
+
+#examples p {
+  margin: 0 0 25px 0;
+  max-width: 600px;
+}
+
+#examples .example {
+  margin-top: 60px;
+}
+
+#examples #todoExample {
+  font-size: 14px;
+}
+
+#examples #todoExample ul {
+  list-style-type: square;
+  margin: 0 0 10px 0;
+}
+
+#examples #todoExample input {
+  border: 1px solid #ccc;
+  font: 14px proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif;
+  padding: 3px;
+  width: 150px;
+}
+
+#examples #todoExample button {
+  font: 14px proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif;
+  margin-left: 5px;
+  padding: 4px 10px;
+}
+
+#examples #markdownExample textarea {
+  border: 1px solid #ccc;
+  font: 14px proxima-nova, "Helvetica Neue", Helvetica, Arial, sans-serif;
+  margin-bottom: 10px;
+  padding: 5px;
+}
+
+.home-bottom-section {
+  margin-bottom: 100px;
+}
+
+.docs-nextprev {
+  *zoom: 1;
+}
+
+.docs-nextprev:before, .docs-nextprev:after {
+  content: " ";
+  display: table;
+}
+
+.docs-nextprev:after {
+  clear: both;
+}
+
+.docs-prev {
+  float: left;
+}
+
+.docs-next {
+  float: right;
+}
+
+footer {
+  font-size: 13px;
+  font-weight: 600;
+  padding-top: 36px;
+  padding-bottom: 18px;
+  overflow: auto;
+}
+
+section.black content {
+  padding-bottom: 18px;
+}
+
+.blogContent {
+  *zoom: 1;
+  padding-top: 20px;
+}
+
+.blogContent:before, .blogContent:after {
+  content: " ";
+  display: table;
+}
+
+.blogContent:after {
+  clear: both;
+}
+
+.blogContent blockquote {
+  padding: 5px 15px;
+  margin: 20px 0;
+  background-color: #f8f5ec;
+  border-left: 5px solid #f7ebc6;
+}
+
+.documentationContent {
+  *zoom: 1;
+  padding-top: 20px;
+}
+
+.documentationContent:before, .documentationContent:after {
+  content: " ";
+  display: table;
+}
+
+.documentationContent:after {
+  clear: both;
+}
+
+.documentationContent .subHeader {
+  font-size: 24px;
+}
+
+.documentationContent h2 {
+  margin-top: 30px;
+}
+
+.documentationContent blockquote {
+  padding: 15px 30px 15px 15px;
+  margin: 20px 0;
+  background-color: rgba(204, 122, 111, 0.1);
+  border-left: 5px solid rgba(191, 87, 73, 0.2);
+}
+
+.documentationContent blockquote h4 {
+  margin-top: 0;
+}
+
+.documentationContent blockquote p {
+  margin-bottom: 0;
+}
+
+.documentationContent blockquote p:first-child {
+  font-weight: bold;
+  font-size: 17.5px;
+  line-height: 20px;
+  margin-top: 0;
+  text-rendering: optimizelegibility;
+}
+
+.docs-prevnext {
+  padding-top: 40px;
+  padding-bottom: 40px;
+}
+
+.button {
+  background: -webkit-linear-gradient( #9a9a9a, #646464);
+  background: linear-gradient( #9a9a9a, #646464);
+  border-radius: 4px;
+  padding: 8px 16px;
+  font-size: 18px;
+  font-weight: 400;
+  margin: 0 12px;
+  display: inline-block;
+  color: #fafafa;
+  text-decoration: none;
+  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
+  text-decoration: none;
+}
+
+.button:hover {
+  text-decoration: none;
+}
+
+.button:active {
+  box-shadow: none;
+}
+
+.hero .button {
+  box-shadow: 1px 3px 3px rgba(0, 0, 0, 0.3);
+}
+
+.button.blue {
+  background: -webkit-linear-gradient( #77a3d2, #4783c2);
+  background: linear-gradient( #77a3d2, #4783c2);
+}
+
+.row {
+  padding-bottom: 4px;
+}
+
+.row .span4 {
+  width: 33.33%;
+  display: table-cell;
+}
+
+.row .span8 {
+  width: 66.66%;
+  display: table-cell;
+}
+
+.row .span6 {
+  width: 50%;
+  display: table-cell;
+}
+
+p, .diagram {
+  margin: 16px 0;
+}
+
+.diagram, .diagram img,
+.video, .video iframe {
+  width: 100%;
+}
+
+/**
+ * fluid/responsive 16:9 video css from
+ * http://webdesignerwall.com/tutorials/css-elastic-videos
+ */
+.video-container {
+  margin-bottom: 10px;
+  position: relative;
+  padding-bottom: 56.25%;
+  padding-top: 30px;
+  height: 0;
+  overflow: hidden;
+}
+
+.video-container iframe,
+.video-container object,
+.video-container embed {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.associated-with-next-sibling {
+  margin-bottom: 10px;
+}
+
+.disassociated-with-next-sibling {
+  margin-bottom: 30px;
+}
+
+.highlight {
+  padding: 10px;
+  margin-bottom: 20px;
+}
+
+figure {
+  text-align: center;
+}
+
+.inner-content {
+  float: right;
+  width: 650px;
+}
+
+.nosidebar .inner-content {
+  float: none;
+  margin: 0 auto;
+}
+
+.post-list-item+.post-list-item {
+  margin-top: 60px;
+}
+
+small code, li code, p code {
+  color: #555;
+  background-color: rgba(0, 0, 0, 0.04);
+  font-family: 'source-code-pro', Menlo, 'Courier New', Consolas, monospace;
+  font-size: 14px;
+  padding: 1px 3px;
+}
+
+.playground {
+  *zoom: 1;
+}
+
+.playground:before, .playground:after {
+  content: " ";
+  display: table;
+}
+
+.playground:after {
+  clear: both;
+}
+
+.playground-tab {
+  border-bottom: none !important;
+  border-radius: 3px 3px 0 0;
+  padding: 6px 8px;
+  font-size: 12px;
+  font-weight: bold;
+  color: #c2c0bc;
+  background-color: #f1ede4;
+  display: inline-block;
+  cursor: pointer;
+}
+
+.playgroundCode, .playground-tab, .playgroundPreview {
+  border: 1px solid rgba(16, 16, 16, 0.1);
+}
+
+.playground-tab-active {
+  color: #222;
+}
+
+.playgroundCode {
+  border-radius: 0 3px 3px 3px;
+  float: left;
+  overflow: hidden;
+  width: 600px;
+}
+
+.playgroundPreview {
+  background-color: white;
+  border-radius: 3px;
+  float: right;
+  padding: 15px 20px;
+  width: 280px;
+}
+
+.playgroundError {
+  color: #c5695c;
+  font-size: 15px;
+}
+
+.MarkdownEditor textarea {
+  width: 100%;
+  height: 100px;
+}
+
+.hll {
+  background-color: #f7ebc6;
+  border-left: 5px solid #f7d87c;
+  display: block;
+  margin-left: -14px;
+  margin-right: -14px;
+  padding-left: 9px;
+}
+
+.highlight .javascript .err {
+  background-color: transparent;
+  color: inherit;
+}
+
+.highlight {
+  position: relative;
+  margin-bottom: 14px;
+  padding: 30px 14px 14px;
+  border: none;
+  border-radius: 0;
+  overflow: auto;
+}
+
+.highlight pre {
+  padding: 0;
+  margin-top: 0;
+  margin-bottom: 0;
+  background-color: transparent;
+  border: 0;
+}
+
+.highlight pre code {
+  background: none;
+  font-size: inherit;
+  padding: 0;
+}
+
+.highlight pre .lineno {
+  display: inline-block;
+  width: 22px;
+  padding-right: 5px;
+  margin-right: 10px;
+  color: #bebec5;
+  text-align: right;
+}
+
+.highlight:after {
+  position: absolute;
+  top: 0;
+  right: 0;
+  left: 0;
+  padding: 3px 7px;
+  font-size: 12px;
+  font-weight: bold;
+  color: #c2c0bc;
+  background-color: #f1ede4;
+  content: "Code";
+}
+
+.downloadCenter {
+  text-align: center;
+  margin-top: 20px;
+  margin-bottom: 25px;
+}
+
+.downloadSection:hover {
+  text-decoration: none !important;
+}
+
+@media screen and (max-width: 960px) {
+  .nav-main {
+    position: static;
+  }
+
+  .container {
+    padding-top: 0;
+  }
+}
+
+.post {
+  margin-bottom: 30px;
+}
+
+.pagination {
+  margin-bottom: 30px;
+  width: 100%;
+  overflow: hidden;
+}
+
+.pagination .next {
+  float: right;
+}
+
+div[data-twttr-id] iframe {
+  margin: 10px auto !important;
+}
+
+.three-column {
+  *zoom: 1;
+}
+
+.three-column:before, .three-column:after {
+  content: " ";
+  display: table;
+}
+
+.three-column:after {
+  clear: both;
+}
+
+.three-column>ul {
+  float: left;
+  margin-left: 30px;
+  width: 190px;
+}
+
+.three-column > ul:first-child {
+  margin-left: 20px;
+}
+
+.home-why {
+  margin-top: 25px;
+}
+
+.home-why h3 {
+  text-align: center;
+}
+
+.home-why .blurb {
+  margin-bottom: 20px;
+  text-align: center;
+}
+
+.home-why .list {
+  margin: 0 auto;
+  max-width: 460px;
+}
+
+.home-getting-started {
+  width: 500px;
+  margin: 20px auto 40px auto;
+}
+
+.home-getting-started h3 {
+  text-align: center;
+}
+
+#content {
+  display: none;
+}
+
+p + .apiIndex {
+  margin-top: 0;
+}
+.apiIndex {
+  list-style: none;
+  margin: 0 0 30px 0;
+}
+.apiIndex li {
+  margin: 0 0 5px 0;
+  color: rgba(0, 0, 0, 0.35);
+}
+.apiIndex li a {
+  color: inherit;
+  display: block;
+  position: relative;
+  text-decoration: none;
+}
+.apiIndex li a:hover::before {
+  color: #f26b00;
+  content: '#';
+  font-size: 16px;
+  left: -2em;
+  line-height: 20px;
+  position: absolute;
+}
+.apiIndex .prism {
+  margin-bottom: 0;
+}
+
+@media only screen and (max-device-width : 1024px) {
+  #content {
+    display: inline;
+  }
+
+  .container {
+    min-width: 0;
+    overflow: scroll;
+  }
+  .wrap {
+    width: auto;
+  }
+  .home-getting-started {
+    width: auto;
+  }
+  .inner-content {
+    width: auto;
+    float: none;
+  }
+  .marketing-col {
+    margin-left: 0;
+    float: none;
+    margin-bottom: 30px;
+    text-align: center;
+  }
+  .home-section, .marketing-row {
+    margin: 0;
+  }
+  .nav-main .nav-site a {
+    padding: 0 4px;
+  }
+  .nav-main .wrap {
+    padding: 0 2px 0 4px;
+  }
+  .home-divider {
+    display: none;
+  }
+  .hero {
+    padding: 10px 0 30px 0;
+  }
+  .prism {
+    padding: 4px 8px;
+    margin-left: -12px;
+    font-size: 11px;
+  }
+  .nav-docs .nav-docs-section {
+    border: none;
+    padding: 0;
+  }
+  .nav-docs h3 {
+    margin: 0;
+  }
+  .nav-docs {
+    float: none;
+    width: auto;
+    margin-top: -20px;
+    margin-bottom: 20px;
+  }
+  h1 {
+    font-size: 30px;
+    line-height: 30px;
+  }
+  ol {
+    margin: 0;
+  }
+}
+
+/* http://prismjs.com/download.html?themes=prism&languages=markup+clike+javascript+jsx */
+/**
+ * prism.js default theme for JavaScript, CSS and HTML
+ * Based on dabblet (http://dabblet.com)
+ * @author Lea Verou
+ */
+
+code[class*="language-"],
+pre[class*="language-"],
+.apiIndex pre {
+	color: black;
+	text-shadow: 0 1px white;
+	font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
+	direction: ltr;
+	text-align: left;
+	white-space: pre;
+	word-spacing: normal;
+	word-break: normal;
+	line-height: 1.5;
+
+	-moz-tab-size: 4;
+	-o-tab-size: 4;
+	tab-size: 4;
+
+	-webkit-hyphens: none;
+	-moz-hyphens: none;
+	-ms-hyphens: none;
+	hyphens: none;
+}
+
+pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
+code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
+code[class*="language-"]::selection, code[class*="language-"] ::selection {
+	text-shadow: none;
+	background: #b3d4fc;
+}
+
+@media print {
+	code[class*="language-"],
+	pre[class*="language-"] {
+		text-shadow: none;
+	}
+}
+
+/* Code blocks */
+pre[class*="language-"] {
+	padding: 1em;
+	margin: .5em 0;
+	overflow: auto;
+}
+
+:not(pre) > code[class*="language-"],
+pre[class*="language-"] {
+	background: #f5f2f0;
+}
+
+/* Inline code */
+:not(pre) > code[class*="language-"] {
+	padding: .1em;
+	border-radius: .3em;
+}
+
+.token.comment,
+.token.prolog,
+.token.doctype,
+.token.cdata {
+	color: slategray;
+}
+
+.token.punctuation {
+	color: #999;
+}
+
+.namespace {
+	opacity: .7;
+}
+
+.token.property,
+.token.tag,
+.token.boolean,
+.token.number,
+.token.constant,
+.token.symbol,
+.token.deleted {
+	color: #905;
+}
+
+.token.selector,
+.token.attr-name,
+.token.string,
+.token.char,
+.token.builtin,
+.token.inserted {
+	color: #690;
+}
+
+.token.operator,
+.token.entity,
+.token.url,
+.language-css .token.string,
+.style .token.string {
+	color: #a67f59;
+	background: hsla(0, 0%, 100%, .5);
+}
+
+.token.atrule,
+.token.attr-value,
+.token.keyword {
+	color: #07a;
+}
+
+.token.function {
+	color: #DD4A68;
+}
+
+.token.regex,
+.token.important,
+.token.variable {
+	color: #e90;
+}
+
+.token.important,
+.token.bold {
+	font-weight: bold;
+}
+.token.italic {
+	font-style: italic;
+}
+
+.token.entity {
+	cursor: help;
+}
+
+/* Custom prism rules for the Relay docs */
+
+code[class*="language-"],
+pre[class*="language-"] {
+  margin-bottom: 10px;
+}
+code[class*="language-"],
+pre[class*="language-"],
+.apiIndex pre {
+  font-family: 'source-code-pro', Menlo, 'Courier New', Consolas, monospace;
+  white-space: pre-wrap;
+}
+
+code[class*="language-"],
+pre[class*="language-"],
+.apiIndex li {
+  font-size: 13px;
+  line-height: 20px;
+  border-left: 4px solid #f26b00;
+  padding: 5px 10px;
+  background-color: rgba(75, 145, 80, 0.05);
+}
+
+.prism + .prism {
+  margin-top: -15px;
+}
+
+/* Line highlighting */
+
+.prism {
+  position: relative;
+}
+
+.line-highlight {
+  background: rgba(75, 145, 80, 0.15);
+  left: 0;
+  line-height: inherit;
+  margin-top: 5px;
+  pointer-events: none;
+  position: absolute;
+  right: 0;
+}
diff --git a/website/src/draft-js/docs/advanced-topics-block-components.js b/website/src/draft-js/docs/advanced-topics-block-components.js
new file mode 100644
index 0000000..0f16e9f
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-block-components.js
@@ -0,0 +1,127 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Draft is designed to solve problems for straightforward rich text interfaces
+like comments and chat messages, but it also powers richer editor experiences
+like [Facebook Notes](https://www.facebook.com/notes/).
+
+Users can embed images within their Notes, either loading from their existing
+Facebook photos or by uploading new images from the desktop. To that end,
+the Draft framework supports custom rendering at the block level, to render
+content like rich media in place of plain text.
+
+The [TeX editor](https://github.com/facebook/draft-js/tree/master/examples/tex)
+in the Draft repository provides a live example of custom block rendering, with
+TeX syntax translated on the fly into editable embedded formula rendering via the
+[KaTeX library](https://khan.github.io/KaTeX/).
+
+By using a custom block renderer, it is possible to introduce complex rich
+interactions within the frame of your editor.
+
+## Custom Block Components
+
+Within the \`Editor\` component, one may specify the \`blockRendererFn\` prop.
+This prop function allows a higher-level component to define custom React
+rendering for \`ContentBlock\` objects, based on block type, text, or other
+criteria.
+
+For instance, we may wish to render \`ContentBlock\` objects of type \`'media'\` using
+a custom \`MediaComponent\`.
+
+\`\`\`
+function myBlockRenderer(contentBlock) {
+  const type = contentBlock.getType();
+  if (type === 'media') {
+    return {
+      component: MediaComponent,
+      props: {
+        foo: 'bar',
+      },
+    };
+  }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithMedia = React.createClass({
+  ...
+  render() {
+    return <Editor ... blockRendererFn={myBlockRenderer} />;
+  }
+});
+\`\`\`
+
+If no custom renderer object is returned by the \`blockRendererFn\` function,
+\`Editor\` will render the default \`DraftEditorBlock\` text block component.
+
+The \`component\` property defines the component to be used, while the optional
+\`props\` object includes props that will be passed through to the rendered
+custom component.
+
+By defining this function within the context of a higher-level component,
+the props for this custom component may be bound to that component, allowing
+instance methods for custom component props.
+
+## Defining custom block components
+
+Within \`MediaComponent\`, the most likely use case is that you will want to
+retrieve entity metadata to render your custom block. You may apply an entity
+key to the text within a \`'media'\` block during \`EditorState\` management,
+then retrieve the metadata for that key in your custom component \`render()\`
+code.
+
+\`\`\`
+import {Entity} from 'draft-js';
+const MediaComponent = React.createClass({
+  render() {
+    const {block, foo} = this.props;
+    const data = Entity.get(block.getEntityAt(0)).getData();
+    // Return a <figure> or some other content using this data.
+  }
+});
+\`\`\`
+
+The \`ContentBlock\` object is made available within the custom component, along
+with the props defined at the top level. By extracting entity information from
+the \`ContentBlock\` and the \`Entity\` map, you can obtain the metadata required to
+render your custom component.
+
+_Retrieving the entity from the block is admittedly a bit of an awkward API,
+and is worth revisiting._
+
+## Recommendations and other notes
+
+If your custom block renderer requires mouse interaction, it is often wise
+to temporarily set your \`Editor\` to \`readOnly={true}\` during this
+interaction. In this way, the user does not trigger any selection changes within
+the editor while interacting with the custom block. This should not be a problem
+with respect to editor behavior, since interacting with your custom block
+component is most likely mutually exclusive from text changes within the editor.
+
+The recommendation above is especially important for custom block renderers
+that involve text input, like the TeX editor example.
+
+It is also worth noting that within the Facebook Notes editor, we have not
+tried to perform any special SelectionState rendering or management on embedded
+media, such as rendering a highlight on an embedded photo when selecting it.
+This is in part because of the rich interaction provided on the media
+itself, with resize handles and other controls exposed to mouse behavior.
+
+Since an engineer using Draft has full awareness of the selection state
+of the editor and full control over native Selection APIs, it would be possible
+to build selection behavior on static embedded media if desired. So far, though,
+we have not tried to solve this at Facebook, so we have not packaged solutions
+for this use case into the Draft project at this time.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-block-components","title":"Custom Block Components","layout":"docs","category":"Advanced Topics","next":"advanced-topics-inline-styles","permalink":"docs/advanced-topics-block-components.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-block-styling.js b/website/src/draft-js/docs/advanced-topics-block-styling.js
new file mode 100644
index 0000000..06120d0
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-block-styling.js
@@ -0,0 +1,66 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Within \`Editor\`, some block types are given default CSS styles to limit the amount
+of basic configuration required to get engineers up and running with custom
+editors.
+
+By defining a \`blockStyleFn\` prop function for a \`Editor\`, it is possible
+to specify classes that should be applied to blocks at render time.
+
+## DraftStyleDefault.css
+
+The Draft library includes default block CSS styles within
+[DraftStyleDefault.css](https://github.com/facebook/draft-js/blob/master/src/component/utils/DraftStyleDefault.css). _(Note that the annotations on the CSS class names are
+artifacts of Facebook's internal CSS module management system._
+
+These CSS rules are largely devoted to providing default styles for list items,
+without which callers would be responsible for managing their own default list
+styles.
+
+## blockStyleFn
+
+The \`blockStyleFn\` prop on \`Editor\` allows you to define CSS classes to
+style blocks at render time. For instance, you may wish to style \`'blockquote'\`
+type blocks with fancy italic text.
+
+\`\`\`
+function myBlockStyleFn(contentBlock) {
+  const type = contentBlock.getType();
+  if (type === 'blockquote') {
+    return 'superFancyBlockquote';
+  }
+}
+
+// Then...
+import {Editor} from 'draft-js';
+const EditorWithFancyBlockquotes = React.createClass({
+  render() {
+    return <Editor ... blockStyleFn={myBlockStyleFn} />;
+  }
+});
+\`\`\`
+
+Then in your own CSS:
+
+\`\`\`
+.superFancyBlockquote {
+  color: #999;
+  font-family: 'Hoefler Text', Georgia, serif;
+  font-style: italic;
+  text-align: center;
+}
+\`\`\`
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-block-styling","title":"Block Styling","layout":"docs","category":"Advanced Topics","next":"advanced-topics-block-components","permalink":"docs/advanced-topics-block-styling.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-decorators.js b/website/src/draft-js/docs/advanced-topics-decorators.js
new file mode 100644
index 0000000..3930756
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-decorators.js
@@ -0,0 +1,159 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Inline and block styles aren't the only kind of rich styling that we might
+want to add to our editor. The Facebook comment input, for example, provides
+blue background highlights for mentions and hashtags.
+
+To support flexibility for custom rich text, Draft provides a "decorator"
+system. The [tweet example](https://github.com/facebook/draft-js/tree/master/examples/tweet)
+offers a live example of decorators in action.
+
+## CompositeDecorator
+
+The decorator concept is based on scanning the contents of a given
+[ContentBlock](/draft-js/docs/api-reference-content-block.html)
+for ranges of text that match a defined strategy, then rendering them
+with a specified React component.
+
+You can use the \`CompositeDecorator\` class to define your desired
+decorator behavior. This class allows you to supply multiple \`DraftDecorator\`
+objects, and will search through a block of text with each strategy in turn.
+
+Decorators are stored within the \`EditorState\` record. When creating a new
+\`EditorState\` object, e.g. via \`EditorState.createEmpty()\`, a decorator may
+optionally be provided.
+
+> Under the hood
+>
+> When contents change in a Draft editor, the resulting \`EditorState\` object
+> will evaluate the new \`ContentState\` with its decorator, and identify ranges
+> to be decorated. A complete tree of blocks, decorators, and inline styles is
+> formed at this time, and serves as the basis for our rendered output.
+>
+> In this way, we always ensure that as contents change, rendered decorations
+> are in sync with our \`EditorState\`.
+
+In the "Tweet" editor example, for instance, we use a \`CompositeDecorator\` that
+searches for @-handle strings as well as hashtag strings:
+
+\`\`\`
+const compositeDecorator = new CompositeDecorator([
+  {
+    strategy: handleStrategy,
+    component: HandleSpan,
+  },
+  {
+    strategy: hashtagStrategy,
+    component: HashtagSpan,
+  },
+]);
+\`\`\`
+
+This composite decorator will first scan a given block of text for @-handle
+matches, then for hashtag matches.
+
+\`\`\`
+// Note: these aren't very good regexes, don't use them!
+const HANDLE_REGEX = /\\@[\\w]+/g;
+const HASHTAG_REGEX = /\\#[\\w\\u0590-\\u05ff]+/g;
+
+function handleStrategy(contentBlock, callback) {
+  findWithRegex(HANDLE_REGEX, contentBlock, callback);
+}
+
+function hashtagStrategy(contentBlock, callback) {
+  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+}
+
+function findWithRegex(regex, contentBlock, callback) {
+  const text = contentBlock.getText();
+  let matchArr, start;
+  while ((matchArr = regex.exec(text)) !== null) {
+    start = matchArr.index;
+    callback(start, start + matchArr[0].length);
+  }
+}
+\`\`\`
+
+The strategy functions execute the provided callback with the \`start\` and
+\`end\` values of the matching range of text.
+
+## Decorator Components
+
+For your decorated ranges of text, you must define a React component to use
+to render them. These tend to be simple \`span\` elements with CSS classes or
+styles applied to them.
+
+In our current example, the \`CompositeDecorator\` object names \`HandleSpan\` and
+\`HashtagSpan\` as the components to use for decoration. These are just basic
+stateless components:
+
+\`\`\`
+const HandleSpan = (props) => {
+  return <span {...props} style={styles.handle}>{props.children}</span>;
+};
+
+const HashtagSpan = (props) => {
+  return <span {...props} style={styles.hashtag}>{props.children}</span>;
+};
+\`\`\`
+
+Note that \`props.children\` is passed through to the rendered output. This is
+done to ensure that the text is rendered within the decorated \`span\`.
+
+You can use the same approach for links, as demonstrated in our
+[link example](https://github.com/facebook/draft-js/tree/master/examples/link).
+
+### Beyond CompositeDecorator
+
+The decorator object supplied to an \`EditorState\` need only match the expectations
+of the
+[DraftDecoratorType](https://github.com/facebook/draft-js/blob/master/src/model/decorators/DraftDecoratorType.js)
+Flow type definition, which means that you can create any decorator classes
+you wish, as long as they match the expected type -- you are not bound by
+\`CompositeDecorator\`.
+
+## Setting new decorators
+
+Further, it is acceptable to set a new \`decorator\` value on the \`EditorState\`
+on the fly, during normal state propagation -- through immutable means, of course.
+
+This means that during your app workflow, if your decorator becomes invalid or
+requires a modification, you can create a new decorator object (or use
+\`null\` to remove all decorations) and \`EditorState.set()\` to make use of the new
+decorator setting.
+
+For example, if for some reason we wished to disable the creation of @-handle
+decorations while the user interacts with the editor, it would be fine to do the
+following:
+
+\`\`\`
+function turnOffHandleDecorations(editorState) {
+  const onlyHashtags = new CompositeDecorator([{
+    strategy: hashtagStrategy,
+    component: HashtagSpan,
+  }]);
+  return EditorState.set(editorState, {decorator: onlyHashtags});
+}
+\`\`\`
+
+The \`ContentState\` for this \`editorState\` will be re-evaluated with the new
+decorator, and @-handle decorations will no longer be present in the next
+render pass.
+
+Again, this remains memory-efficient due to data persistence across immutable
+objects.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-decorators","title":"Decorators","layout":"docs","category":"Advanced Topics","next":"advanced-topics-key-bindings","permalink":"docs/advanced-topics-decorators.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-entities.js b/website/src/draft-js/docs/advanced-topics-entities.js
new file mode 100644
index 0000000..3226e24
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-entities.js
@@ -0,0 +1,143 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This article discusses the Entity system, which Draft uses for annotating
+ranges of text with metadata. Entities enable engineers to introduce levels of
+richness beyond styled text to their editors. Links, mentions, and embedded
+content can all be implemented using entities.
+
+In the Draft repository, the
+[link editor](https://github.com/facebook/draft-js/tree/master/examples/link)
+and
+[entity demo](https://github.com/facebook/draft-js/tree/master/examples/entity)
+provide live code examples to help clarify how entities can be used, as well
+as their built-in behavior.
+
+The [Entity API Reference](/draf-js/docs/api-reference-entity.html) provides
+details on the static methods to be used when creating, retrieving, or updating
+entity objects.
+
+## Introduction
+
+An entity is an object that represents metadata for a range of text within a
+Draft editor. It has three properties:
+
+- **type**: A string that indicates what kind of entity it is, e.g. \`'LINK'\`,
+\`'MENTION'\`, \`'PHOTO'\`.
+- **mutability**: Not to be confused with immutability a la \`immutable-js\`, this
+property denotes the behavior of a range of text annotated with this entity
+object when editing the text range within the editor. This is addressed in
+greater detail below.
+- **data**: An optional object containing metadata for the entity. For instance,
+a \`'LINK'\` entity might contain a \`data\` object that contains the \`href\` value
+for that link.
+
+All entities are stored in a single object store within the \`Entity\` module,
+and are referenced by key within \`ContentState\` and React components used to
+decorate annotated ranges. _(We are considering future changes to bring
+the entity store into \`EditorState\` or \`ContentState\`.)_
+
+Using [decorators](/draft-js/docs/advanced-topics-decorators.html) or
+[custom block components](docs/advanced-topics-block-components.html), you can
+add rich rendering to your editor based on entity metadata.
+
+## Creating and Retrieving Entities
+
+Entities should be created using \`Entity.create\`, which accepts the three
+properties above as arguments. This method returns a string key, which can then
+be used to refer to the entity.
+
+This key is the value that should be used when applying entities to your
+content. For instance, the \`Modifier\` module contains an \`applyEntity\` method:
+
+\`\`\`
+const key = Entity.create('LINK', 'MUTABLE', {href: 'http://www.zombo.com'});
+const contentStateWithLink = Modifier.applyEntity(
+  contentState,
+  targetRange,
+  key
+);
+\`\`\`
+
+For a given range of text, then, you can extract its associated entity key by using
+the \`getEntityAt()\` method on a \`ContentBlock\` object, passing in the target
+offset value.
+
+\`\`\`
+const blockWithLinkAtBeginning = contentState.getBlockForKey('...');
+const linkKey = blockWithLinkAtBeginning.getEntityAt(0);
+const linkInstance = Entity.get(linkKey);
+const {href} = linkInstance.getData();
+\`\`\`
+
+## "Mutability"
+
+Entities may have one of three "mutability" values. The difference between them
+is the way they behave when the user makes edits to them.
+
+Note that \`DraftEntityInstance\` objects are always immutable Records, and this
+property is meant only to indicate how the annotated text may be "mutated" within
+the editor. _(Future changes may rename this property to ward off potential
+confusion around naming.)_
+
+### Immutable
+
+This text cannot be altered without removing the entity annotation
+from the text. Entities with this mutability type are effectively atomic.
+
+For instance, in a Facebook input, add a mention for a Page (i.e. Barack Obama).
+Then, either add a character within the mentioned text, or try to delete a character.
+Note that when adding characters, the entity is removed, and when deleting charaters,
+the entire entity is removed.
+
+This mutability value is useful in cases where the text absolutely must match
+its relevant metadata, and may not be altered.
+
+### Mutable
+
+This text may be altered freely. For instance, link text is
+generally intended to be "mutable" since the href and linkified text are not
+tightly coupled.
+
+### Segmented
+
+Entities that are "segmented" are tightly coupled to their text in much the
+same way as "immutable" entities, but allow customization via deletion.
+
+For instance, in a Facebook input, add a mention for a friend. Then, add a
+character to the text. Note that the entity is removed from the entire string,
+since your mentioned friend may not have their name altered in your text.
+
+Next, try deleting a character or word within the mention. Note that only the
+section of the mention that you have deleted is removed. In this way, we can
+allow short names for mentions.
+
+## Modifying Entities
+
+Since \`DraftEntityInstance\` records are immutable, you may not update the \`data\`
+property on an instance directly.
+
+Instead, two \`Entity\` methods are available to modify entities: \`mergeData\` and
+\`replaceData\`. The former allows updating data by passing in an object to merge,
+while the latter completely swaps in the new data object.
+
+## Using Entities for Rich Content
+
+The next article in this section covers the usage of decorator objects, which
+can be used to retrieve entities for rendering purposes.
+
+The [link editor example](https://github.com/facebook/draft-js/tree/master/examples/link)
+provides a working example of entity creation and decoration in use.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-entities","title":"Entities","layout":"docs","category":"Advanced Topics","next":"advanced-topics-decorators","permalink":"docs/advanced-topics-entities.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-inline-styles.js b/website/src/draft-js/docs/advanced-topics-inline-styles.js
new file mode 100644
index 0000000..3ea6039
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-inline-styles.js
@@ -0,0 +1,119 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Within your editor, you may wish to provide a wide variety of inline style
+behavior that goes well beyond the bold/italic/underline basics. For instance,
+you may want to support variety with color, font families, font sizes, and more.
+Further, your desired styles may overlap or be mutually exclusive.
+
+The [Rich Editor](http://github.com/facebook/draft-js/examples/rich) and
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+examples demonstrate complex inline style behavior in action.
+
+### Model
+
+Within the Draft model, inline styles are represented at the character level,
+using an immutable \`OrderedSet\` to define the list of styles to be applied to
+each character. These styles are identified by string. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+For example, consider the text "Hello **world**". The first six characters of
+the string are represented by the empty set, \`OrderedSet()\`. The final five
+characters are represented by \`OrderedSet.of('BOLD')\`. For convenience, we can
+think of these \`OrderedSet\` objects as arrays, though in reality we aggressively
+reuse identical immutable objects.
+
+In essence, our styles are:
+
+\`\`\`
+[
+  [], // H
+  [], // e
+  ...
+  ['BOLD'], // w
+  ['BOLD'], // o
+  // etc.
+]
+\`\`\`
+
+### Overlapping Styles
+
+Now let's say that we wish to make the middle range of characters italic as well:
+"He_llo **wo**_**rld**". This operation can be performed via the
+[Modifier](/draft-js/docs/api-reference-modifier.html) API.
+
+The end result will accommodate the overlap by including \`'ITALIC'\` in the
+relevant \`OrderedSet\` objects as well.
+
+\`\`\`
+[
+  [], // H
+  [], // e
+  ['ITALIC'], // l
+  ...
+  ['BOLD', 'ITALIC'], // w
+  ['BOLD', 'ITALIC'], // o
+  ['BOLD'], // r
+  // etc.
+]
+\`\`\`
+
+When determining how to render inline-styled text, Draft will identify
+contiguous ranges of identically styled characters and render those characters
+together in styled \`span\` nodes.
+
+### Mapping a style string to CSS
+
+By default, \`Editor\` provides support for a basic list of inline styles:
+\`'BOLD'\`, \`'ITALIC'\`, \`'UNDERLINE'\`, and \`'CODE'\`. These are mapped to simple CSS
+style objects, which are used to apply styles to the relevant ranges.
+
+For your editor, you may define custom style strings to include with these
+defaults, or you may override the default style objects for the basic styles.
+
+Within your \`Editor\` use case, you may provide the \`customStyleMap\` prop
+to define your style objects. (See
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+for a live example.)
+
+For example, you may want to add a \`'STRIKETHROUGH'\` style. To do so, define a
+custom style map:
+
+\`\`\`
+import {Editor} from 'draft-js';
+
+const styleMap = {
+  'STRIKETHROUGH': {
+    textDecoration: 'line-through',
+  },
+};
+
+class MyEditor extends React.Component {
+  // ...
+  render() {
+    return (
+      <Editor
+        customStyleMap={styleMap}
+        editorState={this.state.editorState}
+        ...
+      />
+    );
+  }
+}
+\`\`\`
+
+When rendered, the \`textDecoration: line-through\` style will be applied to all
+character ranges with the \`STRIKETHROUGH\` style.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-inline-styles","title":"Complex Inline Styles","layout":"docs","category":"Advanced Topics","next":"advanced-topics-nested-lists","permalink":"docs/advanced-topics-inline-styles.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-issues-and-pitfalls.js b/website/src/draft-js/docs/advanced-topics-issues-and-pitfalls.js
new file mode 100644
index 0000000..eccb9a9
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-issues-and-pitfalls.js
@@ -0,0 +1,82 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This article addresses some known issues with the Draft editor framework, as
+well as some common pitfalls that we have encountered while using the framework
+at Facebook.
+
+## Common Pitfalls
+
+### Delayed state updates
+
+A common pattern for unidirectional data management is to batch or otherwise
+delay updates to data stores, using a setTimeout or another mechanism. Stores are
+updated, then emit changes to the relevant React components to propagate
+re-rendering.
+
+When delays are introduced to a React application with a Draft editor, however,
+it is possible to cause significant interaction problems. This is because the
+editor expects immediate updates and renders that stay in sync with the user's typing
+behavior. Delays can prevent updates from being propagated through the editor
+component tree, which can cause a disconnect between keystrokes and updates.
+
+To avoid this while still using a delaying or batching mechanism, you should
+separate the delay behavior from your \`Editor\` state propagation. That is,
+you must always allow your \`EditorState\` to propagate to your \`Editor\`
+component without delay, and independently perform batched updates that do
+not affect the state of your \`Editor\` component.
+
+## Known Issues
+
+### React ContentEditable Warning
+
+Within the React core, a warning is used to ward off engineers who wish to
+use ContentEditable within their components, since by default the
+browser-controlled nature of ContentEditable does not mesh with strict React
+control over the DOM. The Draft editor resolves this issue, so for our case,
+the warning is noise. You can ignore it for now.
+
+We are currently looking into removing or replacing the warning to alleviate
+the irritation it may cause: https://github.com/facebook/react/issues/6081
+
+### Custom OSX Keybindings
+
+Because the browser has no access to OS-level custom keybindings, it is not
+possible to intercept edit intent behaviors that do not map to default system
+key bindings.
+
+The result of this is that users who use custom keybindings may encounter
+issues with Draft editors, since their key commands may not behave as expected.
+
+### Browser plugins/extensions
+
+As with any React application, browser plugins and extensions that modify the
+DOM can cause Draft editors to break.
+
+Grammar checkers, for instance, may modify the DOM within contentEditable
+elements, adding styles like underlines and backgrounds. Since React cannot
+reconcile the DOM if the browser does not match its expectations,
+the editor state may fail to remain in sync with the DOM.
+
+Certain old ad blockers are also known to break the native DOM Selection
+API -- a bad idea no matter what! -- and since Draft depends on this API to
+maintain controlled selection state, this can cause trouble for editor
+interaction.
+
+### IME and Internet Explorer
+
+As of IE11, Internet Explorer demonstrates notable issues with certain international
+input methods, most significantly Korean input.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-issues-and-pitfalls","title":"Issues and Pitfalls","layout":"docs","category":"Advanced Topics","next":"api-reference-editor","permalink":"docs/advanced-topics-issues-and-pitfalls.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-key-bindings.js b/website/src/draft-js/docs/advanced-topics-key-bindings.js
new file mode 100644
index 0000000..7eb4272
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-key-bindings.js
@@ -0,0 +1,107 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+The \`Editor\` component offers flexibility to define custom key bindings
+for your editor, via the \`keyBindingFn\` prop. This allows you to match key
+commands to behaviors in your editor component.
+
+### Defaults
+
+The default key binding function is \`getDefaultKeyBinding\`.
+
+Since the Draft framework maintains tight control over DOM rendering and
+behavior, basic editing commands must be captured and routed through the key
+binding system.
+
+\`getDefaultKeyBinding\` maps known OS-level editor commands to \`DraftEditorCommand\`
+strings, which then correspond to behaviors within component handlers.
+
+For instance, \`Ctrl+Z\` (Win) and \`Cmd+Z\` (OSX) map to the \`'undo'\` command,
+which then routes our handler to perform an \`EditorState.undo()\`.
+
+### Customization
+
+You may provide your own key binding function to supply custom command strings.
+
+It is recommended that your function use \`getDefaultKeyBinding\` as a
+fall-through case, so that your editor may benefit from default commands.
+
+With your custom command string, you may then implement the \`handleKeyCommand\`
+prop function, which allows you to map that command string to your desired
+behavior. If \`handleKeyCommand\` returns \`true\`, the command is considered
+handled. If it returns \`false\`, the command will fall through
+
+### Example
+
+Let's say we have an editor that should have a "Save" mechanism to periodically
+write your contents to the server as a draft copy.
+
+First, let's define our key binding function.
+
+\`\`\`
+import {KeyBindingUtil} from 'draft-js';
+const {hasCommandModifier} = KeyBindingUtil;
+
+function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
+  if (e.keyCode === 83 /* \`S\` key */ && hasCommandModifier(e)) {
+    return 'myeditor-save';
+  }
+  return getDefaultKeyBinding(e);
+}
+\`\`\`
+
+Our function receives a key event, and we check whether it matches our criteria:
+it must be an \`S\` key, and it must have a command modifier, i.e. the command
+key for OSX, or the control key otherwise.
+
+If the command is a match, return a string that names the command. Otherwise,
+fall through to the default key bindings.
+
+In our editor component, we can then make use of the command via the
+\`handleKeyCommand\` prop:
+
+\`\`\`
+import {Editor} from 'draft-js';
+class MyEditor extends React.Component {
+  // ...
+
+  handleKeyCommand(command: string): boolean {
+    if (command === 'myeditor-save') {
+      // Perform a request to save your contents, set
+      // a new \`editorState\`, etc.
+      return true;
+    }
+    return false;
+  }
+
+  render() {
+    return (
+      <Editor
+        editorState={this.state.editorState}
+        handleKeyCommand={this.handleKeyCommand.bind(this)}
+        ...
+      />
+    );
+  }
+}
+\`\`\`
+
+The \`'myeditor-save'\` command can be used for our custom behavior, and returning
+true instructs the editor that the command has been handled and no more work
+is required.
+
+By returning false in all other cases, default commands are able to fall
+through to default handler behavior.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-key-bindings","title":"Key Bindings","layout":"docs","category":"Advanced Topics","next":"advanced-topics-managing-focus","permalink":"docs/advanced-topics-key-bindings.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-managing-focus.js b/website/src/draft-js/docs/advanced-topics-managing-focus.js
new file mode 100644
index 0000000..87198b5
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-managing-focus.js
@@ -0,0 +1,47 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Managing text input focus can be a tricky task within React components. The browser
+focus/blur API is imperative, so setting or removing focus via declarative means
+purely through \`render()\` tends to feel awkward and incorrect, and it requires
+challenging attempts at controlling focus state.
+
+With that in mind, at Facebook we often choose to expose \`focus()\` methods
+on components that wrap text inputs. This breaks the declarative paradigm,
+but it also simplifies the work needed for engineers to successfully manage
+focus behavior within their apps.
+
+The \`Editor\` component follows this pattern, so there is a public \`focus()\`
+method available on the component. This allows you to use a ref within your
+higher-level component to call \`focus()\` directly on the component when needed.
+
+The event listeners within the component will observe focus changes and
+propagate them through \`onChange\` as expected, so state and DOM will remain
+correctly in sync.
+
+## Translating container clicks to focus
+
+Your higher-level component will most likely wrap the \`Editor\` component in a
+container of some kind, perhaps with padding to style it to match your app.
+
+By default, if a user clicks within this container but outside of the rendered
+\`Editor\` while attempting to focus the editor, the editor will have no awareness
+of the click event. It is therefore recommended that you use a click listener
+on your container component, and use the \`focus()\` method described above to
+apply focus to your editor.
+
+The [plaintext editor example](https://github.com/facebook/draft-js/tree/master/examples/plaintext),
+for instance, uses this pattern.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-managing-focus","title":"Managing Focus","layout":"docs","category":"Advanced Topics","next":"advanced-topics-block-styling","permalink":"docs/advanced-topics-managing-focus.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-nested-lists.js b/website/src/draft-js/docs/advanced-topics-nested-lists.js
new file mode 100644
index 0000000..7fc6202
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-nested-lists.js
@@ -0,0 +1,29 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+The Draft framework provides support for nested lists, as demonstrated in the
+Facebook Notes editor. There, you can use \`Tab\` and \`Shift+Tab\` to add or remove
+depth to a list item.
+
+The \`RichUtils\` module provides a handy \`onTab\` method that manages this
+behavior, and should be sufficient for most nested list needs. You can use
+the \`onTab\` prop on your \`Editor\` to make use of this utility.
+
+By default, styling is applied to list items to set appropriate spacing and
+list style behavior, via \`DraftStyleDefault.css\`.
+
+Note that there is currently no support for handling depth for blocks of any type
+except \`'ordered-list-item'\` and \`'unordered-list-item'\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-nested-lists","title":"Nested Lists","layout":"docs","category":"Advanced Topics","next":"advanced-topics-text-direction","permalink":"docs/advanced-topics-nested-lists.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced-topics-text-direction.js b/website/src/draft-js/docs/advanced-topics-text-direction.js
new file mode 100644
index 0000000..ce00ed8
--- /dev/null
+++ b/website/src/draft-js/docs/advanced-topics-text-direction.js
@@ -0,0 +1,42 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Facebook supports dozens of languages, which means that our text inputs need
+to be flexible enough to handle considerable variety.
+
+For example, we want input behavior for RTL languages such as Arabic and Hebrew
+to meet users' expectations. We also want to be able to support editor contents
+with a mixture of LTR and RTL text.
+
+To that end, Draft uses a bidi direction algorithm to determine appropriate
+text alignment and direction on a per-block basis.
+
+Text is rendered with an LTR or RTL direction automatically as the user types.
+You should not need to do anything to set direction yourself.
+
+## Text Alignment
+
+While languages are automatically aligned to the left or right during composition,
+as defined by the content characters, it is also possible for engineers to
+manually set the text alignment for an editor's contents.
+
+This may be useful, for instance, if an editor requires strictly centered
+contents, or needs to keep text aligned flush against another UI element.
+
+The \`Editor\` component therefore provides a \`textAlignment\` prop, with a
+simple set of values: \`'left'\`, \`'center'\`, and \`'right'\`. Using these values,
+the contents of your editor will be aligned to the specified direction regardless
+of language and character set.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-text-direction","title":"Text Direction","layout":"docs","category":"Advanced Topics","next":"advanced-topics-issues-and-pitfalls","permalink":"docs/advanced-topics-text-direction.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/block-components.js b/website/src/draft-js/docs/advanced/block-components.js
new file mode 100644
index 0000000..ec8e635
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/block-components.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Custom Block Components
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-block-components","title":"Custom Block Components","layout":"docs","category":"Advanced Topics","next":"advanced-topics-inline-styles","permalink":"docs/advanced/block-components.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/block-styling.js b/website/src/draft-js/docs/advanced/block-styling.js
new file mode 100644
index 0000000..ee07e2f
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/block-styling.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Block Styling
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-block-styling","title":"Block Styling","layout":"docs","category":"Advanced Topics","next":"advanced-topics-block-components","permalink":"docs/advanced/block-styling.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/decorators.js b/website/src/draft-js/docs/advanced/decorators.js
new file mode 100644
index 0000000..d4c13bf
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/decorators.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Decorators
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-decorators","title":"Decorators","layout":"docs","category":"Advanced Topics","next":"advanced-topics-key-bindings","permalink":"docs/advanced/decorators.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/entities.js b/website/src/draft-js/docs/advanced/entities.js
new file mode 100644
index 0000000..aa39c6a
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/entities.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Entities
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-entities","title":"Entities","layout":"docs","category":"Advanced Topics","next":"advanced-topics-decorators","permalink":"docs/advanced/entities.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/event-handling.js b/website/src/draft-js/docs/advanced/event-handling.js
new file mode 100644
index 0000000..302a274
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/event-handling.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Event Handling
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-event-handling","title":"Event Handling","layout":"docs","category":"Advanced Topics","next":"advanced-topics-block-styling","permalink":"docs/advanced/event-handling.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/inline-styles.js b/website/src/draft-js/docs/advanced/inline-styles.js
new file mode 100644
index 0000000..a0ab435
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/inline-styles.js
@@ -0,0 +1,116 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Within your editor, you may wish to provide a wide variety of inline style
+behavior that goes well beyond the bold/italic/underline basics. For instance,
+you may want to support variety with color, font families, font sizes, and more.
+Further, your desired styles may overlap or be mutually exclusive.
+
+The [Rich Editor](http://github.com/facebook/draft-js/examples/rich) and
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+examples demonstrate complex inline style behavior in action.
+
+### Model
+
+Within the Draft model, inline styles are represented at the character level,
+using an immutable \`OrderedSet\` to define the list of styles to be applied to
+each character. These styles are identified by string. (See [CharacterMetadata](/draft-js/docs/api/character-metadata.html) for details.)
+
+For example, consider the text "Hello **world**". The first six characters of
+the string are represented by the empty set, \`OrderedSet()\`. The final five
+characters are represented by \`OrderedSet.of('BOLD')\`. For convenience, we can
+think of these \`OrderedSet\` objects as arrays, though in reality we aggressively
+reuse identical immutable objects.
+
+In essence, our styles are:
+
+\`\`\`
+[
+  [], // H
+  [], // e
+  ...
+  ['BOLD'], // w
+  ['BOLD'], // o
+  // etc.
+]
+\`\`\`
+
+### Overlapping Styles
+
+Now let's say that we wish to make the middle range of characters italic as well:
+"He_llo **wo**_**rld**". This operation can be performed via the
+[Modifier](/draft-js/docs/api/modifier.html) API.
+
+The end result will accommodate the overlap by including \`'ITALIC'\` in the
+relevant \`OrderedSet\` objects as well.
+
+\`\`\`
+[
+  [], // H
+  [], // e
+  ['ITALIC'], // l
+  ...
+  ['BOLD', 'ITALIC'], // w
+  ['BOLD', 'ITALIC'], // o
+  ['BOLD'], // r
+  // etc.
+]
+\`\`\`
+
+When determining how to render inline-styled text, Draft will identify
+contiguous ranges of identically styled characters and render those characters
+together in styled \`span\` nodes.
+
+### Mapping a style string to CSS
+
+By default, \`DraftEditor\` provides support for a basic list of inline styles:
+\`'BOLD'\`, \`'ITALIC'\`, \`'UNDERLINE'\`, and \`'CODE'\`. These are mapped to simple CSS
+style objects, which are used to apply styles to the relevant ranges.
+
+For your editor, you may define custom style strings to include with these
+defaults, or you may override the default style objects for the basic styles.
+
+Within your \`DraftEditor\` use case, you may provide the \`customStyleMap\` prop
+to define your style objects. (See
+[Colorful Editor](http://github.com/facebook/draft-js/examples/color)
+for a live example.)
+
+For example, you may want to add a \`'STRIKETHROUGH'\` style. To do so, define a
+custom style map:
+
+\`\`\`
+const styleMap = {
+  'STRIKETHROUGH': {
+    textDecoration: 'line-through',
+  },
+};
+
+class MyEditor extends React.Component {
+  // ...
+  render() {
+    return (
+      <DraftEditor
+        customStyleMap={styleMap}
+        editorState={this.state.editorState}
+        ...
+      />
+    );
+  }
+}
+\`\`\`
+
+When rendered, the \`textDecoration: line-through\` style will be applied to all
+character ranges with the \`STRIKETHROUGH\` style.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-inline-styles","title":"Complex Inline Styles","layout":"docs","category":"Advanced Topics","next":"advanced-topics-nested-lists","permalink":"docs/advanced/inline-styles.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/key-bindings.js b/website/src/draft-js/docs/advanced/key-bindings.js
new file mode 100644
index 0000000..3fb5c49
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/key-bindings.js
@@ -0,0 +1,106 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+The \`DraftEditor\` component offers flexibility to define custom key bindings
+for your editor, via the \`keyBindingFn\` prop. This allows you to match key
+commands to behaviors in your editor component.
+
+### Defaults
+
+The default key binding function is \`getDefaultKeyBinding\`.
+
+Since the Draft framework maintains tight control over DOM rendering and
+behavior, basic editing commands must be captured and routed through the key
+binding system.
+
+\`getDefaultKeyBinding\` maps known OS-level editor commands to \`DraftEditorCommand\`
+strings, which then correspond to behaviors within component handlers.
+
+For instance, \`Ctrl+Z\` (Win) and \`Cmd+Z\` (OSX) map to the \`'undo'\` command,
+which then routes our handler to perform an \`EditorState.undo()\`.
+
+### Customization
+
+You may provide your own key binding function to supply custom command strings.
+
+It is recommended that your function use \`getDefaultKeyBinding\` as a
+fall-through case, so that your editor may benefit from default commands.
+
+With your custom command string, you may then implement the \`handleKeyCommand\`
+prop function, which allows you to map that command string to your desired
+behavior. If \`handleKeyCommand\` returns \`true\`, the command is considered
+handled. If it returns \`false\`, the command will fall through
+
+### Example
+
+Let's say we have an editor that should have a "Save" mechanism to periodically
+write your contents to the server as a draft copy.
+
+First, let's define our key binding function.
+
+\`\`\`
+import KeyBindingUtil from 'Draft';
+const {hasCommandModifier} = KeyBindingUtil;
+
+function myKeyBindingFn(e: SyntheticKeyboardEvent): string {
+  if (e.keyCode === 83 /* \`S\` key */ && hasCommandModifier(e)) {
+    return 'myeditor-save';
+  }
+  return getDefaultKeyBinding(e);
+}
+\`\`\`
+
+Our function receives a key event, and we check whether it matches our criteria:
+it must be an \`S\` key, and it must have a command modifier, i.e. the command
+key for OSX, or the control key otherwise.
+
+If the command is a match, return a string that names the command. Otherwise,
+fall through to the default key bindings.
+
+In our editor component, we can then make use of the command via the
+\`handleKeyCommand\` prop:
+
+\`\`\`
+class MyEditor extends React.Component {
+  // ...
+
+  handleKeyCommand(command: string): boolean {
+    if (command === 'myeditor-save') {
+      // Perform a request to save your contents, set
+      // a new \`editorState\`, etc.
+      return true;
+    }
+    return false;
+  }
+
+  render() {
+    return (
+      <DraftEditor
+        editorState={this.state.editorState}
+        handleKeyCommand={this.handleKeyCommand.bind(this)}
+        ...
+      />
+    );
+  }
+}
+\`\`\`
+
+The \`'myeditor-save'\` command can be used for our custom behavior, and returning
+true instructs the editor that the command has been handled and no more work
+is required.
+
+By returning false in all other cases, default commands are able to fall
+through to default handler behavior.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-key-bindings","title":"Key Bindings","layout":"docs","category":"Advanced Topics","next":"advanced-topics-event-handling","permalink":"docs/advanced/key-bindings.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/nested-lists.js b/website/src/draft-js/docs/advanced/nested-lists.js
new file mode 100644
index 0000000..a9e981f
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/nested-lists.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Nested Lists
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-nested-lists","title":"Nested Lists","layout":"docs","category":"Advanced Topics","next":"advanced-topics-text-direction","permalink":"docs/advanced/nested-lists.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/performance.js b/website/src/draft-js/docs/advanced/performance.js
new file mode 100644
index 0000000..ff186f3
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/performance.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Performance
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-performance","title":"Performance","layout":"docs","category":"Advanced Topics","next":"api-reference-editor-state","permalink":"docs/advanced/performance.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/text-direction.js b/website/src/draft-js/docs/advanced/text-direction.js
new file mode 100644
index 0000000..ce4f8fa
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/text-direction.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Nested Lists
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-text-direction","title":"Text Direction","layout":"docs","category":"Advanced Topics","next":"advanced-topics-unicode","permalink":"docs/advanced/text-direction.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/undo-redo.js b/website/src/draft-js/docs/advanced/undo-redo.js
new file mode 100644
index 0000000..fd18bce
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/undo-redo.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Undo/Redo
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-undo-redo","title":"Undo/Redo","layout":"docs","category":"Advanced Topics","next":"advanced-topics-performance","permalink":"docs/advanced/undo-redo.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/advanced/unicode.js b/website/src/draft-js/docs/advanced/unicode.js
new file mode 100644
index 0000000..2d262ec
--- /dev/null
+++ b/website/src/draft-js/docs/advanced/unicode.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Nested Lists
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"advanced-topics-unicode","title":"Unicode","layout":"docs","category":"Advanced Topics","next":"advanced-topics-undo-redo","permalink":"docs/advanced/unicode.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-character-metadata.js b/website/src/draft-js/docs/api-reference-character-metadata.js
new file mode 100644
index 0000000..6876322
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-character-metadata.js
@@ -0,0 +1,164 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`CharacterMetadata\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents inline style and entity information for a single character.
+
+\`CharacterMetadata\` objects are aggressively pooled and shared. If two characters
+have the same inline style and entity, they are represented with the same
+\`CharacterMetadata\` object. We therefore need only as many objects as combinations
+of utilized inline style sets with entity keys, keeping our memory footprint
+small even as the contents grow in size and complexity.
+
+To that end, you should create or apply changes to \`CharacterMetadata\` objects
+via the provided set of static methods, which will ensure that pooling is utilized.
+
+Most Draft use cases are unlikely to use these static methods, since most common edit
+operations are already implemented and available via utility modules. The getter
+methods, however, may come in handy at render time.
+
+See the API reference on
+[ContentBlock](/draft-js/docs/api-reference-content-block.html#representing-styles-and-entities)
+for information on how \`CharacterMetadata\` is used within \`ContentBlock\`.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#create">
+      <pre>static create(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applystyle">
+      <pre>static applyStyle(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removestyle">
+      <pre>static removeStyle(...): CharacterMetadata</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyentity">
+      <pre>static applyEntity(...): CharacterMetadata</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getstyle">
+      <pre>getStyle(): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasstyle">
+      <pre>hasStyle(style: string): boolean</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getentity">
+      <pre>getEntity(): ?string</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+Under the hood, these methods will utilize pooling to return a matching object,
+or return a new object if none exists.
+
+### create
+
+\`\`\`
+static create(config?: CharacterMetadataConfig): CharacterMetadata
+\`\`\`
+Generates a \`CharacterMetadata\` object from the provided configuration. This
+function should be used in lieu of a constructor.
+
+The configuration will be used to check whether a pooled match for this
+configuration already exists. If so, the pooled object will be returned.
+Otherwise, a new \`CharacterMetadata\` will be pooled for this configuration,
+and returned.
+
+### applyStyle
+
+\`\`\`
+static applyStyle(
+  record: CharacterMetadata,
+  style: string
+): CharacterMetadata
+\`\`\`
+Apply an inline style to this \`CharacterMetadata\`.
+
+### removeStyle
+
+\`\`\`
+static removeStyle(
+  record: CharacterMetadata,
+  style: string
+): CharacterMetadata
+\`\`\`
+Remove an inline style from this \`CharacterMetadata\`.
+
+### applyEntity
+
+\`\`\`
+static applyEntity(
+  record: CharacterMetadata,
+  entityKey: ?string
+): CharacterMetadata
+\`\`\`
+
+Apply an entity key -- or provide \`null\` to remove an entity key -- on this
+\`CharacterMetadata\`.
+
+## Methods
+
+### getStyle
+
+\`\`\`
+getStyle(): DraftInlineStyle
+\`\`\`
+Returns the \`DraftInlineStyle\` for this character, an \`OrderedSet\` of strings
+that represents the inline style to apply for the character at render time.
+
+### hasStyle
+
+\`\`\`
+hasStyle(style: string): boolean
+\`\`\`
+Returns whether this character has the specified style.
+
+### getEntity
+
+\`\`\`
+getEntity(): ?string
+\`\`\`
+Returns the entity key (if any) for this character, as mapped to the global set of
+entities tracked by the [\`Entity\`](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntity.js)
+module.
+
+By tracking a string key here, we can keep the corresponding metadata separate
+from the character representation.
+
+If null, no entity is applied for this character.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-character-metadata","title":"CharacterMetadata","layout":"docs","category":"API Reference","next":"api-reference-entity","permalink":"docs/api-reference-character-metadata.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-content-block.js b/website/src/draft-js/docs/api-reference-content-block.js
new file mode 100644
index 0000000..d3cc339
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-content-block.js
@@ -0,0 +1,253 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of a single block of editor content, including:
+
+  - Plain text contents of the block
+  - Type, e.g. paragraph, header, list item
+  - Entity, inline style, and depth information
+
+A \`ContentState\` object contains an \`OrderedMap\` of these \`ContentBlock\` objects,
+which together comprise the full contents of the editor.
+
+\`ContentBlock\` objects are largely analogous to block-level HTML elements like
+paragraphs and list items.
+
+New \`ContentBlock\` objects may be created directly using the constructor.
+Expected Record values are detailed below.
+
+### Representing styles and entities
+
+The \`characterList\` field is an immutable \`List\` containing a \`CharacterMetadata\`
+object for every character in the block. This is how we encode styles and
+entities for a given block.
+
+By making heavy use of immutability and data persistence for these lists and
+\`CharacterMetadata\` objects, edits to the content generally have little impact
+on the memory footprint of the editor.
+
+By encoding inline styles and entities together in this way, a function that
+performs edits on a \`ContentBlock\` can perform slices, concats, and other List
+methods on a single \`List\` object.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getkey">
+      <pre>getKey(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettype">
+      <pre>getType(): DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettext">
+      <pre>getText(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcharacterlist">
+      <pre>getCharacterList(): List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlength">
+      <pre>getLength(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getdepth">
+      <pre>getDepth(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getinlinestyleat">
+      <pre>getInlineStyleAt(offset: number): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getentityat">
+      <pre>getEntityAt(offset: number): ?string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findstyleranges">
+      <pre>findStyleRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findentityranges">
+      <pre>findEntityRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the \`ContentBlock\` constructor or to set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#key">
+      <pre>key: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#type">
+      <pre>type: DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#text">
+      <pre>text: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#characterlist">
+      <pre>characterList: List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#depth">
+      <pre>depth: number</pre>
+    </a>
+  </li>
+</ul>
+
+## Methods
+
+### getKey()
+
+\`\`\`
+getKey(): string
+\`\`\`
+Returns the string key for this \`ContentBlock\`.
+
+### getType()
+
+\`\`\`
+getType(): DraftBlockType
+\`\`\`
+Returns the type for this \`ContentBlock\`. Type values are largely analogous to
+block-level HTML elements.
+
+### getText()
+
+\`\`\`
+getText(): string
+\`\`\`
+Returns the full plaintext for this \`ContentBlock\`. This value does not contain
+any styling, decoration, or HTML information.
+
+### getCharacterList()
+
+\`\`\`
+getCharacterList(): List<CharacterMetadata>
+\`\`\`
+Returns an immutable \`List\` of \`CharacterMetadata\` objects, one for each
+character in the \`ContentBlock\`. (See [CharacterMetadata](/draft-js/docs/api-reference-character-metadata.html)
+for details.)
+
+This \`List\` contains all styling and entity information for the block.
+
+### getLength()
+
+\`\`\`
+getLength(): number
+\`\`\`
+Returns the length of the plaintext for the \`ContentBlock\`.
+
+This value uses the standard JavaScript \`length\` property for the string, and
+is therefore not Unicode-aware -- surrogate pairs will be counted as two
+characters.
+
+### getDepth()
+
+\`\`\`
+getDepth(): number
+\`\`\`
+Returns the depth value for this block, if any. This is currently used only
+for list items.
+
+### getInlineStyleAt()
+
+\`\`\`
+getInlineStyleAt(offset: number): DraftInlineStyle
+\`\`\`
+Returns the \`DraftInlineStyle\` value (an \`OrderedSet<string>\`) at a given offset
+within this \`ContentBlock\`.
+
+### getEntityAt()
+
+\`\`\`
+getEntityAt(offset: number): ?string
+\`\`\`
+Returns the entity key value (or \`null\` if none) at a given offset within this
+\`ContentBlock\`.
+
+### findStyleRanges()
+
+\`\`\`
+findStyleRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+\`\`\`
+Executes a callback for each contiguous range of styles within this
+\`ContentBlock\`.
+
+### findEntityRanges()
+
+\`\`\`
+findEntityRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+\`\`\`
+Executes a callback for each contiguous range of entities within this
+\`ContentBlock\`.
+
+## Properties
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the \`ContentBlock\` constructor or to set properties.
+
+### key
+See \`getKey()\`.
+
+### text
+See \`getText()\`.
+
+### type
+See \`getType()\`.
+
+### characterList
+See \`getCharacterList()\`.
+
+### depth
+See \`getDepth()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-content-block","title":"ContentBlock","layout":"docs","category":"API Reference","next":"api-reference-character-metadata","permalink":"docs/api-reference-content-block.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-content-state.js b/website/src/draft-js/docs/api-reference-content-state.js
new file mode 100644
index 0000000..6e2925d
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-content-state.js
@@ -0,0 +1,272 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentState\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>static createFromText(text: string): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>static createFromBlockArray(blocks: Array<ContentBlock>): ContentState</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+static createFromText(
+  text: string,
+  delimiter?: string
+): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+static createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-content-state","title":"ContentState","layout":"docs","category":"API Reference","next":"api-reference-content-block","permalink":"docs/api-reference-content-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-data-conversion.js b/website/src/draft-js/docs/api-reference-data-conversion.js
new file mode 100644
index 0000000..02e5511
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-data-conversion.js
@@ -0,0 +1,53 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Because a text editor doesn't exist in a vacuum and it's important to save
+contents for storage or transmission, you will want to be able to
+convert a \`ContentState\` into raw JS, and vice versa.
+
+To that end, we provide a couple of utility functions that allow you to perform
+these conversions.
+
+Note that the Draft library does not currently provide utilities to convert to
+and from markdown or markup, since different clients may have different requirements
+for these formats. We instead provide JavaScript objects that can be converted
+to other formats as needed.
+
+The Flow type [\`RawDraftContentState\`](https://github.com/facebook/draft-js/blob/master/src/model/encoding/RawDraftContentState.js)
+denotes the expected structure of the raw format of the contents. The raw state
+contains a list of content blocks, as well as a map of all relevant entity
+objects.
+
+## Functions
+
+### convertFromRaw
+
+\`\`\`
+convertFromRaw(rawState: RawDraftContentState): ContentState
+\`\`\`
+
+Given a raw state, convert it to  \`ContentState\` object. This is useful when
+restoring contents to use within a Draft editor.
+
+### convertToRaw
+
+\`\`\`
+convertToRaw(contentState: ContentState): RawDraftContentState
+\`\`\`
+
+Given a \`ContentState\` object, convert it to a raw JS structure. This is useful
+when saving an editor state for storage, conversion to other formats, or
+other usage within an application.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-data-conversion","title":"Data Conversion","layout":"docs","category":"API Reference","next":"api-reference-modifier","permalink":"docs/api-reference-data-conversion.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-editor-state.js b/website/src/draft-js/docs/api-reference-editor-state.js
new file mode 100644
index 0000000..349ca0a
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-editor-state.js
@@ -0,0 +1,488 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`EditorState\` is the top-level state object for the editor.
+
+It is an Immutable [Record](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+that represents the entire state of a Draft editor, including:
+
+  - The current text content state
+  - The current selection state
+  - The fully decorated representation of the contents
+  - Undo/redo stacks
+  - The most recent type of change made to the contents
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods below.
+
+## Overview
+
+*Common instance methods*
+
+The list below includes the most commonly used instance methods for \`EditorState\` objects.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getcurrentcontent">
+      <pre>getCurrentContent(): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselection">
+      <pre>getSelection(): SelectionState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcurrentinlinestyle">
+      <pre>getCurrentInlineStyle(): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocktree">
+      <pre>getBlockTree(): OrderedMap</pre>
+    </a>
+  </li>
+</ul>
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createwithcontent">
+      <pre>static createWithContent(contentState, ?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#create">
+      <pre>static create(config): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#push">
+      <pre>static push(editorState, contentState, changeType): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undo">
+      <pre>static undo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redo">
+      <pre>static redo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#acceptselection">
+      <pre>static acceptSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>static forceSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use the static \`EditorState\` methods to set properties, rather than using
+> the Immutable API directly.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#allowundo">
+      <pre>allowUndo</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#currentcontent">
+      <pre>currentContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#decorator">
+      <pre>decorator</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#directionMap">
+      <pre>directionMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>forceSelection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#incompositionmode">
+      <pre>inCompositionMode</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#inlinestyleoverride">
+      <pre>inlineStyleOverride</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#lastchangetype">
+      <pre>lastChangeType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#nativelyrenderedcontent">
+      <pre>nativelyRenderedContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redostack">
+      <pre>redoStack</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selection">
+      <pre>selection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#treemap">
+      <pre>treeMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undostack">
+      <pre>undoStack</pre>
+    </a>
+  </li>
+</ul>
+
+## Common Instance Methods
+
+### getCurrentContent
+
+\`\`\`
+getCurrentContent(): ContentState
+\`\`\`
+Returns the current contents of the editor.
+
+### getSelection
+
+\`\`\`
+getSelection(): SelectionState
+\`\`\`
+Returns the current cursor/selection state of the editor.
+
+### getCurrentInlineStyle
+
+\`\`\`
+getCurrentInlineStyle(): DraftInlineStyle
+\`\`\`
+Returns an \`OrderedSet<string>\` that represents the "current" inline style
+for the editor.
+
+This is the inline style value that would be used if a character were inserted
+for the current \`ContentState\` and \`SelectionState\`, and takes into account
+any inline style overrides that should be applied.
+
+### getBlockTree
+
+\`\`\`
+getBlockTree(blockKey: string): List;
+\`\`\`
+Returns an Immutable \`List\` of decorated and styled ranges. This is used for
+rendering purposes, and is generated based on the \`currentContent\` and
+\`decorator\`.
+
+At render time, this object is used to break the contents into the appropriate
+block, decorator, and styled range components.
+
+## Static Methods
+
+### createEmpty
+
+\`\`\`
+static createEmpty(decorator?: DraftDecoratorType): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with an empty \`ContentState\` and default
+configuration.
+
+### createWithContent
+
+\`\`\`
+static createWithContent(
+  contentState: ContentState,
+  decorator?: DraftDecoratorType
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object based on the \`ContentState\` and decorator
+provided.
+
+### create
+
+\`\`\`
+static create(config: EditorStateCreationConfig): EditorState
+\`\`\`
+Returns a new \`EditorState\` object based on a configuration object. Use this
+if you need custom configuration not available via the methods above.
+
+### push
+
+\`\`\`
+static push(
+  editorState: EditorState,
+  contentState: ContentState,
+  changeType: EditorChangeType
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`ContentState\` applied
+as the new \`currentContent\`. Based on the \`changeType\`, this \`ContentState\`
+may be regarded as a boundary state for undo/redo behavior. See
+[Undo/Redo](/draft-js/docs/advanced-undo-redo.html) discussion for details.
+
+All content changes must be applied to the EditorState with this method.
+
+_To be renamed._
+
+### undo
+
+\`\`\`
+static undo(editorState: EditorState): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the top of the undo stack applied
+as the new \`currentContent\`.
+
+The existing \`currentContent\` is pushed onto the \`redo\` stack.
+
+### redo
+
+\`\`\`
+static redo(editorState: EditorState): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the top of the redo stack applied
+as the new \`currentContent\`.
+
+The existing \`currentContent\` is pushed onto the \`undo\` stack.
+
+### acceptSelection
+
+\`\`\`
+static acceptSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`SelectionState\` applied,
+but without requiring the selection to be rendered.
+
+For example, this is useful when the DOM selection has changed outside of our
+control, and no re-renders are necessary.
+
+### forceSelection
+
+\`\`\`
+static forceSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`SelectionState\` applied,
+forcing the selection to be rendered.
+
+This is useful when the selection should be manually rendered in the correct
+location to maintain control of the rendered output.
+
+## Properties and Getters
+
+In most cases, the instance and static methods above should be sufficient to
+manage the state of your Draft editor.
+
+Below is the full list of properties tracked by an \`EditorState\`, as well as
+their corresponding getter methods, to better provide detail on the scope of the
+state tracked in this object.
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods above.
+
+### allowUndo
+
+\`\`\`
+allowUndo: boolean;
+getAllowUndo()
+\`\`\`
+Whether to allow undo/redo behavior in this editor. Default is \`true\`.
+
+Since the undo/redo stack is the major source of memory retention, if you have
+an editor UI that does not require undo/redo behavior, you might consider
+setting this to \`false\`.
+
+### currentContent
+
+\`\`\`
+currentContent: ContentState;
+getCurrentContent()
+\`\`\`
+The currently rendered \`ContentState\`. See [getCurrentContent()](#getcurrentcontent).
+
+### decorator
+
+\`\`\`
+decorator: ?DraftDecoratorType;
+getDecorator()
+\`\`\`
+The current decorator object, if any.
+
+Note that the \`ContentState\` is independent of your decorator. If a decorator
+is provided, it will be used to decorate ranges of text for rendering.
+
+### directionMap
+
+\`\`\`
+directionMap: BlockMap;
+getDirectionMap()
+\`\`\`
+A map of each block and its text direction, as determined by UnicodeBidiService.
+
+You should not manage this manually.
+
+### forceSelection
+
+\`\`\`
+forceSelection: boolean;
+mustForceSelection()
+\`\`\`
+Whether to force the current \`SelectionState\` to be rendered.
+
+You should not set this property manually -- see
+[forceSelection()](#forceselection).
+
+### inCompositionMode
+
+\`\`\`
+inCompositionMode: boolean;
+isInCompositionMode()
+\`\`\`
+Whether the user is in IME composition mode. This is useful for rendering the
+appropriate UI for IME users, even when no content changes have been committed
+to the editor. You should not set this property manually.
+
+### inlineStyleOverride
+
+\`\`\`
+inlineStyleOverride: DraftInlineStyle;
+getInlineStyleOverride()
+\`\`\`
+An inline style value to be applied to the next inserted characters. This is
+used when keyboard commands or style buttons are used to apply an inline style
+for a collapsed selection range.
+
+\`DraftInlineStyle\` is a type alias for an immutable \`OrderedSet\` of strings,
+each of which corresponds to an inline style.
+
+### lastChangeType
+
+\`\`\`
+lastChangeType: EditorChangeType;
+getLastChangeType()
+\`\`\`
+The type of content change that took place in order to bring us to our current
+\`ContentState\`. This is used when determining boundary states for undo/redo.
+
+### nativelyRenderedContent
+
+\`\`\`
+nativelyRenderedContent: ?ContentState;
+getNativelyRenderedContent()
+\`\`\`
+During edit behavior, the editor may allow certain actions to render natively.
+For instance, during normal typing behavior in the contentEditable-based component,
+we can typically allow key events to fall through to print characters in the DOM.
+In doing so, we can avoid extra re-renders and preserve spellcheck highlighting.
+
+When allowing native rendering behavior, it is appropriate to use the
+\`nativelyRenderedContent\` property to indicate that no re-render is necessary
+for this \`EditorState\`.
+
+### redoStack
+
+\`\`\`
+redoStack: Stack<ContentState>;
+getRedoStack()
+\`\`\`
+An immutable stack of \`ContentState\` objects that can be resurrected for redo
+operations. When performing an undo operation, the current \`ContentState\` is
+pushed onto the \`redoStack\`.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the \`allowUndo\` property.
+
+See also [undoStack](#undostack).
+
+### selection
+
+\`\`\`
+selection: SelectionState;
+getSelection()
+\`\`\`
+The currently rendered \`SelectionState\`. See [acceptSelection()](#acceptselection)
+and [forceSelection()](#forceselection).
+
+You should not manage this property manually.
+
+### treeMap
+
+\`\`\`
+treeMap: OrderedMap<string, List>;
+\`\`\`
+The fully decorated and styled tree of ranges to be rendered in the editor
+component. The \`treeMap\` object is generated based on a \`ContentState\` and an
+optional decorator (\`DraftDecoratorType\`).
+
+At render time, components should iterate through the \`treeMap\` object to
+render decorated ranges and styled ranges, using the [getBlockTree()](#getblocktree)
+method.
+
+You should not manage this property manually.
+
+### undoStack
+
+\`\`\`
+undoStack: Stack<ContentState>;
+getUndoStack()
+\`\`\`
+An immutable stack of \`ContentState\` objects that can be restored for undo
+operations.
+
+When performing operations that modify contents, we determine whether the
+current \`ContentState\` should be regarded as a "boundary" state that the user
+can reach by performing an undo operation. If so, the \`ContentState\` is pushed
+onto the \`undoStack\`. If not, the outgoing \`ContentState\` is simply discarded.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the \`allowUndo\` property.
+
+See also [redoStack](#redostack).
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-editor-state","title":"EditorState","layout":"docs","category":"API Reference","next":"api-reference-content-state","permalink":"docs/api-reference-editor-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-editor.js b/website/src/draft-js/docs/api-reference-editor.js
new file mode 100644
index 0000000..d08fbb8
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-editor.js
@@ -0,0 +1,200 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This article discusses the API and props of the core controlled contentEditable
+component itself, \`Editor\`. Props are defined within
+[\`DraftEditorProps\`](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js).
+
+## Props
+
+### Basics
+
+See [API Basics](/draft-js/docs/quickstart-api-basics.html) for an introduction.
+
+#### editorState
+\`\`\`
+editorState: EditorState
+\`\`\`
+The \`EditorState\` object to be rendered by the \`Editor\`.
+
+#### onChange
+\`\`\`
+onChange: (editorState: EditorState) => void
+\`\`\`
+The \`onChange\` function to be executed by the \`Editor\` when edits and selection
+changes occur.
+
+### Presentation (Optional)
+
+#### placeholder
+\`\`\`
+placeholder?: string
+\`\`\`
+Optional placeholder string to display when the editor is empty.
+
+Note: You can use CSS to style or hide your placeholder as needed. For instance,
+in the [rich editor example](https://github.com/facebook/draft-js/tree/master/examples/rich),
+the placeholder is hidden when the user changes block styling in an empty editor.
+This is because the placeholder may not line up with the cursor when the style
+is changed.
+
+#### textAlignment
+\`\`\`
+textAlignment?: DraftTextAlignment
+\`\`\`
+Optionally set the overriding text alignment for this editor. This alignment value will
+apply to the entire contents, regardless of default text direction for input text.
+
+You may use this if you wish to center your text or align it flush in one direction
+to fit it within your UI design.
+
+If this value is not set, text alignment will be based on the characters within
+the editor, on a per-block basis.
+
+#### blockRendererFn
+\`\`\`
+blockRendererFn?: (block: ContentBlock) => ?Object
+\`\`\`
+Optionally set a function to define custom block rendering. See
+[Advanced Topics: Block Components](/draft-js/docs/advanced-topics-block-components.html)
+for details on usage.
+
+#### blockStyleFn
+\`\`\`
+blockStyleFn?: (block: ContentBlock) => string
+\`\`\`
+Optionally set a function to define class names to apply to the given block
+when it is rendered. See
+[Advanced Topics: Block Styling](/draft-js/docs/advanced-topics-block-styling.html)
+for details on usage.
+
+#### customStyleMap
+\`\`\`
+customStyleMap?: Object
+\`\`\`
+Optionally define a map of inline styles to apply to spans of text with the specified
+style. See
+[Advanced Topics: Inline Styles](/draft-js/docs/advanced-topics-inline-styles.html)
+for details on usage.
+
+### Behavior (Optional)
+
+#### readOnly
+\`\`\`
+readOnly?: boolean
+\`\`\`
+Set whether the editor should be rendered as static DOM, with all editability
+disabled.
+
+This is useful when supporting interaction within
+[custom block components](/draft-js/docs/advanced-topics-block-components.html)
+or if you just want to display content for a static use case.
+
+Default is \`false\`.
+
+#### spellCheck
+\`\`\`
+spellCheck?: boolean
+\`\`\`
+Set whether spellcheck is turned on for your editor.
+
+Note that in OSX Safari, enabling spellcheck also enables autocorrect, if the user
+has it turned on. Also note that spellcheck is always disabled in IE, since the events
+needed to observe spellcheck events are not fired in IE.
+
+Default is \`false\`.
+
+#### stripPastedStyles
+\`\`\`
+stripPastedStyles?: boolean
+\`\`\`
+Set whether to remove all information except plaintext from pasted content.
+
+This should be used if your editor does not support rich styles.
+
+Default is \`false\`.
+
+### DOM and Accessibility (Optional)
+
+#### tabIndex
+#### ARIA props
+
+These props allow you to set accessibility properties on your editor. See
+[DraftEditorProps](https://github.com/facebook/draft-js/blob/master/src/component/base/DraftEditorProps.js) for the exhaustive list of supported attributes.
+
+### Cancelable Handlers (Optional)
+
+These prop functions are provided to allow custom event handling for a small
+set of useful events. By returning true from your handler, you indicate that
+the event is handled and the Draft core should do nothing more with it. By returning
+false, you defer to Draft to handle the event.
+
+#### handleReturn
+\`\`\`
+handleReturn?: (e: SyntheticKeyboardEvent) => boolean
+\`\`\`
+Handle a \`RETURN\` keydown event. Example usage: Choosing a mention tag from a
+rendered list of results to trigger applying the mention entity to your content.
+
+#### handleKeyCommand
+\`\`\`
+handleKeyCommand?: (command: string) => boolean
+\`\`\`
+Handle the named editor command. See
+[Advanced Topics: Key Bindings](/draft-js/docs/advanced-topics-key-bindings.html)
+for details on usage.
+
+#### handleBeforeInput
+\`\`\`
+handleBeforeInput?: (e: SyntheticInputEvent) => boolean
+\`\`\`
+Handle a \`beforeInput\` event before character insertion occurs within the editor.
+Example usage: After a user has typed \`- \` at the start of a new block, you might
+convert that \`ContentBlock\` into an \`unordered-list-item\`.
+
+At Facebook, we also use this to convert typed ASCII quotes into "smart" quotes,
+and to convert typed emoticons into images.
+
+#### handlePastedFiles
+\`\`\`
+handlePastedFiles?: (files: Array<Blob>) => boolean
+\`\`\`
+Handle files that have been pasted directly into the editor.
+
+### Key Handlers (Optional)
+
+These prop functions expose common useful key events. Example: at Facebook, these are
+used to provide keyboard interaction for mention results in inputs.
+
+#### onEscape
+\`\`\`
+onEscape?: (e: SyntheticKeyboardEvent) => void
+\`\`\`
+
+#### onTab
+\`\`\`
+onTab?: (e: SyntheticKeyboardEvent) => void
+\`\`\`
+
+#### onUpArrow
+\`\`\`
+onUpArrow?: (e: SyntheticKeyboardEvent) => void
+\`\`\`
+
+#### onDownArrow
+\`\`\`
+onDownArrow?: (e: SyntheticKeyboardEvent) => void
+\`\`\`
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-editor","title":"Editor Component","layout":"docs","category":"API Reference","next":"api-reference-editor-state","permalink":"docs/api-reference-editor.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-entity.js b/website/src/draft-js/docs/api-reference-entity.js
new file mode 100644
index 0000000..453a3fd
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-entity.js
@@ -0,0 +1,124 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`Entity\` is a static module containing the API for creating, retrieving, and
+updating entity objects, which are used for annotating text ranges with metadata.
+This module also houses the single store used to maintain entity data.
+
+This article is dedicated to covering the details of the API. See the
+[advanced topics article on entities](/draft-js/docs/advanced-topics-entities.html)
+for more detail on how entities may be used.
+
+Entity objects returned by \`Entity\` methods are represented as
+[DraftEntityInstance](https://github.com/facebook/draft-js/blob/master/src/model/entity/DraftEntityInstance.js) immutable records. These have a simple set of getter functions and should
+be used only for retrieval.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#create">
+      <pre>create(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#add">
+      <pre>add(instance: DraftEntityInstance): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#get">
+      <pre>get(key: string): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#mergedata">
+      <pre>mergeData(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#replacedata">
+      <pre>replaceData(...): DraftEntityInstance</pre>
+    </a>
+  </li>
+</ul>
+
+## Methods
+
+### create
+
+\`\`\`
+create(
+  type: DraftEntityType,
+  mutability: DraftEntityMutability,
+  data?: Object
+): string
+\`\`\`
+The \`create\` method should be used to generate a new entity object with the
+supplied properties.
+
+Note that a string is returned from this function. This is because entities
+are referenced by their string key in \`ContentState\`. The string value should
+be used within \`CharacterMetadata\` objects to track the entity for annotated
+characters.
+
+### add
+
+\`\`\`
+add(instance: DraftEntityInstance): string
+\`\`\`
+In most cases, you will use \`Entity.create()\`. This is a convenience method
+that you probably will not need in typical Draft usage.
+
+The \`add\` function is useful in cases where the instances have already been
+created, and now need to be added to the \`Entity\` store. This may occur in cases
+where a vanilla JavaScript representation of a \`ContentState\` is being revived
+for editing.
+
+### get
+
+\`\`\`
+get(key: string): DraftEntityInstance
+\`\`\`
+Returns the \`DraftEntityInstance\` for the specified key. Throws if no instance
+exists for that key.
+
+### mergeData
+
+\`\`\`
+mergeData(
+  key: string,
+  toMerge: {[key: string]: any}
+): DraftEntityInstance
+\`\`\`
+Since \`DraftEntityInstance\` objects are immutable, you cannot update an entity's
+metadata through typical mutative means.
+
+The \`mergeData\` method allows you to apply updates to the specified entity.
+
+### replaceData
+
+\`\`\`
+replaceData(
+  key: string,
+  newData: {[key: string]: any}
+): DraftEntityInstance
+\`\`\`
+The \`replaceData\` method is similar to the \`mergeData\` method, except it will
+totally discard the existing \`data\` value for the instance and replace it with
+the specified \`newData\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-entity","title":"Entity","layout":"docs","category":"API Reference","next":"api-reference-selection-state","permalink":"docs/api-reference-entity.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-modifier.js b/website/src/draft-js/docs/api-reference-modifier.js
new file mode 100644
index 0000000..cd83916
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-modifier.js
@@ -0,0 +1,215 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+The \`Modifier\` module is a static set of utility functions that encapsulate common
+edit operations on \`ContentState\` objects. It is highly recommended that you use
+these methods for edit operations.
+
+These methods also take care of removing or modifying entity ranges appropriately,
+given the mutability types of any affected entities.
+
+In each case, these methods accept \`ContentState\` objects with relevant
+parameters and return \`ContentState\` objects. The returned \`ContentState\`
+will be the same as the input object if no edit was actually performed.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#replacetext">
+      <pre>replaceText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#inserttext">
+      <pre>insertText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#movetext">
+      <pre>moveText(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#replacewithfragment">
+      <pre>replaceWithFragment(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removerange">
+      <pre>removeRange(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#splitblock">
+      <pre>splitBlock(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyinlinestyle">
+      <pre>applyInlineStyle(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#removeinlinestyle">
+      <pre>removeInlineStyle(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#setblocktype">
+      <pre>setBlockType(...): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#applyentity">
+      <pre>applyEntity(...): ContentState</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### replaceText
+
+\`\`\`
+replaceText(
+  contentState: ContentState,
+  rangeToReplace: SelectionState,
+  text: string,
+  inlineStyle?: DraftInlineStyle,
+  entityKey?: ?string
+): ContentState
+\`\`\`
+Replaces the specified range of this \`ContentState\` with the supplied string,
+with the inline style and entity key applied to the entire inserted string.
+
+Example: On Facebook, when replacing \`@abraham lincoln\` with a mention of
+Abraham Lincoln, the entire old range is the target to replace and the mention
+entity should be applied to the inserted string.
+
+### insertText
+
+\`\`\`
+insertText(
+  contentState: ContentState,
+  targetRange: SelectionState,
+  text: string,
+  inlineStyle?: DraftInlineStyle,
+  entityKey?: ?string
+): ContentState
+\`\`\`
+Identical to \`replaceText\`, but enforces that the target range is collapsed
+so that no characters are replaced. This is just for convenience, since text
+edits are so often insertions rather than replacements.
+
+### moveText
+
+\`\`\`
+moveText(
+  contentState: ContentState,
+  removalRange: SelectionState,
+  targetRange: SelectionState
+): ContentState
+\`\`\`
+Moves the "removal" range to the "target" range, replacing the target text.
+
+### replaceWithFragment
+
+\`\`\`
+replaceWithFragment(
+  contentState: ContentState,
+  targetRange: SelectionState,
+  fragment: BlockMap
+): ContentState
+\`\`\`
+A "fragment" is a section of a block map, effectively just an
+\`OrderedMap<string, ContentBlock>\` much the same as the full block map of a
+\`ContentState\` object.
+
+This method will replace the "target" range with the fragment.
+
+Example: When pasting content, we convert the paste into a fragment to be inserted
+into the editor, then use this method to add it.
+
+### removeRange
+
+\`\`\`
+removeRange(
+  contentState: ContentState,
+  rangeToRemove: SelectionState,
+  removalDirection: DraftRemovalDirection
+): ContentState
+\`\`\`
+Remove an entire range of text from the editor. The removal direction is important
+for proper entity deletion behavior.
+
+### splitBlock
+
+\`\`\`
+splitBlock(
+  contentState: ContentState,
+  selectionState: SelectionState
+): ContentState
+\`\`\`
+Split the selected block into two blocks. This should only be used if the
+selection is collapsed.
+
+### applyInlineStyle
+
+\`\`\`
+applyInlineStyle(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  inlineStyle: string
+): ContentState
+\`\`\`
+Apply the specified inline style to the entire selected range.
+
+### removeInlineStyle
+
+\`\`\`
+removeInlineStyle(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  inlineStyle: string
+): ContentState
+\`\`\`
+Remove the specified inline style from the entire selected range.
+
+### setBlockType
+
+\`\`\`
+setBlockType(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  blockType: DraftBlockType
+): ContentState
+\`\`\`
+Set the block type for all selected blocks.
+
+### applyEntity
+
+\`\`\`
+applyEntity(
+  contentState: ContentState,
+  selectionState: SelectionState,
+  entityKey: ?string
+): ContentState
+\`\`\`
+Apply an entity to the entire selected range, or remove all entities from the
+range if \`entityKey\` is \`null\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-modifier","title":"Modifier","layout":"docs","category":"API Reference","permalink":"docs/api-reference-modifier.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api-reference-selection-state.js b/website/src/draft-js/docs/api-reference-selection-state.js
new file mode 100644
index 0000000..0b66e34
--- /dev/null
+++ b/website/src/draft-js/docs/api-reference-selection-state.js
@@ -0,0 +1,331 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`SelectionState\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents a selection range in the editor.
+
+The most common use for the \`SelectionState\` object is via \`EditorState.getSelection()\`,
+which provides the \`SelectionState\` currently being rendered in the editor.
+
+### Keys and Offsets
+
+A selection range has two points: an **anchor** and a **focus**. (Read more on
+[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary)).
+
+The native DOM approach represents each point as a Node/offset pair, where the offset
+is a number corresponding either to a position within a Node's \`childNodes\` or, if the
+Node is a text node, a character offset within the text contents.
+
+Since Draft maintains the contents of the editor using \`ContentBlock\` objects,
+we can use our own model to represent these points. Thus, selection points are
+tracked as key/offset pairs, where the \`key\` value is the key of the \`ContentBlock\`
+where the point is positioned and the \`offset\` value is the character offset
+within the block.
+
+### Start/End vs. Anchor/Focus
+
+The concept of **anchor** and **focus** is very useful when actually rendering
+a selection state in the browser, as it allows us to use forward and backward
+selection as needed. For editing operations, however, the direction of the selection
+doesn't matter. In this case, it is more appropriate to think in terms of
+**start** and **end** points.
+
+The \`SelectionState\` therefore exposes both anchor/focus values and
+start/end values. When managing selection behavior, we recommend that
+you work with _anchor_ and _focus_ values to maintain selection direction.
+When managing content operations, however, we recommend that you use _start_
+and _end_ values.
+
+For instance, when extracting a slice of text from a block based on a
+\`SelectionState\`, it is irrelevant whether the selection is backward:
+
+\`\`\`
+var start = selectionState.getStartOffset();
+var end = selectionState.getEndOffset();
+var selectedText = myContentBlock.getText().slice(start, end);
+\`\`\`
+
+Note that \`SelectionState\` itself tracks only _anchor_ and _focus_ values.
+_Start_ and _end_ values are derived.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(blockKey)</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getstartkey">
+      <pre>getStartKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getstartoffset">
+      <pre>getStartOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendkey">
+      <pre>getEndKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendoffset">
+      <pre>getEndOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchorkey">
+      <pre>getAnchorKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchoroffset">
+      <pre>getAnchorOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocuskey">
+      <pre>getFocusKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocusoffset">
+      <pre>getFocusOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getisbackward">
+      <pre>getIsBackward()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gethasfocus">
+      <pre>getHasFocus()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#iscollapsed">
+      <pre>isCollapsed()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasedgewithin">
+      <pre>hasEdgeWithin(blockKey, start, end)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#serialize">
+      <pre>serialize()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#anchorkey">
+      <pre>anchorKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#anchoroffset">
+      <pre>anchorOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focuskey">
+      <pre>focusKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focusoffset">
+      <pre>focusOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#isbackward">
+      <pre>isBackward</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasfocus">
+      <pre>hasFocus</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createEmpty()
+
+\`\`\`
+createEmpty(blockKey: string): SelectionState
+\`\`\`
+Create a \`SelectionState\` object at the zero offset of the provided block key
+and \`hasFocus\` set to false.
+
+## Methods
+
+### getStartKey()
+
+\`\`\`
+getStartKey(): string
+\`\`\`
+Returns the key of the block containing the start position of the selection range.
+
+### getStartOffset()
+
+\`\`\`
+getStartOffset(): number
+\`\`\`
+Returns the block-level character offset of the start position of the selection range.
+
+### getEndKey()
+
+\`\`\`
+getEndKey(): string
+\`\`\`
+Returns the key of the block containing the end position of the selection range.
+
+### getEndOffset()
+
+\`\`\`
+getEndOffset(): number
+\`\`\`
+Returns the block-level character offset of the end position of the selection range.
+
+### getAnchorKey()
+
+\`\`\`
+getAnchorKey(): string
+\`\`\`
+Returns the key of the block containing the anchor position of the selection range.
+
+### getAnchorOffset()
+
+\`\`\`
+getAnchorOffset(): number
+\`\`\`
+Returns the block-level character offset of the anchor position of the selection range.
+
+### getFocusKey()
+
+\`\`\`
+getFocusKey(): string
+\`\`\`
+Returns the key of the block containing the focus position of the selection range.
+
+### getFocusOffset()
+
+\`\`\`
+getFocusOffset(): number
+\`\`\`
+Returns the block-level character offset of the focus position of the selection range.
+
+### getIsBackward()
+
+\`\`\`
+getIsBackward(): boolean
+\`\`\`
+Returns whether the focus position is before the anchor position in the document.
+
+This must be derived from the key order of the active \`ContentState\`, or if the selection
+range is entirely within one block, a comparison of the anchor and focus offset values.
+
+### getHasFocus()
+
+\`\`\`
+getHasFocus(): boolean
+\`\`\`
+Returns whether the editor has focus.
+
+### isCollapsed()
+
+\`\`\`
+isCollapsed(): boolean
+\`\`\`
+Returns whether the selection range is collapsed, i.e. a caret. This is true
+when the anchor and focus keys are the same /and/ the anchor and focus offsets
+are the same.
+
+### hasEdgeWithin()
+
+\`\`\`
+hasEdgeWithin(blockKey: string, start: number, end: number): boolean
+\`\`\`
+Returns whether the selection range has an edge that overlaps with the specified
+start/end range within a given block.
+
+This is useful when setting DOM selection within a block after contents are
+rendered.
+
+### serialize()
+
+\`\`\`
+serialize(): string
+\`\`\`
+Returns a serialized version of the \`SelectionState\`. Useful for debugging.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+\`\`\`
+var selectionState = SelectionState.createEmpty('foo');
+var updatedSelection = selectionState.merge({
+  focusKey: 'bar',
+  focusOffset: 0,
+});
+var anchorKey = updatedSelection.getAnchorKey(); // 'foo'
+var focusKey = updatedSelection.getFocusKey(); // 'bar'
+\`\`\`
+
+### anchorKey
+The block containing the anchor end of the selection range.
+
+### anchorOffset
+The offset position of the anchor end of the selection range.
+
+### focusKey
+The block containing the focus end of the selection range.
+
+### focusOffset
+The offset position of the focus end of the selection range.
+
+### isBackward
+If the anchor position is lower in the document than the focus position, the
+selection is backward. Note: The \`SelectionState\` is an object with
+no knowledge of the \`ContentState\` structure. Therefore, when updating
+\`SelectionState\` values, you are responsible for updating \`isBackward\` as well.
+
+### hasFocus
+Whether the editor currently has focus.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-selection-state","title":"SelectionState","layout":"docs","category":"API Reference","next":"api-reference-data-conversion","permalink":"docs/api-reference-selection-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/character-metadata.js b/website/src/draft-js/docs/api/character-metadata.js
new file mode 100644
index 0000000..95ea318
--- /dev/null
+++ b/website/src/draft-js/docs/api/character-metadata.js
@@ -0,0 +1,262 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`CharacterMetadata\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents inline style and entity information for a single character.
+
+\`CharacterMetadata\` objects are pooled and shared.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray()</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-character-metadata","title":"CharacterMetadata","layout":"docs","category":"API Reference","next":"api-reference-selection-state","permalink":"docs/api/character-metadata.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/content-block.js b/website/src/draft-js/docs/api/content-block.js
new file mode 100644
index 0000000..1e04b58
--- /dev/null
+++ b/website/src/draft-js/docs/api/content-block.js
@@ -0,0 +1,239 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of a single block of editor content, including:
+
+  - Plain text contents of the block
+  - Type, e.g. paragraph, header, list item
+  - Entity, inline style, and depth information
+
+A \`ContentState\` object contains an \`OrderedMap\` of these \`ContentBlock\` objects,
+which together comprise the full contents of the editor.
+
+\`ContentBlock\` objects are largely analogous to block-level HTML elements like
+paragraphs and list items.
+
+New \`ContentBlock\` objects may be created directly using the constructor.
+Expected Record values are detailed below.
+
+## Overview
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getkey">
+      <pre>getKey(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettype">
+      <pre>getType(): DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gettext">
+      <pre>getText(): string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcharacterlist">
+      <pre>getCharacterList(): List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlength">
+      <pre>getLength(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getdepth">
+      <pre>getDepth(): number</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getinlinestyleat">
+      <pre>getInlineStyleAt(offset: number): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getentityat">
+      <pre>getEntityAt(offset: number): ?string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findstyleranges">
+      <pre>findStyleRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#findentityranges">
+      <pre>findEntityRanges(filterFn: Function, callback: Function): void</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the \`ContentBlock\` constructor or to set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#key">
+      <pre>key: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#type">
+      <pre>type: DraftBlockType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#text">
+      <pre>text: string</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#characterlist">
+      <pre>characterList: List<CharacterMetadata></pre>
+    </a>
+  </li>
+  <li>
+    <a href="#depth">
+      <pre>depth: number</pre>
+    </a>
+  </li>
+</ul>
+
+## Methods
+
+### getKey()
+
+\`\`\`
+getKey(): string
+\`\`\`
+Returns the string key for this \`ContentBlock\`.
+
+### getType()
+
+\`\`\`
+getType(): DraftBlockType
+\`\`\`
+Returns the type for this \`ContentBlock\`. Type values are largely analogous to
+block-level HTML elements.
+
+### getText()
+
+\`\`\`
+getText(): string
+\`\`\`
+Returns the full plaintext for this \`ContentBlock\`. This value does not contain
+any styling, decoration, or HTML information.
+
+### getCharacterList()
+
+\`\`\`
+getCharacterList(): List<CharacterMetadata>
+\`\`\`
+Returns an immutable \`List\` of \`CharacterMetadata\` objects, one for each
+character in the \`ContentBlock\`. (See [CharacterMetadata](/draft-js/docs/api/character-metadata.html)
+for details.)
+
+This \`List\` contains all styling and entity information for the block.
+
+### getLength()
+
+\`\`\`
+getLength(): number
+\`\`\`
+Returns the length of the plaintext for the \`ContentBlock\`.
+
+This value uses the standard JavaScript \`length\` property for the string, and
+is therefore not Unicode-aware -- surrogate pairs will be counted as two
+characters.
+
+### getDepth()
+
+\`\`\`
+getDepth(): number
+\`\`\`
+Returns the depth value for this block, if any. This is currently used only
+for list items.
+
+### getInlineStyleAt()
+
+\`\`\`
+getInlineStyleAt(offset: number): DraftInlineStyle
+\`\`\`
+Returns the \`DraftInlineStyle\` value (an \`OrderedSet<string>\`) at a given offset
+within this \`ContentBlock\`.
+
+### getEntityAt()
+
+\`\`\`
+getEntityAt(offset: number): ?string
+\`\`\`
+Returns the entity key value (or \`null\` if none) at a given offset within this
+\`ContentBlock\`.
+
+### findStyleRanges()
+
+\`\`\`
+findStyleRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+\`\`\`
+Executes a callback for each contiguous range of styles within this
+\`ContentBlock\`.
+
+### findEntityRanges()
+
+\`\`\`
+findEntityRanges(
+  filterFn: (value: CharacterMetadata) => boolean,
+  callback: (start: number, end: number) => void
+): void
+\`\`\`
+Executes a callback for each contiguous range of entities within this
+\`ContentBlock\`.
+
+## Properties
+
+> Note
+>
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+> for the \`ContentBlock\` constructor or to set properties.
+
+### key
+See \`getKey()\`.
+
+### text
+See \`getText()\`.
+
+### type
+See \`getType()\`.
+
+### characterList
+See \`getCharacterList()\`.
+
+### depth
+See \`getDepth()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-content-block","title":"ContentBlock","layout":"docs","category":"API Reference","next":"api-reference-character-metadata","permalink":"docs/api/content-block.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/content-state.js b/website/src/draft-js/docs/api/content-state.js
new file mode 100644
index 0000000..d669258
--- /dev/null
+++ b/website/src/draft-js/docs/api/content-state.js
@@ -0,0 +1,269 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentState\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText(text: string)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray(blocks: Array<ContentBlock>)</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-content-state","title":"ContentState","layout":"docs","category":"API Reference","next":"api-reference-content-block","permalink":"docs/api/content-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/data-conversion.js b/website/src/draft-js/docs/api/data-conversion.js
new file mode 100644
index 0000000..e5396f1
--- /dev/null
+++ b/website/src/draft-js/docs/api/data-conversion.js
@@ -0,0 +1,269 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray()</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-data-conversion","title":"Data Conversion","layout":"docs","category":"API Reference","next":"api-reference-modifier","permalink":"docs/api/data-conversion.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/drafteditor.js b/website/src/draft-js/docs/api/drafteditor.js
new file mode 100644
index 0000000..8c26073
--- /dev/null
+++ b/website/src/draft-js/docs/api/drafteditor.js
@@ -0,0 +1,269 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray()</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-drafteditor","title":"DraftEditor","layout":"docs","category":"API Reference","next":"further-future-work","permalink":"docs/api/drafteditor.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/editor-state.js b/website/src/draft-js/docs/api/editor-state.js
new file mode 100644
index 0000000..faba7d4
--- /dev/null
+++ b/website/src/draft-js/docs/api/editor-state.js
@@ -0,0 +1,488 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`EditorState\` is the top-level state object for the editor.
+
+It is an Immutable [Record](http://facebook.github.io/immutable-js/docs/#/Record/Record)
+that represents the entire state of a Draft editor, including:
+
+  - The current text content state
+  - The current selection state
+  - The fully decorated representation of the contents
+  - Undo/redo stacks
+  - The most recent type of change made to the contents
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods below.
+
+## Overview
+
+*Common instance methods*
+
+The list below includes the most commonly used instance methods for \`EditorState\` objects.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getcurrentcontent">
+      <pre>getCurrentContent(): ContentState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselection">
+      <pre>getSelection(): SelectionState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getcurrentinlinestyle">
+      <pre>getCurrentInlineStyle(): DraftInlineStyle</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocktree">
+      <pre>getBlockTree(): OrderedMap</pre>
+    </a>
+  </li>
+</ul>
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createwithcontent">
+      <pre>static createWithContent(contentState, ?decorator): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#create">
+      <pre>static create(config): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#push">
+      <pre>static push(editorState, contentState, changeType): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undo">
+      <pre>static undo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redo">
+      <pre>static redo(editorState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#acceptselection">
+      <pre>static acceptSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>static forceSelection(editorState, selectionState): EditorState</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Note
+>
+> Use the static \`EditorState\` methods to set properties, rather than using
+> the Immutable API directly.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#allowundo">
+      <pre>allowUndo</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#currentcontent">
+      <pre>currentContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#decorator">
+      <pre>decorator</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#directionMap">
+      <pre>directionMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#forceselection">
+      <pre>forceSelection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#incompositionmode">
+      <pre>inCompositionMode</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#inlinestyleoverride">
+      <pre>inlineStyleOverride</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#lastchangetype">
+      <pre>lastChangeType</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#nativelyrenderedcontent">
+      <pre>nativelyRenderedContent</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#redostack">
+      <pre>redoStack</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selection">
+      <pre>selection</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#treemap">
+      <pre>treeMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#undostack">
+      <pre>undoStack</pre>
+    </a>
+  </li>
+</ul>
+
+## Common Instance Methods
+
+### getCurrentContent
+
+\`\`\`
+getCurrentContent(): ContentState
+\`\`\`
+Returns the current contents of the editor.
+
+### getSelection
+
+\`\`\`
+getSelection(): SelectionState
+\`\`\`
+Returns the current cursor/selection state of the editor.
+
+### getCurrentInlineStyle
+
+\`\`\`
+getCurrentInlineStyle(): DraftInlineStyle
+\`\`\`
+Returns an \`OrderedSet<string>\` that represents the "current" inline style
+for the editor.
+
+This is the inline style value that would be used if a character were inserted
+for the current \`ContentState\` and \`SelectionState\`, and takes into account
+any inline style overrides that should be applied.
+
+### getBlockTree
+
+\`\`\`
+getBlockTree(blockKey: string): List;
+\`\`\`
+Returns an Immutable \`List\` of decorated and styled ranges. This is used for
+rendering purposes, and is generated based on the \`currentContent\` and
+\`decorator\`.
+
+At render time, this object is used to break the contents into the appropriate
+block, decorator, and styled range components.
+
+## Static Methods
+
+### createEmpty
+
+\`\`\`
+static createEmpty(decorator?: DraftDecoratorType): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with an empty \`ContentState\` and default
+configuration.
+
+### createWithContent
+
+\`\`\`
+static createWithContent(
+  contentState: ContentState,
+  decorator?: DraftDecoratorType
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object based on the \`ContentState\` and decorator
+provided.
+
+### create
+
+\`\`\`
+static create(config: EditorStateCreationConfig): EditorState
+\`\`\`
+Returns a new \`EditorState\` object based on a configuration object. Use this
+if you need custom configuration not available via the methods above.
+
+### push
+
+\`\`\`
+static push(
+  editorState: EditorState,
+  contentState: ContentState,
+  changeType: EditorChangeType
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`ContentState\` applied
+as the new \`currentContent\`. Based on the \`changeType\`, this \`ContentState\`
+may be regarded as a boundary state for undo/redo behavior. See
+[Undo/Redo](/draft-js/docs/advanced/undo-redo.html) discussion for details.
+
+All content changes must be applied to the EditorState with this method.
+
+_To be renamed._
+
+### undo
+
+\`\`\`
+static undo(editorState: EditorState): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the top of the undo stack applied
+as the new \`currentContent\`.
+
+The existing \`currentContent\` is pushed onto the \`redo\` stack.
+
+### redo
+
+\`\`\`
+static redo(editorState: EditorState): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the top of the redo stack applied
+as the new \`currentContent\`.
+
+The existing \`currentContent\` is pushed onto the \`undo\` stack.
+
+### acceptSelection
+
+\`\`\`
+static acceptSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`SelectionState\` applied,
+but without requiring the selection to be rendered.
+
+For example, this is useful when the DOM selection has changed outside of our
+control, and no re-renders are necessary.
+
+### forceSelection
+
+\`\`\`
+static forceSelection(
+  editorState: EditorState,
+  selectionState: SelectionState
+): EditorState
+\`\`\`
+Returns a new \`EditorState\` object with the specified \`SelectionState\` applied,
+forcing the selection to be rendered.
+
+This is useful when the selection should be manually rendered in the correct
+location to maintain control of the rendered output.
+
+## Properties and Getters
+
+In most cases, the instance and static methods above should be sufficient to
+manage the state of your Draft editor.
+
+Below is the full list of properties tracked by an \`EditorState\`, as well as
+their corresponding getter methods, to better provide detail on the scope of the
+state tracked in this object.
+
+> Note
+>
+> You should not use the Immutable API when using EditorState objects.
+> Instead, use the instance getters and static methods above.
+
+### allowUndo
+
+\`\`\`
+allowUndo: boolean;
+getAllowUndo()
+\`\`\`
+Whether to allow undo/redo behavior in this editor. Default is \`true\`.
+
+Since the undo/redo stack is the major source of memory retention, if you have
+an editor UI that does not require undo/redo behavior, you might consider
+setting this to \`false\`.
+
+### currentContent
+
+\`\`\`
+currentContent: ContentState;
+getCurrentContent()
+\`\`\`
+The currently rendered \`ContentState\`. See [getCurrentContent()](#getcurrentcontent).
+
+### decorator
+
+\`\`\`
+decorator: ?DraftDecoratorType;
+getDecorator()
+\`\`\`
+The current decorator object, if any.
+
+Note that the \`ContentState\` is independent of your decorator. If a decorator
+is provided, it will be used to decorate ranges of text for rendering.
+
+### directionMap
+
+\`\`\`
+directionMap: BlockMap;
+getDirectionMap()
+\`\`\`
+A map of each block and its text direction, as determined by UnicodeBidiService.
+
+You should not manage this manually.
+
+### forceSelection
+
+\`\`\`
+forceSelection: boolean;
+mustForceSelection()
+\`\`\`
+Whether to force the current \`SelectionState\` to be rendered.
+
+You should not set this property manually -- see
+[forceSelection()](#forceselection).
+
+### inCompositionMode
+
+\`\`\`
+inCompositionMode: boolean;
+isInCompositionMode()
+\`\`\`
+Whether the user is in IME composition mode. This is useful for rendering the
+appropriate UI for IME users, even when no content changes have been committed
+to the editor. You should not set this property manually.
+
+### inlineStyleOverride
+
+\`\`\`
+inlineStyleOverride: DraftInlineStyle;
+getInlineStyleOverride()
+\`\`\`
+An inline style value to be applied to the next inserted characters. This is
+used when keyboard commands or style buttons are used to apply an inline style
+for a collapsed selection range.
+
+\`DraftInlineStyle\` is a type alias for an immutable \`OrderedSet\` of strings,
+each of which corresponds to an inline style.
+
+### lastChangeType
+
+\`\`\`
+lastChangeType: EditorChangeType;
+getLastChangeType()
+\`\`\`
+The type of content change that took place in order to bring us to our current
+\`ContentState\`. This is used when determining boundary states for undo/redo.
+
+### nativelyRenderedContent
+
+\`\`\`
+nativelyRenderedContent: ?ContentState;
+getNativelyRenderedContent()
+\`\`\`
+During edit behavior, the editor may allow certain actions to render natively.
+For instance, during normal typing behavior in the contentEditable-based component,
+we can typically allow key events to fall through to print characters in the DOM.
+In doing so, we can avoid extra re-renders and preserve spellcheck highlighting.
+
+When allowing native rendering behavior, it is appropriate to use the
+\`nativelyRenderedContent\` property to indicate that no re-render is necessary
+for this \`EditorState\`.
+
+### redoStack
+
+\`\`\`
+redoStack: Stack<ContentState>;
+getRedoStack()
+\`\`\`
+An immutable stack of \`ContentState\` objects that can be resurrected for redo
+operations. When performing an undo operation, the current \`ContentState\` is
+pushed onto the \`redoStack\`.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the \`allowUndo\` property.
+
+See also [undoStack](#undostack).
+
+### selection
+
+\`\`\`
+selection: SelectionState;
+getSelection()
+\`\`\`
+The currently rendered \`SelectionState\`. See [acceptSelection()](#acceptselection)
+and [forceSelection()](#forceselection).
+
+You should not manage this property manually.
+
+### treeMap
+
+\`\`\`
+treeMap: OrderedMap<string, List>;
+\`\`\`
+The fully decorated and styled tree of ranges to be rendered in the editor
+component. The \`treeMap\` object is generated based on a \`ContentState\` and an
+optional decorator (\`DraftDecoratorType\`).
+
+At render time, components should iterate through the \`treeMap\` object to
+render decorated ranges and styled ranges, using the [getBlockTree()](#getblocktree)
+method.
+
+You should not manage this property manually.
+
+### undoStack
+
+\`\`\`
+undoStack: Stack<ContentState>;
+getUndoStack()
+\`\`\`
+An immutable stack of \`ContentState\` objects that can be restored for undo
+operations.
+
+When performing operations that modify contents, we determine whether the
+current \`ContentState\` should be regarded as a "boundary" state that the user
+can reach by performing an undo operation. If so, the \`ContentState\` is pushed
+onto the \`undoStack\`. If not, the outgoing \`ContentState\` is simply discarded.
+
+You should not manage this property manually. If you would like to disable
+undo/redo behavior, use the \`allowUndo\` property.
+
+See also [redoStack](#redostack).
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-editor-state","title":"EditorState","layout":"docs","category":"API Reference","next":"api-reference-content-state","permalink":"docs/api/editor-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/modifier.js b/website/src/draft-js/docs/api/modifier.js
new file mode 100644
index 0000000..2903519
--- /dev/null
+++ b/website/src/draft-js/docs/api/modifier.js
@@ -0,0 +1,269 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray()</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-modifier","title":"Modifier","layout":"docs","category":"API Reference","next":"api-reference-transaction-functions","permalink":"docs/api/modifier.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/selection-state.js b/website/src/draft-js/docs/api/selection-state.js
new file mode 100644
index 0000000..2ebac65
--- /dev/null
+++ b/website/src/draft-js/docs/api/selection-state.js
@@ -0,0 +1,331 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`SelectionState\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents a selection range in the editor.
+
+The most common use for the \`SelectionState\` object is via \`EditorState.getSelection()\`,
+which provides the \`SelectionState\` currently being rendered in the editor.
+
+### Keys and Offsets
+
+A selection range has two points: an **anchor** and a **focus**. (Read more on
+[MDN](https://developer.mozilla.org/en-US/docs/Web/API/Selection#Glossary)).
+
+The native DOM approach represents each point as a Node/offset pair, where the offset
+is a number corresponding either to a position within a Node's \`childNodes\` or, if the
+Node is a text node, a character offset within the text contents.
+
+Since Draft maintains the contents of the editor using \`ContentBlock\` objects,
+we can use our own model to represent these points. Thus, selection points are
+tracked as key/offset pairs, where the \`key\` value is the key of the \`ContentBlock\`
+where the point is positioned and the \`offset\` value is the character offset
+within the block.
+
+### Start/End vs. Anchor/Focus
+
+The concept of **anchor** and **focus** is very useful when actually rendering
+a selection state in the browser, as it allows us to use forward and backward
+selection as needed. For editing operations, however, the direction of the selection
+doesn't matter. In this case, it is more appropriate to think in terms of
+**start** and **end** points.
+
+The \`SelectionState\` therefore exposes both anchor/focus values and
+start/end values. When managing selection behavior, we recommend that
+you work with _anchor_ and _focus_ values to maintain selection direction.
+When managing content operations, however, we recommend that you use _start_
+and _end_ values.
+
+For instance, when extracting a slice of text from a block based on a
+\`SelectionState\`, it is irrelevant whether the selection is backward:
+
+\`\`\`
+var start = selectionState.getStartOffset();
+var end = selectionState.getEndOffset();
+var selectedText = myContentBlock.getText().slice(start, end);
+\`\`\`
+
+Note that \`SelectionState\` itself tracks only _anchor_ and _focus_ values.
+_Start_ and _end_ values are derived.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createempty">
+      <pre>static createEmpty(blockKey)</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getstartkey">
+      <pre>getStartKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getstartoffset">
+      <pre>getStartOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendkey">
+      <pre>getEndKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getendoffset">
+      <pre>getEndOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchorkey">
+      <pre>getAnchorKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getanchoroffset">
+      <pre>getAnchorOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocuskey">
+      <pre>getFocusKey()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getfocusoffset">
+      <pre>getFocusOffset()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getisbackward">
+      <pre>getIsBackward()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#gethasfocus">
+      <pre>getHasFocus()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#iscollapsed">
+      <pre>isCollapsed()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasedgewithin">
+      <pre>hasEdgeWithin(blockKey, start, end)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#serialize">
+      <pre>serialize()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#anchorkey">
+      <pre>anchorKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#anchoroffset">
+      <pre>anchorOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focuskey">
+      <pre>focusKey</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#focusoffset">
+      <pre>focusOffset</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#isbackward">
+      <pre>isBackward</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hasfocus">
+      <pre>hasFocus</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createEmpty()
+
+\`\`\`
+createEmpty(blockKey: string): SelectionState
+\`\`\`
+Create a \`SelectionState\` object at the zero offset of the provided block key
+and \`hasFocus\` set to false.
+
+## Methods
+
+### getStartKey()
+
+\`\`\`
+getStartKey(): string
+\`\`\`
+Returns the key of the block containing the start position of the selection range.
+
+### getStartOffset()
+
+\`\`\`
+getStartOffset(): number
+\`\`\`
+Returns the block-level character offset of the start position of the selection range.
+
+### getEndKey()
+
+\`\`\`
+getEndKey(): string
+\`\`\`
+Returns the key of the block containing the end position of the selection range.
+
+### getEndOffset()
+
+\`\`\`
+getEndOffset(): number
+\`\`\`
+Returns the block-level character offset of the end position of the selection range.
+
+### getAnchorKey()
+
+\`\`\`
+getAnchorKey(): string
+\`\`\`
+Returns the key of the block containing the anchor position of the selection range.
+
+### getAnchorOffset()
+
+\`\`\`
+getAnchorOffset(): number
+\`\`\`
+Returns the block-level character offset of the anchor position of the selection range.
+
+### getFocusKey()
+
+\`\`\`
+getFocusKey(): string
+\`\`\`
+Returns the key of the block containing the focus position of the selection range.
+
+### getFocusOffset()
+
+\`\`\`
+getFocusOffset(): number
+\`\`\`
+Returns the block-level character offset of the focus position of the selection range.
+
+### getIsBackward()
+
+\`\`\`
+getIsBackward(): boolean
+\`\`\`
+Returns whether the focus position is before the anchor position in the document.
+
+This must be derived from the key order of the active \`ContentState\`, or if the selection
+range is entirely within one block, a comparison of the anchor and focus offset values.
+
+### getHasFocus()
+
+\`\`\`
+getHasFocus(): boolean
+\`\`\`
+Returns whether the editor has focus.
+
+### isCollapsed()
+
+\`\`\`
+isCollapsed(): boolean
+\`\`\`
+Returns whether the selection range is collapsed, i.e. a caret. This is true
+when the anchor and focus keys are the same /and/ the anchor and focus offsets
+are the same.
+
+### hasEdgeWithin()
+
+\`\`\`
+hasEdgeWithin(blockKey: string, start: number, end: number): boolean
+\`\`\`
+Returns whether the selection range has an edge that overlaps with the specified
+start/end range within a given block.
+
+This is useful when setting DOM selection within a block after contents are
+rendered.
+
+### serialize()
+
+\`\`\`
+serialize(): string
+\`\`\`
+Returns a serialized version of the \`SelectionState\`. Useful for debugging.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+\`\`\`
+var selectionState = SelectionState.createEmpty('foo');
+var updatedSelection = selectionState.merge({
+  focusKey: 'bar',
+  focusOffset: 0,
+});
+var anchorKey = updatedSelection.getAnchorKey(); // 'foo'
+var focusKey = updatedSelection.getFocusKey(); // 'bar'
+\`\`\`
+
+### anchorKey
+The block containing the anchor end of the selection range.
+
+### anchorOffset
+The offset position of the anchor end of the selection range.
+
+### focusKey
+The block containing the focus end of the selection range.
+
+### focusOffset
+The offset position of the focus end of the selection range.
+
+### isBackward
+If the anchor position is lower in the document than the focus position, the
+selection is backward. Note: The \`SelectionState\` is an object with
+no knowledge of the \`ContentState\` structure. Therefore, when updating
+\`SelectionState\` values, you are responsible for updating \`isBackward\` as well.
+
+### hasFocus
+Whether the editor currently has focus.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-selection-state","title":"SelectionState","layout":"docs","category":"API Reference","next":"api-reference-data-conversion","permalink":"docs/api/selection-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/api/transaction-functions.js b/website/src/draft-js/docs/api/transaction-functions.js
new file mode 100644
index 0000000..ef311e3
--- /dev/null
+++ b/website/src/draft-js/docs/api/transaction-functions.js
@@ -0,0 +1,269 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+\`ContentBlock\` is an Immutable
+[Record](http://facebook.github.io/immutable-js/docs/#/Record/Record) that
+represents the full state of:
+
+- The entire **contents** of an editor: text, block and inline styles, and entity ranges.
+- Two **selection states** of an editor: before and after the rendering of these contents.
+
+The most common use for the \`ContentState\` object is via \`EditorState.getCurrentContent()\`,
+which provides the \`ContentState\` currently being rendered in the editor.
+
+An \`EditorState\` object maintains undo and redo stacks comprised of \`ContentState\`
+objects.
+
+## Overview
+
+*Static Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#createfromtext">
+      <pre>createFromText()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#createfromblockarray">
+      <pre>createFromBlockArray()</pre>
+    </a>
+  </li>
+</ul>
+
+*Methods*
+
+<ul class="apiIndex">
+  <li>
+    <a href="#getblockmap">
+      <pre>getBlockMap()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionbefore">
+      <pre>getSelectionBefore()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getselectionafter">
+      <pre>getSelectionAfter()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockforkey">
+      <pre>getBlockForKey(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeybefore">
+      <pre>getKeyBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getkeyafter">
+      <pre>getKeyAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockbefore">
+      <pre>getBlockBefore(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblockafter">
+      <pre>getBlockAfter(key)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getblocksasarray">
+      <pre>getBlocksAsArray()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getlastblock">
+      <pre>getLastBlock()</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#getplaintext">
+      <pre>getPlainText(delimiter)</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#hastext">
+      <pre>hasText()</pre>
+    </a>
+  </li>
+</ul>
+
+*Properties*
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+<ul class="apiIndex">
+  <li>
+    <a href="#blockmap">
+      <pre>blockMap</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionbefore">
+      <pre>selectionBefore</pre>
+    </a>
+  </li>
+  <li>
+    <a href="#selectionafter">
+      <pre>selectionAfter</pre>
+    </a>
+  </li>
+</ul>
+
+## Static Methods
+
+### createFromText
+
+\`\`\`
+createFromText(text: string, delimiter?: string): ContentState
+\`\`\`
+Generates a \`ContentState\` from a string, with a delimiter to split the string
+into \`ContentBlock\` objects. If no delimiter is provided, '\\n' is used.
+
+### createFromBlockArray
+
+\`\`\`
+createFromBlockArray(blocks: Array<ContentBlock>): ContentState
+\`\`\`
+Generates a \`ContentState\` from an array of \`ContentBlock\` objects. The default
+\`selectionBefore\` and \`selectionAfter\` states have the cursor at the start of
+the content.
+
+## Methods
+
+### getBlockMap
+
+\`\`\`
+getBlockMap(): BlockMap
+\`\`\`
+Returns the full ordered map of \`ContentBlock\` objects representing the state
+of an entire document.
+
+In most cases, you should be able to use the convenience methods below to target
+specific \`ContentBlock\` objects or obtain information about the state of the content.
+
+### getSelectionBefore
+
+\`\`\`
+getSelectionBefore(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor before rendering \`blockMap\`.
+
+When performing an \`undo\` action in the editor, the \`selectionBefore\` of the current
+\`ContentState\` is used to place the selection range in the appropriate position.
+
+### getSelectionAfter
+
+\`\`\`
+getSelectionAfter(): SelectionState
+\`\`\`
+Returns the \`SelectionState\` displayed in the editor after rendering \`blockMap\`.
+
+When performing any action in the editor that leads to this \`blockMap\` being rendered,
+the selection range will be placed in the \`selectionAfter\` position.
+
+### getBlockForKey
+
+\`\`\`
+getBlockForKey(key: string): ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` corresponding to the given block key.
+
+#### Example
+
+\`\`\`
+var {editorState} = this.state;
+var blockKey = editorState.getSelection().getStartKey();
+var selectedBlockType = editorState
+  .getCurrentContent()
+  .getBlockForKey(startKey)
+  .getType();
+\`\`\`
+
+### getKeyBefore()
+
+\`\`\`
+getKeyBefore(key: string): ?string
+\`\`\`
+Returns the key before the specified key in \`blockMap\`, or null if this is the first key.
+
+### getKeyAfter()
+
+\`\`\`
+getKeyAfter(key: string): ?string
+\`\`\`
+Returns the key after the specified key in \`blockMap\`, or null if this is the last key.
+
+### getBlockBefore()
+
+\`\`\`
+getBlockBefore(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` before the specified key in \`blockMap\`, or null if this is
+the first key.
+
+### getBlockAfter()
+
+\`\`\`
+getBlockAfter(key: string): ?ContentBlock
+\`\`\`
+Returns the \`ContentBlock\` after the specified key in \`blockMap\`, or null if this is
+the last key.
+
+### getBlocksAsArray()
+
+\`\`\`
+getBlocksAsArray(): Array<ContentBlock>
+\`\`\`
+Returns the values of \`blockMap\` as an array.
+
+### getPlainText()
+
+\`\`\`
+getPlainText(delimiter?: string): string
+\`\`\`
+Returns the full plaintext value of the contents, joined with a delimiter. If no
+delimiter is specified, the line feed character (\`\\u000A\`) is used.
+
+### hasText()
+
+\`\`\`
+hasText(): boolean
+\`\`\`
+Returns whether the contents contain any text at all.
+
+## Properties
+
+> Use [Immutable Map API](http://facebook.github.io/immutable-js/docs/#/Record/Record) to
+> set properties.
+
+### blockMap
+See \`getBlockMap()\`.
+
+### selectionBefore
+See \`getSelectionBefore()\`.
+
+### selectionAfter
+See \`getSelectionAfter()\`.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"api-reference-transaction-functions","title":"Transaction Functions","layout":"docs","category":"API Reference","next":"api-reference-drafteditor","permalink":"docs/api/transaction-functions.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/guides/an-immutable-model.js b/website/src/draft-js/docs/guides/an-immutable-model.js
new file mode 100644
index 0000000..b1ef73d
--- /dev/null
+++ b/website/src/draft-js/docs/guides/an-immutable-model.js
@@ -0,0 +1,19 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+
+
+## Immutable
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"guides-an-immutable-model","title":"An Immutable Model","layout":"docs","category":"Guides","next":"advanced-topics-entities","permalink":"docs/guides/an-immutable-model.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/guides/controlled-contenteditable.js b/website/src/draft-js/docs/guides/controlled-contenteditable.js
new file mode 100644
index 0000000..a3ef49f
--- /dev/null
+++ b/website/src/draft-js/docs/guides/controlled-contenteditable.js
@@ -0,0 +1,18 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Draft is a JavaScript framework that enables the creation of rich text editing
+experiences.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"guides-why-draft","title":"Why Draft?","layout":"docs","category":"Guides","next":"guides-controlling-contenteditable","permalink":"docs/guides/controlled-contenteditable.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/guides/controlling-contenteditable.js b/website/src/draft-js/docs/guides/controlling-contenteditable.js
new file mode 100644
index 0000000..2b2047b
--- /dev/null
+++ b/website/src/draft-js/docs/guides/controlling-contenteditable.js
@@ -0,0 +1,19 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+
+
+## ContentEditable: The Good Parts
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"guides-controlling-contenteditable","title":"Controlling ContentEditable","layout":"docs","category":"Guides","next":"guides-an-immutable-model","permalink":"docs/guides/controlling-contenteditable.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/guides/why-draft.js b/website/src/draft-js/docs/guides/why-draft.js
new file mode 100644
index 0000000..38e0212
--- /dev/null
+++ b/website/src/draft-js/docs/guides/why-draft.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## Hey
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"guides-why-draft","title":"Why Draft?","layout":"docs","category":"Guides","next":"guides-controlling-contenteditable","permalink":"docs/guides/why-draft.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/model/overview.js b/website/src/draft-js/docs/model/overview.js
new file mode 100644
index 0000000..6d75497
--- /dev/null
+++ b/website/src/draft-js/docs/model/overview.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Test
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"model-overview","title":"Overview","layout":"docs","category":"Model","next":"model-selection-state","permalink":"docs/model/overview.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/model/selection-state.js b/website/src/draft-js/docs/model/selection-state.js
new file mode 100644
index 0000000..c9fa3dc
--- /dev/null
+++ b/website/src/draft-js/docs/model/selection-state.js
@@ -0,0 +1,21 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Draft is the application architecture that Facebook uses for building
+client-side web applications.  It complements React's composable view
+components by utilizing a unidirectional data flow.  It's more of a pattern
+rather than a formal framework, and you can start using Rewrite immediately
+without a lot of new code.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"model-selection-state","title":"SelectionState","layout":"docs","category":"Model","next":"api-reference-editor-state","permalink":"docs/model/selection-state.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/overview.js b/website/src/draft-js/docs/overview.js
new file mode 100644
index 0000000..0fbe93f
--- /dev/null
+++ b/website/src/draft-js/docs/overview.js
@@ -0,0 +1,52 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Draft.js is a framework for building rich text editors in React, powered by an immutable model and abstracting over cross-browser differences.
+
+Draft.js makes it easy to build any type of rich text input, whether you're just looking to support a few inline text styles or building a complex text editor for composing long-form articles.
+
+### Installation
+
+Currently Draft.js is distributed via npm. It depends on React and React DOM which must also be installed.
+
+\`\`\`sh
+npm install --save draft-js react react-dom
+\`\`\`
+
+### Usage
+
+\`\`\`js
+import React from 'react';
+import ReactDOM from 'react-dom';
+import {Editor} from 'draft-js';
+
+class MyEditor extends React.Component {
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <Editor editorState={editorState} onChange={this.onChange} />;
+  }
+}
+
+ReactDOM.render(
+  <MyEditor />,
+  document.getElementById('container')
+);
+\`\`\`
+
+Next, let's go into the basics of the API and learn what else you can do with Draft.js.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"getting-started","title":"Overview","layout":"docs","category":"Quick Start","next":"quickstart-api-basics","permalink":"docs/overview.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart-api-basics.js b/website/src/draft-js/docs/quickstart-api-basics.js
new file mode 100644
index 0000000..2e88155
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart-api-basics.js
@@ -0,0 +1,82 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This document provides an overview of the basics of the \`Draft\` API. A
+[working example](https://github.com/facebook/draft-js/tree/master/examples/plaintext)
+is also available to follow along.
+
+## Controlled Inputs
+
+The \`Editor\` React component is built as a controlled ContentEditable component,
+with the goal of providing a top-level API modeled on the familiar React
+*controlled input* API.
+
+As a brief refresher, controlled inputs involve two key pieces:
+
+1. A _value_ to represent the state of the input
+2. An _onChange_ prop function to receive updates to the input
+
+This approach allows the component that composes the input to have strict
+control over the state of the input, while still allowing updates to the DOM
+to provide information about the text that the user has written.
+
+\`\`\`
+const MyInput = React.createClass({
+  onChange(evt) {
+    this.setState({value: evt.target.value});
+  },
+  render() {
+    return <input value={this.state.value} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+The top-level component can maintain control over the input state via this
+\`value\` state property.
+
+## Controlling Rich Text
+
+In a React rich text scenario, however, there are two clear problems:
+
+1. A string of plaintext is insufficient to represent the complex state of
+a rich editor.
+2. There is no such \`onChange\` event available for a ContentEditable element.
+
+State is therefore represented as a single immutable
+[EditorState](/draft-js/docs/api-reference-editor-state.html) object, and
+\`onChange\` is implemented within the \`Editor\` core to provide this state
+value to the top level.
+
+The \`EditorState\` object is a complete snapshot of the state of the editor,
+including contents, cursor, and undo/redo history. All changes to content and
+selection within the editor will create new \`EditorState\` objects. Note that
+this remains efficient due to data persistence across immutable objects.
+
+\`\`\`
+import {Editor} from 'draft-js';
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <Editor editorState={editorState} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+For any edits or selection changes that occur in the editor DOM, your \`onChange\`
+handler will execute with the latest \`EditorState\` object based on those changes.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quickstart-api-basics","title":"API Basics","layout":"docs","category":"Quick Start","next":"quickstart-rich-styling","permalink":"docs/quickstart-api-basics.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart-rich-styling.js b/website/src/draft-js/docs/quickstart-rich-styling.js
new file mode 100644
index 0000000..7b88a27
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart-rich-styling.js
@@ -0,0 +1,119 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Now that we have established the basics of the top-level API, we can go a step
+further and examine how basic rich styling can be added to a \`Draft\` editor.
+
+A [rich text example](https://github.com/facebook/draft-js/tree/master/examples/rich)
+is also available to follow along.
+
+## EditorState: Yours to Command
+
+The previous article introduced the \`EditorState\` object as a snapshot of the
+full state of the editor, as provided by the \`Editor\` core via the
+\`onChange\` prop.
+
+However, since your top-level React component is responsible for maintaining the
+state, you also have the freedom to apply changes to that \`EditorState\` object
+in any way you see fit.
+
+For inline and block style behavior, for example, the \`RichUtils\` module
+provides a number of useful functions to help manipulate state.
+
+Similarly, the [Modifier](/draft-js/docs/api-reference-modifier.html) module also provides a
+number of common operations that allow you to apply edits, including changes
+to text, styles, and more. This module is a suite of edit functions that
+compose simpler, smaller edit functions to return the desired \`EditorState\`
+object.
+
+For this example, we'll stick with \`RichUtils\` to demonstrate how to apply basic
+rich styling within the top-level component.
+
+## RichUtils and Key Commands
+
+\`RichUtils\` has information about the core key commands available to web editors,
+such as Cmd+B (bold), Cmd+I (italic), and so on.
+
+We can observe and handle key commands via the \`handleKeyCommand\` prop, and
+hook these into \`RichUtils\` to apply or remove the desired style.
+
+\`\`\`
+import {Editor, RichUtils} from 'draft-js';
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  handleKeyCommand(command) {
+    const {editorState} = this.state;
+    const newState = RichUtils.handleKeyCommand(editorState, command);
+    if (newState) {
+      this.onChange(newState);
+      return true;
+    }
+    return false;
+  },
+  render() {
+    const {editorState} = this.state;
+    return (
+      <Editor
+        editorState={editorState}
+        handleKeyCommand={this.handleKeyCommand}
+        onChange={this.onChange}
+      />
+    );
+  }
+});
+\`\`\`
+
+> handleKeyCommand
+>
+> The \`command\` argument supplied to \`handleKeyCommand\` is a string value, the
+> name of the command to be executed. This is mapped from a DOM key event. See
+> [Advanced Topics - Key Binding](/draft-js/docs/advanced-topics-key-bindings.html) for more
+> on this, as well as details on why the function returns a boolean.
+
+## Styling Controls in UI
+
+Within your React component, you can add buttons or other controls to allow
+the user to modify styles within the editor. In the example above, we are using
+known key commands, but we can add more complex UI to provide these rich
+features.
+
+Here's a super-basic example with a "Bold" button to toggle the \`BOLD\` style.
+
+\`\`\`
+const MyEditor = React.createClass({
+  ...
+
+  _onBoldClick() {
+    this.onChange(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
+  }
+
+  render() {
+    const {editorState} = this.state;
+    return (
+      <div>
+        <button onClick={this._onBoldClick}>Bold</button>
+        <Editor
+          editorState={editorState}
+          handleKeyCommand={this.handleKeyCommand}
+          onChange={this.onChange}
+        />
+      </div>
+    );
+  }
+});
+\`\`\`
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quickstart-rich-styling","title":"Rich Styling","layout":"docs","category":"Quick Start","next":"advanced-topics-entities","permalink":"docs/quickstart-rich-styling.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart/api-basics.js b/website/src/draft-js/docs/quickstart/api-basics.js
new file mode 100644
index 0000000..2fd60f9
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart/api-basics.js
@@ -0,0 +1,81 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This document provides an overview of the basics of the \`Draft\` API. A
+[working example](https://github.com/facebook/draft-js/tree/master/examples/plaintext)
+is also available to follow along.
+
+## Controlled Inputs
+
+The \`DraftEditor\` React component is built as a controlled ContentEditable component,
+with the goal of providing a top-level API modeled on the familiar React
+*controlled input* API.
+
+As a brief refresher, controlled inputs involve two key pieces:
+
+1. A _value_ to represent the state of the input
+2. An _onChange_ prop function to receive updates to the input
+
+This approach allows the component that composes the input to have strict
+control over the state of the input, while still allowing updates to the DOM
+to provide information about the text that the user has written.
+
+\`\`\`
+const MyInput = React.createClass({
+  onChange(evt) {
+    this.setState({value: evt.target.value});
+  },
+  render() {
+    return <input value={this.state.value} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+The top-level component can maintain control over the input state via this
+\`value\` state property.
+
+## Controlling Rich Text
+
+In a React rich text scenario, however, there are two clear problems:
+
+1. A string of plaintext is insufficient to represent the complex state of
+a rich editor.
+2. There is no such \`onChange\` event available for a ContentEditable element.
+
+State is therefore represented as a single immutable
+[EditorState](/draft-js/docs/api/editor-state.html) object, and \`onChange\` is
+implemented within the \`DraftEditor\` core to provide this state value to the
+top level.
+
+The \`EditorState\` object is a complete snapshot of the state of the editor,
+including contents, cursor, and undo/redo history. All changes to content and
+selection within the editor will create new \`EditorState\` objects. Note that
+this remains efficient due to data persistence across immutable objects.
+
+\`\`\`
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <DraftEditor editorState={editorState} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+For any edits or selection changes that occur in the editor DOM, your \`onChange\`
+handler will execute with the latest \`EditorState\` object based on those changes.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quick-start-api-basics","title":"API Basics","layout":"docs","category":"Quick Start","next":"quick-start-rich-styling","permalink":"docs/quickstart/api-basics.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart/customizing-your-editor.js b/website/src/draft-js/docs/quickstart/customizing-your-editor.js
new file mode 100644
index 0000000..5be2676
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart/customizing-your-editor.js
@@ -0,0 +1,17 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+## hi
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quick-start-customizing-your-editor","title":"Customizing Your Editor","layout":"docs","category":"Quick Start","next":"guides-why-draft","permalink":"docs/quickstart/customizing-your-editor.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart/decorated-text.js b/website/src/draft-js/docs/quickstart/decorated-text.js
new file mode 100644
index 0000000..9e53625
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart/decorated-text.js
@@ -0,0 +1,119 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Inline and block styles aren't the only kind of rich styling that we might
+want to add to our editor. The Facebook comment input, for example, provides
+blue background highlights for mentions and hashtags.
+
+To support flexibility for custom rich text, Draft provides a "decorator"
+system. The [tweet example](https://github.com/facebook/draft-js/tree/master/examples/tweet)
+offers a live example of decorators in action.
+
+## CompositeDecorator
+
+The decorator concept is based on scanning the contents of a given
+[ContentBlock](/draft-js/docs/api/content-block.html)
+for ranges of text that match a defined strategy, then rendering them
+with a specified React component.
+
+You can use the \`CompositeDecorator\` class to define your desired
+decorator behavior. This class allows you to supply multiple \`DraftDecorator\`
+objects, and will search through a block of text with each strategy in turn.
+
+Decorators are stored within the \`EditorState\` record. When creating a new
+\`EditorState\` object, e.g. via \`EditorState.createEmpty()\`, a decorator may
+optionally be provided.
+
+> Under the hood
+>
+> When contents change in a Draft editor, the resulting \`EditorState\` object
+> will evaluate the new \`ContentState\` with its decorator, and identify ranges
+> to be decorated. A complete tree of blocks, decorators, and inline styles is
+> formed at this time, and serves as the basis for our rendered output.
+>
+> In this way, we always ensure that as contents change, rendered decorations
+> are in sync with our \`EditorState\`.
+
+In the "Tweet" editor example, for instance, we use a \`CompositeDecorator\` that
+searches for @-handle strings as well as hashtag strings:
+
+\`\`\`
+const compositeDecorator = new CompositeDecorator([
+  {
+    strategy: handleStrategy,
+    component: HandleSpan,
+  },
+  {
+    strategy: hashtagStrategy,
+    component: HashtagSpan,
+  },
+]);
+\`\`\`
+
+This composite decorator will first scan a given block of text for @-handle
+matches, then for hashtag matches.
+
+\`\`\`
+// Note: these aren't very good regexes, don't use them!
+const HANDLE_REGEX = /\\@[\\w]+/g;
+const HASHTAG_REGEX = /\\#[\\w\\u0590-\\u05ff]+/g;
+
+function handleStrategy(contentBlock, callback) {
+  findWithRegex(HANDLE_REGEX, contentBlock, callback);
+}
+
+function hashtagStrategy(contentBlock, callback) {
+  findWithRegex(HASHTAG_REGEX, contentBlock, callback);
+}
+
+function findWithRegex(regex, contentBlock, callback) {
+  const text = contentBlock.getText();
+  let matchArr, start;
+  while ((matchArr = regex.exec(text)) !== null) {
+    start = matchArr.index;
+    callback(start, start + matchArr[0].length);
+  }
+}
+\`\`\`
+
+The strategy functions execute the provided callback with the \`start\` and
+\`end\` values of the matching range of text.
+
+## Decorator Components
+
+For your decorated ranges of text, you must define a React component to use
+to render them. These tend to be simple \`span\` elements with CSS classes or
+styles applied to them.
+
+In our current example, the \`CompositeDecorator\` object names \`HandleSpan\` and
+\`HashtagSpan\` as the components to use for decoration. These are just basic
+stateless components:
+
+\`\`\`
+const HandleSpan = (props) => {
+  return <span {...props} style={styles.handle}>{props.children}</span>;
+};
+
+const HashtagSpan = (props) => {
+  return <span {...props} style={styles.hashtag}>{props.children}</span>;
+};
+\`\`\`
+
+Note that \`props.children\` is passed through to the rendered output. This is
+done to ensure that the text is rendered within the decorated \`span\`.
+
+You can use the same approach for links, as demonstrated in our
+[link example](https://github.com/facebook/draft-js/tree/master/examples/link).
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quick-start-decorated-text","title":"Decorated Text","layout":"docs","category":"Quick Start","next":"quick-start-customizing-your-editor","permalink":"docs/quickstart/decorated-text.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart/rich-styling.js b/website/src/draft-js/docs/quickstart/rich-styling.js
new file mode 100644
index 0000000..3909005
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart/rich-styling.js
@@ -0,0 +1,118 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+Now that we have established the basics of the top-level API, we can go a step
+further and examine how basic rich styling can be added to a \`Draft\` editor.
+
+A [rich text example](https://github.com/facebook/draft-js/tree/master/examples/rich)
+is also available to follow along.
+
+## EditorState: Yours to Command
+
+The previous article introduced the \`EditorState\` object as a snapshot of the
+full state of the editor, as provided by the \`DraftEditor\` core via the
+\`onChange\` prop.
+
+However, since your top-level React component is responsible for maintaining the
+state, you also have the freedom to apply changes to that \`EditorState\` object
+in any way you see fit.
+
+For inline and block style behavior, for example, the \`RichUtils\` module
+provides a number of useful functions to help manipulate state.
+
+Similarly, the [Modifier](/draft-js/docs/api/modifier.html) module also provides a
+number of common operations that allow you to apply edits, including changes
+to text, styles, and more. This module is a suite of edit functions that
+compose simpler, smaller edit functions to return the desired \`EditorState\`
+object.
+
+For this example, we'll stick with \`RichUtils\` to demonstrate how to apply basic
+rich styling within the top-level component.
+
+## RichUtils and Key Commands
+
+\`RichUtils\` has information about the core key commands available to web editors,
+such as Cmd+B (bold), Cmd+I (italic), and so on.
+
+We can observe and handle key commands via the \`handleKeyCommand\` prop, and
+hook these into \`RichUtils\` to apply or remove the desired style.
+
+\`\`\`
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  handleKeyCommand(command) {
+    const {editorState} = this.state;
+    const newState = RichUtils.handleKeyCommand(editorState, command);
+    if (newState) {
+      this.onChange(newState);
+      return true;
+    }
+    return false;
+  },
+  render() {
+    const {editorState} = this.state;
+    return (
+      <DraftEditor
+        editorState={editorState}
+        handleKeyCommand={this.handleKeyCommand}
+        onChange={this.onChange}
+      />
+    );
+  }
+});
+\`\`\`
+
+> handleKeyCommand
+>
+> The \`command\` argument supplied to \`handleKeyCommand\` is a string value, the
+> name of the command to be executed. This is mapped from a DOM key event. See
+> [Advanced Topics - Key Binding](docs/advanced/key-bindings.html) for more
+> on this, as well as details on why the function returns a boolean.
+
+## Styling Controls in UI
+
+Within your React component, you can add buttons or other controls to allow
+the user to modify styles within the editor. In the example above, we are using
+known key commands, but we can add more complex UI to provide these rich
+features.
+
+Here's a super-basic example with a "Bold" button to toggle the \`BOLD\` style.
+
+\`\`\`
+const MyEditor = React.createClass({
+  ...
+
+  _onBoldClick() {
+    this.onChange(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
+  }
+
+  render() {
+    const {editorState} = this.state;
+    return (
+      <div>
+        <button onClick={this._onBoldClick}>Bold</button>
+        <DraftEditor
+          editorState={editorState}
+          handleKeyCommand={this.handleKeyCommand}
+          onChange={this.onChange}
+        />
+      </div>
+    );
+  }
+});
+\`\`\`
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quick-start-rich-styling","title":"Rich Styling","layout":"docs","category":"Quick Start","next":"quick-start-decorated-text","permalink":"docs/quickstart/rich-styling.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/docs/quickstart/the-basics.js b/website/src/draft-js/docs/quickstart/the-basics.js
new file mode 100644
index 0000000..1d8f9e5
--- /dev/null
+++ b/website/src/draft-js/docs/quickstart/the-basics.js
@@ -0,0 +1,77 @@
+/**
+ * @generated
+ */
+var React = require("React");
+var Layout = require("DocsLayout");
+var content = `
+This document provides an overview of the basics of the \`Draft\` API. A
+[working example](http://github.com/facebook/draft-js/examples/plaintext)
+is also available to follow along.
+
+## Controlled Inputs
+
+The \`DraftEditor\` React component is built as a controlled ContentEditable component,
+with the goal of providing a top-level API modeled on the familiar React
+*controlled input* API.
+
+As a brief refresher, controlled inputs involve two key pieces:
+
+1. A _value_ to represent the state of the input
+2. An _onChange_ prop function to receive updates to the input
+
+This approach allows the component that composes the input to have strict
+control over the state of the input, while still allowing updates to the DOM
+to provide information about the text that the user has written.
+
+\`\`\`
+const MyInput = React.createClass({
+  onChange(evt) {
+    this.setState({value: evt.target.value});
+  },
+  render() {
+    return <input value={this.state.value} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+The top-level component can maintain control over the input state via this
+\`value\` state property.
+
+## Controlling Rich Text
+
+In a React rich text scenario, however, there are two clear problems:
+
+1. A string of plaintext is insufficient to represent the complex state of
+a rich editor.
+2. There is no such \`onChange\` event available for a ContentEditable element.
+
+State is therefore represented as a single immutable
+[EditorState](/draft-js/docs/api/editor-state.html) object, and \`onChange\` is
+implemented within the \`DraftEditor\` core to provide this state value to the
+top level. The \`EditorState\` object is a complete snapshot of the state of the
+editor, including contents, cursor, and undo/redo history.
+
+\`\`\`
+const MyEditor = React.createClass({
+  onChange(editorState) {
+    this.setState({editorState});
+  },
+  render() {
+    const {editorState} = this.state;
+    return <DraftEditor editorState={editorState} onChange={this.onChange} />;
+  }
+});
+\`\`\`
+
+For any edits or selection changes that occur in the editor DOM, your \`onChange\`
+handler will execute with the latest \`EditorState\` object based on those changes.
+`
+var Post = React.createClass({
+  statics: {
+    content: content
+  },
+  render: function() {
+    return <Layout metadata={{"id":"quick-start-the-basics","title":"The Basics","layout":"docs","category":"Quick Start","next":"quick-start-rich-styling","permalink":"docs/quickstart/the-basics.html"}}>{content}</Layout>;
+  }
+});
+module.exports = Post;
diff --git a/website/src/draft-js/img/flux-simple-f8-diagram-1300w.png b/website/src/draft-js/img/flux-simple-f8-diagram-1300w.png
new file mode 100644
index 0000000..5971f86
Binary files /dev/null and b/website/src/draft-js/img/flux-simple-f8-diagram-1300w.png differ
diff --git a/website/src/draft-js/img/flux-simple-f8-diagram-explained-1300w.png b/website/src/draft-js/img/flux-simple-f8-diagram-explained-1300w.png
new file mode 100644
index 0000000..84c21e1
Binary files /dev/null and b/website/src/draft-js/img/flux-simple-f8-diagram-explained-1300w.png differ
diff --git a/website/src/draft-js/img/flux-simple-f8-diagram-with-client-action-1300w.png b/website/src/draft-js/img/flux-simple-f8-diagram-with-client-action-1300w.png
new file mode 100644
index 0000000..9286f70
Binary files /dev/null and b/website/src/draft-js/img/flux-simple-f8-diagram-with-client-action-1300w.png differ
diff --git a/website/src/draft-js/img/flux_logo.svg b/website/src/draft-js/img/flux_logo.svg
new file mode 100644
index 0000000..85bb338
--- /dev/null
+++ b/website/src/draft-js/img/flux_logo.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg 
+  version="1.1" 
+  xmlns="http://www.w3.org/2000/svg" 
+  xmlns:xlink="http://www.w3.org/1999/xlink" 
+  x="0px" 
+  y="0px"
+  width="100px" 
+  height="100px" 
+  viewBox="0 0 100 100" 
+  enable-background="new 0 0 100 100" 
+  xml:space="preserve">
+  <g>
+    <path fill="#44B74A" d="M76.92,32.34c-3.454,0-6.671,1.004-9.385,2.729L50.439,47.625l-9.011-6.617
+    c-0.543,0.881-1.09,1.757-1.613,2.664c-0.126,0.218-0.243,0.439-0.367,0.657l7.532,5.531L31.175,61.466
+    c-2.166,1.35-4.658,2.064-7.217,2.064c-7.537,0-13.67-6.132-13.67-13.67c0-7.538,6.132-13.67,13.67-13.67
+    c2.559,0,5.052,0.713,7.217,2.064l1.571,1.153c0.649-1.122,1.322-2.218,2.001-3.306l-1.404-1.031
+    c-2.713-1.726-5.931-2.729-9.385-2.729c-9.675,0-17.519,7.844-17.519,17.519s7.843,17.519,17.519,17.519
+    c3.454,0,6.672-1.004,9.385-2.729l17.096-12.555l9.046,6.643c0.554-0.908,1.107-1.819,1.654-2.768
+    c0.108-0.188,0.206-0.375,0.313-0.563l-7.553-5.547l15.804-11.606c2.166-1.35,4.659-2.064,7.217-2.064
+    c7.538,0,13.67,6.132,13.67,13.67c0,7.538-6.132,13.67-13.67,13.67c-2.559,0-5.051-0.713-7.217-2.064l-1.548-1.137
+    c-0.656,1.133-1.32,2.234-1.987,3.316l1.368,1.004c2.714,1.726,5.931,2.729,9.385,2.729c9.675,0,17.519-7.844,17.519-17.519
+    C94.439,40.184,86.596,32.34,76.92,32.34z"/>
+  </g>
+</svg>
diff --git a/website/src/draft-js/index.js b/website/src/draft-js/index.js
new file mode 100644
index 0000000..09e2ef4
--- /dev/null
+++ b/website/src/draft-js/index.js
@@ -0,0 +1,264 @@
+/**
+ * This file provided by Facebook is for non-commercial testing and evaluation
+ * purposes only. Facebook reserves all rights not expressly granted.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+var React = require('React');
+var Site = require('Site');
+var Prism = require('Prism');
+var Marked = require('Marked');
+var unindent = require('unindent');
+
+var richExample = `
+'use strict';
+
+const {Editor, EditorState, RichUtils} = Draft;
+
+class RichEditorExample extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {editorState: EditorState.createEmpty()};
+
+    this.focus = () => this.refs.editor.focus();
+    this.onChange = (editorState) => this.setState({editorState});
+
+    this.handleKeyCommand = (command) => this._handleKeyCommand(command);
+    this.toggleBlockType = (type) => this._toggleBlockType(type);
+    this.toggleInlineStyle = (style) => this._toggleInlineStyle(style);
+  }
+
+  _handleKeyCommand(command) {
+    const {editorState} = this.state;
+    const newState = RichUtils.handleKeyCommand(editorState, command);
+    if (newState) {
+      this.onChange(newState);
+      return true;
+    }
+    return false;
+  }
+
+  _toggleBlockType(blockType) {
+    this.onChange(
+      RichUtils.toggleBlockType(
+        this.state.editorState,
+        blockType
+      )
+    );
+  }
+
+  _toggleInlineStyle(inlineStyle) {
+    this.onChange(
+      RichUtils.toggleInlineStyle(
+        this.state.editorState,
+        inlineStyle
+      )
+    );
+  }
+
+  render() {
+    const {editorState} = this.state;
+
+    // If the user changes block type before entering any text, we can
+    // either style the placeholder or hide it. Let's just hide it now.
+    let className = 'RichEditor-editor';
+    var contentState = editorState.getCurrentContent();
+    if (!contentState.hasText()) {
+      if (contentState.getBlockMap().first().getType() !== 'unstyled') {
+        className += ' RichEditor-hidePlaceholder';
+      }
+    }
+
+    return (
+      <div className="RichEditor-root">
+        <BlockStyleControls
+          editorState={editorState}
+          onToggle={this.toggleBlockType}
+        />
+        <InlineStyleControls
+          editorState={editorState}
+          onToggle={this.toggleInlineStyle}
+        />
+        <div className={className} onClick={this.focus}>
+          <Editor
+            blockStyleFn={getBlockStyle}
+            customStyleMap={styleMap}
+            editorState={editorState}
+            handleKeyCommand={this.handleKeyCommand}
+            onChange={this.onChange}
+            placeholder="Tell a story..."
+            ref="editor"
+            spellCheck={true}
+          />
+        </div>
+      </div>
+    );
+  }
+}
+
+// Custom overrides for "code" style.
+const styleMap = {
+  CODE: {
+    backgroundColor: 'rgba(0, 0, 0, 0.05)',
+    fontFamily: '"Inconsolata", "Menlo", "Consolas", monospace',
+    fontSize: 16,
+    padding: 2,
+  },
+};
+
+function getBlockStyle(block) {
+  switch (block.getType()) {
+    case 'blockquote': return 'RichEditor-blockquote';
+    default: return null;
+  }
+}
+
+class StyleButton extends React.Component {
+  constructor() {
+    super();
+    this.onToggle = (e) => {
+      e.preventDefault();
+      this.props.onToggle(this.props.style);
+    };
+  }
+
+  render() {
+    let className = 'RichEditor-styleButton';
+    if (this.props.active) {
+      className += ' RichEditor-activeButton';
+    }
+
+    return (
+      <span className={className} onMouseDown={this.onToggle}>
+        {this.props.label}
+      </span>
+    );
+  }
+}
+
+const BLOCK_TYPES = [
+  {label: 'H1', style: 'header-one'},
+  {label: 'H2', style: 'header-two'},
+  {label: 'Blockquote', style: 'blockquote'},
+  {label: 'UL', style: 'unordered-list-item'},
+  {label: 'OL', style: 'ordered-list-item'},
+  {label: 'Code Block', style: 'code-block'},
+];
+
+const BlockStyleControls = (props) => {
+  const {editorState} = props;
+  const selection = editorState.getSelection();
+  const blockType = editorState
+    .getCurrentContent()
+    .getBlockForKey(selection.getStartKey())
+    .getType();
+
+  return (
+    <div className="RichEditor-controls">
+      {BLOCK_TYPES.map((type) =>
+        <StyleButton
+          active={type.style === blockType}
+          label={type.label}
+          onToggle={props.onToggle}
+          style={type.style}
+        />
+      )}
+    </div>
+  );
+};
+
+var INLINE_STYLES = [
+  {label: 'Bold', style: 'BOLD'},
+  {label: 'Italic', style: 'ITALIC'},
+  {label: 'Underline', style: 'UNDERLINE'},
+  {label: 'Monospace', style: 'CODE'},
+];
+
+const InlineStyleControls = (props) => {
+  var currentStyle = props.editorState.getCurrentInlineStyle();
+  return (
+    <div className="RichEditor-controls">
+      {INLINE_STYLES.map(type =>
+        <StyleButton
+          active={currentStyle.has(type.style)}
+          label={type.label}
+          onToggle={props.onToggle}
+          style={type.style}
+        />
+      )}
+    </div>
+  );
+};
+
+ReactDOM.render(
+  <RichEditorExample />,
+  document.getElementById('rich-example')
+);
+`;
+
+var index = React.createClass({
+  render: function() {
+    return (
+      <Site>
+        <div className="hero">
+          <div className="wrap">
+            <div className="text"><strong>Draft.js</strong></div>
+            <div className="minitext">
+              Rich Text Editor Framework for React
+            </div>
+          </div>
+        </div>
+
+        <section className="content wrap">
+          <section className="home-section home-getting-started">
+            <p>
+              Draft.js is a framework for building rich text editors in React,
+              powered by an immutable model and abstracting over cross-browser
+              differences.
+            </p>
+
+            <p>
+              Draft.js makes it easy to build any type of rich text input,
+              whether you're just looking to support a few inline text styles
+              or building a complex text editor for composing long-form
+              articles.
+            </p>
+
+            <p>
+              In Draft.js, everything is customizable – we provide the building
+              blocks so that you have full control over the user interface.
+              Here's a simple example of a rich text editor built in Draft.js:
+            </p>
+
+            <div id="rich-example"></div>
+          </section>
+
+          <section className="home-bottom-section">
+            <div className="buttons-unit">
+              <a href="docs/overview.html#content" className="button">Learn more about Draft</a>
+            </div>
+          </section>
+        </section>
+
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.7.6/immutable.js"></script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.34/browser.js"></script>
+        <script src="lib/Draft.js"></script>
+        <style dangerouslySetInnerHTML={{__html: `
+          @import "lib/Draft.css";
+          @import "lib/RichEditor.css";
+        `}} />
+        <script type="text/babel" dangerouslySetInnerHTML={{__html: richExample}} />
+      </Site>
+    );
+  }
+});
+
+module.exports = index;
