MediaWiki
REL1_22
|
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 }