MediaWiki  REL1_23
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 
00119         return "OK";
00120     }
00121 
00125     public function __construct( $text ) {
00126         $this->text = $text;
00127     }
00128 
00165     public function edit( $ops ) {
00166         $this->parse();
00167 
00168         $this->edits = array(
00169             array( 'copy', 0, strlen( $this->text ) )
00170         );
00171         foreach ( $ops as $op ) {
00172             $type = $op['type'];
00173             $path = $op['path'];
00174             $value = isset( $op['value'] ) ? $op['value'] : null;
00175             $key = isset( $op['key'] ) ? $op['key'] : null;
00176 
00177             switch ( $type ) {
00178                 case 'delete':
00179                     list( $start, $end ) = $this->findDeletionRegion( $path );
00180                     $this->replaceSourceRegion( $start, $end, false );
00181                     break;
00182                 case 'set':
00183                     if ( isset( $this->pathInfo[$path] ) ) {
00184                         list( $start, $end ) = $this->findValueRegion( $path );
00185                         $encValue = $value; // var_export( $value, true );
00186                         $this->replaceSourceRegion( $start, $end, $encValue );
00187                         break;
00188                     }
00189                     // No existing path, fall through to append
00190                     $slashPos = strrpos( $path, '/' );
00191                     $key = var_export( substr( $path, $slashPos + 1 ), true );
00192                     $path = substr( $path, 0, $slashPos );
00193                     // Fall through
00194                 case 'append':
00195                     // Find the last array element
00196                     $lastEltPath = $this->findLastArrayElement( $path );
00197                     if ( $lastEltPath === false ) {
00198                         throw new MWException( "Can't find any element of array \"$path\"" );
00199                     }
00200                     $lastEltInfo = $this->pathInfo[$lastEltPath];
00201 
00202                     // Has it got a comma already?
00203                     if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
00204                         // No comma, insert one after the value region
00205                         list( , $end ) = $this->findValueRegion( $lastEltPath );
00206                         $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
00207                     }
00208 
00209                     // Make the text to insert
00210                     list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
00211 
00212                     if ( $key === null ) {
00213                         list( $indent, ) = $this->getIndent( $start );
00214                         $textToInsert = "$indent$value,";
00215                     } else {
00216                         list( $indent, $arrowIndent ) =
00217                             $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
00218                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00219                     }
00220                     $textToInsert .= ( $indent === false ? ' ' : "\n" );
00221 
00222                     // Insert the item
00223                     $this->replaceSourceRegion( $end, $end, $textToInsert );
00224                     break;
00225                 case 'insert':
00226                     // Find first array element
00227                     $firstEltPath = $this->findFirstArrayElement( $path );
00228                     if ( $firstEltPath === false ) {
00229                         throw new MWException( "Can't find array element of \"$path\"" );
00230                     }
00231                     list( $start, ) = $this->findDeletionRegion( $firstEltPath );
00232                     $info = $this->pathInfo[$firstEltPath];
00233 
00234                     // Make the text to insert
00235                     if ( $key === null ) {
00236                         list( $indent, ) = $this->getIndent( $start );
00237                         $textToInsert = "$indent$value,";
00238                     } else {
00239                         list( $indent, $arrowIndent ) =
00240                             $this->getIndent( $start, $key, $info['arrowByte'] );
00241                         $textToInsert = "$indent$key$arrowIndent=> $value,";
00242                     }
00243                     $textToInsert .= ( $indent === false ? ' ' : "\n" );
00244 
00245                     // Insert the item
00246                     $this->replaceSourceRegion( $start, $start, $textToInsert );
00247                     break;
00248                 default:
00249                     throw new MWException( "Unrecognised operation: \"$type\"" );
00250             }
00251         }
00252 
00253         // Do the edits
00254         $out = '';
00255         foreach ( $this->edits as $edit ) {
00256             if ( $edit[0] == 'copy' ) {
00257                 $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] );
00258             } else { // if ( $edit[0] == 'insert' )
00259                 $out .= $edit[1];
00260             }
00261         }
00262 
00263         // Do a second parse as a sanity check
00264         $this->text = $out;
00265         try {
00266             $this->parse();
00267         } catch ( ConfEditorParseError $e ) {
00268             throw new MWException(
00269                 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
00270                 $e->getMessage() );
00271         }
00272 
00273         return $out;
00274     }
00275 
00280     function getVars() {
00281         $vars = array();
00282         $this->parse();
00283         foreach ( $this->pathInfo as $path => $data ) {
00284             if ( $path[0] != '$' ) {
00285                 continue;
00286             }
00287             $trimmedPath = substr( $path, 1 );
00288             $name = $data['name'];
00289             if ( $name[0] == '@' ) {
00290                 continue;
00291             }
00292             if ( $name[0] == '$' ) {
00293                 $name = substr( $name, 1 );
00294             }
00295             $parentPath = substr( $trimmedPath, 0,
00296                 strlen( $trimmedPath ) - strlen( $name ) );
00297             if ( substr( $parentPath, -1 ) == '/' ) {
00298                 $parentPath = substr( $parentPath, 0, -1 );
00299             }
00300 
00301             $value = substr( $this->text, $data['valueStartByte'],
00302                 $data['valueEndByte'] - $data['valueStartByte']
00303             );
00304             $this->setVar( $vars, $parentPath, $name,
00305                 $this->parseScalar( $value ) );
00306         }
00307 
00308         return $vars;
00309     }
00310 
00320     function setVar( &$array, $path, $key, $value ) {
00321         $pathArr = explode( '/', $path );
00322         $target =& $array;
00323         if ( $path !== '' ) {
00324             foreach ( $pathArr as $p ) {
00325                 if ( !isset( $target[$p] ) ) {
00326                     $target[$p] = array();
00327                 }
00328                 $target =& $target[$p];
00329             }
00330         }
00331         if ( !isset( $target[$key] ) ) {
00332             $target[$key] = $value;
00333         }
00334     }
00335 
00340     function parseScalar( $str ) {
00341         if ( $str !== '' && $str[0] == '\'' ) {
00342             // Single-quoted string
00343             // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00344             // appended to the token; without it we ended up reading in the
00345             // extra quote on the end!
00346             return strtr( substr( trim( $str ), 1, -1 ),
00347                 array( '\\\'' => '\'', '\\\\' => '\\' ) );
00348         }
00349         if ( $str !== '' && $str[0] == '"' ) {
00350             // Double-quoted string
00351             // @todo FIXME: trim() call is due to mystery bug where whitespace gets
00352             // appended to the token; without it we ended up reading in the
00353             // extra quote on the end!
00354             return stripcslashes( substr( trim( $str ), 1, -1 ) );
00355         }
00356         if ( substr( $str, 0, 4 ) == 'true' ) {
00357             return true;
00358         }
00359         if ( substr( $str, 0, 5 ) == 'false' ) {
00360             return false;
00361         }
00362         if ( substr( $str, 0, 4 ) == 'null' ) {
00363             return null;
00364         }
00365 
00366         // Must be some kind of numeric value, so let PHP's weak typing
00367         // be useful for a change
00368         return $str;
00369     }
00370 
00375     function replaceSourceRegion( $start, $end, $newText = false ) {
00376         // Split all copy operations with a source corresponding to the region
00377         // in question.
00378         $newEdits = array();
00379         foreach ( $this->edits as $edit ) {
00380             if ( $edit[0] !== 'copy' ) {
00381                 $newEdits[] = $edit;
00382                 continue;
00383             }
00384             $copyStart = $edit[1];
00385             $copyEnd = $edit[2];
00386             if ( $start >= $copyEnd || $end <= $copyStart ) {
00387                 // Outside this region
00388                 $newEdits[] = $edit;
00389                 continue;
00390             }
00391             if ( ( $start < $copyStart && $end > $copyStart )
00392                 || ( $start < $copyEnd && $end > $copyEnd )
00393             ) {
00394                 throw new MWException( "Overlapping regions found, can't do the edit" );
00395             }
00396             // Split the copy
00397             $newEdits[] = array( 'copy', $copyStart, $start );
00398             if ( $newText !== false ) {
00399                 $newEdits[] = array( 'insert', $newText );
00400             }
00401             $newEdits[] = array( 'copy', $end, $copyEnd );
00402         }
00403         $this->edits = $newEdits;
00404     }
00405 
00414     function findDeletionRegion( $pathName ) {
00415         if ( !isset( $this->pathInfo[$pathName] ) ) {
00416             throw new MWException( "Can't find path \"$pathName\"" );
00417         }
00418         $path = $this->pathInfo[$pathName];
00419         // Find the start
00420         $this->firstToken();
00421         while ( $this->pos != $path['startToken'] ) {
00422             $this->nextToken();
00423         }
00424         $regionStart = $path['startByte'];
00425         for ( $offset = -1; $offset >= -$this->pos; $offset-- ) {
00426             $token = $this->getTokenAhead( $offset );
00427             if ( !$token->isSkip() ) {
00428                 // If there is other content on the same line, don't move the start point
00429                 // back, because that will cause the regions to overlap.
00430                 $regionStart = $path['startByte'];
00431                 break;
00432             }
00433             $lfPos = strrpos( $token->text, "\n" );
00434             if ( $lfPos === false ) {
00435                 $regionStart -= strlen( $token->text );
00436             } else {
00437                 // The line start does not include the LF
00438                 $regionStart -= strlen( $token->text ) - $lfPos - 1;
00439                 break;
00440             }
00441         }
00442         // Find the end
00443         while ( $this->pos != $path['endToken'] ) {
00444             $this->nextToken();
00445         }
00446         $regionEnd = $path['endByte']; // past the end
00447         $count = count( $this->tokens );
00448         for ( $offset = 0; $offset < $count - $this->pos; $offset++ ) {
00449             $token = $this->getTokenAhead( $offset );
00450             if ( !$token->isSkip() ) {
00451                 break;
00452             }
00453             $lfPos = strpos( $token->text, "\n" );
00454             if ( $lfPos === false ) {
00455                 $regionEnd += strlen( $token->text );
00456             } else {
00457                 // This should point past the LF
00458                 $regionEnd += $lfPos + 1;
00459                 break;
00460             }
00461         }
00462 
00463         return array( $regionStart, $regionEnd );
00464     }
00465 
00476     function findValueRegion( $pathName ) {
00477         if ( !isset( $this->pathInfo[$pathName] ) ) {
00478             throw new MWException( "Can't find path \"$pathName\"" );
00479         }
00480         $path = $this->pathInfo[$pathName];
00481         if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) {
00482             throw new MWException( "Can't find value region for path \"$pathName\"" );
00483         }
00484 
00485         return array( $path['valueStartByte'], $path['valueEndByte'] );
00486     }
00487 
00494     function findLastArrayElement( $path ) {
00495         // Try for a real element
00496         $lastEltPath = false;
00497         foreach ( $this->pathInfo as $candidatePath => $info ) {
00498             $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00499             $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00500             if ( $part2 == '@' ) {
00501                 // Do nothing
00502             } elseif ( $part1 == "$path/" ) {
00503                 $lastEltPath = $candidatePath;
00504             } elseif ( $lastEltPath !== false ) {
00505                 break;
00506             }
00507         }
00508         if ( $lastEltPath !== false ) {
00509             return $lastEltPath;
00510         }
00511 
00512         // Try for an interstitial element
00513         $extraPath = false;
00514         foreach ( $this->pathInfo as $candidatePath => $info ) {
00515             $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00516             if ( $part1 == "$path/" ) {
00517                 $extraPath = $candidatePath;
00518             } elseif ( $extraPath !== false ) {
00519                 break;
00520             }
00521         }
00522 
00523         return $extraPath;
00524     }
00525 
00532     function findFirstArrayElement( $path ) {
00533         // Try for an ordinary element
00534         foreach ( $this->pathInfo as $candidatePath => $info ) {
00535             $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00536             $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 );
00537             if ( $part1 == "$path/" && $part2 != '@' ) {
00538                 return $candidatePath;
00539             }
00540         }
00541 
00542         // Try for an interstitial element
00543         foreach ( $this->pathInfo as $candidatePath => $info ) {
00544             $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 );
00545             if ( $part1 == "$path/" ) {
00546                 return $candidatePath;
00547             }
00548         }
00549 
00550         return false;
00551     }
00552 
00558     function getIndent( $pos, $key = false, $arrowPos = false ) {
00559         $arrowIndent = ' ';
00560         if ( $pos == 0 || $this->text[$pos - 1] == "\n" ) {
00561             $indentLength = strspn( $this->text, " \t", $pos );
00562             $indent = substr( $this->text, $pos, $indentLength );
00563         } else {
00564             $indent = false;
00565         }
00566         if ( $indent !== false && $arrowPos !== false ) {
00567             $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
00568             if ( $arrowIndentLength > 0 ) {
00569                 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
00570             }
00571         }
00572 
00573         return array( $indent, $arrowIndent );
00574     }
00575 
00580     public function parse() {
00581         $this->initParse();
00582         $this->pushState( 'file' );
00583         $this->pushPath( '@extra-' . ( $this->serial++ ) );
00584         $token = $this->firstToken();
00585 
00586         while ( !$token->isEnd() ) {
00587             $state = $this->popState();
00588             if ( !$state ) {
00589                 $this->error( 'internal error: empty state stack' );
00590             }
00591 
00592             switch ( $state ) {
00593                 case 'file':
00594                     $this->expect( T_OPEN_TAG );
00595                     $token = $this->skipSpace();
00596                     if ( $token->isEnd() ) {
00597                         break 2;
00598                     }
00599                     $this->pushState( 'statement', 'file 2' );
00600                     break;
00601                 case 'file 2':
00602                     $token = $this->skipSpace();
00603                     if ( $token->isEnd() ) {
00604                         break 2;
00605                     }
00606                     $this->pushState( 'statement', 'file 2' );
00607                     break;
00608                 case 'statement':
00609                     $token = $this->skipSpace();
00610                     if ( !$this->validatePath( $token->text ) ) {
00611                         $this->error( "Invalid variable name \"{$token->text}\"" );
00612                     }
00613                     $this->nextPath( $token->text );
00614                     $this->expect( T_VARIABLE );
00615                     $this->skipSpace();
00616                     $arrayAssign = false;
00617                     if ( $this->currentToken()->type == '[' ) {
00618                         $this->nextToken();
00619                         $token = $this->skipSpace();
00620                         if ( !$token->isScalar() ) {
00621                             $this->error( "expected a string or number for the array key" );
00622                         }
00623                         if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00624                             $text = $this->parseScalar( $token->text );
00625                         } else {
00626                             $text = $token->text;
00627                         }
00628                         if ( !$this->validatePath( $text ) ) {
00629                             $this->error( "Invalid associative array name \"$text\"" );
00630                         }
00631                         $this->pushPath( $text );
00632                         $this->nextToken();
00633                         $this->skipSpace();
00634                         $this->expect( ']' );
00635                         $this->skipSpace();
00636                         $arrayAssign = true;
00637                     }
00638                     $this->expect( '=' );
00639                     $this->skipSpace();
00640                     $this->startPathValue();
00641                     if ( $arrayAssign ) {
00642                         $this->pushState( 'expression', 'array assign end' );
00643                     } else {
00644                         $this->pushState( 'expression', 'statement end' );
00645                     }
00646                     break;
00647                 case 'array assign end':
00648                 case 'statement end':
00649                     $this->endPathValue();
00650                     if ( $state == 'array assign end' ) {
00651                         $this->popPath();
00652                     }
00653                     $this->skipSpace();
00654                     $this->expect( ';' );
00655                     $this->nextPath( '@extra-' . ( $this->serial++ ) );
00656                     break;
00657                 case 'expression':
00658                     $token = $this->skipSpace();
00659                     if ( $token->type == T_ARRAY ) {
00660                         $this->pushState( 'array' );
00661                     } elseif ( $token->isScalar() ) {
00662                         $this->nextToken();
00663                     } elseif ( $token->type == T_VARIABLE ) {
00664                         $this->nextToken();
00665                     } else {
00666                         $this->error( "expected simple expression" );
00667                     }
00668                     break;
00669                 case 'array':
00670                     $this->skipSpace();
00671                     $this->expect( T_ARRAY );
00672                     $this->skipSpace();
00673                     $this->expect( '(' );
00674                     $this->skipSpace();
00675                     $this->pushPath( '@extra-' . ( $this->serial++ ) );
00676                     if ( $this->isAhead( ')' ) ) {
00677                         // Empty array
00678                         $this->pushState( 'array end' );
00679                     } else {
00680                         $this->pushState( 'element', 'array end' );
00681                     }
00682                     break;
00683                 case 'array end':
00684                     $this->skipSpace();
00685                     $this->popPath();
00686                     $this->expect( ')' );
00687                     break;
00688                 case 'element':
00689                     $token = $this->skipSpace();
00690                     // Look ahead to find the double arrow
00691                     if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) {
00692                         // Found associative element
00693                         $this->pushState( 'assoc-element', 'element end' );
00694                     } else {
00695                         // Not associative
00696                         $this->nextPath( '@next' );
00697                         $this->startPathValue();
00698                         $this->pushState( 'expression', 'element end' );
00699                     }
00700                     break;
00701                 case 'element end':
00702                     $token = $this->skipSpace();
00703                     if ( $token->type == ',' ) {
00704                         $this->endPathValue();
00705                         $this->markComma();
00706                         $this->nextToken();
00707                         $this->nextPath( '@extra-' . ( $this->serial++ ) );
00708                         // Look ahead to find ending bracket
00709                         if ( $this->isAhead( ")" ) ) {
00710                             // Found ending bracket, no continuation
00711                             $this->skipSpace();
00712                         } else {
00713                             // No ending bracket, continue to next element
00714                             $this->pushState( 'element' );
00715                         }
00716                     } elseif ( $token->type == ')' ) {
00717                         // End array
00718                         $this->endPathValue();
00719                     } else {
00720                         $this->error( "expected the next array element or the end of the array" );
00721                     }
00722                     break;
00723                 case 'assoc-element':
00724                     $token = $this->skipSpace();
00725                     if ( !$token->isScalar() ) {
00726                         $this->error( "expected a string or number for the array key" );
00727                     }
00728                     if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) {
00729                         $text = $this->parseScalar( $token->text );
00730                     } else {
00731                         $text = $token->text;
00732                     }
00733                     if ( !$this->validatePath( $text ) ) {
00734                         $this->error( "Invalid associative array name \"$text\"" );
00735                     }
00736                     $this->nextPath( $text );
00737                     $this->nextToken();
00738                     $this->skipSpace();
00739                     $this->markArrow();
00740                     $this->expect( T_DOUBLE_ARROW );
00741                     $this->skipSpace();
00742                     $this->startPathValue();
00743                     $this->pushState( 'expression' );
00744                     break;
00745             }
00746         }
00747         if ( count( $this->stateStack ) ) {
00748             $this->error( 'unexpected end of file' );
00749         }
00750         $this->popPath();
00751     }
00752 
00756     protected function initParse() {
00757         $this->tokens = token_get_all( $this->text );
00758         $this->stateStack = array();
00759         $this->pathStack = array();
00760         $this->firstToken();
00761         $this->pathInfo = array();
00762         $this->serial = 1;
00763     }
00764 
00769     protected function setPos( $pos ) {
00770         $this->pos = $pos;
00771         if ( $this->pos >= count( $this->tokens ) ) {
00772             $this->currentToken = ConfEditorToken::newEnd();
00773         } else {
00774             $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] );
00775         }
00776 
00777         return $this->currentToken;
00778     }
00779 
00784     function newTokenObj( $internalToken ) {
00785         if ( is_array( $internalToken ) ) {
00786             return new ConfEditorToken( $internalToken[0], $internalToken[1] );
00787         } else {
00788             return new ConfEditorToken( $internalToken, $internalToken );
00789         }
00790     }
00791 
00795     function firstToken() {
00796         $this->setPos( 0 );
00797         $this->prevToken = ConfEditorToken::newEnd();
00798         $this->lineNum = 1;
00799         $this->colNum = 1;
00800         $this->byteNum = 0;
00801 
00802         return $this->currentToken;
00803     }
00804 
00808     function currentToken() {
00809         return $this->currentToken;
00810     }
00811 
00815     function nextToken() {
00816         if ( $this->currentToken ) {
00817             $text = $this->currentToken->text;
00818             $lfCount = substr_count( $text, "\n" );
00819             if ( $lfCount ) {
00820                 $this->lineNum += $lfCount;
00821                 $this->colNum = strlen( $text ) - strrpos( $text, "\n" );
00822             } else {
00823                 $this->colNum += strlen( $text );
00824             }
00825             $this->byteNum += strlen( $text );
00826         }
00827         $this->prevToken = $this->currentToken;
00828         $this->setPos( $this->pos + 1 );
00829 
00830         return $this->currentToken;
00831     }
00832 
00838     function getTokenAhead( $offset ) {
00839         $pos = $this->pos + $offset;
00840         if ( $pos >= count( $this->tokens ) || $pos < 0 ) {
00841             return ConfEditorToken::newEnd();
00842         } else {
00843             return $this->newTokenObj( $this->tokens[$pos] );
00844         }
00845     }
00846 
00850     function skipSpace() {
00851         while ( $this->currentToken && $this->currentToken->isSkip() ) {
00852             $this->nextToken();
00853         }
00854 
00855         return $this->currentToken;
00856     }
00857 
00862     function expect( $type ) {
00863         if ( $this->currentToken && $this->currentToken->type == $type ) {
00864             return $this->nextToken();
00865         } else {
00866             $this->error( "expected " . $this->getTypeName( $type ) .
00867                 ", got " . $this->getTypeName( $this->currentToken->type ) );
00868         }
00869     }
00870 
00874     function pushState( $nextState, $stateAfterThat = null ) {
00875         if ( $stateAfterThat !== null ) {
00876             $this->stateStack[] = $stateAfterThat;
00877         }
00878         $this->stateStack[] = $nextState;
00879     }
00880 
00885     function popState() {
00886         return array_pop( $this->stateStack );
00887     }
00888 
00894     function validatePath( $path ) {
00895         return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
00896     }
00897 
00902     function endPath() {
00903         $key = '';
00904         foreach ( $this->pathStack as $pathInfo ) {
00905             if ( $key !== '' ) {
00906                 $key .= '/';
00907             }
00908             $key .= $pathInfo['name'];
00909         }
00910         $pathInfo['endByte'] = $this->byteNum;
00911         $pathInfo['endToken'] = $this->pos;
00912         $this->pathInfo[$key] = $pathInfo;
00913     }
00914 
00918     function pushPath( $path ) {
00919         $this->pathStack[] = array(
00920             'name' => $path,
00921             'level' => count( $this->pathStack ) + 1,
00922             'startByte' => $this->byteNum,
00923             'startToken' => $this->pos,
00924             'valueStartToken' => false,
00925             'valueStartByte' => false,
00926             'valueEndToken' => false,
00927             'valueEndByte' => false,
00928             'nextArrayIndex' => 0,
00929             'hasComma' => false,
00930             'arrowByte' => false
00931         );
00932     }
00933 
00937     function popPath() {
00938         $this->endPath();
00939         array_pop( $this->pathStack );
00940     }
00941 
00947     function nextPath( $path ) {
00948         $this->endPath();
00949         $i = count( $this->pathStack ) - 1;
00950         if ( $path == '@next' ) {
00951             $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex'];
00952             $this->pathStack[$i]['name'] = $nextArrayIndex;
00953             $nextArrayIndex++;
00954         } else {
00955             $this->pathStack[$i]['name'] = $path;
00956         }
00957         $this->pathStack[$i] =
00958             array(
00959                 'startByte' => $this->byteNum,
00960                 'startToken' => $this->pos,
00961                 'valueStartToken' => false,
00962                 'valueStartByte' => false,
00963                 'valueEndToken' => false,
00964                 'valueEndByte' => false,
00965                 'hasComma' => false,
00966                 'arrowByte' => false,
00967             ) + $this->pathStack[$i];
00968     }
00969 
00973     function startPathValue() {
00974         $path =& $this->pathStack[count( $this->pathStack ) - 1];
00975         $path['valueStartToken'] = $this->pos;
00976         $path['valueStartByte'] = $this->byteNum;
00977     }
00978 
00982     function endPathValue() {
00983         $path =& $this->pathStack[count( $this->pathStack ) - 1];
00984         $path['valueEndToken'] = $this->pos;
00985         $path['valueEndByte'] = $this->byteNum;
00986     }
00987 
00991     function markComma() {
00992         $path =& $this->pathStack[count( $this->pathStack ) - 1];
00993         $path['hasComma'] = true;
00994     }
00995 
00999     function markArrow() {
01000         $path =& $this->pathStack[count( $this->pathStack ) - 1];
01001         $path['arrowByte'] = $this->byteNum;
01002     }
01003 
01007     function error( $msg ) {
01008         throw new ConfEditorParseError( $this, $msg );
01009     }
01010 
01015     function getTypeName( $type ) {
01016         if ( is_int( $type ) ) {
01017             return token_name( $type );
01018         } else {
01019             return "\"$type\"";
01020         }
01021     }
01022 
01029     function isAhead( $type, $offset = 0 ) {
01030         $ahead = $offset;
01031         $token = $this->getTokenAhead( $offset );
01032         while ( !$token->isEnd() ) {
01033             if ( $token->isSkip() ) {
01034                 $ahead++;
01035                 $token = $this->getTokenAhead( $ahead );
01036                 continue;
01037             } elseif ( $token->type == $type ) {
01038                 // Found the type
01039                 return true;
01040             } else {
01041                 // Not found
01042                 return false;
01043             }
01044         }
01045 
01046         return false;
01047     }
01048 
01052     function prevToken() {
01053         return $this->prevToken;
01054     }
01055 
01059     function dumpTokens() {
01060         $out = '';
01061         foreach ( $this->tokens as $token ) {
01062             $obj = $this->newTokenObj( $token );
01063             $out .= sprintf( "%-28s %s\n",
01064                 $this->getTypeName( $obj->type ),
01065                 addcslashes( $obj->text, "\0..\37" ) );
01066         }
01067         echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
01068     }
01069 }
01070 
01074 class ConfEditorParseError extends MWException {
01075     var $lineNum, $colNum;
01076 
01077     function __construct( $editor, $msg ) {
01078         $this->lineNum = $editor->lineNum;
01079         $this->colNum = $editor->colNum;
01080         parent::__construct( "Parse error on line {$editor->lineNum} " .
01081             "col {$editor->colNum}: $msg" );
01082     }
01083 
01084     function highlight( $text ) {
01085         $lines = StringUtils::explode( "\n", $text );
01086         foreach ( $lines as $lineNum => $line ) {
01087             if ( $lineNum == $this->lineNum - 1 ) {
01088                 return "$line\n" . str_repeat( ' ', $this->colNum - 1 ) . "^\n";
01089             }
01090         }
01091 
01092         return '';
01093     }
01094 }
01095 
01099 class ConfEditorToken {
01100     var $type, $text;
01101 
01102     static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING );
01103     static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT );
01104 
01105     static function newEnd() {
01106         return new self( 'END', '' );
01107     }
01108 
01109     function __construct( $type, $text ) {
01110         $this->type = $type;
01111         $this->text = $text;
01112     }
01113 
01114     function isSkip() {
01115         return in_array( $this->type, self::$skipTypes );
01116     }
01117 
01118     function isScalar() {
01119         return in_array( $this->type, self::$scalarTypes );
01120     }
01121 
01122     function isEnd() {
01123         return $this->type == 'END';
01124     }
01125 }