Files
Ismo Vuorinen 7e73994e40 Sparks: Assets, 1.5.1
http://getsparks.org/packages/assets/versions/HEAD/show

Creates our minified and combined .js and .css files
2013-07-11 07:46:29 +03:00

1350 lines
30 KiB
PHP
Executable File

<?php
namespace CoffeeScript;
Init::init();
/**
* CoffeeScript lexer. For the most part it's directly from the original
* source code, though there are some relatively minor differences in how it
* works with the parser (since we're using Lemon).
*/
class Lexer
{
static $COFFEE_ALIASES = array(
'and' => '&&',
'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;
}
}
?>