MediaWiki
REL1_23
|
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 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 00273 return $out; 00274 } 00275 00280 function getVars() { 00281 $vars = array(); 00282 $this->parse(); 00283 foreach ( $this->pathInfo as $path => $data ) { 00284 if ( $path[0] != '$' ) { 00285 continue; 00286 } 00287 $trimmedPath = substr( $path, 1 ); 00288 $name = $data['name']; 00289 if ( $name[0] == '@' ) { 00290 continue; 00291 } 00292 if ( $name[0] == '$' ) { 00293 $name = substr( $name, 1 ); 00294 } 00295 $parentPath = substr( $trimmedPath, 0, 00296 strlen( $trimmedPath ) - strlen( $name ) ); 00297 if ( substr( $parentPath, -1 ) == '/' ) { 00298 $parentPath = substr( $parentPath, 0, -1 ); 00299 } 00300 00301 $value = substr( $this->text, $data['valueStartByte'], 00302 $data['valueEndByte'] - $data['valueStartByte'] 00303 ); 00304 $this->setVar( $vars, $parentPath, $name, 00305 $this->parseScalar( $value ) ); 00306 } 00307 00308 return $vars; 00309 } 00310 00320 function setVar( &$array, $path, $key, $value ) { 00321 $pathArr = explode( '/', $path ); 00322 $target =& $array; 00323 if ( $path !== '' ) { 00324 foreach ( $pathArr as $p ) { 00325 if ( !isset( $target[$p] ) ) { 00326 $target[$p] = array(); 00327 } 00328 $target =& $target[$p]; 00329 } 00330 } 00331 if ( !isset( $target[$key] ) ) { 00332 $target[$key] = $value; 00333 } 00334 } 00335 00340 function parseScalar( $str ) { 00341 if ( $str !== '' && $str[0] == '\'' ) { 00342 // Single-quoted string 00343 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00344 // appended to the token; without it we ended up reading in the 00345 // extra quote on the end! 00346 return strtr( substr( trim( $str ), 1, -1 ), 00347 array( '\\\'' => '\'', '\\\\' => '\\' ) ); 00348 } 00349 if ( $str !== '' && $str[0] == '"' ) { 00350 // Double-quoted string 00351 // @todo FIXME: trim() call is due to mystery bug where whitespace gets 00352 // appended to the token; without it we ended up reading in the 00353 // extra quote on the end! 00354 return stripcslashes( substr( trim( $str ), 1, -1 ) ); 00355 } 00356 if ( substr( $str, 0, 4 ) == 'true' ) { 00357 return true; 00358 } 00359 if ( substr( $str, 0, 5 ) == 'false' ) { 00360 return false; 00361 } 00362 if ( substr( $str, 0, 4 ) == 'null' ) { 00363 return null; 00364 } 00365 00366 // Must be some kind of numeric value, so let PHP's weak typing 00367 // be useful for a change 00368 return $str; 00369 } 00370 00375 function replaceSourceRegion( $start, $end, $newText = false ) { 00376 // Split all copy operations with a source corresponding to the region 00377 // in question. 00378 $newEdits = array(); 00379 foreach ( $this->edits as $edit ) { 00380 if ( $edit[0] !== 'copy' ) { 00381 $newEdits[] = $edit; 00382 continue; 00383 } 00384 $copyStart = $edit[1]; 00385 $copyEnd = $edit[2]; 00386 if ( $start >= $copyEnd || $end <= $copyStart ) { 00387 // Outside this region 00388 $newEdits[] = $edit; 00389 continue; 00390 } 00391 if ( ( $start < $copyStart && $end > $copyStart ) 00392 || ( $start < $copyEnd && $end > $copyEnd ) 00393 ) { 00394 throw new MWException( "Overlapping regions found, can't do the edit" ); 00395 } 00396 // Split the copy 00397 $newEdits[] = array( 'copy', $copyStart, $start ); 00398 if ( $newText !== false ) { 00399 $newEdits[] = array( 'insert', $newText ); 00400 } 00401 $newEdits[] = array( 'copy', $end, $copyEnd ); 00402 } 00403 $this->edits = $newEdits; 00404 } 00405 00414 function findDeletionRegion( $pathName ) { 00415 if ( !isset( $this->pathInfo[$pathName] ) ) { 00416 throw new MWException( "Can't find path \"$pathName\"" ); 00417 } 00418 $path = $this->pathInfo[$pathName]; 00419 // Find the start 00420 $this->firstToken(); 00421 while ( $this->pos != $path['startToken'] ) { 00422 $this->nextToken(); 00423 } 00424 $regionStart = $path['startByte']; 00425 for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { 00426 $token = $this->getTokenAhead( $offset ); 00427 if ( !$token->isSkip() ) { 00428 // If there is other content on the same line, don't move the start point 00429 // back, because that will cause the regions to overlap. 00430 $regionStart = $path['startByte']; 00431 break; 00432 } 00433 $lfPos = strrpos( $token->text, "\n" ); 00434 if ( $lfPos === false ) { 00435 $regionStart -= strlen( $token->text ); 00436 } else { 00437 // The line start does not include the LF 00438 $regionStart -= strlen( $token->text ) - $lfPos - 1; 00439 break; 00440 } 00441 } 00442 // Find the end 00443 while ( $this->pos != $path['endToken'] ) { 00444 $this->nextToken(); 00445 } 00446 $regionEnd = $path['endByte']; // past the end 00447 $count = count( $this->tokens ); 00448 for ( $offset = 0; $offset < $count - $this->pos; $offset++ ) { 00449 $token = $this->getTokenAhead( $offset ); 00450 if ( !$token->isSkip() ) { 00451 break; 00452 } 00453 $lfPos = strpos( $token->text, "\n" ); 00454 if ( $lfPos === false ) { 00455 $regionEnd += strlen( $token->text ); 00456 } else { 00457 // This should point past the LF 00458 $regionEnd += $lfPos + 1; 00459 break; 00460 } 00461 } 00462 00463 return array( $regionStart, $regionEnd ); 00464 } 00465 00476 function findValueRegion( $pathName ) { 00477 if ( !isset( $this->pathInfo[$pathName] ) ) { 00478 throw new MWException( "Can't find path \"$pathName\"" ); 00479 } 00480 $path = $this->pathInfo[$pathName]; 00481 if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { 00482 throw new MWException( "Can't find value region for path \"$pathName\"" ); 00483 } 00484 00485 return array( $path['valueStartByte'], $path['valueEndByte'] ); 00486 } 00487 00494 function findLastArrayElement( $path ) { 00495 // Try for a real element 00496 $lastEltPath = false; 00497 foreach ( $this->pathInfo as $candidatePath => $info ) { 00498 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00499 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00500 if ( $part2 == '@' ) { 00501 // Do nothing 00502 } elseif ( $part1 == "$path/" ) { 00503 $lastEltPath = $candidatePath; 00504 } elseif ( $lastEltPath !== false ) { 00505 break; 00506 } 00507 } 00508 if ( $lastEltPath !== false ) { 00509 return $lastEltPath; 00510 } 00511 00512 // Try for an interstitial element 00513 $extraPath = false; 00514 foreach ( $this->pathInfo as $candidatePath => $info ) { 00515 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00516 if ( $part1 == "$path/" ) { 00517 $extraPath = $candidatePath; 00518 } elseif ( $extraPath !== false ) { 00519 break; 00520 } 00521 } 00522 00523 return $extraPath; 00524 } 00525 00532 function findFirstArrayElement( $path ) { 00533 // Try for an ordinary element 00534 foreach ( $this->pathInfo as $candidatePath => $info ) { 00535 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00536 $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); 00537 if ( $part1 == "$path/" && $part2 != '@' ) { 00538 return $candidatePath; 00539 } 00540 } 00541 00542 // Try for an interstitial element 00543 foreach ( $this->pathInfo as $candidatePath => $info ) { 00544 $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); 00545 if ( $part1 == "$path/" ) { 00546 return $candidatePath; 00547 } 00548 } 00549 00550 return false; 00551 } 00552 00558 function getIndent( $pos, $key = false, $arrowPos = false ) { 00559 $arrowIndent = ' '; 00560 if ( $pos == 0 || $this->text[$pos - 1] == "\n" ) { 00561 $indentLength = strspn( $this->text, " \t", $pos ); 00562 $indent = substr( $this->text, $pos, $indentLength ); 00563 } else { 00564 $indent = false; 00565 } 00566 if ( $indent !== false && $arrowPos !== false ) { 00567 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); 00568 if ( $arrowIndentLength > 0 ) { 00569 $arrowIndent = str_repeat( ' ', $arrowIndentLength ); 00570 } 00571 } 00572 00573 return array( $indent, $arrowIndent ); 00574 } 00575 00580 public function parse() { 00581 $this->initParse(); 00582 $this->pushState( 'file' ); 00583 $this->pushPath( '@extra-' . ( $this->serial++ ) ); 00584 $token = $this->firstToken(); 00585 00586 while ( !$token->isEnd() ) { 00587 $state = $this->popState(); 00588 if ( !$state ) { 00589 $this->error( 'internal error: empty state stack' ); 00590 } 00591 00592 switch ( $state ) { 00593 case 'file': 00594 $this->expect( T_OPEN_TAG ); 00595 $token = $this->skipSpace(); 00596 if ( $token->isEnd() ) { 00597 break 2; 00598 } 00599 $this->pushState( 'statement', 'file 2' ); 00600 break; 00601 case 'file 2': 00602 $token = $this->skipSpace(); 00603 if ( $token->isEnd() ) { 00604 break 2; 00605 } 00606 $this->pushState( 'statement', 'file 2' ); 00607 break; 00608 case 'statement': 00609 $token = $this->skipSpace(); 00610 if ( !$this->validatePath( $token->text ) ) { 00611 $this->error( "Invalid variable name \"{$token->text}\"" ); 00612 } 00613 $this->nextPath( $token->text ); 00614 $this->expect( T_VARIABLE ); 00615 $this->skipSpace(); 00616 $arrayAssign = false; 00617 if ( $this->currentToken()->type == '[' ) { 00618 $this->nextToken(); 00619 $token = $this->skipSpace(); 00620 if ( !$token->isScalar() ) { 00621 $this->error( "expected a string or number for the array key" ); 00622 } 00623 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00624 $text = $this->parseScalar( $token->text ); 00625 } else { 00626 $text = $token->text; 00627 } 00628 if ( !$this->validatePath( $text ) ) { 00629 $this->error( "Invalid associative array name \"$text\"" ); 00630 } 00631 $this->pushPath( $text ); 00632 $this->nextToken(); 00633 $this->skipSpace(); 00634 $this->expect( ']' ); 00635 $this->skipSpace(); 00636 $arrayAssign = true; 00637 } 00638 $this->expect( '=' ); 00639 $this->skipSpace(); 00640 $this->startPathValue(); 00641 if ( $arrayAssign ) { 00642 $this->pushState( 'expression', 'array assign end' ); 00643 } else { 00644 $this->pushState( 'expression', 'statement end' ); 00645 } 00646 break; 00647 case 'array assign end': 00648 case 'statement end': 00649 $this->endPathValue(); 00650 if ( $state == 'array assign end' ) { 00651 $this->popPath(); 00652 } 00653 $this->skipSpace(); 00654 $this->expect( ';' ); 00655 $this->nextPath( '@extra-' . ( $this->serial++ ) ); 00656 break; 00657 case 'expression': 00658 $token = $this->skipSpace(); 00659 if ( $token->type == T_ARRAY ) { 00660 $this->pushState( 'array' ); 00661 } elseif ( $token->isScalar() ) { 00662 $this->nextToken(); 00663 } elseif ( $token->type == T_VARIABLE ) { 00664 $this->nextToken(); 00665 } else { 00666 $this->error( "expected simple expression" ); 00667 } 00668 break; 00669 case 'array': 00670 $this->skipSpace(); 00671 $this->expect( T_ARRAY ); 00672 $this->skipSpace(); 00673 $this->expect( '(' ); 00674 $this->skipSpace(); 00675 $this->pushPath( '@extra-' . ( $this->serial++ ) ); 00676 if ( $this->isAhead( ')' ) ) { 00677 // Empty array 00678 $this->pushState( 'array end' ); 00679 } else { 00680 $this->pushState( 'element', 'array end' ); 00681 } 00682 break; 00683 case 'array end': 00684 $this->skipSpace(); 00685 $this->popPath(); 00686 $this->expect( ')' ); 00687 break; 00688 case 'element': 00689 $token = $this->skipSpace(); 00690 // Look ahead to find the double arrow 00691 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { 00692 // Found associative element 00693 $this->pushState( 'assoc-element', 'element end' ); 00694 } else { 00695 // Not associative 00696 $this->nextPath( '@next' ); 00697 $this->startPathValue(); 00698 $this->pushState( 'expression', 'element end' ); 00699 } 00700 break; 00701 case 'element end': 00702 $token = $this->skipSpace(); 00703 if ( $token->type == ',' ) { 00704 $this->endPathValue(); 00705 $this->markComma(); 00706 $this->nextToken(); 00707 $this->nextPath( '@extra-' . ( $this->serial++ ) ); 00708 // Look ahead to find ending bracket 00709 if ( $this->isAhead( ")" ) ) { 00710 // Found ending bracket, no continuation 00711 $this->skipSpace(); 00712 } else { 00713 // No ending bracket, continue to next element 00714 $this->pushState( 'element' ); 00715 } 00716 } elseif ( $token->type == ')' ) { 00717 // End array 00718 $this->endPathValue(); 00719 } else { 00720 $this->error( "expected the next array element or the end of the array" ); 00721 } 00722 break; 00723 case 'assoc-element': 00724 $token = $this->skipSpace(); 00725 if ( !$token->isScalar() ) { 00726 $this->error( "expected a string or number for the array key" ); 00727 } 00728 if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { 00729 $text = $this->parseScalar( $token->text ); 00730 } else { 00731 $text = $token->text; 00732 } 00733 if ( !$this->validatePath( $text ) ) { 00734 $this->error( "Invalid associative array name \"$text\"" ); 00735 } 00736 $this->nextPath( $text ); 00737 $this->nextToken(); 00738 $this->skipSpace(); 00739 $this->markArrow(); 00740 $this->expect( T_DOUBLE_ARROW ); 00741 $this->skipSpace(); 00742 $this->startPathValue(); 00743 $this->pushState( 'expression' ); 00744 break; 00745 } 00746 } 00747 if ( count( $this->stateStack ) ) { 00748 $this->error( 'unexpected end of file' ); 00749 } 00750 $this->popPath(); 00751 } 00752 00756 protected function initParse() { 00757 $this->tokens = token_get_all( $this->text ); 00758 $this->stateStack = array(); 00759 $this->pathStack = array(); 00760 $this->firstToken(); 00761 $this->pathInfo = array(); 00762 $this->serial = 1; 00763 } 00764 00769 protected function setPos( $pos ) { 00770 $this->pos = $pos; 00771 if ( $this->pos >= count( $this->tokens ) ) { 00772 $this->currentToken = ConfEditorToken::newEnd(); 00773 } else { 00774 $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); 00775 } 00776 00777 return $this->currentToken; 00778 } 00779 00784 function newTokenObj( $internalToken ) { 00785 if ( is_array( $internalToken ) ) { 00786 return new ConfEditorToken( $internalToken[0], $internalToken[1] ); 00787 } else { 00788 return new ConfEditorToken( $internalToken, $internalToken ); 00789 } 00790 } 00791 00795 function firstToken() { 00796 $this->setPos( 0 ); 00797 $this->prevToken = ConfEditorToken::newEnd(); 00798 $this->lineNum = 1; 00799 $this->colNum = 1; 00800 $this->byteNum = 0; 00801 00802 return $this->currentToken; 00803 } 00804 00808 function currentToken() { 00809 return $this->currentToken; 00810 } 00811 00815 function nextToken() { 00816 if ( $this->currentToken ) { 00817 $text = $this->currentToken->text; 00818 $lfCount = substr_count( $text, "\n" ); 00819 if ( $lfCount ) { 00820 $this->lineNum += $lfCount; 00821 $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); 00822 } else { 00823 $this->colNum += strlen( $text ); 00824 } 00825 $this->byteNum += strlen( $text ); 00826 } 00827 $this->prevToken = $this->currentToken; 00828 $this->setPos( $this->pos + 1 ); 00829 00830 return $this->currentToken; 00831 } 00832 00838 function getTokenAhead( $offset ) { 00839 $pos = $this->pos + $offset; 00840 if ( $pos >= count( $this->tokens ) || $pos < 0 ) { 00841 return ConfEditorToken::newEnd(); 00842 } else { 00843 return $this->newTokenObj( $this->tokens[$pos] ); 00844 } 00845 } 00846 00850 function skipSpace() { 00851 while ( $this->currentToken && $this->currentToken->isSkip() ) { 00852 $this->nextToken(); 00853 } 00854 00855 return $this->currentToken; 00856 } 00857 00862 function expect( $type ) { 00863 if ( $this->currentToken && $this->currentToken->type == $type ) { 00864 return $this->nextToken(); 00865 } else { 00866 $this->error( "expected " . $this->getTypeName( $type ) . 00867 ", got " . $this->getTypeName( $this->currentToken->type ) ); 00868 } 00869 } 00870 00874 function pushState( $nextState, $stateAfterThat = null ) { 00875 if ( $stateAfterThat !== null ) { 00876 $this->stateStack[] = $stateAfterThat; 00877 } 00878 $this->stateStack[] = $nextState; 00879 } 00880 00885 function popState() { 00886 return array_pop( $this->stateStack ); 00887 } 00888 00894 function validatePath( $path ) { 00895 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; 00896 } 00897 00902 function endPath() { 00903 $key = ''; 00904 foreach ( $this->pathStack as $pathInfo ) { 00905 if ( $key !== '' ) { 00906 $key .= '/'; 00907 } 00908 $key .= $pathInfo['name']; 00909 } 00910 $pathInfo['endByte'] = $this->byteNum; 00911 $pathInfo['endToken'] = $this->pos; 00912 $this->pathInfo[$key] = $pathInfo; 00913 } 00914 00918 function pushPath( $path ) { 00919 $this->pathStack[] = array( 00920 'name' => $path, 00921 'level' => count( $this->pathStack ) + 1, 00922 'startByte' => $this->byteNum, 00923 'startToken' => $this->pos, 00924 'valueStartToken' => false, 00925 'valueStartByte' => false, 00926 'valueEndToken' => false, 00927 'valueEndByte' => false, 00928 'nextArrayIndex' => 0, 00929 'hasComma' => false, 00930 'arrowByte' => false 00931 ); 00932 } 00933 00937 function popPath() { 00938 $this->endPath(); 00939 array_pop( $this->pathStack ); 00940 } 00941 00947 function nextPath( $path ) { 00948 $this->endPath(); 00949 $i = count( $this->pathStack ) - 1; 00950 if ( $path == '@next' ) { 00951 $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; 00952 $this->pathStack[$i]['name'] = $nextArrayIndex; 00953 $nextArrayIndex++; 00954 } else { 00955 $this->pathStack[$i]['name'] = $path; 00956 } 00957 $this->pathStack[$i] = 00958 array( 00959 'startByte' => $this->byteNum, 00960 'startToken' => $this->pos, 00961 'valueStartToken' => false, 00962 'valueStartByte' => false, 00963 'valueEndToken' => false, 00964 'valueEndByte' => false, 00965 'hasComma' => false, 00966 'arrowByte' => false, 00967 ) + $this->pathStack[$i]; 00968 } 00969 00973 function startPathValue() { 00974 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00975 $path['valueStartToken'] = $this->pos; 00976 $path['valueStartByte'] = $this->byteNum; 00977 } 00978 00982 function endPathValue() { 00983 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00984 $path['valueEndToken'] = $this->pos; 00985 $path['valueEndByte'] = $this->byteNum; 00986 } 00987 00991 function markComma() { 00992 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 00993 $path['hasComma'] = true; 00994 } 00995 00999 function markArrow() { 01000 $path =& $this->pathStack[count( $this->pathStack ) - 1]; 01001 $path['arrowByte'] = $this->byteNum; 01002 } 01003 01007 function error( $msg ) { 01008 throw new ConfEditorParseError( $this, $msg ); 01009 } 01010 01015 function getTypeName( $type ) { 01016 if ( is_int( $type ) ) { 01017 return token_name( $type ); 01018 } else { 01019 return "\"$type\""; 01020 } 01021 } 01022 01029 function isAhead( $type, $offset = 0 ) { 01030 $ahead = $offset; 01031 $token = $this->getTokenAhead( $offset ); 01032 while ( !$token->isEnd() ) { 01033 if ( $token->isSkip() ) { 01034 $ahead++; 01035 $token = $this->getTokenAhead( $ahead ); 01036 continue; 01037 } elseif ( $token->type == $type ) { 01038 // Found the type 01039 return true; 01040 } else { 01041 // Not found 01042 return false; 01043 } 01044 } 01045 01046 return false; 01047 } 01048 01052 function prevToken() { 01053 return $this->prevToken; 01054 } 01055 01059 function dumpTokens() { 01060 $out = ''; 01061 foreach ( $this->tokens as $token ) { 01062 $obj = $this->newTokenObj( $token ); 01063 $out .= sprintf( "%-28s %s\n", 01064 $this->getTypeName( $obj->type ), 01065 addcslashes( $obj->text, "\0..\37" ) ); 01066 } 01067 echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; 01068 } 01069 } 01070 01074 class ConfEditorParseError extends MWException { 01075 var $lineNum, $colNum; 01076 01077 function __construct( $editor, $msg ) { 01078 $this->lineNum = $editor->lineNum; 01079 $this->colNum = $editor->colNum; 01080 parent::__construct( "Parse error on line {$editor->lineNum} " . 01081 "col {$editor->colNum}: $msg" ); 01082 } 01083 01084 function highlight( $text ) { 01085 $lines = StringUtils::explode( "\n", $text ); 01086 foreach ( $lines as $lineNum => $line ) { 01087 if ( $lineNum == $this->lineNum - 1 ) { 01088 return "$line\n" . str_repeat( ' ', $this->colNum - 1 ) . "^\n"; 01089 } 01090 } 01091 01092 return ''; 01093 } 01094 } 01095 01099 class ConfEditorToken { 01100 var $type, $text; 01101 01102 static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); 01103 static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); 01104 01105 static function newEnd() { 01106 return new self( 'END', '' ); 01107 } 01108 01109 function __construct( $type, $text ) { 01110 $this->type = $type; 01111 $this->text = $text; 01112 } 01113 01114 function isSkip() { 01115 return in_array( $this->type, self::$skipTypes ); 01116 } 01117 01118 function isScalar() { 01119 return in_array( $this->type, self::$scalarTypes ); 01120 } 01121 01122 function isEnd() { 01123 return $this->type == 'END'; 01124 } 01125 }