3 * Configuration file editor.
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
24 * This is a state machine style parser with two internal stacks:
25 * * A next state stack, which determines the state the machine will progress to next
26 * * A path stack, which keeps track of the logical location in the file.
30 * file = T_OPEN_TAG *statement
31 * statement = T_VARIABLE "=" expression ";"
32 * expression = array / scalar / T_VARIABLE
33 * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")"
34 * element = assoc-element / expression
35 * assoc-element = scalar T_DOUBLE_ARROW expression
36 * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING
39 /** The text to parse */
42 /** The token array from token_get_all() */
45 /** The current position in the token array */
48 /** The current 1-based line number */
51 /** The current 1-based column number */
54 /** The current 0-based byte number */
57 /** The current ConfEditorToken object */
60 /** The previous ConfEditorToken object */
64 * The state machine stack. This is an array of strings where the topmost
65 * element will be popped off and become the next parser state.
70 * The path stack is a stack of associative arrays with the following elements:
71 * name The name of top level of the path
72 * level The level (number of elements) of the path
73 * startByte The byte offset of the start of the path
74 * startToken The token offset of the start
75 * endByte The byte offset of thee
76 * endToken The token offset of the end, plus one
77 * valueStartToken The start token offset of the value part
78 * valueStartByte The start byte offset of the value part
79 * valueEndToken The end token offset of the value part, plus one
80 * valueEndByte The end byte offset of the value part, plus one
81 * nextArrayIndex The next numeric array index at this level
82 * hasComma True if the array element ends with a comma
83 * arrowByte The byte offset of the "=>", or false if there isn't one
88 * The elements of the top of the pathStack for every path encountered, indexed
89 * by slash-separated path.
94 * Next serial number for whitespace placeholder paths (\@extra-N)
99 * Editor state. This consists of the internal copy/insert operations which
100 * are applied to the source string to obtain the destination string.
105 * Simple entry point for command-line testing
107 * @param $text string
111 static function test( $text ) {
113 $ce = new self( $text );
115 } catch ( ConfEditorParseError
$e ) {
116 return $e->getMessage() . "\n" . $e->highlight( $text );
122 * Construct a new parser
124 public function __construct( $text ) {
129 * Edit the text. Returns the edited text.
130 * @param array $ops of operations.
132 * Operations are given as an associative array, with members:
133 * type: One of delete, set, append or insert (required)
134 * path: The path to operate on (required)
135 * key: The array key to insert/append, with PHP quotes
136 * value: The value, with PHP quotes
139 * Deletes an array element or statement with the specified path.
141 * array('type' => 'delete', 'path' => '$foo/bar/baz' )
142 * is equivalent to the runtime PHP code:
143 * unset( $foo['bar']['baz'] );
146 * Sets the value of an array element. If the element doesn't exist, it
147 * is appended to the array. If it does exist, the value is set, with
148 * comments and indenting preserved.
151 * Appends a new element to the end of the array. Adds a trailing comma.
153 * array( 'type' => 'append', 'path', '$foo/bar',
154 * 'key' => 'baz', 'value' => "'x'" )
155 * is like the PHP code:
156 * $foo['bar']['baz'] = 'x';
159 * Insert a new element at the start of the array.
161 * @throws MWException
164 public function edit( $ops ) {
167 $this->edits
= array(
168 array( 'copy', 0, strlen( $this->text
) )
170 foreach ( $ops as $op ) {
173 $value = isset( $op['value'] ) ?
$op['value'] : null;
174 $key = isset( $op['key'] ) ?
$op['key'] : null;
178 list( $start, $end ) = $this->findDeletionRegion( $path );
179 $this->replaceSourceRegion( $start, $end, false );
182 if ( isset( $this->pathInfo
[$path] ) ) {
183 list( $start, $end ) = $this->findValueRegion( $path );
184 $encValue = $value; // var_export( $value, true );
185 $this->replaceSourceRegion( $start, $end, $encValue );
188 // No existing path, fall through to append
189 $slashPos = strrpos( $path, '/' );
190 $key = var_export( substr( $path, $slashPos +
1 ), true );
191 $path = substr( $path, 0, $slashPos );
194 // Find the last array element
195 $lastEltPath = $this->findLastArrayElement( $path );
196 if ( $lastEltPath === false ) {
197 throw new MWException( "Can't find any element of array \"$path\"" );
199 $lastEltInfo = $this->pathInfo
[$lastEltPath];
201 // Has it got a comma already?
202 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
203 // No comma, insert one after the value region
204 list( , $end ) = $this->findValueRegion( $lastEltPath );
205 $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
208 // Make the text to insert
209 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
211 if ( $key === null ) {
212 list( $indent, ) = $this->getIndent( $start );
213 $textToInsert = "$indent$value,";
215 list( $indent, $arrowIndent ) =
216 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
217 $textToInsert = "$indent$key$arrowIndent=> $value,";
219 $textToInsert .= ( $indent === false ?
' ' : "\n" );
222 $this->replaceSourceRegion( $end, $end, $textToInsert );
225 // Find first array element
226 $firstEltPath = $this->findFirstArrayElement( $path );
227 if ( $firstEltPath === false ) {
228 throw new MWException( "Can't find array element of \"$path\"" );
230 list( $start, ) = $this->findDeletionRegion( $firstEltPath );
231 $info = $this->pathInfo
[$firstEltPath];
233 // Make the text to insert
234 if ( $key === null ) {
235 list( $indent, ) = $this->getIndent( $start );
236 $textToInsert = "$indent$value,";
238 list( $indent, $arrowIndent ) =
239 $this->getIndent( $start, $key, $info['arrowByte'] );
240 $textToInsert = "$indent$key$arrowIndent=> $value,";
242 $textToInsert .= ( $indent === false ?
' ' : "\n" );
245 $this->replaceSourceRegion( $start, $start, $textToInsert );
248 throw new MWException( "Unrecognised operation: \"$type\"" );
254 foreach ( $this->edits
as $edit ) {
255 if ( $edit[0] == 'copy' ) {
256 $out .= substr( $this->text
, $edit[1], $edit[2] - $edit[1] );
257 } else { // if ( $edit[0] == 'insert' )
262 // Do a second parse as a sanity check
266 } catch ( ConfEditorParseError
$e ) {
267 throw new MWException(
268 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
275 * Get the variables defined in the text
276 * @return array( varname => value )
281 foreach( $this->pathInfo
as $path => $data ) {
282 if ( $path[0] != '$' )
284 $trimmedPath = substr( $path, 1 );
285 $name = $data['name'];
286 if ( $name[0] == '@' )
288 if ( $name[0] == '$' )
289 $name = substr( $name, 1 );
290 $parentPath = substr( $trimmedPath, 0,
291 strlen( $trimmedPath ) - strlen( $name ) );
292 if( substr( $parentPath, -1 ) == '/' )
293 $parentPath = substr( $parentPath, 0, -1 );
295 $value = substr( $this->text
, $data['valueStartByte'],
296 $data['valueEndByte'] - $data['valueStartByte']
298 $this->setVar( $vars, $parentPath, $name,
299 $this->parseScalar( $value ) );
305 * Set a value in an array, unless it's set already. For instance,
306 * setVar( $arr, 'foo/bar', 'baz', 3 ); will set
307 * $arr['foo']['bar']['baz'] = 3;
308 * @param $array array
309 * @param string $path slash-delimited path
310 * @param $key mixed Key
311 * @param $value mixed Value
313 function setVar( &$array, $path, $key, $value ) {
314 $pathArr = explode( '/', $path );
316 if ( $path !== '' ) {
317 foreach ( $pathArr as $p ) {
318 if( !isset( $target[$p] ) )
319 $target[$p] = array();
320 $target =& $target[$p];
323 if ( !isset( $target[$key] ) )
324 $target[$key] = $value;
328 * Parse a scalar value in PHP
329 * @return mixed Parsed value
331 function parseScalar( $str ) {
332 if ( $str !== '' && $str[0] == '\'' )
333 // Single-quoted string
334 // @todo FIXME: trim() call is due to mystery bug where whitespace gets
335 // appended to the token; without it we ended up reading in the
336 // extra quote on the end!
337 return strtr( substr( trim( $str ), 1, -1 ),
338 array( '\\\'' => '\'', '\\\\' => '\\' ) );
339 if ( $str !== '' && $str[0] == '"' )
340 // Double-quoted string
341 // @todo FIXME: trim() call is due to mystery bug where whitespace gets
342 // appended to the token; without it we ended up reading in the
343 // extra quote on the end!
344 return stripcslashes( substr( trim( $str ), 1, -1 ) );
345 if ( substr( $str, 0, 4 ) == 'true' )
347 if ( substr( $str, 0, 5 ) == 'false' )
349 if ( substr( $str, 0, 4 ) == 'null' )
351 // Must be some kind of numeric value, so let PHP's weak typing
352 // be useful for a change
357 * Replace the byte offset region of the source with $newText.
358 * Works by adding elements to the $this->edits array.
360 function replaceSourceRegion( $start, $end, $newText = false ) {
361 // Split all copy operations with a source corresponding to the region
364 foreach ( $this->edits
as $edit ) {
365 if ( $edit[0] !== 'copy' ) {
369 $copyStart = $edit[1];
371 if ( $start >= $copyEnd ||
$end <= $copyStart ) {
372 // Outside this region
376 if ( ( $start < $copyStart && $end > $copyStart )
377 ||
( $start < $copyEnd && $end > $copyEnd )
379 throw new MWException( "Overlapping regions found, can't do the edit" );
382 $newEdits[] = array( 'copy', $copyStart, $start );
383 if ( $newText !== false ) {
384 $newEdits[] = array( 'insert', $newText );
386 $newEdits[] = array( 'copy', $end, $copyEnd );
388 $this->edits
= $newEdits;
392 * Finds the source byte region which you would want to delete, if $pathName
393 * was to be deleted. Includes the leading spaces and tabs, the trailing line
394 * break, and any comments in between.
396 * @throws MWException
399 function findDeletionRegion( $pathName ) {
400 if ( !isset( $this->pathInfo
[$pathName] ) ) {
401 throw new MWException( "Can't find path \"$pathName\"" );
403 $path = $this->pathInfo
[$pathName];
406 while ( $this->pos
!= $path['startToken'] ) {
409 $regionStart = $path['startByte'];
410 for ( $offset = -1; $offset >= -$this->pos
; $offset-- ) {
411 $token = $this->getTokenAhead( $offset );
412 if ( !$token->isSkip() ) {
413 // If there is other content on the same line, don't move the start point
414 // back, because that will cause the regions to overlap.
415 $regionStart = $path['startByte'];
418 $lfPos = strrpos( $token->text
, "\n" );
419 if ( $lfPos === false ) {
420 $regionStart -= strlen( $token->text
);
422 // The line start does not include the LF
423 $regionStart -= strlen( $token->text
) - $lfPos - 1;
428 while ( $this->pos
!= $path['endToken'] ) {
431 $regionEnd = $path['endByte']; // past the end
432 for ( $offset = 0; $offset < count( $this->tokens
) - $this->pos
; $offset++
) {
433 $token = $this->getTokenAhead( $offset );
434 if ( !$token->isSkip() ) {
437 $lfPos = strpos( $token->text
, "\n" );
438 if ( $lfPos === false ) {
439 $regionEnd +
= strlen( $token->text
);
441 // This should point past the LF
442 $regionEnd +
= $lfPos +
1;
446 return array( $regionStart, $regionEnd );
450 * Find the byte region in the source corresponding to the value part.
451 * This includes the quotes, but does not include the trailing comma
454 * The end position is the past-the-end (end + 1) value as per convention.
456 * @throws MWException
459 function findValueRegion( $pathName ) {
460 if ( !isset( $this->pathInfo
[$pathName] ) ) {
461 throw new MWException( "Can't find path \"$pathName\"" );
463 $path = $this->pathInfo
[$pathName];
464 if ( $path['valueStartByte'] === false ||
$path['valueEndByte'] === false ) {
465 throw new MWException( "Can't find value region for path \"$pathName\"" );
467 return array( $path['valueStartByte'], $path['valueEndByte'] );
471 * Find the path name of the last element in the array.
472 * If the array is empty, this will return the \@extra interstitial element.
473 * If the specified path is not found or is not an array, it will return false.
474 * @return bool|int|string
476 function findLastArrayElement( $path ) {
477 // Try for a real element
478 $lastEltPath = false;
479 foreach ( $this->pathInfo
as $candidatePath => $info ) {
480 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
481 $part2 = substr( $candidatePath, strlen( $path ) +
1, 1 );
482 if ( $part2 == '@' ) {
484 } elseif ( $part1 == "$path/" ) {
485 $lastEltPath = $candidatePath;
486 } elseif ( $lastEltPath !== false ) {
490 if ( $lastEltPath !== false ) {
494 // Try for an interstitial element
496 foreach ( $this->pathInfo
as $candidatePath => $info ) {
497 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
498 if ( $part1 == "$path/" ) {
499 $extraPath = $candidatePath;
500 } elseif ( $extraPath !== false ) {
508 * Find the path name of first element in the array.
509 * If the array is empty, this will return the \@extra interstitial element.
510 * If the specified path is not found or is not an array, it will return false.
511 * @return bool|int|string
513 function findFirstArrayElement( $path ) {
514 // Try for an ordinary element
515 foreach ( $this->pathInfo
as $candidatePath => $info ) {
516 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
517 $part2 = substr( $candidatePath, strlen( $path ) +
1, 1 );
518 if ( $part1 == "$path/" && $part2 != '@' ) {
519 return $candidatePath;
523 // Try for an interstitial element
524 foreach ( $this->pathInfo
as $candidatePath => $info ) {
525 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
526 if ( $part1 == "$path/" ) {
527 return $candidatePath;
534 * Get the indent string which sits after a given start position.
535 * Returns false if the position is not at the start of the line.
538 function getIndent( $pos, $key = false, $arrowPos = false ) {
540 if ( $pos == 0 ||
$this->text
[$pos - 1] == "\n" ) {
541 $indentLength = strspn( $this->text
, " \t", $pos );
542 $indent = substr( $this->text
, $pos, $indentLength );
546 if ( $indent !== false && $arrowPos !== false ) {
547 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
548 if ( $arrowIndentLength > 0 ) {
549 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
552 return array( $indent, $arrowIndent );
556 * Run the parser on the text. Throws an exception if the string does not
557 * match our defined subset of PHP syntax.
559 public function parse() {
561 $this->pushState( 'file' );
562 $this->pushPath( '@extra-' . ($this->serial++
) );
563 $token = $this->firstToken();
565 while ( !$token->isEnd() ) {
566 $state = $this->popState();
568 $this->error( 'internal error: empty state stack' );
573 $this->expect( T_OPEN_TAG
);
574 $token = $this->skipSpace();
575 if ( $token->isEnd() ) {
578 $this->pushState( 'statement', 'file 2' );
581 $token = $this->skipSpace();
582 if ( $token->isEnd() ) {
585 $this->pushState( 'statement', 'file 2' );
588 $token = $this->skipSpace();
589 if ( !$this->validatePath( $token->text
) ) {
590 $this->error( "Invalid variable name \"{$token->text}\"" );
592 $this->nextPath( $token->text
);
593 $this->expect( T_VARIABLE
);
595 $arrayAssign = false;
596 if ( $this->currentToken()->type
== '[' ) {
598 $token = $this->skipSpace();
599 if ( !$token->isScalar() ) {
600 $this->error( "expected a string or number for the array key" );
602 if ( $token->type
== T_CONSTANT_ENCAPSED_STRING
) {
603 $text = $this->parseScalar( $token->text
);
605 $text = $token->text
;
607 if ( !$this->validatePath( $text ) ) {
608 $this->error( "Invalid associative array name \"$text\"" );
610 $this->pushPath( $text );
613 $this->expect( ']' );
617 $this->expect( '=' );
619 $this->startPathValue();
621 $this->pushState( 'expression', 'array assign end' );
623 $this->pushState( 'expression', 'statement end' );
625 case 'array assign end':
626 case 'statement end':
627 $this->endPathValue();
628 if ( $state == 'array assign end' )
631 $this->expect( ';' );
632 $this->nextPath( '@extra-' . ($this->serial++
) );
635 $token = $this->skipSpace();
636 if ( $token->type
== T_ARRAY
) {
637 $this->pushState( 'array' );
638 } elseif ( $token->isScalar() ) {
640 } elseif ( $token->type
== T_VARIABLE
) {
643 $this->error( "expected simple expression" );
648 $this->expect( T_ARRAY
);
650 $this->expect( '(' );
652 $this->pushPath( '@extra-' . ($this->serial++
) );
653 if ( $this->isAhead( ')' ) ) {
655 $this->pushState( 'array end' );
657 $this->pushState( 'element', 'array end' );
663 $this->expect( ')' );
666 $token = $this->skipSpace();
667 // Look ahead to find the double arrow
668 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW
, 1 ) ) {
669 // Found associative element
670 $this->pushState( 'assoc-element', 'element end' );
673 $this->nextPath( '@next' );
674 $this->startPathValue();
675 $this->pushState( 'expression', 'element end' );
679 $token = $this->skipSpace();
680 if ( $token->type
== ',' ) {
681 $this->endPathValue();
684 $this->nextPath( '@extra-' . ($this->serial++
) );
685 // Look ahead to find ending bracket
686 if ( $this->isAhead( ")" ) ) {
687 // Found ending bracket, no continuation
690 // No ending bracket, continue to next element
691 $this->pushState( 'element' );
693 } elseif ( $token->type
== ')' ) {
695 $this->endPathValue();
697 $this->error( "expected the next array element or the end of the array" );
700 case 'assoc-element':
701 $token = $this->skipSpace();
702 if ( !$token->isScalar() ) {
703 $this->error( "expected a string or number for the array key" );
705 if ( $token->type
== T_CONSTANT_ENCAPSED_STRING
) {
706 $text = $this->parseScalar( $token->text
);
708 $text = $token->text
;
710 if ( !$this->validatePath( $text ) ) {
711 $this->error( "Invalid associative array name \"$text\"" );
713 $this->nextPath( $text );
717 $this->expect( T_DOUBLE_ARROW
);
719 $this->startPathValue();
720 $this->pushState( 'expression' );
724 if ( count( $this->stateStack
) ) {
725 $this->error( 'unexpected end of file' );
731 * Initialise a parse.
733 protected function initParse() {
734 $this->tokens
= token_get_all( $this->text
);
735 $this->stateStack
= array();
736 $this->pathStack
= array();
738 $this->pathInfo
= array();
743 * Set the parse position. Do not call this except from firstToken() and
744 * nextToken(), there is more to update than just the position.
746 protected function setPos( $pos ) {
748 if ( $this->pos
>= count( $this->tokens
) ) {
749 $this->currentToken
= ConfEditorToken
::newEnd();
751 $this->currentToken
= $this->newTokenObj( $this->tokens
[$this->pos
] );
753 return $this->currentToken
;
757 * Create a ConfEditorToken from an element of token_get_all()
758 * @return ConfEditorToken
760 function newTokenObj( $internalToken ) {
761 if ( is_array( $internalToken ) ) {
762 return new ConfEditorToken( $internalToken[0], $internalToken[1] );
764 return new ConfEditorToken( $internalToken, $internalToken );
769 * Reset the parse position
771 function firstToken() {
773 $this->prevToken
= ConfEditorToken
::newEnd();
777 return $this->currentToken
;
781 * Get the current token
783 function currentToken() {
784 return $this->currentToken
;
788 * Advance the current position and return the resulting next token
790 function nextToken() {
791 if ( $this->currentToken
) {
792 $text = $this->currentToken
->text
;
793 $lfCount = substr_count( $text, "\n" );
795 $this->lineNum +
= $lfCount;
796 $this->colNum
= strlen( $text ) - strrpos( $text, "\n" );
798 $this->colNum +
= strlen( $text );
800 $this->byteNum +
= strlen( $text );
802 $this->prevToken
= $this->currentToken
;
803 $this->setPos( $this->pos +
1 );
804 return $this->currentToken
;
808 * Get the token $offset steps ahead of the current position.
809 * $offset may be negative, to get tokens behind the current position.
810 * @return ConfEditorToken
812 function getTokenAhead( $offset ) {
813 $pos = $this->pos +
$offset;
814 if ( $pos >= count( $this->tokens
) ||
$pos < 0 ) {
815 return ConfEditorToken
::newEnd();
817 return $this->newTokenObj( $this->tokens
[$pos] );
822 * Advances the current position past any whitespace or comments
824 function skipSpace() {
825 while ( $this->currentToken
&& $this->currentToken
->isSkip() ) {
828 return $this->currentToken
;
832 * Throws an error if the current token is not of the given type, and
833 * then advances to the next position.
835 function expect( $type ) {
836 if ( $this->currentToken
&& $this->currentToken
->type
== $type ) {
837 return $this->nextToken();
839 $this->error( "expected " . $this->getTypeName( $type ) .
840 ", got " . $this->getTypeName( $this->currentToken
->type
) );
845 * Push a state or two on to the state stack.
847 function pushState( $nextState, $stateAfterThat = null ) {
848 if ( $stateAfterThat !== null ) {
849 $this->stateStack
[] = $stateAfterThat;
851 $this->stateStack
[] = $nextState;
855 * Pop a state from the state stack.
858 function popState() {
859 return array_pop( $this->stateStack
);
863 * Returns true if the user input path is valid.
864 * This exists to allow "/" and "@" to be reserved for string path keys
867 function validatePath( $path ) {
868 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
872 * Internal function to update some things at the end of a path region. Do
873 * not call except from popPath() or nextPath().
877 foreach ( $this->pathStack
as $pathInfo ) {
881 $key .= $pathInfo['name'];
883 $pathInfo['endByte'] = $this->byteNum
;
884 $pathInfo['endToken'] = $this->pos
;
885 $this->pathInfo
[$key] = $pathInfo;
889 * Go up to a new path level, for example at the start of an array.
891 function pushPath( $path ) {
892 $this->pathStack
[] = array(
894 'level' => count( $this->pathStack
) +
1,
895 'startByte' => $this->byteNum
,
896 'startToken' => $this->pos
,
897 'valueStartToken' => false,
898 'valueStartByte' => false,
899 'valueEndToken' => false,
900 'valueEndByte' => false,
901 'nextArrayIndex' => 0,
908 * Go down a path level, for example at the end of an array.
912 array_pop( $this->pathStack
);
916 * Go to the next path on the same level. This ends the current path and
917 * starts a new one. If $path is \@next, the new path is set to the next
918 * numeric array element.
920 function nextPath( $path ) {
922 $i = count( $this->pathStack
) - 1;
923 if ( $path == '@next' ) {
924 $nextArrayIndex =& $this->pathStack
[$i]['nextArrayIndex'];
925 $this->pathStack
[$i]['name'] = $nextArrayIndex;
928 $this->pathStack
[$i]['name'] = $path;
930 $this->pathStack
[$i] =
932 'startByte' => $this->byteNum
,
933 'startToken' => $this->pos
,
934 'valueStartToken' => false,
935 'valueStartByte' => false,
936 'valueEndToken' => false,
937 'valueEndByte' => false,
939 'arrowByte' => false,
940 ) +
$this->pathStack
[$i];
944 * Mark the start of the value part of a path.
946 function startPathValue() {
947 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
948 $path['valueStartToken'] = $this->pos
;
949 $path['valueStartByte'] = $this->byteNum
;
953 * Mark the end of the value part of a path.
955 function endPathValue() {
956 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
957 $path['valueEndToken'] = $this->pos
;
958 $path['valueEndByte'] = $this->byteNum
;
962 * Mark the comma separator in an array element
964 function markComma() {
965 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
966 $path['hasComma'] = true;
970 * Mark the arrow separator in an associative array element
972 function markArrow() {
973 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
974 $path['arrowByte'] = $this->byteNum
;
978 * Generate a parse error
980 function error( $msg ) {
981 throw new ConfEditorParseError( $this, $msg );
985 * Get a readable name for the given token type.
988 function getTypeName( $type ) {
989 if ( is_int( $type ) ) {
990 return token_name( $type );
997 * Looks ahead to see if the given type is the next token type, starting
998 * from the current position plus the given offset. Skips any intervening
1002 function isAhead( $type, $offset = 0 ) {
1004 $token = $this->getTokenAhead( $offset );
1005 while ( !$token->isEnd() ) {
1006 if ( $token->isSkip() ) {
1008 $token = $this->getTokenAhead( $ahead );
1010 } elseif ( $token->type
== $type ) {
1022 * Get the previous token object
1024 function prevToken() {
1025 return $this->prevToken
;
1029 * Echo a reasonably readable representation of the tokenizer array.
1031 function dumpTokens() {
1033 foreach ( $this->tokens
as $token ) {
1034 $obj = $this->newTokenObj( $token );
1035 $out .= sprintf( "%-28s %s\n",
1036 $this->getTypeName( $obj->type
),
1037 addcslashes( $obj->text
, "\0..\37" ) );
1039 echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
1044 * Exception class for parse errors
1046 class ConfEditorParseError
extends MWException
{
1047 var $lineNum, $colNum;
1048 function __construct( $editor, $msg ) {
1049 $this->lineNum
= $editor->lineNum
;
1050 $this->colNum
= $editor->colNum
;
1051 parent
::__construct( "Parse error on line {$editor->lineNum} " .
1052 "col {$editor->colNum}: $msg" );
1055 function highlight( $text ) {
1056 $lines = StringUtils
::explode( "\n", $text );
1057 foreach ( $lines as $lineNum => $line ) {
1058 if ( $lineNum == $this->lineNum
- 1 ) {
1059 return "$line\n" . str_repeat( ' ', $this->colNum
- 1 ) . "^\n";
1068 * Class to wrap a token from the tokenizer.
1070 class ConfEditorToken
{
1073 static $scalarTypes = array( T_LNUMBER
, T_DNUMBER
, T_STRING
, T_CONSTANT_ENCAPSED_STRING
);
1074 static $skipTypes = array( T_WHITESPACE
, T_COMMENT
, T_DOC_COMMENT
);
1076 static function newEnd() {
1077 return new self( 'END', '' );
1080 function __construct( $type, $text ) {
1081 $this->type
= $type;
1082 $this->text
= $text;
1086 return in_array( $this->type
, self
::$skipTypes );
1089 function isScalar() {
1090 return in_array( $this->type
, self
::$scalarTypes );
1094 return $this->type
== 'END';