* @author Daniel Jahnke * @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 '

' . print_r( count( $this->_fitTables ) , true) ; echo '
'; 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 .= ''; $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, -1, PREG_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, -1 ); $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: '' * @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( ' ', ' ', $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: '
' * @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 = trim( strip_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( $string, 1, $last ); } if( $string[0] == "'" && $string[$last] == "'" ) { $last -= 1; return substr( $string, 1, $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-1 ); if( $delRow > $row || $delRow < 0 ) { 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 == 0 ) { $this->_fitTables[$table]['children'][1]['after'] = $this->_fitTables[$table]['children'][0]['after']; $this->_fitTables[$table]['children'] = array_slice( $this->_fitTables[$table]['children'], 1 ); 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-1 ); $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-1 ); /* * 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'] = ' '; } $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' => " ", '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-1 ); $cell = array( 'before' => '', 'fullStart' => '', 'fullEnd' => '', 'tag' => 'td', 'attributes' => array(), 'children' => array(), 'cData' => ' ', '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 <= -1 ) { return false; } $row = $this->countChildNodes( $table ); $col = $this->countChildNodes( $table, $row-1 ); if( $col == $column ) { return $this->appendColumn( $table ); } $cell = array( 'before' => '', 'fullStart' => '', 'fullEnd' => '', 'tag' => 'td', 'attributes' => array(), 'children' => array(), 'cData' => ' ', 'after' => '', ); $part1 = array(); $part2 = array(); for( $i = 0; $i < $row; $i++ ) { array_push( $part1, array_slice( $this->_fitTables[$table]['children'][$i]['children'], 0, $column ) ); array_push( $part2, array_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 - 1 ); $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; } } ?>