Currently, Drupal is lacking proper UBB support, so I'd thought it would be a good idea to write a stack-based UBB parser. For me, this is a first, but also a challenge. I am stuck now, and really could use some help. Regarding the code below, I have two questions:

  1. the code gets stuck in a never-ending loop when the first tag in $text is a closing tag (e.g. [/b ]). Why, and what's your suggested solution?
  2. I want the parser to support "double tags" (tags that require an opening and closing, like [b ]) and "single tags" (like [ hr] and perhaps ones you define yourself). I'm not sure on how to implement the handling of single tags here - any suggestions?

PHP Code:
$text 'The [b foo="fVal"]quick [i bar="bVal"]brown[/i] [u]fox[/b] jumped over the [b]lazy[/b] dog. [img src="http://www.google.com/intl/en_ALL/images/logo.gif"]';
         
echo 
ubbParse($text); 

function 
ubbParse($text) {
  
$validDoubleTags = array('b''i''foo''span''table''tr''td''cite');
  
$validSingleTags = array('img''hr');
  
$lastNode = array();
  
$stack    = array();
  
$size     0;

  while (
false !== ($node ubbSearchTag($text))) {
    
// First node, special conditions
    
if (empty($lastNode)) {
      
// Text before node on stack
      
array_push($stacksubstr($text0$node['OPEN']));
      
$size array_push($stack$node); // First node on stack
    
}
    
// All but the first node
    
else {
      
// Opening tag
      
if ('/' != substr($node['TAG'], 01)) {
        
// Text before node on stack
        
array_push($stacksubstr($text$lastNode['CLOSE'] + 1$node['OPEN'] - $lastNode['CLOSE']));
        
// Add node to the stack
        
$size array_push($stack$node);
      }
      
// Closing tag
      
else {
        
$prevText '';
        
// Grab all text on the end of the array
        
$prevText _popText($stack);
        
// Text from lastNode to current node (fox, lazy)
        
$prevText.= substr($text$lastNode['CLOSE'] + 1$node['OPEN'] - $lastNode['CLOSE']);
        
$match = array();
        
        do {
          
$match array_pop($stack);
          if (
in_array($match['TAG'], $validDoubleTags)) {
            
$output "<{$match['TAG']}"
            if (isset(
$match['ATTR'])) { 
              foreach (
$match['ATTR'] as $id => $attribute) {
                
$output.= " $id=\"$attribute\"";
              }
            }
            
$output.= ">{$prevText}</{$match['TAG']}>";
            
$prevText _popText($stack) . $output;
          }
          else {
            
// Do not automagically close the tag
            
$output "[{$match['TAG']}]{$prevText}"
            
$prevText _popText($stack) . $output;
          }
        }
        while (
$match['TAG'] != substr($node['TAG'], 1));
        
        
array_push($stack$prevText);
      }
    }
    
$lastNode $node;
  }
  
$stack[] = substr($text$lastNode['CLOSE'] + 1strlen($text) - $lastNode['CLOSE']);
  
  return 
implode(''$stack);
}

/**
 * Every time it's called, it returns the next tag in $text
 * 
 * @param string $text
 * @return array|false UBB Node or false when no more nodes
 */
function ubbSearchTag($text) {
  static 
$curr 0;

  
$length strlen($text);
  
$open   strpos($text'['$curr);
  
$close  strpos($text']'$curr);

  if (
== $close) {
    return 
false;
  }

  
$slice substr($text$open$close $open);
  
$tag = array();
  
preg_match('/\[([^ \[\]]*) ?/i'$slice$tag);

  
$attributes = array();
  
preg_match_all('/ ([^="\[\]]*)="([^="]*)"/i'$slice$attributes);

  
$node = array(
    
'TAG'   => strtolower($tag[1]),
    
'OPEN'  => $open,
    
'CLOSE' => $close,
  );
  
  foreach (
$attributes[0] as $key => $attribute) {
    
$node['ATTR'][$attributes[1][$key]] = $attributes[2][$key];
  }

  unset(
$tag$attributes);
  
$curr $close 1;

  return 
$node;
}

/**
 * Pops off all array elements that only contain text and return them all as one
 * string
 */
function _popText(&$stack) {
  
$text '';
  while (!
is_array($stack[count($stack) - 1])) {
    
$text array_pop($stack) . $text;
    if (
== count($stack)) {
      break;
    }
  }
  return 
$text;

Any help would be greatly appreciated (and credited for, of course :))!