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