'&&', 'or' => '||', 'is' => '==', 'isnt' => '!=', 'not' => '!', 'yes' => 'true', 'no' => 'false', 'on' => 'true', 'off' => 'false' ); static $COFFEE_KEYWORDS = array( 'by', 'loop', 'of', 'then', 'undefined', 'unless', 'until', 'when' ); // exports.RESERVED. static $COFFEE_RESERVED = array(); static $JS_KEYWORDS = array( 'break', 'catch', 'class', 'continue', 'debugger', 'delete', 'do', 'else', 'extends', 'false', 'finally', 'for', 'if', 'in', 'instanceof', 'new', 'null', 'this', 'throw', 'typeof', 'return', 'switch', 'super', 'true', 'try', 'while', ); // RESERVED. static $JS_RESERVED = array( '__bind', '__extends', '__hasProp', '__indexOf', '__slice', 'case', 'const', 'default', 'enum', 'export', 'function', 'implements', 'import', 'interface', 'let', 'native', 'package', 'protected', 'private', 'public', 'static', 'var', 'void', 'with', 'yield', ); static $STRICT_PROSCRIBED = array('arguments', 'eval'); static $JS_FORBIDDEN = array(); static $CODE = '/^[-=]>/'; static $COMMENT = '/^###([^#][\s\S]*?)(?:###[^\n\S]*|(?:###)?$)|^(?:\s*#(?!##[^#]).*)+/'; static $HEREDOC = '/^("""|\'\'\')([\s\S]*?)(?:\n[^\n\S]*)?\1/'; static $HEREDOC_INDENT = '/\n+([^\n\S]*)/'; static $HEREDOC_ILLEGAL = '%\*/%'; static $HEREGEX = '%^/{3}([\s\S]+?)/{3}([imgy]{0,4})(?!\w)%'; static $HEREGEX_OMIT = '/\s+(?:#.*)?/'; static $IDENTIFIER = '/^([$A-Za-z_\x7f-\x{ffff}][$\w\x7f-\x{ffff}]*)([^\n\S]*:(?!:))?/u'; static $JSTOKEN = '/^`[^\\\\`]*(?:\\\\.[^\\\\`]*)*`/'; static $LINE_CONTINUER = '/^\s*(?:,|\??\.(?![.\d])|::)/'; static $MULTI_DENT = '/^(?:\n[^\n\S]*)+/'; static $MULTILINER = '/\n/'; static $NUMBER = '/^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i'; static $OPERATOR = '#^(?:[-=]>|[-+*/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>])\2=?|\?\.|\.{2,3})#'; static $REGEX = '%^(/(?![\s=])[^[/\n\\\\]*(?:(?:\\\\[\s\S]|\[[^\]\n\\\\]*(?:\\\\[\s\S][^\]\n\\\\]*)*\])[^[/\n\\\\]*)*/)([imgy]{0,4})(?!\w)%'; static $SIMPLESTR = '/^\'[^\\\\\']*(?:\\\\.[^\\\\\']*)*\'/i'; static $TRAILING_SPACES = '/\s+$/'; static $WHITESPACE = '/^[^\n\S]+/'; static $BOOL = array('TRUE', 'FALSE', 'NULL', 'UNDEFINED'); static $CALLABLE = array('IDENTIFIER', 'STRING', 'REGEX', ')', ']', '}', '?', '::', '@', 'THIS', 'SUPER'); static $COMPARE = array('==', '!=', '<', '>', '<=', '>='); static $COMPOUND_ASSIGN = array('-=', '+=', '/=', '*=', '%=', '||=', '&&=', '?=', '<<=', '>>=', '>>>=', '&=', '^=', '|=' ); static $INDEXABLE = array('NUMBER', 'BOOL'); static $LINE_BREAK = array('INDENT', 'OUTDENT', 'TERMINATOR'); static $LOGIC = array('&&', '||', '&', '|', '^'); static $MATH = array('*', '/', '%'); static $NOT_REGEX = array('NUMBER', 'REGEX', 'BOOL', '++', '--', ']'); static $NOT_SPACED_REGEX = array(')', '}', 'THIS', 'IDENTIFIER', 'STRING'); static $RELATION = array('IN', 'OF', 'INSTANCEOF'); static $SHIFT = array('<<', '>>', '>>>'); static $UNARY = array('!', '~', 'NEW', 'TYPEOF', 'DELETE', 'DO'); static $INVERSES = array(); static $initialized = FALSE; /** * Initialize some static variables (called at the end of this file). */ static function init() { if (self::$initialized) return; self::$initialized = TRUE; self::$COFFEE_KEYWORDS = array_merge(self::$COFFEE_KEYWORDS, array_keys(self::$COFFEE_ALIASES)); self::$COFFEE_RESERVED = array_merge(self::$JS_RESERVED, self::$JS_KEYWORDS, self::$COFFEE_KEYWORDS, self::$STRICT_PROSCRIBED); self::$JS_FORBIDDEN = array_merge(self::$JS_KEYWORDS, self::$JS_RESERVED, self::$STRICT_PROSCRIBED); self::$INDEXABLE = array_merge(self::$CALLABLE, self::$INDEXABLE); self::$NOT_SPACED_REGEX = array_merge(self::$NOT_REGEX, self::$NOT_SPACED_REGEX); Rewriter::init(); self::$INVERSES = Rewriter::$INVERSES; } /** * In Jison, token tags can be represented simply using strings, whereas with * ParserGenerator (a port of Lemon) we're stuck using numeric constants for * everything. * * This static function maps those string representations to their numeric constants, * making it easier to port directly from the CoffeeScript source. */ static function t($name) { static $map = array( '.' => 'ACCESSOR', '[' => 'ARRAY_START', ']' => 'ARRAY_END', '@' => 'AT_SIGN', '=>' => 'BOUND_FUNC', ':' => 'COLON', ',' => 'COMMA', '--' => 'DECREMENT', '=' => 'EQUALS', '?' => 'EXISTENTIAL', '?.' => 'EXISTENTIAL_ACCESSOR', '->' => 'FUNC', '++' => 'INCREMENT', '&' => 'LOGIC', '&&' => 'LOGIC', '||' => 'LOGIC', '-' => 'MINUS', '{' => 'OBJECT_START', '}' => 'OBJECT_END', '(' => 'PAREN_START', ')' => 'PAREN_END', '+' => 'PLUS', '::' => 'PROTOTYPE', '...' => 'RANGE_EXCLUSIVE', '..' => 'RANGE_INCLUSIVE', ); if (is_array($name) || (func_num_args() > 1 && $name = func_get_args())) { $tags = array(); foreach ($name as $v) { $tags[] = t($v); } return $tags; } $name = 'CoffeeScript\Parser::YY_'.(isset($map[$name]) ? $map[$name] : $name); // Don't return the original name if there's no matching constant, in some // cases intermediate token types are created and the value returned by this // static function still needs to be unique. return defined($name) ? constant($name) : $name; } /** * Change a CoffeeScript PHP token tag to it's equivalent canonical form (the * form used in the JavaScript version). * * This static function is used for testing purposes only. */ static function t_canonical($token) { static $map = array( 'ACCESSOR' => '.', // These are separate from INDEX_START and INDEX_END. 'ARRAY_START' => '[', 'ARRAY_END' => ']', 'AT_SIGN' => '@', 'BOUND_FUNC' => '=>', 'COLON' => ':', 'COMMA' => ',', 'DECREMENT' => '--', 'EQUALS' => '=', 'EXISTENTIAL' => '?', 'EXISTENTIAL_ACCESSOR' => '?.', 'FUNC' => '->', 'INCREMENT' => '++', 'MINUS' => '-', 'OBJECT_START' => '{', 'OBJECT_END' => '}', // These are separate from CALL_START and CALL_END. 'PAREN_START' => '(', 'PAREN_END' => ')', 'PLUS' => '+', 'PROTOTYPE' => '::', 'RANGE_EXCLUSIVE' => '...', 'RANGE_INCLUSIVE' => '..' ); if (is_array($token)) { if (is_array($token[0])) { foreach ($token as & $t) { $t = t_canonical($t); } } else { // Single token. $token[0] = t_canonical($token[0]); if (is_object($token[1])) { $str = "< {$token[1]} "; foreach ($token[1] as $k => $v) { if ($k !== 'v' && $v) { $str.= $k.' '; } } $token[1] = $str.'>'; } } return $token; } else if (is_numeric($token)) { $token = substr(Parser::tokenName($token), 3); } else if (is_string($token)) { // The token type isn't known to the parser, so t() returned a unique // string to use instead. $token = substr($token, strlen('CoffeeScript\Parser::YY_')); } return isset($map[$token]) ? $map[$token] : $token; } function __construct($code, $options) { self::init(); if (preg_match(self::$WHITESPACE, $code)) { $code = "\n{$code}"; } $code = preg_replace(self::$TRAILING_SPACES, '', str_replace("\r", '', $code)); $options = array_merge(array( 'indent' => 0, 'index' => 0, 'line' => 0, 'rewrite' => TRUE ), $options); $this->code = $code; $this->chunk = $code; $this->ends = array(); $this->indent = 0; $this->indents = array(); $this->indebt = 0; $this->index = $options['index']; $this->length = strlen($this->code); $this->line = $options['line']; $this->outdebt = 0; $this->options = $options; $this->tokens = array(); } function balanced_string($str, $end) { $continue_count = 0; $stack = array($end); $prev = NULL; $len = strlen($str); for ($i = 1; $i < $len; $i++) { if ($continue_count) { --$continue_count; continue; } switch ($letter = $str{$i}) { case '\\': ++$continue_count; continue 2; case $end: array_pop($stack); if (count($stack) === 0) { return substr($str, 0, $i + 1); } $end = $stack[count($stack) - 1]; continue 2; } if ($end === '}' && ($letter === '"' || $letter === "'")) { $stack[] = $end = $letter; } else if ($end === '}' && $letter === '/' && (preg_match(self::$HEREGEX, substr($str, $i), $match) || preg_match(self::$REGEX, substr($str, $i), $match))) { $continue_count += strlen($match[0]) - 1; } else if ($end === '}' && $letter === '{') { $stack[] = $end = '}'; } else if ($end === '"' && $prev === '#' && $letter === '{') { $stack[] = $end = '}'; } $prev = $letter; } $this->error('missing '.array_pop($stack).', starting'); } function close_indentation() { $this->outdent_token($this->indent); } function comment_token() { if ( ! preg_match(self::$COMMENT, $this->chunk, $match)) { return 0; } $comment = $match[0]; if (isset($match[1]) && ($here = $match[1])) { $this->token('HERECOMMENT', $this->sanitize_heredoc($here, array( 'herecomment' => TRUE, 'indent' => str_pad('', $this->indent) ))); } $this->line += substr_count($comment, "\n"); return strlen($comment); } function error($message) { throw new SyntaxError($message.' on line '.($this->line + 1)); } function escape_lines($str, $heredoc = NULL) { return preg_replace(self::$MULTILINER, $heredoc ? '\\n' : '', $str); } function heredoc_token() { if ( ! preg_match(self::$HEREDOC, $this->chunk, $match)) { return 0; } $heredoc = $match[0]; $quote = $heredoc{0}; $doc = $this->sanitize_heredoc($match[2], array('quote' => $quote, 'indent' => NULL)); if ($quote === '"' && strpos($doc, '#{') !== FALSE) { $this->interpolate_string($doc, array('heredoc' => TRUE)); } else { $this->token('STRING', $this->make_string($doc, $quote, TRUE)); } $this->line += substr_count($heredoc, "\n"); return strlen($heredoc); } function heregex_token($match) { list($heregex, $body, $flags) = $match; if (strpos($body, '#{') === FALSE) { $re = preg_replace(self::$HEREGEX_OMIT, '', $body); $re = preg_replace('/\//', '\\/', $re); if (preg_match('/^\*/', $re)) { $this->error('regular expressions cannot begin with `*`'); } $this->token('REGEX', '/'.($re ? $re : '(?:)').'/'.$flags); return strlen($heregex); } $this->token('IDENTIFIER', 'RegExp'); $this->tokens[] = array(t('CALL_START'), '('); $tokens = array(); foreach ($this->interpolate_string($body, array('regex' => TRUE)) as $token) { list($tag, $value) = $token; if ($tag === 'TOKENS') { $tokens = array_merge($tokens, (array) $value); } else { if ( ! ($value = preg_replace(self::$HEREGEX_OMIT, '', $value))) { continue; } $value = preg_replace('/\\\\/', '\\\\\\\\', $value); $tokens[] = array(t('STRING'), $this->make_string($value, '"', TRUE)); } $tokens[] = array(t('+'), '+'); } array_pop($tokens); if ( ! (isset($tokens[0]) && $tokens[0][0] === 'STRING')) { array_push($this->tokens, array(t('STRING'), '""'), array(t('+'), '+')); } $this->tokens = array_merge($this->tokens, $tokens); if ($flags) { array_push($this->tokens, array(t(','), ','), array(t('STRING'), "\"{$flags}\"")); } $this->token(')', ')'); return strlen($heregex); } function identifier_token() { if ( ! preg_match(self::$IDENTIFIER, $this->chunk, $match)) { return 0; } list($input, $id) = $match; $colon = isset($match[2]) ? $match[2] : NULL; if ($id === 'own' && $this->tag() === t('FOR')) { $this->token('OWN', $id); return strlen($id); } $forced_identifier = $colon || ($prev = last($this->tokens)) && (in_array($prev[0], t('.', '?.', '::')) || ( ! (isset($prev['spaced']) && $prev['spaced']) && $prev[0] === t('@'))); $tag = 'IDENTIFIER'; if ( ! $forced_identifier and (in_array($id, self::$JS_KEYWORDS) || in_array($id, self::$COFFEE_KEYWORDS))) { $tag = strtoupper($id); if ($tag === 'WHEN' && in_array($this->tag(), t(self::$LINE_BREAK))) { $tag = 'LEADING_WHEN'; } else if ($tag === 'FOR') { $this->seen_for = TRUE; } else if ($tag === 'UNLESS') { $tag = 'IF'; } else if (in_array($tag, self::$UNARY)) { $tag = 'UNARY'; } else if (in_array($tag, self::$RELATION)) { if ($tag !== 'INSTANCEOF' && (isset($this->seen_for) && $this->seen_for)) { $tag = 'FOR'.$tag; $this->seen_for = FALSE; } else { $tag = 'RELATION'; if ($this->value() === '!') { array_pop($this->tokens); $id = '!'. $id; } } } } if (in_array($id, self::$JS_FORBIDDEN, TRUE)) { if ($forced_identifier) { $id = wrap($id); $id->reserved = TRUE; $tag = 'IDENTIFIER'; } else if (in_array($id, self::$JS_RESERVED, TRUE)) { $this->error("reserved word $id"); } } if ( ! $forced_identifier) { if (isset(self::$COFFEE_ALIASES[$id])) { $id = self::$COFFEE_ALIASES[$id]; } $map = array( 'UNARY' => array('!'), 'COMPARE' => array('==', '!='), 'LOGIC' => array('&&', '||'), 'BOOL' => array('true', 'false', 'null', 'undefined'), 'STATEMENT' => array('break', 'continue') ); foreach ($map as $k => $v) { if (in_array($id, $v)) { $tag = $k; break; } } } $this->token($tag, $id); if ($colon) { $this->token(':', ':'); } return strlen($input); } function interpolate_string($str, array $options = array()) // #{0} { $options = array_merge(array( 'heredoc' => '', 'regex' => NULL ), $options); $tokens = array(); $pi = 0; $i = -1; while ( isset($str{++$i}) ) { $letter = $str{$i}; if ($letter === '\\') { $i++; continue; } if ( ! ($letter === '#' && (substr($str, $i + 1, 1) === '{') && ($expr = $this->balanced_string(substr($str, $i + 1), '}'))) ) { continue; } if ($pi < $i) { $tokens[] = array('NEOSTRING', substr($str, $pi, $i - $pi)); } $inner = substr($expr, 1, -1); if (strlen($inner)) { $lexer = new Lexer($inner, array( 'line' => $this->line, 'rewrite' => FALSE, )); $nested = $lexer->tokenize(); array_pop($nested); if (isset($nested[0]) && $nested[0][0] === t('TERMINATOR')) { array_shift($nested); } if ( ($length = count($nested)) ) { if ($length > 1) { array_unshift($nested, array(t('('), '(', $this->line)); $nested[] = array(t(')'), ')', $this->line); } $tokens[] = array('TOKENS', $nested); } } $i += strlen($expr); $pi = $i + 1; } if ($i > $pi && $pi < strlen($str)) { $tokens[] = array('NEOSTRING', substr($str, $pi)); } if ($options['regex']) { return $tokens; } if ( ! count($tokens)) { return $this->token('STRING', '""'); } if ( ! ($tokens[0][0] === 'NEOSTRING')) { array_unshift($tokens, array('', '')); } if ( ($interpolated = count($tokens) > 1) ) { $this->token('(', '('); } for ($i = 0; $i < count($tokens); $i++) { list($tag, $value) = $tokens[$i]; if ($i) { $this->token('+', '+'); } if ($tag === 'TOKENS') { $this->tokens = array_merge($this->tokens, $value); } else { $this->token('STRING', $this->make_string($value, '"', $options['heredoc'])); } } if ($interpolated) { $this->token(')', ')'); } return $tokens; } function js_token() { if ( ! ($this->chunk{0} === '`' && preg_match(self::$JSTOKEN, $this->chunk, $match))) { return 0; } $this->token('JS', substr($script = $match[0], 1, -1)); return strlen($script); } function line_token() { if ( ! preg_match(self::$MULTI_DENT, $this->chunk, $match)) { return 0; } $indent = $match[0]; $this->line += substr_count($indent, "\n"); $this->seen_for = FALSE; // $prev = & last($this->tokens, 1); $size = strlen($indent) - 1 - strrpos($indent, "\n"); $no_newlines = $this->unfinished(); if (($size - $this->indebt) === $this->indent) { if ($no_newlines) { $this->suppress_newlines(); } else { $this->newline_token(); } return strlen($indent); } if ($size > $this->indent) { if ($no_newlines) { $this->indebt = $size - $this->indent; $this->suppress_newlines(); return strlen($indent); } $diff = $size - $this->indent + $this->outdebt; $this->token('INDENT', $diff); $this->indents[] = $diff; $this->ends[] = 'OUTDENT'; $this->outdebt = $this->indebt = 0; } else { $this->indebt = 0; $this->outdent_token($this->indent - $size, $no_newlines); } $this->indent = $size; return strlen($indent); } function literal_token() { if (preg_match(self::$OPERATOR, $this->chunk, $match)) { list($value) = $match; if (preg_match(self::$CODE, $value)) { $this->tag_parameters(); } } else { $value = $this->chunk{0}; } $tag = t($value); $prev = & last($this->tokens); if ($value === '=' && $prev) { if ( ! (isset($prev[1]->reserved) && $prev[1]->reserved) && in_array(''.$prev[1], self::$JS_FORBIDDEN)) { $this->error('reserved word "'.$this->value().'" can\'t be assigned'); } if (in_array($prev[1], array('||', '&&'))) { $prev[0] = t('COMPOUND_ASSIGN'); $prev[1] .= '='; return 1; } } if ($value === ';') { $this->seen_for = FALSE; $tag = t('TERMINATOR'); } else if (in_array($value, self::$MATH)) { $tag = t('MATH'); } else if (in_array($value, self::$COMPARE)) { $tag = t('COMPARE'); } else if (in_array($value, self::$COMPOUND_ASSIGN)) { $tag = t('COMPOUND_ASSIGN'); } else if (in_array($value, self::$UNARY)) { $tag = t('UNARY'); } else if (in_array($value, self::$SHIFT)) { $tag = t('SHIFT'); } else if (in_array($value, self::$LOGIC) || $value === '?' && (isset($prev['spaced']) && $prev['spaced'])) { $tag = t('LOGIC'); } else if ($prev && ! (isset($prev['spaced']) && $prev['spaced'])) { if ($value === '(' && in_array($prev[0], t(self::$CALLABLE))) { if ($prev[0] === t('?')) { $prev[0] = t('FUNC_EXIST'); } $tag = t('CALL_START'); } else if ($value === '[' && in_array($prev[0], t(self::$INDEXABLE))) { $tag = t('INDEX_START'); if ($prev[0] === t('?')) { $prev[0] = t('INDEX_SOAK'); } } } if (in_array($value, array('(', '{', '['))) { $this->ends[] = self::$INVERSES[$value]; } else if (in_array($value, array(')', '}', ']'))) { $this->pair($value); } $this->token($tag, $value); return strlen($value); } function make_string($body, $quote, $heredoc = NULL) { if (!strlen($body)) { return $quote.$quote; } $body = preg_replace_callback('/\\\\([\s\S])/', function($match) use ($quote) { $contents = $match[1]; if (in_array($contents, array("\n", $quote))) { return $contents; } return $match[0]; }, $body); $body = preg_replace('/'.$quote.'/', '\\\\$0', $body); return $quote.$this->escape_lines($body, $heredoc).$quote; } function newline_token() { while ($this->value() === ';') { array_pop($this->tokens); } if ($this->tag() !== t('TERMINATOR')) { $this->token('TERMINATOR', "\n"); } } function number_token() { if ( ! preg_match(self::$NUMBER, $this->chunk, $match)) { return 0; } $number = $match[0]; if (preg_match('/^0[BOX]/', $number)) { $this->error("radix prefix '$number' must be lowercase"); } else if (preg_match('/E/', $number) && ! preg_match('/^0x/', $number)) { $this->error("exponential notation '$number' must be indicated with a lowercase 'e'"); } else if (preg_match('/^0\d*[89]/', $number)) { $this->error("decimal literal '$number' must not be prefixed with '0'"); } else if (preg_match('/^0\d+/', $number)) { $this->error("octal literal '$number' must be prefixed with 0o"); } $lexed_length = strlen($number); if (preg_match('/^0o([0-7]+)/', $number, $octal_literal)) { $number = '0x'.base_convert(intval($octal_literal[1], 8), 8, 16); } if (preg_match('/^0b([01]+)/', $number, $binary_literal)) { $number = '0x'.base_convert(intval($binary_literal[1], 2), 2, 16); } $this->token('NUMBER', $number); return $lexed_length; } function outdent_token($move_out, $no_newlines = FALSE) { while ($move_out > 0) { $len = count($this->indents) - 1; if ( ! isset($this->indents[$len])) { $move_out = 0; } else if ($this->indents[$len] === $this->outdebt) { $move_out -= $this->outdebt; $this->outdebt = 0; } else if ($this->indents[$len] < $this->outdebt) { $this->outdebt -= $this->indents[$len]; $move_out -= $this->indents[$len]; } else { $dent = array_pop($this->indents) - $this->outdebt; $move_out -= $dent; $this->outdebt = 0; $this->pair('OUTDENT'); $this->token('OUTDENT', $dent); } } if (isset($dent) && $dent) { $this->outdebt -= $move_out; } while ($this->value() == ';') { array_pop($this->tokens); } if ( ! ($this->tag() === t('TERMINATOR') || $no_newlines)) { $this->token('TERMINATOR', "\n"); } return $this; } function pair($tag) { if ( ! ($tag === ($wanted = last($this->ends)))) { if ($wanted !== 'OUTDENT') { $this->error("unmateched $tag"); } $this->indent -= $size = last($this->indents); $this->outdent_token($size, TRUE); return $this->pair($tag); } return array_pop($this->ends); } function regex_token() { if ($this->chunk{0} !== '/') { return 0; } if (preg_match(self::$HEREGEX, $this->chunk, $match)) { $length = $this->heregex_token($match); $this->line += substr_count($match[0], "\n"); return $length; } $prev = last($this->tokens); if ($prev) { if (in_array($prev[0], t((isset($prev['spaced']) && $prev['spaced']) ? self::$NOT_REGEX : self::$NOT_SPACED_REGEX))) { return 0; } } if ( ! preg_match(self::$REGEX, $this->chunk, $match)) { return 0; } list($match, $regex, $flags) = $match; if (substr($regex, 0, -1) === '/*') { $this->error('regular expressions cannot begin with `*`'); } $regex = $regex === '//' ? '/(?:)/' : $regex; $this->token('REGEX', "{$regex}{$flags}"); return strlen($match); } function sanitize_heredoc($doc, array $options) { $herecomment = isset($options['herecomment']) ? $options['herecomment'] : NULL; $indent = isset($options['indent']) ? $options['indent'] : NULL; if ($herecomment) { if (preg_match(self::$HEREDOC_ILLEGAL, $doc)) { $this->error('block comment cannot contain "*/*, starting'); } if ( ! strpos($doc, "\n")) { return $doc; } } else { $offset = 0; while (preg_match(self::$HEREDOC_INDENT, $doc, $match, PREG_OFFSET_CAPTURE, $offset)) { $attempt = $match[1][0]; $offset = strlen($match[0][0]) + $match[0][1]; if ( is_null($indent) || (strlen($indent) > strlen($attempt) && strlen($attempt) > 0)) { $indent = $attempt; } } } if ($indent) { $doc = preg_replace('/\n'.$indent.'/', "\n", $doc); } if ( ! $herecomment) { $doc = preg_replace('/^\n/', '', $doc); } return $doc; } function string_token() { switch ($this->chunk{0}) { case "'": if ( ! preg_match(self::$SIMPLESTR, $this->chunk, $match)) { return 0; } $this->token('STRING', preg_replace(self::$MULTILINER, "\\\n", $string = $match[0])); break; case '"': if ( ! ($string = $this->balanced_string($this->chunk, '"'))) { return 0; } if (strpos($string, '#{', 1) > 0) { $this->interpolate_string(substr($string, 1, -1)); } else { $this->token('STRING', $this->escape_lines($string)); } break; default: return 0; } if (preg_match('/^(?:\\\\.|[^\\\\])*\\\\[0-7]/', $string, $octal_esc)) { $this->error("octal escape sequences $string are not allowed"); } $this->line += substr_count($string, "\n"); return strlen($string); } function suppress_newlines() { if ($this->value() === '\\') { array_pop($this->tokens); } } function tag($index = 0, $tag = NULL) { $token = & last($this->tokens, $index); if ( ! is_null($tag)) { $token[0] = $tag; } return $token[0]; } function tag_parameters() { if ($this->tag() !== t(')')) { return $this; } $stack = array(); $tokens = &$this->tokens; $i = count($tokens); $tokens[--$i][0] = t('PARAM_END'); while ( ($tok = &$tokens[--$i]) ) { if ($tok[0] === t(')')) { $stack[] = $tok; } else if (in_array($tok[0], t('(', 'CALL_START'))) { if (count($stack)) { array_pop($stack); } else if ($tok[0] === t('(')) { $tok[0] = t('PARAM_START'); return $this; } else { return $this; } } } return $this; } function token($tag, $value = NULL) { if ( ! is_numeric($tag)) { $tag = t($tag); } $token = array($tag, $value, $this->line); return ($this->tokens[] = $token); } function tokenize() { while ( ($this->chunk = substr($this->code, $this->index)) !== FALSE ) { $types = array('identifier', 'comment', 'whitespace', 'line', 'heredoc', 'string', 'number', 'regex', 'js', 'literal'); foreach ($types as $type) { if ( ($d = $this->{$type.'_token'}()) ) { $this->index += $d; break; } } } $this->close_indentation(); if (($tag = array_pop($this->ends)) !== NULL) { $this->error('missing '.t_canonical($tag)); } if ($this->options['rewrite']) { $rewriter = new Rewriter($this->tokens); $this->tokens = $rewriter->rewrite(); } return $this->tokens; } function value($index = 0, $value = NULL) { $token = & last($this->tokens, $index); if ( ! is_null($value)) { $token[1] = $value; } return $token[1]; } function unfinished() { return preg_match(self::$LINE_CONTINUER, $this->chunk) || in_array($this->tag(), t('\\', '.', '?.', 'UNARY', 'MATH', '+', '-', 'SHIFT', 'RELATION', 'COMPARE', 'LOGIC', 'THROW', 'EXTENDS')); } function whitespace_token() { if ( ! (preg_match(self::$WHITESPACE, $this->chunk, $match) || ($nline = ($this->chunk{0} === "\n")))) { return 0; } $prev = & last($this->tokens); if ($prev) { $prev[$match ? 'spaced' : 'newLine'] = TRUE; } return $match ? strlen($match[0]) : 0; } } ?>