MediaWiki
REL1_21
|
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 $trimmedPath = substr( $path, 1 ); 00285 $name = $data['name']; 00286 if ( $name[0] == '@' ) 00287 continue; 00288 if ( $name[0] == '$' ) 00289 $name = substr( $name, 1 ); 00290 $parentPath = substr( $trimmedPath, 0, 00291 strlen( $trimmedPath ) - strlen( $name ) ); 00292 if( substr( $parentPath, -1 ) == '/' ) 00293 $parentPath = substr( $parentPath, 0, -1 ); 00294 00295 $value = substr( $this->text, $data['valueStartByte'], 00296 $data['valueEndByte'] - $data['valueStartByte'] 00297 ); 00298 $this->setVar( $vars, $parentPath, $name, 00299 $this->parseScalar( $value ) ); 00300 } 00301 return $vars; 00302 } 00303 00313 function setVar( &$array, $path, $key, $value ) { 00314 $pathArr = explode( '/', $path ); 00315 $target =& $array; 00316 if ( $path !== '' ) { 00317 foreach ( $pathArr as $p ) { 00318 if( !isset( $target[$p] ) ) 00319 $target[$p] = array(); 00320 $target =& $target[$p]; 00321 } 00322 } 00323 if ( !isset( $target[$key] ) ) 00324 $target[$key] = $value; 00325 } 00326 00331 function parseScalar( $str ) { 00332 if ( $str !== '' && $str[0] == '\'' ) 00333 // Single-quoted string 00334 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00335 // appended to the token; without it we ended up reading in the 00336 // extra quote on the end! 00337 return strtr( substr( trim( $str ), 1, -1 ), 00338 array( '\\\'' => '\'', '\\\\' => '\\' ) ); 00339 if ( $str !== '' && $str[0] == '"' ) 00340 // Double-quoted string 00341 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00342 // appended to the token; without it we ended up reading in the 00343 // extra quote on the end! 00344 return stripcslashes( substr( trim( $str ), 1, -1 ) ); 00345 if ( substr( $str, 0, 4 ) == 'true' ) 00346 return true; 00347 if ( substr( $str, 0, 5 ) == 'false' ) 00348 return false; 00349 if ( substr( $str, 0, 4 ) == 'null' ) 00350 return null; 00351 // Must be some kind of numeric value, so let PHP's weak typing 00352 // be useful for a change 00353 return $str; 00354 } 00355 00360 function replaceSourceRegion( $start, $end, $newText = false ) { 00361 // Split all copy operations with a source corresponding to the region 00362 // in question. 00363 $newEdits = array(); 00364 foreach ( $this->edits as $edit ) { 00365 if ( $edit[0] !== 'copy' ) { 00366 $newEdits[] = $edit; 00367 continue; 00368 } 00369 $copyStart = $edit[1]; 00370 $copyEnd = $edit[2]; 00371 if ( $start >= $copyEnd || $end <= $copyStart ) { 00372 // Outside this region 00373 $newEdits[] = $edit; 00374 continue; 00375 } 00376 if ( ( $start < $copyStart && $end > $copyStart ) 00377 || ( $start < $copyEnd && $end > $copyEnd ) 00378 ) { 00379 throw new MWException( "Overlapping regions found, can't do the edit" ); 00380 } 00381 // Split the copy 00382 $newEdits[] = array( 'copy', $copyStart, $start ); 00383 if ( $newText !== false ) { 00384 $newEdits[] = array( 'insert', $newText ); 00385 } 00386 $newEdits[] = array( 'copy', $end, $copyEnd ); 00387 } 00388 $this->edits = $newEdits; 00389 } 00390 00399 function findDeletionRegion( $pathName ) { 00400 if ( !isset( $this->pathInfo[$pathName] ) ) { 00401 throw new MWException( "Can't find path \"$pathName\"" ); 00402 } 00403 $path = $this->pathInfo[$pathName]; 00404 // Find the start 00405 $this->firstToken(); 00406 while ( $this->pos != $path['startToken'] ) { 00407 $this->nextToken(); 00408 } 00409 $regionStart = $path['startByte']; 00410 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { 00411 $token = $this->getTokenAhead( $offset ); 00412 if ( !$token->isSkip() ) { 00413 // If there is other content on the same line, don't move the start point 00414 // back, because that will cause the regions to overlap. 00415 $regionStart = $path['startByte']; 00416 break; 00417 } 00418 $lfPos = strrpos( $token->text, "\n" ); 00419 if ( $lfPos === false ) { 00420 $regionStart -= strlen( $token->text ); 00421 } else { 00422 // The line start does not include the LF 00423 $regionStart -= strlen( $token->text ) - $lfPos - 1; 00424 break; 00425 } 00426 } 00427 // Find the end 00428 while ( $this->pos != $path['endToken'] ) { 00429 $this->nextToken(); 00430 } 00431 $regionEnd = $path['endByte']; // past the end 00432 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { 00433 $token = $this->getTokenAhead( $offset ); 00434 if ( !$token->isSkip() ) { 00435 break; 00436 } 00437 $lfPos = strpos( $token->text, "\n" ); 00438 if ( $lfPos === false ) { 00439 $regionEnd += strlen( $token->text ); 00440 } else { 00441 // This should point past the LF 00442 $regionEnd += $lfPos + 1; 00443 break; 00444 } 00445 } 00446 return array( $regionStart, $regionEnd ); 00447 } 00448 00459 function findValueRegion( $pathName ) { 00460 if ( !isset( $this->pathInfo[$pathName] ) ) { 00461 throw new MWException( "Can't find path \"$pathName\"" ); 00462 } 00463 $path = $this->pathInfo[$pathName]; 00464 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { 00465 throw new MWException( "Can't find value region for path \"$pathName\"" ); 00466 } 00467 return array( $path['valueStartByte'], $path['valueEndByte'] ); 00468 } 00469 00476 function findLastArrayElement( $path ) { 00477 // Try for a real element 00478 $lastEltPath = false; 00479 foreach ( $this->pathInfo as $candidatePath => $info ) { 00480 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00481 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00482 if ( $part2 == '@' ) { 00483 // Do nothing 00484 } elseif ( $part1 == "$path/" ) { 00485 $lastEltPath = $candidatePath; 00486 } elseif ( $lastEltPath !== false ) { 00487 break; 00488 } 00489 } 00490 if ( $lastEltPath !== false ) { 00491 return $lastEltPath; 00492 } 00493 00494 // Try for an interstitial element 00495 $extraPath = false; 00496 foreach ( $this->pathInfo as $candidatePath => $info ) { 00497 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00498 if ( $part1 == "$path/" ) { 00499 $extraPath = $candidatePath; 00500 } elseif ( $extraPath !== false ) { 00501 break; 00502 } 00503 } 00504 return $extraPath; 00505 } 00506 00513 function findFirstArrayElement( $path ) { 00514 // Try for an ordinary element 00515 foreach ( $this->pathInfo as $candidatePath => $info ) { 00516 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00517 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00518 if ( $part1 == "$path/" && $part2 != '@' ) { 00519 return $candidatePath; 00520 } 00521 } 00522 00523 // Try for an interstitial element 00524 foreach ( $this->pathInfo as $candidatePath => $info ) { 00525 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00526 if ( $part1 == "$path/" ) { 00527 return $candidatePath; 00528 } 00529 } 00530 return false; 00531 } 00532 00538 function getIndent( $pos, $key = false, $arrowPos = false ) { 00539 $arrowIndent = ' '; 00540 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) { 00541 $indentLength = strspn( $this->text, " \t", $pos ); 00542 $indent = substr( $this->text, $pos, $indentLength ); 00543 } else { 00544 $indent = false; 00545 } 00546 if ( $indent !== false && $arrowPos !== false ) { 00547 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); 00548 if ( $arrowIndentLength > 0 ) { 00549 $arrowIndent = str_repeat( ' ', $arrowIndentLength ); 00550 } 00551 } 00552 return array( $indent, $arrowIndent ); 00553 } 00554 00559 public function parse() { 00560 $this->initParse(); 00561 $this->pushState( 'file' ); 00562 $this->pushPath( '@extra-' . ($this->serial++) ); 00563 $token = $this->firstToken(); 00564 00565 while ( !$token->isEnd() ) { 00566 $state = $this->popState(); 00567 if ( !$state ) { 00568 $this->error( 'internal error: empty state stack' ); 00569 } 00570 00571 switch ( $state ) { 00572 case 'file': 00573 $this->expect( T_OPEN_TAG ); 00574 $token = $this->skipSpace(); 00575 if ( $token->isEnd() ) { 00576 break 2; 00577 } 00578 $this->pushState( 'statement', 'file 2' ); 00579 break; 00580 case 'file 2': 00581 $token = $this->skipSpace(); 00582 if ( $token->isEnd() ) { 00583 break 2; 00584 } 00585 $this->pushState( 'statement', 'file 2' ); 00586 break; 00587 case 'statement': 00588 $token = $this->skipSpace(); 00589 if ( !$this->validatePath( $token->text ) ) { 00590 $this->error( "Invalid variable name \"{$token->text}\"" ); 00591 } 00592 $this->nextPath( $token->text ); 00593 $this->expect( T_VARIABLE ); 00594 $this->skipSpace(); 00595 $arrayAssign = false; 00596 if ( $this->currentToken()->type == '[' ) { 00597 $this->nextToken(); 00598 $token = $this->skipSpace(); 00599 if ( !$token->isScalar() ) { 00600 $this->error( "expected a string or number for the array key" ); 00601 } 00602 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00603 $text = $this->parseScalar( $token->text ); 00604 } else { 00605 $text = $token->text; 00606 } 00607 if ( !$this->validatePath( $text ) ) { 00608 $this->error( "Invalid associative array name \"$text\"" ); 00609 } 00610 $this->pushPath( $text ); 00611 $this->nextToken(); 00612 $this->skipSpace(); 00613 $this->expect( ']' ); 00614 $this->skipSpace(); 00615 $arrayAssign = true; 00616 } 00617 $this->expect( '=' ); 00618 $this->skipSpace(); 00619 $this->startPathValue(); 00620 if ( $arrayAssign ) 00621 $this->pushState( 'expression', 'array assign end' ); 00622 else 00623 $this->pushState( 'expression', 'statement end' ); 00624 break; 00625 case 'array assign end': 00626 case 'statement end': 00627 $this->endPathValue(); 00628 if ( $state == 'array assign end' ) 00629 $this->popPath(); 00630 $this->skipSpace(); 00631 $this->expect( ';' ); 00632 $this->nextPath( '@extra-' . ($this->serial++) ); 00633 break; 00634 case 'expression': 00635 $token = $this->skipSpace(); 00636 if ( $token->type == T_ARRAY ) { 00637 $this->pushState( 'array' ); 00638 } elseif ( $token->isScalar() ) { 00639 $this->nextToken(); 00640 } elseif ( $token->type == T_VARIABLE ) { 00641 $this->nextToken(); 00642 } else { 00643 $this->error( "expected simple expression" ); 00644 } 00645 break; 00646 case 'array': 00647 $this->skipSpace(); 00648 $this->expect( T_ARRAY ); 00649 $this->skipSpace(); 00650 $this->expect( '(' ); 00651 $this->skipSpace(); 00652 $this->pushPath( '@extra-' . ($this->serial++) ); 00653 if ( $this->isAhead( ')' ) ) { 00654 // Empty array 00655 $this->pushState( 'array end' ); 00656 } else { 00657 $this->pushState( 'element', 'array end' ); 00658 } 00659 break; 00660 case 'array end': 00661 $this->skipSpace(); 00662 $this->popPath(); 00663 $this->expect( ')' ); 00664 break; 00665 case 'element': 00666 $token = $this->skipSpace(); 00667 // Look ahead to find the double arrow 00668 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { 00669 // Found associative element 00670 $this->pushState( 'assoc-element', 'element end' ); 00671 } else { 00672 // Not associative 00673 $this->nextPath( '@next' ); 00674 $this->startPathValue(); 00675 $this->pushState( 'expression', 'element end' ); 00676 } 00677 break; 00678 case 'element end': 00679 $token = $this->skipSpace(); 00680 if ( $token->type == ',' ) { 00681 $this->endPathValue(); 00682 $this->markComma(); 00683 $this->nextToken(); 00684 $this->nextPath( '@extra-' . ($this->serial++) ); 00685 // Look ahead to find ending bracket 00686 if ( $this->isAhead( ")" ) ) { 00687 // Found ending bracket, no continuation 00688 $this->skipSpace(); 00689 } else { 00690 // No ending bracket, continue to next element 00691 $this->pushState( 'element' ); 00692 } 00693 } elseif ( $token->type == ')' ) { 00694 // End array 00695 $this->endPathValue(); 00696 } else { 00697 $this->error( "expected the next array element or the end of the array" ); 00698 } 00699 break; 00700 case 'assoc-element': 00701 $token = $this->skipSpace(); 00702 if ( !$token->isScalar() ) { 00703 $this->error( "expected a string or number for the array key" ); 00704 } 00705 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00706 $text = $this->parseScalar( $token->text ); 00707 } else { 00708 $text = $token->text; 00709 } 00710 if ( !$this->validatePath( $text ) ) { 00711 $this->error( "Invalid associative array name \"$text\"" ); 00712 } 00713 $this->nextPath( $text ); 00714 $this->nextToken(); 00715 $this->skipSpace(); 00716 $this->markArrow(); 00717 $this->expect( T_DOUBLE_ARROW ); 00718 $this->skipSpace(); 00719 $this->startPathValue(); 00720 $this->pushState( 'expression' ); 00721 break; 00722 } 00723 } 00724 if ( count( $this->stateStack ) ) { 00725 $this->error( 'unexpected end of file' ); 00726 } 00727 $this->popPath(); 00728 } 00729 00733 protected function initParse() { 00734 $this->tokens = token_get_all( $this->text ); 00735 $this->stateStack = array(); 00736 $this->pathStack = array(); 00737 $this->firstToken(); 00738 $this->pathInfo = array(); 00739 $this->serial = 1; 00740 } 00741 00746 protected function setPos( $pos ) { 00747 $this->pos = $pos; 00748 if ( $this->pos >= count( $this->tokens ) ) { 00749 $this->currentToken = ConfEditorToken::newEnd(); 00750 } else { 00751 $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); 00752 } 00753 return $this->currentToken; 00754 } 00755 00760 function newTokenObj( $internalToken ) { 00761 if ( is_array( $internalToken ) ) { 00762 return new ConfEditorToken( $internalToken[0], $internalToken[1] ); 00763 } else { 00764 return new ConfEditorToken( $internalToken, $internalToken ); 00765 } 00766 } 00767 00771 function firstToken() { 00772 $this->setPos( 0 ); 00773 $this->prevToken = ConfEditorToken::newEnd(); 00774 $this->lineNum = 1; 00775 $this->colNum = 1; 00776 $this->byteNum = 0; 00777 return $this->currentToken; 00778 } 00779 00783 function currentToken() { 00784 return $this->currentToken; 00785 } 00786 00790 function nextToken() { 00791 if ( $this->currentToken ) { 00792 $text = $this->currentToken->text; 00793 $lfCount = substr_count( $text, "\n" ); 00794 if ( $lfCount ) { 00795 $this->lineNum += $lfCount; 00796 $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); 00797 } else { 00798 $this->colNum += strlen( $text ); 00799 } 00800 $this->byteNum += strlen( $text ); 00801 } 00802 $this->prevToken = $this->currentToken; 00803 $this->setPos( $this->pos + 1 ); 00804 return $this->currentToken; 00805 } 00806 00812 function getTokenAhead( $offset ) { 00813 $pos = $this->pos + $offset; 00814 if ( $pos >= count( $this->tokens ) || $pos < 0 ) { 00815 return ConfEditorToken::newEnd(); 00816 } else { 00817 return $this->newTokenObj( $this->tokens[$pos] ); 00818 } 00819 } 00820 00824 function skipSpace() { 00825 while ( $this->currentToken && $this->currentToken->isSkip() ) { 00826 $this->nextToken(); 00827 } 00828 return $this->currentToken; 00829 } 00830 00835 function expect( $type ) { 00836 if ( $this->currentToken && $this->currentToken->type == $type ) { 00837 return $this->nextToken(); 00838 } else { 00839 $this->error( "expected " . $this->getTypeName( $type ) . 00840 ", got " . $this->getTypeName( $this->currentToken->type ) ); 00841 } 00842 } 00843 00847 function pushState( $nextState, $stateAfterThat = null ) { 00848 if ( $stateAfterThat !== null ) { 00849 $this->stateStack[] = $stateAfterThat; 00850 } 00851 $this->stateStack[] = $nextState; 00852 } 00853 00858 function popState() { 00859 return array_pop( $this->stateStack ); 00860 } 00861 00867 function validatePath( $path ) { 00868 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; 00869 } 00870 00875 function endPath() { 00876 $key = ''; 00877 foreach ( $this->pathStack as $pathInfo ) { 00878 if ( $key !== '' ) { 00879 $key .= '/'; 00880 } 00881 $key .= $pathInfo['name']; 00882 } 00883 $pathInfo['endByte'] = $this->byteNum; 00884 $pathInfo['endToken'] = $this->pos; 00885 $this->pathInfo[$key] = $pathInfo; 00886 } 00887 00891 function pushPath( $path ) { 00892 $this->pathStack[] = array( 00893 'name' => $path, 00894 'level' => count( $this->pathStack ) + 1, 00895 'startByte' => $this->byteNum, 00896 'startToken' => $this->pos, 00897 'valueStartToken' => false, 00898 'valueStartByte' => false, 00899 'valueEndToken' => false, 00900 'valueEndByte' => false, 00901 'nextArrayIndex' => 0, 00902 'hasComma' => false, 00903 'arrowByte' => false 00904 ); 00905 } 00906 00910 function popPath() { 00911 $this->endPath(); 00912 array_pop( $this->pathStack ); 00913 } 00914 00920 function nextPath( $path ) { 00921 $this->endPath(); 00922 $i = count( $this->pathStack ) - 1; 00923 if ( $path == '@next' ) { 00924 $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; 00925 $this->pathStack[$i]['name'] = $nextArrayIndex; 00926 $nextArrayIndex++; 00927 } else { 00928 $this->pathStack[$i]['name'] = $path; 00929 } 00930 $this->pathStack[$i] = 00931 array( 00932 'startByte' => $this->byteNum, 00933 'startToken' => $this->pos, 00934 'valueStartToken' => false, 00935 'valueStartByte' => false, 00936 'valueEndToken' => false, 00937 'valueEndByte' => false, 00938 'hasComma' => false, 00939 'arrowByte' => false, 00940 ) + $this->pathStack[$i]; 00941 } 00942 00946 function startPathValue() { 00947 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00948 $path['valueStartToken'] = $this->pos; 00949 $path['valueStartByte'] = $this->byteNum; 00950 } 00951 00955 function endPathValue() { 00956 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00957 $path['valueEndToken'] = $this->pos; 00958 $path['valueEndByte'] = $this->byteNum; 00959 } 00960 00964 function markComma() { 00965 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00966 $path['hasComma'] = true; 00967 } 00968 00972 function markArrow() { 00973 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00974 $path['arrowByte'] = $this->byteNum; 00975 } 00976 00980 function error( $msg ) { 00981 throw new ConfEditorParseError( $this, $msg ); 00982 } 00983 00988 function getTypeName( $type ) { 00989 if ( is_int( $type ) ) { 00990 return token_name( $type ); 00991 } else { 00992 return "\"$type\""; 00993 } 00994 } 00995 01002 function isAhead( $type, $offset = 0 ) { 01003 $ahead = $offset; 01004 $token = $this->getTokenAhead( $offset ); 01005 while ( !$token->isEnd() ) { 01006 if ( $token->isSkip() ) { 01007 $ahead++; 01008 $token = $this->getTokenAhead( $ahead ); 01009 continue; 01010 } elseif ( $token->type == $type ) { 01011 // Found the type 01012 return true; 01013 } else { 01014 // Not found 01015 return false; 01016 } 01017 } 01018 return false; 01019 } 01020 01024 function prevToken() { 01025 return $this->prevToken; 01026 } 01027 01031 function dumpTokens() { 01032 $out = ''; 01033 foreach ( $this->tokens as $token ) { 01034 $obj = $this->newTokenObj( $token ); 01035 $out .= sprintf( "%-28s %s\n", 01036 $this->getTypeName( $obj->type ), 01037 addcslashes( $obj->text, "\0..\37" ) ); 01038 } 01039 echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; 01040 } 01041 } 01042 01046 class ConfEditorParseError extends MWException { 01047 var $lineNum, $colNum; 01048 function __construct( $editor, $msg ) { 01049 $this->lineNum = $editor->lineNum; 01050 $this->colNum = $editor->colNum; 01051 parent::__construct( "Parse error on line {$editor->lineNum} " . 01052 "col {$editor->colNum}: $msg" ); 01053 } 01054 01055 function highlight( $text ) { 01056 $lines = StringUtils::explode( "\n", $text ); 01057 foreach ( $lines as $lineNum => $line ) { 01058 if ( $lineNum == $this->lineNum - 1 ) { 01059 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n"; 01060 } 01061 } 01062 return ''; 01063 } 01064 01065 } 01066 01070 class ConfEditorToken { 01071 var $type, $text; 01072 01073 static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); 01074 static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); 01075 01076 static function newEnd() { 01077 return new self( 'END', '' ); 01078 } 01079 01080 function __construct( $type, $text ) { 01081 $this->type = $type; 01082 $this->text = $text; 01083 } 01084 01085 function isSkip() { 01086 return in_array( $this->type, self::$skipTypes ); 01087 } 01088 01089 function isScalar() { 01090 return in_array( $this->type, self::$scalarTypes ); 01091 } 01092 01093 function isEnd() { 01094 return $this->type == 'END'; 01095 } 01096 }