diff options
Diffstat (limited to 'includes/ConfEditor.php')
-rw-r--r-- | includes/ConfEditor.php | 1109 |
1 files changed, 0 insertions, 1109 deletions
diff --git a/includes/ConfEditor.php b/includes/ConfEditor.php deleted file mode 100644 index 67cb87db..00000000 --- a/includes/ConfEditor.php +++ /dev/null @@ -1,1109 +0,0 @@ -<?php -/** - * Configuration file editor. - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -/** - * This is a state machine style parser with two internal stacks: - * * A next state stack, which determines the state the machine will progress to next - * * A path stack, which keeps track of the logical location in the file. - * - * Reference grammar: - * - * file = T_OPEN_TAG *statement - * statement = T_VARIABLE "=" expression ";" - * expression = array / scalar / T_VARIABLE - * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")" - * element = assoc-element / expression - * assoc-element = scalar T_DOUBLE_ARROW expression - * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING - */ -class ConfEditor { - /** The text to parse */ - var $text; - - /** The token array from token_get_all() */ - var $tokens; - - /** The current position in the token array */ - var $pos; - - /** The current 1-based line number */ - var $lineNum; - - /** The current 1-based column number */ - var $colNum; - - /** The current 0-based byte number */ - var $byteNum; - - /** The current ConfEditorToken object */ - var $currentToken; - - /** The previous ConfEditorToken object */ - var $prevToken; - - /** - * The state machine stack. This is an array of strings where the topmost - * element will be popped off and become the next parser state. - */ - var $stateStack; - - /** - * The path stack is a stack of associative arrays with the following elements: - * name The name of top level of the path - * level The level (number of elements) of the path - * startByte The byte offset of the start of the path - * startToken The token offset of the start - * endByte The byte offset of thee - * endToken The token offset of the end, plus one - * valueStartToken The start token offset of the value part - * valueStartByte The start byte offset of the value part - * valueEndToken The end token offset of the value part, plus one - * valueEndByte The end byte offset of the value part, plus one - * nextArrayIndex The next numeric array index at this level - * hasComma True if the array element ends with a comma - * arrowByte The byte offset of the "=>", or false if there isn't one - */ - var $pathStack; - - /** - * The elements of the top of the pathStack for every path encountered, indexed - * by slash-separated path. - */ - var $pathInfo; - - /** - * Next serial number for whitespace placeholder paths (\@extra-N) - */ - var $serial; - - /** - * Editor state. This consists of the internal copy/insert operations which - * are applied to the source string to obtain the destination string. - */ - var $edits; - - /** - * Simple entry point for command-line testing - * - * @param $text string - * - * @return string - */ - static function test( $text ) { - try { - $ce = new self( $text ); - $ce->parse(); - } catch ( ConfEditorParseError $e ) { - return $e->getMessage() . "\n" . $e->highlight( $text ); - } - return "OK"; - } - - /** - * Construct a new parser - */ - public function __construct( $text ) { - $this->text = $text; - } - - /** - * Edit the text. Returns the edited text. - * @param array $ops of operations. - * - * Operations are given as an associative array, with members: - * type: One of delete, set, append or insert (required) - * path: The path to operate on (required) - * key: The array key to insert/append, with PHP quotes - * value: The value, with PHP quotes - * - * delete - * Deletes an array element or statement with the specified path. - * e.g. - * array('type' => 'delete', 'path' => '$foo/bar/baz' ) - * is equivalent to the runtime PHP code: - * unset( $foo['bar']['baz'] ); - * - * set - * Sets the value of an array element. If the element doesn't exist, it - * is appended to the array. If it does exist, the value is set, with - * comments and indenting preserved. - * - * append - * Appends a new element to the end of the array. Adds a trailing comma. - * e.g. - * array( 'type' => 'append', 'path', '$foo/bar', - * 'key' => 'baz', 'value' => "'x'" ) - * is like the PHP code: - * $foo['bar']['baz'] = 'x'; - * - * insert - * Insert a new element at the start of the array. - * - * @throws MWException - * @return string - */ - public function edit( $ops ) { - $this->parse(); - - $this->edits = array( - array( 'copy', 0, strlen( $this->text ) ) - ); - foreach ( $ops as $op ) { - $type = $op['type']; - $path = $op['path']; - $value = isset( $op['value'] ) ? $op['value'] : null; - $key = isset( $op['key'] ) ? $op['key'] : null; - - switch ( $type ) { - case 'delete': - list( $start, $end ) = $this->findDeletionRegion( $path ); - $this->replaceSourceRegion( $start, $end, false ); - break; - case 'set': - if ( isset( $this->pathInfo[$path] ) ) { - list( $start, $end ) = $this->findValueRegion( $path ); - $encValue = $value; // var_export( $value, true ); - $this->replaceSourceRegion( $start, $end, $encValue ); - break; - } - // No existing path, fall through to append - $slashPos = strrpos( $path, '/' ); - $key = var_export( substr( $path, $slashPos + 1 ), true ); - $path = substr( $path, 0, $slashPos ); - // Fall through - case 'append': - // Find the last array element - $lastEltPath = $this->findLastArrayElement( $path ); - if ( $lastEltPath === false ) { - throw new MWException( "Can't find any element of array \"$path\"" ); - } - $lastEltInfo = $this->pathInfo[$lastEltPath]; - - // Has it got a comma already? - if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) { - // No comma, insert one after the value region - list( , $end ) = $this->findValueRegion( $lastEltPath ); - $this->replaceSourceRegion( $end - 1, $end - 1, ',' ); - } - - // Make the text to insert - list( $start, $end ) = $this->findDeletionRegion( $lastEltPath ); - - if ( $key === null ) { - list( $indent, ) = $this->getIndent( $start ); - $textToInsert = "$indent$value,"; - } else { - list( $indent, $arrowIndent ) = - $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] ); - $textToInsert = "$indent$key$arrowIndent=> $value,"; - } - $textToInsert .= ( $indent === false ? ' ' : "\n" ); - - // Insert the item - $this->replaceSourceRegion( $end, $end, $textToInsert ); - break; - case 'insert': - // Find first array element - $firstEltPath = $this->findFirstArrayElement( $path ); - if ( $firstEltPath === false ) { - throw new MWException( "Can't find array element of \"$path\"" ); - } - list( $start, ) = $this->findDeletionRegion( $firstEltPath ); - $info = $this->pathInfo[$firstEltPath]; - - // Make the text to insert - if ( $key === null ) { - list( $indent, ) = $this->getIndent( $start ); - $textToInsert = "$indent$value,"; - } else { - list( $indent, $arrowIndent ) = - $this->getIndent( $start, $key, $info['arrowByte'] ); - $textToInsert = "$indent$key$arrowIndent=> $value,"; - } - $textToInsert .= ( $indent === false ? ' ' : "\n" ); - - // Insert the item - $this->replaceSourceRegion( $start, $start, $textToInsert ); - break; - default: - throw new MWException( "Unrecognised operation: \"$type\"" ); - } - } - - // Do the edits - $out = ''; - foreach ( $this->edits as $edit ) { - if ( $edit[0] == 'copy' ) { - $out .= substr( $this->text, $edit[1], $edit[2] - $edit[1] ); - } else { // if ( $edit[0] == 'insert' ) - $out .= $edit[1]; - } - } - - // Do a second parse as a sanity check - $this->text = $out; - try { - $this->parse(); - } catch ( ConfEditorParseError $e ) { - throw new MWException( - "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " . - $e->getMessage() ); - } - return $out; - } - - /** - * Get the variables defined in the text - * @return array( varname => value ) - */ - function getVars() { - $vars = array(); - $this->parse(); - foreach ( $this->pathInfo as $path => $data ) { - if ( $path[0] != '$' ) { - continue; - } - $trimmedPath = substr( $path, 1 ); - $name = $data['name']; - if ( $name[0] == '@' ) { - continue; - } - if ( $name[0] == '$' ) { - $name = substr( $name, 1 ); - } - $parentPath = substr( $trimmedPath, 0, - strlen( $trimmedPath ) - strlen( $name ) ); - if ( substr( $parentPath, -1 ) == '/' ) { - $parentPath = substr( $parentPath, 0, -1 ); - } - - $value = substr( $this->text, $data['valueStartByte'], - $data['valueEndByte'] - $data['valueStartByte'] - ); - $this->setVar( $vars, $parentPath, $name, - $this->parseScalar( $value ) ); - } - return $vars; - } - - /** - * Set a value in an array, unless it's set already. For instance, - * setVar( $arr, 'foo/bar', 'baz', 3 ); will set - * $arr['foo']['bar']['baz'] = 3; - * @param $array array - * @param string $path slash-delimited path - * @param $key mixed Key - * @param $value mixed Value - */ - function setVar( &$array, $path, $key, $value ) { - $pathArr = explode( '/', $path ); - $target =& $array; - if ( $path !== '' ) { - foreach ( $pathArr as $p ) { - if ( !isset( $target[$p] ) ) { - $target[$p] = array(); - } - $target =& $target[$p]; - } - } - if ( !isset( $target[$key] ) ) { - $target[$key] = $value; - } - } - - /** - * Parse a scalar value in PHP - * @return mixed Parsed value - */ - function parseScalar( $str ) { - if ( $str !== '' && $str[0] == '\'' ) { - // Single-quoted string - // @todo FIXME: trim() call is due to mystery bug where whitespace gets - // appended to the token; without it we ended up reading in the - // extra quote on the end! - return strtr( substr( trim( $str ), 1, -1 ), - array( '\\\'' => '\'', '\\\\' => '\\' ) ); - } - if ( $str !== '' && $str[0] == '"' ) { - // Double-quoted string - // @todo FIXME: trim() call is due to mystery bug where whitespace gets - // appended to the token; without it we ended up reading in the - // extra quote on the end! - return stripcslashes( substr( trim( $str ), 1, -1 ) ); - } - if ( substr( $str, 0, 4 ) == 'true' ) { - return true; - } - if ( substr( $str, 0, 5 ) == 'false' ) { - return false; - } - if ( substr( $str, 0, 4 ) == 'null' ) { - return null; - } - // Must be some kind of numeric value, so let PHP's weak typing - // be useful for a change - return $str; - } - - /** - * Replace the byte offset region of the source with $newText. - * Works by adding elements to the $this->edits array. - */ - function replaceSourceRegion( $start, $end, $newText = false ) { - // Split all copy operations with a source corresponding to the region - // in question. - $newEdits = array(); - foreach ( $this->edits as $edit ) { - if ( $edit[0] !== 'copy' ) { - $newEdits[] = $edit; - continue; - } - $copyStart = $edit[1]; - $copyEnd = $edit[2]; - if ( $start >= $copyEnd || $end <= $copyStart ) { - // Outside this region - $newEdits[] = $edit; - continue; - } - if ( ( $start < $copyStart && $end > $copyStart ) - || ( $start < $copyEnd && $end > $copyEnd ) - ) { - throw new MWException( "Overlapping regions found, can't do the edit" ); - } - // Split the copy - $newEdits[] = array( 'copy', $copyStart, $start ); - if ( $newText !== false ) { - $newEdits[] = array( 'insert', $newText ); - } - $newEdits[] = array( 'copy', $end, $copyEnd ); - } - $this->edits = $newEdits; - } - - /** - * Finds the source byte region which you would want to delete, if $pathName - * was to be deleted. Includes the leading spaces and tabs, the trailing line - * break, and any comments in between. - * @param $pathName - * @throws MWException - * @return array - */ - function findDeletionRegion( $pathName ) { - if ( !isset( $this->pathInfo[$pathName] ) ) { - throw new MWException( "Can't find path \"$pathName\"" ); - } - $path = $this->pathInfo[$pathName]; - // Find the start - $this->firstToken(); - while ( $this->pos != $path['startToken'] ) { - $this->nextToken(); - } - $regionStart = $path['startByte']; - for ( $offset = -1; $offset >= -$this->pos; $offset-- ) { - $token = $this->getTokenAhead( $offset ); - if ( !$token->isSkip() ) { - // If there is other content on the same line, don't move the start point - // back, because that will cause the regions to overlap. - $regionStart = $path['startByte']; - break; - } - $lfPos = strrpos( $token->text, "\n" ); - if ( $lfPos === false ) { - $regionStart -= strlen( $token->text ); - } else { - // The line start does not include the LF - $regionStart -= strlen( $token->text ) - $lfPos - 1; - break; - } - } - // Find the end - while ( $this->pos != $path['endToken'] ) { - $this->nextToken(); - } - $regionEnd = $path['endByte']; // past the end - for ( $offset = 0; $offset < count( $this->tokens ) - $this->pos; $offset++ ) { - $token = $this->getTokenAhead( $offset ); - if ( !$token->isSkip() ) { - break; - } - $lfPos = strpos( $token->text, "\n" ); - if ( $lfPos === false ) { - $regionEnd += strlen( $token->text ); - } else { - // This should point past the LF - $regionEnd += $lfPos + 1; - break; - } - } - return array( $regionStart, $regionEnd ); - } - - /** - * Find the byte region in the source corresponding to the value part. - * This includes the quotes, but does not include the trailing comma - * or semicolon. - * - * The end position is the past-the-end (end + 1) value as per convention. - * @param $pathName - * @throws MWException - * @return array - */ - function findValueRegion( $pathName ) { - if ( !isset( $this->pathInfo[$pathName] ) ) { - throw new MWException( "Can't find path \"$pathName\"" ); - } - $path = $this->pathInfo[$pathName]; - if ( $path['valueStartByte'] === false || $path['valueEndByte'] === false ) { - throw new MWException( "Can't find value region for path \"$pathName\"" ); - } - return array( $path['valueStartByte'], $path['valueEndByte'] ); - } - - /** - * Find the path name of the last element in the array. - * If the array is empty, this will return the \@extra interstitial element. - * If the specified path is not found or is not an array, it will return false. - * @return bool|int|string - */ - function findLastArrayElement( $path ) { - // Try for a real element - $lastEltPath = false; - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); - if ( $part2 == '@' ) { - // Do nothing - } elseif ( $part1 == "$path/" ) { - $lastEltPath = $candidatePath; - } elseif ( $lastEltPath !== false ) { - break; - } - } - if ( $lastEltPath !== false ) { - return $lastEltPath; - } - - // Try for an interstitial element - $extraPath = false; - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - if ( $part1 == "$path/" ) { - $extraPath = $candidatePath; - } elseif ( $extraPath !== false ) { - break; - } - } - return $extraPath; - } - - /** - * Find the path name of first element in the array. - * If the array is empty, this will return the \@extra interstitial element. - * If the specified path is not found or is not an array, it will return false. - * @return bool|int|string - */ - function findFirstArrayElement( $path ) { - // Try for an ordinary element - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - $part2 = substr( $candidatePath, strlen( $path ) + 1, 1 ); - if ( $part1 == "$path/" && $part2 != '@' ) { - return $candidatePath; - } - } - - // Try for an interstitial element - foreach ( $this->pathInfo as $candidatePath => $info ) { - $part1 = substr( $candidatePath, 0, strlen( $path ) + 1 ); - if ( $part1 == "$path/" ) { - return $candidatePath; - } - } - return false; - } - - /** - * Get the indent string which sits after a given start position. - * Returns false if the position is not at the start of the line. - * @return array - */ - function getIndent( $pos, $key = false, $arrowPos = false ) { - $arrowIndent = ' '; - if ( $pos == 0 || $this->text[$pos - 1] == "\n" ) { - $indentLength = strspn( $this->text, " \t", $pos ); - $indent = substr( $this->text, $pos, $indentLength ); - } else { - $indent = false; - } - if ( $indent !== false && $arrowPos !== false ) { - $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key ); - if ( $arrowIndentLength > 0 ) { - $arrowIndent = str_repeat( ' ', $arrowIndentLength ); - } - } - return array( $indent, $arrowIndent ); - } - - /** - * Run the parser on the text. Throws an exception if the string does not - * match our defined subset of PHP syntax. - */ - public function parse() { - $this->initParse(); - $this->pushState( 'file' ); - $this->pushPath( '@extra-' . ( $this->serial++ ) ); - $token = $this->firstToken(); - - while ( !$token->isEnd() ) { - $state = $this->popState(); - if ( !$state ) { - $this->error( 'internal error: empty state stack' ); - } - - switch ( $state ) { - case 'file': - $this->expect( T_OPEN_TAG ); - $token = $this->skipSpace(); - if ( $token->isEnd() ) { - break 2; - } - $this->pushState( 'statement', 'file 2' ); - break; - case 'file 2': - $token = $this->skipSpace(); - if ( $token->isEnd() ) { - break 2; - } - $this->pushState( 'statement', 'file 2' ); - break; - case 'statement': - $token = $this->skipSpace(); - if ( !$this->validatePath( $token->text ) ) { - $this->error( "Invalid variable name \"{$token->text}\"" ); - } - $this->nextPath( $token->text ); - $this->expect( T_VARIABLE ); - $this->skipSpace(); - $arrayAssign = false; - if ( $this->currentToken()->type == '[' ) { - $this->nextToken(); - $token = $this->skipSpace(); - if ( !$token->isScalar() ) { - $this->error( "expected a string or number for the array key" ); - } - if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { - $text = $this->parseScalar( $token->text ); - } else { - $text = $token->text; - } - if ( !$this->validatePath( $text ) ) { - $this->error( "Invalid associative array name \"$text\"" ); - } - $this->pushPath( $text ); - $this->nextToken(); - $this->skipSpace(); - $this->expect( ']' ); - $this->skipSpace(); - $arrayAssign = true; - } - $this->expect( '=' ); - $this->skipSpace(); - $this->startPathValue(); - if ( $arrayAssign ) { - $this->pushState( 'expression', 'array assign end' ); - } else { - $this->pushState( 'expression', 'statement end' ); - } - break; - case 'array assign end': - case 'statement end': - $this->endPathValue(); - if ( $state == 'array assign end' ) { - $this->popPath(); - } - $this->skipSpace(); - $this->expect( ';' ); - $this->nextPath( '@extra-' . ( $this->serial++ ) ); - break; - case 'expression': - $token = $this->skipSpace(); - if ( $token->type == T_ARRAY ) { - $this->pushState( 'array' ); - } elseif ( $token->isScalar() ) { - $this->nextToken(); - } elseif ( $token->type == T_VARIABLE ) { - $this->nextToken(); - } else { - $this->error( "expected simple expression" ); - } - break; - case 'array': - $this->skipSpace(); - $this->expect( T_ARRAY ); - $this->skipSpace(); - $this->expect( '(' ); - $this->skipSpace(); - $this->pushPath( '@extra-' . ( $this->serial++ ) ); - if ( $this->isAhead( ')' ) ) { - // Empty array - $this->pushState( 'array end' ); - } else { - $this->pushState( 'element', 'array end' ); - } - break; - case 'array end': - $this->skipSpace(); - $this->popPath(); - $this->expect( ')' ); - break; - case 'element': - $token = $this->skipSpace(); - // Look ahead to find the double arrow - if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW, 1 ) ) { - // Found associative element - $this->pushState( 'assoc-element', 'element end' ); - } else { - // Not associative - $this->nextPath( '@next' ); - $this->startPathValue(); - $this->pushState( 'expression', 'element end' ); - } - break; - case 'element end': - $token = $this->skipSpace(); - if ( $token->type == ',' ) { - $this->endPathValue(); - $this->markComma(); - $this->nextToken(); - $this->nextPath( '@extra-' . ( $this->serial++ ) ); - // Look ahead to find ending bracket - if ( $this->isAhead( ")" ) ) { - // Found ending bracket, no continuation - $this->skipSpace(); - } else { - // No ending bracket, continue to next element - $this->pushState( 'element' ); - } - } elseif ( $token->type == ')' ) { - // End array - $this->endPathValue(); - } else { - $this->error( "expected the next array element or the end of the array" ); - } - break; - case 'assoc-element': - $token = $this->skipSpace(); - if ( !$token->isScalar() ) { - $this->error( "expected a string or number for the array key" ); - } - if ( $token->type == T_CONSTANT_ENCAPSED_STRING ) { - $text = $this->parseScalar( $token->text ); - } else { - $text = $token->text; - } - if ( !$this->validatePath( $text ) ) { - $this->error( "Invalid associative array name \"$text\"" ); - } - $this->nextPath( $text ); - $this->nextToken(); - $this->skipSpace(); - $this->markArrow(); - $this->expect( T_DOUBLE_ARROW ); - $this->skipSpace(); - $this->startPathValue(); - $this->pushState( 'expression' ); - break; - } - } - if ( count( $this->stateStack ) ) { - $this->error( 'unexpected end of file' ); - } - $this->popPath(); - } - - /** - * Initialise a parse. - */ - protected function initParse() { - $this->tokens = token_get_all( $this->text ); - $this->stateStack = array(); - $this->pathStack = array(); - $this->firstToken(); - $this->pathInfo = array(); - $this->serial = 1; - } - - /** - * Set the parse position. Do not call this except from firstToken() and - * nextToken(), there is more to update than just the position. - */ - protected function setPos( $pos ) { - $this->pos = $pos; - if ( $this->pos >= count( $this->tokens ) ) { - $this->currentToken = ConfEditorToken::newEnd(); - } else { - $this->currentToken = $this->newTokenObj( $this->tokens[$this->pos] ); - } - return $this->currentToken; - } - - /** - * Create a ConfEditorToken from an element of token_get_all() - * @return ConfEditorToken - */ - function newTokenObj( $internalToken ) { - if ( is_array( $internalToken ) ) { - return new ConfEditorToken( $internalToken[0], $internalToken[1] ); - } else { - return new ConfEditorToken( $internalToken, $internalToken ); - } - } - - /** - * Reset the parse position - */ - function firstToken() { - $this->setPos( 0 ); - $this->prevToken = ConfEditorToken::newEnd(); - $this->lineNum = 1; - $this->colNum = 1; - $this->byteNum = 0; - return $this->currentToken; - } - - /** - * Get the current token - */ - function currentToken() { - return $this->currentToken; - } - - /** - * Advance the current position and return the resulting next token - */ - function nextToken() { - if ( $this->currentToken ) { - $text = $this->currentToken->text; - $lfCount = substr_count( $text, "\n" ); - if ( $lfCount ) { - $this->lineNum += $lfCount; - $this->colNum = strlen( $text ) - strrpos( $text, "\n" ); - } else { - $this->colNum += strlen( $text ); - } - $this->byteNum += strlen( $text ); - } - $this->prevToken = $this->currentToken; - $this->setPos( $this->pos + 1 ); - return $this->currentToken; - } - - /** - * Get the token $offset steps ahead of the current position. - * $offset may be negative, to get tokens behind the current position. - * @return ConfEditorToken - */ - function getTokenAhead( $offset ) { - $pos = $this->pos + $offset; - if ( $pos >= count( $this->tokens ) || $pos < 0 ) { - return ConfEditorToken::newEnd(); - } else { - return $this->newTokenObj( $this->tokens[$pos] ); - } - } - - /** - * Advances the current position past any whitespace or comments - */ - function skipSpace() { - while ( $this->currentToken && $this->currentToken->isSkip() ) { - $this->nextToken(); - } - return $this->currentToken; - } - - /** - * Throws an error if the current token is not of the given type, and - * then advances to the next position. - */ - function expect( $type ) { - if ( $this->currentToken && $this->currentToken->type == $type ) { - return $this->nextToken(); - } else { - $this->error( "expected " . $this->getTypeName( $type ) . - ", got " . $this->getTypeName( $this->currentToken->type ) ); - } - } - - /** - * Push a state or two on to the state stack. - */ - function pushState( $nextState, $stateAfterThat = null ) { - if ( $stateAfterThat !== null ) { - $this->stateStack[] = $stateAfterThat; - } - $this->stateStack[] = $nextState; - } - - /** - * Pop a state from the state stack. - * @return mixed - */ - function popState() { - return array_pop( $this->stateStack ); - } - - /** - * Returns true if the user input path is valid. - * This exists to allow "/" and "@" to be reserved for string path keys - * @return bool - */ - function validatePath( $path ) { - return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@'; - } - - /** - * Internal function to update some things at the end of a path region. Do - * not call except from popPath() or nextPath(). - */ - function endPath() { - $key = ''; - foreach ( $this->pathStack as $pathInfo ) { - if ( $key !== '' ) { - $key .= '/'; - } - $key .= $pathInfo['name']; - } - $pathInfo['endByte'] = $this->byteNum; - $pathInfo['endToken'] = $this->pos; - $this->pathInfo[$key] = $pathInfo; - } - - /** - * Go up to a new path level, for example at the start of an array. - */ - function pushPath( $path ) { - $this->pathStack[] = array( - 'name' => $path, - 'level' => count( $this->pathStack ) + 1, - 'startByte' => $this->byteNum, - 'startToken' => $this->pos, - 'valueStartToken' => false, - 'valueStartByte' => false, - 'valueEndToken' => false, - 'valueEndByte' => false, - 'nextArrayIndex' => 0, - 'hasComma' => false, - 'arrowByte' => false - ); - } - - /** - * Go down a path level, for example at the end of an array. - */ - function popPath() { - $this->endPath(); - array_pop( $this->pathStack ); - } - - /** - * Go to the next path on the same level. This ends the current path and - * starts a new one. If $path is \@next, the new path is set to the next - * numeric array element. - */ - function nextPath( $path ) { - $this->endPath(); - $i = count( $this->pathStack ) - 1; - if ( $path == '@next' ) { - $nextArrayIndex =& $this->pathStack[$i]['nextArrayIndex']; - $this->pathStack[$i]['name'] = $nextArrayIndex; - $nextArrayIndex++; - } else { - $this->pathStack[$i]['name'] = $path; - } - $this->pathStack[$i] = - array( - 'startByte' => $this->byteNum, - 'startToken' => $this->pos, - 'valueStartToken' => false, - 'valueStartByte' => false, - 'valueEndToken' => false, - 'valueEndByte' => false, - 'hasComma' => false, - 'arrowByte' => false, - ) + $this->pathStack[$i]; - } - - /** - * Mark the start of the value part of a path. - */ - function startPathValue() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['valueStartToken'] = $this->pos; - $path['valueStartByte'] = $this->byteNum; - } - - /** - * Mark the end of the value part of a path. - */ - function endPathValue() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['valueEndToken'] = $this->pos; - $path['valueEndByte'] = $this->byteNum; - } - - /** - * Mark the comma separator in an array element - */ - function markComma() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['hasComma'] = true; - } - - /** - * Mark the arrow separator in an associative array element - */ - function markArrow() { - $path =& $this->pathStack[count( $this->pathStack ) - 1]; - $path['arrowByte'] = $this->byteNum; - } - - /** - * Generate a parse error - */ - function error( $msg ) { - throw new ConfEditorParseError( $this, $msg ); - } - - /** - * Get a readable name for the given token type. - * @return string - */ - function getTypeName( $type ) { - if ( is_int( $type ) ) { - return token_name( $type ); - } else { - return "\"$type\""; - } - } - - /** - * Looks ahead to see if the given type is the next token type, starting - * from the current position plus the given offset. Skips any intervening - * whitespace. - * @return bool - */ - function isAhead( $type, $offset = 0 ) { - $ahead = $offset; - $token = $this->getTokenAhead( $offset ); - while ( !$token->isEnd() ) { - if ( $token->isSkip() ) { - $ahead++; - $token = $this->getTokenAhead( $ahead ); - continue; - } elseif ( $token->type == $type ) { - // Found the type - return true; - } else { - // Not found - return false; - } - } - return false; - } - - /** - * Get the previous token object - */ - function prevToken() { - return $this->prevToken; - } - - /** - * Echo a reasonably readable representation of the tokenizer array. - */ - function dumpTokens() { - $out = ''; - foreach ( $this->tokens as $token ) { - $obj = $this->newTokenObj( $token ); - $out .= sprintf( "%-28s %s\n", - $this->getTypeName( $obj->type ), - addcslashes( $obj->text, "\0..\37" ) ); - } - echo "<pre>" . htmlspecialchars( $out ) . "</pre>"; - } -} - -/** - * Exception class for parse errors - */ -class ConfEditorParseError extends MWException { - var $lineNum, $colNum; - function __construct( $editor, $msg ) { - $this->lineNum = $editor->lineNum; - $this->colNum = $editor->colNum; - parent::__construct( "Parse error on line {$editor->lineNum} " . - "col {$editor->colNum}: $msg" ); - } - - function highlight( $text ) { - $lines = StringUtils::explode( "\n", $text ); - foreach ( $lines as $lineNum => $line ) { - if ( $lineNum == $this->lineNum - 1 ) { - return "$line\n" . str_repeat( ' ', $this->colNum - 1 ) . "^\n"; - } - } - return ''; - } - -} - -/** - * Class to wrap a token from the tokenizer. - */ -class ConfEditorToken { - var $type, $text; - - static $scalarTypes = array( T_LNUMBER, T_DNUMBER, T_STRING, T_CONSTANT_ENCAPSED_STRING ); - static $skipTypes = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); - - static function newEnd() { - return new self( 'END', '' ); - } - - function __construct( $type, $text ) { - $this->type = $type; - $this->text = $text; - } - - function isSkip() { - return in_array( $this->type, self::$skipTypes ); - } - - function isScalar() { - return in_array( $this->type, self::$scalarTypes ); - } - - function isEnd() { - return $this->type == 'END'; - } -} |