diff --git a/core/modules/filter/filter.admin.inc b/core/modules/filter/filter.admin.inc
index 4f56856..634fc32 100644
--- a/core/modules/filter/filter.admin.inc
+++ b/core/modules/filter/filter.admin.inc
@@ -132,10 +132,11 @@ function theme_filter_admin_overview($variables) {
  */
 function filter_admin_format_page($format = NULL) {
   if (!isset($format->name)) {
-    drupal_set_title(t('Add text format'));
+    drupal_set_title(t('Add text editor'));
     $format = (object) array(
       'format' => NULL,
       'name' => '',
+      'editor' => '',
     );
   }
   return drupal_get_form('filter_admin_format_form', $format);
@@ -186,6 +187,20 @@ function filter_admin_format_form($form, &$form_state, $format) {
     '#disabled' => !empty($format->format),
   );
 
+  // Associate an editor with this format.
+  $editors = filter_editors();
+  $editor_options = array('' => t('- None -'));
+  foreach ($editors as $editor_name => $editor) {
+    $editor_options[$editor_name] = $editor['label'];
+  }
+  $form['editor'] = array(
+    '#type' => 'select',
+    '#title' => t('Editor'),
+    '#options' => $editor_options,
+    '#access' => count($editor_options) > 1,
+    '#default_value' => $format->editor,
+  );
+
   // Add user role access selection.
   $form['roles'] = array(
     '#type' => 'checkboxes',
diff --git a/core/modules/filter/filter.api.php b/core/modules/filter/filter.api.php
index f11a528..6d5b14a 100644
--- a/core/modules/filter/filter.api.php
+++ b/core/modules/filter/filter.api.php
@@ -61,6 +61,9 @@
  *     details.
  *   - default settings: An associative array containing default settings for
  *     the filter, to be applied when the filter has not been configured yet.
+ *   - js settings callback: The name of a function that returns configuration
+ *     options that should be added to the page via JavaScript for use on the
+ *     front-end. See hook_filter_FILTER_js_settings() for details.
  *   - prepare callback: The name of a function that escapes the content before
  *     the actual filtering happens. See hook_filter_FILTER_prepare() for
  *     details.
@@ -119,6 +122,73 @@ function hook_filter_info_alter(&$info) {
 }
 
 /**
+ * Define text editors, such as WYSIWYGs or toolbars to assist with text input.
+ *
+ * Text editors are bound to an individual text format. When a format is
+ * activated in a 'text_format' element, the text editor associated with the
+ * format should be activated on the text area.
+ *
+ * @return
+ *   An associative array of editors, whose keys are internal editor names,
+ *   which should be unique and therefore prefixed with the name of the module.
+ *   Each value is an associative array describing the editor, with the
+ *   following elements (all are optional except as noted):
+ *   - title: (required) A human readable name for the editor.
+ *   - settings callback: The name of a function that returns configuration
+ *     form elements for the editor. See hook_editor_EDITOR_settings() for
+ *     details.
+ *   - default settings: An associative array containing default settings for
+ *     the editor, to be applied when the editor has not been configured yet.
+ *   - js settings callback: The name of a function that returns configuration
+ *     options that should be added to the page via JavaScript for use on the
+ *     front-end. See hook_editor_EDITOR_js_settings() for details.
+ *
+ * @see filter_example.module
+ * @see hook_filter_info_alter()
+ */
+function hook_editor_info() {
+  $editors['myeditor'] = array(
+    'title' => t('My Editor'),
+    'settings callback' => '_myeditor_settings',
+    'default settings' => array(
+      'enable_toolbar' => TRUE,
+      'toolbar_buttons' => array('bold', 'italic', 'underline', 'link', 'image'),
+      'resizeable' => TRUE,
+    ),
+    'js settings callback' => '_myeditor_js_settings',
+  );
+  return $editors;
+}
+
+/**
+ * Perform alterations on 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');
+}
+
+/**
+ * Perform alterations on the JavaScript settings that are added for filters.
+ *
+ * Note that changing settings here only affects the front-end behavior of the
+ * filter. To affect the filter globally both on the front-end and back-end, 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 for which a user has access.
+ */
+function hook_filter_js_settings_alter(&$settings) {
+  $settings['full_html']['allowed_tags'][] = 'strong';
+  $settings['full_html']['allowed_tags'][] = 'em';
+  $settings['full_html']['allowed_tags'][] = 'img';
+}
+
+/**
  * @} End of "addtogroup hooks".
  */
 
@@ -272,6 +342,134 @@ function hook_filter_FILTER_tips($filter, $format, $long) {
 }
 
 /**
+ * JavaScript settings callback for hook_filter_info().
+ *
+ * Note: This is not really a hook. The function name is manually specified via
+ * 'js settings callback' in hook_filter_info(), with this recommended callback
+ * name pattern. It is called from filter_get_js_settings().
+ *
+ * Some filters include a JavaScript implementation of their filter that can be
+ * used in editing interfaces. This integration can be used by rich text editors
+ * to provide a better WYSIWYG experience, where the filtering is simulated in
+ * a way that helps the user understand the effects of a filter while editing
+ * content. This callback allows configuration options for a filter to be passed
+ * to the page as a JavaScript setting. As not all settings need to be passed
+ * to the front-end, this function may be used to send only applicable settings.
+ *
+ * @param $filter
+ *   The filter object containing the current settings for the given format,
+ *   in $filter->settings.
+ * @param $format
+ *   The format object which contains the filter.
+ * @param $filters
+ *   The complete list of filter objects that are enabled for the given format.
+ *
+ * @return
+ *   An array of settings that will be added to the page for use by this
+ *   filter's JavaScript integration.
+ */
+function hook_filter_FILTER_js_settings($filter, $format, $filters) {
+  return array(
+    'myFilterSetting' => $filter->settings['my_filter_setting'],
+  );
+}
+
+/**
+ * Settings callback for hook_editor_info().
+ *
+ * Note: This is not really a hook. The function name is manually specified via
+ * 'settings callback' in hook_editor_info(), with this recommended callback
+ * name pattern. It is called from filter_admin_format_form().
+ *
+ * This callback function is used to provide a settings form for editor
+ * settings. This function should return the form elements for the settings; the
+ * Filter module will take care of saving the settings in the database.
+ *
+ * If the editor's behavior depends on an extensive list and/or external data,
+ * then the editor module can choose to provide a separate, global configuration
+ * page rather than per-text-format settings. In that case, the settings
+ * callback function should provide a link to the separate settings page.
+ *
+ * @param $form
+ *   The prepopulated form array of the filter administration form.
+ * @param $form_state
+ *   The state of the (entire) configuration form.
+ * @param $format
+ *   The format object being configured.
+ * @param $defaults
+ *   The default settings for the editor, as defined in 'default settings' in
+ *   hook_editor_info(). These should be combined with $editor->settings to
+ *   define the form element defaults.
+ * @param $filters
+ *   The complete list of filter objects that are enabled for the given format.
+ *
+ * @return
+ *   An array of form elements defining settings for the filter. Array keys
+ *   should match the array keys in $filter->settings and $defaults.
+ */
+function hook_editor_EDITOR_settings($form, &$form_state, $format, $defaults, $filters) {
+  $format->settings += $defaults;
+
+  $elements = array();
+  $elements['enable_toolbar'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Enable toolbar'),
+    '#default_value' => $format->settings['enable_toolbar'],
+  );
+  $elements['buttons'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Enabled buttons'),
+    '#options' => array(
+      'bold' => t('Bold'),
+      'italic' => t('Italic'),
+      'underline' => t('Underline'),
+      'link' => t('Link'),
+      'image' => t('Image'),
+    ),
+    '#default_value' => $format->settings['buttons'],
+  );
+  $elements['resizeable'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Resizeable'),
+    '#default_value' => $format->settings['resizeable'],
+  );
+  return $elements;
+}
+
+/**
+ * JavaScript settings callback for hook_editor_info().
+ *
+ * Note: This is not really a hook. The function name is manually specified via
+ * 'js settings callback' in hook_editor_info(), with this recommended callback
+ * name pattern. It is called from filter_get_js_settings().
+ *
+ * Most editors use JavaScript to provide a WYSIWYG or toolbar on the front-end
+ * interface. This callback can be used to convert internal settings of the
+ * editor into JavaScript variables that will be accessible when the editor
+ * is loaded.
+ *
+ * @param $format
+ *   The format object on which this editor will be used.
+ * @param $filters
+ *   The complete list of filter objects that are enabled for the given format.
+ * @param $existing_settings
+ *   The existing settings that have so far been added to the page, including
+ *   all settings by individual filters. The existing settings added by filters
+ *   can be used to adjust the editor-specific settings.
+ *
+ * @return
+ *   An array of settings that will be added to the page for use by this
+ *   editor's JavaScript integration.
+ */
+function hook_editor_EDITOR_js_settings($format, $filters, $existing_settings) {
+  return array(
+    'toolbar' => $format->settings['enable_toolbar'],
+    'buttons' => $format->settings['buttons'],
+    'resizeable' => $format->settings['resizeable'],
+  );
+}
+
+/**
  * @addtogroup hooks
  * @{
  */
diff --git a/core/modules/filter/filter.install b/core/modules/filter/filter.install
index 9237ad1..01b686a 100644
--- a/core/modules/filter/filter.install
+++ b/core/modules/filter/filter.install
@@ -74,6 +74,13 @@ function filter_schema() {
         'description' => 'Name of the text format (Filtered HTML).',
         'translatable' => TRUE,
       ),
+      'editor' => array(
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => TRUE,
+        'default' => '',
+        'description' => 'Machine name of the text editor associated with this format.',
+      ),
       'cache' => array(
         'type' => 'int',
         'not null' => TRUE,
@@ -95,6 +102,13 @@ function filter_schema() {
         'default' => 0,
         'description' => 'Weight of text format to use when listing.',
       ),
+      'settings' => array(
+        'type' => 'blob',
+        'not null' => FALSE,
+        'size' => 'big',
+        'serialize' => TRUE,
+        'description' => 'A serialized array of name value pairs that store the format and editor settings.',
+      ),
     ),
     'primary key' => array('format'),
     'unique keys' => array(
@@ -147,3 +161,32 @@ function filter_install() {
   // Set the fallback format to plain text.
   variable_set('filter_fallback_format', $plain_text_format->format);
 }
+
+/**
+ * @addtogroup updates-7.x-to-8.x
+ * @{
+ */
+
+/**
+ * Add 'editor' and 'settings' to {filter_format}.
+ */
+function filter_update_8000() {
+  db_add_field('filter_format', 'editor', array(
+    'type' => 'varchar',
+    'length' => 255,
+    'not null' => TRUE,
+    'default' => '',
+    'description' => 'Machine name of the text editor associated with this format.',
+  ));
+  db_add_field('filter_format', 'settings', array(
+    'type' => 'blob',
+    'not null' => FALSE,
+    'size' => 'big',
+    'serialize' => TRUE,
+    'description' => 'A serialized array of name value pairs that store the format and editor settings.',
+  ));
+}
+
+/**
+ * @} End of "addtogroup updates-7.x-to-8.x".
+ */
diff --git a/core/modules/filter/filter.js b/core/modules/filter/filter.js
index 4bc8c18..844adc3 100644
--- a/core/modules/filter/filter.js
+++ b/core/modules/filter/filter.js
@@ -8,6 +8,11 @@
 "use strict";
 
 /**
+ * Initialize an empty object where editors where place their attachment code.
+ */
+Drupal.editors = {};
+
+/**
  * Displays the guidelines of the selected text format automatically.
  */
 Drupal.behaviors.filterGuidelines = {
@@ -24,4 +29,64 @@ Drupal.behaviors.filterGuidelines = {
   }
 };
 
+/**
+ * Enables rich text editors
+ */
+Drupal.behaviors.filterEditors = {
+  attach: function (context, settings) {
+    // If there are no filter settings, there are no editors to enable.
+    if (!settings.filter) {
+      return;
+    }
+
+    var $context = $(context);
+    $context.find('.filter-list:input').once('filterEditors', 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 input format is enabled or there is
+      // only one input format at all.
+      if ($this.is(':input')) {
+        if (Drupal.settings.filter.formats[activeEditor]) {
+          Drupal.filterEditorAttach(field, Drupal.settings.filter.formats[activeEditor]);
+        }
+      }
+      // Attach onChange handlers to input format selector elements.
+      if ($this.is('select')) {
+        $this.change(function() {
+          // Detach the current editor (if any) and attach a new editor.
+          if (Drupal.settings.filter.formats[activeEditor]) {
+            Drupal.filterEditorDetach(field, Drupal.settings.filter.formats[activeEditor]);
+          }
+          activeEditor = $this.val();
+          if (Drupal.settings.filter.formats[activeEditor]) {
+            Drupal.filterEditorAttach(field, Drupal.settings.filter.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.filterEditorDetach(field, Drupal.settings.filter.formats[activeEditor]);
+      });
+    });
+  }
+};
+
+Drupal.filterEditorAttach = function(field, format) {
+  if (format.editor) {
+    Drupal.editors[format.editor].attach(field, format);
+  }
+};
+
+Drupal.filterEditorDetach = function(field, format) {
+  if (format.editor) {
+    Drupal.editors[format.editor].detach(field, format);
+  }
+};
+
 })(jQuery);
diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module
index b55066f..ae78611 100644
--- a/core/modules/filter/filter.module
+++ b/core/modules/filter/filter.module
@@ -58,8 +58,8 @@ function filter_help($path, $arg) {
       return $output;
 
     case 'admin/config/content/formats':
-      $output = '<p>' . t('Text formats define the HTML tags, code, and other formatting that can be used when entering text. <strong>Improper text format configuration is a security risk</strong>. Learn more on the <a href="@filterhelp">Filter module help page</a>.', array('@filterhelp' => url('admin/help/filter'))) . '</p>';
-      $output .= '<p>' . t('Text formats are presented on content editing pages in the order defined on this page. The first format available to a user will be selected by default.') . '</p>';
+      $output = '<p>' . t('Text editors define the HTML tags, code, and other formatting that can be used when entering text. <strong>Improper text editor configuration is a security risk</strong>. Learn more on the <a href="@filterhelp">Filter module help page</a>.', array('@filterhelp' => url('admin/help/filter'))) . '</p>';
+      $output .= '<p>' . t('Text editors are presented on content editing pages in the order defined on this page. The first editor available to a user will be selected by default.') . '</p>';
       return $output;
 
     case 'admin/config/content/formats/%':
@@ -135,8 +135,8 @@ function filter_menu() {
     'file' => 'filter.pages.inc',
   );
   $items['admin/config/content/formats'] = array(
-    'title' => 'Text formats',
-    'description' => 'Configure how content input by users is filtered, including allowed HTML tags. Also allows enabling of module-provided filters.',
+    'title' => 'Text editors and formats',
+    'description' => 'Configure WYSIWYG and text editors on the site. Restrict or allow certain HTML tags to be used in content.',
     'page callback' => 'drupal_get_form',
     'page arguments' => array('filter_admin_overview'),
     'access arguments' => array('administer filters'),
@@ -147,7 +147,7 @@ function filter_menu() {
     'type' => MENU_DEFAULT_LOCAL_TASK,
   );
   $items['admin/config/content/formats/add'] = array(
-    'title' => 'Add text format',
+    'title' => 'Add text editor',
     'page callback' => 'filter_admin_format_page',
     'access arguments' => array('administer filters'),
     'type' => MENU_LOCAL_ACTION,
@@ -245,12 +245,16 @@ function filter_format_save($format) {
   if (!isset($format->weight)) {
     $format->weight = 0;
   }
+  if (!isset($format->editor)) {
+    $format->editor = '';
+  }
 
   // Insert or update the text format.
   $return = db_merge('filter_format')
     ->key(array('format' => $format->format))
     ->fields(array(
       'name' => $format->name,
+      'editor' => $format->editor,
       'cache' => (int) $format->cache,
       'status' => (int) $format->status,
       'weight' => (int) $format->weight,
@@ -494,6 +498,28 @@ function filter_formats_reset() {
 }
 
 /**
+ * Returns a list of text editors that are used with 'filtered_html' elements.
+ */
+function filter_editors() {
+  $editors = &drupal_static(__FUNCTION__, NULL);
+
+  if (!isset($editors)) {
+    $editors = module_invoke_all('editor_info');
+    drupal_alter('editor_info', $editors);
+  }
+
+  return $editors;
+}
+
+/**
+ * Loads an individual editor's information.
+ */
+function filter_editor_load($editor_name) {
+  $editors = filter_editors();
+  return isset($editors[$editor_name]) ? $editors[$editor_name] : FALSE;
+}
+
+/**
  * Retrieves a list of roles that are allowed to use a given text format.
  *
  * @param $format
@@ -792,6 +818,32 @@ function filter_list_format($format_id) {
 }
 
 /**
+ * Get a list of HTML tags and attributes that are allowed by a given text
+ * format, by performing black-box testing.
+ *
+ * @param  string $format_name
+ *   A text format name.
+ * @return array
+ *   An array of which the keys list all allowed tags and the corresponding
+ *   values list the allowed attributes. An empty array as value means no
+ *   attributes are allowed, array('*') means all attributes are allowed. In
+ *   other cases, it's an enumeration of the allowed attributes, plus "data-" if
+ *   any data- attribute is allowed.
+ */
+function filter_allowed_tags($format_name) {
+  $cache_id = 'filter-allowed-tags:' . $format_name;
+  if ($cached = cache()->get($cache_id)) {
+    return $cached->data;
+  }
+
+  module_load_include('inc', 'filter', 'filter.pages');
+  $allowed_tags = _filter_calculate_allowed_tags($format_name);
+  cache()->set($cache_id, $allowed_tags);
+
+  return $allowed_tags;
+}
+
+/**
  * Runs all the enabled filters on a piece of text.
  *
  * Note: Because filters can inject JavaScript or execute PHP code, security is
@@ -961,9 +1013,13 @@ function filter_process_format($element) {
   $element['value']['#type'] = $element['#base_type'];
   $element['value'] += element_info($element['#base_type']);
 
+  // Get a list of formats that the current user has access to.
+  $formats = filter_formats($user);
+
   // Turn original element into a text format wrapper.
   $path = drupal_get_path('module', 'filter');
   $element['#attached']['library'][] = array('filter', 'drupal.filter');
+  filter_add_js($formats);
 
   // Setup child container for the text format widget.
   $element['format'] = array(
@@ -977,8 +1033,6 @@ function filter_process_format($element) {
     '#attributes' => array('class' => array('filter-guidelines')),
     '#weight' => 20,
   );
-  // Get a list of formats that the current user has access to.
-  $formats = filter_formats($user);
   foreach ($formats as $format) {
     $options[$format->format] = $format->name;
     $element['format']['guidelines'][$format->format] = array(
@@ -994,7 +1048,7 @@ function filter_process_format($element) {
 
   $element['format']['format'] = array(
     '#type' => 'select',
-    '#title' => t('Text format'),
+    '#title' => t('Editor'),
     '#options' => $options,
     '#default_value' => $element['#format'],
     '#access' => count($formats) > 1,
@@ -1057,6 +1111,80 @@ function filter_process_format($element) {
 }
 
 /**
+ * Adds filter configuration information to the page for access by JavaScript.
+ *
+ * @param array $formats
+ *   An array of formats as returned by filter_formats(), whose settings should
+ *   be added to the page.
+ * @return NULL
+ *   This function has no return value, the settings are added to the page
+ *   through drupal_add_js().
+ */
+function filter_add_js($formats) {
+  $added = &drupal_static(__FUNCTION__, array());
+
+  foreach ($formats as $format_name => $format_info) {
+    if (isset($added[$format_name])) {
+      unset($formats[$format_name]);
+    }
+    else {
+      // Add the library associated with a format's editor if needed.
+      if ($format_info->editor && ($editor = filter_editor_load($format_info->editor))) {
+        drupal_add_library($editor['library'][0], $editor['library'][1]);
+      }
+      $added[$format_name] = TRUE;
+    }
+  }
+
+  if (!empty($formats)) {
+    $settings = filter_get_js_settings($formats);
+    drupal_add_js(array('filter' => array('formats' => $settings)), 'setting');
+  }
+}
+
+/**
+ * Retrieve JavaScript settings that should be added by each filter.
+ *
+ * @param array $formats
+ *   An array of formats as returned by filter_formats().
+ * @return array
+ *   An array of JavaScript settings representing the configuration of the
+ *   filters.
+ */
+function filter_get_js_settings($formats) {
+  $settings = array();
+  $filter_info = filter_get_filters();
+  $editor_info = filter_editors();
+
+  foreach ($formats as $format_name => $format) {
+    $filter_settings = array();
+    $format_filters = filter_list_format($format_name);
+    foreach ($format_filters as $filter_name => $filter) {
+      if ($filter->status && isset($filter_info[$filter_name]['js settings callback'])) {
+        $function = $filter_info[$filter_name]['js settings callback'];
+        $filter_settings += $function($filter, $format, $format_filters);
+      }
+    }
+    $settings[$format_name] = array(
+      'allowedTags' => filter_allowed_tags($format_name),
+      'className' => drupal_html_class('filter-format-' . $format_name),
+      'filterSettings' => $filter_settings,
+      'editor' => $format->editor,
+      'editorSettings' => array(),
+    );
+
+    if ($format->editor && isset($editor_info[$format->editor]['js settings callback'])) {
+      $function = $editor_info[$format->editor]['js settings callback'];
+      $settings[$format_name]['editorSettings'] = $function($format, $format_filters, $settings);
+    }
+  }
+
+  drupal_alter('filter_js_settings', $settings, $formats);
+
+  return $settings;
+}
+
+/**
  * Render API callback: Hides the field value of 'text_format' elements.
  *
  * To not break form processing and previews if a user does not have access to
@@ -1267,7 +1395,7 @@ function filter_dom_serialize_escape_cdata_element($dom_document, $dom_element,
  * @ingroup themeable
  */
 function theme_filter_tips_more_info() {
-  return '<p>' . l(t('More information about text formats'), 'filter/tips', array('attributes' => array('target' => '_blank'))) . '</p>';
+  return '<p>' . l(t('More information about text editors'), 'filter/tips', array('attributes' => array('target' => '_blank'))) . '</p>';
 }
 
 /**
@@ -1324,6 +1452,7 @@ function filter_filter_info() {
     'type' => FILTER_TYPE_MARKUP_LANGUAGE,
     'process callback' => '_filter_url',
     'settings callback' => '_filter_url_settings',
+    'js settings callback' => '_filter_url_js_settings',
     'default settings' => array(
       'filter_url_length' => 72,
     ),
@@ -1522,6 +1651,15 @@ function _filter_url_settings($form, &$form_state, $filter, $format, $defaults)
 }
 
 /**
+ * Filter URL JS settings callback: return settings for JavaScript.
+ */
+function _filter_url_js_settings($filter, $format, $defaults) {
+  return array(
+    'filterUrlLength' => $filter->settings['filter_url_length'],
+  );
+}
+
+/**
  * Converts text into hyperlinks automatically.
  *
  * This filter identifies and makes clickable three types of "links".
@@ -1955,11 +2093,14 @@ function filter_library_info() {
       array('system', 'drupal.form'),
     ),
   );
+
+  // The filter.js file is added as a library so all editors can extend the
+  // Drupal.editors global object.
   $libraries['drupal.filter'] = array(
     'title' => 'Filter',
     'version' => VERSION,
     'js' => array(
-      drupal_get_path('module', 'filter') . '/filter.js' => array(),
+      drupal_get_path('module', 'filter') . '/filter.js' => array('group' => JS_LIBRARY, 'weight' => -1),
     ),
     'css' => array(
       drupal_get_path('module', 'filter') . '/filter.admin.css'
diff --git a/core/modules/filter/filter.pages.inc b/core/modules/filter/filter.pages.inc
index dec59c3..b6781ee 100644
--- a/core/modules/filter/filter.pages.inc
+++ b/core/modules/filter/filter.pages.inc
@@ -85,3 +85,144 @@ function theme_filter_tips($variables) {
 
   return $output;
 }
+
+/**
+ * Provides the test cases used by _filter_calculate_allowed_tags().
+ */
+function _filter_allowed_tags_tests() {
+  return array(
+    array(
+      'tag' => 'p',
+      // For a single <p> that would be stripped, filter_autop would generate
+      // it again, making it impossible to detect that filter_html actually
+      // stripped it away. Hence we need to use two adjacent <p> tags.
+      'testcase' => '<p>Paragraph 1</p><p>Paragraph 2</p>',
+      'attributes' => array(
+        'prefix' => '<p ',
+        'suffix' => '>Paragraph 1</p>',
+        'names' => array('dir'),
+      ),
+    ),
+    array(
+      'tag' => 'blockquote',
+      'testcase' => '<blockquote>Quoted content</blockquote>',
+      'attributes' => array(
+        'prefix' => '<blockquote ',
+        'suffix' => '>Quoted content</blockquote>',
+        'names' => array('cite'),
+      ),
+    ),
+    array('tag' => 'h1', 'testcase' => '<h1>Heading 1</h1>'),
+    array('tag' => 'h2', 'testcase' => '<h2>Heading 2</h2>'),
+    array('tag' => 'h3', 'testcase' => '<h3>Heading 3</h3>'),
+    array('tag' => 'h4', 'testcase' => '<h4>Heading 4</h4>'),
+    array('tag' => 'h5', 'testcase' => '<h5>Heading 5</h5>'),
+    array('tag' => 'h6', 'testcase' => '<h6>Heading 6</h6>'),
+    array('tag' => 'ul', 'testcase' => '<ul><li>A</li><li>B</li></ul>'),
+    array('tag' => 'ol', 'testcase' => '<ol><li>A</li><li>B</li></ol>'),
+    array('tag' => 'li', 'testcase' => '<li>A</li>'),
+    array('tag' => 'pre', 'testcase' => '<pre>Preformatted text</pre>'),
+    // Text-level elements. (Should all be wrapped in <p> tags.)
+    array(
+      'tag' => 'a',
+      'testcase' => '<p><a>hyperlink</a></p>',
+      'attributes' => array(
+        'prefix' => '<p><a ',
+        'suffix' => '>hyperlink</a></p>',
+        'names' => array('href', 'target', 'rel', 'media'),
+      ),
+    ),
+    array('tag' => 'em', 'testcase' => '<p><em>emphasized</em></p>'),
+    array('tag' => 'strong', 'testcase' => '<p><strong>strong</strong></p>'),
+    array('tag' => 'i', 'testcase' => '<p><i>italicized</i></p>'),
+    array('tag' => 'b', 'testcase' => '<p><b>bold</b></p>'),
+    array('tag' => 'u', 'testcase' => '<p><u>underlined</u></p>'),
+    array('tag' => 'cite', 'testcase' => '<p><cite>He</cite> said "Wow"</p>'),
+    array('tag' => 'q', 'testcase' => '<p>He said <q>"Wow"</q></p>'),
+    array('tag' => 'br', 'testcase' => '<p>First line<br />Second line</p>'),
+    array(
+      'tag' => 'code',
+      'testcase' => '<p><code>Hello, world!</code></p>',
+      'attributes' => array(
+        'prefix' => '<p><code ',
+        'suffix' => '>Hello, world!</code></p>',
+        'names' => array('lang'),
+      ),
+    ),
+    array(
+      'tag' => 'img',
+      'testcase' => '<p><img /></p>',
+      'attributes' => array(
+        'prefix' => '<p><img ',
+        'suffix' => ' /></p>',
+        'names' => array('src', 'alt', 'title')
+      ),
+    ),
+  );
+}
+
+/**
+ * Returns list of allowed tags for a particular format. Do not call directly.
+ *
+ * @see filter_allowed_tag_list().
+ */
+function _filter_calculate_allowed_tags($format_name) {
+  // For testing whether any attribute is allowed.
+  $any = 'extremelyUnlikelyAttributeName';
+
+  // For testing whether data- attributes are allowed.
+  $data = 'data-' . chr(mt_rand(65, 90));
+
+  // Helper function to assert the filtered text still contains the original.
+  $assert_match = function ($original, $format_name) {
+    $filtered = check_markup($original, $format_name);
+    $result = preg_replace("/>\s+</", "><", $filtered);
+    return stripos($result, $original) !== FALSE;
+  };
+
+  // Run all the above tagtests to determine which tags and attributes are
+  // allowed in the given text format.
+  $allowed_tags = array_reduce(
+    _filter_allowed_tags_tests(),
+    function($result, $tagtest) use ($assert_match, $format_name, $any, $data) {
+      $tag = $tagtest['tag'];
+      if ($assert_match($tagtest['testcase'], $format_name)) {
+        $result[$tag] = array();
+
+        // Break early if there are no attributes to test.
+        if (!isset($tagtest['attributes'])) {
+          return $result;
+        }
+
+        // Anonymous function to generate attribute test cases.
+        $generate_attr_testcase = function($attr) use ($tagtest) {
+          $case = '';
+          $case .= $tagtest['attributes']['prefix'];
+          $case .= $attr . '="' . $attr . ' attribute test"';
+          $case .= $tagtest['attributes']['suffix'];
+          return $case;
+        };
+
+        // The tag test passed, then we can also do the attribute tests. First,
+        // check if any attribute is allowed, if that's not the case, then
+        // figure out which specific attributes are allowed.
+        if ($assert_match($generate_attr_testcase($any), $format_name)) {
+          $result[$tag][] = '*';
+        }
+        else {
+          $attrs = array_merge(array($data), $tagtest['attributes']['names']);
+          foreach ($attrs as $attr) {
+            if ($assert_match($generate_attr_testcase($attr), $format_name)) {
+              $result[$tag][] = ($attr === $data) ? 'data-' : $attr;
+            }
+          }
+        }
+      }
+
+      return $result;
+    },
+    array()
+  );
+
+  return $allowed_tags;
+}
diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterAdminTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterAdminTest.php
index 9a09a76..f4e2043 100644
--- a/core/modules/filter/lib/Drupal/filter/Tests/FilterAdminTest.php
+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterAdminTest.php
@@ -37,9 +37,9 @@ function setUp() {
   }
 
   function testFormatAdmin() {
-    // Add text format.
+    // Add text editor.
     $this->drupalGet('admin/config/content/formats');
-    $this->clickLink('Add text format');
+    $this->clickLink('Add text editor');
     $format_id = drupal_strtolower($this->randomName());
     $name = $this->randomName();
     $edit = array(
diff --git a/core/modules/filter/lib/Drupal/filter/Tests/FilterFormatAccessTest.php b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormatAccessTest.php
index 4636610..2408687 100644
--- a/core/modules/filter/lib/Drupal/filter/Tests/FilterFormatAccessTest.php
+++ b/core/modules/filter/lib/Drupal/filter/Tests/FilterFormatAccessTest.php
@@ -228,7 +228,7 @@ function testFormatWidgetPermissions() {
     $new_title = $this->randomName(8);
     $edit = array('title' => $new_title);
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
-    $this->assertText(t('!name field is required.', array('!name' => t('Text format'))), 'Error message is displayed.');
+    $this->assertText(t('!name field is required.', array('!name' => t('Editor'))), 'Error message is displayed.');
     $this->drupalGet('node/' . $node->nid);
     $this->assertText($old_title, 'Old title found.');
     $this->assertNoText($new_title, 'New title not found.');
@@ -262,7 +262,7 @@ function testFormatWidgetPermissions() {
     $new_title = $this->randomName(8);
     $edit = array('title' => $new_title);
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
-    $this->assertText(t('!name field is required.', array('!name' => t('Text format'))), 'Error message is displayed.');
+    $this->assertText(t('!name field is required.', array('!name' => t('Editor'))), 'Error message is displayed.');
     $this->drupalGet('node/' . $node->nid);
     $this->assertText($old_title, 'Old title found.');
     $this->assertNoText($new_title, 'New title not found.');
