Index: modules/filter/filter.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.module,v
retrieving revision 1.331
diff -u -p -r1.331 filter.module
--- modules/filter/filter.module	13 May 2010 07:53:02 -0000	1.331
+++ modules/filter/filter.module	23 May 2010 21:41:59 -0000
@@ -1359,13 +1359,10 @@ function _filter_htmlcorrector($text) {
 }
 
 /**
- * Convert line breaks into <p> and <br> in an intelligent fashion.
- * Based on: http://photomatt.net/scripts/autop
+ * Convert line breaks into <p> and <br /> in an intelligent fashion.
  */
 function _filter_autop($text) {
-  // All block level tags
-  $block = '(?:table|thead|tfoot|caption|colgroup|tbody|tr|td|th|div|dl|dd|dt|ul|ol|li|pre|select|form|blockquote|address|p|h[1-6]|hr)';
-
+  global $_p_open, $_tag_stack;
   // Split at <pre>, <script>, <style> and </pre>, </script>, </style> tags.
   // We don't apply any processing to the contents of these tags to avoid messing
   // up code. We look for matched pairs and allow basic nesting. For example:
@@ -1376,6 +1373,7 @@ function _filter_autop($text) {
   $ignore = FALSE;
   $ignoretag = '';
   $output = '';
+  $blocklevel = _filter_autop_blocklevel_tags();
   foreach ($chunks as $i => $chunk) {
     if ($i % 2) {
       // Opening or closing tag?
@@ -1394,23 +1392,28 @@ function _filter_autop($text) {
       }
     }
     elseif (!$ignore) {
-      $chunk = preg_replace('|\n*$|', '', $chunk) . "\n\n"; // just to make things a little easier, pad the end
-      $chunk = preg_replace('|<br />\s*<br />|', "\n\n", $chunk);
-      $chunk = preg_replace('!(<' . $block . '[^>]*>)!', "\n$1", $chunk); // Space things out a little
-      $chunk = preg_replace('!(</' . $block . '>)!', "$1\n\n", $chunk); // Space things out a little
-      $chunk = preg_replace("/\n\n+/", "\n\n", $chunk); // take care of duplicates
-      $chunk = preg_replace('/^\n|\n\s*\n$/', '', $chunk);
-      $chunk = '<p>' . preg_replace('/\n\s*\n\n?(.)/', "</p>\n<p>$1", $chunk) . "</p>\n"; // make paragraphs, including one at the end
-      $chunk = preg_replace("|<p>(<li.+?)</p>|", "$1", $chunk); // problem with nested lists
-      $chunk = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $chunk);
-      $chunk = str_replace('</blockquote></p>', '</p></blockquote>', $chunk);
-      $chunk = preg_replace('|<p>\s*</p>\n?|', '', $chunk); // under certain strange conditions it could create a P of entirely whitespace
-      $chunk = preg_replace('!<p>\s*(</?' . $block . '[^>]*>)!', "$1", $chunk);
-      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*</p>!', "$1", $chunk);
-      $chunk = preg_replace('|(?<!<br />)\s*\n|', "<br />\n", $chunk); // make line breaks
-      $chunk = preg_replace('!(</?' . $block . '[^>]*>)\s*<br />!', "$1", $chunk);
-      $chunk = preg_replace('!<br />(\s*</?(?:p|li|div|th|pre|td|ul|ol)>)!', '$1', $chunk);
-      $chunk = preg_replace('/&([^#])(?![A-Za-z0-9]{1,8};)/', '&amp;$1', $chunk);
+      $_p_open = FALSE;
+      $_tag_stack = array();
+      // Normalize line endings.
+      $chunk = str_replace(array("\r\n", "\r"), "\n", $chunk);
+      // Convert double break tags to double newlines, so they can be
+      // interpreted as paragraphs later.
+      $chunk = preg_replace('~<br />\s*<br />~', "\n\n", $chunk);
+      // Add line breaks inside divs, so paragraphs can start immediately
+      // after a <div> tag.
+      $chunk = preg_replace("~(<div[^>]*>)([^\s])~", "$1\n$2", $chunk);
+      $chunk = preg_replace("~([^\s])(</div[^>]*>)~", "$1\n$2", $chunk);
+      // Send chunks deliminated by whitespace to our callback.
+      $chunk = preg_replace_callback('~.+($|\s+)~', '_filter_autop_callback', $chunk);
+      if ($_p_open) {
+        // Close the dangling paragraph tag.
+        $chunk = preg_replace('~(\s*)$~', '</p>$1', $chunk, 1);
+      }
+      // Remove trailing whitespace from inside paragrahs.
+      $chunk = preg_replace('~(\s+)</p>~', '</p>$1', $chunk);
+      // Convert single newlines to <br />.
+      $chunk = preg_replace("~([^\n])\n([^\n])~", "$1<br />\n$2", $chunk);
+      $chunk = preg_replace("~<br />\n(\s*</?(?:" . $blocklevel . "|legend)[^>]*>)~", "\n$1", $chunk);
     }
     $output .= $chunk;
   }
@@ -1418,6 +1421,57 @@ function _filter_autop($text) {
 }
 
 /**
+ * Internal helper. Convert line breaks in XHTML to <p> and </p>.
+ */
+function _filter_autop_callback($matches) {
+  global $_p_open, $_tag_stack;
+  $blocklevel = _filter_autop_blocklevel_tags() . '|legend';
+  $text = $matches[0];
+  preg_match_all('~<(/)?(\w+)(?: [^/>]*)?>~', $text, $tag_matches);
+  if ($tag_matches[2]) {
+    foreach ($tag_matches[2] as $i => $tag) {
+      $is_close = (bool) $tag_matches[1][$i];
+      if ($is_close && $tag == end($_tag_stack)) {
+        array_pop($_tag_stack);
+      }
+      elseif (!$is_close) {
+        array_push($_tag_stack, $tag);
+      }
+    }
+  }
+  // If no <p> is started and no block level element is imminent, start one.
+  if (!$_p_open && !preg_match('~</?(' . $blocklevel . ')[^>]*>~', $text)) {
+    $_p_open = TRUE;
+    $text = '<p>' . $text;
+  }
+  // If a <p> is started and a block level element is being closed, close <p>.
+  if ($_p_open && preg_match('~</(' . $blocklevel . ')[^>]*>~', $text)) {
+    $_p_open = FALSE;
+    $text = preg_replace('~(\s*)?(</(' . $blocklevel . ')[^>]*>)~', '$1</p>$2', $text, 1);
+  }
+  // If <p> is started and we have double line breaks, convert to </p><p>.
+  if ($_p_open && preg_match("~\n\s*\n+~", $text)) {
+    // Except when we're inside an inline tag, in that case use <br /><br />.
+    if (array_intersect($_tag_stack, explode('|', _filter_autop_inline_tags()))) {
+      return preg_replace("~\n(\s*\n+\s*)~", "<br /><br />\n$1", $text);
+    }
+    return preg_replace("~\n(\s*\n+\s*)~", "</p>\n$1<p>", $text);
+  }
+  return $text;
+}
+
+/**
+ * Internal helper. Return a regular expression listing block level elements.
+ */
+function _filter_autop_blocklevel_tags() {
+  return 'body|table|thead|tfoot|caption|col|colgroup|tbody|tr|td|th|div|p|dl|dd|dt|ul|ol|li|pre|select|option|form|map|area|blockquote|address|math|style|p|h[1-6]|hr|fieldset|section|article|aside|hgroup|header|footer|nav|figure|figcaption|details|menu|summary';
+}
+
+function _filter_autop_inline_tags() {
+  return 'a|abbr|acronym|b|basefont|bdo|big|br|cite|code|dfn|em|font|i|img|input|kbd|label|q|s|samp|select|small|span|strike|strong|sub|sup|textarea|tt|u|var';
+}
+
+/**
  * Filter tips callback for auto-paragraph filter.
  */
 function _filter_autop_tips($filter, $format, $long = FALSE) {
Index: modules/filter/filter.test
===================================================================
RCS file: /cvs/drupal/drupal/modules/filter/filter.test,v
retrieving revision 1.66
diff -u -p -r1.66 filter.test
--- modules/filter/filter.test	11 Apr 2010 18:33:44 -0000	1.66
+++ modules/filter/filter.test	23 May 2010 21:41:59 -0000
@@ -676,6 +676,24 @@ class FilterUnitTestCase extends DrupalU
     $limit = max(ini_get('pcre.backtrack_limit'), ini_get('pcre.recursion_limit'));
     $f = _filter_autop($this->randomName($limit));
     $this->assertNotEqual($f, '', t('Make sure line breaking can process long strings.'));
+
+    $text = <<<EOT
+<form action="http://example.com/">
+  <fieldset class="collapsible collapsed">
+    <legend>This is the legend</legend>
+
+    <div class="collapse-text">
+      <p>Paragraph text</p>
+    </div>
+  </fieldset>
+</form>
+EOT;
+    $f = _filter_autop($text);
+    $this->assertEqual($f, $text, t('Line break filter leaves form markup intact'));
+
+    $f = _filter_autop("<strong>Line 1 \n Line2\n\n Line3</strong>");
+    $f = preg_replace('~[\r\n]*~', '', $f);
+    $this->assertEqual($f, "<p><strong>Line 1 <br /> Line2<br /><br /> Line3</strong></p>", t('Line break filter doesn\'t end paragraphs inside inline tags.'));
   }
 
   /**
