diff --git a/quote.module b/quote.module
index 0de554e..49b07d7 100644
--- a/quote.module
+++ b/quote.module
@@ -266,41 +266,91 @@ function _quote_get_node_types($keys = FALSE) {
  *   Filtered text.
  */
 function _quote_filter_process($text) {
-  if (stristr($text, '[quote')) {
-    // Single regexp with callback allowing for theme calls and quote
-    // nesting/recursion with regexp code from
-    // http://de.php.net/manual/en/function.preg-replace-callback.php#85836
-    $text = preg_replace_callback('#\[(quote.*?)]((?>\[(?!/?quote[^[]*?])|[^[]|(?R))*)\[/quote]#is', '_quote_filter_process_callback', $text);
-  }
-
-  return $text;
-}
-
-/**
- * Generate and return the quote theming for a quote occurence found by
- * _quote_filter_process().
- *
- * @param $matches
- *   The RegExp matches (for author and quote) found in _quote_filter_process().
- *
- * @return $output_quote
- *   Themed quote.
- */
-function _quote_filter_process_callback($matches) {
-  static $index = 0;
+  $output = '';      // The processsed output string.
+  $buffer = array(); // Buffer for all yet-unprocessed (nested) quotes
+
+  // Process the input until the next quote tag; split $text before/after tag.
+  while (preg_match('#\\[\\/?quote(?:\\=(.{1,64}?))?\\]#is', $text, $match, NULL)) {
+    $text = explode($match[0], $text, 2);
+
+    if (empty($buffer)) {
+      // The text leading up to this first tag goes in the final output.
+      $output .= $text[0];
+      if ($match[0]{1} != '/') {
+        // Start tag: the full tag goes into the buffer for later processing,
+        // along with the extracted author. (We'll use one of these later.)
+        $buffer[] = $match[0];
+        $buffer[] = empty($match[1]) ? '' : trim($match[1]);
+      }
+      else {
+        // End tag without start. Append this tag to the output unprocessed.
+        $output .= $match[0];
+      }
+    }
+    else {
+      // Not the first tag we encounter (i.e. end tag, or nested start tag).
+
+      // The last element in the array is either an author name, or text which
+      // was processed previously (from a nested quote; see below.)
+      // In the last case, the number of array values is divisible by 3.
+      $processed = '';
+      if (count($buffer) % 3 == 0) {
+        $processed = array_pop($buffer);
+      }
 
-  $nest = ++$index;
+      if ($match[0]{1} != '/') {
+        // Nested start tag: the text before the tag goes in the buffer, then
+        // the full tag and author, for later processing (like above).
+        $buffer[] = $processed . $text[0];
+        $buffer[] = $match[0];
+        $buffer[] = empty($match[1]) ? '' : trim($match[1]);
+      }
+      else {
+        // End tag: process the quote.
+
+        $author = array_pop($buffer);
+        // Delete the 'quote start tag'; we have no use for it here.
+        array_pop($buffer);
+
+        if (empty($buffer)) {
+          // The quote is not nested; append processed text directly to output.
+          $output .= theme('quote', array(
+            'quote_content' => $processed . $text[0],
+            'quote_author' => $author,
+            'nest' => 1));
+        }
+        else {
+          // Keep the processed text the in buffer, to surround it by an
+          // encompassing quote later. The last element is text (not author);
+          // we should append the processed quote to it.
+          $buffer[count($buffer) - 1] .=  theme('quote', array(
+            'quote_content' => $processed . $text[0],
+            'quote_author' => $author,
+            'nest' => 1 + count($buffer) / 3));
+        }
+      }
+    }
 
-  if (!stristr($matches[2], '[quote')) {
-    $index = 0;
+    // Continue processing the remainder of $text.
+    $text = $text[1];
   }
 
-  $quote_author = trim(drupal_substr($matches[1], 6));
-  $quote_content = _quote_filter_process($matches[2]);
+  // If the buffer still contains anything, it's because there were quote tags
+  // without end quotes. Add those as literal strings.
+  while (!empty($buffer)) {
+    // Here, we use the 'quote start tag', not the author.
+    $output .= array_shift($buffer);
+    array_shift($buffer);
+    // The next element, if it exists, is text from a (processed) nested quote.
+    if (!empty($buffer)) {
+      $output .= array_shift($buffer);
+    }
+  }
 
-  $quote_output = theme('quote', array('quote_content' => $quote_content, 'quote_author' => $quote_author, 'nest' => $nest));
+  // Add the remaining text after the last quote.
+  $output .= $text;
 
-  return $quote_output;
+  return $output;
 }
 
 /**
