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