From 3d64cb5ecae7c0d093e1343f87901769dc7d819e Mon Sep 17 00:00:00 2001
From: Dries Buytaert <dries@buytaert.net>
Date: Fri, 12 Jun 2009 08:39:40 +0000
Subject: [PATCH] - Patch #372743 by bjaspan, yched, KarenS, catch et al: node
 body and teasers as fields. Oh, my.

---
 CHANGELOG.txt                               |   2 +
 modules/aggregator/aggregator.pages.inc     |   8 +-
 modules/aggregator/aggregator.test          |   2 +-
 modules/blog/blog.module                    |  10 +-
 modules/blog/blog.test                      |   2 +-
 modules/blogapi/blogapi.module              |  22 +-
 modules/book/book.module                    |   4 +-
 modules/book/book.test                      |   5 +-
 modules/dblog/dblog.test                    |   4 +-
 modules/field/field.crud.inc                |   4 +-
 modules/field/field.test                    |   7 +-
 modules/field/modules/list/list.info        |   1 +
 modules/field/modules/number/number.info    |   1 +
 modules/field/modules/options/options.info  |   1 +
 modules/field/modules/text/text.info        |   1 +
 modules/field/modules/text/text.module      | 444 +++++++++++++++++---
 modules/field/modules/text/text.test        | 153 ++++++-
 modules/field/theme/field.css               |   8 +-
 modules/filter/filter.admin.inc             |   4 -
 modules/filter/filter.test                  |   4 +-
 modules/forum/forum.module                  |   2 -
 modules/forum/forum.test                    |   4 +-
 modules/help/help.test                      |  21 +-
 modules/locale/locale.test                  |   2 +-
 modules/node/node.api.php                   |   3 +-
 modules/node/node.install                   | 134 +++++-
 modules/node/node.module                    | 355 +++++-----------
 modules/node/node.pages.inc                 |  82 +---
 modules/node/node.test                      | 187 +--------
 modules/path/path.test                      |   2 +-
 modules/php/php.test                        |   4 +-
 modules/search/search.api.php               |  31 +-
 modules/search/search.test                  |   4 +-
 modules/simpletest/drupal_web_test_case.php |  32 +-
 modules/simpletest/tests/common.test        |   4 +-
 modules/simpletest/tests/module.test        |  33 +-
 modules/system/system.install               |  11 +
 modules/system/system.test                  |   6 +-
 modules/taxonomy/taxonomy.test              |   4 +-
 modules/translation/translation.module      |   1 -
 modules/translation/translation.test        |  10 +-
 modules/trigger/trigger.test                |   4 +-
 42 files changed, 920 insertions(+), 703 deletions(-)

diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 798c55977481..5ce66c7c9215 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -110,6 +110,8 @@ Drupal 7.0, xxxx-xx-xx (development version)
       for RDFa support.
 - Field API:
     * Custom data fields may be attached to nodes and users in Drupal.
+    * Node bodies and teasers are now Field API fields instead of
+      being a hard-coded property of node objects.
     * In addition, any other object type may register with Field API
       and allow custom data fields to be attached to itself.
     * Provides a subset of the features of the Content Construction
diff --git a/modules/aggregator/aggregator.pages.inc b/modules/aggregator/aggregator.pages.inc
index 5c5d0c70c41f..3884f509fb96 100644
--- a/modules/aggregator/aggregator.pages.inc
+++ b/modules/aggregator/aggregator.pages.inc
@@ -378,11 +378,11 @@ function theme_aggregator_page_rss($feeds, $category = NULL) {
   foreach ($feeds as $feed) {
     switch ($feed_length) {
       case 'teaser':
-        $teaser = node_teaser($feed->description, NULL, variable_get('aggregator_teaser_length', 600));
-        if ($teaser != $feed->description) {
-          $teaser .= '<p><a href="' . check_url($feed->link) . '">' . t('read more') . "</a></p>\n";
+        $summary = text_summary($feed->description, NULL, variable_get('aggregator_teaser_length', 600));
+        if ($summary != $feed->description) {
+          $summary .= '<p><a href="' . check_url($feed->link) . '">' . t('read more') . "</a></p>\n";
         }
-        $feed->description = $teaser;
+        $feed->description = $summary;
         break;
       case 'title':
         $feed->description = '';
diff --git a/modules/aggregator/aggregator.test b/modules/aggregator/aggregator.test
index aa9e1db59520..4abef3721cdd 100644
--- a/modules/aggregator/aggregator.test
+++ b/modules/aggregator/aggregator.test
@@ -252,7 +252,7 @@ EOF;
     for ($i = 0; $i < 5; $i++) {
       $edit = array();
       $edit['title'] = $this->randomName();
-      $edit['body'] = $this->randomName();
+      $edit['body[0][value]'] = $this->randomName();
       $this->drupalPost('node/add/article', $edit, t('Save'));
     }
   }
diff --git a/modules/blog/blog.module b/modules/blog/blog.module
index d4e1171b1d94..900e0930ab85 100644
--- a/modules/blog/blog.module
+++ b/modules/blog/blog.module
@@ -73,12 +73,7 @@ function blog_help($path, $arg) {
  * Implement hook_form().
  */
 function blog_form($node, $form_state) {
-  global $nid;
-  $type = node_type_get_type($node);
-
-  $form['title'] = array('#type' => 'textfield', '#title' => check_plain($type->title_label), '#required' => TRUE, '#default_value' => !empty($node->title) ? $node->title : NULL, '#weight' => -5);
-  $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
-  return $form;
+  return node_content_form($node, $form_state);
 }
 
 /**
@@ -89,8 +84,7 @@ function blog_view($node, $teaser) {
     // Breadcrumb navigation.
     drupal_set_breadcrumb(array(l(t('Home'), NULL), l(t('Blogs'), 'blog'), l(t("!name's blog", array('!name' => $node->name)), 'blog/' . $node->uid)));
   }
-
-  return node_prepare($node, $teaser);
+  return $node;
 }
 
 /**
diff --git a/modules/blog/blog.test b/modules/blog/blog.test
index 8b231d878b03..a59a63cf768e 100644
--- a/modules/blog/blog.test
+++ b/modules/blog/blog.test
@@ -154,7 +154,7 @@ class BlogTestCase extends DrupalWebTestCase {
       // Edit blog node.
       $edit = array();
       $edit['title'] = 'node/' . $node->nid;
-      $edit['body'] = $this->randomName(256);
+      $edit['body[0][value]'] = $this->randomName(256);
       $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
       $this->assertRaw(t('Blog entry %title has been updated.', array('%title' => $edit['title'])), t('Blog node was edited'));
 
diff --git a/modules/blogapi/blogapi.module b/modules/blogapi/blogapi.module
index 52ebbb0ebc41..ef8e2269307e 100644
--- a/modules/blogapi/blogapi.module
+++ b/modules/blogapi/blogapi.module
@@ -213,7 +213,7 @@ function blogapi_blogger_new_post($appkey, $blogid, $username, $password, $conte
   }
   else {
     $edit['title'] = blogapi_blogger_title($content);
-    $edit['body'] = $content;
+    $edit['body'][0]['value'] = $content;
   }
 
   if (!node_access('create', $edit['type'])) {
@@ -274,12 +274,12 @@ function blogapi_blogger_edit_post($appkey, $postid, $username, $password, $cont
   // Check for bloggerAPI vs. metaWeblogAPI.
   if (is_array($content)) {
     $node->title = $content['title'];
-    $node->body = $content['description'];
+    $node->body[0]['value'] = $content['description'];
     _blogapi_mt_extra($node, $content);
   }
   else {
     $node->title = blogapi_blogger_title($content);
-    $node->body = $content;
+    $node->body[0]['value'] = $content;
   }
 
   module_invoke_all('node_blogapi_edit', $node);
@@ -379,7 +379,7 @@ function blogapi_blogger_get_recent_posts($appkey, $blogid, $username, $password
   }
 
   if ($bodies) {
-    $result = db_query_range("SELECT n.nid, n.title, r.body, r.format, n.comment, n.created, u.name FROM {node} n, {node_revision} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC",  array(
+    $result = db_query_range("SELECT n.nid, n.title, n.comment, n.created, u.name FROM {node} n, {node_revision} r, {users} u WHERE n.uid = u.uid AND n.vid = r.vid AND n.type = :type AND n.uid = :uid ORDER BY n.created DESC",  array(
       ':type' => $blogid,
       ':uid' => $user->uid
     ), 0, $number_of_posts);
@@ -892,14 +892,14 @@ function _blogapi_mt_extra($node, $struct) {
 
   // Merge the 3 body sections (description, mt_excerpt, mt_text_more) into one body.
   if ($struct['mt_excerpt']) {
-    $node->body = $struct['mt_excerpt'] . '<!--break-->' . $node->body;
+    $node->body[0]['value'] = $struct['mt_excerpt'] . '<!--break-->' . $node->body[0]['value'];
   }
   if ($struct['mt_text_more']) {
-    $node->body = $node->body . '<!--extended-->' . $struct['mt_text_more'];
+    $node->body[0]['value'] = $node->body[0]['value'] . '<!--extended-->' . $struct['mt_text_more'];
   }
 
   if ($struct['mt_convert_breaks']) {
-    $node->format = $struct['mt_convert_breaks'];
+    $node->body[0]['format'] = $struct['mt_convert_breaks'];
   }
 
   if ($struct['dateCreated']) {
@@ -922,17 +922,19 @@ function _blogapi_get_post($node, $bodies = TRUE) {
   );
 
   if ($bodies) {
+    $body = $node->body[0]['value'];
+    $format = $node->body[0]['format'];
     if ($node->comment == 1) {
       $comment = 2;
     }
     elseif ($node->comment == 2) {
       $comment = 1;
     }
-    $xmlrpcval['content'] = "<title>$node->title</title>$node->body";
-    $xmlrpcval['description'] = $node->body;
+    $xmlrpcval['content'] = "<title>$node->title</title>$body";
+    $xmlrpcval['description'] = $body;
     // Add MT specific fields
     $xmlrpcval['mt_allow_comments'] = (int) $comment;
-    $xmlrpcval['mt_convert_breaks'] = $node->format;
+    $xmlrpcval['mt_convert_breaks'] = $format;
   }
 
   return $xmlrpcval;
diff --git a/modules/book/book.module b/modules/book/book.module
index e70b824a2fb5..c17e45199de9 100644
--- a/modules/book/book.module
+++ b/modules/book/book.module
@@ -1037,7 +1037,7 @@ function book_export_traverse($tree, $visit_func) {
 function book_node_export($node, $children = '') {
   $node->build_mode = NODE_BUILD_PRINT;
   $node = node_build_content($node, FALSE, FALSE);
-  $node->body = drupal_render($node->content);
+  $node->rendered = drupal_render($node->content);
 
   return theme('book_node_export_html', $node, $children);
 }
@@ -1054,7 +1054,7 @@ function book_node_export($node, $children = '') {
 function template_preprocess_book_node_export_html(&$variables) {
   $variables['depth'] = $variables['node']->book['depth'];
   $variables['title'] = check_plain($variables['node']->title);
-  $variables['content'] = $variables['node']->body;
+  $variables['content'] = $variables['node']->rendered;
 }
 
 /**
diff --git a/modules/book/book.test b/modules/book/book.test
index 8290f6f17d16..1c8ac0c32014 100644
--- a/modules/book/book.test
+++ b/modules/book/book.test
@@ -141,8 +141,7 @@ class BookTestCase extends DrupalWebTestCase {
     // Check printer friendly version.
     $this->drupalGet('book/export/html/' . $node->nid);
     $this->assertText($node->title, t('Printer friendly title found.'));
-    $node->body = str_replace('<!--break-->', '', $node->body);
-    $this->assertRaw(check_markup($node->body, $node->format), t('Printer friendly body found.'));
+    $this->assertRaw(check_markup($node->body[0]['value'], $node->body[0]['format']), t('Printer friendly body found.'));
 
     $number++;
   }
@@ -174,7 +173,7 @@ class BookTestCase extends DrupalWebTestCase {
 
     $edit = array();
     $edit['title'] = $number . ' - SimpleTest test node ' . $this->randomName(10);
-    $edit['body'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32);
+    $edit['body[0][value]'] = 'SimpleTest test body ' . $this->randomName(32) . ' ' . $this->randomName(32);
     $edit['book[bid]'] = $book_nid;
 
     if ($parent !== NULL) {
diff --git a/modules/dblog/dblog.test b/modules/dblog/dblog.test
index 644ae63c7778..d411478043a1 100644
--- a/modules/dblog/dblog.test
+++ b/modules/dblog/dblog.test
@@ -327,7 +327,7 @@ class DBLogTestCase extends DrupalWebTestCase {
       default:
         $content = array(
           'title' => $this->randomName(8),
-          'body' => $this->randomName(32),
+          'body[0][value]' => $this->randomName(32),
         );
       break;
     }
@@ -351,7 +351,7 @@ class DBLogTestCase extends DrupalWebTestCase {
 
       default:
         $content = array(
-          'body' => $this->randomName(32),
+          'body[0][value]' => $this->randomName(32),
         );
       break;
     }
diff --git a/modules/field/field.crud.inc b/modules/field/field.crud.inc
index 97b3d8a7076b..956122ecfab5 100644
--- a/modules/field/field.crud.inc
+++ b/modules/field/field.crud.inc
@@ -425,7 +425,9 @@ function field_create_instance($instance) {
   // Set the field id.
   $instance['field_id'] = $field['id'];
 
-  // TODO: Check that the specifed bundle exists.
+  // Note that we do *not* prevent creating a field on non-existing bundles,
+  // because that would break the 'Body as field' upgrade for contrib
+  // node types.
 
   // TODO: Check that the widget type is known and can handle the field type ?
   // TODO: Check that the formatters are known and can handle the field type ?
diff --git a/modules/field/field.test b/modules/field/field.test
index c4aee219bb0e..07d047f0650a 100644
--- a/modules/field/field.test
+++ b/modules/field/field.test
@@ -930,10 +930,9 @@ class FieldInfoTestCase extends DrupalWebTestCase {
       $this->assertEqual($info[$w_key]['module'], 'field_test',  t("Widget type field_test module appears"));
     }
 
-    // Verify that no fields or instances exist
-    $fields = field_info_fields();
+    // Verify that no unexpected instances exist.
+    $core_fields = field_info_fields();
     $instances = field_info_instances(FIELD_TEST_BUNDLE);
-    $this->assertTrue(empty($fields), t('With no fields, info fields is empty.'));
     $this->assertTrue(empty($instances), t('With no instances, info bundles is empty.'));
 
     // Create a field, verify it shows up.
@@ -943,7 +942,7 @@ class FieldInfoTestCase extends DrupalWebTestCase {
     );
     field_create_field($field);
     $fields = field_info_fields();
-    $this->assertEqual(count($fields), 1, t('One field exists'));
+    $this->assertEqual(count($fields), count($core_fields) + 1, t('One new field exists'));
     $this->assertEqual($fields[$field['field_name']]['field_name'], $field['field_name'], t('info fields contains field name'));
     $this->assertEqual($fields[$field['field_name']]['type'], $field['type'], t('info fields contains field type'));
     $this->assertEqual($fields[$field['field_name']]['module'], 'field_test', t('info fields contains field module'));
diff --git a/modules/field/modules/list/list.info b/modules/field/modules/list/list.info
index 1e201a386ce6..96a98d500d6b 100644
--- a/modules/field/modules/list/list.info
+++ b/modules/field/modules/list/list.info
@@ -5,3 +5,4 @@ package = Core - fields
 version = VERSION
 core = 7.x
 files[]=list.module
+required = TRUE
diff --git a/modules/field/modules/number/number.info b/modules/field/modules/number/number.info
index e77fa7959c7c..314506807026 100644
--- a/modules/field/modules/number/number.info
+++ b/modules/field/modules/number/number.info
@@ -5,3 +5,4 @@ package = Core - fields
 version = VERSION
 core = 7.x
 files[]=number.module
+required = TRUE
diff --git a/modules/field/modules/options/options.info b/modules/field/modules/options/options.info
index d61d108b80f3..04d53c422b5b 100644
--- a/modules/field/modules/options/options.info
+++ b/modules/field/modules/options/options.info
@@ -5,3 +5,4 @@ package = Core - fields
 version = VERSION
 core = 7.x
 files[]=options.module
+required = TRUE
diff --git a/modules/field/modules/text/text.info b/modules/field/modules/text/text.info
index 17a6760254b8..6e8bb24ee1e5 100644
--- a/modules/field/modules/text/text.info
+++ b/modules/field/modules/text/text.info
@@ -6,3 +6,4 @@ version = VERSION
 core = 7.x
 files[] = text.module
 files[] = text.test
+required = TRUE
\ No newline at end of file
diff --git a/modules/field/modules/text/text.module b/modules/field/modules/text/text.module
index 7c31d54cb0a3..00ba52017870 100644
--- a/modules/field/modules/text/text.module
+++ b/modules/field/modules/text/text.module
@@ -14,6 +14,9 @@ function text_theme() {
     'text_textarea' => array(
       'arguments' => array('element' => NULL),
     ),
+    'text_textarea_with_summary' => array(
+      'arguments' => array('element' => NULL),
+    ),
     'text_textfield' => array(
       'arguments' => array('element' => NULL),
     ),
@@ -26,11 +29,22 @@ function text_theme() {
     'field_formatter_text_trimmed' => array(
       'arguments' => array('element' => NULL),
     ),
+    'field_formatter_text_summary_or_trimmed' => array(
+      'arguments' => array('element' => NULL),
+    ),
   );
 }
 
 /**
  * Implement hook_field_info().
+ *
+ * Field settings:
+ *   - max_length: the maximum length for a varchar field.
+ * Instance settings:
+ *   - text_processing: whether text input filters should be used.
+ *   - display_summary: whether the summary field should be displayed.
+ *     When empty and not displayed the summary will take its value from the
+ *     trimmed value of the main text field.
  */
 function text_field_info() {
   return array(
@@ -39,18 +53,25 @@ function text_field_info() {
       'description' => t('This field stores varchar text in the database.'),
       'settings' => array('max_length' => 255),
       'instance_settings' => array('text_processing' => 0),
-      'widget_settings' => array('size' => 60),
       'default_widget' => 'text_textfield',
       'default_formatter' => 'text_default',
     ),
     'text_long' => array(
       'label' => t('Long text'),
       'description' => t('This field stores long text in the database.'),
+      'settings' => array('max_length' => ''),
       'instance_settings' => array('text_processing' => 0),
-      'widget_settings' => array('rows' => 5),
       'default_widget' => 'text_textarea',
       'default_formatter' => 'text_default',
     ),
+    'text_with_summary' => array(
+      'label' => t('Long text and summary'),
+      'description' => t('This field stores long text in the database along with optional summary text.'),
+      'settings' => array('max_length' => ''),
+      'instance_settings' => array('text_processing' => 1, 'display_summary' => 0),
+      'default_widget' => 'text_textarea_with_summary',
+      'default_formatter' => 'text_summary_or_trimmed',
+    ),
   );
 }
 
@@ -58,23 +79,39 @@ function text_field_info() {
  * Implement hook_field_schema().
  */
 function text_field_schema($field) {
-  if ($field['type'] == 'text_long') {
-    $columns = array(
-      'value' => array(
-        'type' => 'text',
-        'size' => 'big',
-        'not null' => FALSE,
-      ),
-    );
-  }
-  else {
-    $columns = array(
-      'value' => array(
-        'type' => 'varchar',
-        'length' => $field['settings']['max_length'],
-        'not null' => FALSE,
-      ),
-    );
+  switch ($field['type']) {
+    case 'text':
+      $columns = array(
+        'value' => array(
+          'type' => 'varchar',
+          'length' => $field['settings']['max_length'],
+          'not null' => FALSE,
+        ),
+      );
+      break;
+    case 'text_long':
+      $columns = array(
+        'value' => array(
+          'type' => 'text',
+          'size' => 'big',
+          'not null' => FALSE,
+        ),
+      );
+      break;
+    case 'text_with_summary':
+      $columns = array(
+        'value' => array(
+          'type' => 'text',
+          'size' => 'big',
+          'not null' => FALSE,
+        ),
+        'summary' => array(
+          'type' => 'text',
+          'size' => 'big',
+          'not null' => FALSE,
+        ),
+      );
+      break;
   }
   $columns += array(
     'format' => array(
@@ -95,16 +132,27 @@ function text_field_schema($field) {
  * Implement hook_field_validate().
  *
  * Possible error codes:
- * - 'text_max_length': The value exceeds the maximum length.
+ * - 'text_value_max_length': The value exceeds the maximum length.
+ * - 'text_summary_max_length': The summary exceeds the maximum length.
  */
 function text_field_validate($obj_type, $object, $field, $instance, $items, &$errors) {
   foreach ($items as $delta => $item) {
-    if (!empty($item['value'])) {
-      if (!empty($field['settings']['max_length']) && drupal_strlen($item['value']) > $field['settings']['max_length']) {
-        $errors[$field['field_name']][$delta][] = array(
-          'error' => 'text_max_length',
-          'message' => t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length'])),
-        );
+    foreach (array('value' => t('full text'), 'summary' => t('summary')) as $column => $desc) {
+      if (!empty($item[$column])) {
+        if (!empty($field['settings']['max_length']) && drupal_strlen($item[$column]) > $field['settings']['max_length']) {
+          switch ($column) {
+            case 'value':
+              $message = t('%name: the text may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
+              break;
+            case 'summary':
+              $message = t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']));
+              break;
+          }
+          $errors[$field['field_name']][$delta][] = array(
+            'error' => "text_{$column}_length",
+            'message' => $message,
+          );
+        }
       }
     }
   }
@@ -126,17 +174,22 @@ function text_field_load($obj_type, $objects, $field, $instances, &$items) {
       if (!empty($instances[$id]['settings']['text_processing'])) {
         // Only process items with a cacheable format, the rest will be
         // handled by text_field_sanitize().
-        if (filter_format_allowcache($item['format'])) {
+        $format = $item['format'];
+        if (filter_format_allowcache($format)) {
           // TODO D7 : this code is really node-related.
           $check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW);
-          $text = isset($item['value']) ? check_markup($item['value'], $item['format'], isset($object->language) ? $object->language : $language->language, $check, FALSE) : '';
+          $lang = isset($object->language) ? $object->language : $language->language;
+          $items[$id][$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, $check, FALSE) : '';
+          if ($field['type'] == 'text_with_summary') {
+            $items[$id][$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, $check, FALSE) : '';
+          }
         }
       }
       else {
-        $text = check_plain($item['value']);
-      }
-      if (isset($text)) {
-        $items[$id][$delta]['safe'] = $text;
+        $items[$id][$delta]['safe'] = check_plain($item['value']);
+        if ($field['type'] == 'text_with_summary') {
+          $items[$id][$delta]['safe_summary'] = check_plain($item['summary']);
+        }
       }
     }
   }
@@ -155,14 +208,21 @@ function text_field_sanitize($obj_type, $object, $field, $instance, &$items) {
     // from a form preview.
     if (!isset($items[$delta]['safe'])) {
       if (!empty($instance['settings']['text_processing'])) {
+        $format = $item['format'];
         // TODO D7 : this code is really node-related.
         $check = is_null($object) || (isset($object->build_mode) && $object->build_mode == NODE_BUILD_PREVIEW);
-        $text = isset($item['value']) ? check_markup($item['value'], $item['format'], isset($object->language) ? $object->language : $language->language, $check) : '';
+        $lang = isset($object->language) ? $object->language : $language->language;
+        $items[$delta]['safe'] = isset($item['value']) ? check_markup($item['value'], $format, $lang, $check) : '';
+        if ($field['type'] == 'text_with_summary') {
+          $items[$delta]['safe_summary'] = isset($item['summary']) ? check_markup($item['summary'], $format, $lang, $check) : '';
+        }
       }
       else {
-        $text = check_plain($item['value']);
+        $item[$delta]['safe'] = check_plain($item['value']);
+        if ($field['type'] == 'text_with_summary') {
+          $item[$delta]['safe_summary'] = check_plain($item['summary']);
+        }
       }
-      $items[$delta]['safe'] = $text;
     }
   }
 }
@@ -184,21 +244,39 @@ function text_field_formatter_info() {
   return array(
     'text_default' => array(
       'label' => t('Default'),
-      'field types' => array('text', 'text_long'),
+      'field types' => array('text', 'text_long', 'text_with_summary'),
       'behaviors' => array(
         'multiple values' => FIELD_BEHAVIOR_DEFAULT,
       ),
     ),
     'text_plain' => array(
       'label' => t('Plain text'),
-      'field types' => array('text', 'text_long'),
+      'field types' => array('text', 'text_long', 'text_with_summary'),
       'behaviors' => array(
         'multiple values' => FIELD_BEHAVIOR_DEFAULT,
       ),
     ),
+
+    // The text_trimmed formatter displays the trimmed version of the
+    // full element of the field.  It is intended to be used with text
+    // and text_long fields.  It also works with text_with_summary
+    // fields though the text_summary_or_trimmed formatter makes more
+    // sense for that field type.
     'text_trimmed' => array(
       'label' => t('Trimmed'),
-      'field types' => array('text', 'text_long'),
+      'field types' => array('text', 'text_long', 'text_with_summary'),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
+      ),
+    ),
+
+    // The 'summary or trimmed' field formatter for text_with_summary
+    // fields displays returns the summary element of the field or, if
+    // the summary is empty, the trimmed version of the full element
+    // of the field.
+    'text_summary_or_trimmed' => array(
+      'label' => t('Summary or trimmed'),
+      'field types' => array('text_with_summary'),
       'behaviors' => array(
         'multiple values' => FIELD_BEHAVIOR_DEFAULT,
       ),
@@ -226,7 +304,150 @@ function theme_field_formatter_text_plain($element) {
 function theme_field_formatter_text_trimmed($element) {
   $field = field_info_field($element['#field_name']);
   $instance = field_info_instance($element['#field_name'], $element['#bundle']);
-  return $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL;
+  return text_summary($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL);
+}
+
+/**
+ * Theme function for 'summary or trimmed' field formatter for
+ * text_with_summary fields.  This formatter returns the summary
+ * element of the field or, if the summary is empty, the trimmed
+ * version of the full element of the field.
+ */
+function theme_field_formatter_text_summary_or_trimmed($element) {
+  $field = field_info_field($element['#field_name']);
+  $instance = field_info_instance($element['#field_name'], $element['#bundle']);
+
+  if (!empty($element['#item']['safe_summary'])) {
+    return $element['#item']['safe_summary'];
+  }
+  else {
+    return text_summary($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['format'] : NULL);
+  }
+}
+
+/**
+ * Generate a trimmed, formatted version of a text field value.
+ *
+ * If the end of the summary is not indicated using the <!--break--> delimiter
+ * then we generate the summary automatically, trying to end it at a sensible
+ * place such as the end of a paragraph, a line break, or the end of a
+ * sentence (in that order of preference).
+ *
+ * @param $text
+ *   The content for which a summary will be generated.
+ * @param $format
+ *   The format of the content.
+ *   If the PHP filter is present and $text contains PHP code, we do not
+ *   split it up to prevent parse errors.
+ *   If the line break filter is present then we treat newlines embedded in
+ *   $text as line breaks.
+ *   If the htmlcorrector filter is present, it will be run on the generated
+ *   summary (if different from the incoming $text).
+ * @param $size
+ *   The desired character length of the summary. If omitted, the default
+ *   value will be used. Ignored if the special delimiter is present
+ *   in $text.
+ * @return
+ *   The generated summary.
+ */
+function text_summary($text, $format = NULL, $size = NULL) {
+
+  if (!isset($size)) {
+    // What used to be called 'teaser' is now called 'summary', but
+    // the variable 'teaser_length' is preserved for backwards compatibility.
+    $size = variable_get('teaser_length', 600);
+  }
+
+  // Find where the delimiter is in the body
+  $delimiter = strpos($text, '<!--break-->');
+
+  // If the size is zero, and there is no delimiter, the entire body is the summary.
+  if ($size == 0 && $delimiter === FALSE) {
+    return $text;
+  }
+
+  // If a valid delimiter has been specified, use it to chop off the summary.
+  if ($delimiter !== FALSE) {
+    return substr($text, 0, $delimiter);
+  }
+
+  // We check for the presence of the PHP evaluator filter in the current
+  // format. If the body contains PHP code, we do not split it up to prevent
+  // parse errors.
+  if (isset($format)) {
+    $filters = filter_list_format($format);
+    if (isset($filters['php/0']) && strpos($text, '<?') !== FALSE) {
+      return $text;
+    }
+  }
+
+  // If we have a short body, the entire body is the summary.
+  if (drupal_strlen($text) <= $size) {
+    return $text;
+  }
+
+  // If the delimiter has not been specified, try to split at paragraph or
+  // sentence boundaries.
+
+  // The summary may not be longer than maximum length specified. Initial slice.
+  $summary = truncate_utf8($text, $size);
+
+  // Store the actual length of the UTF8 string -- which might not be the same
+  // as $size.
+  $max_rpos = strlen($summary);
+
+  // How much to cut off the end of the summary so that it doesn't end in the
+  // middle of a paragraph, sentence, or word.
+  // Initialize it to maximum in order to find the minimum.
+  $min_rpos = $max_rpos;
+
+  // Store the reverse of the summary. We use strpos on the reversed needle and
+  // haystack for speed and convenience.
+  $reversed = strrev($summary);
+
+  // Build an array of arrays of break points grouped by preference.
+  $break_points = array();
+
+  // A paragraph near the end of sliced summary is most preferable.
+  $break_points[] = array('</p>' => 0);
+
+  // If no complete paragraph then treat line breaks as paragraphs.
+  $line_breaks = array('<br />' => 6, '<br>' => 4);
+  // Newline only indicates a line break if line break converter
+  // filter is present.
+  if (isset($filters['filter/1'])) {
+    $line_breaks["\n"] = 1;
+  }
+  $break_points[] = $line_breaks;
+
+  // If the first paragraph is too long, split at the end of a sentence.
+  $break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
+
+  // Iterate over the groups of break points until a break point is found.
+  foreach ($break_points as $points) {
+    // Look for each break point, starting at the end of the summary.
+    foreach ($points as $point => $offset) {
+      // The summary is already reversed, but the break point isn't.
+      $rpos = strpos($reversed, strrev($point));
+      if ($rpos !== FALSE) {
+        $min_rpos = min($rpos + $offset, $min_rpos);
+      }
+    }
+
+    // If a break point was found in this group, slice and stop searching.
+    if ($min_rpos !== $max_rpos) {
+      // Don't slice with length 0. Length must be <0 to slice from RHS.
+      $summary = ($min_rpos === 0) ? $summary : substr($summary, 0, 0 - $min_rpos);
+      break;
+    }
+  }
+
+  // If the htmlcorrector filter is present, apply it to the generated summary.
+  if (isset($filters['filter/3'])) {
+    $summary = _filter_htmlcorrector($summary);
+  }
+
+  return $summary;
 }
 
 /**
@@ -260,6 +481,15 @@ function text_field_widget_info() {
         'default value' => FIELD_BEHAVIOR_DEFAULT,
       ),
     ),
+    'text_textarea_with_summary' => array(
+      'label' => t('Text area with a summary'),
+      'field types' => array('text_with_summary'),
+      'settings' => array('rows' => 20, 'summary_rows' => 5),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
+        'default value' => FIELD_BEHAVIOR_DEFAULT,
+      ),
+    ),
   );
 }
 
@@ -281,14 +511,23 @@ function text_elements() {
       '#input' => TRUE,
       '#columns' => array('value'), '#delta' => 0,
       '#process' => array('text_textfield_elements_process'),
+      '#theme_wrapper' => 'text_textfield',
       '#autocomplete_path' => FALSE,
-      ),
+    ),
     'text_textarea' => array(
       '#input' => TRUE,
       '#columns' => array('value', 'format'), '#delta' => 0,
       '#process' => array('text_textarea_elements_process'),
+      '#theme_wrapper' => 'text_textarea',
       '#filter_value' => FILTER_FORMAT_DEFAULT,
-      ),
+    ),
+    'text_textarea_with_summary' => array(
+      '#input' => TRUE,
+      '#columns' => array('value', 'format', 'summary'), '#delta' => 0,
+      '#process' => array('text_textarea_with_summary_process'),
+      '#theme_wrapper' => 'text_textarea_with_summary',
+      '#filter_value' => FILTER_FORMAT_DEFAULT,
+    ),
   );
 }
 
@@ -329,6 +568,10 @@ function text_field_widget(&$form, &$form_state, $field, $instance, $items, $del
     '#type' => $instance['widget']['type'],
     '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
   );
+  if (!empty($instance['settings']['text_processing'])) {
+    $element['#value_callback'] = 'text_field_widget_formatted_text_value';
+  }
+
   return $element;
 }
 
@@ -336,7 +579,17 @@ function text_field_widget(&$form, &$form_state, $field, $instance, $items, $del
  * Implement hook_field_widget_error().
  */
 function text_field_widget_error($element, $error) {
-  form_error($element['value'], $error['message']);
+  switch ($error['error']) {
+    case 'text_summary_max_length':
+      $error_element = $element[$element['#columns'][1]];
+      break;
+
+    default:
+      $error_element = $element[$element['#columns'][0]];
+      break;
+  }
+
+  form_error($error_element, $error['message']);
 }
 
 /**
@@ -364,24 +617,17 @@ function text_textfield_elements_process($element, $form_state, $form) {
     '#autocomplete_path' => $element['#autocomplete_path'],
     '#size' => $instance['widget']['settings']['size'],
     '#attributes' => array('class' => 'text'),
-    // The following values were set by the field module and need
-    // to be passed down to the nested element.
     '#title' => $element['#title'],
     '#description' => $element['#description'],
     '#required' => $element['#required'],
-    '#field_name' => $element['#field_name'],
-    '#bundle' => $element['#bundle'],
-    '#delta' => $element['#delta'],
-    '#columns' => $element['#columns'],
   );
 
   $element[$field_key]['#maxlength'] = !empty($field['settings']['max_length']) ? $field['settings']['max_length'] : NULL;
 
   if (!empty($instance['settings']['text_processing'])) {
-    $filter_key = $element['#columns'][1];
+    $filter_key  = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format';
     $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT;
-    $parents = array_merge($element['#parents'] , array($filter_key));
-    $element[$filter_key] = filter_form($format, 1, $parents);
+    $element[$field_key]['#text_format'] = $format;
   }
 
   return $element;
@@ -400,32 +646,94 @@ function text_textarea_elements_process($element, $form_state, $form) {
   $instance = $form['#fields'][$element['#field_name']]['instance'];
   $field_key = $element['#columns'][0];
   $delta = $element['#delta'];
+
   $element[$field_key] = array(
     '#type' => 'textarea',
     '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL,
     '#rows' => $instance['widget']['settings']['rows'],
     '#weight' => 0,
-    // The following values were set by the field module and need
-    // to be passed down to the nested element.
     '#title' => $element['#title'],
     '#description' => $element['#description'],
     '#required' => $element['#required'],
-    '#field_name' => $element['#field_name'],
-    '#bundle' => $element['#bundle'],
-    '#delta' => $element['#delta'],
-    '#columns' => $element['#columns'],
   );
 
   if (!empty($instance['settings']['text_processing'])) {
     $filter_key = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format';
     $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT;
-    $parents = array_merge($element['#parents'] , array($filter_key));
-    $element[$filter_key] = filter_form($format, 1, $parents);
+    $element[$field_key]['#text_format'] = $format;
+  }
+
+  return $element;
+}
+
+/**
+ * Process an individual element.
+ *
+ * Build the form element. When creating a form using FAPI #process,
+ * note that $element['#value'] is already set.
+ *
+ * The $field and $instance arrays are in $form['#fields'][$element['#field_name']].
+ */
+function text_textarea_with_summary_process($element, $form_state, $form) {
+  $field = $form['#fields'][$element['#field_name']]['field'];
+  $instance = $form['#fields'][$element['#field_name']]['instance'];
+  $delta = $element['#delta'];
+
+  $field_key = $element['#columns'][1];
+  $display = !empty($element['#value'][$field_key]) || !empty($instance['settings']['display_summary']);
+  $element[$field_key] = array(
+    '#title' => t('Summary'),
+    '#type' => $display ? 'textarea' : 'value',
+    '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL,
+    '#rows' => $instance['widget']['settings']['summary_rows'],
+    '#weight' => 0,
+    '#title' => t('Summary'),
+    '#description' => t('Leave blank to use trimmed value of full text as the summary.'),
+    '#required' => $element['#required'],
+    '#display' => $display,
+  );
+
+  $field_key = $element['#columns'][0];
+  $element[$field_key] = array(
+    '#type' => 'textarea',
+    '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL,
+    '#rows' => $instance['widget']['settings']['rows'],
+    '#weight' => 1,
+    '#title' => $display ? t('Full text') : $element['#title'],
+    '#description' => $element['#description'],
+    '#required' => $element['#required'],
+    '#required' => $instance['required'],
+  );
+
+  if (!empty($instance['settings']['text_processing'])) {
+    $filter_key  = (count($element['#columns']) == 2) ? $element['#columns'][1] : 'format';
+    $format = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT;
+    $element[$field_key]['#text_format'] = $format;
   }
 
   return $element;
 }
 
+/**
+ * Helper function to determine the value for a formatted text widget.
+ *
+ * '#text_format' puts the format in '[column 0]_format' in incoming values,
+ * while we need it in '[column 1]'.
+ */
+function text_field_widget_formatted_text_value($form, $edit = FALSE) {
+  if ($edit !== FALSE) {
+    $field_key = $form['#columns'][0];
+    $filter_key = (count($form['#columns']) == 2) ? $form['#columns'][1] : 'format';
+    $default_key = $field_key . '_format';
+    // The format selector uses #access = FALSE if only one format is
+    // available. In this case, we don't receive its value, and need to
+    // manually set it.
+    $edit['format'] = !empty($edit[$default_key]) ? $edit[$default_key] : filter_resolve_format(FILTER_FORMAT_DEFAULT);
+    unset($edit[$default_key]);
+    return $edit;
+  }
+}
+
 /**
  * FAPI theme for an individual text elements.
  *
@@ -444,3 +752,21 @@ function theme_text_textfield($element) {
 function theme_text_textarea($element) {
   return $element['#children'];
 }
+
+function theme_text_textarea_with_summary($element) {
+  // If displaying both a textarea and a summary field, wrap them
+  // in a fieldset to make it clear they belong together.
+  $field_key = $element['#columns'][1];
+  if (!empty($element[$field_key]['#display'])) {
+    $fieldset = array(
+      '#title' => $element['#title'],
+      '#value' => $element['#children'],
+      '#attributes' => array('class' => 'text-textarea'),
+      '#id' => str_replace('_', '-', $element['#field_name']) . '-summary-wrapper',
+    );
+    return theme('fieldset', $fieldset);
+  }
+  else {
+    return $element['#children'];
+  }
+}
diff --git a/modules/field/modules/text/text.test b/modules/field/modules/text/text.test
index cb240b6c9fa9..3c2d706b92ea 100644
--- a/modules/field/modules/text/text.test
+++ b/modules/field/modules/text/text.test
@@ -13,7 +13,7 @@ class TextFieldTestCase extends DrupalWebTestCase {
   }
 
   function setUp() {
-    parent::setUp('field', 'text', 'field_test');
+    parent::setUp('field_test');
 
     $web_user = $this->drupalCreateUser(array('access field_test content', 'administer field_test content'));
     $this->drupalLogin($web_user);
@@ -149,7 +149,7 @@ class TextFieldTestCase extends DrupalWebTestCase {
     // selector is displayed
     $this->drupalGet('test-entity/add/test-bundle');
     $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed'));
-    $this->assertNoFieldByName($this->field_name . '[0][format]', '1', t('Format selector is not displayed'));
+    $this->assertNoFieldByName($this->field_name . '[0][value_format]', '1', t('Format selector is not displayed'));
 
     // Submit with data that should be filtered.
     $value = $this->randomName() . '<br />' . $this->randomName();
@@ -175,11 +175,11 @@ class TextFieldTestCase extends DrupalWebTestCase {
     // We should now have a 'text format' selector.
     $this->drupalGet('test-entity/' . $id . '/edit');
     $this->assertFieldByName($this->field_name . '[0][value]', '', t('Widget is displayed'));
-    $this->assertFieldByName($this->field_name . '[0][format]', '1', t('Format selector is displayed'));
+    $this->assertFieldByName($this->field_name . '[0][value_format]', '1', t('Format selector is displayed'));
 
     // Edit and change the format to 'Full HTML'.
     $edit = array(
-      $this->field_name . '[0][format]' => 2,
+      $this->field_name . '[0][value_format]' => 2,
     );
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertRaw(t('test_entity @id has been updated.', array('@id' => $id)), t('Entity was updated'));
@@ -196,3 +196,148 @@ class TextFieldTestCase extends DrupalWebTestCase {
    *
    */
 }
+
+class TextSummaryTestCase extends DrupalWebTestCase {
+  public static function getInfo() {
+    return array(
+      'name' => t('Text summary'),
+      'description' => t('Test text_summary() with different strings and lengths.'),
+      'group' => t('Field'),
+    );
+  }
+
+  /**
+   * Tests an edge case where the first sentence is a question and
+   * subsequent sentences are not. This edge case is documented at
+   * http://drupal.org/node/180425.
+   */
+  function testFirstSentenceQuestion() {
+    $text = 'A question? A sentence. Another sentence.';
+    $expected = 'A question? A sentence.';
+    $this->callTextSummary($text, $expected, NULL, 30);
+  }
+
+  /**
+   * Test summary with long example.
+   */
+  function testLongSentence() {
+    $text = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' . // 125
+            'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' . // 108
+            'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . // 103
+            'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; // 110
+    $expected = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
+                'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
+                'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
+    // First three sentences add up to: 336, so add one for space and then 3 to get half-way into next word.
+    $this->callTextSummary($text, $expected, NULL, 340);
+  }
+
+  /**
+   * Test various summary length edge cases.
+   */
+  function testLength() {
+    // This string tests a number of edge cases.
+    $text = "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>";
+
+    // The summaries we expect text_summary() to return when $size is the index
+    // of each array item.
+    // Using no text format:
+    $expected = array(
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<",
+      "<p",
+      "<p>",
+      "<p>\n",
+      "<p>\nH",
+      "<p>\nHi",
+      "<p>\nHi\n",
+      "<p>\nHi\n<",
+      "<p>\nHi\n</",
+      "<p>\nHi\n</p",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+    );
+
+    // And using a text format WITH the line-break and htmlcorrector filters.
+    $expected_lb = array(
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<",
+      "<p",
+      "<p></p>",
+      "<p></p>",
+      "<p></p>",
+      "<p></p>",
+      "<p>\nHi</p>",
+      "<p>\nHi</p>",
+      "<p>\nHi</p>",
+      "<p>\nHi</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
+    );
+
+    // Test text_summary() for different sizes.
+    for ($i = 0; $i <= 37; $i++) {
+      $this->callTextSummary($text, $expected[$i],    NULL, $i);
+      $this->callTextSummary($text, $expected_lb[$i], 1,    $i);
+      $this->callTextSummary($text, $expected_lb[$i], 2,    $i);
+    }
+  }
+
+  /**
+   * Calls text_summary() and asserts that the expected teaser is returned.
+   */
+  function callTextSummary($text, $expected, $format = NULL, $size = NULL) {
+    $summary = text_summary($text, $format, $size);
+    $this->assertIdentical($summary, $expected, t('Generated summary "@summary" matches expected "@expected".', array('@summary' => $summary, '@expected' => $expected)));
+  }
+}
diff --git a/modules/field/theme/field.css b/modules/field/theme/field.css
index 17bf18915be3..93dcbcb08fb1 100644
--- a/modules/field/theme/field.css
+++ b/modules/field/theme/field.css
@@ -32,7 +32,11 @@ form .field-add-more-submit {
   margin: .5em 0 0;
 }
 
-.form-item .number {
+form .form-item .text {
   display: inline;
   width: auto;
-}
\ No newline at end of file
+}
+form .form-item .number {
+  display: inline;
+  width: auto;
+}
diff --git a/modules/filter/filter.admin.inc b/modules/filter/filter.admin.inc
index 4b58e4fc1e8b..aa9bc8b7a4a5 100644
--- a/modules/filter/filter.admin.inc
+++ b/modules/filter/filter.admin.inc
@@ -311,10 +311,6 @@ function filter_admin_delete_submit($form, &$form_state) {
 
   $default = variable_get('filter_default_format', 1);
   // Replace existing instances of the deleted format with the default format.
-  db_update('node_revision')
-    ->fields(array('format' => $default))
-    ->condition('format', $form_state['values']['format'])
-    ->execute();
   if (db_table_exists('comment')) {
     db_update('comment')
       ->fields(array('format' => $default))
diff --git a/modules/filter/filter.test b/modules/filter/filter.test
index e50e08538a9b..53fc98518556 100644
--- a/modules/filter/filter.test
+++ b/modules/filter/filter.test
@@ -109,8 +109,8 @@ class FilterAdminTestCase extends DrupalWebTestCase {
 
     $edit = array();
     $edit['title'] = $this->randomName();
-    $edit['body'] = $body . '<random>' . $extra_text . '</random>';
-    $edit['body_format'] = $filtered;
+    $edit['body[0][value]'] = $body . '<random>' . $extra_text . '</random>';
+    $edit['body[0][value_format]'] = $filtered;
     $this->drupalPost('node/add/page', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Filtered node created.'));
 
diff --git a/modules/forum/forum.module b/modules/forum/forum.module
index eac3c58c8e32..8579ca51963f 100644
--- a/modules/forum/forum.module
+++ b/modules/forum/forum.module
@@ -549,8 +549,6 @@ function forum_form($node, $form_state) {
     $form['shadow'] = array('#type' => 'checkbox', '#title' => t('Leave shadow copy'), '#default_value' => $shadow, '#description' => t('If you move this topic, you can leave a link in the old forum to the new forum.'));
   }
 
-  $form['body_field'] = node_body_field($node, $type->body_label, 1);
-
   $form['#submit'][] = 'forum_submit';
   // Assign the forum topic submit handler.
 
diff --git a/modules/forum/forum.test b/modules/forum/forum.test
index 4941734bf733..0b2978800e4e 100644
--- a/modules/forum/forum.test
+++ b/modules/forum/forum.test
@@ -233,7 +233,7 @@ class ForumTestCase extends DrupalWebTestCase {
 
     $edit = array(
       'title' => $title,
-      'body' => $body,
+      'body[0][value]' => $body,
       'taxonomy[1]' => $tid
     );
 
@@ -322,7 +322,7 @@ class ForumTestCase extends DrupalWebTestCase {
       // Edit forum node (including moving it to another forum).
       $edit = array();
       $edit['title'] = 'node/' . $node->nid;
-      $edit['body'] = $this->randomName(256);
+      $edit['body[0][value]'] = $this->randomName(256);
       $edit['taxonomy[1]'] = $this->root_forum['tid']; // Assumes the topic is initially associated with $forum.
       $edit['shadow'] = TRUE;
       $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
diff --git a/modules/help/help.test b/modules/help/help.test
index 089bd48624a6..9bdd41d19a58 100644
--- a/modules/help/help.test
+++ b/modules/help/help.test
@@ -54,14 +54,19 @@ class HelpTestCase extends DrupalWebTestCase {
       // View module help node.
       $this->drupalGet('admin/help/' . $module);
       $this->assertResponse($response);
-      if ($response == 200) {
-        // NOTE: The asserts fail on blog and poll because the get returns the 'admin/help' node instead of the indicated node???
-//        if ($module == 'blog' || $module == 'poll') {
-//          continue;
-//        }
-        $this->assertTitle($name . ' | Drupal', t('[' . $module . '] Title was displayed'));
-        $this->assertRaw('<h2>' . t($name) . '</h2>', t('[' . $module . '] Heading was displayed'));
-        $this->assertText(t('Home ' . $crumb . ' Administer ' . $crumb . ' Help'), t('[' . $module . '] Breadcrumbs were displayed'));
+      if (drupal_function_exists($module . '_help')) {
+        // View module help node.
+        $this->drupalGet('admin/help/' . $module);
+        $this->assertResponse($response);
+        if ($response == 200) {
+          // NOTE: The asserts fail on blog and poll because the get returns the 'admin/help' node instead of the indicated node???
+//          if ($module == 'blog' || $module == 'poll') {
+//            continue;
+//          }
+          $this->assertTitle($name . ' | Drupal', t('[' . $module . '] Title was displayed'));
+          $this->assertRaw('<h2>' . t($name) . '</h2>', t('[' . $module . '] Heading was displayed'));
+          $this->assertText(t('Home ' . $crumb . ' Administer ' . $crumb . ' Help'), t('[' . $module . '] Breadcrumbs were displayed'));
+        }
       }
     }
   }
diff --git a/modules/locale/locale.test b/modules/locale/locale.test
index 95ab2d9981b0..a2546e0be973 100644
--- a/modules/locale/locale.test
+++ b/modules/locale/locale.test
@@ -1395,7 +1395,7 @@ class LocaleContentFunctionalTest extends DrupalWebTestCase {
     $edit = array(
       'type' => 'page',
       'title' => $node_title,
-      'body' => $node_body,
+      'body' => array(array('value' => $node_body)),
       'language' => $langcode,
     );
     $node = $this->drupalCreateNode($edit);
diff --git a/modules/node/node.api.php b/modules/node/node.api.php
index d3058a57dfb3..66630b7d015c 100644
--- a/modules/node/node.api.php
+++ b/modules/node/node.api.php
@@ -204,7 +204,7 @@ function hook_node_grants_alter(&$grants, $account, $op) {
 
   // Get our list of banned roles.
   $restricted = variable_get('example_restricted_roles', array());
-  
+
   if ($op != 'view' && !empty($restricted)) {
     // Now check the roles for this account against the restrictions.
     foreach ($restricted as $role_id) {
@@ -904,7 +904,6 @@ function hook_view($node, $teaser = FALSE) {
     menu_set_location($breadcrumb);
   }
 
-  $node = node_prepare($node, $teaser);
   $node->content['myfield'] = array(
     '#value' => theme('mymodule_myfield', $node->myfield),
     '#weight' => 1,
diff --git a/modules/node/node.install b/modules/node/node.install
index 9ec782deac7c..72c82481a0d6 100644
--- a/modules/node/node.install
+++ b/modules/node/node.install
@@ -213,18 +213,6 @@ function node_schema() {
         'not null' => TRUE,
         'default' => '',
       ),
-      'body' => array(
-        'description' => 'The body of this version.',
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-      ),
-      'teaser' => array(
-        'description' => 'The teaser of this version.',
-        'type' => 'text',
-        'not null' => TRUE,
-        'size' => 'big',
-      ),
       'log' => array(
         'description' => 'The log entry explaining the changes in this version.',
         'type' => 'text',
@@ -237,12 +225,6 @@ function node_schema() {
         'not null' => TRUE,
         'default' => 0,
       ),
-      'format' => array(
-        'description' => "The text format used by this version's body.",
-        'type' => 'int',
-        'not null' => TRUE,
-        'default' => 0,
-      ),
     ),
     'indexes' => array(
       'nid' => array('nid'),
@@ -304,7 +286,7 @@ function node_schema() {
         'default' => '',
       ),
       'has_body' => array(
-        'description' => 'Boolean indicating whether this type uses the {node_revision}.body field.',
+        'description' => 'Boolean indicating whether this type has the body field attached.',
         'type' => 'int',
         'unsigned' => TRUE,
         'not null' => TRUE,
@@ -416,6 +398,7 @@ function node_update_7004() {
 
   // Map old preview setting to new values order.
   $original_preview ? $original_preview = 2 : $original_preview = 1;
+  $node_types_clear();
   $type_list = node_type_get_types();
 
   // Apply original settings to all types.
@@ -429,6 +412,119 @@ function node_update_7004() {
   return array();
 }
 
+/**
+ * Convert body and teaser from node properties to fields.
+ */
+function node_update_7005(&$context) {
+  $ret = array('#finished' => 0);
+
+  // Get node type info for every invocation.
+  node_type_clear();
+  $node_types = node_type_get_types();
+  $body_types = array();
+  foreach ($node_types as $type => $info) {
+    if ($info->has_body) {
+      $body_types[] = $type;
+    }
+  }
+
+  if (!isset($context['total'])) {
+    // Initial invocation.
+
+    // Re-save node types to create body field instances.
+    foreach ($node_types as $type => $info) {
+      if ($info->has_body) {
+        node_type_save($info);
+      }
+    }
+
+    // Initialize state for future calls.
+    $context['last'] = 0;
+    $context['count'] = 0;
+
+    $query = db_select('node', 'n');
+    $query->join('node_revision', 'nr', 'n.vid = nr.vid');
+    $query->condition('n.type', $body_types, 'IN');
+    $context['total'] = $query->countQuery()->execute()->fetchField();
+  }
+  else {
+    // Subsequent invocations.
+
+    $found = FALSE;
+    if ($context['total']) {
+      // Operate on every revision of every node (whee!), in batches.
+      $batch_size = 50;
+      $query = db_select('node', 'n');
+      $nr = $query->innerJoin('node_revision', 'nr', 'n.vid = nr.vid');
+      $revisions = $query
+        ->fields('n', array('type'))
+        ->fields($nr)
+        ->condition('nr.vid', $context['last'], '>')
+        ->condition('n.type', $body_types, 'IN')
+        ->orderBy('nr.vid', 'ASC')
+        ->execute();
+
+      // Load each reversion of each node, set up 'body'
+      // appropriately, and save the node's field data.  Note that
+      // node_load() will not return the body or teaser values from
+      // {node_revision} because those columns have been removed from the
+      // schema structure in memory (but not yet from the database),
+      // so we get the values from the explicit query of the table
+      // instead.
+      foreach ($revisions as $revision) {
+        $found = TRUE;
+
+        if ($node_types[$revision->type]->has_body) {
+          $node = (object) array(
+            'nid' => $revision->nid,
+            'vid' => $revision->vid,
+            'type' => $revision->type,
+          );
+          if (!empty($revision->teaser) && $revision->teaser != text_summary($revision->body)) {
+            $node->body[0]['summary'] = $revision->teaser;
+          }
+          // Do this after text_summary() above.
+          $break = '<!--break-->';
+          if (substr($revision->body, 0, strlen($break)) == $break) {
+            $revision->body = substr($revision->body, strlen($break));
+          }
+          $node->body[0]['value'] = $revision->body;
+          $node->body[0]['format'] = $revision->format;
+          // This is a core update and no contrib modules are enabled yet, so
+          // we can assume default field storage for a faster update.
+          field_sql_storage_field_storage_write('node', $node, FIELD_STORAGE_INSERT, array());
+        }
+
+        $context['last'] = $revision->vid;
+        $context['count'] += 1;
+
+        if (--$batch_size == 0) {
+          break;
+        }
+      }
+
+      $ret['#finished'] = min(0.99, $context['count'] / $context['total']);
+    }
+
+    if (!$found) {
+      // All nodes are processed.
+      $ret[] = array('success' => TRUE, 'query' => "{$context['total']} node body and teaser properties migrated to the 'body' field.");
+
+      // Remove the now-obsolete body info from node_revision.
+      db_drop_field($ret, 'node_revision', 'body');
+      db_drop_field($ret, 'node_revision', 'teaser');
+      db_drop_field($ret, 'node_revision', 'format');
+
+      // We're done.
+      $ret['#finished'] = 1;
+    }
+  }
+
+  return $ret;
+}
+
+
+
 /**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
diff --git a/modules/node/node.module b/modules/node/node.module
index d853da33ff1c..85bcceb364c3 100644
--- a/modules/node/node.module
+++ b/modules/node/node.module
@@ -290,186 +290,6 @@ function node_mark($nid, $timestamp) {
   return MARK_READ;
 }
 
-/**
- * See if the user used JS to submit a teaser.
- */
-function node_teaser_js(&$form, &$form_state) {
-  if (isset($form_state['input']['teaser_js'])) {
-    // Glue the teaser to the body.
-    if (trim($form_state['values']['teaser_js'])) {
-      // Space the teaser from the body
-      $body = trim($form_state['values']['teaser_js']) . "\r\n<!--break-->\r\n" . trim($form_state['values']['body']);
-    }
-    else {
-      // Empty teaser, no spaces.
-      $body = '<!--break-->' . $form_state['values']['body'];
-    }
-    // Pass updated body value on to preview/submit form processing.
-    form_set_value($form['body'], $body, $form_state);
-    // Pass updated body value back onto form for those cases
-    // in which the form is redisplayed.
-    $form['body']['#value'] = $body;
-  }
-  return $form;
-}
-
-/**
- * Ensure value of "teaser_include" checkbox is consistent with other form data.
- *
- * This handles two situations in which an unchecked checkbox is rejected:
- *
- *   1. The user defines a teaser (summary) but it is empty;
- *   2. The user does not define a teaser (summary) (in this case an
- *      unchecked checkbox would cause the body to be empty, or missing
- *      the auto-generated teaser).
- *
- * If JavaScript is active then it is used to force the checkbox to be
- * checked when hidden, and so the second case will not arise.
- *
- * In either case a warning message is output.
- */
-function node_teaser_include_verify(&$form, &$form_state) {
-  $message = '';
-
-  // $form_state['input'] is set only when the form is built for preview/submit.
-  if (isset($form_state['input']['body']) && isset($form_state['values']['teaser_include']) && !$form_state['values']['teaser_include']) {
-    // "teaser_include" checkbox is present and unchecked.
-    if (strpos($form_state['values']['body'], '<!--break-->') === 0) {
-      // Teaser is empty string.
-      $message = t('You specified that the summary should not be shown when this post is displayed in full view. This setting is ignored when the summary is empty.');
-    }
-    elseif (strpos($form_state['values']['body'], '<!--break-->') === FALSE) {
-      // Teaser delimiter is not present in the body.
-      $message = t('You specified that the summary should not be shown when this post is displayed in full view. This setting has been ignored since you have not defined a summary for the post. (To define a summary, insert the delimiter "&lt;!--break--&gt;" (without the quotes) in the Body of the post to indicate the end of the summary and the start of the main content.)');
-    }
-
-    if (!empty($message)) {
-      drupal_set_message($message, 'warning');
-      // Pass new checkbox value on to preview/submit form processing.
-      form_set_value($form['teaser_include'], 1, $form_state);
-      // Pass new checkbox value back onto form for those cases
-      // in which form is redisplayed.
-      $form['teaser_include']['#value'] = 1;
-    }
-  }
-
-  return $form;
-}
-
-/**
- * Generate a teaser for a node body.
- *
- * If the end of the teaser is not indicated using the <!--break--> delimiter
- * then we generate the teaser automatically, trying to end it at a sensible
- * place such as the end of a paragraph, a line break, or the end of a
- * sentence (in that order of preference).
- *
- * @param $body
- *   The content for which a teaser will be generated.
- * @param $format
- *   The format of the content. If the content contains PHP code, we do not
- *   split it up to prevent parse errors. If the line break filter is present
- *   then we treat newlines embedded in $body as line breaks.
- * @param $size
- *   The desired character length of the teaser. If omitted, the default
- *   value will be used. Ignored if the special delimiter is present
- *   in $body.
- * @return
- *   The generated teaser.
- */
-function node_teaser($body, $format = NULL, $size = NULL) {
-
-  if (!isset($size)) {
-    $size = variable_get('teaser_length', 600);
-  }
-
-  // Find where the delimiter is in the body
-  $delimiter = strpos($body, '<!--break-->');
-
-  // If the size is zero, and there is no delimiter, the entire body is the teaser.
-  if ($size == 0 && $delimiter === FALSE) {
-    return $body;
-  }
-
-  // If a valid delimiter has been specified, use it to chop off the teaser.
-  if ($delimiter !== FALSE) {
-    return substr($body, 0, $delimiter);
-  }
-
-  // We check for the presence of the PHP evaluator filter in the current
-  // format. If the body contains PHP code, we do not split it up to prevent
-  // parse errors.
-  if (isset($format)) {
-    $filters = filter_list_format($format);
-    if (isset($filters['php/0']) && strpos($body, '<?') !== FALSE) {
-      return $body;
-    }
-  }
-
-  // If we have a short body, the entire body is the teaser.
-  if (drupal_strlen($body) <= $size) {
-    return $body;
-  }
-
-  // If the delimiter has not been specified, try to split at paragraph or
-  // sentence boundaries.
-
-  // The teaser may not be longer than maximum length specified. Initial slice.
-  $teaser = truncate_utf8($body, $size);
-
-  // Store the actual length of the UTF8 string -- which might not be the same
-  // as $size.
-  $max_rpos = strlen($teaser);
-
-  // How much to cut off the end of the teaser so that it doesn't end in the
-  // middle of a paragraph, sentence, or word.
-  // Initialize it to maximum in order to find the minimum.
-  $min_rpos = $max_rpos;
-
-  // Store the reverse of the teaser. We use strpos on the reversed needle and
-  // haystack for speed and convenience.
-  $reversed = strrev($teaser);
-
-  // Build an array of arrays of break points grouped by preference.
-  $break_points = array();
-
-  // A paragraph near the end of sliced teaser is most preferable.
-  $break_points[] = array('</p>' => 0);
-
-  // If no complete paragraph then treat line breaks as paragraphs.
-  $line_breaks = array('<br />' => 6, '<br>' => 4);
-  // Newline only indicates a line break if line break converter
-  // filter is present.
-  if (isset($filters['filter/1'])) {
-    $line_breaks["\n"] = 1;
-  }
-  $break_points[] = $line_breaks;
-
-  // If the first paragraph is too long, split at the end of a sentence.
-  $break_points[] = array('. ' => 1, '! ' => 1, '? ' => 1, '。' => 0, '؟ ' => 1);
-
-  // Iterate over the groups of break points until a break point is found.
-  foreach ($break_points as $points) {
-    // Look for each break point, starting at the end of the teaser.
-    foreach ($points as $point => $offset) {
-      // The teaser is already reversed, but the break point isn't.
-      $rpos = strpos($reversed, strrev($point));
-      if ($rpos !== FALSE) {
-        $min_rpos = min($rpos + $offset, $min_rpos);
-      }
-    }
-
-    // If a break point was found in this group, slice and return the teaser.
-    if ($min_rpos !== $max_rpos) {
-      // Don't slice with length 0. Length must be <0 to slice from RHS.
-      return ($min_rpos === 0) ? $teaser : substr($teaser, 0, 0 - $min_rpos);
-    }
-  }
-
-  // If a break point was not found, still return a teaser.
-  return $teaser;
-}
-
 /**
  * Extract the type name.
  *
@@ -632,6 +452,7 @@ function node_type_save($info) {
     if (!empty($type->old_type) && $type->old_type != $type->type) {
       field_attach_rename_bundle($type->old_type, $type->type);
     }
+    node_configure_fields($type);
     module_invoke_all('node_type', 'update', $type);
     return SAVED_UPDATED;
   }
@@ -642,12 +463,73 @@ function node_type_save($info) {
       ->execute();
 
     field_attach_create_bundle($type->type);
-
+    node_configure_fields($type);
     module_invoke_all('node_type', 'insert', $type);
     return SAVED_NEW;
   }
 }
 
+/**
+ * Manage the field(s) for a node type.
+ *
+ * Currently, the node module manages a single Field API field,
+ * 'body'.  If $type->has_body is true, this function ensures the
+ * 'body' field exists and creates an instance of it for the bundle
+ * $type->type (e.g. 'page', 'story', ...).  If $type->has_body is
+ * false, this function removes the instance (if it exists) for the
+ * 'body' field on $type->type.
+ */
+function node_configure_fields($type) {
+   // Add or remove the body field, as needed.
+  $field = field_info_field('body');
+  $instance = field_info_instance('body', $type->type);
+  if ($type->has_body) {
+    if (empty($field)) {
+      $field = array(
+        'field_name' => 'body',
+        'type' => 'text_with_summary',
+      );
+      $field = field_create_field($field);
+    }
+    if (empty($instance)) {
+      $instance = array(
+        'field_name' => 'body',
+        'bundle' => $type->type,
+        'label' => $type->body_label,
+        'widget_type' => 'text_textarea_with_summary',
+        'settings' => array('display_summary' => TRUE),
+
+        // With no UI in core, we have to define default
+        // formatters for the teaser and full view.
+        // This may change if the method of handling displays
+        // is changed or if a UI gets into core.
+        'display' => array(
+          'full' => array(
+            'label' => 'hidden',
+            'type' => 'text_default',
+            'exclude' => 0,
+            ),
+          'teaser' => array(
+            'label' => 'hidden',
+            'type' => 'text_summary_or_trimmed',
+            'exclude' => 0,
+            ),
+          ),
+        );
+      field_create_instance($instance);
+    }
+    else {
+      $instance['label'] = $type->body_label;
+      $instance['settings']['display_summary'] = TRUE;
+      field_update_instance($instance);
+    }
+  }
+  elseif (!empty($instance)) {
+    field_delete_instance($instance);
+  }
+
+}
+
 /**
  * Deletes a node type from the database.
  *
@@ -1004,7 +886,8 @@ function node_validate($node, $form = array()) {
 
   // Make sure the body has the minimum number of words.
   // TODO : use a better word counting algorithm that will work in other languages
-  if (!empty($type->min_word_count) && isset($node->body) && count(explode(' ', $node->body)) < $type->min_word_count) {
+  if (!empty($type->min_word_count) && isset($node->body[0]['value']) && count(explode(' ', $node->body[0]['value'])) < $type->min_word_count) {
+    // TODO: Use Field API to set this error.
     form_set_error('body', t('The body of your @type is too short. You need at least %words words.', array('%words' => $type->min_word_count, '@type' => $type->name)));
   }
 
@@ -1041,25 +924,6 @@ function node_submit($node) {
   // Convert the node to an object, if necessary.
   $node = (object)$node;
 
-  // Generate the teaser, but only if it hasn't been set (e.g. by a
-  // module-provided 'teaser' form item).
-  if (!isset($node->teaser)) {
-    if (isset($node->body)) {
-      $node->format = (!empty($node->body_format) ? $node->body_format : FILTER_FORMAT_DEFAULT);
-      $node->teaser = node_teaser($node->body, isset($node->format) ? $node->format : NULL, variable_get('teaser_length_' . $node->type, 600));
-      // Chop off the teaser from the body if needed. The teaser_include
-      // property might not be set (eg. in Blog API postings), so only act on
-      // it, if it was set with a given value.
-      if (isset($node->teaser_include) && !$node->teaser_include && $node->teaser == substr($node->body, 0, strlen($node->teaser))) {
-        $node->body = substr($node->body, strlen($node->teaser));
-      }
-    }
-    else {
-      $node->teaser = '';
-      $node->format = 0;
-    }
-  }
-
   if (user_access('administer nodes')) {
     // Populate the "authored by" field.
     if ($account = user_load_by_name($node->name)) {
@@ -1105,16 +969,6 @@ function node_save($node) {
     if (!isset($node->log)) {
       $node->log = '';
     }
-
-    // For the same reasons, make sure we have $node->teaser and
-    // $node->body. We should consider making these fields nullable
-    // in a future version since node types are not required to use them.
-    if (!isset($node->teaser)) {
-      $node->teaser = '';
-    }
-    if (!isset($node->body)) {
-      $node->body = '';
-    }
   }
   elseif (!empty($node->revision)) {
     $node->old_vid = $node->vid;
@@ -1273,30 +1127,6 @@ function node_build($node, $teaser = FALSE) {
   return $build;
 }
 
-/**
- * Apply filters and build the node's standard elements.
- */
-function node_prepare($node, $teaser = FALSE) {
-  // First we'll overwrite the existing node teaser and body with
-  // the filtered copies! Then, we'll stick those into the content
-  // array and set the read more flag if appropriate.
-  $node->readmore = (strlen($node->teaser) < strlen($node->body));
-
-  if ($teaser == FALSE) {
-    $node->body = check_markup($node->body, $node->format, $node->language, FALSE);
-  }
-  else {
-    $node->teaser = check_markup($node->teaser, $node->format, $node->language, FALSE);
-  }
-
-  $node->content['body'] = array(
-    '#markup' => $teaser ? $node->teaser : $node->body,
-    '#weight' => 0,
-  );
-
-  return $node;
-}
-
 /**
  * Builds a structured array representing the node's content.
  *
@@ -1322,30 +1152,41 @@ function node_prepare($node, $teaser = FALSE) {
  *
  * @return
  *   An structured array containing the individual elements
- *   of the node's body.
+ *   of the node's content.
  */
 function node_build_content($node, $teaser = FALSE) {
-
   // The build mode identifies the target for which the node is built.
   if (!isset($node->build_mode)) {
     $node->build_mode = NODE_BUILD_NORMAL;
   }
 
-  // Remove the delimiter (if any) that separates the teaser from the body.
-  $node->body = isset($node->body) ? str_replace('<!--break-->', '', $node->body) : '';
-
   // The 'view' hook can be implemented to overwrite the default function
   // to display nodes.
   if (node_hook($node, 'view')) {
     $node = node_invoke($node, 'view', $teaser);
   }
-  else {
-    $node = node_prepare($node, $teaser);
-  }
 
   // Build fields content.
+  if (empty($node->content)) {
+    $node->content = array();
+  };
   $node->content += field_attach_view('node', $node, $teaser);
 
+  // Always display a read more link on teasers because we have no way
+  // to know when a teaser view is different than a full view.
+  $links = array();
+  if ($teaser) {
+    $links['node_readmore'] = array(
+      'title' => t('Read more'),
+      'href' => 'node/' . $node->nid,
+      'attributes' => array('rel' => 'tag', 'title' => strip_tags($node->title))
+    );
+  }
+  $node->content['links']['node'] = array(
+    '#type' => 'node_links',
+    '#value' => $links
+  );
+
   // Allow modules to make their own additions to the node.
   module_invoke_all('node_view', $node, $teaser);
 
@@ -1649,16 +1490,16 @@ function node_search($op = 'search', $keys = NULL) {
       // Load results.
       $results = array();
       foreach ($find as $item) {
-        // Build the node body.
+        // Render the node.
         $node = node_load($item->sid);
         $node->build_mode = NODE_BUILD_SEARCH_RESULT;
         $node = node_build_content($node, FALSE, FALSE);
-        $node->body = drupal_render($node->content);
+        $node->rendered = drupal_render($node->content);
 
         // Fetch comments for snippet.
-        $node->body .= module_invoke('comment', 'node_update_index', $node);
+        $node->rendered .= module_invoke('comment', 'node_update_index', $node);
         // Fetch terms for snippet.
-        $node->body .= module_invoke('taxonomy', 'node_update_index', $node);
+        $node->rendered .= module_invoke('taxonomy', 'node_update_index', $node);
 
         $extra = module_invoke_all('node_search_result', $node);
 
@@ -1671,7 +1512,7 @@ function node_search($op = 'search', $keys = NULL) {
           'node' => $node,
           'extra' => $extra,
           'score' => $total ? ($item->calculated_score / $total) : 0,
-          'snippet' => search_excerpt($keys, $node->body),
+          'snippet' => search_excerpt($keys, $node->rendered),
         );
       }
       return $results;
@@ -1801,7 +1642,7 @@ function node_link($type, $node = NULL, $teaser = FALSE) {
   $links = array();
 
   if ($type == 'node') {
-    if ($teaser == 1 && $node->teaser && !empty($node->readmore)) {
+    if ($teaser == 1) {
       $links['node_read_more'] = array(
         'title' => t('Read more'),
         'href' => "node/$node->nid",
@@ -2254,12 +2095,12 @@ function _node_index_node($node) {
   // results half-life calculation.
   variable_set('node_cron_last', $node->changed);
 
-  // Build the node body.
+  // Render the node.
   $node->build_mode = NODE_BUILD_SEARCH_INDEX;
   $node = node_build_content($node, FALSE, FALSE);
-  $node->body = drupal_render($node->content);
+  $node->rendered = drupal_render($node->content);
 
-  $text = '<h1>' . check_plain($node->title) . '</h1>' . $node->body;
+  $text = '<h1>' . check_plain($node->title) . '</h1>' . $node->rendered;
 
   // Fetch extra data normally not visible
   $extra = module_invoke_all('node_update_index', $node);
@@ -2464,10 +2305,6 @@ function node_access($op, $node, $account = NULL) {
   if (empty($account)) {
     $account = $user;
   }
-  // If the node is in a restricted format, disallow editing.
-  if ($op == 'update' && !filter_access($node->format)) {
-    return FALSE;
-  }
 
   if (user_access('bypass node access', $account)) {
     return TRUE;
@@ -2977,9 +2814,9 @@ function node_content_access($op, $node, $account) {
  * Implement hook_form().
  */
 function node_content_form($node, $form_state) {
-  
+
   $type = node_type_get_type($node);
-  
+
   $form = array();
 
   if ($type->has_title) {
@@ -2993,8 +2830,6 @@ function node_content_form($node, $form_state) {
     );
   }
 
-  $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);
-
   return $form;
 }
 
diff --git a/modules/node/node.pages.inc b/modules/node/node.pages.inc
index 729a991dbd23..ee362cbf60b4 100644
--- a/modules/node/node.pages.inc
+++ b/modules/node/node.pages.inc
@@ -114,7 +114,7 @@ function node_form(&$form_state, $node) {
     $form['#prefix'] = $form_state['node_preview'];
   }
   $node = (object)$node;
-  foreach (array('body', 'title', 'format') as $key) {
+  foreach (array('title') as $key) {
     if (!isset($node->$key)) {
       $node->$key = NULL;
     }
@@ -286,52 +286,6 @@ function node_form(&$form_state, $node) {
   return $form;
 }
 
-/**
- * Return a node body field, with format and teaser.
- */
-function node_body_field($node, $label, $word_count) {
-  // Do not generate a body field if the type does not specify one.
-  if (!node_type_get_type($node->type)->has_body) {
-    return array();
-  }
-
-  // Check if we need to restore the teaser at the beginning of the body.
-  $include = !isset($node->teaser) || ($node->teaser == substr($node->body, 0, strlen($node->teaser)));
-
-  $form = array(
-    '#after_build' => array('node_teaser_js', 'node_teaser_include_verify'));
-
-  $form['#prefix'] = '<div class="body-field-wrapper clearfix">';
-  $form['#suffix'] = '</div>';
-
-  $form['teaser_js'] = array(
-    '#type' => 'textarea',
-    '#rows' => 10,
-    '#teaser' => 'edit-body',
-    '#teaser_checkbox' => 'edit-teaser-include',
-    '#disabled' => TRUE,
-  );
-
-  $form['teaser_include'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Show summary in full view'),
-    '#default_value' => $include,
-    '#prefix' => '<div class="teaser-checkbox">',
-    '#suffix' => '</div>',
-  );
-
-  $form['body'] = array(
-    '#type' => 'textarea',
-    '#title' => check_plain($label),
-    '#default_value' => $include ? $node->body : ($node->teaser . $node->body),
-    '#rows' => 20,
-    '#required' => ($word_count > 0),
-    '#text_format' => isset($node->format) ? $node->format : FILTER_FORMAT_DEFAULT,
-  );
-
-  return $form;
-}
-
 /**
  * Button submit function: handle the 'Delete' button on the node form.
  */
@@ -393,16 +347,6 @@ function node_preview($node) {
 
     $node->changed = REQUEST_TIME;
 
-    // Extract a teaser, if it hasn't been set (e.g. by a module-provided
-    // 'teaser' form item).
-    if (!isset($node->teaser)) {
-      $node->teaser = empty($node->body) ? '' : node_teaser($node->body, $node->format, variable_get('teaser_length_' . $type, 600));
-      // Chop off the teaser from the body if needed.
-      if (!$node->teaser_include && $node->teaser == substr($node->body, 0, strlen($node->teaser))) {
-        $node->body = substr($node->body, strlen($node->teaser));
-      }
-    }
-
     // Display a preview of the node.
     // Previewing alters $node so it needs to be cloned.
     if (!form_get_errors()) {
@@ -428,28 +372,20 @@ function theme_node_preview($node) {
   $output = '<div class="preview">';
 
   $preview_trimmed_version = FALSE;
-  // Do we need to preview trimmed version of post as well as full version?
-  if (isset($node->teaser) && isset($node->body)) {
-    $teaser = trim($node->teaser);
-    $body = trim(str_replace('<!--break-->', '', $node->body));
-
-    // Preview trimmed version if teaser and body will appear different;
-    // also (edge case) if both teaser and body have been specified by the user
-    // and are actually the same.
-    if ($teaser != $body || ($body && strpos($node->body, '<!--break-->') === 0)) {
-      $preview_trimmed_version = TRUE;
-    }
-  }
 
-  if ($preview_trimmed_version) {
+  $trimmed = drupal_render(node_build(clone $node, TRUE));
+  $full = drupal_render(node_build($node, FALSE));
+
+  // Do we need to preview trimmed version of post as well as full version?
+  if ($trimmed != $full) {
     drupal_set_message(t('The trimmed version of your post shows what your post looks like when promoted to the main page or when exported for syndication.<span class="no-js"> You can insert the delimiter "&lt;!--break--&gt;" (without the quotes) to fine-tune where your post gets split.</span>'));
     $output .= '<h3>' . t('Preview trimmed version') . '</h3>';
-    $output .= drupal_render(node_build(clone $node, TRUE));
+    $output .= $trimmed;
     $output .= '<h3>' . t('Preview full version') . '</h3>';
-    $output .= drupal_render(node_build($node, FALSE));
+    $output .= $full;
   }
   else {
-    $output .= drupal_render(node_build($node, FALSE));
+    $output .= $full;
   }
   $output .= "</div>\n";
 
diff --git a/modules/node/node.test b/modules/node/node.test
index 252ebbd3cd63..152a1353b279 100644
--- a/modules/node/node.test
+++ b/modules/node/node.test
@@ -141,7 +141,7 @@ class NodeRevisionsTestCase extends DrupalWebTestCase {
 
     // Confirm the correct revision text appears on "view revisions" page.
     $this->drupalGet("node/$node->nid/revisions/$node->vid/view");
-    $this->assertText($node->body, t('Correct text displays for version.'));
+    $this->assertText($node->body[0]['value'], t('Correct text displays for version.'));
 
     // Confirm the correct log message appears on "revisions overview" page.
     $this->drupalGet("node/$node->nid/revisions");
@@ -155,7 +155,7 @@ class NodeRevisionsTestCase extends DrupalWebTestCase {
                         array('@type' => 'Page', '%title' => $nodes[1]->title,
                               '%revision-date' => format_date($nodes[1]->revision_timestamp))), t('Revision reverted.'));
     $reverted_node = node_load($node->nid);
-    $this->assertTrue(($nodes[1]->body == $reverted_node->body), t('Node reverted correctly.'));
+    $this->assertTrue(($nodes[1]->body[0]['value'] == $reverted_node->body[0]['value']), t('Node reverted correctly.'));
 
     // Confirm revisions delete properly.
     $this->drupalPost("node/$node->nid/revisions/{$nodes[1]->vid}/delete", array(), t('Delete'));
@@ -166,151 +166,6 @@ class NodeRevisionsTestCase extends DrupalWebTestCase {
   }
 }
 
-class NodeTeaserTestCase extends DrupalWebTestCase {
-  public static function getInfo() {
-    return array(
-      'name' => t('Node teaser'),
-      'description' => t('Test node_teaser() with different strings and lengths.'),
-      'group' => t('Node'),
-    );
-  }
-
-  /**
-   * Tests an edge case where if the first sentence is a question and
-   * subsequent sentences are not. This is edge case is documented at
-   * http://drupal.org/node/180425.
-   */
-  function testFirstSentenceQuestion() {
-    $body = 'A question? A sentence. Another sentence.';
-    $expected = 'A question? A sentence.';
-    $this->callNodeTeaser($body, $expected, NULL, 30);
-  }
-
-  /**
-   * Test teaser with long example.
-   */
-  function testLongSentence() {
-    $body = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' . // 125
-            'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' . // 108
-            'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ' . // 103
-            'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'; // 110
-    $expected = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' .
-                'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ' .
-                'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.';
-    // First three sentences add up to: 336, so add one for space and then 3 to get half-way into next word.
-    $this->callNodeTeaser($body, $expected, NULL, 340);
-  }
-
-  /**
-   * Test various teaser length edge cases.
-   */
-  function testLength() {
-    // This body string tests a number of edge cases.
-    $body = "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>";
-
-    // The teasers we expect node_teaser() to return when $size is the index
-    // of each array item.
-    // Using an text format with no line-break filter:
-    $teasers = array(
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<",
-      "<p",
-      "<p>",
-      "<p>\n",
-      "<p>\nH",
-      "<p>\nHi",
-      "<p>\nHi\n",
-      "<p>\nHi\n<",
-      "<p>\nHi\n</",
-      "<p>\nHi\n</p",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-    );
-
-    // And Using an text format WITH the line-break filter.
-    $teasers_lb = array(
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<",
-      "<p",
-      "<p>",
-      "<p>",
-      "<p>",
-      "<p>",
-      "<p>\nHi",
-      "<p>\nHi",
-      "<p>\nHi",
-      "<p>\nHi",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-      "<p>\nHi\n</p>\n<p>\nfolks\n<br />\n!\n</p>",
-    );
-
-    // Test node_teaser() for different sizes.
-    for ($i = 0; $i <= 37; $i++) {
-      $this->callNodeTeaser($body, $teasers[$i],    NULL, $i);
-      $this->callNodeTeaser($body, $teasers_lb[$i], 1,    $i);
-      $this->callNodeTeaser($body, $teasers_lb[$i], 2,    $i);
-    }
-  }
-
-  /**
-   * Calls node_teaser() and asserts that the expected teaser is returned.
-   */
-  function callNodeTeaser($body, $expected, $format = NULL, $size = NULL) {
-    $teaser = node_teaser($body, $format, $size);
-    $this->assertIdentical($teaser, $expected, t('Generated teaser "@teaser" matches expected teaser.', array('@teaser' => $teaser)));
-  }
-}
-
 class PageEditTestCase extends DrupalWebTestCase {
   public static function getInfo() {
     return array(
@@ -331,10 +186,11 @@ class PageEditTestCase extends DrupalWebTestCase {
    * Check node edit functionality.
    */
   function testPageEdit() {
+    $body_key = 'body[0][value]';
     // Create node to edit.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit[$body_key] = $this->randomName(16);
     $this->drupalPost('node/add/page', $edit, t('Save'));
 
     // Check that the node exists in the database.
@@ -350,18 +206,18 @@ class PageEditTestCase extends DrupalWebTestCase {
     // Check that the title and body fields are displayed with the correct values.
     $this->assertLink(t('Edit'), 0, t('Edit tab found.'));
     $this->assertFieldByName('title', $edit['title'], t('Title field displayed.'));
-    $this->assertFieldByName('body', '<!--break-->' . $edit['body'], t('Body field displayed.'));
+    $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.'));
 
     // Edit the content of the node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit[$body_key] = $this->randomName(16);
     // Stay on the current page, without reloading.
     $this->drupalPost(NULL, $edit, t('Save'));
 
     // Check that the title and body fields are displayed with the updated values.
     $this->assertText($edit['title'], t('Title displayed.'));
-    $this->assertText($edit['body'], t('Body displayed.'));
+    $this->assertText($edit[$body_key], t('Body displayed.'));
   }
 }
 
@@ -385,44 +241,47 @@ class PagePreviewTestCase extends DrupalWebTestCase {
    * Check the node preview functionality.
    */
   function testPagePreview() {
+    $body_key = 'body[0][value]';
+
     // Fill in node creation form and preview node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit[$body_key] = $this->randomName(16);
     $this->drupalPost('node/add/page', $edit, t('Preview'));
 
     // Check that the preview is displaying the title and body.
     $this->assertTitle(t('Preview | Drupal'), t('Page title is preview.'));
     $this->assertText($edit['title'], t('Title displayed.'));
-    $this->assertText($edit['body'], t('Body displayed.'));
+    $this->assertText($edit[$body_key], t('Body displayed.'));
 
     // Check that the title and body fields are displayed with the correct values.
     $this->assertFieldByName('title', $edit['title'], t('Title field displayed.'));
-    $this->assertFieldByName('body', '<!--break-->' . $edit['body'], t('Body field displayed.'));
+    $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.'));
   }
 
   /**
    * Check the node preview functionality, when using revisions.
    */
   function testPagePreviewWithRevisions() {
+    $body_key = 'body[0][value]';
     // Force revision on page content.
     variable_set('node_options_page', array('status', 'revision'));
 
     // Fill in node creation form and preview node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit[$body_key] = $this->randomName(16);
     $edit['log'] = $this->randomName(32);
     $this->drupalPost('node/add/page', $edit, t('Preview'));
 
     // Check that the preview is displaying the title and body.
     $this->assertTitle(t('Preview | Drupal'), t('Page title is preview.'));
     $this->assertText($edit['title'], t('Title displayed.'));
-    $this->assertText($edit['body'], t('Body displayed.'));
+    $this->assertText($edit[$body_key], t('Body displayed.'));
 
     // Check that the title and body fields are displayed with the correct values.
     $this->assertFieldByName('title', $edit['title'], t('Title field displayed.'));
-    $this->assertFieldByName('body', '<!--break-->' . $edit['body'], t('Body field displayed.'));
+    $this->assertFieldByName($body_key, $edit[$body_key], t('Body field displayed.'));
 
     // Check that the log field has the correct value.
     $this->assertFieldByName('log', $edit['log'], t('Log field displayed.'));
@@ -452,7 +311,7 @@ class PageCreationTestCase extends DrupalWebTestCase {
     // Create a node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit['body[0][value]'] = $this->randomName(16);
     $this->drupalPost('node/add/page', $edit, t('Save'));
 
     // Check that the page has been created.
@@ -599,7 +458,7 @@ class NodePostSettingsTestCase extends DrupalWebTestCase {
     // Create a node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit['body[0][value]'] = $this->randomName(16);
     $this->drupalPost('node/add/page', $edit, t('Save'));
 
     // Check that the post information is displayed.
@@ -620,7 +479,7 @@ class NodePostSettingsTestCase extends DrupalWebTestCase {
     // Create a node.
     $edit = array();
     $edit['title'] = $this->randomName(8);
-    $edit['body'] = $this->randomName(16);
+    $edit['body[0][value]'] = $this->randomName(16);
     $this->drupalPost('node/add/page', $edit, t('Save'));
 
     // Check that the post information is displayed.
@@ -726,7 +585,7 @@ class NodeAccessRecordsAlterUnitTest extends DrupalWebTestCase {
     $this->assertEqual(count($records), 1, t('Returned the correct number of rows.'));
     $this->assertEqual($records[0]->realm, 'test_page_realm', t('Grant with page_realm acquired for node without alteration.'));
     $this->assertEqual($records[0]->gid, 1, t('Grant with gid = 1 acquired for node without alteration.'));
-    
+
     // Create a promoted page node.
     $node3 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1));
     $this->assertTrue(node_load($node3->nid), t('Promoted page node created.'));
@@ -785,7 +644,7 @@ class NodeSaveTestCase extends DrupalWebTestCase {
     $title = $this->randomName(8);
     $node = array(
       'title' => $title,
-      'body' => $this->randomName(32),
+      'body' => array(array('value' => $this->randomName(32))),
       'uid' => $this->web_user->uid,
       'type' => 'article',
       'nid' => $test_nid,
@@ -845,7 +704,7 @@ class NodeAccessRebuildTestCase extends DrupalWebTestCase {
       'group' => t('Node'),
     );
   }
-  
+
   function setUp() {
     parent::setUp();
 
@@ -853,7 +712,7 @@ class NodeAccessRebuildTestCase extends DrupalWebTestCase {
     $this->drupalLogin($web_user);
     $this->web_user = $web_user;
   }
-  
+
   function testNodeAccessRebuild() {
     $this->drupalGet('admin/reports/status');
     $this->clickLink(t('Rebuild permissions'));
diff --git a/modules/path/path.test b/modules/path/path.test
index 421f58247658..2475a538201e 100644
--- a/modules/path/path.test
+++ b/modules/path/path.test
@@ -212,7 +212,7 @@ class PathLanguageTestCase extends DrupalWebTestCase {
     $this->clickLink(t('add translation'));
     $edit = array();
     $edit['title'] = $this->randomName();
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $edit['path'] = $this->randomName();
     $this->drupalPost(NULL, $edit, t('Save'));
 
diff --git a/modules/php/php.test b/modules/php/php.test
index 1d6d317f4ce4..441563e3a88b 100644
--- a/modules/php/php.test
+++ b/modules/php/php.test
@@ -23,7 +23,7 @@ class PHPTestCase extends DrupalWebTestCase {
    * @return stdObject Node object.
    */
   function createNodeWithCode() {
-    return $this->drupalCreateNode(array('body' => '<?php print "SimpleTest PHP was executed!"; ?>'));
+    return $this->drupalCreateNode(array('body' => array('value' => '<?php print "SimpleTest PHP was executed!"; ?>')));
   }
 }
 
@@ -60,7 +60,7 @@ class PHPFilterTestCase extends PHPTestCase {
 
     // Change filter to PHP filter and see that PHP code is evaluated.
     $edit = array();
-    $edit['body_format'] = 3;
+    $edit['body[0][value_format]'] = 3;
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been updated.', array('%title' => $node->title)), t('PHP code filter turned on.'));
 
diff --git a/modules/search/search.api.php b/modules/search/search.api.php
index a31162fb8471..417ea7d0ef54 100644
--- a/modules/search/search.api.php
+++ b/modules/search/search.api.php
@@ -245,32 +245,23 @@ function hook_search_preprocess($text) {
  * @ingroup search
  */
 function hook_update_index() {
-  $last = variable_get('node_cron_last', 0);
   $limit = (int)variable_get('search_cron_limit', 100);
 
-  $result = db_query_range('SELECT n.nid, c.last_comment_timestamp FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid WHERE n.status = 1 AND n.moderate = 0 AND (n.created > %d OR n.changed > %d OR c.last_comment_timestamp > %d) ORDER BY GREATEST(n.created, n.changed, c.last_comment_timestamp) ASC', $last, $last, $last, 0, $limit);
+  $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit);
 
-  while ($node = db_fetch_object($result)) {
-    $last_comment = $node->last_comment_timestamp;
-    $node = node_load(array('nid' => $node->nid));
+  foreach ($result as $node) {
+    $node = node_load($node->nid);
 
-    // We update this variable per node in case cron times out, or if the node
-    // cannot be indexed (PHP nodes which call drupal_goto, for example).
-    // In rare cases this can mean a node is only partially indexed, but the
-    // chances of this happening are very small.
-    variable_set('node_cron_last', max($last_comment, $node->changed, $node->created));
+    // Save the changed time of the most recent indexed node, for the search
+    // results half-life calculation.
+    variable_set('node_cron_last', $node->changed);
 
-    // Get node output (filtered and with module-specific fields).
-    if (node_hook($node, 'view')) {
-      node_invoke($node, 'view', FALSE, FALSE);
-    }
-    else {
-      $node = node_prepare($node, FALSE);
-    }
-    // Allow modules to change $node->body before viewing.
-    module_invoke_all('node_view', $node, FALSE, FALSE);
+    // Render the node.
+    $node->build_mode = NODE_BUILD_SEARCH_INDEX;
+    $node = node_build_content($node, FALSE, FALSE);
+    $node->rendered = drupal_render($node->content);
 
-    $text = '<h1>' . drupal_specialchars($node->title) . '</h1>' . $node->body;
+    $text = '<h1>' . check_plain($node->title) . '</h1>' . $node->rendered;
 
     // Fetch extra data normally not visible
     $extra = module_invoke_all('node_update_index', $node);
diff --git a/modules/search/search.test b/modules/search/search.test
index fcde2c67e0c5..6788744c1636 100644
--- a/modules/search/search.test
+++ b/modules/search/search.test
@@ -312,7 +312,7 @@ class SearchRankingTestCase extends DrupalWebTestCase {
 
     // Create nodes for testing.
     foreach ($node_ranks as $node_rank) {
-      $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => "Drupal's search rocks");
+      $settings = array('type' => 'page', 'title' => 'Drupal rocks', 'body' => array(array('value' => "Drupal's search rocks")));
       foreach (array(0, 1) as $num) {
         if ($num == 1) {
           switch ($node_rank) {
@@ -321,7 +321,7 @@ class SearchRankingTestCase extends DrupalWebTestCase {
               $settings[$node_rank] = 1;
               break;
             case 'relevance':
-              $settings['body'] .= " really rocks";
+              $settings['body'][0]['value'] .= " really rocks";
               break;
             case 'recent':
               $settings['created'] = REQUEST_TIME + 3600;
diff --git a/modules/simpletest/drupal_web_test_case.php b/modules/simpletest/drupal_web_test_case.php
index b2d5af76d2c2..535e18b4cc8c 100644
--- a/modules/simpletest/drupal_web_test_case.php
+++ b/modules/simpletest/drupal_web_test_case.php
@@ -632,18 +632,17 @@ function drupalGetNodeByTitle($title) {
    *
    * @param $settings
    *   An associative array of settings to change from the defaults, keys are
-   *   node properties, for example 'body' => 'Hello, world!'.
+   *   node properties, for example 'title' => 'Hello, world!'.
    * @return
    *   Created node object.
    */
   protected function drupalCreateNode($settings = array()) {
     // Populate defaults array.
     $settings += array(
-      'body'      => $this->randomName(32),
+      'body'      => array(array()),
       'title'     => $this->randomName(8),
       'comment'   => 2,
       'changed'   => REQUEST_TIME,
-      'format'    => FILTER_FORMAT_DEFAULT,
       'moderate'  => 0,
       'promote'   => 0,
       'revision'  => 1,
@@ -660,11 +659,6 @@ protected function drupalCreateNode($settings = array()) {
       $settings['date'] = format_date($settings['created'], 'custom', 'Y-m-d H:i:s O');
     }
 
-    // Add the default teaser.
-    if (!isset($settings['teaser'])) {
-      $settings['teaser'] = $settings['body'];
-    }
-
     // If the node's user uid is not specified manually, use the currently
     // logged in user if available, or else the user running the test.
     if (!isset($settings['uid'])) {
@@ -677,6 +671,13 @@ protected function drupalCreateNode($settings = array()) {
       }
     }
 
+    // Merge body field value and format separately.
+    $body = array(
+      'value' => $this->randomName(32),
+      'format' => FILTER_FORMAT_DEFAULT
+    );
+    $settings['body'][0] += $body;
+
     $node = (object) $settings;
     node_save($node);
 
@@ -989,9 +990,18 @@ protected function setUp() {
     $this->preloadRegistry();
 
     // Add the specified modules to the list of modules in the default profile.
-    $args = func_get_args();
-    $modules = array_unique(array_merge(drupal_get_profile_modules('default', 'en'), $args));
-    drupal_install_modules($modules, TRUE);
+    // Install the modules specified by the default profile.
+    $core_modules = drupal_get_profile_modules('default', 'en');
+    drupal_install_modules($core_modules, TRUE);
+
+    node_type_clear();
+
+    // Install additional modules one at a time in order to make sure that the
+    // list of modules is updated between each module's installation.
+    $modules = func_get_args();
+    foreach ($modules as $module) {
+      drupal_install_modules(array($module), TRUE);
+    }
 
     // Because the schema is static cached, we need to flush
     // it between each run. If we don't, then it will contain
diff --git a/modules/simpletest/tests/common.test b/modules/simpletest/tests/common.test
index 6b767a527399..4ea5cb4cea2c 100644
--- a/modules/simpletest/tests/common.test
+++ b/modules/simpletest/tests/common.test
@@ -261,9 +261,7 @@ class CascadingStylesheetsTestCase extends DrupalWebTestCase {
     // Create a node, using the PHP filter that tests drupal_add_css().
     $settings = array(
       'type' => 'page',
-      'format' => 3, // PHP filter.
-      'body_format' => 3,
-      'body' => t('This tests the inline CSS!') . "<?php drupal_add_css('$css', 'inline'); ?>",
+      'body' => array(array('value' => t('This tests the inline CSS!') . "<?php drupal_add_css('$css', 'inline'); ?>", 'format' => 3)), // PHP filter.
       'promote' => 1,
     );
     $node = $this->drupalCreateNode($settings);
diff --git a/modules/simpletest/tests/module.test b/modules/simpletest/tests/module.test
index 87f1a154452d..f42753f588d1 100644
--- a/modules/simpletest/tests/module.test
+++ b/modules/simpletest/tests/module.test
@@ -22,19 +22,24 @@ class ModuleUnitTest extends DrupalWebTestCase {
    * The basic functionality of module_list().
    */
   function testModuleList() {
-    $base_module_list = drupal_get_profile_modules('default', 'en');
-    // Key the list by module name.
-    $base_module_list = array_combine($base_module_list, $base_module_list);
-    // All default profile modules have a weight equal to 0, the default sort
-    // order is thus simply alphabetical.
-    ksort($base_module_list);
-    $this->assertModuleList($base_module_list, t('Default profile'));
+    // Build a list of modules filenames.
+    $base_module_list = array();
+    foreach (drupal_get_profile_modules('default', 'en') as $module) {
+      $base_module_list[$module] = drupal_get_path('module', $module);
+    }
+    asort($base_module_list);
+    // Build a list of module names based on that order. Since all default
+    // profile modules have a weight equal to 0, the default sort order is
+    // simply alphabetical.
+    $module_list = array_keys($base_module_list);
+    $this->assertModuleList($module_list, t('Default profile'));
 
     // Try to install a new module.
     drupal_install_modules(array('path'));
-    $base_module_list['path'] = 'path';
-    ksort($base_module_list);
-    $this->assertModuleList($base_module_list, t('After adding a module'));
+    $base_module_list['path'] = drupal_get_path('module', 'path');
+    asort($base_module_list);
+    $module_list = array_keys($base_module_list);
+    $this->assertModuleList($module_list, t('After adding a module'));
 
     // Try to mess with the module weights.
     db_update('system')
@@ -46,8 +51,9 @@ class ModuleUnitTest extends DrupalWebTestCase {
     module_list(TRUE);
     // Move path at the end of the array.
     unset($base_module_list['path']);
-    $base_module_list['path'] = 'path';
-    $this->assertModuleList($base_module_list, t('After changing weights'));
+    $base_module_list['path'] = drupal_get_path('module', 'path');
+    $module_list = array_keys($base_module_list);
+    $this->assertModuleList($module_list, t('After changing weights'));
 
     // Test the fixed list feature.
     $fixed_list = array(
@@ -60,7 +66,7 @@ class ModuleUnitTest extends DrupalWebTestCase {
 
     // Reset the module list.
     module_list(TRUE);
-    $this->assertModuleList($base_module_list, t('After reset'));
+    $this->assertModuleList($module_list, t('After reset'));
   }
 
   /**
@@ -70,6 +76,7 @@ class ModuleUnitTest extends DrupalWebTestCase {
    *   The expected values, sorted by weight and file name.
    */
   protected function assertModuleList(Array $expected_values, $condition) {
+    $expected_values = array_combine($expected_values, $expected_values);
     $this->assertIdentical($expected_values, module_list(), t('@condition: module_list() returns correct results', array('@condition' => $condition)));
     ksort($expected_values);
     $this->assertIdentical($expected_values, module_list(FALSE, TRUE), t('@condition: module_list() returns correctly sorted results', array('@condition' => $condition)));
diff --git a/modules/system/system.install b/modules/system/system.install
index af694203e55f..1c787f13fde1 100644
--- a/modules/system/system.install
+++ b/modules/system/system.install
@@ -3548,6 +3548,17 @@ function system_update_7026() {
   return $ret;
 }
 
+/**
+ * Enable field type modules.
+ */
+function system_update_7027() {
+  $ret = array();
+  $module_list = array('text', 'number', 'list', 'options');
+  drupal_install_modules($module_list);
+  module_enable($module_list);
+  return $ret;
+}
+
 /**
  * @} End of "defgroup updates-6.x-to-7.x"
  * The next series of updates should start at 8000.
diff --git a/modules/system/system.test b/modules/system/system.test
index 533a3cdc8b99..46cafc00171f 100644
--- a/modules/system/system.test
+++ b/modules/system/system.test
@@ -518,7 +518,7 @@ class AccessDeniedTestCase extends DrupalWebTestCase {
 
     $edit = array(
       'title' => $this->randomName(10),
-      'body' => $this->randomName(100)
+      'body' => array(array('value' => $this->randomName(100))),
     );
     $node = $this->drupalCreateNode($edit);
 
@@ -579,7 +579,7 @@ class PageNotFoundTestCase extends DrupalWebTestCase {
 
     $edit = array(
       'title' => $this->randomName(10),
-      'body' => $this->randomName(100)
+      'body' => array(array('value' => $this->randomName(100))),
     );
     $node = $this->drupalCreateNode($edit);
 
@@ -705,7 +705,7 @@ class PageTitleFiltering extends DrupalWebTestCase {
     // Generate node content.
     $edit = array(
      'title' => '!SimpleTest! ' . $title . $this->randomName(20),
-     'body' => '!SimpleTest! test body' . $this->randomName(200),
+     'body[0][value]' => '!SimpleTest! test body' . $this->randomName(200),
     );
     // Create the node with HTML in the title.
     $this->drupalPost('node/add/page', $edit, t('Save'));
diff --git a/modules/taxonomy/taxonomy.test b/modules/taxonomy/taxonomy.test
index 87123bc39dbd..ba91e6de21b7 100644
--- a/modules/taxonomy/taxonomy.test
+++ b/modules/taxonomy/taxonomy.test
@@ -474,7 +474,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     // Post an article.
     $edit = array();
     $edit['title'] = $this->randomName();
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $edit['taxonomy[' . $this->vocabulary->vid . ']'] = $term1->tid;
     $this->drupalPost('node/add/article', $edit, t('Save'));
 
@@ -516,7 +516,7 @@ class TaxonomyTermTestCase extends TaxonomyWebTestCase {
     // Insert the terms in a comma separated list. Vocabulary 1 is a
     // free-tagging field created by the default profile.
     $edit['taxonomy[tags][' . $this->vocabulary->vid . ']'] =  implode(', ', $terms);
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $this->drupalPost('node/add/article', $edit, t('Save'));
     $this->assertRaw(t('@type %title has been created.', array('@type' => t('Article'), '%title' => $edit['title'])), t('The node was created successfully'));
     foreach ($terms as $term) {
diff --git a/modules/translation/translation.module b/modules/translation/translation.module
index 58045f39694e..4b9dbecd73c7 100644
--- a/modules/translation/translation.module
+++ b/modules/translation/translation.module
@@ -213,7 +213,6 @@ function translation_node_prepare($node) {
       $node->language = $language;
       $node->translation_source = $source_node;
       $node->title = $node->translation_source->title;
-      $node->body = $node->translation_source->body;
       // Let every module add custom translated fields.
       module_invoke_all('node_prepare_translation', $node);
     }
diff --git a/modules/translation/translation.test b/modules/translation/translation.test
index 73afe8409a6e..6dc2ce456034 100644
--- a/modules/translation/translation.test
+++ b/modules/translation/translation.test
@@ -60,14 +60,14 @@ class TranslationTestCase extends DrupalWebTestCase {
     // to return to the page then resubmitting the form without a refresh.
     $edit = array();
     $edit['title'] = $this->randomName();
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $this->drupalPost('node/add/page', $edit, t('Save'), array('query' => array('translation' => $node->nid, 'language' => 'es')));
     $duplicate = $this->drupalGetNodeByTitle($edit['title']);
     $this->assertEqual($duplicate->tnid, 0, t('The node does not have a tnid.'));
 
     // Update original and mark translation as outdated.
     $edit = array();
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $edit['translation[retranslate]'] = TRUE;
     $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_title)), t('Original node updated.'));
@@ -78,7 +78,7 @@ class TranslationTestCase extends DrupalWebTestCase {
 
     // Update translation and mark as updated.
     $edit = array();
-    $edit['body'] = $this->randomName();
+    $edit['body[0][value]'] = $this->randomName();
     $edit['translation[status]'] = FALSE;
     $this->drupalPost('node/' . $node_translation->nid . '/edit', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been updated.', array('%title' => $node_translation_title)), t('Translated node updated.'));
@@ -128,7 +128,7 @@ class TranslationTestCase extends DrupalWebTestCase {
   function createPage($title, $body, $language) {
     $edit = array();
     $edit['title'] = $title;
-    $edit['body'] = $body;
+    $edit['body[0][value]'] = $body;
     $edit['language'] = $language;
     $this->drupalPost('node/add/page', $edit, t('Save'));
     $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Page created.'));
@@ -153,7 +153,7 @@ class TranslationTestCase extends DrupalWebTestCase {
 
     $edit = array();
     $edit['title'] = $title;
-    $edit['body'] = $body;
+    $edit['body[0][value]'] = $body;
     $this->drupalPost(NULL, $edit, t('Save'));
     $this->assertRaw(t('Page %title has been created.', array('%title' => $edit['title'])), t('Translation created.'));
 
diff --git a/modules/trigger/trigger.test b/modules/trigger/trigger.test
index 32f056cffdf3..5e9d7a28653e 100644
--- a/modules/trigger/trigger.test
+++ b/modules/trigger/trigger.test
@@ -37,8 +37,8 @@ class TriggerContentTestCase extends DrupalWebTestCase {
       $web_user = $this->drupalCreateUser(array('create page content', 'access content', 'administer nodes'));
       $this->drupalLogin($web_user);
       $edit = array();
-      $edit['title']    = '!SimpleTest test node! ' . $this->randomName(10);
-      $edit['body']     = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32);
+      $edit['title'] = '!SimpleTest test node! ' . $this->randomName(10);
+      $edit['body[0][value]'] = '!SimpleTest test body! ' . $this->randomName(32) . ' ' . $this->randomName(32);
       $edit[$info['property']] = !$info['expected'];
       $this->drupalPost('node/add/page', $edit, t('Save'));
       // Make sure the text we want appears.
-- 
GitLab