MediaWiki  REL1_19
ConfEditor.php
Go to the documentation of this file.
00001 <?php
00002 
00018 class ConfEditor {
00020         var $text;
00021 
00023         var $tokens;
00024 
00026         var $pos;
00027 
00029         var $lineNum;
00030 
00032         var $colNum;
00033 
00035         var $byteNum;
00036 
00038         var $currentToken;
00039 
00041         var $prevToken;
00042 
00047         var $stateStack;
00048 
00049 
00066         var $pathStack;
00067 
00072         var $pathInfo;
00073 
00077         var $serial;
00078 
00083         var $edits;
00084 
00092         static function test( $text ) {
00093                 try {
00094                         $ce = new self( $text );
00095                         $ce->parse();
00096                 } catch ( ConfEditorParseError $e ) {
00097                         return $e->getMessage() . "\n" . $e->highlight( $text );
00098                 }
00099                 return "OK";
00100         }
00101 
00105         public function __construct( $text ) {
00106                 $this->text = $text;
00107         }
00108 
00143         public function edit( $ops ) {
00144                 $this->parse();
00145 
00146                 $this->edits = array(
00147                         array( 'copy', 0, strlen( $this->text ) )
00148                 );
00149                 foreach ( $ops as $op ) {
00150                         $type = $op['type'];
00151                         $path = $op['path'];
00152                         $value = isset( $op['value'] ) ? $op['value'] : null;
00153                         $key = isset( $op['key'] ) ? $op['key'] : null;
00154 
00155                         switch ( $type ) {
00156                         case 'delete':
00157                                 list( $start, $end ) = $this->findDeletionRegion( $path );
00158                                 $this->replaceSourceRegion( $start, $end, false );
00159                                 break;
00160                         case 'set':
00161                                 if ( isset( $this->pathInfo[$path] ) ) {
00162                                         list( $start, $end ) = $this->findValueRegion( $path );
00163                                         $encValue = $value; // var_export( $value, true );
00164                                         $this->replaceSourceRegion( $start, $end, $encValue );
00165                                         break;
00166                                 }
00167                                 // No existing path, fall through to append
00168                                 $slashPos = strrpos( $path, '/' );
00169                                 $key = var_export( substr( $path, $slashPos + 1 ), true );
00170                                 $path = substr( $path, 0, $slashPos );
00171                                 // Fall through
00172                         case 'append':
00173                                 // Find the last array element
00174                                 $lastEltPath = $this->findLastArrayElement( $path );
00175                                 if ( $lastEltPath === false ) {
00176                                         throw new MWException( "Can't find any element of array \"$path\"" );
00177                                 }
00178                                 $lastEltInfo = $this->pathInfo[$lastEltPath];
00179 
00180                                 // Has it got a comma already?
00181                                 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
00182                                         // No comma, insert one after the value region
00183                                         list( , $end ) = $this->findValueRegion( $lastEltPath );
00184                                         $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
00185                                 }
00186 
00187                                 // Make the text to insert
00188                                 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
00189 
00190                                 if ( $key === null ) {
00191                                         list( $indent, ) = $this->getIndent( $start );
00192                                         $textToInsert = "$indent$value,";
00193                                 } else {
00194                                         list( $indent, $arrowIndent ) =
00195                                                 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
00196                                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00197                                 }
00198                                 $textToInsert .= ( $indent === false ? ' ' : "\n" );
00199 
00200                                 // Insert the item
00201                                 $this->replaceSourceRegion( $end, $end, $textToInsert );
00202                                 break;
00203                         case 'insert':
00204                                 // Find first array element
00205                                 $firstEltPath = $this->findFirstArrayElement( $path );
00206                                 if ( $firstEltPath === false ) {
00207                                         throw new MWException( "Can't find array element of \"$path\"" );
00208                                 }
00209                                 list( $start, ) = $this->findDeletionRegion( $firstEltPath );
00210                                 $info = $this->pathInfo[$firstEltPath];
00211 
00212                                 // Make the text to insert
00213                                 if ( $key === null ) {
00214                                         list( $indent, ) = $this->getIndent( $start );
00215                                         $textToInsert = "$indent$value,";
00216                                 } else {
00217                                         list( $indent, $arrowIndent ) =
00218                                                 $this->getIndent( $start, $key, $info['arrowByte'] );
00219                                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00220                                 }
00221                                 $textToInsert .= ( $indent === false ? ' ' : "\n" );
00222 
00223                                 // Insert the item
00224                                 $this->replaceSourceRegion( $start, $start, $textToInsert );
00225                                 break;
00226                         default:
00227                                 throw new MWException( "Unrecognised operation: \"$type\"" );
00228                         }
00229                 }
00230 
00231                 // Do the edits
00232                 $out = '';
00233                 foreach ( $this->edits as $edit ) {
00234                         if ( $edit[0] == 'copy' ) {
00235                                 $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] );
00236                         } else { // if ( $edit[0] == 'insert' )
00237                                 $out .= $edit[1];
00238                         }
00239                 }
00240 
00241                 // Do a second parse as a sanity check
00242                 $this->text = $out;
00243                 try {
00244                         $this->parse();
00245                 } catch ( ConfEditorParseError $e ) {
00246                         throw new MWException(
00247                                 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
00248                                 $e->getMessage() );
00249                 }
00250                 return $out;
00251         }
00252 
00257         function getVars() {
00258                 $vars = array();
00259                 $this->parse();
00260                 foreach( $this->pathInfo as $path => $data ) {
00261                         if ( $path[0] != '$' )
00262                                 continue;
00263                         $trimmedPath = substr( $path, 1 );
00264                         $name = $data['name'];
00265                         if ( $name[0] == '@' )
00266                                 continue;
00267                         if ( $name[0] == '$' )
00268                                 $name = substr( $name, 1 );
00269                         $parentPath = substr( $trimmedPath, 0,
00270                                 strlen( $trimmedPath ) - strlen( $name ) );
00271                         if( substr( $parentPath, -1 ) == '/' )
00272                                 $parentPath = substr( $parentPath, 0, -1 );
00273 
00274                         $value = substr( $this->text, $data['valueStartByte'],
00275                                 $data['valueEndByte'] - $data['valueStartByte']
00276                         );
00277                         $this->setVar( $vars, $parentPath, $name,
00278                                 $this->parseScalar( $value ) );
00279                 }
00280                 return $vars;
00281         }
00282 
00292         function setVar( &$array, $path, $key, $value ) {
00293                 $pathArr = explode( '/', $path );
00294                 $target =& $array;
00295                 if ( $path !== '' ) {
00296                         foreach ( $pathArr as $p ) {
00297                                 if( !isset( $target[$p] ) )
00298                                         $target[$p] = array();
00299                                 $target =& $target[$p];
00300                         }
00301                 }
00302                 if ( !isset( $target[$key] ) )
00303                         $target[$key] = $value;
00304         }
00305 
00310         function parseScalar( $str ) {
00311                 if ( $str !== '' && $str[0] == '\'' )
00312                         // Single-quoted string
00313                         // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00314                         // appended to the token; without it we ended up reading in the
00315                         // extra quote on the end!
00316                         return strtr( substr( trim( $str ), 1, -1 ),
00317                                 array( '\\\'' => '\'', '\\\\' => '\\' ) );
00318                 if ( $str !== '' && $str[0] == '"' )
00319                         // Double-quoted string
00320                         // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00321                         // appended to the token; without it we ended up reading in the
00322                         // extra quote on the end!
00323                         return stripcslashes( substr( trim( $str ), 1, -1 ) );
00324                 if ( substr( $str, 0, 4 ) == 'true' )
00325                         return true;
00326                 if ( substr( $str, 0, 5 ) == 'false' )
00327                         return false;
00328                 if ( substr( $str, 0, 4 ) == 'null' )
00329                         return null;
00330                 // Must be some kind of numeric value, so let PHP's weak typing
00331                 // be useful for a change
00332                 return $str;
00333         }
00334 
00339         function replaceSourceRegion( $start, $end, $newText = false ) {
00340                 // Split all copy operations with a source corresponding to the region
00341                 // in question.
00342                 $newEdits = array();
00343                 foreach ( $this->edits as $edit ) {
00344                         if ( $edit[0] !== 'copy' ) {
00345                                 $newEdits[] = $edit;
00346                                 continue;
00347                         }
00348                         $copyStart = $edit[1];
00349                         $copyEnd = $edit[2];
00350                         if ( $start >= $copyEnd || $end <= $copyStart ) {
00351                                 // Outside this region
00352                                 $newEdits[] = $edit;
00353                                 continue;
00354                         }
00355                         if ( ( $start < $copyStart && $end > $copyStart )
00356                                 || ( $start < $copyEnd && $end > $copyEnd )
00357                         ) {
00358                                 throw new MWException( "Overlapping regions found, can't do the edit" );
00359                         }
00360                         // Split the copy
00361                         $newEdits[] = array( 'copy', $copyStart, $start );
00362                         if ( $newText !== false ) {
00363                                 $newEdits[] = array( 'insert', $newText );
00364                         }
00365                         $newEdits[] = array( 'copy', $end, $copyEnd );
00366                 }
00367                 $this->edits = $newEdits;
00368         }
00369 
00375         function findDeletionRegion( $pathName ) {
00376                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00377                         throw new MWException( "Can't find path \"$pathName\"" );
00378                 }
00379                 $path = $this->pathInfo[$pathName];
00380                 // Find the start
00381                 $this->firstToken();
00382                 while ( $this->pos != $path['startToken'] ) {
00383                         $this->nextToken();
00384                 }
00385                 $regionStart = $path['startByte'];
00386                 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) {
00387                         $token = $this->getTokenAhead( $offset );
00388                         if ( !$token->isSkip() ) {
00389                                 // If there is other content on the same line, don't move the start point
00390                                 // back, because that will cause the regions to overlap.
00391                                 $regionStart = $path['startByte'];
00392                                 break;
00393                         }
00394                         $lfPos = strrpos( $token->text, "\n" );
00395                         if ( $lfPos === false ) {
00396                                 $regionStart -= strlen( $token->text );
00397                         } else {
00398                                 // The line start does not include the LF
00399                                 $regionStart -= strlen( $token->text ) - $lfPos - 1;
00400                                 break;
00401                         }
00402                 }
00403                 // Find the end
00404                 while ( $this->pos != $path['endToken'] ) {
00405                         $this->nextToken();
00406                 }
00407                 $regionEnd = $path['endByte']; // past the end
00408                 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) {
00409                         $token = $this->getTokenAhead( $offset );
00410                         if ( !$token->isSkip() ) {
00411                                 break;
00412                         }
00413                         $lfPos = strpos( $token->text, "\n" );
00414                         if ( $lfPos === false ) {
00415                                 $regionEnd += strlen( $token->text );
00416                         } else {
00417                                 // This should point past the LF
00418                                 $regionEnd += $lfPos + 1;
00419                                 break;
00420                         }
00421                 }
00422                 return array( $regionStart, $regionEnd );
00423         }
00424 
00432         function findValueRegion( $pathName ) {
00433                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00434                         throw new MWException( "Can't find path \"$pathName\"" );
00435                 }
00436                 $path = $this->pathInfo[$pathName];
00437                 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) {
00438                         throw new MWException( "Can't find value region for path \"$pathName\"" );
00439                 }
00440                 return array( $path['valueStartByte'], $path['valueEndByte'] );
00441         }
00442 
00448         function findLastArrayElement( $path ) {
00449                 // Try for a real element
00450                 $lastEltPath = false;
00451                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00452                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00453                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00454                         if ( $part2 == '@' ) {
00455                                 // Do nothing
00456                         } elseif ( $part1 == "$path/" ) {
00457                                 $lastEltPath = $candidatePath;
00458                         } elseif ( $lastEltPath !== false ) {
00459                                 break;
00460                         }
00461                 }
00462                 if ( $lastEltPath !== false ) {
00463                         return $lastEltPath;
00464                 }
00465 
00466                 // Try for an interstitial element
00467                 $extraPath = false;
00468                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00469                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00470                         if ( $part1 == "$path/" ) {
00471                                 $extraPath = $candidatePath;
00472                         } elseif ( $extraPath !== false ) {
00473                                 break;
00474                         }
00475                 }
00476                 return $extraPath;
00477         }
00478 
00484         function findFirstArrayElement( $path ) {
00485                 // Try for an ordinary element
00486                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00487                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00488                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00489                         if ( $part1 == "$path/" && $part2 != '@' ) {
00490                                 return $candidatePath;
00491                         }
00492                 }
00493 
00494                 // Try for an interstitial element
00495                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00496                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00497                         if ( $part1 == "$path/" ) {
00498                                 return $candidatePath;
00499                         }
00500                 }
00501                 return false;
00502         }
00503 
00508         function getIndent( $pos, $key = false, $arrowPos = false ) {
00509                 $arrowIndent = ' ';
00510                 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) {
00511                         $indentLength = strspn( $this->text, " \t", $pos );
00512                         $indent = substr( $this->text, $pos, $indentLength );
00513                 } else {
00514                         $indent = false;
00515                 }
00516                 if ( $indent !== false && $arrowPos !== false ) {
00517                         $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
00518                         if ( $arrowIndentLength > 0 ) {
00519                                 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
00520                         }
00521                 }
00522                 return array( $indent, $arrowIndent );
00523         }
00524 
00529         public function parse() {
00530                 $this->initParse();
00531                 $this->pushState( 'file' );
00532                 $this->pushPath( '@extra-' . ($this->serial++) );
00533                 $token = $this->firstToken();
00534 
00535                 while ( !$token->isEnd() ) {
00536                         $state = $this->popState();
00537                         if ( !$state ) {
00538                                 $this->error( 'internal error: empty state stack' );
00539                         }
00540 
00541                         switch ( $state ) {
00542                         case 'file':
00543                                 $this->expect( T_OPEN_TAG );
00544                                 $token = $this->skipSpace();
00545                                 if ( $token->isEnd() ) {
00546                                         break 2;
00547                                 }
00548                                 $this->pushState( 'statement', 'file 2' );
00549                                 break;
00550                         case 'file 2':
00551                                 $token = $this->skipSpace();
00552                                 if ( $token->isEnd() ) {
00553                                         break 2;
00554                                 }
00555                                 $this->pushState( 'statement', 'file 2' );
00556                                 break;
00557                         case 'statement':
00558                                 $token = $this->skipSpace();
00559                                 if ( !$this->validatePath( $token->text ) ) {
00560                                         $this->error( "Invalid variable name \"{$token->text}\"" );
00561                                 }
00562                                 $this->nextPath( $token->text );
00563                                 $this->expect( T_VARIABLE );
00564                                 $this->skipSpace();
00565                                 $arrayAssign = false;
00566                                 if ( $this->currentToken()->type == '[' ) {
00567                                         $this->nextToken();
00568                                         $token = $this->skipSpace();
00569                                         if ( !$token->isScalar() ) {
00570                                                 $this->error( "expected a string or number for the array key" );
00571                                         }
00572                                         if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00573                                                 $text = $this->parseScalar( $token->text );
00574                                         } else {
00575                                                 $text = $token->text;
00576                                         }
00577                                         if ( !$this->validatePath( $text ) ) {
00578                                                 $this->error( "Invalid associative array name \"$text\"" );
00579                                         }
00580                                         $this->pushPath( $text );
00581                                         $this->nextToken();
00582                                         $this->skipSpace();
00583                                         $this->expect( ']' );
00584                                         $this->skipSpace();
00585                                         $arrayAssign = true;
00586                                 }
00587                                 $this->expect( '=' );
00588                                 $this->skipSpace();
00589                                 $this->startPathValue();
00590                                 if ( $arrayAssign )
00591                                         $this->pushState( 'expression', 'array assign end' );
00592                                 else
00593                                         $this->pushState( 'expression', 'statement end' );
00594                                 break;
00595                         case 'array assign end':
00596                         case 'statement end':
00597                                 $this->endPathValue();
00598                                 if ( $state == 'array assign end' )
00599                                         $this->popPath();
00600                                 $this->skipSpace();
00601                                 $this->expect( ';' );
00602                                 $this->nextPath( '@extra-' . ($this->serial++) );
00603                                 break;
00604                         case 'expression':
00605                                 $token = $this->skipSpace();
00606                                 if ( $token->type == T_ARRAY ) {
00607                                         $this->pushState( 'array' );
00608                                 } elseif ( $token->isScalar() ) {
00609                                         $this->nextToken();
00610                                 } elseif ( $token->type == T_VARIABLE ) {
00611                                         $this->nextToken();
00612                                 } else {
00613                                         $this->error( "expected simple expression" );
00614                                 }
00615                                 break;
00616                         case 'array':
00617                                 $this->skipSpace();
00618                                 $this->expect( T_ARRAY );
00619                                 $this->skipSpace();
00620                                 $this->expect( '(' );
00621                                 $this->skipSpace();
00622                                 $this->pushPath( '@extra-' . ($this->serial++) );
00623                                 if ( $this->isAhead( ')' ) ) {
00624                                         // Empty array
00625                                         $this->pushState( 'array end' );
00626                                 } else {
00627                                         $this->pushState( 'element', 'array end' );
00628                                 }
00629                                 break;
00630                         case 'array end':
00631                                 $this->skipSpace();
00632                                 $this->popPath();
00633                                 $this->expect( ')' );
00634                                 break;
00635                         case 'element':
00636                                 $token = $this->skipSpace();
00637                                 // Look ahead to find the double arrow
00638                                 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) {
00639                                         // Found associative element
00640                                         $this->pushState( 'assoc-element', 'element end' );
00641                                 } else {
00642                                         // Not associative
00643                                         $this->nextPath( '@next' );
00644                                         $this->startPathValue();
00645                                         $this->pushState( 'expression', 'element end' );
00646                                 }
00647                                 break;
00648                         case 'element end':
00649                                 $token = $this->skipSpace();
00650                                 if ( $token->type == ',' ) {
00651                                         $this->endPathValue();
00652                                         $this->markComma();
00653                                         $this->nextToken();
00654                                         $this->nextPath( '@extra-' . ($this->serial++) );
00655                                         // Look ahead to find ending bracket
00656                                         if ( $this->isAhead( ")" ) ) {
00657                                                 // Found ending bracket, no continuation
00658                                                 $this->skipSpace();
00659                                         } else {
00660                                                 // No ending bracket, continue to next element
00661                                                 $this->pushState( 'element' );
00662                                         }
00663                                 } elseif ( $token->type == ')' ) {
00664                                         // End array
00665                                         $this->endPathValue();
00666                                 } else {
00667                                         $this->error( "expected the next array element or the end of the array" );
00668                                 }
00669                                 break;
00670                         case 'assoc-element':
00671                                 $token = $this->skipSpace();
00672                                 if ( !$token->isScalar() ) {
00673                                         $this->error( "expected a string or number for the array key" );
00674                                 }
00675                                 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00676                                         $text = $this->parseScalar( $token->text );
00677                                 } else {
00678                                         $text = $token->text;
00679                                 }
00680                                 if ( !$this->validatePath( $text ) ) {
00681                                         $this->error( "Invalid associative array name \"$text\"" );
00682                                 }
00683                                 $this->nextPath( $text );
00684                                 $this->nextToken();
00685                                 $this->skipSpace();
00686                                 $this->markArrow();
00687                                 $this->expect( T_DOUBLE_ARROW );
00688                                 $this->skipSpace();
00689                                 $this->startPathValue();
00690                                 $this->pushState( 'expression' );
00691                                 break;
00692                         }
00693                 }
00694                 if ( count( $this->stateStack ) ) {
00695                         $this->error( 'unexpected end of file' );
00696                 }
00697                 $this->popPath();
00698         }
00699 
00703         protected function initParse() {
00704                 $this->tokens = token_get_all( $this->text );
00705                 $this->stateStack = array();
00706                 $this->pathStack = array();
00707                 $this->firstToken();
00708                 $this->pathInfo = array();
00709                 $this->serial = 1;
00710         }
00711 
00716         protected function setPos( $pos ) {
00717                 $this->pos = $pos;
00718                 if ( $this->pos >= count( $this->tokens ) ) {
00719                         $this->currentToken = ConfEditorToken::newEnd();
00720                 } else {
00721                         $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] );
00722                 }
00723                 return $this->currentToken;
00724         }
00725 
00729         function newTokenObj( $internalToken ) {
00730                 if ( is_array( $internalToken ) ) {
00731                         return new ConfEditorToken( $internalToken[0], $internalToken[1] );
00732                 } else {
00733                         return new ConfEditorToken( $internalToken, $internalToken );
00734                 }
00735         }
00736 
00740         function firstToken() {
00741                 $this->setPos( 0 );
00742                 $this->prevToken = ConfEditorToken::newEnd();
00743                 $this->lineNum = 1;
00744                 $this->colNum = 1;
00745                 $this->byteNum = 0;
00746                 return $this->currentToken;
00747         }
00748 
00752         function currentToken() {
00753                 return $this->currentToken;
00754         }
00755 
00759         function nextToken() {
00760                 if ( $this->currentToken ) {
00761                         $text = $this->currentToken->text;
00762                         $lfCount = substr_count( $text, "\n" );
00763                         if ( $lfCount ) {
00764                                 $this->lineNum += $lfCount;
00765                                 $this->colNum = strlen( $text ) - strrpos( $text, "\n" );
00766                         } else {
00767                                 $this->colNum += strlen( $text );
00768                         }
00769                         $this->byteNum += strlen( $text );
00770                 }
00771                 $this->prevToken = $this->currentToken;
00772                 $this->setPos( $this->pos + 1 );
00773                 return $this->currentToken;
00774         }
00775 
00780         function getTokenAhead( $offset ) {
00781                 $pos = $this->pos + $offset;
00782                 if ( $pos >= count( $this->tokens ) || $pos < 0 ) {
00783                         return ConfEditorToken::newEnd();
00784                 } else {
00785                         return $this->newTokenObj( $this->tokens[$pos] );
00786                 }
00787         }
00788 
00792         function skipSpace() {
00793                 while ( $this->currentToken && $this->currentToken->isSkip() ) {
00794                         $this->nextToken();
00795                 }
00796                 return $this->currentToken;
00797         }
00798 
00803         function expect( $type ) {
00804                 if ( $this->currentToken && $this->currentToken->type == $type ) {
00805                         return $this->nextToken();
00806                 } else {
00807                         $this->error( "expected " . $this->getTypeName( $type ) .
00808                                 ", got " . $this->getTypeName( $this->currentToken->type ) );
00809                 }
00810         }
00811 
00815         function pushState( $nextState, $stateAfterThat = null ) {
00816                 if ( $stateAfterThat !== null ) {
00817                         $this->stateStack[] = $stateAfterThat;
00818                 }
00819                 $this->stateStack[] = $nextState;
00820         }
00821 
00825         function popState() {
00826                 return array_pop( $this->stateStack );
00827         }
00828 
00833         function validatePath( $path ) {
00834                 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
00835         }
00836 
00841         function endPath() {
00842                 $key = '';
00843                 foreach ( $this->pathStack as $pathInfo ) {
00844                         if ( $key !== '' ) {
00845                                 $key .= '/';
00846                         }
00847                         $key .= $pathInfo['name'];
00848                 }
00849                 $pathInfo['endByte'] = $this->byteNum;
00850                 $pathInfo['endToken'] = $this->pos;
00851                 $this->pathInfo[$key] = $pathInfo;
00852         }
00853 
00857         function pushPath( $path ) {
00858                 $this->pathStack[] = array(
00859                         'name' => $path,
00860                         'level' => count( $this->pathStack ) + 1,
00861                         'startByte' => $this->byteNum,
00862                         'startToken' => $this->pos,
00863                         'valueStartToken' => false,
00864                         'valueStartByte' => false,
00865                         'valueEndToken' => false,
00866                         'valueEndByte' => false,
00867                         'nextArrayIndex' => 0,
00868                         'hasComma' => false,
00869                         'arrowByte' => false
00870                 );
00871         }
00872 
00876         function popPath() {
00877                 $this->endPath();
00878                 array_pop( $this->pathStack );
00879         }
00880 
00886         function nextPath( $path ) {
00887                 $this->endPath();
00888                 $i = count( $this->pathStack ) - 1;
00889                 if ( $path == '@next' ) {
00890                         $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex'];
00891                         $this->pathStack[$i]['name'] = $nextArrayIndex;
00892                         $nextArrayIndex++;
00893                 } else {
00894                         $this->pathStack[$i]['name'] = $path;
00895                 }
00896                 $this->pathStack[$i] =
00897                         array(
00898                                 'startByte' => $this->byteNum,
00899                                 'startToken' => $this->pos,
00900                                 'valueStartToken' => false,
00901                                 'valueStartByte' => false,
00902                                 'valueEndToken' => false,
00903                                 'valueEndByte' => false,
00904                                 'hasComma' => false,
00905                                 'arrowByte' => false,
00906                         ) + $this->pathStack[$i];
00907         }
00908 
00912         function startPathValue() {
00913                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00914                 $path['valueStartToken'] = $this->pos;
00915                 $path['valueStartByte'] = $this->byteNum;
00916         }
00917 
00921         function endPathValue() {
00922                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00923                 $path['valueEndToken'] = $this->pos;
00924                 $path['valueEndByte'] = $this->byteNum;
00925         }
00926 
00930         function markComma() {
00931                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00932                 $path['hasComma'] = true;
00933         }
00934 
00938         function markArrow() {
00939                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00940                 $path['arrowByte'] = $this->byteNum;
00941         }
00942 
00946         function error( $msg ) {
00947                 throw new ConfEditorParseError( $this, $msg );
00948         }
00949 
00953         function getTypeName( $type ) {
00954                 if ( is_int( $type ) ) {
00955                         return token_name( $type );
00956                 } else {
00957                         return "\"$type\"";
00958                 }
00959         }
00960 
00966         function isAhead( $type, $offset = 0 ) {
00967                 $ahead = $offset;
00968                 $token = $this->getTokenAhead( $offset );
00969                 while ( !$token->isEnd() ) {
00970                         if ( $token->isSkip() ) {
00971                                 $ahead++;
00972                                 $token = $this->getTokenAhead( $ahead );
00973                                 continue;
00974                         } elseif ( $token->type == $type ) {
00975                                 // Found the type
00976                                 return true;
00977                         } else {
00978                                 // Not found
00979                                 return false;
00980                         }
00981                 }
00982                 return false;
00983         }
00984 
00988         function prevToken() {
00989                 return $this->prevToken;
00990         }
00991 
00995         function dumpTokens() {
00996                 $out = '';
00997                 foreach ( $this->tokens as $token ) {
00998                         $obj = $this->newTokenObj( $token );
00999                         $out .= sprintf( "%-28s %s\n",
01000                                 $this->getTypeName( $obj->type ),
01001                                 addcslashes( $obj->text, "\0..\37" ) );
01002                 }
01003                 echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
01004         }
01005 }
01006 
01010 class ConfEditorParseError extends MWException {
01011         var $lineNum, $colNum;
01012         function __construct( $editor, $msg ) {
01013                 $this->lineNum = $editor->lineNum;
01014                 $this->colNum = $editor->colNum;
01015                 parent::__construct( "Parse error on line {$editor->lineNum} " .
01016                         "col {$editor->colNum}: $msg" );
01017         }
01018 
01019         function highlight( $text ) {
01020                 $lines = StringUtils::explode( "\n", $text );
01021                 foreach ( $lines as $lineNum => $line ) {
01022                         if ( $lineNum == $this->lineNum - 1 ) {
01023                                 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n";
01024                         }
01025                 }
01026         }
01027 
01028 }
01029 
01033 class ConfEditorToken {
01034         var $type, $text;
01035 
01036         static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING );
01037         static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT );
01038 
01039         static function newEnd() {
01040                 return new self( 'END', '' );
01041         }
01042 
01043         function __construct( $type, $text ) {
01044                 $this->type = $type;
01045                 $this->text = $text;
01046         }
01047 
01048         function isSkip() {
01049                 return in_array( $this->type, self::$skipTypes );
01050         }
01051 
01052         function isScalar() {
01053                 return in_array( $this->type, self::$scalarTypes );
01054         }
01055 
01056         function isEnd() {
01057                 return $this->type == 'END';
01058         }
01059 }
01060