MediaWiki
REL1_19
|
00001 <?php 00002 00018 class ConfEditor { 00020 var $text; 00021 00023 var $tokens; 00024 00026 var $pos; 00027 00029 var $lineNum; 00030 00032 var $colNum; 00033 00035 var $byteNum; 00036 00038 var $currentToken; 00039 00041 var $prevToken; 00042 00047 var $stateStack; 00048 00049 00066 var $pathStack; 00067 00072 var $pathInfo; 00073 00077 var $serial; 00078 00083 var $edits; 00084 00092 static function test( $text ) { 00093 try { 00094 $ce = new self( $text ); 00095 $ce->parse(); 00096 } catch ( ConfEditorParseError $e ) { 00097 return $e->getMessage() . "\n" . $e->highlight( $text ); 00098 } 00099 return "OK"; 00100 } 00101 00105 public function __construct( $text ) { 00106 $this->text = $text; 00107 } 00108 00143 public function edit( $ops ) { 00144 $this->parse(); 00145 00146 $this->edits = array( 00147 array( 'copy', 0, strlen( $this->text ) ) 00148 ); 00149 foreach ( $ops as $op ) { 00150 $type = $op['type']; 00151 $path = $op['path']; 00152 $value = isset( $op['value'] ) ? $op['value'] : null; 00153 $key = isset( $op['key'] ) ? $op['key'] : null; 00154 00155 switch ( $type ) { 00156 case 'delete': 00157 list( $start, $end ) = $this->findDeletionRegion( $path ); 00158 $this->replaceSourceRegion( $start, $end, false ); 00159 break; 00160 case 'set': 00161 if ( isset( $this->pathInfo[$path] ) ) { 00162 list( $start, $end ) = $this->findValueRegion( $path ); 00163 $encValue = $value; // var_export( $value, true ); 00164 $this->replaceSourceRegion( $start, $end, $encValue ); 00165 break; 00166 } 00167 // No existing path, fall through to append 00168 $slashPos = strrpos( $path, '/' ); 00169 $key = var_export( substr( $path, $slashPos + 1 ), true ); 00170 $path = substr( $path, 0, $slashPos ); 00171 // Fall through 00172 case 'append': 00173 // Find the last array element 00174 $lastEltPath = $this->findLastArrayElement( $path ); 00175 if ( $lastEltPath === false ) { 00176 throw new MWException( "Can't find any element of array \"$path\"" ); 00177 } 00178 $lastEltInfo = $this->pathInfo[$lastEltPath]; 00179 00180 // Has it got a comma already? 00181 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) { 00182 // No comma, insert one after the value region 00183 list( , $end ) = $this->findValueRegion( $lastEltPath ); 00184 $this->replaceSourceRegion( $end - 1, $end - 1, ',' ); 00185 } 00186 00187 // Make the text to insert 00188 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath ); 00189 00190 if ( $key === null ) { 00191 list( $indent, ) = $this->getIndent( $start ); 00192 $textToInsert = "$indent$value,"; 00193 } else { 00194 list( $indent, $arrowIndent ) = 00195 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); 00196 $textToInsert = "$indent$key$arrowIndent=> $value,"; 00197 } 00198 $textToInsert .= ( $indent === false ? ' ' : "\n" ); 00199 00200 // Insert the item 00201 $this->replaceSourceRegion( $end, $end, $textToInsert ); 00202 break; 00203 case 'insert': 00204 // Find first array element 00205 $firstEltPath = $this->findFirstArrayElement( $path ); 00206 if ( $firstEltPath === false ) { 00207 throw new MWException( "Can't find array element of \"$path\"" ); 00208 } 00209 list( $start, ) = $this->findDeletionRegion( $firstEltPath ); 00210 $info = $this->pathInfo[$firstEltPath]; 00211 00212 // Make the text to insert 00213 if ( $key === null ) { 00214 list( $indent, ) = $this->getIndent( $start ); 00215 $textToInsert = "$indent$value,"; 00216 } else { 00217 list( $indent, $arrowIndent ) = 00218 $this->getIndent( $start, $key, $info['arrowByte'] ); 00219 $textToInsert = "$indent$key$arrowIndent=> $value,"; 00220 } 00221 $textToInsert .= ( $indent === false ? ' ' : "\n" ); 00222 00223 // Insert the item 00224 $this->replaceSourceRegion( $start, $start, $textToInsert ); 00225 break; 00226 default: 00227 throw new MWException( "Unrecognised operation: \"$type\"" ); 00228 } 00229 } 00230 00231 // Do the edits 00232 $out = ''; 00233 foreach ( $this->edits as $edit ) { 00234 if ( $edit[0] == 'copy' ) { 00235 $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] ); 00236 } else { // if ( $edit[0] == 'insert' ) 00237 $out .= $edit[1]; 00238 } 00239 } 00240 00241 // Do a second parse as a sanity check 00242 $this->text = $out; 00243 try { 00244 $this->parse(); 00245 } catch ( ConfEditorParseError $e ) { 00246 throw new MWException( 00247 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . 00248 $e->getMessage() ); 00249 } 00250 return $out; 00251 } 00252 00257 function getVars() { 00258 $vars = array(); 00259 $this->parse(); 00260 foreach( $this->pathInfo as $path => $data ) { 00261 if ( $path[0] != '$' ) 00262 continue; 00263 $trimmedPath = substr( $path, 1 ); 00264 $name = $data['name']; 00265 if ( $name[0] == '@' ) 00266 continue; 00267 if ( $name[0] == '$' ) 00268 $name = substr( $name, 1 ); 00269 $parentPath = substr( $trimmedPath, 0, 00270 strlen( $trimmedPath ) - strlen( $name ) ); 00271 if( substr( $parentPath, -1 ) == '/' ) 00272 $parentPath = substr( $parentPath, 0, -1 ); 00273 00274 $value = substr( $this->text, $data['valueStartByte'], 00275 $data['valueEndByte'] - $data['valueStartByte'] 00276 ); 00277 $this->setVar( $vars, $parentPath, $name, 00278 $this->parseScalar( $value ) ); 00279 } 00280 return $vars; 00281 } 00282 00292 function setVar( &$array, $path, $key, $value ) { 00293 $pathArr = explode( '/', $path ); 00294 $target =& $array; 00295 if ( $path !== '' ) { 00296 foreach ( $pathArr as $p ) { 00297 if( !isset( $target[$p] ) ) 00298 $target[$p] = array(); 00299 $target =& $target[$p]; 00300 } 00301 } 00302 if ( !isset( $target[$key] ) ) 00303 $target[$key] = $value; 00304 } 00305 00310 function parseScalar( $str ) { 00311 if ( $str !== '' && $str[0] == '\'' ) 00312 // Single-quoted string 00313 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00314 // appended to the token; without it we ended up reading in the 00315 // extra quote on the end! 00316 return strtr( substr( trim( $str ), 1, -1 ), 00317 array( '\\\'' => '\'', '\\\\' => '\\' ) ); 00318 if ( $str !== '' && $str[0] == '"' ) 00319 // Double-quoted string 00320 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00321 // appended to the token; without it we ended up reading in the 00322 // extra quote on the end! 00323 return stripcslashes( substr( trim( $str ), 1, -1 ) ); 00324 if ( substr( $str, 0, 4 ) == 'true' ) 00325 return true; 00326 if ( substr( $str, 0, 5 ) == 'false' ) 00327 return false; 00328 if ( substr( $str, 0, 4 ) == 'null' ) 00329 return null; 00330 // Must be some kind of numeric value, so let PHP's weak typing 00331 // be useful for a change 00332 return $str; 00333 } 00334 00339 function replaceSourceRegion( $start, $end, $newText = false ) { 00340 // Split all copy operations with a source corresponding to the region 00341 // in question. 00342 $newEdits = array(); 00343 foreach ( $this->edits as $edit ) { 00344 if ( $edit[0] !== 'copy' ) { 00345 $newEdits[] = $edit; 00346 continue; 00347 } 00348 $copyStart = $edit[1]; 00349 $copyEnd = $edit[2]; 00350 if ( $start >= $copyEnd || $end <= $copyStart ) { 00351 // Outside this region 00352 $newEdits[] = $edit; 00353 continue; 00354 } 00355 if ( ( $start < $copyStart && $end > $copyStart ) 00356 || ( $start < $copyEnd && $end > $copyEnd ) 00357 ) { 00358 throw new MWException( "Overlapping regions found, can't do the edit" ); 00359 } 00360 // Split the copy 00361 $newEdits[] = array( 'copy', $copyStart, $start ); 00362 if ( $newText !== false ) { 00363 $newEdits[] = array( 'insert', $newText ); 00364 } 00365 $newEdits[] = array( 'copy', $end, $copyEnd ); 00366 } 00367 $this->edits = $newEdits; 00368 } 00369 00375 function findDeletionRegion( $pathName ) { 00376 if ( !isset( $this->pathInfo[$pathName] ) ) { 00377 throw new MWException( "Can't find path \"$pathName\"" ); 00378 } 00379 $path = $this->pathInfo[$pathName]; 00380 // Find the start 00381 $this->firstToken(); 00382 while ( $this->pos != $path['startToken'] ) { 00383 $this->nextToken(); 00384 } 00385 $regionStart = $path['startByte']; 00386 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { 00387 $token = $this->getTokenAhead( $offset ); 00388 if ( !$token->isSkip() ) { 00389 // If there is other content on the same line, don't move the start point 00390 // back, because that will cause the regions to overlap. 00391 $regionStart = $path['startByte']; 00392 break; 00393 } 00394 $lfPos = strrpos( $token->text, "\n" ); 00395 if ( $lfPos === false ) { 00396 $regionStart -= strlen( $token->text ); 00397 } else { 00398 // The line start does not include the LF 00399 $regionStart -= strlen( $token->text ) - $lfPos - 1; 00400 break; 00401 } 00402 } 00403 // Find the end 00404 while ( $this->pos != $path['endToken'] ) { 00405 $this->nextToken(); 00406 } 00407 $regionEnd = $path['endByte']; // past the end 00408 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { 00409 $token = $this->getTokenAhead( $offset ); 00410 if ( !$token->isSkip() ) { 00411 break; 00412 } 00413 $lfPos = strpos( $token->text, "\n" ); 00414 if ( $lfPos === false ) { 00415 $regionEnd += strlen( $token->text ); 00416 } else { 00417 // This should point past the LF 00418 $regionEnd += $lfPos + 1; 00419 break; 00420 } 00421 } 00422 return array( $regionStart, $regionEnd ); 00423 } 00424 00432 function findValueRegion( $pathName ) { 00433 if ( !isset( $this->pathInfo[$pathName] ) ) { 00434 throw new MWException( "Can't find path \"$pathName\"" ); 00435 } 00436 $path = $this->pathInfo[$pathName]; 00437 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { 00438 throw new MWException( "Can't find value region for path \"$pathName\"" ); 00439 } 00440 return array( $path['valueStartByte'], $path['valueEndByte'] ); 00441 } 00442 00448 function findLastArrayElement( $path ) { 00449 // Try for a real element 00450 $lastEltPath = false; 00451 foreach ( $this->pathInfo as $candidatePath => $info ) { 00452 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00453 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00454 if ( $part2 == '@' ) { 00455 // Do nothing 00456 } elseif ( $part1 == "$path/" ) { 00457 $lastEltPath = $candidatePath; 00458 } elseif ( $lastEltPath !== false ) { 00459 break; 00460 } 00461 } 00462 if ( $lastEltPath !== false ) { 00463 return $lastEltPath; 00464 } 00465 00466 // Try for an interstitial element 00467 $extraPath = false; 00468 foreach ( $this->pathInfo as $candidatePath => $info ) { 00469 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00470 if ( $part1 == "$path/" ) { 00471 $extraPath = $candidatePath; 00472 } elseif ( $extraPath !== false ) { 00473 break; 00474 } 00475 } 00476 return $extraPath; 00477 } 00478 00484 function findFirstArrayElement( $path ) { 00485 // Try for an ordinary element 00486 foreach ( $this->pathInfo as $candidatePath => $info ) { 00487 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00488 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00489 if ( $part1 == "$path/" && $part2 != '@' ) { 00490 return $candidatePath; 00491 } 00492 } 00493 00494 // Try for an interstitial element 00495 foreach ( $this->pathInfo as $candidatePath => $info ) { 00496 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00497 if ( $part1 == "$path/" ) { 00498 return $candidatePath; 00499 } 00500 } 00501 return false; 00502 } 00503 00508 function getIndent( $pos, $key = false, $arrowPos = false ) { 00509 $arrowIndent = ' '; 00510 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) { 00511 $indentLength = strspn( $this->text, " \t", $pos ); 00512 $indent = substr( $this->text, $pos, $indentLength ); 00513 } else { 00514 $indent = false; 00515 } 00516 if ( $indent !== false && $arrowPos !== false ) { 00517 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); 00518 if ( $arrowIndentLength > 0 ) { 00519 $arrowIndent = str_repeat( ' ', $arrowIndentLength ); 00520 } 00521 } 00522 return array( $indent, $arrowIndent ); 00523 } 00524 00529 public function parse() { 00530 $this->initParse(); 00531 $this->pushState( 'file' ); 00532 $this->pushPath( '@extra-' . ($this->serial++) ); 00533 $token = $this->firstToken(); 00534 00535 while ( !$token->isEnd() ) { 00536 $state = $this->popState(); 00537 if ( !$state ) { 00538 $this->error( 'internal error: empty state stack' ); 00539 } 00540 00541 switch ( $state ) { 00542 case 'file': 00543 $this->expect( T_OPEN_TAG ); 00544 $token = $this->skipSpace(); 00545 if ( $token->isEnd() ) { 00546 break 2; 00547 } 00548 $this->pushState( 'statement', 'file 2' ); 00549 break; 00550 case 'file 2': 00551 $token = $this->skipSpace(); 00552 if ( $token->isEnd() ) { 00553 break 2; 00554 } 00555 $this->pushState( 'statement', 'file 2' ); 00556 break; 00557 case 'statement': 00558 $token = $this->skipSpace(); 00559 if ( !$this->validatePath( $token->text ) ) { 00560 $this->error( "Invalid variable name \"{$token->text}\"" ); 00561 } 00562 $this->nextPath( $token->text ); 00563 $this->expect( T_VARIABLE ); 00564 $this->skipSpace(); 00565 $arrayAssign = false; 00566 if ( $this->currentToken()->type == '[' ) { 00567 $this->nextToken(); 00568 $token = $this->skipSpace(); 00569 if ( !$token->isScalar() ) { 00570 $this->error( "expected a string or number for the array key" ); 00571 } 00572 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00573 $text = $this->parseScalar( $token->text ); 00574 } else { 00575 $text = $token->text; 00576 } 00577 if ( !$this->validatePath( $text ) ) { 00578 $this->error( "Invalid associative array name \"$text\"" ); 00579 } 00580 $this->pushPath( $text ); 00581 $this->nextToken(); 00582 $this->skipSpace(); 00583 $this->expect( ']' ); 00584 $this->skipSpace(); 00585 $arrayAssign = true; 00586 } 00587 $this->expect( '=' ); 00588 $this->skipSpace(); 00589 $this->startPathValue(); 00590 if ( $arrayAssign ) 00591 $this->pushState( 'expression', 'array assign end' ); 00592 else 00593 $this->pushState( 'expression', 'statement end' ); 00594 break; 00595 case 'array assign end': 00596 case 'statement end': 00597 $this->endPathValue(); 00598 if ( $state == 'array assign end' ) 00599 $this->popPath(); 00600 $this->skipSpace(); 00601 $this->expect( ';' ); 00602 $this->nextPath( '@extra-' . ($this->serial++) ); 00603 break; 00604 case 'expression': 00605 $token = $this->skipSpace(); 00606 if ( $token->type == T_ARRAY ) { 00607 $this->pushState( 'array' ); 00608 } elseif ( $token->isScalar() ) { 00609 $this->nextToken(); 00610 } elseif ( $token->type == T_VARIABLE ) { 00611 $this->nextToken(); 00612 } else { 00613 $this->error( "expected simple expression" ); 00614 } 00615 break; 00616 case 'array': 00617 $this->skipSpace(); 00618 $this->expect( T_ARRAY ); 00619 $this->skipSpace(); 00620 $this->expect( '(' ); 00621 $this->skipSpace(); 00622 $this->pushPath( '@extra-' . ($this->serial++) ); 00623 if ( $this->isAhead( ')' ) ) { 00624 // Empty array 00625 $this->pushState( 'array end' ); 00626 } else { 00627 $this->pushState( 'element', 'array end' ); 00628 } 00629 break; 00630 case 'array end': 00631 $this->skipSpace(); 00632 $this->popPath(); 00633 $this->expect( ')' ); 00634 break; 00635 case 'element': 00636 $token = $this->skipSpace(); 00637 // Look ahead to find the double arrow 00638 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { 00639 // Found associative element 00640 $this->pushState( 'assoc-element', 'element end' ); 00641 } else { 00642 // Not associative 00643 $this->nextPath( '@next' ); 00644 $this->startPathValue(); 00645 $this->pushState( 'expression', 'element end' ); 00646 } 00647 break; 00648 case 'element end': 00649 $token = $this->skipSpace(); 00650 if ( $token->type == ',' ) { 00651 $this->endPathValue(); 00652 $this->markComma(); 00653 $this->nextToken(); 00654 $this->nextPath( '@extra-' . ($this->serial++) ); 00655 // Look ahead to find ending bracket 00656 if ( $this->isAhead( ")" ) ) { 00657 // Found ending bracket, no continuation 00658 $this->skipSpace(); 00659 } else { 00660 // No ending bracket, continue to next element 00661 $this->pushState( 'element' ); 00662 } 00663 } elseif ( $token->type == ')' ) { 00664 // End array 00665 $this->endPathValue(); 00666 } else { 00667 $this->error( "expected the next array element or the end of the array" ); 00668 } 00669 break; 00670 case 'assoc-element': 00671 $token = $this->skipSpace(); 00672 if ( !$token->isScalar() ) { 00673 $this->error( "expected a string or number for the array key" ); 00674 } 00675 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00676 $text = $this->parseScalar( $token->text ); 00677 } else { 00678 $text = $token->text; 00679 } 00680 if ( !$this->validatePath( $text ) ) { 00681 $this->error( "Invalid associative array name \"$text\"" ); 00682 } 00683 $this->nextPath( $text ); 00684 $this->nextToken(); 00685 $this->skipSpace(); 00686 $this->markArrow(); 00687 $this->expect( T_DOUBLE_ARROW ); 00688 $this->skipSpace(); 00689 $this->startPathValue(); 00690 $this->pushState( 'expression' ); 00691 break; 00692 } 00693 } 00694 if ( count( $this->stateStack ) ) { 00695 $this->error( 'unexpected end of file' ); 00696 } 00697 $this->popPath(); 00698 } 00699 00703 protected function initParse() { 00704 $this->tokens = token_get_all( $this->text ); 00705 $this->stateStack = array(); 00706 $this->pathStack = array(); 00707 $this->firstToken(); 00708 $this->pathInfo = array(); 00709 $this->serial = 1; 00710 } 00711 00716 protected function setPos( $pos ) { 00717 $this->pos = $pos; 00718 if ( $this->pos >= count( $this->tokens ) ) { 00719 $this->currentToken = ConfEditorToken::newEnd(); 00720 } else { 00721 $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); 00722 } 00723 return $this->currentToken; 00724 } 00725 00729 function newTokenObj( $internalToken ) { 00730 if ( is_array( $internalToken ) ) { 00731 return new ConfEditorToken( $internalToken[0], $internalToken[1] ); 00732 } else { 00733 return new ConfEditorToken( $internalToken, $internalToken ); 00734 } 00735 } 00736 00740 function firstToken() { 00741 $this->setPos( 0 ); 00742 $this->prevToken = ConfEditorToken::newEnd(); 00743 $this->lineNum = 1; 00744 $this->colNum = 1; 00745 $this->byteNum = 0; 00746 return $this->currentToken; 00747 } 00748 00752 function currentToken() { 00753 return $this->currentToken; 00754 } 00755 00759 function nextToken() { 00760 if ( $this->currentToken ) { 00761 $text = $this->currentToken->text; 00762 $lfCount = substr_count( $text, "\n" ); 00763 if ( $lfCount ) { 00764 $this->lineNum += $lfCount; 00765 $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); 00766 } else { 00767 $this->colNum += strlen( $text ); 00768 } 00769 $this->byteNum += strlen( $text ); 00770 } 00771 $this->prevToken = $this->currentToken; 00772 $this->setPos( $this->pos + 1 ); 00773 return $this->currentToken; 00774 } 00775 00780 function getTokenAhead( $offset ) { 00781 $pos = $this->pos + $offset; 00782 if ( $pos >= count( $this->tokens ) || $pos < 0 ) { 00783 return ConfEditorToken::newEnd(); 00784 } else { 00785 return $this->newTokenObj( $this->tokens[$pos] ); 00786 } 00787 } 00788 00792 function skipSpace() { 00793 while ( $this->currentToken && $this->currentToken->isSkip() ) { 00794 $this->nextToken(); 00795 } 00796 return $this->currentToken; 00797 } 00798 00803 function expect( $type ) { 00804 if ( $this->currentToken && $this->currentToken->type == $type ) { 00805 return $this->nextToken(); 00806 } else { 00807 $this->error( "expected " . $this->getTypeName( $type ) . 00808 ", got " . $this->getTypeName( $this->currentToken->type ) ); 00809 } 00810 } 00811 00815 function pushState( $nextState, $stateAfterThat = null ) { 00816 if ( $stateAfterThat !== null ) { 00817 $this->stateStack[] = $stateAfterThat; 00818 } 00819 $this->stateStack[] = $nextState; 00820 } 00821 00825 function popState() { 00826 return array_pop( $this->stateStack ); 00827 } 00828 00833 function validatePath( $path ) { 00834 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; 00835 } 00836 00841 function endPath() { 00842 $key = ''; 00843 foreach ( $this->pathStack as $pathInfo ) { 00844 if ( $key !== '' ) { 00845 $key .= '/'; 00846 } 00847 $key .= $pathInfo['name']; 00848 } 00849 $pathInfo['endByte'] = $this->byteNum; 00850 $pathInfo['endToken'] = $this->pos; 00851 $this->pathInfo[$key] = $pathInfo; 00852 } 00853 00857 function pushPath( $path ) { 00858 $this->pathStack[] = array( 00859 'name' => $path, 00860 'level' => count( $this->pathStack ) + 1, 00861 'startByte' => $this->byteNum, 00862 'startToken' => $this->pos, 00863 'valueStartToken' => false, 00864 'valueStartByte' => false, 00865 'valueEndToken' => false, 00866 'valueEndByte' => false, 00867 'nextArrayIndex' => 0, 00868 'hasComma' => false, 00869 'arrowByte' => false 00870 ); 00871 } 00872 00876 function popPath() { 00877 $this->endPath(); 00878 array_pop( $this->pathStack ); 00879 } 00880 00886 function nextPath( $path ) { 00887 $this->endPath(); 00888 $i = count( $this->pathStack ) - 1; 00889 if ( $path == '@next' ) { 00890 $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; 00891 $this->pathStack[$i]['name'] = $nextArrayIndex; 00892 $nextArrayIndex++; 00893 } else { 00894 $this->pathStack[$i]['name'] = $path; 00895 } 00896 $this->pathStack[$i] = 00897 array( 00898 'startByte' => $this->byteNum, 00899 'startToken' => $this->pos, 00900 'valueStartToken' => false, 00901 'valueStartByte' => false, 00902 'valueEndToken' => false, 00903 'valueEndByte' => false, 00904 'hasComma' => false, 00905 'arrowByte' => false, 00906 ) + $this->pathStack[$i]; 00907 } 00908 00912 function startPathValue() { 00913 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00914 $path['valueStartToken'] = $this->pos; 00915 $path['valueStartByte'] = $this->byteNum; 00916 } 00917 00921 function endPathValue() { 00922 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00923 $path['valueEndToken'] = $this->pos; 00924 $path['valueEndByte'] = $this->byteNum; 00925 } 00926 00930 function markComma() { 00931 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00932 $path['hasComma'] = true; 00933 } 00934 00938 function markArrow() { 00939 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00940 $path['arrowByte'] = $this->byteNum; 00941 } 00942 00946 function error( $msg ) { 00947 throw new ConfEditorParseError( $this, $msg ); 00948 } 00949 00953 function getTypeName( $type ) { 00954 if ( is_int( $type ) ) { 00955 return token_name( $type ); 00956 } else { 00957 return "\"$type\""; 00958 } 00959 } 00960 00966 function isAhead( $type, $offset = 0 ) { 00967 $ahead = $offset; 00968 $token = $this->getTokenAhead( $offset ); 00969 while ( !$token->isEnd() ) { 00970 if ( $token->isSkip() ) { 00971 $ahead++; 00972 $token = $this->getTokenAhead( $ahead ); 00973 continue; 00974 } elseif ( $token->type == $type ) { 00975 // Found the type 00976 return true; 00977 } else { 00978 // Not found 00979 return false; 00980 } 00981 } 00982 return false; 00983 } 00984 00988 function prevToken() { 00989 return $this->prevToken; 00990 } 00991 00995 function dumpTokens() { 00996 $out = ''; 00997 foreach ( $this->tokens as $token ) { 00998 $obj = $this->newTokenObj( $token ); 00999 $out .= sprintf( "%-28s %s\n", 01000 $this->getTypeName( $obj->type ), 01001 addcslashes( $obj->text, "\0..\37" ) ); 01002 } 01003 echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; 01004 } 01005 } 01006 01010 class ConfEditorParseError extends MWException { 01011 var $lineNum, $colNum; 01012 function __construct( $editor, $msg ) { 01013 $this->lineNum = $editor->lineNum; 01014 $this->colNum = $editor->colNum; 01015 parent::__construct( "Parse error on line {$editor->lineNum} " . 01016 "col {$editor->colNum}: $msg" ); 01017 } 01018 01019 function highlight( $text ) { 01020 $lines = StringUtils::explode( "\n", $text ); 01021 foreach ( $lines as $lineNum => $line ) { 01022 if ( $lineNum == $this->lineNum - 1 ) { 01023 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n"; 01024 } 01025 } 01026 } 01027 01028 } 01029 01033 class ConfEditorToken { 01034 var $type, $text; 01035 01036 static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); 01037 static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); 01038 01039 static function newEnd() { 01040 return new self( 'END', '' ); 01041 } 01042 01043 function __construct( $type, $text ) { 01044 $this->type = $type; 01045 $this->text = $text; 01046 } 01047 01048 function isSkip() { 01049 return in_array( $this->type, self::$skipTypes ); 01050 } 01051 01052 function isScalar() { 01053 return in_array( $this->type, self::$scalarTypes ); 01054 } 01055 01056 function isEnd() { 01057 return $this->type == 'END'; 01058 } 01059 } 01060