Index: field/field.info.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/field/field.info.inc,v retrieving revision 1.4 diff -u -r1.4 field.info.inc --- field/field.info.inc 10 Feb 2009 16:09:00 -0000 1.4 +++ field/field.info.inc 21 Mar 2009 15:47:53 -0000 @@ -142,7 +142,7 @@ cache_set('field_info_types', $info, 'cache_field'); } } - + return $info; } Index: field/modules/list/list.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/list/list.info,v retrieving revision 1.2 diff -u -r1.2 list.info --- field/modules/list/list.info 5 Feb 2009 03:42:57 -0000 1.2 +++ field/modules/list/list.info 21 Mar 2009 15:16:35 -0000 @@ -4,3 +4,4 @@ package = Core - fields core = 7.x files[]=list.module +required = TRUE Index: field/modules/number/number.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/number/number.info,v retrieving revision 1.2 diff -u -r1.2 number.info --- field/modules/number/number.info 5 Feb 2009 03:42:57 -0000 1.2 +++ field/modules/number/number.info 21 Mar 2009 15:16:35 -0000 @@ -4,3 +4,4 @@ package = Core - fields core = 7.x files[]=number.module +required = TRUE Index: field/modules/options/options.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/options/options.info,v retrieving revision 1.1 diff -u -r1.1 options.info --- field/modules/options/options.info 3 Feb 2009 17:30:11 -0000 1.1 +++ field/modules/options/options.info 21 Mar 2009 15:16:36 -0000 @@ -4,3 +4,4 @@ package = Core - fields core = 7.x files[]=options.module +required = TRUE Index: field/modules/text/text.info =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.info,v retrieving revision 1.2 diff -u -r1.2 text.info --- field/modules/text/text.info 5 Feb 2009 03:42:57 -0000 1.2 +++ field/modules/text/text.info 21 Mar 2009 15:16:36 -0000 @@ -4,3 +4,4 @@ package = Core - fields core = 7.x files[]=text.module +required = TRUE Index: field/modules/text/text.module =================================================================== RCS file: /cvs/drupal/drupal/modules/field/modules/text/text.module,v retrieving revision 1.4 diff -u -r1.4 text.module --- field/modules/text/text.module 10 Mar 2009 09:45:31 -0000 1.4 +++ field/modules/text/text.module 21 Mar 2009 21:51:40 -0000 @@ -14,6 +14,9 @@ '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,26 @@ 'field_formatter_text_trimmed' => array( 'arguments' => array('element' => NULL), ), + 'field_formatter_text_summary_or_trimmed' => array( + 'arguments' => array('element' => NULL), + ), + 'field_formatter_text_trimmed_if_teaser_empty' => array( + 'arguments' => array('element' => NULL), + ), ); } /** * Implementation of hook_field_info(). + * + * @param $max_length + * The maximum length for a varchar field. + * @param $text_processing + * Whether text input filters should be used. + * @param $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( @@ -38,19 +56,24 @@ 'label' => t('Text'), '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), + 'instance_settings' => array('text_processing' => 0, 'summary_field' => ''), '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.'), - 'instance_settings' => array('text_processing' => 0), - 'widget_settings' => array('rows' => 5), + 'instance_settings' => array('text_processing' => 0, 'summary_field' => ''), 'default_widget' => 'text_textarea', 'default_formatter' => 'text_default', ), + 'text_with_summary' => array( + 'label' => t('Long text with a summary field'), + 'description' => t('This field stores long text in the database along with optional summary/teaser text.'), + 'instance_settings' => array('text_processing' => 1, 'display_summary' => 0), + 'default_widget' => 'text_textarea_with_summary', + 'default_formatter' => 'text_summary_or_trimmed', + ), ); } @@ -65,6 +88,30 @@ 'size' => 'big', 'not null' => FALSE, ), + 'value_format' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), + ); + } + else if ($field['type'] == 'text_with_summary') { + $columns = array( + 'value' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'summary' => array( + 'type' => 'text', + 'size' => 'big', + 'not null' => FALSE, + ), + 'value_format' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), ); } else { @@ -74,15 +121,13 @@ 'length' => $field['settings']['max_length'], 'not null' => FALSE, ), + 'value_format' => array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => FALSE, + ), ); } - $columns += array( - 'format' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => FALSE, - ), - ); return $columns; } @@ -99,6 +144,11 @@ form_set_error($error_element, t('%name: the value may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']))); } } + if (!empty($item['summary'])) { + if (!empty($field['settings']['max_length']) && drupal_strlen($item['summary']) > $field['settings']['max_length']) { + form_set_error($error_element, t('%name: the summary may not be longer than %max characters.', array('%name' => $instance['label'], '%max' => $field['settings']['max_length']))); + } + } } } } @@ -107,14 +157,22 @@ global $language; foreach ($items as $delta => $item) { // TODO D7 : this code is really node-related. + $format = $item['value_format']; if (!empty($instance['settings']['text_processing'])) { $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, $check) : ''; + $text = isset($item['value']) ? check_markup($item['value'], $format, isset($object->language) ? $object->language : $language, $check) : ''; + if ($field['type'] == 'text_with_summary') { + $summary = isset($item['summary']) ? check_markup($item['summary'], $format, isset($object->language) ? $object->language : $language, $check) : ''; + } } else { $text = check_plain($item['value']); + if ($field['type'] == 'text_with_summary') { + $summary = check_plain($item['summary']); + } } $items[$delta]['safe'] = $text; + $items[$delta]['safe_summary'] = isset($summary) ? $summary : ''; } } @@ -123,7 +181,12 @@ */ function text_field_is_empty($item, $field) { if (empty($item['value']) && (string)$item['value'] !== '0') { - return TRUE; + if ($field['type'] == 'text_with_summary') { + return (empty($item['summary']) && (string)$item['summary'] !== '0'); + } + else { + return TRUE; + } } return FALSE; } @@ -153,6 +216,20 @@ 'behaviors' => array( 'multiple values' => FIELD_BEHAVIOR_DEFAULT, ), + ), + 'text_trimmed_if_teaser_empty' => array( + 'label' => t('Trimmed, if teaser is empty'), + 'field types' => array('text', 'text_long'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), + ), + 'text_summary_or_trimmed' => array( + 'label' => t('Summary or Trimmed'), + 'field types' => array('text_with_summary'), + 'behaviors' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, + ), ), ); } @@ -177,7 +254,155 @@ 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_teaser($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['value_format'] : NULL); +} + +/** + * Theme function for 'trimmed' text field formatter if the referenced + * teaser field is empty. + */ +function theme_field_formatter_text_trimmed_if_teaser_empty($element) { + $field = field_info_field($element['#field_name']); + $instance = field_info_instance($element['#field_name'], $element['#bundle']); + + $object = $element['#object']; + if (!empty($instance['settings']['summary_field'])) { + $teaser = $instance['settings']['summary_field']; + if (!empty($object->{$teaser}[$element['#item']['#delta']]['safe'])) { + return ''; + } + } + + return text_teaser($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['value_format'] : NULL); +} + +/** + * Theme function for 'summary or trimmed' text field formatter. + */ +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_teaser($element['#item']['safe'], $instance['settings']['text_processing'] ? $element['#item']['value_format'] : NULL); + } +} + +/** + * Generate a trimmed, formatted version of a text field value. + * + * If the end of the teaser is not indicated using the 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 text_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, ''); + + // 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, '' => 0); + + // If no complete paragraph then treat line breaks as paragraphs. + $line_breaks = array('
' => 6, '
' => 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; } /** @@ -211,6 +436,15 @@ '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, + ), + ), ); } @@ -232,12 +466,21 @@ '#input' => TRUE, '#columns' => array('value'), '#delta' => 0, '#process' => array('text_textfield_process'), + '#theme_wrapper' => 'text_textfield', '#autocomplete_path' => FALSE, ), 'text_textarea' => array( '#input' => TRUE, '#columns' => array('value', 'format'), '#delta' => 0, '#process' => array('text_textarea_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, ), ); @@ -301,7 +544,8 @@ $instance = $form['#fields'][$element['#field_name']]['instance']; $field_key = $element['#columns'][0]; $delta = $element['#delta']; - + $filter_key = 'value_format'; + $element[$field_key] = array( '#type' => 'textfield', '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, @@ -317,17 +561,14 @@ '#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]; - $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'] = isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT; } - + // Used so that hook_field('validate') knows where to flag an error. // TODO: rework that. See http://groups.drupal.org/node/18019. $element['_error_element'] = array( @@ -351,6 +592,8 @@ $instance = $form['#fields'][$element['#field_name']]['instance']; $field_key = $element['#columns'][0]; $delta = $element['#delta']; + $filter_key = $filter_key = 'value_format'; + $element[$field_key] = array( '#type' => 'textarea', '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL, @@ -365,20 +608,79 @@ '#bundle' => $element['#bundle'], '#delta' => $element['#delta'], '#columns' => $element['#columns'], + '#text_format' => isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT, + ); + + // Used so that hook_field('validate') knows where to flag an error. + $element['_error_element'] = array( + '#type' => 'value', + '#value' => implode('][', array_merge($element['#parents'], array($field_key))), ); - 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); - } + 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, $edit, $form_state, $form) { + $field = $form['#fields'][$element['#field_name']]['field']; + $instance = $form['#fields'][$element['#field_name']]['instance']; + $delta = $element['#delta']; + $filter_key = $filter_key = 'value_format'; + + $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' => 4, + '#weight' => 0, + // The following values were set by the field module and need + // to be passed down to the nested element. + '#title' => t('Summary'), + '#description' => t('Leave blank to use trimmed value of full text as the summary.'), + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#bundle' => $element['#bundle'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + '#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, + // The following values were set by the field module and need + // to be passed down to the nested element. + '#title' => t('Full text'), + '#description' => $element['#description'], + '#required' => $element['#required'], + '#field_name' => $element['#field_name'], + '#bundle' => $element['#bundle'], + '#delta' => $element['#delta'], + '#columns' => $element['#columns'], + '#text_format' => isset($element['#value'][$filter_key]) ? $element['#value'][$filter_key] : FILTER_FORMAT_DEFAULT, + '#required' => $instance['required'], + ); // Used so that hook_field('validate') knows where to flag an error. $element['_error_element'] = array( '#type' => 'value', - '#value' => implode('][', array_merge($element['#parents'], array($field_key))), + '#value' => implode('][', array_merge($element['#parents'], array($element['#columns'][0]))), ); + $element['_error_element'][1]['#type'] = 'value'; + $element['_error_element'][1]['#value'] = implode('][', array_merge($element['#parents'], array($element['#columns'][1]))); + return $element; } @@ -399,4 +701,21 @@ function theme_text_textarea($element) { return $element['#children']; -} \ No newline at end of file +} + +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'), + ); + return theme('fieldset', $fieldset); + } + else { + return $element['#children']; + } +} Index: field/theme/field.css =================================================================== RCS file: /cvs/drupal/drupal/modules/field/theme/field.css,v retrieving revision 1.3 diff -u -r1.3 field.css --- field/theme/field.css 10 Mar 2009 09:45:32 -0000 1.3 +++ field/theme/field.css 21 Mar 2009 19:16:30 -0000 @@ -32,7 +32,11 @@ 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; +} Index: filter/filter.module =================================================================== RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v retrieving revision 1.244 diff -u -r1.244 filter.module --- filter/filter.module 15 Mar 2009 01:53:16 -0000 1.244 +++ filter/filter.module 21 Mar 2009 18:52:29 -0000 @@ -433,7 +433,7 @@ // Check for a cached version of this piece of text. $cache_id = $format . ':' . $langcode . ':' . md5($text); if ($cached = cache_get($cache_id, 'cache_filter')) { - return $cached->data; + //return $cached->data; } // See if caching is allowed for this format. @@ -458,13 +458,12 @@ // Store in cache with a minimum expiration time of 1 day. if ($cache) { - cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24)); + //cache_set($cache_id, $text, 'cache_filter', REQUEST_TIME + (60 * 60 * 24)); } } else { $text = t('n/a'); } - return $text; } Index: node/node.install =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.install,v retrieving revision 1.14 diff -u -r1.14 node.install --- node/node.install 17 Mar 2009 12:41:54 -0000 1.14 +++ node/node.install 21 Mar 2009 15:16:36 -0000 @@ -239,18 +239,6 @@ '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', @@ -326,7 +314,7 @@ '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, Index: node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.1032 diff -u -r1.1032 node.module --- node/node.module 20 Mar 2009 19:18:10 -0000 1.1032 +++ node/node.module 21 Mar 2009 21:20:45 -0000 @@ -46,6 +46,11 @@ define('NODE_BUILD_PRINT', 5); /** + * Name of the default body field. + */ +define('NODE_BODY_FIELD', 'node_body'); + +/** * Implementation of hook_help(). */ function node_help($path, $arg) { @@ -290,186 +295,6 @@ } /** - * 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\r\n" . trim($form_state['values']['body']); - } - else { - // Empty teaser, no spaces. - $body = '' . $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'], '') === 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'], '') === 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 "<!--break-->" (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 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, ''); - - // 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, '' => 0); - - // If no complete paragraph then treat line breaks as paragraphs. - $line_breaks = array('
' => 6, '
' => 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; -} - -/** * Builds a list of available node types, and returns all of part of this list * in the specified format. * @@ -583,6 +408,7 @@ 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; } @@ -591,13 +417,49 @@ db_insert('node_type')->fields($fields)->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. + */ +function node_configure_fields($type) { + + // Add or remove the body field, as needed. + $field = field_info_field(NODE_BODY_FIELD); + $instance = field_info_instance(NODE_BODY_FIELD, $type->type); + if ($type->has_body) { + if (empty($field)) { + $field = array( + 'field_name' => NODE_BODY_FIELD, + 'type' => 'text_with_summary', + ); + $field = field_create_field($field); + } + if (empty($instance)) { + $instance = array( + 'field_name' => NODE_BODY_FIELD, + 'bundle' => $type->type, + 'widget_type' => 'text_textarea_with_summary', + 'label' => $type->body_label, + ); + field_create_instance($instance); + } + else { + $instance['label'] = $type->body_label; + field_update_instance($instance); + } + } + elseif (!empty($instance)) { + field_delete_instance($instance); + } + +} + +/** * Deletes a node type from the database. * * @param $type @@ -974,7 +836,8 @@ // 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->{NODE_BODY_FIELD}[0]['value']) && count(explode(' ', $node->{NODE_BODY_FIELD}[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))); } @@ -1014,25 +877,6 @@ // 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); - // 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)) { @@ -1072,16 +916,6 @@ 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; @@ -1216,30 +1050,6 @@ } /** - * 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. * * @param $node @@ -1249,7 +1059,7 @@ * * @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) { @@ -1258,21 +1068,33 @@ $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('', '', $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. node_invoke_node($node, 'view', $teaser); @@ -1492,16 +1314,16 @@ // 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', $node, 'update_index'); + $node->rendered .= module_invoke('comment', 'node', $node, 'update_index'); // Fetch terms for snippet. - $node->body .= module_invoke('taxonomy', 'node', $node, 'update_index'); + $node->rendered .= module_invoke('taxonomy', 'node', $node, 'update_index'); $extra = node_invoke_node($node, 'search_result'); @@ -1514,7 +1336,7 @@ '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; @@ -1623,7 +1445,7 @@ $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", @@ -1929,24 +1751,11 @@ if (node_hook($item, 'view')) { $item = node_invoke($item, 'view', $teaser, FALSE); } - else { - $item = node_prepare($item, $teaser); - } // Allow modules to change $node->content before the node is rendered. node_invoke_node($item, 'view', $teaser, FALSE); - // Set the proper node property, then unset unused $node property so that a - // bad theme can not open a security hole. - $content = drupal_render($item->content); - if ($teaser) { - $item->teaser = $content; - unset($item->body); - } - else { - $item->body = $content; - unset($item->teaser); - } + $item->rendered = drupal_render($item->content); // Allow modules to modify the fully-built node. node_invoke_node($item, 'alter', $teaser, FALSE); @@ -1964,13 +1773,11 @@ // Prepare the item description switch ($item_length) { case 'fulltext': - $item_text = $item->body; + $item_text = $item->rendered; break; case 'teaser': - $item_text = $item->teaser; - if (!empty($item->readmore)) { - $item_text .= '

' . l(t('read more'), 'node/' . $item->nid, array('absolute' => TRUE, 'attributes' => array('target' => '_blank'))) . '

'; - } + $item_text = $item->rendered; + $item_text .= '

' . l(t('read more'), 'node/' . $item->nid, array('absolute' => TRUE, 'attributes' => array('target' => '_blank'))) . '

'; break; case 'title': $item_text = ''; @@ -2103,12 +1910,12 @@ // save the changed time of the most recent indexed node, for the search 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 = '

' . check_plain($node->title) . '

' . $node->body; + $text = '

' . check_plain($node->title) . '

' . $node->rendered; // Fetch extra data normally not visible $extra = node_invoke_node($node, 'update_index'); @@ -2792,10 +2599,6 @@ ); } - if ($type->has_body) { - $form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count); - } - return $form; } Index: node/node.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.pages.inc,v retrieving revision 1.57 diff -u -r1.57 node.pages.inc --- node/node.pages.inc 14 Mar 2009 23:01:36 -0000 1.57 +++ node/node.pages.inc 21 Mar 2009 18:06:46 -0000 @@ -106,7 +106,7 @@ $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; } @@ -267,48 +267,6 @@ } /** - * Return a node body field, with format and teaser. - */ -function node_body_field(&$node, $label, $word_count) { - - // 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'] = '
'; - $form['#suffix'] = '
'; - - $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' => '
', - '#suffix' => '
', - ); - - $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. */ function node_form_delete_submit($form, &$form_state) { @@ -369,16 +327,6 @@ $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); - // 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()) { @@ -404,28 +352,20 @@ $output = '
'; $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('', '', $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, '') === 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. You can insert the delimiter "<!--break-->" (without the quotes) to fine-tune where your post gets split.')); $output .= '

' . t('Preview trimmed version') . '

'; - $output .= drupal_render(node_build(clone $node, TRUE)); + $output .= $trimmed; $output .= '

' . t('Preview full version') . '

'; - $output .= drupal_render(node_build($node, FALSE)); + $output .= $full; } else { - $output .= drupal_render(node_build($node, FALSE)); + $output .= $full; } $output .= "
\n";