 core/modules/edit/css/edit.css                     |   13 +-
 core/modules/edit/edit.module                      |   24 ++-
 core/modules/edit/js/createjs/editable.js          |   31 +---
 .../editingWidgets/drupalcontenteditablewidget.js  |    7 +
 .../edit/js/createjs/editingWidgets/formwidget.js  |   11 ++
 core/modules/edit/js/edit.js                       |    9 +-
 core/modules/edit/js/viejs/EditService.js          |   18 +-
 .../edit/js/views/propertyeditordecoration-view.js |   55 +++---
 core/modules/edit/js/views/toolbar-view.js         |   62 +++++--
 core/modules/edit/lib/Drupal/edit/EditBundle.php   |    8 +-
 .../edit/lib/Drupal/edit/EditController.php        |    2 +-
 .../edit/lib/Drupal/edit/EditorSelector.php        |  179 ++++++++------------
 .../edit/lib/Drupal/edit/MetadataGenerator.php     |   42 +++--
 .../lib/Drupal/edit/MetadataGeneratorInterface.php |    2 +-
 .../lib/Drupal/edit/Plugin/EditorInterFace.php     |   60 +++++++
 .../edit/lib/Drupal/edit/Plugin/EditorManager.php  |   57 +++++++
 .../Drupal/edit/Plugin/ProcessedTextEditorBase.php |   26 ---
 .../edit/Plugin/ProcessedTextEditorInterface.php   |   35 ----
 .../edit/Plugin/ProcessedTextEditorManager.php     |   46 -----
 .../lib/Drupal/edit/Plugin/edit/editor/Direct.php  |   68 ++++++++
 .../lib/Drupal/edit/Plugin/edit/editor/Form.php    |   53 ++++++
 .../edit/lib/Drupal/edit/Tests/EditTestBase.php    |    2 +-
 .../lib/Drupal/edit/Tests/EditorSelectionTest.php  |   32 ++--
 .../Drupal/edit/Tests/MetadataGeneratorTest.php    |   74 +++++++-
 .../edit_test/Plugin/edit/editor/Wysiwyg.php       |   71 ++++++++
 .../processed_text_editor/TestProcessedEditor.php  |   32 ----
 26 files changed, 626 insertions(+), 393 deletions(-)

diff --git a/core/modules/edit/css/edit.css b/core/modules/edit/css/edit.css
index 646454b..37e10eb 100644
--- a/core/modules/edit/css/edit.css
+++ b/core/modules/edit/css/edit.css
@@ -121,7 +121,7 @@
   outline: none;
 }
 .edit-field.edit-editable,
-.edit-field.edit-type-direct .edit-editable {
+.edit-field .edit-editable {
   box-shadow: 0 0 1px 1px #4d9de9;
 }
 
@@ -131,12 +131,12 @@
 }
 .edit-field.edit-editable.edit-highlighted,
 .edit-form.edit-editable.edit-highlighted,
-.edit-field.edit-type-direct .edit-editable.edit-highlighted {
+.edit-field .edit-editable.edit-highlighted {
   box-shadow: 0 0 1px 1px #0199ff, 0 0 3px 3px rgba(153, 153, 153, .5);
 }
 .edit-field.edit-editable.edit-highlighted.edit-validation-error,
 .edit-form.edit-editable.edit-highlighted.edit-validation-error,
-.edit-field.edit-type-direct .edit-editable.edit-highlighted.edit-validation-error {
+.edit-field .edit-editable.edit-highlighted.edit-validation-error {
   box-shadow: 0 0 1px 1px red, 0 0 3px 3px rgba(153, 153, 153, .5);
 }
 .edit-form.edit-editable .form-item .error {
@@ -146,7 +146,7 @@
 
 /* Editing (focused) editable. */
 .edit-form.edit-editable.edit-editing,
-.edit-field.edit-type-direct .edit-editable.edit-editing {
+.edit-field .edit-editable.edit-editing {
   /* In the latest design, there's no special styling when editing as opposed to
    * just hovering.
    * This will be necessary again for http://drupal.org/node/1844220.
@@ -290,9 +290,8 @@
 .edit-toolbar-heightfaker {
   clip: rect(-1000px, 1000px, auto, -1000px); /* Remove bottom box-shadow. */
 }
-/* Exception: when used for a directly WYSIWYG editable field that is actively
-   being edited. */
-.edit-type-direct-with-wysiwyg .edit-editing .edit-toolbar-heightfaker {
+/* Exception: when the toolbar is instructed to be "full width". */
+.edit-toolbar-fullwidth .edit-toolbar-heightfaker {
   width: 100%;
   clip: auto;
 }
diff --git a/core/modules/edit/edit.module b/core/modules/edit/edit.module
index e3beec0..98a2181 100644
--- a/core/modules/edit/edit.module
+++ b/core/modules/edit/edit.module
@@ -104,8 +104,6 @@ function edit_library_info() {
       // Create.js subclasses.
       $path . '/js/createjs/editable.js' => $options,
       $path . '/js/createjs/storage.js' => $options,
-      $path . '/js/createjs/editingWidgets/formwidget.js' => $options,
-      $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options,
       // Other.
       $path . '/js/util.js' => $options,
       $path . '/js/theme.js' => $options,
@@ -135,6 +133,26 @@ function edit_library_info() {
       array('system', 'drupalSettings'),
     ),
   );
+  $libraries['edit.editor.form'] = array(
+    'title' => '"Form" Create.js PropertyEditor widget',
+    'version' => VERSION,
+    'js' => array(
+      $path . '/js/createjs/editingWidgets/formwidget.js' => $options,
+    ),
+    'dependencies' => array(
+      array('edit', 'edit'),
+    ),
+  );
+  $libraries['edit.editor.direct'] = array(
+    'title' => '"Direct" Create.js PropertyEditor widget',
+    'version' => VERSION,
+    'js' => array(
+      $path . '/js/createjs/editingWidgets/drupalcontenteditablewidget.js' => $options,
+    ),
+    'dependencies' => array(
+      array('edit', 'edit'),
+    ),
+  );
 
   return $libraries;
 }
@@ -145,7 +163,7 @@ function edit_library_info() {
 function edit_preprocess_field(&$variables) {
   $element = $variables['element'];
   $entity = $element['#object'];
-  $variables['attributes']['data-edit-id'] = $entity->entityType() . ':' . $entity->id() . ':' . $element['#field_name'] . ':' . $element['#language'] . ':' . $element['#view_mode'];
+  $variables['attributes']['data-edit-id'] = $entity->entityType() . '/' . $entity->id() . '/' . $element['#field_name'] . '/' . $element['#language'] . '/' . $element['#view_mode'];
 }
 
 /**
diff --git a/core/modules/edit/js/createjs/editable.js b/core/modules/edit/js/createjs/editable.js
index aac1ed2..1316023 100644
--- a/core/modules/edit/js/createjs/editable.js
+++ b/core/modules/edit/js/createjs/editable.js
@@ -1,8 +1,8 @@
 /**
  * @file
- * Determines which editor to use based on a class attribute.
+ * Determines which editor (Create.js PropertyEditor widget) to use.
  */
-(function (jQuery, drupalSettings) {
+(function (jQuery, Drupal, drupalSettings) {
 
 "use strict";
 
@@ -13,31 +13,18 @@
       this.options.domService = 'edit';
       this.options.predicateSelector = '*'; //'.edit-field.edit-allowed';
 
-      this.options.editors.direct = {
-        widget: 'drupalContentEditableWidget',
-        options: {}
-      };
-      this.options.editors['direct-with-wysiwyg'] = {
-        widget: drupalSettings.edit.wysiwygEditorWidgetName,
-        options: {}
-      };
-      this.options.editors.form = {
-        widget: 'drupalFormWidget',
-        options: {}
-      };
+      // The Create.js PropertyEditor widget configuration is not hardcoded; it
+      // is generated by the server.
+      this.options.propertyEditorWidgetsConfiguration = drupalSettings.edit.editors;
 
       jQuery.Midgard.midgardEditable.prototype._create.call(this);
     },
 
     _propertyEditorName: function(data) {
-      if (jQuery(this.element).hasClass('edit-type-direct')) {
-        if (jQuery(this.element).hasClass('edit-type-direct-with-wysiwyg')) {
-          return 'direct-with-wysiwyg';
-        }
-        return 'direct';
-      }
-      return 'form';
+      // Pick a PropertyEditor widget for a property depending on its metadata.
+      var propertyID = Drupal.edit.util.calcPropertyID(data.entity, data.property);
+      return Drupal.edit.metadataCache[propertyID].editor;
     }
   });
 
-})(jQuery, drupalSettings);
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js
index c773e6e..cb59f67 100644
--- a/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js
+++ b/core/modules/edit/js/createjs/editingWidgets/drupalcontenteditablewidget.js
@@ -9,6 +9,13 @@
   jQuery.widget('Drupal.drupalContentEditableWidget', jQuery.Create.editWidget, {
 
     /**
+     * Implements getEditUiIntegration() method.
+     */
+    getEditUiIntegration: function() {
+      return { padding: true, unifiedToolbar: false, fullWidthToolbar: false };
+    },
+
+    /**
      * Implements jQuery UI widget factory's _init() method.
      *
      * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142)
diff --git a/core/modules/edit/js/createjs/editingWidgets/formwidget.js b/core/modules/edit/js/createjs/editingWidgets/formwidget.js
index f7c77cd..511fec9 100644
--- a/core/modules/edit/js/createjs/editingWidgets/formwidget.js
+++ b/core/modules/edit/js/createjs/editingWidgets/formwidget.js
@@ -12,6 +12,13 @@
     $formContainer: null,
 
     /**
+     * Implements getEditUiIntegration() method.
+     */
+    getEditUiIntegration: function() {
+      return { padding: false, unifiedToolbar: false, fullWidthToolbar: false };
+    },
+
+    /**
      * Implements jQuery UI widget factory's _init() method.
      *
      * @todo: POSTPONED_ON(Create.js, https://github.com/bergie/create/issues/142)
@@ -42,11 +49,15 @@
         case 'candidate':
           if (from !== 'inactive') {
             this.disable();
+            if (from !== 'highlighted') {
+              this.element.removeClass('edit-belowoverlay');
+            }
           }
           break;
         case 'highlighted':
           break;
         case 'activating':
+          this.element.addClass('edit-belowoverlay');
           this.enable();
           break;
         case 'active':
diff --git a/core/modules/edit/js/edit.js b/core/modules/edit/js/edit.js
index 1f6c715..cfaf76b 100644
--- a/core/modules/edit/js/edit.js
+++ b/core/modules/edit/js/edit.js
@@ -39,14 +39,7 @@ Drupal.behaviors.edit = {
           field.$el
             .attr('data-edit-field-label', meta.label)
             .attr('aria-label', meta.aria)
-            .addClass('edit-field edit-type-' + meta.editor);
-          if (meta.editor === 'direct-with-wysiwyg') {
-            field.$el
-              // This editor also uses the Backbone.syncDirect saving mechanism.
-              .addClass('edit-type-direct')
-              .attr('data-edit-text-format', meta.format)
-              .addClass((meta.formatHasTransformations) ? 'edit-text-with-transformation-filters' : 'edit-text-without-transformation-filters');
-          }
+            .addClass('edit-field edit-type-' + ((meta.editor === 'form') ? 'form' : 'direct'));
         }
 
         return true;
diff --git a/core/modules/edit/js/viejs/EditService.js b/core/modules/edit/js/viejs/EditService.js
index f52a6c0..00cb04b 100644
--- a/core/modules/edit/js/viejs/EditService.js
+++ b/core/modules/edit/js/viejs/EditService.js
@@ -62,7 +62,7 @@
 
       // Let's only have this overhead for direct types. Form-based editors are
       // handled in backbone.drupalform.js and the PropertyEditor instance.
-      if (!jQuery(element).hasClass('edit-type-direct')) {
+      if (jQuery(element).hasClass('edit-type-form')) {
         return;
       }
 
@@ -142,7 +142,7 @@
     // Returns the "URI" of an entity of an element in format
     // `<entity type>/<id>`.
     getElementSubject: function (element) {
-      return this._getID(element).split(':').slice(0, 2).join('/');
+      return this._getID(element).split('/').slice(0, 2).join('/');
     },
 
     // Returns the field name for an element in format
@@ -152,11 +152,11 @@
       if (!this._getID(element)) {
         throw new Error('Could not find predicate for element');
       }
-      return this._getID(element).split(':').slice(2, 5).join('/');
+      return this._getID(element).split('/').slice(2, 5).join('/');
     },
 
     getElementType: function (element) {
-      return this._getID(element).split(':').slice(0, 1)[0];
+      return this._getID(element).split('/').slice(0, 1)[0];
     },
 
     // Reads all editable entities (currently each Drupal field is considered an
@@ -214,15 +214,7 @@
       if (type.attributes.get(predicate)) {
         return type;
       }
-
-      var label = element.data('edit-field-label');
-      var range = 'Form';
-      if (element.hasClass('edit-type-direct')) {
-        range = 'Direct';
-      }
-      if (element.hasClass('edit-type-direct-with-wysiwyg')) {
-        range = 'Wysiwyg';
-      }
+      var range = predicate.split('/')[0];
       type.attributes.add(predicate, [range], 0, 1, {
         label: element.data('edit-field-label')
       });
diff --git a/core/modules/edit/js/views/propertyeditordecoration-view.js b/core/modules/edit/js/views/propertyeditordecoration-view.js
index 269259a..d0c52e0 100644
--- a/core/modules/edit/js/views/propertyeditordecoration-view.js
+++ b/core/modules/edit/js/views/propertyeditordecoration-view.js
@@ -12,10 +12,6 @@ Drupal.edit = Drupal.edit || {};
 Drupal.edit.views = Drupal.edit.views || {};
 Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
 
-  editor: null,
-  entity: null,
-  predicate : null,
-  editorName: null,
   toolbarId: null,
 
   _widthAttributeIsEmpty: null,
@@ -35,8 +31,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
    *   - editor: the editor object with an 'options' object that has these keys:
    *      * entity: the VIE entity for the property.
    *      * property: the predicate of the property.
-   *      * editorName: the editor name: 'form', 'direct' or
-   *        'direct-with-wysiwyg'.
    *      * widget: the parent EditableeEntity widget.
    *   - toolbarId: the ID attribute of the toolbar as rendered in the DOM.
    */
@@ -44,10 +38,6 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
     this.editor = options.editor;
     this.toolbarId = options.toolbarId;
 
-    this.entity = this.editor.options.entity;
-    this.predicate = this.editor.options.property;
-    this.editorName = this.editor.options.editorName;
-
     this.$el.css('background-color', this._getBgColor(this.$el));
   },
 
@@ -66,7 +56,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
         if (from !== 'inactive') {
           this.stopHighlight();
           if (from !== 'highlighted') {
-            this.stopEdit(this.editorName);
+            this.stopEdit();
           }
         }
         break;
@@ -74,16 +64,15 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
         this.startHighlight();
         break;
       case 'activating':
-        // NOTE: this step only exists for the 'form' editor! It is skipped by
-        // the 'direct' and 'direct-with-wysiwyg' editors, because no loading is
-        // necessary.
-        this.prepareEdit(this.editorName);
+        // NOTE: this stage is not used by every editor! It's only used by those
+        // that need to interact with the server.
+        this.prepareEdit();
         break;
       case 'active':
-        if (this.editorName !== 'form') {
-          this.prepareEdit(this.editorName);
+        if (from !== 'activating') {
+          this.prepareEdit();
         }
-        this.startEdit(this.editorName);
+        this.startEdit();
         break;
       case 'changed':
         break;
@@ -130,7 +119,7 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
 
   undecorate: function () {
     this.$el
-      .removeClass('edit-candidate edit-editable edit-highlighted edit-editing edit-belowoverlay');
+      .removeClass('edit-candidate edit-editable edit-highlighted edit-editing');
   },
 
   startHighlight: function () {
@@ -146,26 +135,22 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
       .removeClass('edit-highlighted');
   },
 
-  prepareEdit: function(editorName) {
+  prepareEdit: function() {
     this.$el.addClass('edit-editing');
 
     // While editing, don't show *any* other editors.
     // @todo: BLOCKED_ON(Create.js, https://github.com/bergie/create/issues/133)
     // Revisit this.
     $('.edit-candidate').not('.edit-editing').removeClass('edit-editable');
-
-    if (editorName === 'form') {
-      this.$el.addClass('edit-belowoverlay');
-    }
   },
 
-  startEdit: function(editorName) {
-    if (editorName !== 'form') {
+  startEdit: function() {
+    if (this._needsPadding()) {
       this._pad();
     }
   },
 
-  stopEdit: function(editorName) {
+  stopEdit: function() {
     this.$el.removeClass('edit-highlighted edit-editing');
 
     // Make the other editors show up again.
@@ -173,14 +158,22 @@ Drupal.edit.views.PropertyEditorDecorationView = Backbone.View.extend({
     // Revisit this.
     $('.edit-candidate').addClass('edit-editable');
 
-    if (editorName === 'form') {
-      this.$el.removeClass('edit-belowoverlay');
-    }
-    else {
+    if (this._needsPadding()) {
       this._unpad();
     }
   },
 
+  /**
+   * Determines whether the PropertyEditor widget needs us to add padding.
+   *
+   * @see Drupal.edit.views.ToolbarView._editUI()
+   *
+   * @return bool
+   */
+  _needsPadding: function() {
+    return Drupal.edit.views.ToolbarView._editUI(this.editor, 'padding');
+  },
+
   _pad: function () {
     var self = this;
 
diff --git a/core/modules/edit/js/views/toolbar-view.js b/core/modules/edit/js/views/toolbar-view.js
index b60276b..31de8a2 100644
--- a/core/modules/edit/js/views/toolbar-view.js
+++ b/core/modules/edit/js/views/toolbar-view.js
@@ -40,8 +40,7 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
    *   - editor: the editor object with an 'options' object that has these keys:
    *      * entity: the VIE entity for the property.
    *      * property: the predicate of the property.
-   *      * editorName: the editor name: 'form', 'direct' or
-   *        'direct-with-wysiwyg'.
+   *      * editorName: the editor name.
    *      * element: the jQuery-wrapped editor DOM element
    *   - $storageWidgetEl: the DOM element on which the Create Storage widget is
    *     initialized.
@@ -71,8 +70,8 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
         break;
       case 'candidate':
         if (from !== 'inactive') {
-          if (from !== 'highlighted' && this.editorName !== 'form') {
-            this._unpad(this.editorName);
+          if (from !== 'highlighted' && this._editUI('padding')) {
+            this._unpad();
           }
           this.remove();
         }
@@ -86,12 +85,16 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
         this.setLoadingIndicator(true);
         break;
       case 'active':
-        this.startEdit(this.editorName);
+        this.startEdit();
         this.setLoadingIndicator(false);
-        if (this.editorName !== 'form') {
-          this._pad(this.editorName);
+        if (this._editUI('fullWidthToolbar')) {
+          this.$el.addClass('edit-toolbar-fullwidth');
         }
-        if (this.editorName === 'direct-with-wysiwyg') {
+
+        if (this._editUI('padding')) {
+          this._pad();
+        }
+        if (this._editUI('unifiedToolbar')) {
           this.insertWYSIWYGToolGroups();
         }
         break;
@@ -304,11 +307,20 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
   },
 
   /**
+   * Retrieves an Edit UI integration aspect as defined by the editor.
+   *
+   * @see Drupal.edit.views.ToolbarView._editUI()
+   */
+  _editUI: function(aspect) {
+    return Drupal.edit.views.ToolbarView._editUI(this.editor, aspect);
+  },
+
+  /**
    * Adjusts the toolbar to accomodate padding on the PropertyEditor widget.
    *
    * @see PropertyEditorDecorationView._pad().
    */
-  _pad: function(editorName) {
+  _pad: function() {
       // The whole toolbar must move to the top when the property's DOM element
       // is displayed inline.
       if (this.editor.element.css('display') === 'inline') {
@@ -318,9 +330,8 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
       // The toolbar must move to the top and the left.
       var $hf = this.$el.find('.edit-toolbar-heightfaker');
       $hf.css({ bottom: '6px', left: '-5px' });
-      // When using a WYSIWYG editor, the width of the toolbar must match the
-      // width of the editable.
-      if (editorName === 'direct-with-wysiwyg') {
+
+      if (this._editUI('fullWidthToolbar')) {
         $hf.css({ width: this.editor.element.width() + 10 });
       }
   },
@@ -330,12 +341,12 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
    *
    * @see PropertyEditorDecorationView._unpad().
    */
-  _unpad: function(editorName) {
+  _unpad: function() {
       // Move the toolbar back to its original position.
       var $hf = this.$el.find('.edit-toolbar-heightfaker');
       $hf.css({ bottom: '1px', left: '' });
-      // When using a WYSIWYG editor, restore the width of the toolbar.
-      if (editorName === 'direct-with-wysiwyg') {
+
+      if (this._editUI('fullWidthToolbar')) {
         $hf.css({ width: '' });
       }
   },
@@ -485,6 +496,27 @@ Drupal.edit.views.ToolbarView = Backbone.View.extend({
   _find: function (toolgroup) {
     return this.$el.find('.edit-toolbar .edit-toolgroup.' + toolgroup);
   }
+}, {
+  /**
+   * Retrieves an Edit UI integration aspect as defined by the editor.
+   *
+   * If the editor (Create.js PropertyEditor widget) does not configure an
+   * aspect, a default value is used.
+   *
+   * @param editWidget editor
+   *   An editor (Create.js PropertyEditor widget).
+   * @param string aspect
+   *   An Edit UI integration aspect.
+   */
+  _editUI: function(editor, aspect) {
+    var config = {}, defaultConfig = {
+      needsPadding: false, needsFullWidth: false, needsUnifiedToolbar: false
+    };
+    if (typeof editor.getEditUiIntegration === 'function') {
+      config = editor.getEditUiIntegration();
+    }
+    return _.extend(defaultConfig, config)[aspect];
+  }
 });
 
 })(jQuery, _, Backbone, Drupal);
diff --git a/core/modules/edit/lib/Drupal/edit/EditBundle.php b/core/modules/edit/lib/Drupal/edit/EditBundle.php
index d92fd73..f64c29b 100644
--- a/core/modules/edit/lib/Drupal/edit/EditBundle.php
+++ b/core/modules/edit/lib/Drupal/edit/EditBundle.php
@@ -20,18 +20,18 @@ class EditBundle extends Bundle {
    * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
    */
   public function build(ContainerBuilder $container) {
-    // Register the plugin managers for our plugin types with the dependency injection container.
-    $container->register('plugin.manager.edit.processed_text_editor', 'Drupal\edit\Plugin\ProcessedTextEditorManager');
+    $container->register('plugin.manager.edit.editor', 'Drupal\edit\Plugin\EditorManager');
 
     $container->register('access_check.edit.entity_field', 'Drupal\edit\Access\EditEntityFieldAccessCheck')
       ->addTag('access_check');
 
     $container->register('edit.editor.selector', 'Drupal\edit\EditorSelector')
-      ->addArgument(new Reference('plugin.manager.edit.processed_text_editor'));
+      ->addArgument(new Reference('plugin.manager.edit.editor'));
 
     $container->register('edit.metadata.generator', 'Drupal\edit\MetadataGenerator')
       ->addArgument(new Reference('access_check.edit.entity_field'))
-      ->addArgument(new Reference('edit.editor.selector'));
+      ->addArgument(new Reference('edit.editor.selector'))
+      ->addArgument(new Reference('plugin.manager.edit.editor'));
   }
 
 }
diff --git a/core/modules/edit/lib/Drupal/edit/EditController.php b/core/modules/edit/lib/Drupal/edit/EditController.php
index 5d6f8ed..6f2674c 100644
--- a/core/modules/edit/lib/Drupal/edit/EditController.php
+++ b/core/modules/edit/lib/Drupal/edit/EditController.php
@@ -42,7 +42,7 @@ public function metadata(Request $request) {
 
     $metadata = array();
     foreach ($fields as $field) {
-      list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode(':', $field);
+      list($entity_type, $entity_id, $field_name, $langcode, $view_mode) = explode('/', $field);
 
       // Load the entity.
       if (!$entity_type || !entity_get_info($entity_type)) {
diff --git a/core/modules/edit/lib/Drupal/edit/EditorSelector.php b/core/modules/edit/lib/Drupal/edit/EditorSelector.php
index 5c44954..964c539 100644
--- a/core/modules/edit/lib/Drupal/edit/EditorSelector.php
+++ b/core/modules/edit/lib/Drupal/edit/EditorSelector.php
@@ -8,6 +8,7 @@
 namespace Drupal\edit;
 
 use Drupal\Component\Plugin\PluginManagerInterface;
+use Drupal\Component\Utility\NestedArray;
 use Drupal\field\FieldInstance;
 
 /**
@@ -16,152 +17,110 @@
 class EditorSelector implements EditorSelectorInterface {
 
   /**
-   * The manager for processed text editor plugins.
+   * The manager for editor (Create.js PropertyEditor widget) plug-ins.
    *
    * @var \Drupal\Component\Plugin\PluginManagerInterface
    */
-  protected $processedTextEditorManager;
-
-  /**
-   * The processed text editor plugin selected.
-   *
-   * @var \Drupal\edit\Plugin\ProcessedTextEditorInterface
-   */
-  protected $processedTextEditorPlugin;
+  protected $editorManager;
 
   /**
    * Constructs a new EditorSelector.
    *
-   * @param \Drupal\Component\Plugin\PluginManagerInterface $processed_text_editor_manager
-   *   The manager for processed text editor plugins.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface
+   *   The manager for Create.js PropertyEditor widget plug-ins.
    */
-  public function __construct(PluginManagerInterface $processed_text_editor_manager) {
-    $this->processedTextEditorManager = $processed_text_editor_manager;
+  public function __construct(PluginManagerInterface $editor_manager) {
+    $this->editorManager = $editor_manager;
   }
 
   /**
    * Implements \Drupal\edit\EditorSelectorInterface::getEditor().
    */
   public function getEditor($formatter_type, FieldInstance $instance, array $items) {
+    $alternatives = &drupal_static(__FUNCTION__, NULL);
+
+    // Build a static cache of the editors that have registered themselves as
+    // alternatives to a certain editor.
+    if (!isset($alternatives)) {
+      $editors = $this->editorManager->getDefinitions();
+      foreach ($editors as $alternative_editor_id => $editor) {
+        if (isset($editor['alternativeTo'])) {
+          foreach ($editor['alternativeTo'] as $original_editor_id) {
+            $alternatives[$original_editor_id][] = $alternative_editor_id;
+          }
+        }
+      }
+    }
+
     // Check if the formatter defines an appropriate in-place editor. For
     // example, text formatters displaying untrimmed text can choose to use the
     // 'direct' editor. If the formatter doesn't specify, fall back to the
     // 'form' editor, since that can work for any field. Formatter definitions
     // can use 'disabled' to explicitly opt out of in-place editing.
     $formatter_info = field_info_formatter_types($formatter_type);
-    $editor = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form';
-    if ($editor == 'disabled') {
+    $editor_id = isset($formatter_info['edit']['editor']) ? $formatter_info['edit']['editor'] : 'form';
+    if ($editor_id === 'disabled') {
       return;
     }
+    elseif ($editor_id === 'form') {
+      return 'form';
+    }
 
-    // The same text formatters can be used for single-valued and multivalued
-    // fields and for processed and unprocessed text, so we can't rely on the
-    // formatter definition for the final determination, because:
-    // - The direct editor does not work for multivalued fields.
-    // - Processed text can benefit from a WYSIWYG editor.
-    // - Empty processed text without an already selected format requires a form
-    //   to select one.
-    // @todo The processed text logic is too coupled to text fields. Figure out
-    //   how to generalize to other textual field types.
-    // @todo All of this might hint at formatter *definitions* not being the
-    //   ideal place for editor specification. Moving the determination to
-    //   something that works with instantiated formatters, not just their
-    //   definitions, could alleviate that, but might come with its own
-    //   challenges.
-    if ($editor == 'direct') {
-      $field = field_info_field($instance['field_name']);
-      if ($field['cardinality'] != 1) {
-        // The direct editor does not work for multivalued fields.
-        $editor = 'form';
-      }
-      elseif (!empty($instance['settings']['text_processing'])) {
-        $format_id = $items[0]['format'];
-        if (isset($format_id)) {
-          $wysiwyg_plugin = $this->getProcessedTextEditorPlugin();
-          if (isset($wysiwyg_plugin) && $wysiwyg_plugin->checkFormatCompatibility($format_id)) {
-            // Yay! Even though the text is processed, there's a WYSIWYG editor
-            // that can work with it.
-            $editor = 'direct-with-wysiwyg';
-          }
-          else {
-            // @todo We might not have to downgrade all the way to 'form'. The
-            //   'direct' editor might be appropriate for some kinds of
-            //   processed text.
-            $editor = 'form';
-          }
-        }
-        else {
-          // If a format is not yet selected, a form is needed to select one.
-          $editor = 'form';
-        }
+    // No early return, so create a list of all choices.
+    $editor_choices = array($editor_id);
+    if (isset($alternatives[$editor_id])) {
+      $editor_choices = array_merge($editor_choices, $alternatives[$editor_id]);
+    }
+
+    // Make a choice.
+    foreach ($editor_choices as $editor_id) {
+      $editor = $this->editorManager->createInstance($editor_id);
+      if ($editor->isCompatible($instance, $items)) {
+        return $editor_id;
       }
     }
 
-    return $editor;
+    // We still don't have a choice, so fall back to the default 'form' editor.
+    return 'form';
   }
 
   /**
    * Implements \Drupal\edit\EditorSelectorInterface::getAllEditorAttachments().
+   *
+   * @todo Instead of loading all JS/CSS for all editors, load them lazily when
+   *   needed.
+   * @todo The NestedArray stuff is wonky.
    */
   public function getAllEditorAttachments() {
-    $this->getProcessedTextEditorPlugin();
-    if (!isset($this->processedTextEditorPlugin)) {
-      return array();
-    }
-
-    $js = array();
-
-    // Add library and settings for the selected processed text editor plugin.
-    $definition = $this->processedTextEditorPlugin->getDefinition();
-    if (!empty($definition['library'])) {
-      $js['library'][] = array($definition['library']['module'], $definition['library']['name']);
-    }
-    $this->processedTextEditorPlugin->addJsSettings();
-
-    // Also add the setting to register it with Create.js
-    if (!empty($definition['propertyEditorName'])) {
-      $js['js'][] = array(
-        'data' => array(
-          'edit' => array(
-            'wysiwygEditorWidgetName' => $definition['propertyEditorName'],
-          ),
+    $attachments = array();
+
+    // Static attachments: the library for each editor plus corresponding
+    // settings for Edit.
+    $definitions = $this->editorManager->getDefinitions();
+    foreach ($definitions as $definition) {
+      $attachments[] = array(
+        'library' => array(
+          array($definition['library']['module'], $definition['library']['name'])
         ),
-        'type' => 'setting'
+        // This will be used in Create.js' propertyEditorWidgetsConfiguration.
+        'js' => array(
+          array(
+            'type' => 'setting',
+            'data' => array('edit' => array('editors' => array(
+              $definition['id'] => array('widget' => $definition['jsClassName'])
+            )))
+          )
+        )
       );
     }
 
-    return $js;
-  }
-
-  /**
-   * Returns the plugin to use for the 'direct-with-wysiwyg' editor.
-   *
-   * @return \Drupal\edit\Plugin\ProcessedTextEditorInterface
-   *   The editor plugin.
-   *
-   * @todo We currently only support one plugin (the first one returned by the
-   *   manager) for the 'direct-with-wysiwyg' editor on any given page. Enhance
-   *   this to allow different ones per element (e.g., Aloha for one text field
-   *   and CKEditor for another one).
-   *
-   * @todo The terminology here is confusing. 'direct-with-wysiwyg' is one of
-   *   several possible "editor"s for processed text. When using it, we need to
-   *   integrate a particular WYSIWYG editor, which in Create.js is called a
-   *   "PropertyEditor widget", but we're not yet including "widget" in the name
-   *   of ProcessedTextEditorInterface to minimize confusion with Field API
-   *   widgets. So, we're currently refering to these as "plugins", which is
-   *   correct in that it's using Drupal's Plugin API, but less informative than
-   *   naming it "widget" or similar.
-   */
-  protected function getProcessedTextEditorPlugin() {
-    if (!isset($this->processedTextEditorPlugin)) {
-      $definitions = $this->processedTextEditorManager->getDefinitions();
-      if (count($definitions)) {
-        $plugin_ids = array_keys($definitions);
-        $plugin_id = $plugin_ids[0];
-        $this->processedTextEditorPlugin = $this->processedTextEditorManager->createInstance($plugin_id);
-      }
+    // Dynamic attachments.
+    $editor_ids = array_keys($definitions);
+    foreach ($editor_ids as $editor_id) {
+      $editor = $this->editorManager->createInstance($editor_id);
+      $attachments[] = $editor->getDynamicAttachments();;
     }
-    return $this->processedTextEditorPlugin;
+
+    return NestedArray::mergeDeepArray($attachments);
   }
 }
diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
index 5a7da04..99b4ac5 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGenerator.php
@@ -8,6 +8,7 @@
 namespace Drupal\edit;
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Component\Plugin\PluginManagerInterface;
 use Drupal\field\FieldInstance;
 use Drupal\edit\Access\EditEntityFieldAccessCheckInterface;
 
@@ -32,16 +33,26 @@ class MetadataGenerator implements MetadataGeneratorInterface {
   protected $editorSelector;
 
   /**
+   * The manager for editor (Create.js PropertyEditor widget) plug-ins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $editorManager;
+
+  /**
    * Constructs a new MetadataGenerator.
    *
    * @param \Drupal\edit\Access\EditEntityFieldAccessCheckInterface $access_checker
    *   An object that checks if a user has access to edit a given field.
    * @param \Drupal\edit\EditorSelectorInterface $editor_selector
    *   An object that determines which editor to attach to a given field.
+   * @param \Drupal\Component\Plugin\PluginManagerInterface
+   *   The manager for editor plug-ins.
    */
-  public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector) {
+  public function __construct(EditEntityFieldAccessCheckInterface $access_checker, EditorSelectorInterface $editor_selector, PluginManagerInterface $editor_manager) {
     $this->accessChecker = $access_checker;
     $this->editorSelector = $editor_selector;
+    $this->editorManager = $editor_manager;
   }
 
   /**
@@ -56,31 +67,30 @@ public function generate(EntityInterface $entity, FieldInstance $instance, $lang
       return array('access' => FALSE);
     }
 
-    $label = $instance['label'];
+    // Early-return if no editor is available.
     $formatter_id = entity_get_render_display($entity, $view_mode)->getFormatter($instance['field_name'])->getPluginId();
     $items = $entity->get($field_name);
     $items = $items[$langcode];
-    $editor = $this->editorSelector->getEditor($formatter_id, $instance, $items);
+    $editor_id = $this->editorSelector->getEditor($formatter_id, $instance, $items);
+    if (!isset($editor_id)) {
+      return array('access' => FALSE);
+    }
+
+    // Gather metadata, allow the editor to add additional metadata of its own.
+    $label = $instance['label'];
+    $editor = $this->editorManager->createInstance($editor_id);
     $metadata = array(
       'label' => $label,
       'access' => TRUE,
-      'editor' => $editor,
+      'editor' => $editor_id,
       'aria' => t('Entity @type @id, field @field', array('@type' => $entity->entityType(), '@id' => $entity->id(), '@field' => $label)),
     );
-    // Additional metadata for WYSIWYG editor integration.
-    if ($editor === 'direct-with-wysiwyg') {
-      $format_id = $items[0]['format'];
-      $metadata['format'] = $format_id;
-      $metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
+    $custom_metadata = $editor->getMetadata($instance, $items);
+    if (count($custom_metadata)) {
+      $metadata['custom'] = $custom_metadata;
     }
-    return $metadata;
-  }
 
-  /**
-   * Returns whether the text format has transformation filters.
-   */
-  protected function textFormatHasTransformationFilters($format_id) {
-    return (bool) count(array_intersect(array(FILTER_TYPE_TRANSFORM_REVERSIBLE, FILTER_TYPE_TRANSFORM_IRREVERSIBLE), filter_get_filter_types_by_format($format_id)));
+    return $metadata;
   }
 
 }
diff --git a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
index 9e4fb5d..ff052d7 100644
--- a/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
+++ b/core/modules/edit/lib/Drupal/edit/MetadataGeneratorInterface.php
@@ -20,7 +20,7 @@
    *
    * @param \Drupal\Core\Entity\EntityInterface $entity
    *   The entity being edited.
-   * @param Drupal\field\FieldInstance $instance
+   * @param \Drupal\field\FieldInstance $instance
    *   The field instance of the field being edited.
    * @param string $langcode
    *   The name of the language for which the field is being edited.
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/EditorInterFace.php b/core/modules/edit/lib/Drupal/edit/Plugin/EditorInterFace.php
new file mode 100644
index 0000000..f2c8691
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/EditorInterFace.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\EditorInterface.
+ */
+
+namespace Drupal\edit\Plugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\field\FieldInstance;
+
+/**
+ * Defines an interface for Create.js PropertyEditor widgets.
+ *
+ * A PropertyEditor widget is a user-facing interface to edit an entity property
+ * through Create.js.
+ */
+interface EditorInterface extends PluginInspectionInterface {
+
+  /**
+   * Checks whether this editor is compatible with a given field instance.
+   *
+   * @param \Drupal\field\FieldInstance $instance
+   *   The field instance of the field being edited.
+   * @param array $items
+   *   The field's item values.
+   *
+   * @return bool
+   *   TRUE if it is compatible, FALSE otherwise.
+   */
+  public function isCompatible(FieldInstance $instance, array $items);
+
+  /**
+   * Generates metadata that is needed specifically for this editor.
+   *
+   * Will only be called by \Drupal\edit\MetadataGeneratorInterface::generate()
+   * when the passed in field instance & item values will use this editor.
+   *
+   * @param \Drupal\field\FieldInstance $instance
+   *   The field instance of the field being edited.
+   * @param array $items
+   *   The field's item values.
+   *
+   * @return array
+   *   A keyed array with metadata. Each key should be prefixed with the plug-in
+   *   ID of the editor.
+   */
+  public function getMetadata(FieldInstance $instance, array $items);
+
+  /**
+   * Returns the dynamic (context-specific) attachments for this editor.
+   *
+   * @return array
+   *   An array of attachments, for use with #attached.
+   *
+   * @see drupal_process_attached()
+   */
+  public function getDynamicAttachments();
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php
new file mode 100644
index 0000000..7fd04bd
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/EditorManager.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\EditorManager.
+ */
+
+namespace Drupal\edit\Plugin;
+
+use Drupal\Component\Plugin\Discovery\DerivativeDiscoveryDecorator;
+use Drupal\Component\Plugin\Discovery\ProcessDecorator;
+use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Core\Plugin\Discovery\AlterDecorator;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\CacheDecorator;
+
+/**
+ * Editor manager.
+ *
+ * The "Form" Create.js PropertyEditor widget must always be available.
+ */
+class EditorManager extends PluginManagerBase {
+
+  protected $defaults = array(
+    'class' => 'Drupal\edit\Plugin\edit\editor\Form',
+    // 'class' => 'Drupal\edit\Plugin\edit\editor\Direct',
+  );
+
+  /**
+   * Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
+   */
+  public function __construct() {
+    $this->discovery = new AnnotatedClassDiscovery('edit', 'editor');
+    $this->discovery = new DerivativeDiscoveryDecorator($this->discovery);
+    $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition'));
+
+    $this->discovery = new AlterDecorator($this->discovery, 'edit_editor');
+    $this->discovery = new CacheDecorator($this->discovery, 'edit:editor');
+
+    $this->factory = new DefaultFactory($this->discovery);
+  }
+
+  /**
+   * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition().
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    // @todo Remove this check once http://drupal.org/node/1780396 is resolved.
+    if (!module_exists($definition['module'])) {
+      $definition = NULL;
+      return;
+    }
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php
deleted file mode 100644
index 6e1cace..0000000
--- a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorBase.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of \Drupal\edit\Plugin\ProcessedTextPropertyBase.
- */
-
-namespace Drupal\edit\Plugin;
-
-use Drupal\Component\Plugin\PluginBase;
-
-/**
- * Base class for processed text editor plugins.
- */
-abstract class ProcessedTextEditorBase extends PluginBase implements ProcessedTextEditorInterface {
-
-  /**
-   * Implements \Drupal\edit\Plugin\ProcessedTextEditorInterface::addJsSettings().
-   *
-   * This base class provides an empty implementation for text editors that
-   * do not need to add JavaScript settings besides those added by the library.
-   */
-  public function addJsSettings() {
-  }
-
-}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php
deleted file mode 100644
index 1b2efb1..0000000
--- a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorInterface.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of \Drupal\edit\Plugin\ProcessedTextEditorInterface.
- */
-
-namespace Drupal\edit\Plugin;
-
-use Drupal\Component\Plugin\PluginInspectionInterface;
-
-/**
- * Defines an interface for PropertyEditor widgets for processed text fields.
- *
- * A PropertyEditor widget is a user-facing interface to edit an entity property
- * through Create.js.
- */
-interface ProcessedTextEditorInterface extends PluginInspectionInterface {
-
-  /**
-   * Adds JavaScript settings.
-   */
-  public function addJsSettings();
-
-  /**
-   * Checks if the text editor is compatible with a given text format.
-   *
-   * @param $format_id
-   *   A text format ID.
-   *
-   * @return bool
-   *   TRUE if it is compatible, FALSE otherwise.
-   */
-  public function checkFormatCompatibility($format_id);
-}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php b/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php
deleted file mode 100644
index 6349310..0000000
--- a/core/modules/edit/lib/Drupal/edit/Plugin/ProcessedTextEditorManager.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of \Drupal\edit\Plugin\ProcessedTextEditorManager.
- */
-
-namespace Drupal\edit\Plugin;
-
-use Drupal\Component\Plugin\PluginManagerBase;
-use Drupal\Component\Plugin\Factory\DefaultFactory;
-use Drupal\Component\Plugin\Discovery\ProcessDecorator;
-use Drupal\Core\Plugin\Discovery\AlterDecorator;
-use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
-use Drupal\Core\Plugin\Discovery\CacheDecorator;
-
-/**
- * ProcessedTextEditor manager.
- */
-class ProcessedTextEditorManager extends PluginManagerBase {
-
-  /**
-   * Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
-   */
-  public function __construct() {
-    $this->discovery = new AnnotatedClassDiscovery('edit', 'processed_text_editor');
-    $this->discovery = new ProcessDecorator($this->discovery, array($this, 'processDefinition'));
-    $this->discovery = new AlterDecorator($this->discovery, 'edit_wysiwyg');
-    $this->discovery = new CacheDecorator($this->discovery, 'edit:wysiwyg');
-    $this->factory = new DefaultFactory($this->discovery);
-  }
-
-  /**
-   * Overrides Drupal\Component\Plugin\PluginManagerBase::processDefinition().
-   */
-  public function processDefinition(&$definition, $plugin_id) {
-    parent::processDefinition($definition, $plugin_id);
-
-    // @todo Remove this check once http://drupal.org/node/1780396 is resolved.
-    if (!module_exists($definition['module'])) {
-      $definition = NULL;
-      return;
-    }
-  }
-
-}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Direct.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Direct.php
new file mode 100644
index 0000000..cafb3b5
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Direct.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\edit\editor\Direct.
+ */
+
+namespace Drupal\edit\Plugin\edit\editor;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\edit\Plugin\EditorInterface;
+use Drupal\field\FieldInstance;
+
+
+/**
+ * Defines the "direct" Create.js PropertyEditor widget.
+ *
+ * @Plugin(
+ *   id = "direct",
+ *   jsClassName = "drupalContentEditableWidget",
+ *   library = {
+ *     "module" = "edit",
+ *     "name" = "edit.editor.direct"
+ *   },
+ *   module = "edit"
+ * )
+ */
+class Direct extends PluginBase implements EditorInterface {
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::isCompatible().
+   *
+   * @todo The processed text logic is too coupled to text fields. Figure out
+   *   how to generalize to other textual field types.
+   */
+  function isCompatible(FieldInstance $instance, array $items) {
+    $field = field_info_field($instance['field_name']);
+
+    // This editor is incompatible with multivalued fields.
+    if ($field['cardinality'] != 1) {
+      return FALSE;
+    }
+    // This editor is incompatible with processed ("rich") text fields.
+    elseif (!empty($instance['settings']['text_processing'])) {
+      return FALSE;
+    }
+    else {
+      return TRUE;
+    }
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getMetadata().
+   */
+  function getMetadata(FieldInstance $instance, array $items) {
+    return array();
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getDynamicAttachments().
+   */
+  function getDynamicAttachments() {
+    return array();
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Form.php b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Form.php
new file mode 100644
index 0000000..6277096
--- /dev/null
+++ b/core/modules/edit/lib/Drupal/edit/Plugin/edit/editor/Form.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit\Plugin\edit\editor\Form.
+ */
+
+namespace Drupal\edit\Plugin\edit\editor;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\edit\Plugin\EditorInterface;
+use Drupal\field\FieldInstance;
+
+
+/**
+ * Defines the "form" Create.js PropertyEditor widget.
+ *
+ * @Plugin(
+ *   id = "form",
+ *   jsClassName = "drupalFormWidget",
+ *   library = {
+ *     "module" = "edit",
+ *     "name" = "edit.editor.form"
+ *   },
+ *   module = "edit"
+ * )
+ */
+class Form extends PluginBase implements EditorInterface {
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::isCompatible().
+   */
+  function isCompatible(FieldInstance $instance, array $items) {
+    return TRUE;
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getMetadata().
+   */
+  function getMetadata(FieldInstance $instance, array $items) {
+    return array();
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getDynamicAttachments().
+   */
+  function getDynamicAttachments() {
+    return array();
+  }
+
+}
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
index 573ce93..7544abb 100644
--- a/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
+++ b/core/modules/edit/lib/Drupal/edit/Tests/EditTestBase.php
@@ -20,7 +20,7 @@ class EditTestBase extends DrupalUnitTestBase {
    *
    * @var array
    */
-  public static $modules = array('system', 'entity', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test');
+  public static $modules = array('system', 'entity', 'field_test', 'field', 'number', 'text', 'edit');
 
   /**
    * Sets the default field storage backend for fields created during tests.
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php
index 5217b92..b35d668 100644
--- a/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php
+++ b/core/modules/edit/lib/Drupal/edit/Tests/EditorSelectionTest.php
@@ -7,7 +7,7 @@
 
 namespace Drupal\edit\Tests;
 
-use Drupal\edit\Plugin\ProcessedTextEditorManager;
+use Drupal\edit\Plugin\EditorManager;
 use Drupal\edit\EditorSelector;
 
 /**
@@ -16,6 +16,13 @@
 class EditorSelectionTest extends EditTestBase {
 
   /**
+   * The manager for editor (Create.js PropertyEditor widget) plug-ins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $editorManager;
+
+  /**
    * The editor selector object to be tested.
    *
    * @var \Drupal\edit\EditorSelectorInterface
@@ -33,12 +40,8 @@ public static function getInfo() {
   function setUp() {
     parent::setUp();
 
-    // @todo Rather than using the real ProcessedTextEditorManager, which can
-    //   find all text editor plugins in the codebase, create a mock one for
-    //   testing that is populated with only the ones we want to test.
-    $text_editor_manager = new ProcessedTextEditorManager();
-
-    $this->editorSelector = new EditorSelector($text_editor_manager);
+    $this->editorManager = new EditorManager();
+    $this->editorSelector = new EditorSelector($this->editorManager);
   }
 
   /**
@@ -103,6 +106,10 @@ function testText() {
    * format compatibility.
    */
   function testTextWysiwyg() {
+   // Enable edit_test module so that the 'wysiwyg' Create.js PropertyEditor
+   // widget becomes available.
+  $this->enableModules(array('edit_test'), FALSE);
+
     $field_name = 'field_textarea';
     $this->createFieldWithInstance(
       $field_name, 'text', 1, 'Long text field',
@@ -116,18 +123,15 @@ function testTextWysiwyg() {
       array()
     );
 
-    // ProcessedTextEditor plug-in compatible with the full_html text format.
-    state()->set('edit_test.compatible_format', 'full_html');
-
     // Pretend there is an entity with these items for the field.
     $items = array(array('value' => 'Hello, world!', 'format' => 'filtered_html'));
 
-    // Editor selection with cardinality 1, without compatible text format.
-    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "Without cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
+    // Editor selection w/ cardinality 1, text format w/o associated text editor.
+    $this->assertEqual('form', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
 
-    // Editor selection with cardinality 1, with compatible text format.
+    // Editor selection w/ cardinality 1, text format w/ associated text editor.
     $items[0]['format'] = 'full_html';
-    $this->assertEqual('direct-with-wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'direct-with-wysiwyg' editor is selected.");
+    $this->assertEqual('wysiwyg', $this->getSelectedEditor($items, $field_name), "With cardinality 1, and the full_html text format, the 'wysiwyg' editor is selected.");
 
     // Editor selection with text processing, cardinality >1
     $this->field_textarea_field['cardinality'] = 2;
diff --git a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php
index e8060ea..dc268529 100644
--- a/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php
+++ b/core/modules/edit/lib/Drupal/edit/Tests/MetadataGeneratorTest.php
@@ -9,7 +9,7 @@
 
 use Drupal\edit\EditorSelector;
 use Drupal\edit\MetadataGenerator;
-use Drupal\edit\Plugin\ProcessedTextEditorManager;
+use Drupal\edit\Plugin\EditorManager;
 use Drupal\edit_test\MockEditEntityFieldAccessCheck;
 
 /**
@@ -18,6 +18,13 @@
 class MetadataGeneratorTest extends EditTestBase {
 
   /**
+   * The manager for editor (Create.js PropertyEditor widget) plug-ins.
+   *
+   * @var \Drupal\Component\Plugin\PluginManagerInterface
+   */
+  protected $editorManager;
+
+  /**
    * The metadata generator object to be tested.
    *
    * @var \Drupal\edit\MetadataGeneratorInterface.php
@@ -49,14 +56,10 @@ public static function getInfo() {
   function setUp() {
     parent::setUp();
 
-    // @todo Rather than using the real ProcessedTextEditorManager, which can
-    //   find all text editor plugins in the codebase, create a mock one for
-    //   testing that is populated with only the ones we want to test.
-    $text_editor_manager = new ProcessedTextEditorManager();
-
+    $this->editorManager = new EditorManager();
     $this->accessChecker = new MockEditEntityFieldAccessCheck();
-    $this->editorSelector = new EditorSelector($text_editor_manager);
-    $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector);
+    $this->editorSelector = new EditorSelector($this->editorManager);
+    $this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
   }
 
   /**
@@ -119,6 +122,61 @@ function testSimpleEntityType() {
       'aria' => 'Entity test_entity 1, field Simple number field',
     );
     $this->assertEqual($expected_2, $metadata_2, 'The correct metadata is generated for the second field.');
+  }
+
+  function testEditorWithCustomMetadata() {
+    $this->enableModules(array('filter'));
 
+    // Enable edit_test module so that the WYSIWYG Create.js PropertyEditor
+    // widget becomes available.
+    $this->enableModules(array('edit_test'), FALSE);
+
+    // Create a rich text field.
+    $field_name = 'field_rich';
+    $field_label = 'Rich text field';
+    $this->createFieldWithInstance(
+      $field_name, 'text', 1, $field_label,
+      // Instance settings.
+      array('text_processing' => 1),
+      // Widget type & settings.
+      'text_textfield',
+      array('size' => 42),
+      // 'default' formatter type & settings.
+      'text_default',
+      array()
+    );
+
+    // Create a text format.
+    $full_html_format = array(
+      'format' => 'full_html',
+      'name' => 'Full HTML',
+      'weight' => 1,
+      'filters' => array(
+        'filter_htmlcorrector' => array('status' => 1),
+      ),
+    );
+    $full_html_format = (object) $full_html_format;
+    filter_format_save($full_html_format);
+
+    // Create an entity with values for this rich text field.
+    $this->entity = field_test_create_entity();
+    $this->is_new = TRUE;
+    $this->entity->{$field_name}[LANGUAGE_NOT_SPECIFIED] = array(array('value' => 'Test', 'format' => 'full_html'));
+    field_test_entity_save($this->entity);
+    $entity = entity_load('test_entity', $this->entity->ftid);
+
+    // Verify metadata.
+    $instance = field_info_instance($entity->entityType(), $field_name, $entity->bundle());
+    $metadata = $this->metadataGenerator->generate($entity, $instance, LANGUAGE_NOT_SPECIFIED, 'default');
+    $expected = array(
+      'access' => TRUE,
+      'label' => 'Rich text field',
+      'editor' => 'wysiwyg',
+      'aria' => 'Entity test_entity 1, field Rich text field',
+      'custom' => array(
+        'format' => 'full_html'
+      ),
+    );
+    $this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
   }
 }
diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/Wysiwyg.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/Wysiwyg.php
new file mode 100644
index 0000000..5746548
--- /dev/null
+++ b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/editor/Wysiwyg.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\edit_test\Plugin\edit\editor\Wysiwyg.
+ */
+
+namespace Drupal\edit_test\Plugin\edit\editor;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\edit\Plugin\EditorInterface;
+use Drupal\field\FieldInstance;
+
+
+/**
+ * Defines the "wysiwyg" Create.js PropertyEditor widget.
+ *
+ * @Plugin(
+ *   id = "wysiwyg",
+ *   jsClassName = "not needed for test",
+ *   alternativeTo = {"direct"},
+ *   library = {
+ *     "module" = "edit_test",
+ *     "name" = "non-existing-wysiwyg"
+ *   },
+ *   module = "edit_test"
+ * )
+ */
+class Wysiwyg extends PluginBase implements EditorInterface {
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::isCompatible().
+   */
+  function isCompatible(FieldInstance $instance, array $items) {
+    $field = field_info_field($instance['field_name']);
+
+    // This editor is incompatible with multivalued fields.
+    if ($field['cardinality'] != 1) {
+      return FALSE;
+    }
+    // This editor is compatible with processed ("rich") text fields; but only
+    // if there is a currently active text format and that text format is the
+    // 'full_html' text format.
+    elseif (!empty($instance['settings']['text_processing'])) {
+      $format_id = $items[0]['format'];
+      if (isset($format_id) && $format_id === 'full_html') {
+        return TRUE;
+      }
+      return FALSE;
+    }
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getMetadata().
+   */
+  function getMetadata(FieldInstance $instance, array $items) {
+    $format_id = $items[0]['format'];
+    $metadata['format'] = $format_id;
+    return $metadata;
+  }
+
+  /**
+   * Implements \Drupal\edit\Plugin\EditorInterface::getDynamicAttachments().
+   */
+  function getDynamicAttachments() {
+    return array();
+  }
+
+}
diff --git a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php b/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php
deleted file mode 100644
index b43e77f..0000000
--- a/core/modules/edit/tests/modules/lib/Drupal/edit_test/Plugin/edit/processed_text_editor/TestProcessedEditor.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * @file
- * Definition of Drupal\edit_test\Plugin\edit\processed_text_editor\TestProcessedEditor.
- */
-
-namespace Drupal\edit_test\Plugin\edit\processed_text_editor;
-
-use Drupal\edit\Plugin\ProcessedTextEditorBase;
-use Drupal\Core\Annotation\Plugin;
-use Drupal\Core\Annotation\Translation;
-
-/**
- * Defines a test processed text editor plugin.
- *
- * @Plugin(
- *   id = "test_processed_editor",
- *   title = @Translation("Test Processed Editor"),
- *   module = "edit_test"
- * )
- */
-class TestProcessedEditor extends ProcessedTextEditorBase {
-
-  /**
-   * Implements Drupal\edit\Plugin\ProcessedTextEditorInterface::checkFormatCompatibility().
-   */
-  function checkFormatCompatibility($format_id) {
-    return state()->get('edit_test.compatible_format') == $format_id;
-  }
-
-}
