 core/modules/editor/editor.api.php                 |   48 ++++
 core/modules/editor/editor.info                    |    7 +
 core/modules/editor/editor.js                      |   80 +++++++
 core/modules/editor/editor.module                  |  247 ++++++++++++++++++++
 .../editor/lib/Drupal/editor/EditorBundle.php      |   27 +++
 .../Drupal/editor/Plugin/Core/Entity/Editor.php    |   80 +++++++
 .../lib/Drupal/editor/Plugin/EditorInterface.php   |   75 ++++++
 .../lib/Drupal/editor/Plugin/EditorManager.php     |   42 ++++
 core/modules/filter/filter.admin.inc               |    3 +
 9 files changed, 609 insertions(+)

diff --git a/core/modules/editor/editor.api.php b/core/modules/editor/editor.api.php
new file mode 100644
index 0000000..cbca0c7
--- /dev/null
+++ b/core/modules/editor/editor.api.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Documentation for Text Editor API.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Perform alterations on text editor definitions.
+ *
+ * @param $editors
+ *   Array of information on editors exposed by hook_editor_info()
+ *   implementations.
+ */
+function hook_editor_info_alter(&$editors) {
+  $editors['some_other_editor']['title'] = t('A different name');
+  $editors['some_other_editor']['library']['module'] = 'myeditoroverride';
+}
+
+/**
+ * Perform alterations on the JavaScript settings that are added for text formats.
+ *
+ * Note that changing settings here only affects the client side behavior of the
+ * filter. To affect the filter globally both on the client side and server
+ * side, use hook_filter_info_alter().
+ *
+ * @param array $settings
+ *   All the settings that will be added to the page via drupal_add_js() for
+ *   the text formats to which a user has access.
+ * @param array $formats
+ *   The list of format objects for which settings are being added.
+ */
+function hook_editor_js_settings_alter(&$settings, $formats) {
+  if (isset($formats['filtered_html'])) {
+    $settings['filtered_html']['allowedTags'][] = 'strong';
+    $settings['filtered_html']['allowedTags'][] = 'em';
+    $settings['filtered_html']['allowedTags'][] = 'img';
+  }
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/core/modules/editor/editor.info b/core/modules/editor/editor.info
new file mode 100644
index 0000000..15aab7d
--- /dev/null
+++ b/core/modules/editor/editor.info
@@ -0,0 +1,7 @@
+name = Text Editor
+description = "Provides a framework for associating text formats with text editor libraries such as WYSIWYGs or toolbars."
+package = Core
+version = VERSION
+core = 8.x
+dependencies[] = filter
+configure = admin/config/content/formats
diff --git a/core/modules/editor/editor.js b/core/modules/editor/editor.js
new file mode 100644
index 0000000..3759284
--- /dev/null
+++ b/core/modules/editor/editor.js
@@ -0,0 +1,80 @@
+/**
+ * @file
+ * Attaches behavior for the Editor module.
+ */
+
+(function ($, Drupal, drupalSettings) {
+
+"use strict";
+
+/**
+ * Initialize an empty object where editors where place their attachment code.
+ */
+Drupal.editors = {};
+
+/**
+ * Enables editors on text_format elements.
+ */
+Drupal.behaviors.editors = {
+  attach: function (context, settings) {
+    // If there are no editor settings, there are no editors to enable.
+    if (!settings.editor) {
+      return;
+    }
+
+    var $context = $(context);
+    $context.find('.filter-list:input').once('editors', function () {
+      var $this = $(this);
+      var activeEditor = $this.val();
+      var field = $this.closest('.text-format-wrapper').find('textarea').get(-1);
+
+      // Directly attach this editor, if the text format is enabled or there is
+      // only one text format at all.
+      if ($this.is(':input')) {
+        if (drupalSettings.editor.formats[activeEditor]) {
+          Drupal.editorAttach(field, drupalSettings.editor.formats[activeEditor]);
+        }
+      }
+      // Attach onChange handlers to text format selector elements.
+      if ($this.is('select')) {
+        $this.on('change.editorAttach', function () {
+          // Prevent double-binding if the change event is triggered manually.
+          if ($this.val() === activeEditor) {
+            return;
+          }
+
+          // Detach the current editor (if any) and attach a new editor.
+          if (drupalSettings.editor.formats[activeEditor]) {
+            Drupal.editorDetach(field, drupalSettings.editor.formats[activeEditor]);
+          }
+          activeEditor = $this.val();
+          if (drupalSettings.editor.formats[activeEditor]) {
+            Drupal.editorAttach(field, drupalSettings.editor.formats[activeEditor]);
+          }
+        });
+      }
+      // Detach any editor when the containing form is submitted.
+      $this.parents('form').submit(function (event) {
+        // Do not detach if the event was canceled.
+        if (event.isDefaultPrevented()) {
+          return;
+        }
+        Drupal.editorDetach(field, drupalSettings.editor.formats[activeEditor]);
+      });
+    });
+  }
+};
+
+Drupal.editorAttach = function (field, format) {
+  if (format.editor) {
+    Drupal.editors[format.editor].attach(field, format);
+  }
+};
+
+Drupal.editorDetach = function (field, format) {
+  if (format.editor) {
+    Drupal.editors[format.editor].detach(field, format);
+  }
+};
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
new file mode 100644
index 0000000..4d817b3
--- /dev/null
+++ b/core/modules/editor/editor.module
@@ -0,0 +1,247 @@
+<?php
+
+/**
+ * @file
+ * Provides bindings between Filter module text formats and client-side editors.
+ */
+
+use Drupal\file\Plugin\Core\Entity\File;
+use Drupal\editor\Plugin\Core\Entity\Editor;
+
+/**
+ * Implements hook_menu_alter().
+ *
+ * Rewrites the menu entries for filter module that relate to the configuration
+ * of text editors.
+ */
+function editor_menu_alter(&$items) {
+  $items['admin/config/content/formats']['title'] = 'Text formats and editors';
+  $items['admin/config/content/formats']['description'] = 'Configure the processing of text, including toolbars or WYSIWYG editors used on input, and allowed HTML tags and automatic formatting used on output.';
+}
+
+/**
+ * Implements hook_element_info().
+ *
+ * Extends the functionality of text_format elements (provided by Filter
+ * module), so that selecting a text format notifies a client-side text editor
+ * when it should be enabled or disabled.
+ *
+ * @see filter_element_info()
+ */
+function editor_element_info() {
+  $type['text_format'] = array(
+    '#process' => array('editor_process_format'),
+  );
+  return $type;
+}
+
+/**
+ * Implements hook_library_info().
+ */
+function editor_library_info() {
+  $libraries['drupal.editor'] = array(
+    'title' => 'Editor',
+    'version' => VERSION,
+    'js' => array(
+      drupal_get_path('module', 'editor') . '/editor.js' => array('group' => JS_LIBRARY),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'drupalSettings'),
+      array('system', 'jquery.once'),
+    ),
+  );
+
+  return $libraries;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function editor_form_filter_admin_overview_alter(&$form, $form_state) {
+  // Splice in the column for "Associated editor" into the header.
+  $position = array_search('name', $form['formats']['#header']) + 1;
+  $start = array_splice($form['formats']['#header'], 0, $position, array('editor' => t('Associated editor')));
+  $form['formats']['#header'] = array_merge($start, $form['formats']['#header']);
+
+  // Then splice in the name of each text editor for each text format.
+  $editors = drupal_container()->get('plugin.manager.editor')->listAvailableDefinitions();
+  foreach (element_children($form['formats']) as $format_id) {
+    $editor = editor_load($format_id);
+    $editor_name = $editor && isset($editors[$editor->editor]) ? $editors[$editor->editor] : drupal_placeholder(t('None'));
+    $editor_column['editor'] = array(
+      '#type' => 'markup',
+      '#markup' => $editor_name,
+    );
+    $position = array_search('name', array_keys($form['formats'][$format_id])) + 1;
+    $start = array_splice($form['formats'][$format_id], 0, $position, $editor_column);
+    $form['formats'][$format_id] = array_merge($start, $form['formats'][$format_id]);
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function editor_form_filter_admin_format_form_alter(&$form, &$form_state) {
+  $format = $form['#format'];
+  $editor = editor_load($format->format);
+  $manager = drupal_container()->get('plugin.manager.editor');
+
+  // Check if we're switching to a different text editor and load that form.
+  if (isset($form_state['values']['editor'])) {
+    if ($form_state['values']['editor'] === '') {
+      $editor = FALSE;
+    }
+    elseif (empty($editor) || $form_state['values']['editor'] !== $editor->editor) {
+      $editor = entity_create('editor', array(
+        'name' => $format->name,
+        'format' => $format->format,
+        'editor' => $form_state['values']['editor'],
+      ));
+    }
+  }
+
+  // Associate a text editor with this text format.
+  $editor_options = array('' => t('None')) + $manager->listAvailableDefinitions();
+  $form['editor'] = array(
+    '#type' => 'select',
+    '#title' => t('Editor'),
+    '#options' => $editor_options,
+    '#default_value' => $editor ? $editor->editor : '',
+    '#weight' => -9,
+    '#ajax' => array(
+      'callback' => 'editor_form_filter_admin_form_ajax',
+      'wrapper' => 'editor-settings-wrapper',
+    ),
+  );
+
+  // If there aren't any options (other than "None") disable the select list.
+  if (count($editor_options) === 1) {
+    $form['editor']['#disabled'] = TRUE;
+    $form['editor']['#description'] = t('This option is disabled because no modules that provide an editor are currently enabled.');
+  }
+
+  $form['editor_settings'] = array(
+    '#tree' => TRUE,
+    '#weight' => -8,
+    '#type' => 'group',
+    '#prefix' => '<div id="editor-settings-wrapper">',
+    '#suffix' => '</div>',
+  );
+  if ($editor) {
+    $plugin = $manager->createInstance($editor->editor);
+    $form['editor_settings'] = array_merge($form['editor_settings'], $plugin->settingsForm($form, $form_state, $editor));
+    array_unshift($form['#validate'], array($plugin, 'settingsFormValidate'));
+  }
+
+  $form['#submit'][] = 'editor_form_filter_admin_format_submit';
+}
+
+/**
+ * AJAX submit handler for filter_admin_format_form().
+ */
+function editor_form_filter_admin_form_ajax($form, &$form_state) {
+  return $form['editor_settings'];
+}
+
+/**
+ * Additional submit handler for filter_admin_format_form().
+ */
+function editor_form_filter_admin_format_submit($form, &$form_state) {
+  $format = $form['#format'];
+  $editor = editor_load($format->format);
+
+  // Delete the existing editor if disabling or switching between editors.
+  if ($editor && $editor->editor != $form_state['values']['editor']) {
+    $editor->delete();
+    $editor = FALSE;
+  }
+
+  // Create a new editor or update the existing editor.
+  if ($form_state['values']['editor']) {
+    if (!$editor) {
+      $editor = entity_create('editor', array(
+        'name' => $format->name,
+        'format' => $format->format,
+        'editor' => $form_state['values']['editor'],
+      ));
+    }
+    if (isset($form_state['values']['editor_settings'])) {
+      $editor->settings = $form_state['values']['editor_settings'];
+    }
+    $editor->save();
+  }
+}
+
+/**
+ * Loads an individual configured text editor based on format ID.
+ *
+ * @return \Drupal\editor\Plugin\Core\Entity\Editor|FALSE
+ *   A text editor object, or FALSE.
+ */
+function editor_load($format_id) {
+  $editors = entity_load_multiple('editor');
+  return isset($editors[$format_id]) ? $editors[$format_id] : FALSE;
+}
+
+/**
+ * Additional #process callback on 'text_format' elements.
+ */
+function editor_process_format($element) {
+  global $user;
+
+  $formats = filter_formats($user);
+
+  if (!isset($element['#attached'])) {
+    $element['#attached'] = array();
+  }
+  $element['#attached'] = array_merge_recursive($element['#attached'], editor_get_attachments($formats));
+
+  return $element;
+}
+
+/**
+ * Retrieves text editor libraries and JavaScript settings.
+ *
+ * @param array $formats
+ *   An array of formats as returned by filter_formats().
+ * @return array
+ *   An array of library attachments, for use with #attached.
+ *
+ * @see drupal_process_attached()
+ */
+function editor_get_attachments($formats) {
+  $attachments = array();
+
+  $manager = drupal_container()->get('plugin.manager.editor');
+  $definitions = $manager->getDefinitions();
+
+  foreach ($formats as $format_id => $format_info) {
+    $editor = editor_load($format_id);
+    if (!$editor) {
+      continue;
+    }
+
+    // Library.
+    $library = $definitions[$editor->editor]['library'];
+    $attachments['library'][] = array($library['module'], $library['name']);
+
+    // JavaScript settings.
+    $plugin = $manager->createInstance($editor->editor);
+    $settings[$format_id] = array(
+      'editor' => $editor->editor,
+      'editorSettings' => $plugin->generateJsSettings($editor),
+    );
+  }
+
+  // We have all JavaScript settings, allow other modules to alter them.
+  drupal_alter('editor_js_settings', $settings, $formats);
+
+  $attachments['js'][] = array(
+    'type' => 'setting',
+    'data' => array('editor' => array('formats' => $settings)),
+  );
+
+  return $attachments;
+}
diff --git a/core/modules/editor/lib/Drupal/editor/EditorBundle.php b/core/modules/editor/lib/Drupal/editor/EditorBundle.php
new file mode 100644
index 0000000..1e4b86a
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/EditorBundle.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\EditorBundle.
+ */
+
+namespace Drupal\editor;
+
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\HttpKernel\Bundle\Bundle;
+
+/**
+ * Editor dependency injection container.
+ */
+class EditorBundle extends Bundle {
+
+  /**
+   * Overrides Symfony\Component\HttpKernel\Bundle\Bundle::build().
+   */
+  public function build(ContainerBuilder $container) {
+    // Register the plugin manager for our plugin type with the dependency
+    // injection container.
+    $container->register('plugin.manager.editor', 'Drupal\editor\Plugin\EditorManager');
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/Core/Entity/Editor.php b/core/modules/editor/lib/Drupal/editor/Plugin/Core/Entity/Editor.php
new file mode 100644
index 0000000..9a32b80
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Plugin/Core/Entity/Editor.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\editor\Plugin\Core\Entity\Editor.
+ */
+
+namespace Drupal\editor\Plugin\Core\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Annotation\Plugin;
+use Drupal\Core\Annotation\Translation;
+
+/**
+ * Defines the configured text editor entity.
+ *
+ * @Plugin(
+ *   id = "editor",
+ *   label = @Translation("Editor"),
+ *   module = "editor",
+ *   controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
+ *   config_prefix = "editor.editor",
+ *   entity_keys = {
+ *     "id" = "format",
+ *     "label" = "name",
+ *     "editor" = "editor",
+ *     "uuid" = "uuid"
+ *   }
+ * )
+ */
+class Editor extends ConfigEntityBase {
+
+  /**
+   * The text format to which this text editor is bound.
+   *
+   * @var string
+   */
+  public $format;
+
+  /**
+   * The label of the text format to which this text editor is bound.
+   *
+   * @var string
+   */
+  public $name;
+
+  /**
+   * The name of the text editor library.
+   *
+   * @var string
+   */
+  public $editor;
+
+  /**
+   * The array of settings for this text editor.
+   *
+   * @var array
+   */
+  public $settings = array();
+
+  /**
+   * Overrides Drupal\Core\Entity\Entity::id().
+   */
+  public function id() {
+    return $this->format;
+  }
+
+  /**
+   * Overrides Drupal\Core\Entity\Entity::__construct()
+   */
+  public function __construct(array $values, $entity_type) {
+    parent::__construct($values, $entity_type);
+
+    // Initialize settings to the defaults, merging any passed in settings.
+    $manager = drupal_container()->get('plugin.manager.editor');
+    $plugin = $manager->createInstance($this->editor);
+    $this->settings = array_merge($plugin->defaultSettings(), $this->settings);
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/EditorInterface.php b/core/modules/editor/lib/Drupal/editor/Plugin/EditorInterface.php
new file mode 100644
index 0000000..7b0a537
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Plugin/EditorInterface.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\editor\Plugin\EditorInterface.
+ */
+
+namespace Drupal\editor\Plugin;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\editor\Plugin\Core\Entity\Editor;
+
+/**
+ * Defines an interface for configurable text editors.
+ */
+interface EditorInterface extends PluginInspectionInterface {
+
+  /**
+   * Returns the default settings for this configurable text editor.
+   *
+   * @return array
+   *   An array of settings as they would be stored by a configured text editor
+   *   entity (\Drupal\editor\Plugin\Core\Entity\Editor).
+   */
+  function defaultSettings();
+
+  /**
+   * Returns a settings form to configure this text editor.
+   *
+   * Note that this form will be a subform of filter_admin_format_form(). Hence
+   * the Filter module will take care of saving the settings.
+   *
+   * If the editor's behavior depends on an extensive list and/or external data,
+   * then the implementing module can choose to provide a separate, global
+   * configuration page rather than per-text-format settings. In that case, this
+   * form should provide a link to the separate settings page.
+   *
+   * @param array $form
+   *   The prepopulated form array of the filter administration form.
+   * @param array $form_state
+   *   The state of the (entire) configuration form.
+   * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
+   *   A configured text editor object.
+   * @return array
+   *   A render array for the settings form.
+   */
+  function settingsForm(array &$form, array &$form_state, Editor $editor);
+
+  /**
+   * Validates the settings form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  function settingsFormValidate(array $form, array &$form_state);
+
+  /**
+   * Generates JavaScript settings for a configured text editor object.
+   *
+   * Most text editors use JavaScript to provide a WYSIWYG or toolbar on the
+   * client-side interface. This method can be used to convert internal settings
+   * of the text editor into JavaScript variables that will be accessible when
+   * the text editor is loaded.
+   *
+   * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
+   *   A configured text editor object.
+   * @return array
+   *   An array of settings that will be added to the page for use by this text
+   *   editor's JavaScript integration.
+   */
+  function generateJsSettings(Editor $editor);
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php
new file mode 100644
index 0000000..2dab4a7
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Definition of \Drupal\editor\Plugin\EditorManager.
+ */
+
+namespace Drupal\editor\Plugin;
+
+use Drupal\Component\Plugin\PluginManagerBase;
+use Drupal\Component\Plugin\Factory\DefaultFactory;
+use Drupal\Core\Plugin\Discovery\AlterDecorator;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Discovery\CacheDecorator;
+
+/**
+ * Configurable text editor manager.
+ */
+class EditorManager extends PluginManagerBase {
+
+  /**
+   * Overrides \Drupal\Component\Plugin\PluginManagerBase::__construct().
+   */
+  public function __construct() {
+    $this->discovery = new AnnotatedClassDiscovery('editor', 'editor');
+    $this->discovery = new AlterDecorator($this->discovery, 'editor_info');
+    $this->discovery = new CacheDecorator($this->discovery, 'editor');
+    $this->factory = new DefaultFactory($this->discovery);
+  }
+
+  /**
+   * List all available plugin definitions.
+   *
+   * @return array
+   *   An array of available definition titles, keyed by id.
+   */
+  public function listAvailableDefinitions() {
+    return array_reduce($this->getDefinitions(), function($result, $definition) {
+      return $result + array($definition['id'] => $definition['title']);
+    }, array());
+  }
+}
diff --git a/core/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc
index a62f8e3..a53219d 100644
--- a/core/modules/filter/filter.admin.inc
+++ b/core/modules/filter/filter.admin.inc
@@ -152,6 +152,7 @@ function filter_admin_format_form($form, &$form_state, $format) {
     '#title' => t('Name'),
     '#default_value' => $format->name,
     '#required' => TRUE,
+    '#weight' => -20,
   );
   $form['format'] = array(
     '#type' => 'machine_name',
@@ -163,6 +164,7 @@ function filter_admin_format_form($form, &$form_state, $format) {
       'source' => array('name'),
     ),
     '#disabled' => !empty($format->format),
+    '#weight' => -19,
   );
 
   // Add user role access selection.
@@ -171,6 +173,7 @@ function filter_admin_format_form($form, &$form_state, $format) {
     '#title' => t('Roles'),
     '#options' => array_map('check_plain', user_roles()),
     '#disabled' => $is_fallback,
+    '#weight' => -10,
   );
   if ($is_fallback) {
     $form['roles']['#description'] = t('All roles for this text format must be enabled and cannot be changed.');
