<?php
/**
 * FIT Parser
 * 
 * $Id$
 * 
 * @author gERD Schaufelberger <gerd@php-tools.net>
 * @author Daniel Jahnke <daniel.jahnke@web.de>
 * @package FIT
 * @subpackage Parser
 * @license LGPL http://www.gnu.org/copyleft/lesser.html
 */

/**
 * FIT Parser
 * 
 * @version 0.1.1
 * @package FIT
 * @subpackage Fixture
 */
class Testing_FIT_Parser
{
   
/**
    * This is where I keep the table data
    * @var array
    */
    
private $_fitTables = array();
    
   
/**
    * Current table the parser  is working on
    * @var int
    */
    
private $_fitTableCurrent 0;

   
/**
    * Current table's row the parser  is working on
    * @var int
    */
    
private $_fitRowCurrent 0;

   
/**
    * Current table's row's colum the parser  is working on
    * @var int
    */
    
private $_fitColCurrent 0;

   
/**
    * CData stack used while parsing tables
    * @var array
    */
    
private $_cData = array();

   
/**
    * tags currently parsing
    * @var array
    */
    
private $_tags = array();

   
/**
    * current depth
    * @var int current depth
    */
    
private $_fitDepth 0;

   
/**
    * known tags to parse
    * @var array
    */
    
protected $_fitTags = array( 'table''tr''td' );
    
   
/**
    * count nodes
    *
    * @param int $table index of table
    * @param int $row index of table's row
    * @return mixed node value or null if not exists
    */
    
public function countChildNodes$table null$row null )
    {
        
// count tables
        
if( $table === null ) {
            
//echo '<pre><br> ' . print_r( count( $this->_fitTables ) , true) ; echo '</pre>';
            
return count$this->_fitTables );
        }
        
        
// table does not exist
        
if( !isset( $this->_fitTables[$table] ) ) {
            return 
null;
        }

        if( 
$row === null ) {
            return 
count$this->_fitTables[$table]['children'] );
        }
        
        
// row does not exist
        
if( !isset( $this->_fitTables[$table]['children'][$row] ) ) {
            return 
null;
        }
        
        return 
count$this->_fitTables[$table]['children'][$row]['children'] );
    }

   
/**
    * receive node value from parsed table
    *
    * @param string $field node's name (tag, before, after, children)
    * @param int $table index of table
    * @param int $row index of table's row
    * @param int $column index of table's row's columns
    * @return mixed node value or null if not exists
    */
    
public function &getNodeValue$field$table$row null$column null )
    {
        
$failed null;
        
        
// select table
        
if( !isset( $this->_fitTables[$table] ) ) {
            return 
$failed;
        }
        
$current =& $this->_fitTables[$table];
        
        
// select row
        
if( $row !== null ) {
            if( !isset( 
$current['children'][$row] ) ) {
                return 
$failed;
            }                        
            
$current =& $current['children'][$row];
            
            
// select column
            
if( $column !== null ) {
                if( !isset( 
$current['children'][$column] ) ) {
                    return 
$failed;
                }
                
$current =& $current['children'][$column];
            }
        }
        
        if( !isset( 
$current[$field] ) ) {
            return 
$failed;
        }
        
        return 
$current[$field];
    }

   
/**
    * set node value from parsed table
    *
    * @param string $field node's name (tag, before, after, children, cData)
    * @param mixed $value node's new value
    * @param int $table index of table
    * @param int $row index of table's row
    * @param int $column index of table's row's columns
    * @return bool true on success
    */
    
public function setNodeValue$field$value$table$row null$column null )
    {
        
$node   =& $this->getNodeValue$field$table$row$column );
        
$node   $value;
        return 
true;
    }
    
   
/**
    * Serialize current tables to string
    *
    * This undoes the Parse function.
    * 
    * @return string $html tables
    */
    
public function serialize() 
    {
        
$html   =   '';
        for( 
$i 0$i count$this->_fitTables ); ++$i ) {
            
$html   .=  $this->_serializeElement$this->_fitTables[$i] );
        }
        return 
$html;
    }
   
   
/**
    * Serailize elements recurive
    * 
    * @param array $el
    * @return string $html
    */
    
private function _serializeElement( &$el 
    {
        
$html   =   '';
        
$html   .=  $el['before'];
        
// starting tag
        
$html   .=  '<' $el['tag'];

        
// attributes
        
foreach( $el['attributes'] as $name => $value ) {
            if( 
$value == null ) {
                
$html .= ' ' $name;
                continue;
            }
            
$html   .= ' ' $name '="' $value '"';
        }
        
$html   .=  '>';
        
        if( !empty( 
$el['children'] ) ) {
            for( 
$i 0$i count$el['children'] ); ++$i ) {
                
$html   .=  $this->_serializeElement$el['children'][$i] ); 
            }
        }
        else {
            
$html   .=  $el['cData'];
        }
        
        
// end tag
        
$html   .=  '</' $el['tag'] . '>';
        
$html   .=  $el['after'];
        
        return 
$html;
    }
    
   
/**
    * Parse HTML for FIT-like-tables
    *
    * Parse string like SAX does: call sub routines based on events. 
    * There are events for "startTag", "endTag" and "cdata". Of course 
    * this is not an XML parser (as the content may not follow XML standards)
    * Also it can only handle a bunch of well known tags. 
    * 
    * @param string $string to be parsed html
    * @return bool true on success
    */
    
public function parse$string 
    {
        
$this->_fitTables       = array();
        
$this->_fitTableCurrent 0;
        
$this->_fitRowCurrent   0;
        
$this->_fitColCurrent   0;
    
        
$regexp =   '/(<(\/?)([[:alnum:]:]*)[[:space:]]*([^>]*)>)/im';

        
$tokens =   preg_split$regexp$string, -1PREG_SPLIT_DELIM_CAPTURE );
        
$this->_cData    = array();
        
        
$count  =   count$tokens );
        if( !empty( 
$tokens[0] ) ) {
            
$this->_characterData$tocken[0] );
        }

        
$i      1;
        while( 
$i $count ) {
            
$fullTag    $tokens[$i];
            
$closing    $tokens[++$i];
            
$tagname    strtolower$tokens[++$i] );
            
$attString  $tokens[++$i];
            
$empty      substr$attString, -);
            
$data       $tokens[++$i];
            
// opening tag

            
if( empty( $closing ) ) {
                
$this->_startTag$fullTag$tagname$attString );
            }

            
// closing tag
            
if( $closing ) {
                
$this->_endTag$fullTag$tagname );
            }

            
// keep character data
            
$this->_characterData$data );            

            ++
$i;
        }

        
// append tailing HTML
        
if( !empty( $this->_cData[$this->_fitDepth] ) ) {
            
$this->_fitTables[$this->_fitTableCurrent]['after'] = $this->_cData[$this->_fitDepth];
        }

        
$this->_fitTableCurrent 0;
        return 
true;
    }
    
   
/**
    * handle starting tag
    * 
    * @param string $full full tag string like: '<table border="17" width="100%">'
    * @param string $tag extacted tag name like table
    * @param string $attString prepased attribute sting like 'border="17" width="100%"'
    * @return bool true on success
    */
    
private function _startTag$full$tag$attString  )
    {
        if( !
in_array$tag$this->_fitTags ) ) {
            
$this->_characterData$full );
            return 
true;
        }

        
$data   = array(
                
'before'        => null,
                
'fullStart'     => $full,
                
'fullEnd'       => null,
                
'tag'           => $tag,
                
'attributes'    => $this->_parseAttributes$attString ),
                
'children'      => array(),
                
'cData'         => '',
                
'after'         => null,
            );
        
        switch( 
$tag ) {
            case 
'table':
                
$data['before'] = $this->_cData[$this->_fitDepth];
                
$data['after']  = null;
                
$this->_cData[$this->_fitDepth] = '';
                
$this->_fitTableCurrent count$this->_fitTables );
                
$this->_fitTables[$this->_fitTableCurrent] = $data;
                break;
                
            case 
'tr':
                
$this->_fitRowCurrent   count$this->_fitTables[$this->_fitTableCurrent]['children'] );
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent] = $data;
                break;
                
            case 
'td':
                
$data['cData']  =   str_replace'&nbsp;'' '$data['cData'] );
                
$this->_fitColCurrent   count$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['children'] );
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['children'][$this->_fitColCurrent] = $data;
                break;
                
             default:
                break;
        }
        ++
$this->_fitDepth;        
        

        return 
true;
    }

   
/**
    * handle end tag
    * 
    * @param string $full full tag string like: '</table>'
    * @param string $tag extacted tag name like 'table'
    * @return bool true on success
    */
    
private function _endTag$full$tag )
    {
        if( !
in_array$tag$this->_fitTags ) ) {
            
$this->_characterData$full );
            return 
true;
        }
        
$cData $this->_cData[$this->_fitDepth];
        switch( 
$tag ) {
            case 
'table':
                
$this->_fitTables[$this->_fitTableCurrent]['fullEnd'] = $full;
                break;
            
            case 
'tr':
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['fullEnd'] = $full;
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['before'] = $cData;
                break;
                
            case 
'td':
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['children'][$this->_fitColCurrent]['fullEnd'] = $full;
                
$cData trimstrip_tags$cData ) );
                
$this->_fitTables[$this->_fitTableCurrent]['children'][$this->_fitRowCurrent]['children'][$this->_fitColCurrent]['cData'] = $cData;
                break;
                
             default:
                break;
        }
        
        unset( 
$this->_cData[$this->_fitDepth] );
        --
$this->_fitDepth;
        
        return 
true;
    }
    
   
/**
    * handle character data
    * 
    * @param string $cdata - the stuff inside tags
    * @return bool true on success
    */
    
private function _characterData$data )
    {
        if( !isset( 
$this->_cData[$this->_fitDepth] ) ) {
            
$this->_cData[$this->_fitDepth] = $data;
            return 
true;
        }
        
$this->_cData[$this->_fitDepth] .= $data;
        return 
true;
    }
    
   
/**
    * Parse an attribute string and build an array
    *
    * @param string attribute string
    * @param array attribute array
    */
    
private function _parseAttributes$string )
    {
        
$attributes = array();
        if( empty( 
$string ) ) {
            return 
$attributes;
        }
        
        
$match = array();
        if( !
preg_match_all('/([a-zA-Z_0-9]+)(=?)([^\s]*)/'$string$match ) ) {
            return 
$attributes;
        }
        
        for( 
$i 0$i count$match[0] ); ++$i ) {
            
$name strtolower$match[1][$i] );
            
            
// attribute has no value
            
if( !$match[2][$i] ) {
                
$attributes[$name] = null;
            }
            
            
$value $match[3][$i];
            
$attributes[$name] = $this->_stripValue$value );
        }
        
        return 
$attributes;
    }

   
/**
    * strip quotes from values to be compliat with some wordish HTML formats
    * 
    * @param string $string value string 
    * @return string stripped value
    */
    
private function _stripValue$string 
    {
        if( !
strlen$string ) ) {
            return 
'';
        }
        
        
// strip quotes
        
$last strlen$string ) - 1;
        if( 
$string[0] == '"' && $string[$last] == '"' ) {
            
$last -= 1;
            return 
substr$string1$last );
        }

        if( 
$string[0] == "'" && $string[$last] == "'" ) {
            
$last -= 1;
            return 
substr$string1$last );
        }
        
        return 
$string;
    }

   
/**
    * delete row
    *
    * delete a row
    * @param int $table
    * @param int $row
    * @return bool true on success
    */
    
public function deleteRow$table$delRow )
    {
        
$row $this->countChildNodes$table );
        
$col $this->countChildNodes$table$row-);

        if( 
$delRow $row || $delRow ) {
            throw new 
Testing_FIT_Exception_Parser'Delete of row failed. Row index out of range!'$table$row );
            return 
false;
        }
        else {
            if( 
$delRow == $this->countChildNodes$table )  ) {
                
// set the after string from to delete row to the last but one row -> last row
                
$this->_fitTables[$table]['children'][$delRow-1]['after'] = $this->_fitTables[$table]['children'][$delRow]['after'];
                
array_pop$this->_fitTables[$table]['children'] );
                return 
true;
            }

            if( 
$delRow == ) {
                
$this->_fitTables[$table]['children'][1]['after'] = $this->_fitTables[$table]['children'][0]['after'];
                
$this->_fitTables[$table]['children'] = array_slice$this->_fitTables[$table]['children'], );
                return 
true;
            }

            
$partOne array_slice$this->_fitTables[$table]['children'], 0$delRow );
            
$partTwo array_slice$this->_fitTables[$table]['children'], $delRow+1$this->countChildNodes$table ) );
            
$merged array_merge$partOne$partTwo );
            
$this->_fitTables[$table]['children'] = $merged;
        }

        return 
true;
    }

   
/**
    * append row
    *
    * append a new row with the format form the old last row to table 
    * @param int $table
    * @return bool true on success
    */
    
public function appendRow$table )
    {
        
$row    =   $this->countChildNodes$table );
        
$col    =   $this->countChildNodes$table$row-);
        
$cell   =   $this->_getCellFormat$table$row );

        
// build new row
        
$newCells = array();
        for( 
$i 0$i $col$i++ ) {
            
array_push$newCells$cell );
        }

        
//get tags and attributes from actual table
        
$newRow = array(
            
'before'     => $this->_fitTables[$table]['children'][$row-1]['before'],
            
'fullStart'  => $this->_fitTables[$table]['children'][$row-1]['fullStart'],
            
'fullEnd'    => $this->_fitTables[$table]['children'][$row-1]['fullEnd'],
            
'tag'        => $this->_fitTables[$table]['children'][$row-1]['tag'],
            
'attributes' => $this->_fitTables[$table]['children'][$row-1]['attributes'],
            
'children'   => $newCells,
            
'cData'      => $this->_fitTables[$table]['children'][$row-1]['cData'],
            
'after'      => $this->_fitTables[$table]['children'][$row-1]['after'],
                    );

        
// unset the after information for last but one row = ""
        
$this->_fitTables[$table]['children'][$row-1]['after'] = "";

        
array_push$this->_fitTables[$table]['children'], $newRow );
        
$this->serialize$this->_fitTables[$table] );

        return 
true;
    }

   
/**
    * insert before row
    *
    * insert a new row in $table before $newRow
    * @param int $table
    * @param int $newRow
    * @return bool true on success
    * @todo look at fixture, because of first, second row and column
    */
    
public function insertBeforeRow$table$newRow )
    {
        
$row $this->countChildNodes$table );
        
$col $this->countChildNodes$table$row-);

       
/*
        * es muss noch festgestell werden, welche fixture auf die tabelle angewandt wird.
        * bei col und row fixture sind die ersten beiden zeilen ( 0 und 1 ) fix!
        * bei action fixture ist die erste spalte und erste zeile fix!
        */
/*
        if( $newRow < 2 ) {
            return false;
        }
*/
        
if( $newRow >= $row ) {
            
$this->appendRow$table );
            return 
true;
        }

        
$insertRow $this->_fitTables[$table]['children'][$newRow];
        for( 
$i 0$i $col$i++ ) {
            
$insertRow['children'][$i]['cData'] = '&nbsp';
        }

        
$part1 = array();
        
$part2 = array();
        
$part3 = array();

        
$part3 $this->_fitTables;
        
$part3[$table]['children'] = null;

        
$part1 array_slice$this->_fitTables[$table]['children'], 0$newRow );
        
$part1[] = $insertRow;

        
$part2 array_slice$this->_fitTables[$table]['children'], $newRow$this->countChildNodes$table ) );
        
$part1 array_merge$part1$part2 );

        
$part3[$table]['children'] = $part1;

        
$this->_fitTables $part3;

        return 
true;
    }

   
/**
    * get cell format
    *
    * build a formatted raw cell
    * @param int $table
    * @param int $row
    * @return array $cell   new row with html attributes if the cell before got some
    */
    
private function _getCellFormat$table$row )
    {
        
// create cell
        
$cell = array(
            
'before'     => $this->_fitTables[$table]['children'][$row-1]['children'][0]['before'],
            
'fullStart'  => $this->_fitTables[$table]['children'][$row-1]['children'][0]['fullStart'],
            
'fullEnd'    => $this->_fitTables[$table]['children'][$row-1]['children'][0]['fullEnd'],
            
'tag'        => $this->_fitTables[$table]['children'][$row-1]['children'][0]['tag'],
            
'attributes' => $this->_fitTables[$table]['children'][$row-1]['children'][0]['attributes'],
            
'children'   => $this->_fitTables[$table]['children'][$row-1]['children'][0]['children'],
            
'cData'      => "&nbsp;",
            
'after'      => $this->_fitTables[$table]['children'][$row-1]['children'][0]['after'],
                    );

        return 
$cell;
    }

   
/**
    * append column
    *
    * append a new column at $table
    * @param int $table
    * @return array $cell contain one cell with attributes
    */
    
public function appendColumn$table )
    {
        
$row $this->countChildNodes$table );
        
$col $this->countChildNodes$table$row-);

        
$cell = array(
                
'before'     => '',
                
'fullStart'  => '',
                
'fullEnd'    => '',
                
'tag'        => 'td',
                
'attributes' => array(),
                
'children'   => array(),
                
'cData'      => '&nbsp;',
                
'after'      => '',
                    );

        for( 
$i 0$i $row$i++ ) {
            
array_push$this->_fitTables[$table]['children'][$i]['children'], $cell );
        }
        return 
true;
    }

   
/**
    * insert before column
    *
    * insert a new column in $table before $column
    * if $column == last col of table the appendColumn function will be called
    *
    * @param int $table
    * @param int $column
    * @return bool true on success
    */
    
public function insertBeforeColumn$table$column )
    {
        if( 
$column <= -) {
            return 
false;
        }

        
$row $this->countChildNodes$table );
        
$col $this->countChildNodes$table$row-);

        if( 
$col == $column ) {
            return 
$this->appendColumn$table );
        }

        
$cell = array(
                
'before'     => '',
                
'fullStart'  => '',
                
'fullEnd'    => '',
                
'tag'        => 'td',
                
'attributes' => array(),
                
'children'   => array(),
                
'cData'      => '&nbsp;',
                
'after'      => '',
                    );

        
$part1 = array();
        
$part2 = array();

        for( 
$i 0$i $row$i++ ) {
            
array_push$part1array_slice$this->_fitTables[$table]['children'][$i]['children'], 0$column ) );
            
array_push$part2array_slice$this->_fitTables[$table]['children'][$i]['children'], $column ) );

            
array_push$part1[$i], $cell );
            for( 
$j 0$j count$part2[$i] ); $j++ ) {
                
array_push$part1[$i], $part2[$i][$j] );
            }
        }

        for( 
$i 0$i $row$i++ ) {
            
$this->_fitTables[$table]['children'][$i]['children'] = $part1[$i];
        }

        return 
true;
    }

   
/**
    * delete before column
    *
    * deletes exactly that $cell in that $row
    * only to use in combination with colspan
    * be careful to use this funtion!
    *
    * @param int $table
    * @param int $row
    * @param int $col
    */
    
public function deleteBeforeColumn$table$row$col )
    {
        
$lastCol count$this->_fitTables[$table]['children'][$row]['children'] );

        if( 
$col == $lastCol ) {
            
$part array_slice$this->_fitTables[$table]['children'][$row]['children'], 0$col );
            
$this->_fitTables[$table]['children'][$row]['children'] = $part;
            return;
        }

        
$part1 = array();
        
$part2 = array();

        
$part1 array_slice$this->_fitTables[$table]['children'][$row]['children'], 0$col );
        
$part2 array_slice$this->_fitTables[$table]['children'][$row]['children'], $col 1$lastCol );

        
$part1 array_merge$part1$part2 );
        
$this->_fitTables[$table]['children'][$row]['children'] = $part1;

        return;
    }

   
/**
    * append table
    *
    * appends some content after $table
    *
    * @param int $table
    * @param int $content   some html content
    */
    
public function appendTable$table$content )
    {
        
// create new table
        
$tmpParser = new Testing_FIT_Parser();
        
$tmpParser->parse$content );
        
$insert $tmpParser->_fitTables[0];

        
$tmpTable = array();
        for( 
$i 0$i <= $table$i++ ) {
            
$tmpTable[] = $this->_fitTables[$i];
        }

        if( 
$tmpTable[$table]['after'] ) {
            
$after $tmpTable[$table]['after'];
            
$tmpTable[$table]['after'] = null;
            
$insert['after'] = $after;
        }

        
$tmpTable[] = $insert;

        
$table += 1;
        if( isset( 
$this->_fitTables[$table] ) ) {
            for( 
$i $table $i $this->countChildNodes(); $i++ ) {
                
$tmpTable[] = $this->_fitTables[$i];
            }
        }

        
$this->_fitTables $tmpTable;
    }

   
/**
    * remove table
    *
    * remove fit summary table
    *
    * @param int $table
    */
    
public function removeTable$table )
    {
        
$after $this->_fitTables[$table+1]['after'];
        
$this->_fitTables[$table]['after'] = $after;
        unset( 
$this->_fitTables[$table+1] );

        return 
true;
    }
}
?>