--- modules/node/node.module.1.947	Mon Feb 04 14:23:39 2008
+++ modules/node/node.module	Tue Feb 12 14:18:56 2008
@@ -312,7 +312,7 @@ function node_teaser($body, $format = NU
     return $body;
   }
 
-  // If a valid delimiter has been specified, use it to chop off the teaser.
+  // If the delimiter is present, use it to chop off the teaser.
   if ($delimiter !== FALSE) {
     return substr($body, 0, $delimiter);
   }
@@ -332,14 +332,33 @@ function node_teaser($body, $format = NU
     return $body;
   }
 
-  // If the delimiter has not been specified, try to split at paragraph or
+  // The delimiter is not present - now 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);
+  // First, get the initial substring of $body corresponding to the first
+  // $size characters of actual content (i.e. excluding markup).
+  
+  // Split tags from text.
+  $split = preg_split('@(<(?:[a-zA-Z/][^>]*|!--.*?--)>)@', $body, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE);
+  // PHP ensures $split consists of alternating literals and delimiters
+  // and begins and ends with a literal (inserting $null as required).
 
-  // Store the actual length of the UTF8 string -- which might not be the same
-  // as $size.
+  $cum_length = 0;
+  for ($index = 0; $index <= count($split); $index += 2) {
+    $this_length = drupal_strlen($split[$index][0]);
+    $cum_length += $this_length;
+    if ($cum_length > $size) {
+      break;
+    }
+  }
+  if ($cum_length <= $size) {
+    // The whole body is within the character count
+    return $body;
+  }
+  $teaser = substr($body, 0, $split[$index][1]) . drupal_substr($split[$index][0], 0, $size - ($cum_length - $this_length));
+
+  // Store the actual length in bytes of the string - likely to be greater
+  // than $size.
   $max_rpos = strlen($teaser);
 
   // How much to cut off the end of the teaser so that it doesn't end in the
