MediaWiki
master
|
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 00069 00086 var $pathStack; 00087 00092 var $pathInfo; 00093 00097 var $serial; 00098 00103 var $edits; 00104 00112 static function test( $text ) { 00113 try { 00114 $ce = new self( $text ); 00115 $ce->parse(); 00116 } catch ( ConfEditorParseError $e ) { 00117 return $e->getMessage() . "\n" . $e->highlight( $text ); 00118 } 00119 return "OK"; 00120 } 00121 00125 public function __construct( $text ) { 00126 $this->text = $text; 00127 } 00128 00165 public function edit( $ops ) { 00166 $this->parse(); 00167 00168 $this->edits = array( 00169 array( 'copy', 0, strlen( $this->text ) ) 00170 ); 00171 foreach ( $ops as $op ) { 00172 $type = $op['type']; 00173 $path = $op['path']; 00174 $value = isset( $op['value'] ) ? $op['value'] : null; 00175 $key = isset( $op['key'] ) ? $op['key'] : null; 00176 00177 switch ( $type ) { 00178 case 'delete': 00179 list( $start, $end ) = $this->findDeletionRegion( $path ); 00180 $this->replaceSourceRegion( $start, $end, false ); 00181 break; 00182 case 'set': 00183 if ( isset( $this->pathInfo[$path] ) ) { 00184 list( $start, $end ) = $this->findValueRegion( $path ); 00185 $encValue = $value; // var_export( $value, true ); 00186 $this->replaceSourceRegion( $start, $end, $encValue ); 00187 break; 00188 } 00189 // No existing path, fall through to append 00190 $slashPos = strrpos( $path, '/' ); 00191 $key = var_export( substr( $path, $slashPos + 1 ), true ); 00192 $path = substr( $path, 0, $slashPos ); 00193 // Fall through 00194 case 'append': 00195 // Find the last array element 00196 $lastEltPath = $this->findLastArrayElement( $path ); 00197 if ( $lastEltPath === false ) { 00198 throw new MWException( "Can't find any element of array \"$path\"" ); 00199 } 00200 $lastEltInfo = $this->pathInfo[$lastEltPath]; 00201 00202 // Has it got a comma already? 00203 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) { 00204 // No comma, insert one after the value region 00205 list( , $end ) = $this->findValueRegion( $lastEltPath ); 00206 $this->replaceSourceRegion( $end - 1, $end - 1, ',' ); 00207 } 00208 00209 // Make the text to insert 00210 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath ); 00211 00212 if ( $key === null ) { 00213 list( $indent, ) = $this->getIndent( $start ); 00214 $textToInsert = "$indent$value,"; 00215 } else { 00216 list( $indent, $arrowIndent ) = 00217 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); 00218 $textToInsert = "$indent$key$arrowIndent=> $value,"; 00219 } 00220 $textToInsert .= ( $indent === false ? ' ' : "\n" ); 00221 00222 // Insert the item 00223 $this->replaceSourceRegion( $end, $end, $textToInsert ); 00224 break; 00225 case 'insert': 00226 // Find first array element 00227 $firstEltPath = $this->findFirstArrayElement( $path ); 00228 if ( $firstEltPath === false ) { 00229 throw new MWException( "Can't find array element of \"$path\"" ); 00230 } 00231 list( $start, ) = $this->findDeletionRegion( $firstEltPath ); 00232 $info = $this->pathInfo[$firstEltPath]; 00233 00234 // Make the text to insert 00235 if ( $key === null ) { 00236 list( $indent, ) = $this->getIndent( $start ); 00237 $textToInsert = "$indent$value,"; 00238 } else { 00239 list( $indent, $arrowIndent ) = 00240 $this->getIndent( $start, $key, $info['arrowByte'] ); 00241 $textToInsert = "$indent$key$arrowIndent=> $value,"; 00242 } 00243 $textToInsert .= ( $indent === false ? ' ' : "\n" ); 00244 00245 // Insert the item 00246 $this->replaceSourceRegion( $start, $start, $textToInsert ); 00247 break; 00248 default: 00249 throw new MWException( "Unrecognised operation: \"$type\"" ); 00250 } 00251 } 00252 00253 // Do the edits 00254 $out = ''; 00255 foreach ( $this->edits as $edit ) { 00256 if ( $edit[0] == 'copy' ) { 00257 $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] ); 00258 } else { // if ( $edit[0] == 'insert' ) 00259 $out .= $edit[1]; 00260 } 00261 } 00262 00263 // Do a second parse as a sanity check 00264 $this->text = $out; 00265 try { 00266 $this->parse(); 00267 } catch ( ConfEditorParseError $e ) { 00268 throw new MWException( 00269 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . 00270 $e->getMessage() ); 00271 } 00272 return $out; 00273 } 00274 00279 function getVars() { 00280 $vars = array(); 00281 $this->parse(); 00282 foreach( $this->pathInfo as $path => $data ) { 00283 if ( $path[0] != '$' ) 00284 continue; 00285 $trimmedPath = substr( $path, 1 ); 00286 $name = $data['name']; 00287 if ( $name[0] == '@' ) 00288 continue; 00289 if ( $name[0] == '$' ) 00290 $name = substr( $name, 1 ); 00291 $parentPath = substr( $trimmedPath, 0, 00292 strlen( $trimmedPath ) - strlen( $name ) ); 00293 if( substr( $parentPath, -1 ) == '/' ) 00294 $parentPath = substr( $parentPath, 0, -1 ); 00295 00296 $value = substr( $this->text, $data['valueStartByte'], 00297 $data['valueEndByte'] - $data['valueStartByte'] 00298 ); 00299 $this->setVar( $vars, $parentPath, $name, 00300 $this->parseScalar( $value ) ); 00301 } 00302 return $vars; 00303 } 00304 00314 function setVar( &$array, $path, $key, $value ) { 00315 $pathArr = explode( '/', $path ); 00316 $target =& $array; 00317 if ( $path !== '' ) { 00318 foreach ( $pathArr as $p ) { 00319 if( !isset( $target[$p] ) ) 00320 $target[$p] = array(); 00321 $target =& $target[$p]; 00322 } 00323 } 00324 if ( !isset( $target[$key] ) ) 00325 $target[$key] = $value; 00326 } 00327 00332 function parseScalar( $str ) { 00333 if ( $str !== '' && $str[0] == '\'' ) 00334 // Single-quoted string 00335 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00336 // appended to the token; without it we ended up reading in the 00337 // extra quote on the end! 00338 return strtr( substr( trim( $str ), 1, -1 ), 00339 array( '\\\'' => '\'', '\\\\' => '\\' ) ); 00340 if ( $str !== '' && $str[0] == '"' ) 00341 // Double-quoted string 00342 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00343 // appended to the token; without it we ended up reading in the 00344 // extra quote on the end! 00345 return stripcslashes( substr( trim( $str ), 1, -1 ) ); 00346 if ( substr( $str, 0, 4 ) == 'true' ) 00347 return true; 00348 if ( substr( $str, 0, 5 ) == 'false' ) 00349 return false; 00350 if ( substr( $str, 0, 4 ) == 'null' ) 00351 return null; 00352 // Must be some kind of numeric value, so let PHP's weak typing 00353 // be useful for a change 00354 return $str; 00355 } 00356 00361 function replaceSourceRegion( $start, $end, $newText = false ) { 00362 // Split all copy operations with a source corresponding to the region 00363 // in question. 00364 $newEdits = array(); 00365 foreach ( $this->edits as $edit ) { 00366 if ( $edit[0] !== 'copy' ) { 00367 $newEdits[] = $edit; 00368 continue; 00369 } 00370 $copyStart = $edit[1]; 00371 $copyEnd = $edit[2]; 00372 if ( $start >= $copyEnd || $end <= $copyStart ) { 00373 // Outside this region 00374 $newEdits[] = $edit; 00375 continue; 00376 } 00377 if ( ( $start < $copyStart && $end > $copyStart ) 00378 || ( $start < $copyEnd && $end > $copyEnd ) 00379 ) { 00380 throw new MWException( "Overlapping regions found, can't do the edit" ); 00381 } 00382 // Split the copy 00383 $newEdits[] = array( 'copy', $copyStart, $start ); 00384 if ( $newText !== false ) { 00385 $newEdits[] = array( 'insert', $newText ); 00386 } 00387 $newEdits[] = array( 'copy', $end, $copyEnd ); 00388 } 00389 $this->edits = $newEdits; 00390 } 00391 00400 function findDeletionRegion( $pathName ) { 00401 if ( !isset( $this->pathInfo[$pathName] ) ) { 00402 throw new MWException( "Can't find path \"$pathName\"" ); 00403 } 00404 $path = $this->pathInfo[$pathName]; 00405 // Find the start 00406 $this->firstToken(); 00407 while ( $this->pos != $path['startToken'] ) { 00408 $this->nextToken(); 00409 } 00410 $regionStart = $path['startByte']; 00411 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { 00412 $token = $this->getTokenAhead( $offset ); 00413 if ( !$token->isSkip() ) { 00414 // If there is other content on the same line, don't move the start point 00415 // back, because that will cause the regions to overlap. 00416 $regionStart = $path['startByte']; 00417 break; 00418 } 00419 $lfPos = strrpos( $token->text, "\n" ); 00420 if ( $lfPos === false ) { 00421 $regionStart -= strlen( $token->text ); 00422 } else { 00423 // The line start does not include the LF 00424 $regionStart -= strlen( $token->text ) - $lfPos - 1; 00425 break; 00426 } 00427 } 00428 // Find the end 00429 while ( $this->pos != $path['endToken'] ) { 00430 $this->nextToken(); 00431 } 00432 $regionEnd = $path['endByte']; // past the end 00433 for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { 00434 $token = $this->getTokenAhead( $offset ); 00435 if ( !$token->isSkip() ) { 00436 break; 00437 } 00438 $lfPos = strpos( $token->text, "\n" ); 00439 if ( $lfPos === false ) { 00440 $regionEnd += strlen( $token->text ); 00441 } else { 00442 // This should point past the LF 00443 $regionEnd += $lfPos + 1; 00444 break; 00445 } 00446 } 00447 return array( $regionStart, $regionEnd ); 00448 } 00449 00460 function findValueRegion( $pathName ) { 00461 if ( !isset( $this->pathInfo[$pathName] ) ) { 00462 throw new MWException( "Can't find path \"$pathName\"" ); 00463 } 00464 $path = $this->pathInfo[$pathName]; 00465 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { 00466 throw new MWException( "Can't find value region for path \"$pathName\"" ); 00467 } 00468 return array( $path['valueStartByte'], $path['valueEndByte'] ); 00469 } 00470 00477 function findLastArrayElement( $path ) { 00478 // Try for a real element 00479 $lastEltPath = false; 00480 foreach ( $this->pathInfo as $candidatePath => $info ) { 00481 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00482 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00483 if ( $part2 == '@' ) { 00484 // Do nothing 00485 } elseif ( $part1 == "$path/" ) { 00486 $lastEltPath = $candidatePath; 00487 } elseif ( $lastEltPath !== false ) { 00488 break; 00489 } 00490 } 00491 if ( $lastEltPath !== false ) { 00492 return $lastEltPath; 00493 } 00494 00495 // Try for an interstitial element 00496 $extraPath = false; 00497 foreach ( $this->pathInfo as $candidatePath => $info ) { 00498 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00499 if ( $part1 == "$path/" ) { 00500 $extraPath = $candidatePath; 00501 } elseif ( $extraPath !== false ) { 00502 break; 00503 } 00504 } 00505 return $extraPath; 00506 } 00507 00514 function findFirstArrayElement( $path ) { 00515 // Try for an ordinary element 00516 foreach ( $this->pathInfo as $candidatePath => $info ) { 00517 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00518 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00519 if ( $part1 == "$path/" && $part2 != '@' ) { 00520 return $candidatePath; 00521 } 00522 } 00523 00524 // Try for an interstitial element 00525 foreach ( $this->pathInfo as $candidatePath => $info ) { 00526 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00527 if ( $part1 == "$path/" ) { 00528 return $candidatePath; 00529 } 00530 } 00531 return false; 00532 } 00533 00539 function getIndent( $pos, $key = false, $arrowPos = false ) { 00540 $arrowIndent = ' '; 00541 if ( $pos == 0 || $this->text[$pos-1] == "\n" ) { 00542 $indentLength = strspn( $this->text, " \t", $pos ); 00543 $indent = substr( $this->text, $pos, $indentLength ); 00544 } else { 00545 $indent = false; 00546 } 00547 if ( $indent !== false && $arrowPos !== false ) { 00548 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); 00549 if ( $arrowIndentLength > 0 ) { 00550 $arrowIndent = str_repeat( ' ', $arrowIndentLength ); 00551 } 00552 } 00553 return array( $indent, $arrowIndent ); 00554 } 00555 00560 public function parse() { 00561 $this->initParse(); 00562 $this->pushState( 'file' ); 00563 $this->pushPath( '@extra-' . ($this->serial++) ); 00564 $token = $this->firstToken(); 00565 00566 while ( !$token->isEnd() ) { 00567 $state = $this->popState(); 00568 if ( !$state ) { 00569 $this->error( 'internal error: empty state stack' ); 00570 } 00571 00572 switch ( $state ) { 00573 case 'file': 00574 $this->expect( T_OPEN_TAG ); 00575 $token = $this->skipSpace(); 00576 if ( $token->isEnd() ) { 00577 break 2; 00578 } 00579 $this->pushState( 'statement', 'file 2' ); 00580 break; 00581 case 'file 2': 00582 $token = $this->skipSpace(); 00583 if ( $token->isEnd() ) { 00584 break 2; 00585 } 00586 $this->pushState( 'statement', 'file 2' ); 00587 break; 00588 case 'statement': 00589 $token = $this->skipSpace(); 00590 if ( !$this->validatePath( $token->text ) ) { 00591 $this->error( "Invalid variable name \"{$token->text}\"" ); 00592 } 00593 $this->nextPath( $token->text ); 00594 $this->expect( T_VARIABLE ); 00595 $this->skipSpace(); 00596 $arrayAssign = false; 00597 if ( $this->currentToken()->type == '[' ) { 00598 $this->nextToken(); 00599 $token = $this->skipSpace(); 00600 if ( !$token->isScalar() ) { 00601 $this->error( "expected a string or number for the array key" ); 00602 } 00603 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00604 $text = $this->parseScalar( $token->text ); 00605 } else { 00606 $text = $token->text; 00607 } 00608 if ( !$this->validatePath( $text ) ) { 00609 $this->error( "Invalid associative array name \"$text\"" ); 00610 } 00611 $this->pushPath( $text ); 00612 $this->nextToken(); 00613 $this->skipSpace(); 00614 $this->expect( ']' ); 00615 $this->skipSpace(); 00616 $arrayAssign = true; 00617 } 00618 $this->expect( '=' ); 00619 $this->skipSpace(); 00620 $this->startPathValue(); 00621 if ( $arrayAssign ) 00622 $this->pushState( 'expression', 'array assign end' ); 00623 else 00624 $this->pushState( 'expression', 'statement end' ); 00625 break; 00626 case 'array assign end': 00627 case 'statement end': 00628 $this->endPathValue(); 00629 if ( $state == 'array assign end' ) 00630 $this->popPath(); 00631 $this->skipSpace(); 00632 $this->expect( ';' ); 00633 $this->nextPath( '@extra-' . ($this->serial++) ); 00634 break; 00635 case 'expression': 00636 $token = $this->skipSpace(); 00637 if ( $token->type == T_ARRAY ) { 00638 $this->pushState( 'array' ); 00639 } elseif ( $token->isScalar() ) { 00640 $this->nextToken(); 00641 } elseif ( $token->type == T_VARIABLE ) { 00642 $this->nextToken(); 00643 } else { 00644 $this->error( "expected simple expression" ); 00645 } 00646 break; 00647 case 'array': 00648 $this->skipSpace(); 00649 $this->expect( T_ARRAY ); 00650 $this->skipSpace(); 00651 $this->expect( '(' ); 00652 $this->skipSpace(); 00653 $this->pushPath( '@extra-' . ($this->serial++) ); 00654 if ( $this->isAhead( ')' ) ) { 00655 // Empty array 00656 $this->pushState( 'array end' ); 00657 } else { 00658 $this->pushState( 'element', 'array end' ); 00659 } 00660 break; 00661 case 'array end': 00662 $this->skipSpace(); 00663 $this->popPath(); 00664 $this->expect( ')' ); 00665 break; 00666 case 'element': 00667 $token = $this->skipSpace(); 00668 // Look ahead to find the double arrow 00669 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { 00670 // Found associative element 00671 $this->pushState( 'assoc-element', 'element end' ); 00672 } else { 00673 // Not associative 00674 $this->nextPath( '@next' ); 00675 $this->startPathValue(); 00676 $this->pushState( 'expression', 'element end' ); 00677 } 00678 break; 00679 case 'element end': 00680 $token = $this->skipSpace(); 00681 if ( $token->type == ',' ) { 00682 $this->endPathValue(); 00683 $this->markComma(); 00684 $this->nextToken(); 00685 $this->nextPath( '@extra-' . ($this->serial++) ); 00686 // Look ahead to find ending bracket 00687 if ( $this->isAhead( ")" ) ) { 00688 // Found ending bracket, no continuation 00689 $this->skipSpace(); 00690 } else { 00691 // No ending bracket, continue to next element 00692 $this->pushState( 'element' ); 00693 } 00694 } elseif ( $token->type == ')' ) { 00695 // End array 00696 $this->endPathValue(); 00697 } else { 00698 $this->error( "expected the next array element or the end of the array" ); 00699 } 00700 break; 00701 case 'assoc-element': 00702 $token = $this->skipSpace(); 00703 if ( !$token->isScalar() ) { 00704 $this->error( "expected a string or number for the array key" ); 00705 } 00706 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00707 $text = $this->parseScalar( $token->text ); 00708 } else { 00709 $text = $token->text; 00710 } 00711 if ( !$this->validatePath( $text ) ) { 00712 $this->error( "Invalid associative array name \"$text\"" ); 00713 } 00714 $this->nextPath( $text ); 00715 $this->nextToken(); 00716 $this->skipSpace(); 00717 $this->markArrow(); 00718 $this->expect( T_DOUBLE_ARROW ); 00719 $this->skipSpace(); 00720 $this->startPathValue(); 00721 $this->pushState( 'expression' ); 00722 break; 00723 } 00724 } 00725 if ( count( $this->stateStack ) ) { 00726 $this->error( 'unexpected end of file' ); 00727 } 00728 $this->popPath(); 00729 } 00730 00734 protected function initParse() { 00735 $this->tokens = token_get_all( $this->text ); 00736 $this->stateStack = array(); 00737 $this->pathStack = array(); 00738 $this->firstToken(); 00739 $this->pathInfo = array(); 00740 $this->serial = 1; 00741 } 00742 00747 protected function setPos( $pos ) { 00748 $this->pos = $pos; 00749 if ( $this->pos >= count( $this->tokens ) ) { 00750 $this->currentToken = ConfEditorToken::newEnd(); 00751 } else { 00752 $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); 00753 } 00754 return $this->currentToken; 00755 } 00756 00761 function newTokenObj( $internalToken ) { 00762 if ( is_array( $internalToken ) ) { 00763 return new ConfEditorToken( $internalToken[0], $internalToken[1] ); 00764 } else { 00765 return new ConfEditorToken( $internalToken, $internalToken ); 00766 } 00767 } 00768 00772 function firstToken() { 00773 $this->setPos( 0 ); 00774 $this->prevToken = ConfEditorToken::newEnd(); 00775 $this->lineNum = 1; 00776 $this->colNum = 1; 00777 $this->byteNum = 0; 00778 return $this->currentToken; 00779 } 00780 00784 function currentToken() { 00785 return $this->currentToken; 00786 } 00787 00791 function nextToken() { 00792 if ( $this->currentToken ) { 00793 $text = $this->currentToken->text; 00794 $lfCount = substr_count( $text, "\n" ); 00795 if ( $lfCount ) { 00796 $this->lineNum += $lfCount; 00797 $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); 00798 } else { 00799 $this->colNum += strlen( $text ); 00800 } 00801 $this->byteNum += strlen( $text ); 00802 } 00803 $this->prevToken = $this->currentToken; 00804 $this->setPos( $this->pos + 1 ); 00805 return $this->currentToken; 00806 } 00807 00813 function getTokenAhead( $offset ) { 00814 $pos = $this->pos + $offset; 00815 if ( $pos >= count( $this->tokens ) || $pos < 0 ) { 00816 return ConfEditorToken::newEnd(); 00817 } else { 00818 return $this->newTokenObj( $this->tokens[$pos] ); 00819 } 00820 } 00821 00825 function skipSpace() { 00826 while ( $this->currentToken && $this->currentToken->isSkip() ) { 00827 $this->nextToken(); 00828 } 00829 return $this->currentToken; 00830 } 00831 00836 function expect( $type ) { 00837 if ( $this->currentToken && $this->currentToken->type == $type ) { 00838 return $this->nextToken(); 00839 } else { 00840 $this->error( "expected " . $this->getTypeName( $type ) . 00841 ", got " . $this->getTypeName( $this->currentToken->type ) ); 00842 } 00843 } 00844 00848 function pushState( $nextState, $stateAfterThat = null ) { 00849 if ( $stateAfterThat !== null ) { 00850 $this->stateStack[] = $stateAfterThat; 00851 } 00852 $this->stateStack[] = $nextState; 00853 } 00854 00859 function popState() { 00860 return array_pop( $this->stateStack ); 00861 } 00862 00868 function validatePath( $path ) { 00869 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; 00870 } 00871 00876 function endPath() { 00877 $key = ''; 00878 foreach ( $this->pathStack as $pathInfo ) { 00879 if ( $key !== '' ) { 00880 $key .= '/'; 00881 } 00882 $key .= $pathInfo['name']; 00883 } 00884 $pathInfo['endByte'] = $this->byteNum; 00885 $pathInfo['endToken'] = $this->pos; 00886 $this->pathInfo[$key] = $pathInfo; 00887 } 00888 00892 function pushPath( $path ) { 00893 $this->pathStack[] = array( 00894 'name' => $path, 00895 'level' => count( $this->pathStack ) + 1, 00896 'startByte' => $this->byteNum, 00897 'startToken' => $this->pos, 00898 'valueStartToken' => false, 00899 'valueStartByte' => false, 00900 'valueEndToken' => false, 00901 'valueEndByte' => false, 00902 'nextArrayIndex' => 0, 00903 'hasComma' => false, 00904 'arrowByte' => false 00905 ); 00906 } 00907 00911 function popPath() { 00912 $this->endPath(); 00913 array_pop( $this->pathStack ); 00914 } 00915 00921 function nextPath( $path ) { 00922 $this->endPath(); 00923 $i = count( $this->pathStack ) - 1; 00924 if ( $path == '@next' ) { 00925 $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; 00926 $this->pathStack[$i]['name'] = $nextArrayIndex; 00927 $nextArrayIndex++; 00928 } else { 00929 $this->pathStack[$i]['name'] = $path; 00930 } 00931 $this->pathStack[$i] = 00932 array( 00933 'startByte' => $this->byteNum, 00934 'startToken' => $this->pos, 00935 'valueStartToken' => false, 00936 'valueStartByte' => false, 00937 'valueEndToken' => false, 00938 'valueEndByte' => false, 00939 'hasComma' => false, 00940 'arrowByte' => false, 00941 ) + $this->pathStack[$i]; 00942 } 00943 00947 function startPathValue() { 00948 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00949 $path['valueStartToken'] = $this->pos; 00950 $path['valueStartByte'] = $this->byteNum; 00951 } 00952 00956 function endPathValue() { 00957 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00958 $path['valueEndToken'] = $this->pos; 00959 $path['valueEndByte'] = $this->byteNum; 00960 } 00961 00965 function markComma() { 00966 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00967 $path['hasComma'] = true; 00968 } 00969 00973 function markArrow() { 00974 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00975 $path['arrowByte'] = $this->byteNum; 00976 } 00977 00981 function error( $msg ) { 00982 throw new ConfEditorParseError( $this, $msg ); 00983 } 00984 00989 function getTypeName( $type ) { 00990 if ( is_int( $type ) ) { 00991 return token_name( $type ); 00992 } else { 00993 return "\"$type\""; 00994 } 00995 } 00996 01003 function isAhead( $type, $offset = 0 ) { 01004 $ahead = $offset; 01005 $token = $this->getTokenAhead( $offset ); 01006 while ( !$token->isEnd() ) { 01007 if ( $token->isSkip() ) { 01008 $ahead++; 01009 $token = $this->getTokenAhead( $ahead ); 01010 continue; 01011 } elseif ( $token->type == $type ) { 01012 // Found the type 01013 return true; 01014 } else { 01015 // Not found 01016 return false; 01017 } 01018 } 01019 return false; 01020 } 01021 01025 function prevToken() { 01026 return $this->prevToken; 01027 } 01028 01032 function dumpTokens() { 01033 $out = ''; 01034 foreach ( $this->tokens as $token ) { 01035 $obj = $this->newTokenObj( $token ); 01036 $out .= sprintf( "%-28s %s\n", 01037 $this->getTypeName( $obj->type ), 01038 addcslashes( $obj->text, "\0..\37" ) ); 01039 } 01040 echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; 01041 } 01042 } 01043 01047 class ConfEditorParseError extends MWException { 01048 var $lineNum, $colNum; 01049 function __construct( $editor, $msg ) { 01050 $this->lineNum = $editor->lineNum; 01051 $this->colNum = $editor->colNum; 01052 parent::__construct( "Parse error on line {$editor->lineNum} " . 01053 "col {$editor->colNum}: $msg" ); 01054 } 01055 01056 function highlight( $text ) { 01057 $lines = StringUtils::explode( "\n", $text ); 01058 foreach ( $lines as $lineNum => $line ) { 01059 if ( $lineNum == $this->lineNum - 1 ) { 01060 return "$line\n" .str_repeat( ' ', $this->colNum - 1 ) . "^\n"; 01061 } 01062 } 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 } 01097