MediaWiki  REL1_20
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 
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 
00397         function findDeletionRegion( $pathName ) {
00398                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00399                         throw new MWException( "Can't find path \"$pathName\"" );
00400                 }
00401                 $path = $this->pathInfo[$pathName];
00402                 // Find the start
00403                 $this->firstToken();
00404                 while ( $this->pos != $path['startToken'] ) {
00405                         $this->nextToken();
00406                 }
00407                 $regionStart = $path['startByte'];
00408                 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) {
00409                         $token = $this->getTokenAhead( $offset );
00410                         if ( !$token->isSkip() ) {
00411                                 // If there is other content on the same line, don't move the start point
00412                                 // back, because that will cause the regions to overlap.
00413                                 $regionStart = $path['startByte'];
00414                                 break;
00415                         }
00416                         $lfPos = strrpos( $token->text, "\n" );
00417                         if ( $lfPos === false ) {
00418                                 $regionStart -= strlen( $token->text );
00419                         } else {
00420                                 // The line start does not include the LF
00421                                 $regionStart -= strlen( $token->text ) - $lfPos - 1;
00422                                 break;
00423                         }
00424                 }
00425                 // Find the end
00426                 while ( $this->pos != $path['endToken'] ) {
00427                         $this->nextToken();
00428                 }
00429                 $regionEnd = $path['endByte']; // past the end
00430                 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) {
00431                         $token = $this->getTokenAhead( $offset );
00432                         if ( !$token->isSkip() ) {
00433                                 break;
00434                         }
00435                         $lfPos = strpos( $token->text, "\n" );
00436                         if ( $lfPos === false ) {
00437                                 $regionEnd += strlen( $token->text );
00438                         } else {
00439                                 // This should point past the LF
00440                                 $regionEnd += $lfPos + 1;
00441                                 break;
00442                         }
00443                 }
00444                 return array( $regionStart, $regionEnd );
00445         }
00446 
00455         function findValueRegion( $pathName ) {
00456                 if ( !isset( $this->pathInfo[$pathName] ) ) {
00457                         throw new MWException( "Can't find path \"$pathName\"" );
00458                 }
00459                 $path = $this->pathInfo[$pathName];
00460                 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) {
00461                         throw new MWException( "Can't find value region for path \"$pathName\"" );
00462                 }
00463                 return array( $path['valueStartByte'], $path['valueEndByte'] );
00464         }
00465 
00472         function findLastArrayElement( $path ) {
00473                 // Try for a real element
00474                 $lastEltPath = false;
00475                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00476                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00477                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00478                         if ( $part2 == '@' ) {
00479                                 // Do nothing
00480                         } elseif ( $part1 == "$path/" ) {
00481                                 $lastEltPath = $candidatePath;
00482                         } elseif ( $lastEltPath !== false ) {
00483                                 break;
00484                         }
00485                 }
00486                 if ( $lastEltPath !== false ) {
00487                         return $lastEltPath;
00488                 }
00489 
00490                 // Try for an interstitial element
00491                 $extraPath = false;
00492                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00493                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00494                         if ( $part1 == "$path/" ) {
00495                                 $extraPath = $candidatePath;
00496                         } elseif ( $extraPath !== false ) {
00497                                 break;
00498                         }
00499                 }
00500                 return $extraPath;
00501         }
00502 
00509         function findFirstArrayElement( $path ) {
00510                 // Try for an ordinary element
00511                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00512                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00513                         $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00514                         if ( $part1 == "$path/" && $part2 != '@' ) {
00515                                 return $candidatePath;
00516                         }
00517                 }
00518 
00519                 // Try for an interstitial element
00520                 foreach ( $this->pathInfo as $candidatePath => $info ) {
00521                         $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00522                         if ( $part1 == "$path/" ) {
00523                                 return $candidatePath;
00524                         }
00525                 }
00526                 return false;
00527         }
00528 
00534         function getIndent( $pos, $key = false, $arrowPos = false ) {
00535                 $arrowIndent = ' ';
00536                 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) {
00537                         $indentLength = strspn( $this->text, " \t", $pos );
00538                         $indent = substr( $this->text, $pos, $indentLength );
00539                 } else {
00540                         $indent = false;
00541                 }
00542                 if ( $indent !== false && $arrowPos !== false ) {
00543                         $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
00544                         if ( $arrowIndentLength > 0 ) {
00545                                 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
00546                         }
00547                 }
00548                 return array( $indent, $arrowIndent );
00549         }
00550 
00555         public function parse() {
00556                 $this->initParse();
00557                 $this->pushState( 'file' );
00558                 $this->pushPath( '@extra-' . ($this->serial++) );
00559                 $token = $this->firstToken();
00560 
00561                 while ( !$token->isEnd() ) {
00562                         $state = $this->popState();
00563                         if ( !$state ) {
00564                                 $this->error( 'internal error: empty state stack' );
00565                         }
00566 
00567                         switch ( $state ) {
00568                         case 'file':
00569                                 $this->expect( T_OPEN_TAG );
00570                                 $token = $this->skipSpace();
00571                                 if ( $token->isEnd() ) {
00572                                         break 2;
00573                                 }
00574                                 $this->pushState( 'statement', 'file 2' );
00575                                 break;
00576                         case 'file 2':
00577                                 $token = $this->skipSpace();
00578                                 if ( $token->isEnd() ) {
00579                                         break 2;
00580                                 }
00581                                 $this->pushState( 'statement', 'file 2' );
00582                                 break;
00583                         case 'statement':
00584                                 $token = $this->skipSpace();
00585                                 if ( !$this->validatePath( $token->text ) ) {
00586                                         $this->error( "Invalid variable name \"{$token->text}\"" );
00587                                 }
00588                                 $this->nextPath( $token->text );
00589                                 $this->expect( T_VARIABLE );
00590                                 $this->skipSpace();
00591                                 $arrayAssign = false;
00592                                 if ( $this->currentToken()->type == '[' ) {
00593                                         $this->nextToken();
00594                                         $token = $this->skipSpace();
00595                                         if ( !$token->isScalar() ) {
00596                                                 $this->error( "expected a string or number for the array key" );
00597                                         }
00598                                         if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00599                                                 $text = $this->parseScalar( $token->text );
00600                                         } else {
00601                                                 $text = $token->text;
00602                                         }
00603                                         if ( !$this->validatePath( $text ) ) {
00604                                                 $this->error( "Invalid associative array name \"$text\"" );
00605                                         }
00606                                         $this->pushPath( $text );
00607                                         $this->nextToken();
00608                                         $this->skipSpace();
00609                                         $this->expect( ']' );
00610                                         $this->skipSpace();
00611                                         $arrayAssign = true;
00612                                 }
00613                                 $this->expect( '=' );
00614                                 $this->skipSpace();
00615                                 $this->startPathValue();
00616                                 if ( $arrayAssign )
00617                                         $this->pushState( 'expression', 'array assign end' );
00618                                 else
00619                                         $this->pushState( 'expression', 'statement end' );
00620                                 break;
00621                         case 'array assign end':
00622                         case 'statement end':
00623                                 $this->endPathValue();
00624                                 if ( $state == 'array assign end' )
00625                                         $this->popPath();
00626                                 $this->skipSpace();
00627                                 $this->expect( ';' );
00628                                 $this->nextPath( '@extra-' . ($this->serial++) );
00629                                 break;
00630                         case 'expression':
00631                                 $token = $this->skipSpace();
00632                                 if ( $token->type == T_ARRAY ) {
00633                                         $this->pushState( 'array' );
00634                                 } elseif ( $token->isScalar() ) {
00635                                         $this->nextToken();
00636                                 } elseif ( $token->type == T_VARIABLE ) {
00637                                         $this->nextToken();
00638                                 } else {
00639                                         $this->error( "expected simple expression" );
00640                                 }
00641                                 break;
00642                         case 'array':
00643                                 $this->skipSpace();
00644                                 $this->expect( T_ARRAY );
00645                                 $this->skipSpace();
00646                                 $this->expect( '(' );
00647                                 $this->skipSpace();
00648                                 $this->pushPath( '@extra-' . ($this->serial++) );
00649                                 if ( $this->isAhead( ')' ) ) {
00650                                         // Empty array
00651                                         $this->pushState( 'array end' );
00652                                 } else {
00653                                         $this->pushState( 'element', 'array end' );
00654                                 }
00655                                 break;
00656                         case 'array end':
00657                                 $this->skipSpace();
00658                                 $this->popPath();
00659                                 $this->expect( ')' );
00660                                 break;
00661                         case 'element':
00662                                 $token = $this->skipSpace();
00663                                 // Look ahead to find the double arrow
00664                                 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) {
00665                                         // Found associative element
00666                                         $this->pushState( 'assoc-element', 'element end' );
00667                                 } else {
00668                                         // Not associative
00669                                         $this->nextPath( '@next' );
00670                                         $this->startPathValue();
00671                                         $this->pushState( 'expression', 'element end' );
00672                                 }
00673                                 break;
00674                         case 'element end':
00675                                 $token = $this->skipSpace();
00676                                 if ( $token->type == ',' ) {
00677                                         $this->endPathValue();
00678                                         $this->markComma();
00679                                         $this->nextToken();
00680                                         $this->nextPath( '@extra-' . ($this->serial++) );
00681                                         // Look ahead to find ending bracket
00682                                         if ( $this->isAhead( ")" ) ) {
00683                                                 // Found ending bracket, no continuation
00684                                                 $this->skipSpace();
00685                                         } else {
00686                                                 // No ending bracket, continue to next element
00687                                                 $this->pushState( 'element' );
00688                                         }
00689                                 } elseif ( $token->type == ')' ) {
00690                                         // End array
00691                                         $this->endPathValue();
00692                                 } else {
00693                                         $this->error( "expected the next array element or the end of the array" );
00694                                 }
00695                                 break;
00696                         case 'assoc-element':
00697                                 $token = $this->skipSpace();
00698                                 if ( !$token->isScalar() ) {
00699                                         $this->error( "expected a string or number for the array key" );
00700                                 }
00701                                 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00702                                         $text = $this->parseScalar( $token->text );
00703                                 } else {
00704                                         $text = $token->text;
00705                                 }
00706                                 if ( !$this->validatePath( $text ) ) {
00707                                         $this->error( "Invalid associative array name \"$text\"" );
00708                                 }
00709                                 $this->nextPath( $text );
00710                                 $this->nextToken();
00711                                 $this->skipSpace();
00712                                 $this->markArrow();
00713                                 $this->expect( T_DOUBLE_ARROW );
00714                                 $this->skipSpace();
00715                                 $this->startPathValue();
00716                                 $this->pushState( 'expression' );
00717                                 break;
00718                         }
00719                 }
00720                 if ( count( $this->stateStack ) ) {
00721                         $this->error( 'unexpected end of file' );
00722                 }
00723                 $this->popPath();
00724         }
00725 
00729         protected function initParse() {
00730                 $this->tokens = token_get_all( $this->text );
00731                 $this->stateStack = array();
00732                 $this->pathStack = array();
00733                 $this->firstToken();
00734                 $this->pathInfo = array();
00735                 $this->serial = 1;
00736         }
00737 
00742         protected function setPos( $pos ) {
00743                 $this->pos = $pos;
00744                 if ( $this->pos >= count( $this->tokens ) ) {
00745                         $this->currentToken = ConfEditorToken::newEnd();
00746                 } else {
00747                         $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] );
00748                 }
00749                 return $this->currentToken;
00750         }
00751 
00756         function newTokenObj( $internalToken ) {
00757                 if ( is_array( $internalToken ) ) {
00758                         return new ConfEditorToken( $internalToken[0], $internalToken[1] );
00759                 } else {
00760                         return new ConfEditorToken( $internalToken, $internalToken );
00761                 }
00762         }
00763 
00767         function firstToken() {
00768                 $this->setPos( 0 );
00769                 $this->prevToken = ConfEditorToken::newEnd();
00770                 $this->lineNum = 1;
00771                 $this->colNum = 1;
00772                 $this->byteNum = 0;
00773                 return $this->currentToken;
00774         }
00775 
00779         function currentToken() {
00780                 return $this->currentToken;
00781         }
00782 
00786         function nextToken() {
00787                 if ( $this->currentToken ) {
00788                         $text = $this->currentToken->text;
00789                         $lfCount = substr_count( $text, "\n" );
00790                         if ( $lfCount ) {
00791                                 $this->lineNum += $lfCount;
00792                                 $this->colNum = strlen( $text ) - strrpos( $text, "\n" );
00793                         } else {
00794                                 $this->colNum += strlen( $text );
00795                         }
00796                         $this->byteNum += strlen( $text );
00797                 }
00798                 $this->prevToken = $this->currentToken;
00799                 $this->setPos( $this->pos + 1 );
00800                 return $this->currentToken;
00801         }
00802 
00808         function getTokenAhead( $offset ) {
00809                 $pos = $this->pos + $offset;
00810                 if ( $pos >= count( $this->tokens ) || $pos < 0 ) {
00811                         return ConfEditorToken::newEnd();
00812                 } else {
00813                         return $this->newTokenObj( $this->tokens[$pos] );
00814                 }
00815         }
00816 
00820         function skipSpace() {
00821                 while ( $this->currentToken && $this->currentToken->isSkip() ) {
00822                         $this->nextToken();
00823                 }
00824                 return $this->currentToken;
00825         }
00826 
00831         function expect( $type ) {
00832                 if ( $this->currentToken && $this->currentToken->type == $type ) {
00833                         return $this->nextToken();
00834                 } else {
00835                         $this->error( "expected " . $this->getTypeName( $type ) .
00836                                 ", got " . $this->getTypeName( $this->currentToken->type ) );
00837                 }
00838         }
00839 
00843         function pushState( $nextState, $stateAfterThat = null ) {
00844                 if ( $stateAfterThat !== null ) {
00845                         $this->stateStack[] = $stateAfterThat;
00846                 }
00847                 $this->stateStack[] = $nextState;
00848         }
00849 
00854         function popState() {
00855                 return array_pop( $this->stateStack );
00856         }
00857 
00863         function validatePath( $path ) {
00864                 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
00865         }
00866 
00871         function endPath() {
00872                 $key = '';
00873                 foreach ( $this->pathStack as $pathInfo ) {
00874                         if ( $key !== '' ) {
00875                                 $key .= '/';
00876                         }
00877                         $key .= $pathInfo['name'];
00878                 }
00879                 $pathInfo['endByte'] = $this->byteNum;
00880                 $pathInfo['endToken'] = $this->pos;
00881                 $this->pathInfo[$key] = $pathInfo;
00882         }
00883 
00887         function pushPath( $path ) {
00888                 $this->pathStack[] = array(
00889                         'name' => $path,
00890                         'level' => count( $this->pathStack ) + 1,
00891                         'startByte' => $this->byteNum,
00892                         'startToken' => $this->pos,
00893                         'valueStartToken' => false,
00894                         'valueStartByte' => false,
00895                         'valueEndToken' => false,
00896                         'valueEndByte' => false,
00897                         'nextArrayIndex' => 0,
00898                         'hasComma' => false,
00899                         'arrowByte' => false
00900                 );
00901         }
00902 
00906         function popPath() {
00907                 $this->endPath();
00908                 array_pop( $this->pathStack );
00909         }
00910 
00916         function nextPath( $path ) {
00917                 $this->endPath();
00918                 $i = count( $this->pathStack ) - 1;
00919                 if ( $path == '@next' ) {
00920                         $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex'];
00921                         $this->pathStack[$i]['name'] = $nextArrayIndex;
00922                         $nextArrayIndex++;
00923                 } else {
00924                         $this->pathStack[$i]['name'] = $path;
00925                 }
00926                 $this->pathStack[$i] =
00927                         array(
00928                                 'startByte' => $this->byteNum,
00929                                 'startToken' => $this->pos,
00930                                 'valueStartToken' => false,
00931                                 'valueStartByte' => false,
00932                                 'valueEndToken' => false,
00933                                 'valueEndByte' => false,
00934                                 'hasComma' => false,
00935                                 'arrowByte' => false,
00936                         ) + $this->pathStack[$i];
00937         }
00938 
00942         function startPathValue() {
00943                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00944                 $path['valueStartToken'] = $this->pos;
00945                 $path['valueStartByte'] = $this->byteNum;
00946         }
00947 
00951         function endPathValue() {
00952                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00953                 $path['valueEndToken'] = $this->pos;
00954                 $path['valueEndByte'] = $this->byteNum;
00955         }
00956 
00960         function markComma() {
00961                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00962                 $path['hasComma'] = true;
00963         }
00964 
00968         function markArrow() {
00969                 $path =& $this->pathStack[count( $this->pathStack ) - 1];
00970                 $path['arrowByte'] = $this->byteNum;
00971         }
00972 
00976         function error( $msg ) {
00977                 throw new ConfEditorParseError( $this, $msg );
00978         }
00979 
00984         function getTypeName( $type ) {
00985                 if ( is_int( $type ) ) {
00986                         return token_name( $type );
00987                 } else {
00988                         return "\"$type\"";
00989                 }
00990         }
00991 
00998         function isAhead( $type, $offset = 0 ) {
00999                 $ahead = $offset;
01000                 $token = $this->getTokenAhead( $offset );
01001                 while ( !$token->isEnd() ) {
01002                         if ( $token->isSkip() ) {
01003                                 $ahead++;
01004                                 $token = $this->getTokenAhead( $ahead );
01005                                 continue;
01006                         } elseif ( $token->type == $type ) {
01007                                 // Found the type
01008                                 return true;
01009                         } else {
01010                                 // Not found
01011                                 return false;
01012                         }
01013                 }
01014                 return false;
01015         }
01016 
01020         function prevToken() {
01021                 return $this->prevToken;
01022         }
01023 
01027         function dumpTokens() {
01028                 $out = '';
01029                 foreach ( $this->tokens as $token ) {
01030                         $obj = $this->newTokenObj( $token );
01031                         $out .= sprintf( "%-28s %s\n",
01032                                 $this->getTypeName( $obj->type ),
01033                                 addcslashes( $obj->text, "\0..\37" ) );
01034                 }
01035                 echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
01036         }
01037 }
01038 
01042 class ConfEditorParseError extends MWException {
01043         var $lineNum, $colNum;
01044         function __construct( $editor, $msg ) {
01045                 $this->lineNum = $editor->lineNum;
01046                 $this->colNum = $editor->colNum;
01047                 parent::__construct( "Parse error on line {$editor->lineNum} " .
01048                         "col {$editor->colNum}: $msg" );
01049         }
01050 
01051         function highlight( $text ) {
01052                 $lines = StringUtils::explode( "\n", $text );
01053                 foreach ( $lines as $lineNum => $line ) {
01054                         if ( $lineNum == $this->lineNum - 1 ) {
01055                                 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n";
01056                         }
01057                 }
01058         }
01059 
01060 }
01061 
01065 class ConfEditorToken {
01066         var $type, $text;
01067 
01068         static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING );
01069         static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT );
01070 
01071         static function newEnd() {
01072                 return new self( 'END', '' );
01073         }
01074 
01075         function __construct( $type, $text ) {
01076                 $this->type = $type;
01077                 $this->text = $text;
01078         }
01079 
01080         function isSkip() {
01081                 return in_array( $this->type, self::$skipTypes );
01082         }
01083 
01084         function isScalar() {
01085                 return in_array( $this->type, self::$scalarTypes );
01086         }
01087 
01088         function isEnd() {
01089                 return $this->type == 'END';
01090         }
01091 }
01092