MediaWiki  master
ConfEditor.php
Go to the documentation of this file.
00001 <?php
00038 class ConfEditor {
00040         var $text;
00041 
00043         var $tokens;
00044 
00046         var $pos;
00047 
00049         var $lineNum;
00050 
00052         var $colNum;
00053 
00055         var $byteNum;
00056 
00058         var $currentToken;
00059 
00061         var $prevToken;
00062 
00067         var $stateStack;
00068 
00069 
00086         var $pathStack;
00087 
00092         var $pathInfo;
00093 
00097         var $serial;
00098 
00103         var $edits;
00104 
00112         static function test( $text ) {
00113                 try {
00114                         $ce = new self( $text );
00115                         $ce->parse();
00116                 } catch ( ConfEditorParseError $e ) {
00117                         return $e->getMessage() . "\n" . $e->highlight( $text );
00118                 }
00119                 return "OK";
00120         }
00121 
00125         public function __construct( $text ) {
00126                 $this->text = $text;
00127         }
00128 
00165         public function edit( $ops ) {
00166                 $this->parse();
00167 
00168                 $this->edits = array(
00169                         array( 'copy', 0, strlen( $this->text ) )
00170                 );
00171                 foreach ( $ops as $op ) {
00172                         $type = $op['type'];
00173                         $path = $op['path'];
00174                         $value = isset( $op['value'] ) ? $op['value'] : null;
00175                         $key = isset( $op['key'] ) ? $op['key'] : null;
00176 
00177                         switch ( $type ) {
00178                         case 'delete':
00179                                 list( $start, $end ) = $this->findDeletionRegion( $path );
00180                                 $this->replaceSourceRegion( $start, $end, false );
00181                                 break;
00182                         case 'set':
00183                                 if ( isset( $this->pathInfo[$path] ) ) {
00184                                         list( $start, $end ) = $this->findValueRegion( $path );
00185                                         $encValue = $value; // var_export( $value, true );
00186                                         $this->replaceSourceRegion( $start, $end, $encValue );
00187                                         break;
00188                                 }
00189                                 // No existing path, fall through to append
00190                                 $slashPos = strrpos( $path, '/' );
00191                                 $key = var_export( substr( $path, $slashPos + 1 ), true );
00192                                 $path = substr( $path, 0, $slashPos );
00193                                 // Fall through
00194                         case 'append':
00195                                 // Find the last array element
00196                                 $lastEltPath = $this->findLastArrayElement( $path );
00197                                 if ( $lastEltPath === false ) {
00198                                         throw new MWException( "Can't find any element of array \"$path\"" );
00199                                 }
00200                                 $lastEltInfo = $this->pathInfo[$lastEltPath];
00201 
00202                                 // Has it got a comma already?
00203                                 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
00204                                         // No comma, insert one after the value region
00205                                         list( , $end ) = $this->findValueRegion( $lastEltPath );
00206                                         $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
00207                                 }
00208 
00209                                 // Make the text to insert
00210                                 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
00211 
00212                                 if ( $key === null ) {
00213                                         list( $indent, ) = $this->getIndent( $start );
00214                                         $textToInsert = "$indent$value,";
00215                                 } else {
00216                                         list( $indent, $arrowIndent ) =
00217                                                 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
00218                                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00219                                 }
00220                                 $textToInsert .= ( $indent === false ? ' ' : "\n" );
00221 
00222                                 // Insert the item
00223                                 $this->replaceSourceRegion( $end, $end, $textToInsert );
00224                                 break;
00225                         case 'insert':
00226                                 // Find first array element
00227                                 $firstEltPath = $this->findFirstArrayElement( $path );
00228                                 if ( $firstEltPath === false ) {
00229                                         throw new MWException( "Can't find array element of \"$path\"" );
00230                                 }
00231                                 list( $start, ) = $this->findDeletionRegion( $firstEltPath );
00232                                 $info = $this->pathInfo[$firstEltPath];
00233 
00234                                 // Make the text to insert
00235                                 if ( $key === null ) {
00236                                         list( $indent, ) = $this->getIndent( $start );
00237                                         $textToInsert = "$indent$value,";
00238                                 } else {
00239                                         list( $indent, $arrowIndent ) =
00240                                                 $this->getIndent( $start, $key, $info['arrowByte'] );
00241                                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00242                                 }
00243                                 $textToInsert .= ( $indent === false ? ' ' : "\n" );
00244 
00245                                 // Insert the item
00246                                 $this->replaceSourceRegion( $start, $start, $textToInsert );
00247                                 break;
00248                         default:
00249                                 throw new MWException( "Unrecognised operation: \"$type\"" );
00250                         }
00251                 }
00252 
00253                 // Do the edits
00254                 $out = '';
00255                 foreach ( $this->edits as $edit ) {
00256                         if ( $edit[0] == 'copy' ) {
00257                                 $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] );
00258                         } else { // if ( $edit[0] == 'insert' )
00259                                 $out .= $edit[1];
00260                         }
00261                 }
00262 
00263                 // Do a second parse as a sanity check
00264                 $this->text = $out;
00265                 try {
00266                         $this->parse();
00267                 } catch ( ConfEditorParseError $e ) {
00268                         throw new MWException(
00269                                 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
00270                                 $e->getMessage() );
00271                 }
00272                 return $out;
00273         }
00274 
00279         function getVars() {
00280                 $vars = array();
00281                 $this->parse();
00282                 foreach( $this->pathInfo as $path => $data ) {
00283                         if ( $path[0] != '$' )
00284                                 continue;
00285                         $trimmedPath = substr( $path, 1 );
00286                         $name = $data['name'];
00287                         if ( $name[0] == '@' )
00288                                 continue;
00289                         if ( $name[0] == '$' )
00290                                 $name = substr( $name, 1 );
00291                         $parentPath = substr( $trimmedPath, 0,
00292                                 strlen( $trimmedPath ) - strlen( $name ) );
00293                         if( substr( $parentPath, -1 ) == '/' )
00294                                 $parentPath = substr( $parentPath, 0, -1 );
00295 
00296                         $value = substr( $this->text, $data['valueStartByte'],
00297                                 $data['valueEndByte'] - $data['valueStartByte']
00298                         );
00299                         $this->setVar( $vars, $parentPath, $name,
00300                                 $this->parseScalar( $value ) );
00301                 }
00302                 return $vars;
00303         }
00304 
00314         function setVar( &$array, $path, $key, $value ) {
00315                 $pathArr = explode( '/', $path );
00316                 $target =& $array;
00317                 if ( $path !== '' ) {
00318                         foreach ( $pathArr as $p ) {
00319                                 if( !isset( $target[$p] ) )
00320                                         $target[$p] = array();
00321                                 $target =& $target[$p];
00322                         }
00323                 }
00324                 if ( !isset( $target[$key] ) )
00325                         $target[$key] = $value;
00326         }
00327 
00332         function parseScalar( $str ) {
00333                 if ( $str !== '' && $str[0] == '\'' )
00334                         // Single-quoted string
00335                         // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00336                         // appended to the token; without it we ended up reading in the
00337                         // extra quote on the end!
00338                         return strtr( substr( trim( $str ), 1, -1 ),
00339                                 array( '\\\'' => '\'', '\\\\' => '\\' ) );
00340                 if ( $str !== '' && $str[0] == '"' )
00341                         // Double-quoted string
00342                         // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00343                         // appended to the token; without it we ended up reading in the
00344                         // extra quote on the end!
00345                         return stripcslashes( substr( trim( $str ), 1, -1 ) );
00346                 if ( substr( $str, 0, 4 ) == 'true' )
00347                         return true;
00348                 if ( substr( $str, 0, 5 ) == 'false' )
00349                         return false;
00350                 if ( substr( $str, 0, 4 ) == 'null' )
00351                         return null;
00352                 // Must be some kind of numeric value, so let PHP's weak typing
00353                 // be useful for a change
00354                 return $str;
00355         }
00356 
00361         function replaceSourceRegion( $start, $end, $newText = false ) {
00362                 // Split all copy operations with a source corresponding to the region
00363                 // in question.
00364                 $newEdits = array();
00365                 foreach ( $this->edits as $edit ) {
00366                         if ( $edit[0] !== 'copy' ) {
00367                                 $newEdits[] = $edit;
00368                                 continue;
00369                         }
00370                         $copyStart = $edit[1];
00371                         $copyEnd = $edit[2];
00372                         if ( $start >= $copyEnd || $end <= $copyStart ) {
00373                                 // Outside this region
00374                                 $newEdits[] = $edit;
00375                                 continue;
00376                         }
00377                         if ( ( $start < $copyStart && $end > $copyStart )
00378                                 || ( $start < $copyEnd && $end > $copyEnd )
00379                         ) {
00380                                 throw new MWException( "Overlapping regions found, can't do the edit" );
00381                         }
00382                         // Split the copy
00383                         $newEdits[] = array( 'copy', $copyStart, $start );
00384                         if ( $newText !== false ) {
00385                                 $newEdits[] = array( 'insert', $newText );
00386                         }
00387                         $newEdits[] = array( 'copy', $end, $copyEnd );
00388                 }
00389                 $this->edits = $newEdits;
00390         }
00391 
00400         function findDeletionRegion( $pathName ) {
00401                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00402                         throw new MWException( "Can't find path \"$pathName\"" );
00403                 }
00404                 $path = $this->pathInfo[$pathName];
00405                 // Find the start
00406                 $this->firstToken();
00407                 while ( $this->pos != $path['startToken'] ) {
00408                         $this->nextToken();
00409                 }
00410                 $regionStart = $path['startByte'];
00411                 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) {
00412                         $token = $this->getTokenAhead( $offset );
00413                         if ( !$token->isSkip() ) {
00414                                 // If there is other content on the same line, don't move the start point
00415                                 // back, because that will cause the regions to overlap.
00416                                 $regionStart = $path['startByte'];
00417                                 break;
00418                         }
00419                         $lfPos = strrpos( $token->text, "\n" );
00420                         if ( $lfPos === false ) {
00421                                 $regionStart -= strlen( $token->text );
00422                         } else {
00423                                 // The line start does not include the LF
00424                                 $regionStart -= strlen( $token->text ) - $lfPos - 1;
00425                                 break;
00426                         }
00427                 }
00428                 // Find the end
00429                 while ( $this->pos != $path['endToken'] ) {
00430                         $this->nextToken();
00431                 }
00432                 $regionEnd = $path['endByte']; // past the end
00433                 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) {
00434                         $token = $this->getTokenAhead( $offset );
00435                         if ( !$token->isSkip() ) {
00436                                 break;
00437                         }
00438                         $lfPos = strpos( $token->text, "\n" );
00439                         if ( $lfPos === false ) {
00440                                 $regionEnd += strlen( $token->text );
00441                         } else {
00442                                 // This should point past the LF
00443                                 $regionEnd += $lfPos + 1;
00444                                 break;
00445                         }
00446                 }
00447                 return array( $regionStart, $regionEnd );
00448         }
00449 
00460         function findValueRegion( $pathName ) {
00461                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00462                         throw new MWException( "Can't find path \"$pathName\"" );
00463                 }
00464                 $path = $this->pathInfo[$pathName];
00465                 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) {
00466                         throw new MWException( "Can't find value region for path \"$pathName\"" );
00467                 }
00468                 return array( $path['valueStartByte'], $path['valueEndByte'] );
00469         }
00470 
00477         function findLastArrayElement( $path ) {
00478                 // Try for a real element
00479                 $lastEltPath = false;
00480                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00481                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00482                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00483                         if ( $part2 == '@' ) {
00484                                 // Do nothing
00485                         } elseif ( $part1 == "$path/" ) {
00486                                 $lastEltPath = $candidatePath;
00487                         } elseif ( $lastEltPath !== false ) {
00488                                 break;
00489                         }
00490                 }
00491                 if ( $lastEltPath !== false ) {
00492                         return $lastEltPath;
00493                 }
00494 
00495                 // Try for an interstitial element
00496                 $extraPath = false;
00497                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00498                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00499                         if ( $part1 == "$path/" ) {
00500                                 $extraPath = $candidatePath;
00501                         } elseif ( $extraPath !== false ) {
00502                                 break;
00503                         }
00504                 }
00505                 return $extraPath;
00506         }
00507 
00514         function findFirstArrayElement( $path ) {
00515                 // Try for an ordinary element
00516                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00517                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00518                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00519                         if ( $part1 == "$path/" && $part2 != '@' ) {
00520                                 return $candidatePath;
00521                         }
00522                 }
00523 
00524                 // Try for an interstitial element
00525                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00526                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00527                         if ( $part1 == "$path/" ) {
00528                                 return $candidatePath;
00529                         }
00530                 }
00531                 return false;
00532         }
00533 
00539         function getIndent( $pos, $key = false, $arrowPos = false ) {
00540                 $arrowIndent = ' ';
00541                 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) {
00542                         $indentLength = strspn( $this->text, " \t", $pos );
00543                         $indent = substr( $this->text, $pos, $indentLength );
00544                 } else {
00545                         $indent = false;
00546                 }
00547                 if ( $indent !== false && $arrowPos !== false ) {
00548                         $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
00549                         if ( $arrowIndentLength > 0 ) {
00550                                 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
00551                         }
00552                 }
00553                 return array( $indent, $arrowIndent );
00554         }
00555 
00560         public function parse() {
00561                 $this->initParse();
00562                 $this->pushState( 'file' );
00563                 $this->pushPath( '@extra-' . ($this->serial++) );
00564                 $token = $this->firstToken();
00565 
00566                 while ( !$token->isEnd() ) {
00567                         $state = $this->popState();
00568                         if ( !$state ) {
00569                                 $this->error( 'internal error: empty state stack' );
00570                         }
00571 
00572                         switch ( $state ) {
00573                         case 'file':
00574                                 $this->expect( T_OPEN_TAG );
00575                                 $token = $this->skipSpace();
00576                                 if ( $token->isEnd() ) {
00577                                         break 2;
00578                                 }
00579                                 $this->pushState( 'statement', 'file 2' );
00580                                 break;
00581                         case 'file 2':
00582                                 $token = $this->skipSpace();
00583                                 if ( $token->isEnd() ) {
00584                                         break 2;
00585                                 }
00586                                 $this->pushState( 'statement', 'file 2' );
00587                                 break;
00588                         case 'statement':
00589                                 $token = $this->skipSpace();
00590                                 if ( !$this->validatePath( $token->text ) ) {
00591                                         $this->error( "Invalid variable name \"{$token->text}\"" );
00592                                 }
00593                                 $this->nextPath( $token->text );
00594                                 $this->expect( T_VARIABLE );
00595                                 $this->skipSpace();
00596                                 $arrayAssign = false;
00597                                 if ( $this->currentToken()->type == '[' ) {
00598                                         $this->nextToken();
00599                                         $token = $this->skipSpace();
00600                                         if ( !$token->isScalar() ) {
00601                                                 $this->error( "expected a string or number for the array key" );
00602                                         }
00603                                         if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00604                                                 $text = $this->parseScalar( $token->text );
00605                                         } else {
00606                                                 $text = $token->text;
00607                                         }
00608                                         if ( !$this->validatePath( $text ) ) {
00609                                                 $this->error( "Invalid associative array name \"$text\"" );
00610                                         }
00611                                         $this->pushPath( $text );
00612                                         $this->nextToken();
00613                                         $this->skipSpace();
00614                                         $this->expect( ']' );
00615                                         $this->skipSpace();
00616                                         $arrayAssign = true;
00617                                 }
00618                                 $this->expect( '=' );
00619                                 $this->skipSpace();
00620                                 $this->startPathValue();
00621                                 if ( $arrayAssign )
00622                                         $this->pushState( 'expression', 'array assign end' );
00623                                 else
00624                                         $this->pushState( 'expression', 'statement end' );
00625                                 break;
00626                         case 'array assign end':
00627                         case 'statement end':
00628                                 $this->endPathValue();
00629                                 if ( $state == 'array assign end' )
00630                                         $this->popPath();
00631                                 $this->skipSpace();
00632                                 $this->expect( ';' );
00633                                 $this->nextPath( '@extra-' . ($this->serial++) );
00634                                 break;
00635                         case 'expression':
00636                                 $token = $this->skipSpace();
00637                                 if ( $token->type == T_ARRAY ) {
00638                                         $this->pushState( 'array' );
00639                                 } elseif ( $token->isScalar() ) {
00640                                         $this->nextToken();
00641                                 } elseif ( $token->type == T_VARIABLE ) {
00642                                         $this->nextToken();
00643                                 } else {
00644                                         $this->error( "expected simple expression" );
00645                                 }
00646                                 break;
00647                         case 'array':
00648                                 $this->skipSpace();
00649                                 $this->expect( T_ARRAY );
00650                                 $this->skipSpace();
00651                                 $this->expect( '(' );
00652                                 $this->skipSpace();
00653                                 $this->pushPath( '@extra-' . ($this->serial++) );
00654                                 if ( $this->isAhead( ')' ) ) {
00655                                         // Empty array
00656                                         $this->pushState( 'array end' );
00657                                 } else {
00658                                         $this->pushState( 'element', 'array end' );
00659                                 }
00660                                 break;
00661                         case 'array end':
00662                                 $this->skipSpace();
00663                                 $this->popPath();
00664                                 $this->expect( ')' );
00665                                 break;
00666                         case 'element':
00667                                 $token = $this->skipSpace();
00668                                 // Look ahead to find the double arrow
00669                                 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) {
00670                                         // Found associative element
00671                                         $this->pushState( 'assoc-element', 'element end' );
00672                                 } else {
00673                                         // Not associative
00674                                         $this->nextPath( '@next' );
00675                                         $this->startPathValue();
00676                                         $this->pushState( 'expression', 'element end' );
00677                                 }
00678                                 break;
00679                         case 'element end':
00680                                 $token = $this->skipSpace();
00681                                 if ( $token->type == ',' ) {
00682                                         $this->endPathValue();
00683                                         $this->markComma();
00684                                         $this->nextToken();
00685                                         $this->nextPath( '@extra-' . ($this->serial++) );
00686                                         // Look ahead to find ending bracket
00687                                         if ( $this->isAhead( ")" ) ) {
00688                                                 // Found ending bracket, no continuation
00689                                                 $this->skipSpace();
00690                                         } else {
00691                                                 // No ending bracket, continue to next element
00692                                                 $this->pushState( 'element' );
00693                                         }
00694                                 } elseif ( $token->type == ')' ) {
00695                                         // End array
00696                                         $this->endPathValue();
00697                                 } else {
00698                                         $this->error( "expected the next array element or the end of the array" );
00699                                 }
00700                                 break;
00701                         case 'assoc-element':
00702                                 $token = $this->skipSpace();
00703                                 if ( !$token->isScalar() ) {
00704                                         $this->error( "expected a string or number for the array key" );
00705                                 }
00706                                 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00707                                         $text = $this->parseScalar( $token->text );
00708                                 } else {
00709                                         $text = $token->text;
00710                                 }
00711                                 if ( !$this->validatePath( $text ) ) {
00712                                         $this->error( "Invalid associative array name \"$text\"" );
00713                                 }
00714                                 $this->nextPath( $text );
00715                                 $this->nextToken();
00716                                 $this->skipSpace();
00717                                 $this->markArrow();
00718                                 $this->expect( T_DOUBLE_ARROW );
00719                                 $this->skipSpace();
00720                                 $this->startPathValue();
00721                                 $this->pushState( 'expression' );
00722                                 break;
00723                         }
00724                 }
00725                 if ( count( $this->stateStack ) ) {
00726                         $this->error( 'unexpected end of file' );
00727                 }
00728                 $this->popPath();
00729         }
00730 
00734         protected function initParse() {
00735                 $this->tokens = token_get_all( $this->text );
00736                 $this->stateStack = array();
00737                 $this->pathStack = array();
00738                 $this->firstToken();
00739                 $this->pathInfo = array();
00740                 $this->serial = 1;
00741         }
00742 
00747         protected function setPos( $pos ) {
00748                 $this->pos = $pos;
00749                 if ( $this->pos >= count( $this->tokens ) ) {
00750                         $this->currentToken = ConfEditorToken::newEnd();
00751                 } else {
00752                         $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] );
00753                 }
00754                 return $this->currentToken;
00755         }
00756 
00761         function newTokenObj( $internalToken ) {
00762                 if ( is_array( $internalToken ) ) {
00763                         return new ConfEditorToken( $internalToken[0], $internalToken[1] );
00764                 } else {
00765                         return new ConfEditorToken( $internalToken, $internalToken );
00766                 }
00767         }
00768 
00772         function firstToken() {
00773                 $this->setPos( 0 );
00774                 $this->prevToken = ConfEditorToken::newEnd();
00775                 $this->lineNum = 1;
00776                 $this->colNum = 1;
00777                 $this->byteNum = 0;
00778                 return $this->currentToken;
00779         }
00780 
00784         function currentToken() {
00785                 return $this->currentToken;
00786         }
00787 
00791         function nextToken() {
00792                 if ( $this->currentToken ) {
00793                         $text = $this->currentToken->text;
00794                         $lfCount = substr_count( $text, "\n" );
00795                         if ( $lfCount ) {
00796                                 $this->lineNum += $lfCount;
00797                                 $this->colNum = strlen( $text ) - strrpos( $text, "\n" );
00798                         } else {
00799                                 $this->colNum += strlen( $text );
00800                         }
00801                         $this->byteNum += strlen( $text );
00802                 }
00803                 $this->prevToken = $this->currentToken;
00804                 $this->setPos( $this->pos + 1 );
00805                 return $this->currentToken;
00806         }
00807 
00813         function getTokenAhead( $offset ) {
00814                 $pos = $this->pos + $offset;
00815                 if ( $pos >= count( $this->tokens ) || $pos < 0 ) {
00816                         return ConfEditorToken::newEnd();
00817                 } else {
00818                         return $this->newTokenObj( $this->tokens[$pos] );
00819                 }
00820         }
00821 
00825         function skipSpace() {
00826                 while ( $this->currentToken && $this->currentToken->isSkip() ) {
00827                         $this->nextToken();
00828                 }
00829                 return $this->currentToken;
00830         }
00831 
00836         function expect( $type ) {
00837                 if ( $this->currentToken && $this->currentToken->type == $type ) {
00838                         return $this->nextToken();
00839                 } else {
00840                         $this->error( "expected " . $this->getTypeName( $type ) .
00841                                 ", got " . $this->getTypeName( $this->currentToken->type ) );
00842                 }
00843         }
00844 
00848         function pushState( $nextState, $stateAfterThat = null ) {
00849                 if ( $stateAfterThat !== null ) {
00850                         $this->stateStack[] = $stateAfterThat;
00851                 }
00852                 $this->stateStack[] = $nextState;
00853         }
00854 
00859         function popState() {
00860                 return array_pop( $this->stateStack );
00861         }
00862 
00868         function validatePath( $path ) {
00869                 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
00870         }
00871 
00876         function endPath() {
00877                 $key = '';
00878                 foreach ( $this->pathStack as $pathInfo ) {
00879                         if ( $key !== '' ) {
00880                                 $key .= '/';
00881                         }
00882                         $key .= $pathInfo['name'];
00883                 }
00884                 $pathInfo['endByte'] = $this->byteNum;
00885                 $pathInfo['endToken'] = $this->pos;
00886                 $this->pathInfo[$key] = $pathInfo;
00887         }
00888 
00892         function pushPath( $path ) {
00893                 $this->pathStack[] = array(
00894                         'name' => $path,
00895                         'level' => count( $this->pathStack ) + 1,
00896                         'startByte' => $this->byteNum,
00897                         'startToken' => $this->pos,
00898                         'valueStartToken' => false,
00899                         'valueStartByte' => false,
00900                         'valueEndToken' => false,
00901                         'valueEndByte' => false,
00902                         'nextArrayIndex' => 0,
00903                         'hasComma' => false,
00904                         'arrowByte' => false
00905                 );
00906         }
00907 
00911         function popPath() {
00912                 $this->endPath();
00913                 array_pop( $this->pathStack );
00914         }
00915 
00921         function nextPath( $path ) {
00922                 $this->endPath();
00923                 $i = count( $this->pathStack ) - 1;
00924                 if ( $path == '@next' ) {
00925                         $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex'];
00926                         $this->pathStack[$i]['name'] = $nextArrayIndex;
00927                         $nextArrayIndex++;
00928                 } else {
00929                         $this->pathStack[$i]['name'] = $path;
00930                 }
00931                 $this->pathStack[$i] =
00932                         array(
00933                                 'startByte' => $this->byteNum,
00934                                 'startToken' => $this->pos,
00935                                 'valueStartToken' => false,
00936                                 'valueStartByte' => false,
00937                                 'valueEndToken' => false,
00938                                 'valueEndByte' => false,
00939                                 'hasComma' => false,
00940                                 'arrowByte' => false,
00941                         ) + $this->pathStack[$i];
00942         }
00943 
00947         function startPathValue() {
00948                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00949                 $path['valueStartToken'] = $this->pos;
00950                 $path['valueStartByte'] = $this->byteNum;
00951         }
00952 
00956         function endPathValue() {
00957                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00958                 $path['valueEndToken'] = $this->pos;
00959                 $path['valueEndByte'] = $this->byteNum;
00960         }
00961 
00965         function markComma() {
00966                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00967                 $path['hasComma'] = true;
00968         }
00969 
00973         function markArrow() {
00974                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00975                 $path['arrowByte'] = $this->byteNum;
00976         }
00977 
00981         function error( $msg ) {
00982                 throw new ConfEditorParseError( $this, $msg );
00983         }
00984 
00989         function getTypeName( $type ) {
00990                 if ( is_int( $type ) ) {
00991                         return token_name( $type );
00992                 } else {
00993                         return "\"$type\"";
00994                 }
00995         }
00996 
01003         function isAhead( $type, $offset = 0 ) {
01004                 $ahead = $offset;
01005                 $token = $this->getTokenAhead( $offset );
01006                 while ( !$token->isEnd() ) {
01007                         if ( $token->isSkip() ) {
01008                                 $ahead++;
01009                                 $token = $this->getTokenAhead( $ahead );
01010                                 continue;
01011                         } elseif ( $token->type == $type ) {
01012                                 // Found the type
01013                                 return true;
01014                         } else {
01015                                 // Not found
01016                                 return false;
01017                         }
01018                 }
01019                 return false;
01020         }
01021 
01025         function prevToken() {
01026                 return $this->prevToken;
01027         }
01028 
01032         function dumpTokens() {
01033                 $out = '';
01034                 foreach ( $this->tokens as $token ) {
01035                         $obj = $this->newTokenObj( $token );
01036                         $out .= sprintf( "%-28s %s\n",
01037                                 $this->getTypeName( $obj->type ),
01038                                 addcslashes( $obj->text, "\0..\37" ) );
01039                 }
01040                 echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
01041         }
01042 }
01043 
01047 class ConfEditorParseError extends MWException {
01048         var $lineNum, $colNum;
01049         function __construct( $editor, $msg ) {
01050                 $this->lineNum = $editor->lineNum;
01051                 $this->colNum = $editor->colNum;
01052                 parent::__construct( "Parse error on line {$editor->lineNum} " .
01053                         "col {$editor->colNum}: $msg" );
01054         }
01055 
01056         function highlight( $text ) {
01057                 $lines = StringUtils::explode( "\n", $text );
01058                 foreach ( $lines as $lineNum => $line ) {
01059                         if ( $lineNum == $this->lineNum - 1 ) {
01060                                 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n";
01061                         }
01062                 }
01063         }
01064 
01065 }
01066 
01070 class ConfEditorToken {
01071         var $type, $text;
01072 
01073         static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING );
01074         static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT );
01075 
01076         static function newEnd() {
01077                 return new self( 'END', '' );
01078         }
01079 
01080         function __construct( $type, $text ) {
01081                 $this->type = $type;
01082                 $this->text = $text;
01083         }
01084 
01085         function isSkip() {
01086                 return in_array( $this->type, self::$skipTypes );
01087         }
01088 
01089         function isScalar() {
01090                 return in_array( $this->type, self::$scalarTypes );
01091         }
01092 
01093         function isEnd() {
01094                 return $this->type == 'END';
01095         }
01096 }
01097