4 * This is a state machine style parser with two internal stacks:
5 * * A next state stack, which determines the state the machine will progress to next
6 * * A path stack, which keeps track of the logical location in the file.
10 * file = T_OPEN_TAG *statement
11 * statement = T_VARIABLE "=" expression ";"
12 * expression = array / scalar / T_VARIABLE
13 * array = T_ARRAY "(" [ element *( "," element ) [ "," ] ] ")"
14 * element = assoc-element / expression
15 * assoc-element = scalar T_DOUBLE_ARROW expression
16 * scalar = T_LNUMBER / T_DNUMBER / T_STRING / T_CONSTANT_ENCAPSED_STRING
19 /** The text to parse */
22 /** The token array from token_get_all() */
25 /** The current position in the token array */
28 /** The current 1-based line number */
31 /** The current 1-based column number */
34 /** The current 0-based byte number */
37 /** The current ConfEditorToken object */
40 /** The previous ConfEditorToken object */
44 * The state machine stack. This is an array of strings where the topmost
45 * element will be popped off and become the next parser state.
51 * The path stack is a stack of associative arrays with the following elements:
52 * name The name of top level of the path
53 * level The level (number of elements) of the path
54 * startByte The byte offset of the start of the path
55 * startToken The token offset of the start
56 * endByte The byte offset of thee
57 * endToken The token offset of the end, plus one
58 * valueStartToken The start token offset of the value part
59 * valueStartByte The start byte offset of the value part
60 * valueEndToken The end token offset of the value part, plus one
61 * valueEndByte The end byte offset of the value part, plus one
62 * nextArrayIndex The next numeric array index at this level
63 * hasComma True if the array element ends with a comma
64 * arrowByte The byte offset of the "=>", or false if there isn't one
69 * The elements of the top of the pathStack for every path encountered, indexed
70 * by slash-separated path.
75 * Next serial number for whitespace placeholder paths (@extra-N)
80 * Editor state. This consists of the internal copy/insert operations which
81 * are applied to the source string to obtain the destination string.
86 * Simple entry point for command-line testing
88 static function test( $text ) {
90 $ce = new self( $text );
92 } catch ( ConfEditorParseError
$e ) {
93 return $e->getMessage() . "\n" . $e->highlight( $text );
99 * Construct a new parser
101 public function __construct( $text ) {
106 * Edit the text. Returns the edited text.
107 * @param array $ops Array of operations.
109 * Operations are given as an associative array, with members:
110 * type: One of delete, set, append or insert (required)
111 * path: The path to operate on (required)
112 * key: The array key to insert/append, with PHP quotes
113 * value: The value, with PHP quotes
116 * Deletes an array element or statement with the specified path.
118 * array('type' => 'delete', 'path' => '$foo/bar/baz' )
119 * is equivalent to the runtime PHP code:
120 * unset( $foo['bar']['baz'] );
123 * Sets the value of an array element. If the element doesn't exist, it
124 * is appended to the array. If it does exist, the value is set, with
125 * comments and indenting preserved.
128 * Appends a new element to the end of the array. Adds a trailing comma.
130 * array( 'type' => 'append', 'path', '$foo/bar',
131 * 'key' => 'baz', 'value' => "'x'" )
132 * is like the PHP code:
133 * $foo['bar']['baz'] = 'x';
136 * Insert a new element at the start of the array.
139 public function edit( $ops ) {
142 $this->edits
= array(
143 array( 'copy', 0, strlen( $this->text
) )
145 foreach ( $ops as $op ) {
148 $value = isset( $op['value'] ) ?
$op['value'] : null;
149 $key = isset( $op['key'] ) ?
$op['key'] : null;
153 list( $start, $end ) = $this->findDeletionRegion( $path );
154 $this->replaceSourceRegion( $start, $end, false );
157 if ( isset( $this->pathInfo
[$path] ) ) {
158 list( $start, $end ) = $this->findValueRegion( $path );
159 $encValue = $value; // var_export( $value, true );
160 $this->replaceSourceRegion( $start, $end, $encValue );
163 // No existing path, fall through to append
164 $slashPos = strrpos( $path, '/' );
165 $key = var_export( substr( $path, $slashPos +
1 ), true );
166 $path = substr( $path, 0, $slashPos );
169 // Find the last array element
170 $lastEltPath = $this->findLastArrayElement( $path );
171 if ( $lastEltPath === false ) {
172 throw new MWException( "Can't find any element of array \"$path\"" );
174 $lastEltInfo = $this->pathInfo
[$lastEltPath];
176 // Has it got a comma already?
177 if ( strpos( $lastEltPath, '@extra' ) === false && !$lastEltInfo['hasComma'] ) {
178 // No comma, insert one after the value region
179 list( $start, $end ) = $this->findValueRegion( $lastEltPath );
180 $this->replaceSourceRegion( $end - 1, $end - 1, ',' );
183 // Make the text to insert
184 list( $start, $end ) = $this->findDeletionRegion( $lastEltPath );
186 if ( $key === null ) {
187 list( $indent, $arrowIndent ) = $this->getIndent( $start );
188 $textToInsert = "$indent$value,";
190 list( $indent, $arrowIndent ) =
191 $this->getIndent( $start, $key, $lastEltInfo['arrowByte'] );
192 $textToInsert = "$indent$key$arrowIndent=> $value,";
194 $textToInsert .= ( $indent === false ?
' ' : "\n" );
197 $this->replaceSourceRegion( $end, $end, $textToInsert );
200 // Find first array element
201 $firstEltPath = $this->findFirstArrayElement( $path );
202 if ( $firstEltPath === false ) {
203 throw new MWException( "Can't find array element of \"$path\"" );
205 list( $start, $end ) = $this->findDeletionRegion( $firstEltPath );
206 $info = $this->pathInfo
[$firstEltPath];
208 // Make the text to insert
209 if ( $key === null ) {
210 list( $indent, $arrowIndent ) = $this->getIndent( $start );
211 $textToInsert = "$indent$value,";
213 list( $indent, $arrowIndent ) =
214 $this->getIndent( $start, $key, $info['arrowByte'] );
215 $textToInsert = "$indent$key$arrowIndent=> $value,";
217 $textToInsert .= ( $indent === false ?
' ' : "\n" );
220 $this->replaceSourceRegion( $start, $start, $textToInsert );
223 throw new MWException( "Unrecognised operation: \"$type\"" );
229 foreach ( $this->edits
as $edit ) {
230 if ( $edit[0] == 'copy' ) {
231 $out .= substr( $this->text
, $edit[1], $edit[2] - $edit[1] );
232 } else { // if ( $edit[0] == 'insert' )
237 // Do a second parse as a sanity check
241 } catch ( ConfEditorParseError
$e ) {
242 throw new MWException(
243 "Sorry, ConfEditor broke the file during editing and it won't parse anymore: " .
250 * Get the variables defined in the text
251 * @return array( varname => value )
256 foreach( $this->pathInfo
as $path => $data ) {
257 if ( $path[0] != '$' )
259 $trimmedPath = substr( $path, 1 );
260 $name = $data['name'];
261 if ( $name[0] == '@' )
263 if ( $name[0] == '$' )
264 $name = substr( $name, 1 );
265 $parentPath = substr( $trimmedPath, 0,
266 strlen( $trimmedPath ) - strlen( $name ) );
267 if( substr( $parentPath, -1 ) == '/' )
268 $parentPath = substr( $parentPath, 0, -1 );
270 $value = substr( $this->text
, $data['valueStartByte'],
271 $data['valueEndByte'] - $data['valueStartByte']
273 $this->setVar( $vars, $parentPath, $name,
274 $this->parseScalar( $value ) );
280 * Set a value in an array, unless it's set already. For instance,
281 * setVar( $arr, 'foo/bar', 'baz', 3 ); will set
282 * $arr['foo']['bar']['baz'] = 3;
283 * @param $array array
284 * @param $path string slash-delimited path
285 * @param $key mixed Key
286 * @param $value mixed Value
288 function setVar( &$array, $path, $key, $value ) {
289 $pathArr = explode( '/', $path );
291 if ( $path !== '' ) {
292 foreach ( $pathArr as $p ) {
293 if( !isset( $target[$p] ) )
294 $target[$p] = array();
295 $target =& $target[$p];
298 if ( !isset( $target[$key] ) )
299 $target[$key] = $value;
303 * Parse a scalar value in PHP
304 * @return mixed Parsed value
306 function parseScalar( $str ) {
307 if ( $str !== '' && $str[0] == '\'' )
308 // Single-quoted string
309 return strtr( substr( $str, 1, -1 ),
310 array( '\\\'' => '\'', '\\\\' => '\\' ) );
311 if ( $str !== '' && @$str[0] == '"' )
312 // Double-quoted string
313 return stripcslashes( substr( $str, 1, -1 ) );
314 if ( substr( $str, 0, 4 ) == 'true' )
316 if ( substr( $str, 0, 5 ) == 'false' )
318 if ( substr( $str, 0, 4 ) == 'null' )
320 // Must be some kind of numeric value, so let PHP's weak typing
321 // be useful for a change
326 * Replace the byte offset region of the source with $newText.
327 * Works by adding elements to the $this->edits array.
329 function replaceSourceRegion( $start, $end, $newText = false ) {
330 // Split all copy operations with a source corresponding to the region
333 foreach ( $this->edits
as $i => $edit ) {
334 if ( $edit[0] !== 'copy' ) {
338 $copyStart = $edit[1];
340 if ( $start >= $copyEnd ||
$end <= $copyStart ) {
341 // Outside this region
345 if ( ( $start < $copyStart && $end > $copyStart )
346 ||
( $start < $copyEnd && $end > $copyEnd )
348 throw new MWException( "Overlapping regions found, can't do the edit" );
351 $newEdits[] = array( 'copy', $copyStart, $start );
352 if ( $newText !== false ) {
353 $newEdits[] = array( 'insert', $newText );
355 $newEdits[] = array( 'copy', $end, $copyEnd );
357 $this->edits
= $newEdits;
361 * Finds the source byte region which you would want to delete, if $pathName
362 * was to be deleted. Includes the leading spaces and tabs, the trailing line
363 * break, and any comments in between.
365 function findDeletionRegion( $pathName ) {
366 if ( !isset( $this->pathInfo
[$pathName] ) ) {
367 throw new MWException( "Can't find path \"$pathName\"" );
369 $path = $this->pathInfo
[$pathName];
372 while ( $this->pos
!= $path['startToken'] ) {
375 $regionStart = $path['startByte'];
376 for ( $offset = -1; $offset >= -$this->pos
; $offset-- ) {
377 $token = $this->getTokenAhead( $offset );
378 if ( !$token->isSkip() ) {
379 // If there is other content on the same line, don't move the start point
380 // back, because that will cause the regions to overlap.
381 $regionStart = $path['startByte'];
384 $lfPos = strrpos( $token->text
, "\n" );
385 if ( $lfPos === false ) {
386 $regionStart -= strlen( $token->text
);
388 // The line start does not include the LF
389 $regionStart -= strlen( $token->text
) - $lfPos - 1;
394 while ( $this->pos
!= $path['endToken'] ) {
397 $regionEnd = $path['endByte']; // past the end
398 for ( $offset = 0; $offset < count( $this->tokens
) - $this->pos
; $offset++
) {
399 $token = $this->getTokenAhead( $offset );
400 if ( !$token->isSkip() ) {
403 $lfPos = strpos( $token->text
, "\n" );
404 if ( $lfPos === false ) {
405 $regionEnd +
= strlen( $token->text
);
407 // This should point past the LF
408 $regionEnd +
= $lfPos +
1;
412 return array( $regionStart, $regionEnd );
416 * Find the byte region in the source corresponding to the value part.
417 * This includes the quotes, but does not include the trailing comma
420 * The end position is the past-the-end (end + 1) value as per convention.
422 function findValueRegion( $pathName ) {
423 if ( !isset( $this->pathInfo
[$pathName] ) ) {
424 throw new MWEXception( "Can't find path \"$pathName\"" );
426 $path = $this->pathInfo
[$pathName];
427 if ( $path['valueStartByte'] === false ||
$path['valueEndByte'] === false ) {
428 throw new MWException( "Can't find value region for path \"$pathName\"" );
430 return array( $path['valueStartByte'], $path['valueEndByte'] );
434 * Find the path name of the last element in the array.
435 * If the array is empty, this will return the @extra interstitial element.
436 * If the specified path is not found or is not an array, it will return false.
438 function findLastArrayElement( $path ) {
439 // Try for a real element
440 $lastEltPath = false;
441 foreach ( $this->pathInfo
as $candidatePath => $info ) {
442 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
443 $part2 = substr( $candidatePath, strlen( $path ) +
1, 1 );
444 if ( $part2 == '@' ) {
446 } elseif ( $part1 == "$path/" ) {
447 $lastEltPath = $candidatePath;
448 } elseif ( $lastEltPath !== false ) {
452 if ( $lastEltPath !== false ) {
456 // Try for an interstitial element
458 foreach ( $this->pathInfo
as $candidatePath => $info ) {
459 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
460 if ( $part1 == "$path/" ) {
461 $extraPath = $candidatePath;
462 } elseif ( $extraPath !== false ) {
470 * Find the path name of first element in the array.
471 * If the array is empty, this will return the @extra interstitial element.
472 * If the specified path is not found or is not an array, it will return false.
474 function findFirstArrayElement( $path ) {
475 // Try for an ordinary element
476 foreach ( $this->pathInfo
as $candidatePath => $info ) {
477 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
478 $part2 = substr( $candidatePath, strlen( $path ) +
1, 1 );
479 if ( $part1 == "$path/" && $part2 != '@' ) {
480 return $candidatePath;
484 // Try for an interstitial element
485 foreach ( $this->pathInfo
as $candidatePath => $info ) {
486 $part1 = substr( $candidatePath, 0, strlen( $path ) +
1 );
487 if ( $part1 == "$path/" ) {
488 return $candidatePath;
495 * Get the indent string which sits after a given start position.
496 * Returns false if the position is not at the start of the line.
498 function getIndent( $pos, $key = false, $arrowPos = false ) {
500 if ( $pos == 0 ||
$this->text
[$pos-1] == "\n" ) {
501 $indentLength = strspn( $this->text
, " \t", $pos );
502 $indent = substr( $this->text
, $pos, $indentLength );
506 if ( $indent !== false && $arrowPos !== false ) {
507 $textToInsert = "$indent$key ";
508 $arrowIndentLength = $arrowPos - $pos - $indentLength - strlen( $key );
509 if ( $arrowIndentLength > 0 ) {
510 $arrowIndent = str_repeat( ' ', $arrowIndentLength );
513 return array( $indent, $arrowIndent );
517 * Run the parser on the text. Throws an exception if the string does not
518 * match our defined subset of PHP syntax.
520 public function parse() {
522 $this->pushState( 'file' );
523 $this->pushPath( '@extra-' . ($this->serial++
) );
524 $token = $this->firstToken();
526 while ( !$token->isEnd() ) {
527 $state = $this->popState();
529 $this->error( 'internal error: empty state stack' );
534 $token = $this->expect( T_OPEN_TAG
);
535 $token = $this->skipSpace();
536 if ( $token->isEnd() ) {
539 $this->pushState( 'statement', 'file 2' );
542 $token = $this->skipSpace();
543 if ( $token->isEnd() ) {
546 $this->pushState( 'statement', 'file 2' );
549 $token = $this->skipSpace();
550 if ( !$this->validatePath( $token->text
) ) {
551 $this->error( "Invalid variable name \"{$token->text}\"" );
553 $this->nextPath( $token->text
);
554 $this->expect( T_VARIABLE
);
556 $arrayAssign = false;
557 if ( $this->currentToken()->type
== '[' ) {
559 $token = $this->skipSpace();
560 if ( !$token->isScalar() ) {
561 $this->error( "expected a string or number for the array key" );
563 if ( $token->type
== T_CONSTANT_ENCAPSED_STRING
) {
564 $text = $this->parseScalar( $token->text
);
566 $text = $token->text
;
568 if ( !$this->validatePath( $text ) ) {
569 $this->error( "Invalid associative array name \"$text\"" );
571 $this->pushPath( $text );
574 $this->expect( ']' );
578 $this->expect( '=' );
580 $this->startPathValue();
582 $this->pushState( 'expression', 'array assign end' );
584 $this->pushState( 'expression', 'statement end' );
586 case 'array assign end':
587 case 'statement end':
588 $this->endPathValue();
589 if ( $state == 'array assign end' )
592 $this->expect( ';' );
593 $this->nextPath( '@extra-' . ($this->serial++
) );
596 $token = $this->skipSpace();
597 if ( $token->type
== T_ARRAY
) {
598 $this->pushState( 'array' );
599 } elseif ( $token->isScalar() ) {
601 } elseif ( $token->type
== T_VARIABLE
) {
604 $this->error( "expected simple expression" );
609 $this->expect( T_ARRAY
);
611 $this->expect( '(' );
613 $this->pushPath( '@extra-' . ($this->serial++
) );
614 if ( $this->isAhead( ')' ) ) {
616 $this->pushState( 'array end' );
618 $this->pushState( 'element', 'array end' );
624 $this->expect( ')' );
627 $token = $this->skipSpace();
628 // Look ahead to find the double arrow
629 if ( $token->isScalar() && $this->isAhead( T_DOUBLE_ARROW
, 1 ) ) {
630 // Found associative element
631 $this->pushState( 'assoc-element', 'element end' );
634 $this->nextPath( '@next' );
635 $this->startPathValue();
636 $this->pushState( 'expression', 'element end' );
640 $token = $this->skipSpace();
641 if ( $token->type
== ',' ) {
642 $this->endPathValue();
645 $this->nextPath( '@extra-' . ($this->serial++
) );
646 // Look ahead to find ending bracket
647 if ( $this->isAhead( ")" ) ) {
648 // Found ending bracket, no continuation
651 // No ending bracket, continue to next element
652 $this->pushState( 'element' );
654 } elseif ( $token->type
== ')' ) {
656 $this->endPathValue();
658 $this->error( "expected the next array element or the end of the array" );
661 case 'assoc-element':
662 $token = $this->skipSpace();
663 if ( !$token->isScalar() ) {
664 $this->error( "expected a string or number for the array key" );
666 if ( $token->type
== T_CONSTANT_ENCAPSED_STRING
) {
667 $text = $this->parseScalar( $token->text
);
669 $text = $token->text
;
671 if ( !$this->validatePath( $text ) ) {
672 $this->error( "Invalid associative array name \"$text\"" );
674 $this->nextPath( $text );
678 $this->expect( T_DOUBLE_ARROW
);
680 $this->startPathValue();
681 $this->pushState( 'expression' );
685 if ( count( $this->stateStack
) ) {
686 $this->error( 'unexpected end of file' );
692 * Initialise a parse.
694 protected function initParse() {
695 $this->tokens
= token_get_all( $this->text
);
696 $this->stateStack
= array();
697 $this->pathStack
= array();
699 $this->pathInfo
= array();
704 * Set the parse position. Do not call this except from firstToken() and
705 * nextToken(), there is more to update than just the position.
707 protected function setPos( $pos ) {
709 if ( $this->pos
>= count( $this->tokens
) ) {
710 $this->currentToken
= ConfEditorToken
::newEnd();
712 $this->currentToken
= $this->newTokenObj( $this->tokens
[$this->pos
] );
714 return $this->currentToken
;
718 * Create a ConfEditorToken from an element of token_get_all()
720 function newTokenObj( $internalToken ) {
721 if ( is_array( $internalToken ) ) {
722 return new ConfEditorToken( $internalToken[0], $internalToken[1] );
724 return new ConfEditorToken( $internalToken, $internalToken );
729 * Reset the parse position
731 function firstToken() {
733 $this->prevToken
= ConfEditorToken
::newEnd();
737 return $this->currentToken
;
741 * Get the current token
743 function currentToken() {
744 return $this->currentToken
;
748 * Advance the current position and return the resulting next token
750 function nextToken() {
751 if ( $this->currentToken
) {
752 $text = $this->currentToken
->text
;
753 $lfCount = substr_count( $text, "\n" );
755 $this->lineNum +
= $lfCount;
756 $this->colNum
= strlen( $text ) - strrpos( $text, "\n" );
758 $this->colNum +
= strlen( $text );
760 $this->byteNum +
= strlen( $text );
762 $this->prevToken
= $this->currentToken
;
763 $this->setPos( $this->pos +
1 );
764 return $this->currentToken
;
768 * Get the token $offset steps ahead of the current position.
769 * $offset may be negative, to get tokens behind the current position.
771 function getTokenAhead( $offset ) {
772 $pos = $this->pos +
$offset;
773 if ( $pos >= count( $this->tokens
) ||
$pos < 0 ) {
774 return ConfEditorToken
::newEnd();
776 return $this->newTokenObj( $this->tokens
[$pos] );
781 * Advances the current position past any whitespace or comments
783 function skipSpace() {
784 while ( $this->currentToken
&& $this->currentToken
->isSkip() ) {
787 return $this->currentToken
;
791 * Throws an error if the current token is not of the given type, and
792 * then advances to the next position.
794 function expect( $type ) {
795 if ( $this->currentToken
&& $this->currentToken
->type
== $type ) {
796 return $this->nextToken();
798 $this->error( "expected " . $this->getTypeName( $type ) .
799 ", got " . $this->getTypeName( $this->currentToken
->type
) );
804 * Push a state or two on to the state stack.
806 function pushState( $nextState, $stateAfterThat = null ) {
807 if ( $stateAfterThat !== null ) {
808 $this->stateStack
[] = $stateAfterThat;
810 $this->stateStack
[] = $nextState;
814 * Pop a state from the state stack.
816 function popState() {
817 return array_pop( $this->stateStack
);
821 * Returns true if the user input path is valid.
822 * This exists to allow "/" and "@" to be reserved for string path keys
824 function validatePath( $path ) {
825 return strpos( $path, '/' ) === false && substr( $path, 0, 1 ) != '@';
829 * Internal function to update some things at the end of a path region. Do
830 * not call except from popPath() or nextPath().
833 $i = count( $this->pathStack
) - 1;
835 foreach ( $this->pathStack
as $pathInfo ) {
839 $key .= $pathInfo['name'];
841 $pathInfo['endByte'] = $this->byteNum
;
842 $pathInfo['endToken'] = $this->pos
;
843 $this->pathInfo
[$key] = $pathInfo;
847 * Go up to a new path level, for example at the start of an array.
849 function pushPath( $path ) {
850 $this->pathStack
[] = array(
852 'level' => count( $this->pathStack
) +
1,
853 'startByte' => $this->byteNum
,
854 'startToken' => $this->pos
,
855 'valueStartToken' => false,
856 'valueStartByte' => false,
857 'valueEndToken' => false,
858 'valueEndByte' => false,
859 'nextArrayIndex' => 0,
866 * Go down a path level, for example at the end of an array.
870 array_pop( $this->pathStack
);
874 * Go to the next path on the same level. This ends the current path and
875 * starts a new one. If $path is @next, the new path is set to the next
876 * numeric array element.
878 function nextPath( $path ) {
880 $i = count( $this->pathStack
) - 1;
881 if ( $path == '@next' ) {
882 $nextArrayIndex =& $this->pathStack
[$i]['nextArrayIndex'];
883 $this->pathStack
[$i]['name'] = $nextArrayIndex;
886 $this->pathStack
[$i]['name'] = $path;
888 $this->pathStack
[$i] =
890 'startByte' => $this->byteNum
,
891 'startToken' => $this->pos
,
892 'valueStartToken' => false,
893 'valueStartByte' => false,
894 'valueEndToken' => false,
895 'valueEndByte' => false,
897 'arrowByte' => false,
898 ) +
$this->pathStack
[$i];
902 * Mark the start of the value part of a path.
904 function startPathValue() {
905 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
906 $path['valueStartToken'] = $this->pos
;
907 $path['valueStartByte'] = $this->byteNum
;
911 * Mark the end of the value part of a path.
913 function endPathValue() {
914 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
915 $path['valueEndToken'] = $this->pos
;
916 $path['valueEndByte'] = $this->byteNum
;
920 * Mark the comma separator in an array element
922 function markComma() {
923 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
924 $path['hasComma'] = true;
928 * Mark the arrow separator in an associative array element
930 function markArrow() {
931 $path =& $this->pathStack
[count( $this->pathStack
) - 1];
932 $path['arrowByte'] = $this->byteNum
;
936 * Generate a parse error
938 function error( $msg ) {
939 throw new ConfEditorParseError( $this, $msg );
943 * Get a readable name for the given token type.
945 function getTypeName( $type ) {
946 if ( is_int( $type ) ) {
947 return token_name( $type );
954 * Looks ahead to see if the given type is the next token type, starting
955 * from the current position plus the given offset. Skips any intervening
958 function isAhead( $type, $offset = 0 ) {
960 $token = $this->getTokenAhead( $offset );
961 while ( !$token->isEnd() ) {
962 if ( $token->isSkip() ) {
964 $token = $this->getTokenAhead( $ahead );
966 } elseif ( $token->type
== $type ) {
978 * Get the previous token object
980 function prevToken() {
981 return $this->prevToken
;
985 * Echo a reasonably readable representation of the tokenizer array.
987 function dumpTokens() {
989 foreach ( $this->tokens
as $token ) {
990 $obj = $this->newTokenObj( $token );
991 $out .= sprintf( "%-28s %s\n",
992 $this->getTypeName( $obj->type
),
993 addcslashes( $obj->text
, "\0..\37" ) );
995 echo "<pre>" . htmlspecialchars( $out ) . "</pre>";
1000 * Exception class for parse errors
1002 class ConfEditorParseError
extends MWException
{
1003 var $lineNum, $colNum;
1004 function __construct( $editor, $msg ) {
1005 $this->lineNum
= $editor->lineNum
;
1006 $this->colNum
= $editor->colNum
;
1007 parent
::__construct( "Parse error on line {$editor->lineNum} " .
1008 "col {$editor->colNum}: $msg" );
1011 function highlight( $text ) {
1012 $lines = StringUtils
::explode( "\n", $text );
1013 foreach ( $lines as $lineNum => $line ) {
1014 if ( $lineNum == $this->lineNum
- 1 ) {
1015 return "$line\n" .str_repeat( ' ', $this->colNum
- 1 ) . "^\n";
1023 * Class to wrap a token from the tokenizer.
1025 class ConfEditorToken
{
1028 static $scalarTypes = array( T_LNUMBER
, T_DNUMBER
, T_STRING
, T_CONSTANT_ENCAPSED_STRING
);
1029 static $skipTypes = array( T_WHITESPACE
, T_COMMENT
, T_DOC_COMMENT
);
1031 static function newEnd() {
1032 return new self( 'END', '' );
1035 function __construct( $type, $text ) {
1036 $this->type
= $type;
1037 $this->text
= $text;
1041 return in_array( $this->type
, self
::$skipTypes );
1044 function isScalar() {
1045 return in_array( $this->type
, self
::$scalarTypes );
1049 return $this->type
== 'END';