jsm/jsm.inc.php

2080 lines
46 KiB
PHP

<?php
class JSM
{
//NOTE: parsed modules are shared between JSM instances
static private $modules = array();
static private $consts = array();
private $base_dirs = array();
private $file;
private $scope_vars = array(array());
private $includes = array();
private $defs = array();
private $buffers = array();
private $nodes = array();
private $cur_modules = array();
private $args_parser;
function __construct($base_dirs/*can be a string for BC*/, $file)
{
//for rand constants
mt_srand();
if(is_string($base_dirs))
$base_dirs = array($base_dirs);
$this->base_dirs = array_map(
function($base_dir) { return rtrim(jsm_normalize_path($base_dir, true/*nix*/), '/'); },
$base_dirs
);
$this->file = $file;
$this->args_parser = new JSM_ArgsParser();
}
function getRootFile()
{
return $this->file;
}
function getModule($file)
{
if(isset(self::$modules[$file]))
return self::$modules[$file];
throw new Exception("Module for '$file' not found");
}
function _extractModule($file)
{
$txt = file_get_contents($file);
if($txt === false)
throw new Exception("Bad file '{$file}'");
$m = new JSM_Module($file);
$m->node = new JSM_MacroDefNode($file/*using file as name*/);
$this->_pushCurrentModule($m);
$txt = $this->_fixJunk($txt);
$txt = $this->_removeBlockComments($txt);
$txt = $this->_removeLineComments($txt);
$txt = $this->_processIncludes($file, $txt);
$txt = $this->_extractScriptDefs($file, $txt);
$this->_parseDefBody($m->node, $file, $txt);
$this->_popCurrentModule();
return $m;
}
function _pushCurrentModule(JSM_Module $m)
{
$this->cur_modules[] = $m;
}
function _popCurrentModule()
{
if(!$this->cur_modules)
throw new Exception("No module to pop");
$m = array_pop($this->cur_modules);
return $m;
}
function _currentModule() : JSM_Module
{
$m = end($this->cur_modules);
if(!$m)
throw new Exception("No current modules");
return $m;
}
function getCurrentModule() : JSM_Module
{
return $this->_currentModule();
}
function process()
{
$m = $this->_extractModule($this->file);
if($this->cur_modules)
throw new Exception("Non closed modules");
$this->defs = $m->defs;
$this->includes = $m->includes;
$this->_pushBuffer();
$m->node->call($this);
$res = $this->_popBuffer();
if($this->buffers)
throw new Exception("Non closed buffers " . sizeof($this->buffers));
if($this->nodes)
throw new Exception("Non closed nodes " . sizeof($this->nodes));
$res = $this->_processRelpaths($this->file, $res);
return array($res, $m);
}
function _pushNode(JSM_MacroInternalNode $n)
{
$this->nodes[] = $n;
}
function _popNode()
{
if(!$this->nodes)
throw new Exception("No nodes to pop");
$n = array_pop($this->nodes);
return $n;
}
function _node($strict = true)
{
$n = end($this->nodes);
if(!$n)
{
if($strict)
throw new Exception("No current node");
else
return null;
}
return $n;
}
function _pushBuffer()
{
$this->buffers[] = new JSM_TextBuffer();
}
function _popBuffer()
{
if(!$this->buffers)
throw new Exception("No buffers to pop");
$b = array_pop($this->buffers);
return $b->getContent();
}
function _buffer()
{
$b = end($this->buffers);
if(!$b)
throw new Exception("No current buffer");
return $b;
}
function _buffers()
{
return $this->buffers;
}
function _pushVarScope()
{
$this->scope_vars[] = array();
}
function _popVarScope()
{
array_pop($this->scope_vars);
}
function setConst($name, $value)
{
if(isset(self::$consts[$name]) && self::$consts[$name] != $value)
{
$curr_value = self::$consts[$name];
throw new Exception("Const '$name' is already defined as {$curr_value}");
}
self::$consts[$name] = $value;
}
function hasConst($name, $value)
{
return isset(self::$consts[$name]);
}
function getConst($name)
{
if(!isset(self::$consts[$name]))
throw new Exception("Const '$name' is not defined");
return self::$consts[$name];
}
function getAutoConst($name)
{
if(!isset(self::$consts[$name]))
$this->setConst($name, crc32($name) & 0xFFFFFFF);
return $this->getConst($name);
}
function setVar($name, $value)
{
$this->scope_vars[sizeof($this->scope_vars) - 1][$name] = $value;
}
function getVar($name)
{
$vars = end($this->scope_vars);
if(isset($vars[$name]))
return $vars[$name];
throw new Exception("Var '$name' not resolved");
}
function getVarAnyScope($name)
{
for($i=count($this->scope_vars) - 1; $i>=0; --$i)
{
$vars = $this->scope_vars[$i];
if(isset($vars[$name]))
return $vars[$name];
}
throw new Exception("Var '$name' not resolved");
}
private function _fixJunk($txt)
{
//windows eol
$txt = str_replace("\r\n", "\n", $txt);
return $txt;
}
private function _removeBlockComments($txt)
{
if(strpos($txt, '/*') === false)
return $txt;
$regex = '~/\*.*?\*/~s';
$txt = preg_replace_callback(
$regex,
//preserve the new lines for better error reporting
function($m) { return str_repeat("\n", substr_count($m[0], "\n")); },
$txt);
return $txt;
}
private function _removeLineComments($txt)
{
//TODO: it's not robust enough, note a hack for URL addresses
$txt = preg_replace("~\s*(?<!:)//.*~", "\n", $txt);
return $txt;
}
/*private */function _relpathCallback(array $m, $curr_dir)
{
//1) checking if such a file exists and normalizing all .. in the path
$full_path = realpath($curr_dir . '/' . $m[1]);
if(!$full_path)
throw new Exception("Bad relative path '$m[1]'");
//2) now turning the path into *nix alike
$rel_path = jsm_make_rel_path($this->base_dirs, $full_path);
return $rel_path;
}
/*private */function _relconfCallback(array $m, $curr_dir)
{
//1) checking if such a file exists and normalizing all .. in the path
$full_path = realpath($curr_dir . '/' . $m[1] . '.conf.js');
if(!$full_path)
throw new Exception("Bad relative path '$m[1]'" . ($curr_dir . '/' . $m[1]));
$rel_path = jsm_make_rel_path($this->base_dirs, $full_path);
$rel_path = str_replace('.conf.js', '', $rel_path);
return "@$rel_path";
}
private function _processRelpaths($file, $txt)
{
//normalizing all file paths
if(strpos($txt, './') !== false)
{
$curr_dir = dirname($file);
$self = $this;
$txt = preg_replace_callback(
'~\./((.*?)\.(jpg|jpeg|swf|png|mp3|txt|gif|ogg))~',
function($m) use($self, $curr_dir) { return $self->_relpathCallback($m, $curr_dir); },
$txt);
$txt = preg_replace_callback(
'~@\./([^"]+)~',
function($m) use($self, $curr_dir) { return $self->_relconfCallback($m, $curr_dir); },
$txt);
}
return $txt;
}
/*private */function _includesCallback($inc_path, $prefix, $curr_file)
{
$file = jsm_resolve_inc_path($this->base_dirs, $curr_file, $inc_path);
$cm = $this->_currentModule();
if(!isset(self::$modules[$file]))
{
if($this->_hasExtension($file, '.js'))
{
self::$modules[$file] = $this->_extractModule($file);
}
else if($this->_hasExtension($file, '.php'))
{
include_once($file);
//special mark for PHP include
self::$modules[$file] = 1;
}
else
throw new Exception("Unknown extension of the include file '$file'");
}
$m = self::$modules[$file];
$need_include_macro = false;
//NOTE: module can be non object if it's a PHP include
if(is_object($m))
{
$need_include_macro = true;
if(!isset($cm->includes[$file]))
{
foreach($m->includes as $f => $_)
$cm->includes[$f] = 1;
foreach($m->defs as $_n => $d)
{
$n = $prefix.$_n;
if(isset($cm->defs[$n]) && $cm->defs[$n] !== $d)
throw new Exception("Def '$n' is already defined in '{$cm->file}' (check {$cm->defs[$n]->file})");
$cm->defs[$n] = $d;
}
}
}
$cm->includes[$file] = 1;
return $need_include_macro ? "<%INC($file)%>" : '';
}
private function _processIncludes($file, $txt)
{
if(strpos($txt, 'INC') !== false)
{
$self = $this;
$txt = preg_replace_callback(
'~<%\s*INC\s*\(\s*"([^"]+)"\s*(?:,\s*"([^"]+)")?\s*\)\s*%>~',
function($m) use($self, $file) { return $self->_includesCallback($m[1], isset($m[2]) ? $m[2] : '', $file); },
$txt);
}
return $txt;
}
function _isIncluded($file)
{
$file = jsm_normalize_path($file);
return isset($this->includes[$file]);
}
function _hasExtension($file, $ext)
{
return strpos($file, $ext, strlen($file) - strlen($ext) - 1) !== false;
}
static function getDeps(array $base_dirs, $file)
{
$deps = array();
self::_getDeps($base_dirs, $file, $deps);
return $deps;
}
static private function _extractDeps($txt)
{
$dep_files = array();
if(preg_match_all('~<%\s*INC\s*\(\s*"([^\n]+)~', $txt, $ms))
{
foreach($ms[1] as $raw_dep)
{
$dep_file = substr($raw_dep, 0, strpos($raw_dep, '"'));
$dep_files[$dep_file] = true;
}
}
return array_keys($dep_files);
}
static private function _getDeps(array $base_dirs, $file, &$deps)
{
static $cache = array();
$local_deps = array();
if(!isset($cache[$file]))
{
if(!is_file($file))
throw new Exception("Bad file '$file'");
$txt = file_get_contents($file);
if($txt === false)
throw new Exception("Bad file '$file'");
$extracted_deps = self::_extractDeps($txt);
foreach($extracted_deps as $inc)
{
$dep_file = jsm_resolve_inc_path($base_dirs, $file, $inc);
$local_deps[] = $dep_file;
$cache[$file] = $local_deps;
}
}
else
$local_deps = $cache[$file];
foreach($local_deps as $dep_file)
{
if(in_array($dep_file, $deps))
continue;
$deps[] = $dep_file;
try
{
self::_getDeps($base_dirs, $dep_file, $deps);
}
catch(Exception $e)
{
throw new Exception("Bad deps for '$file' : " . $e->getMessage());
}
}
}
private function _extractDefNameAndArgs($full_name)
{
$pos = strpos($full_name, '(');
if($pos == -1)
throw new Exception("Bad args string: $full_name");
$name = substr($full_name, 0, $pos);
$args_str = substr($full_name, $pos+1, strlen($full_name) - ($pos + 2));
$args = $this->_parseMacroDefArgs($args_str);
return array($name, $args);
}
private function _parseMacroDefArgs($args_str)
{
if(!$args_str)
return array();
return $this->args_parser->parseDef($args_str);
}
function nodes2str($nodes_or_str)
{
$str = '';
if(is_array($nodes_or_str))
{
$this->_pushBuffer();
foreach($nodes_or_str as $n)
$n->call($this);
$str = $this->_popBuffer();
}
else if(is_string($nodes_or_str))
$str = $nodes_or_str;
else
throw new Exception("Bad value: $nodes_or_str");
return $str;
}
function _processMacroArgs($arg_nodes_or_str)
{
$str = $this->nodes2str($arg_nodes_or_str);
return $this->_parseMacroArgsStr($str);
}
function _parseMacroArgsStr($orig_args_str)
{
static $cache = array();
if(!$orig_args_str)
return array(false, array());
//NOTE: this is potentially dangerous?
$may_cache = strpos($orig_args_str, '<%') === false;
if($may_cache && isset($cache[$orig_args_str]))
return $cache[$orig_args_str];
$args_str = $this->_removeBlockComments($orig_args_str);
$args_str = $this->_removeLineComments($args_str);
$args_str = rtrim($args_str, ',');
$res = $this->args_parser->parseInvoke($args_str);
if($may_cache)
$cache[$orig_args_str] = $res;
return $res;
}
private function _extractScriptDefs($file, $txt)
{
if(strpos($txt, 'def') === false)
return $txt;
//NOTE: make it more robust
$lexer_rules = array(
'~def\s+((\w+)\([^\)]*\))(.*?)\s*\n\bend\b~sA' => 1,
'~[^\r\n]+~A' => 2,
'~\r\n?|\n~A' => 3,
);
$tokens = jsm_lex($txt, $lexer_rules);
$txt = '';
foreach($tokens as $token)
{
if($token[0] == 1)
{
list($name, $args) = $this->_extractDefNameAndArgs($token[1][1]);
$body = $token[1][3];
$m = $this->_currentModule();
if(isset($m->defs[$name]))
throw new Exception("Def '$name' is already defined in '{$m->defs[$name]->file}' (check '$file')");
if(JSM_MacroPHPNode::isValid($name))
throw new Exception("Def '$name' conflicts with PHP macro");
$node = new JSM_MacroDefNode($name, $args, $file);
$m->defs[$name] = $node;
$this->_parseDefBody($node, $file, $body);
//var_dump($name, $body);
}
else
$txt .= $token[1][0];
}
return $txt;
}
private function _replaceVarsWithMacro($file, $txt)
{
//simple vals
$txt = preg_replace(
'~\{(\$[a-zA-Z0-9_]+)\}~',
'<%SVAL($1)%>',
$txt
);
//complex vals
$txt = preg_replace(
'~\{(\$[^\}]+)\}~',
'<%VAL($1)%>',
$txt
);
return $txt;
}
private function _parseDefBody(JSM_MacroDefNode $node, $file, $txt)
{
$txt = $this->_replaceVarsWithMacro($file, $txt);
//let's exit early if there are no macro calls
if(strpos($txt, '<%') === false)
{
$node->addChild(new JSM_TextNode($txt));
return;
}
$this->_pushNode($node);
//the idea here is to split the whole text by macro start/end tags
$lexer_rules = array(
'~<%\s*(/?\w+)\s*\(~A' => 1,
'~\)\s*%>~A' => 2,
'~[^\r\n]+?(?=(?:<%)|(?:\)\s*%>))~A' => 3,
'~[^\r\n]+~A' => 4,
'~\r\n?|\n~A' => 5,
);
$tokens = jsm_lex($txt, $lexer_rules);
foreach($tokens as $token)
{
if($token[0] == 1) //starting macro invokation
{
$name = $token[1][1];
//for neat things like <%/IF()%> => <%ENDIF()%>
if($name[0] === '/')
$name = 'END'. substr($name, 1);
$mcall = $this->_newCall($name);
if($mcall === null)
throw new Exception("No macro for '$name'");
$this->_pushNode($mcall);
}
else if($token[0] == 2) //finishing macro invocation
{
$mcall = $this->_popNode();
if($mcall === null)
throw new Exception("Non paired macro ending");
$tmp_node = $this->_node(false/*non strict*/);
if($tmp_node == null)
throw new Exception("No current macro (prev. " . $mcall->name . ")");
$tmp_node->addChild($mcall);
}
else
{
$txt = $token[1][0];
$node = $this->_node();
//text append optimization
if($node instanceof JSM_MacroCallNode)
$node->addText($txt);
else
$node->addChild(new JSM_TextNode($txt));
}
}
$this->_popNode();
}
function _newCall($name)
{
$func = "macro_ex_{$name}";
if(is_callable($func))
{
return $func($this);
}
else
{
return new JSM_MacroCallNode($name);
}
}
function _getMacro($name)
{
if(isset($this->defs[$name]))
return $this->defs[$name];
if(JSM_MacroPHPNode::isValid($name))
{
$node = new JSM_MacroPHPNode($name);
$this->defs[$name] = $node;
return $node;
}
throw new Exception("Macro '$name' not found");
}
}
class JSM_Module
{
var $file = '';
var $defs = array();
var $includes = array();
var $node = null;
function __construct($file)
{
$this->file = $file;
}
function getIncludes()
{
return $this->includes;
}
function getFile()
{
return $this->file;
}
}
interface JSM_MacroNode
{
function call(JSM $jsm);
}
interface JSM_MacroInternalNode extends JSM_MacroNode
{
function addChild(JSM_MacroNode $node);
}
class JSM_TextNode implements JSM_MacroNode
{
var $txt;
function __construct($txt)
{
$this->txt = $txt;
}
function call(JSM $jsm)
{
$jsm->_buffer()->addContent($this->txt);
}
}
class JSM_MacroCallNode implements JSM_MacroInternalNode
{
var $name;
var $arg_nodes = array();
function __construct($name)
{
$this->name = $name;
}
//NOTE: children are arg nodes
function addChild(JSM_MacroNode $node)
{
$this->arg_nodes[] = $node;
}
function addText($txt)
{
if($this->arg_nodes && $this->arg_nodes[sizeof($this->arg_nodes)-1] instanceof JSM_TextNode)
$this->arg_nodes[sizeof($this->arg_nodes)-1]->txt .= $txt;
else
$this->arg_nodes[] = new JSM_TextNode($txt);
}
function call(JSM $jsm)
{
try
{
$m = $jsm->_getMacro($this->name);
$m->setUserArgs($this->arg_nodes);
$m->call($jsm);
}
catch(Exception $e)
{
throw new Exception("Error calling '{$this->name}': " . $e->getMessage());
}
}
}
class JSM_MacroPHPNode implements JSM_MacroNode
{
var $func;
var $refl;
var $user_args = '';
var $is_global;
var $is_eval_args; //arguments will be evaluated and eval result will be passed
var $is_raw_args; //arguments result string will be passed, not evaluated
var $is_node_args; //argument nodes will be passed as is
var $is_no_args;
function __construct($func)
{
$this->func = $func;
$php_func = "macro_$func";
$this->refl = new ReflectionFunction($php_func);
$doc = $this->refl->getDocComment();
$this->is_global = strpos($doc, '@global') !== false;
$this->is_raw_args = strpos($doc, '@raw_args') !== false;
$this->is_eval_args = strpos($doc, '@eval_args') !== false;
$this->is_no_args = strpos($doc, '@no_args') !== false;
$this->is_node_args = strpos($doc, '@node_args') !== false;
}
static function isValid($func)
{
$php_func = "macro_$func";
return is_callable($php_func);
}
function setUserArgs($arg_nodes_or_str)
{
$this->user_args = $arg_nodes_or_str;
}
function call(JSM $jsm)
{
//NOTE: making sure file containing PHP macro was actually included by this file
// if it's not a @global one
if(!$this->is_global && !$jsm->_isIncluded($this->refl->getFileName()))
throw new Exception("Macro source file was not included by config file itself: ".$this->refl->getFileName());
$named = false;
$args = array();
if($this->is_no_args)
{} //just do nothing
else if($this->is_eval_args)
$args[] = jsm_eval_string($jsm, $jsm->nodes2str($this->user_args));
else if($this->is_raw_args)
$args[] = $jsm->nodes2str($this->user_args);
else if($this->is_node_args)
$args[] = $this->user_args;
else
list($named, $args) = $jsm->_processMacroArgs($this->user_args);
if($named)
{
$params = $this->refl->getParameters();
if($params)
array_shift($params);
//first parameter is JSM instance
$call_args = array($jsm);
foreach($params as $param)
{
$name = '$'.$param->getName();
if(!$param->isOptional())
{
if(!array_key_exists($name, $args))
throw new Exception("Required argument '$name' missing");
$call_args[] = $args[$name];
}
else
{
if(array_key_exists($name, $args))
$call_args[] = $args[$name];
else
$call_args[] = $param->getDefaultValue();
}
}
$res = $this->refl->invokeArgs($call_args);
}
else
{
//first parameter is JSM instance
array_unshift($args, $jsm);
$res = $this->refl->invokeArgs($args);
}
if($res !== null)
{
$res = $this->_processPHPMacroResult($res);
$jsm->_buffer()->addContent($res);
}
}
private function _processPHPMacroResult($res)
{
if(is_array($res))
$res = jsm_json_encode($res);
else if(!is_string($res))
$res = "$res";
return $res;
}
}
class JSM_MacroDefNode implements JSM_MacroInternalNode
{
var $name = '';
var $file = '';
var $decl_args = array();
var $node_args = array();
var $children = array();
function __construct($name, $decl_args = array(), $file = '')
{
$this->name = $name;
$this->file = $file;
$this->decl_args = $decl_args;
}
function setUserArgs($node_args_or_str)
{
$this->node_args = $node_args_or_str;
}
function addChild(JSM_MacroNode $node)
{
$this->children[] = $node;
}
function call(JSM $jsm)
{
$args_str = $jsm->nodes2str($this->node_args);
$jsm->_pushVarScope();
$this->_prepareArgs($jsm, $args_str);
foreach($this->children as $c)
$c->call($jsm);
$jsm->_popVarScope();
}
function _prepareArgs(JSM $jsm, $args_str)
{
list($named, $user_args) = $jsm->_parseMacroArgsStr($args_str);
$args = $this->decl_args;
//NOTE: overriding default args if any
if($named)
{
foreach($user_args as $k => $v)
{
if(!array_key_exists($k, $args))
throw new Exception("Macro '{$this->name}' doesn't use argument '$k'");
$args[$k] = $v;
}
}
else
{
$arg_keys = array_keys($args);
foreach($user_args as $i => $v)
{
if(!isset($arg_keys[$i]))
{
//var_dump($user_args);
throw new Exception("Macro '{$this->name}' has no argument at pos '".($i+1)."'");
}
$args[$arg_keys[$i]] = $v;
}
}
foreach($args as $k => $v)
{
if($v === null)
throw new Exception("Macro '{$this->name}' argument '$k' not passed");
$jsm->setVar($k, $v);
}
}
}
class JSM_TextBuffer
{
private $txt = '';
function addContent($txt)
{
$this->txt .= $txt;
}
function getContent()
{
return $this->txt;
}
}
class JSM_Eval
{
static function evaluate(JSM $jsm, array $bytecode)
{
$stack = array();
$result = array('value' => 0, 'logical_op' => false);
if(!$bytecode)
throw new Exception("No bytecode");
foreach($bytecode as $bc)
{
if($bc['op'] === 'IDENTIFIER')
{
$stack[] = $jsm->getVar($bc['arg']);
}
else if($bc['op'] === 'FUNC')
{
$result['logical_op'] = self::computeFunc($bc['arg'], $stack);
}
else if($bc['op'] === 'NUMBER')
{
$stack[] = 1*$bc['arg'];
}
else if($bc['op'] === 'STRING')
{
$stack[] = $bc['arg'];
}
else
throw new Exception("Unknow opcode " . $bc['op']);
}
if(sizeof($stack) != 1)
throw new Exception("Missing values on stack");
$result['value'] = $stack[0];
return $result;
}
static private function computeFunc($func, &$stack)
{
//NOP
if($func === "")
return true;
if($func === "+")
{
$b = array_pop($stack); $a = array_pop($stack);
$stack[] = ($a + $b);
return true;
}
else if($func === "-")
{
$b = array_pop($stack); $a = array_pop($stack);
$stack[] = ($a - $b);
return true;
}
else if($func === "*")
{
$b = array_pop($stack); $a = array_pop($stack);
$stack[] = ($a * $b);
return true;
}
else if($func === "/")
{
$b = array_pop($stack); $a = array_pop($stack);
$stack[] = ($a / $b);
return true;
}
else if($func === ">")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a > $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "<")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a < $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "==")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a == $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "!=")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a != $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "&&")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = (bool)$a && (bool)$b;
$stack[] = (int)$res;
return $res;
}
else if($func === "||")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = (bool)$a || (bool)$b;
$stack[] = (int)$res;
return $res;
}
else if($func === "!")
{
$a = array_pop($stack);
$res = !((bool)$a);
$stack[] = (int)$res;
return $res;
}
else if($func === ">=")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a >= $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "<=")
{
$b = array_pop($stack); $a = array_pop($stack);
$res = $a <= $b;
$stack[] = (int)$res;
return $res;
}
else if($func === "u-")
{
$a = array_pop($stack);
$stack[] = -1 * $a;
return true;
}
else if($func === "round")
{
$a = array_pop($stack);
$stack[] = round($a);
return true;
}
else
{
throw new Exception("Unknown func " . $func);
}
}
}
class JSM_Expr
{
const T_Number = 1001;
const T_Identifier = 1002;
const T_String = 1003;
const T_LParen = 1009;
const T_RParen = 1010;
const T_Func = 1011;
//token, precedence, arity
private $funcs = array(
"," => array("", 1, 0),
"&&" => array("&&", 2, 2),
"||" => array("||", 2, 2),
"=" => array("=", 10, 2),
"+" => array("+", 12, 2),
"-" => array("-", 12, 2),
">" => array(">", 12, 2),
"<" => array("<", 12, 2),
"==" => array("==", 12, 2),
"!=" => array("!=", 12, 2),
">=" => array(">=", 12, 2),
"<=" => array("<=", 12, 2),
"*" => array("*", 13, 2),
"/" => array("/", 13, 2),
"!" => array("!", 14, 1),
"u-" => array("u-", 16, 1),
"u+" => array("", 16, 0),
"round" => array("round", 16, 1),
);
private $source;
private $cursor;
function process($expr)
{
$this->source = $expr;
$this->cursor = 0;
$tokens = $this->_tokenize();
$rpl = $this->_rpl($tokens);
$bytecode = array();
foreach($rpl as $t)
{
if($t[0] == self::T_Number)
{
$bytecode[] = array('op' => 'NUMBER', 'arg' => $t[1]);
}
else if($t[0] == self::T_Identifier)
{
$bytecode[] = array('op' => 'IDENTIFIER', 'arg' => $t[1]);
}
else if($t[0] == self::T_String)
{
$bytecode[] = array('op' => 'STRING', 'arg' => $t[1]);
}
else if($t[0] == self::T_Func)
{
$bytecode[] = array('op' => 'FUNC', 'arg' => $t[1][0]);
}
else
$this->_error("Bad token {$t[0]}");
}
return $bytecode;
}
private function _rpl(array $tokens)
{
$rpl = array();
$num_rpl = 0;
$function_stack = array();
$num_function_stack = 0;
$par_level = 0;
foreach($tokens as $token)
{
switch($token[0])
{
case self::T_Number:
case self::T_Identifier:
case self::T_String:
$rpl[$num_rpl++] = $token;
break;
case self::T_LParen:
++$par_level;
break;
case self::T_RParen:
--$par_level;
break;
case self::T_Func:
{
$f = array($token, $token[1][1], $par_level);
while($num_function_stack>0 && $this->_isPrecedenceGreaterOrEqual($function_stack[$num_function_stack-1], $f))
$rpl[$num_rpl++] = $function_stack[--$num_function_stack][0];
$function_stack[$num_function_stack++] = $f;
break;
}
}
}
if($par_level != 0)
$this->_error("Non matching parenthesis");
while($num_function_stack>0)
$rpl[$num_rpl++] = $function_stack[--$num_function_stack][0];
return $rpl;
}
private function _isPrecedenceGreaterOrEqual(array $a, array $b)
{
return $this->_cmpFunc($a, $b) >= 0;
}
private function _cmpFunc(array $a, array $b)
{
//checking paren level first
if($a[2] != $b[2]) return $a[2] - $b[2];
//checking precedence
return $a[1] - $b[1];
}
private function _symbol()
{
return substr($this->source, $this->cursor, 1);
}
private function _error($msg)
{
throw new Exception("Error in '$this->source': $msg");
}
private function _tokenize()
{
$tokens = array();
// Determines if the next + or - is a binary or unary operator.
$binary = false;
while(true)
{
$token = null;
$c = $this->_symbol();
if($c === false || $c === '')
break;
++$this->cursor;
//identifiers
if($c === '$')
{
//collect all chars of an identifier
$start = $this->cursor;
while(ctype_alnum($this->_symbol()) || $this->_symbol() == '_')
$this->cursor++;
$attribute = substr($this->source, $start, $this->cursor - $start);
$token = array(self::T_Identifier, '$'.$attribute);
$binary = true;
}
else if($c === '"')
{
//collect all chars of a string
$start = $this->cursor;
while($this->_symbol() !== '"')
$this->cursor++;
$attribute = substr($this->source, $start, $this->cursor - $start);
$token = array(self::T_String, $attribute);
$this->cursor++;
$binary = true;
}
//functions
else if(ctype_alpha($c))
{
//collect all chars of an identifier
$start = $this->cursor - 1;
while(ctype_alnum($this->_symbol()) || $this->_symbol() == '_')
$this->cursor++;
$attribute = substr($this->source, $start, $this->cursor - $start);
if($this->_symbol() !== '(')
$this->_error('( expected');
$token = $this->_getFuncToken($attribute);
$binary = true;
}
//numbers
else if(ctype_digit($c))
{
$start = $this->cursor - 1;
while(ctype_digit($this->_symbol())) $this->cursor++;
if($this->_symbol() == '.')
{
$this->cursor++;
while(ctype_digit($this->_symbol())) $this->cursor++;
}
$attribute = substr($this->source, $start, $this->cursor - $start);
$token = array(self::T_Number, $attribute);
$binary = true;
}
//operators
else
{
switch($c)
{
case ' ': case "\r": case "\t": case "\n" : break;
case '(': $token = array(self::T_LParen, $c); $binary = false; break;
case ')': $token = array(self::T_RParen, $c); $binary = true; break;
case '+': $token = $this->_getFuncToken(($binary ? '+' : 'u+')); $binary = false; break;
case '-': $token = $this->_getFuncToken(($binary ? '-' : 'u-')); $binary = false; break;
default:
//try to peek the next symbol and check if such an operator exists
$sym = $this->_symbol();
if($sym !== false && $sym !== '' && ($token = $this->_findFuncToken($c . $sym)))
$this->cursor++;
else
$token = $this->_getFuncToken($c);
$binary = false;
}
}
if($token !== null)
$tokens[] = $token;
}
return $tokens;
}
private function _findFuncToken($c)
{
if(!isset($this->funcs[$c]))
return null;
$f = $this->funcs[$c];
return array(self::T_Func, $f);
}
private function _getFuncToken($c)
{
$t = $this->_findFuncToken($c);
if(!$t)
$this->_error("can't find func token '$c'");
return $t;
}
}
function jsm_json_encode($data)
{
//fixig weird PHP json_encode behavior
$fixed = json_encode($data);
$fixed = str_replace(array('\\\\', '\\n', '\/'), array('\\', "\n", '/'), $fixed);
return $fixed;
}
function jsm_lex($string, array $tokenMap)
{
$tokens = array();
$offset = 0; // current offset in string
while(isset($string[$offset])) // loop as long as we aren't at the end of the string
{
foreach($tokenMap as $regex => $token)
{
if(preg_match($regex, $string, $matches, 0, $offset))
{
$tokens[] = array(
$token, // token ID (e.g. T_FIELD_SEPARATOR)
$matches
);
$offset += strlen($matches[0]);
continue 2; // continue the outer while loop
}
}
throw new Exception(sprintf('Unexpected character "%s"', $string[$offset]));
}
return $tokens;
}
function jsm_is_win()
{
return !(DIRECTORY_SEPARATOR == '/');
}
function jsm_normalize_path($path, $unix=null/*null means try to guess*/)
{
if(is_null($unix))
$unix = !jsm_is_win();
$path = str_replace('\\', '/', $path);
$path = preg_replace('/\/+/', '/', $path);
$parts = explode('/', $path);
$absolutes = array();
foreach($parts as $part)
{
if('.' == $part)
continue;
if('..' == $part)
array_pop($absolutes);
else
$absolutes[] = $part;
}
$res = implode($unix ? '/' : '\\', $absolutes);
return $res;
}
function jsm_resolve_inc_path(array $base_dirs, $curr_file, $inc_path)
{
return jsm_normalize_path(
$inc_path[0] == '/' ?
jsm_make_full_path($base_dirs, $inc_path) :
dirname($curr_file) . '/' . $inc_path
);
}
function jsm_make_full_path(array $base_dirs, $rel_path)
{
foreach($base_dirs as $dir)
if(is_file($dir . '/' . $rel_path))
return $dir . '/' . $rel_path;
throw new Exception("No file for relative path '$rel_path'");
}
function jsm_make_rel_path(array $base_dirs, $full_path)
{
$full_path = jsm_normalize_path($full_path, true/*nix*/);
foreach($base_dirs as $base_dir)
{
$rel_path = str_replace($base_dir, '', $full_path);
if(strlen($rel_path) < strlen($full_path))
return ltrim($rel_path, '/');
}
throw new Exception("File '$full_path' not mapped to any base dir");
}
function jsm_eval_string(JSM $jsm, $expr_str)
{
$expr = new JSM_Expr();
$bytecode = $expr->process($expr_str);
return JSM_Eval::evaluate($jsm, $bytecode);
}
function jsm_eval_string_bool(JSM $jsm, $expr_str)
{
$eval_res = jsm_eval_string($jsm, $expr_str);
return $eval_res['logical_op'];
}
function jsm_eval_string_value(JSM $jsm, $expr_str)
{
$eval_res = jsm_eval_string($jsm, $expr_str);
return $eval_res['value'];
}
class JSM_ArgsParser
{
const ERR_CONTEXT_CHARS = 200;
private $in;
private $len;
private $c = 0;
static $ORD_SPACE;
static $ORD_0;
static $ORD_9;
function __construct()
{
self::$ORD_SPACE = ord(' ');
self::$ORD_0 = ord('0');
self::$ORD_9 = ord('9');
}
function reset($input)
{
$this->in = $input;
$this->c = 0;
$this->len = strlen($this->in);
}
function parseInvoke($input)
{
$this->reset($input);
$args = array();
$this->skip_whitespace();
$named = false;
if($this->curr() === '$')
{
$named = true;
$this->parse_named_arg($args);
}
else
$this->parse_arg($args);
if($this->c != $this->len)
$this->_error("Trailing content({$this->c} vs {$this->len})");
return array($named, $args);
}
function parseDef($input)
{
$this->reset($input);
$args = array();
$this->parse_def_arg($args);
return $args;
}
private function next()
{
if($this->c >= $this->len)
return false;
return $this->in[$this->c++];
}
private function peek($n)
{
if(($this->c+$n) >= $this->len)
return false;
return $this->in[$this->c+$n];
}
private function curr()
{
if($this->c >= $this->len)
return false;
return $this->in[$this->c];
}
private function parse_def_arg(&$out)
{
$this->skip_whitespace();
$name = $this->parse_arg_name();
$this->skip_whitespace();
$ch = $this->next();
if($ch !== '=')
{
$out[$name] = null;
}
else
{
$this->skip_whitespace();
$value = $this->parse_arg_value();
if(isset($out[$name]))
throw new Exception("Argument '{$name}' is already defined in def macro");
$out[$name] = $value;
$this->skip_whitespace();
$ch = $this->next();
}
if($ch === ',')
$this->parse_def_arg($out);
}
private function parse_named_arg(&$out)
{
$this->skip_whitespace();
$name = $this->parse_arg_name();
$this->skip_whitespace();
if($this->next() !== '=')
$this->_error("'=' expected");
$this->skip_whitespace();
$value = $this->parse_arg_value();
$out[$name] = $value;
$this->skip_whitespace();
$ch = $this->next();
if($ch === ',')
$this->parse_named_arg($out);
}
private function parse_arg(&$out)
{
$this->skip_whitespace();
$value = $this->parse_arg_value();
$out[] = $value;
$this->skip_whitespace();
$ch = $this->next();
if($ch === ',')
$this->parse_arg($out);
}
private function parse_arg_name()
{
if($this->next() !== '$')
$this->_error("'$' expected");
$name = '$';
while($this->c < $this->len && (ctype_alnum($ch = $this->in[$this->c]) || ($ch === '_')))
{
$name .= $ch;
++$this->c;
}
return $name;
}
private function parse_arg_value()
{
$ch = $this->curr();
if($ch === false)
$this->_error("No data for value");
switch($ch)
{
case '[': return $this->parse_bracket_string();
case '"': return $this->parse_string();
case '\'': return $this->parse_multi_string();
case '-': return $this->parse_number();
default:
if(ord($ch) >= self::$ORD_0 && ord($ch) <= self::$ORD_9)
return $this->parse_number();
else
$this->_error("Not expected symbol");
}
}
private function parse_bracket_string()
{
if($this->curr() !== '[')
$this->_error("'[' expected");
$brackets = 1;
$start = $this->c;
++$this->c;
while($this->c < $this->len)
{
$ch =$this->in[$this->c];
if($ch === ']')
--$brackets;
else if($ch === '[')
++$brackets;
if($brackets == 0)
{
++$this->c;
$end = $this->c;
return substr($this->in, $start, $end - $start);
}
++$this->c;
}
$this->_error("Non matching '['");
}
private function parse_string()
{
if($this->next() !== '"')
$this->_error("'\"' expected");
$start = $this->c;
while($this->c < $this->len)
{
if($this->in[$this->c] === '"')
{
$end = $this->c;
++$this->c;
return substr($this->in, $start, $end - $start);
}
++$this->c;
}
$this->_error("Bad string");
}
private function parse_multi_string()
{
if($this->next() !== '\'')
$this->_error("' expected");
if($this->next() !== '\'')
$this->_error("' expected");
if($this->next() !== '\'')
$this->_error("' expected");
$start = $this->c;
while($this->c < $this->len)
{
if($this->peek(1) === "'" && $this->peek(2) === "'" && $this->peek(3) === "'")
{
$end = $this->c;
$this->c += 4;
return substr($this->in, $start, $end - $start + 1);
}
++$this->c;
}
$this->_error("Bad multi string");
}
private function parse_number()
{
$is_float = false;
$start = $this->c;
if($this->in[$this->c] == '-')
++$this->c;
while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9)
++$this->c;
if($this->c < $this->len && $this->in[$this->c] == '.')
{
$is_float = true;
++$this->c;
while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9)
++$this->c;
}
if($this->c < $this->len && ($this->in[$this->c] == 'e' || $this->in[$this->c] == 'E'))
{
$is_float = true;
++$this->c;
if($this->c < $this->len && ($this->in[$this->c] == '-' || $this->in[$this->c] == '+'))
++$this->c;
while($this->c < $this->len && ord($this->in[$this->c]) >= self::$ORD_0 && ord($this->in[$this->c]) <= self::$ORD_9)
++$this->c;
}
$str_num = substr($this->in, $start, $this->c - $start);
if($is_float)
return floatval($str_num);
else
return intval($str_num);
}
private function _error($error)
{
if($this->c < $this->len)
throw new Exception("Parse error: $error\n" . $this->show_position($this->c));
else
throw new Exception("Parse error: $error\n" . $this->show_position($this->len-1));
}
private function skip_whitespace()
{
while($this->c < $this->len && ord($this->in[$this->c]) <= self::$ORD_SPACE)
++$this->c;
}
private function show_position($p)
{
$pre = str_replace("\n", '', $this->get_past_input($p));
$c = str_repeat('-', max(0, strlen($pre) - 1));
return $pre . str_replace("\n", '', $this->get_upcoming_input($p)) . "\n" . $c . "^";
}
private function get_past_input($c)
{
$past = substr($this->in, 0, $c+1);
return (strlen($past) > self::ERR_CONTEXT_CHARS ? '...' : '') . substr($past, -self::ERR_CONTEXT_CHARS);
}
private function get_upcoming_input($c)
{
$next = substr($this->in, $c+1);
return substr($next, 0, self::ERR_CONTEXT_CHARS) . (strlen($next) > self::ERR_CONTEXT_CHARS ? '...' : '');
}
}
//////////////////////////////////////////////////////
/**
* @global @raw_args
*/
function macro_INC($jsm, $file_and_prefix)
{
$items = explode(',', $file_and_prefix);
//NOTE: we don't care about prefix here, it's handled
// during includes processing
$file = trim($items[0]);
$m = $jsm->getModule($file);
$m->node->call($jsm);
}
/**
* @global @raw_args
*/
function macro_LET($jsm, $txt_args)
{
$items = explode("=", $txt_args, 2);
$eval_res = jsm_eval_string($jsm, $items[1]);
$jsm->setVar(trim($items[0]), $eval_res['value']);
}
/**
* @global @raw_args
*/
function macro_VAL($jsm, $txt_args)
{
if(strpos($txt_args, '$$') === 0)
{
$const_name = $txt_args;
if(!preg_match('~^\$\$[a-zA-Z0-9_]+$~', $const_name))
throw new Exception("Bad characters in const name: '{$const_name}'");
return $jsm->getAutoConst(substr($const_name, 2));
}
if(strpos($txt_args, '$(') === 0)
$txt_args = ltrim($txt_args, '$');
$eval_res = jsm_eval_string($jsm, $txt_args);
return $eval_res['value'];
}
//simple version of variable echoing
/**
* @global @raw_args
*/
function macro_SVAL($jsm, $txt_args)
{
return $jsm->getVar($txt_args);
}
/**
* @global
*/
function macro_CONST($jsm, $k, $v)
{
$jsm->setConst($k, $v);
}
class JSM_If implements JSM_MacroInternalNode
{
private $branches = array();
function addToArgs(JSM_MacroNode $node)
{
if($this->branches && $this->branches[sizeof($this->branches)-1]['cond'] === null)
throw new Exception("ELSE has no args");
$this->branches[sizeof($this->branches)-1]['cond'][] = $node;
}
function addChild(JSM_MacroNode $node)
{
$b = &$this->branches[sizeof($this->branches)-1]['nodes'];
$b[] = $node;
}
function addBranch()
{
if($this->branches && $this->branches[sizeof($this->branches)-1]['cond'] === null)
throw new Exception("Not expecting any more branches");
$this->branches[] = array('cond' => array(), 'nodes' => array());
return new JSM_IfBranch($this);
}
function addElse()
{
$this->branches[] = array('cond' => null, 'nodes' => array());
return new JSM_IfBranch($this);
}
function call(JSM $jsm)
{
$branch_found = false;
foreach($this->branches as $b)
{
//else special case
if($b['cond'] === null)
continue;
$cond_txt = $jsm->nodes2str($b['cond']);
if(jsm_eval_string_bool($jsm, $cond_txt))
{
foreach($b['nodes'] as $node)
{
$node->call($jsm);
}
$branch_found = true;
}
}
if(!$branch_found)
{
$b = $this->branches[sizeof($this->branches)-1];
//checking if it's an else node
if($b['cond'] === null)
{
foreach($b['nodes'] as $node)
$node->call($jsm);
}
}
}
}
class JSM_IfBranch implements JSM_MacroInternalNode
{
private $if;
function __construct(JSM_If $if)
{
$this->if = $if;
}
function addChild(JSM_MacroNode $node)
{
$this->if->addToArgs($node);
}
function call(JSM $jsm)
{}
}
function macro_ex_IF(JSM $jsm)
{
$if = new JSM_If();
$jsm->_pushNode($if);
return $if->addBranch();
}
function macro_ex_ELSIF(JSM $jsm)
{
$if = $jsm->_node();
if(!($if instanceof JSM_If))
throw new Exception("Not expected ELSIF");
return $if->addBranch();
}
function macro_ex_ELSE(JSM $jsm)
{
$if = $jsm->_node();
if(!($if instanceof JSM_If))
throw new Exception("Not expected ELSE");
return $if->addElse();
}
function macro_ex_ENDIF(JSM $jsm)
{
$if = $jsm->_popNode();
if(!($if instanceof JSM_If))
throw new Exception("Not expected ENDIF");
return $if;
}
class JSM_Rep implements JSM_MacroInternalNode
{
private $children = array();
private $rep_args = array();
function addNumber()
{
return new JSM_RepNumber($this);
}
function addToArgs(JSM_MacroNode $node)
{
$this->rep_args[] = $node;
}
function addChild(JSM_MacroNode $n)
{
$this->children[] = $n;
}
function call(JSM $jsm)
{
$reps = jsm_eval_string_value($jsm, $jsm->nodes2str($this->rep_args));
for($i=0;$i<$reps;++$i)
{
$jsm->setVar('$_REP_INDEX_', $i);
foreach($this->children as $n)
$n->call($jsm);
}
}
}
class JSM_RepNumber implements JSM_MacroInternalNode
{
var $rep;
function __construct(JSM_Rep $rep)
{
$this->rep = $rep;
}
function addChild(JSM_MacroNode $n)
{
$this->rep->addToArgs($n);
}
function call(JSM $jsm)
{}
}
function macro_ex_REP(JSM $jsm)
{
$rep = new JSM_Rep();
$jsm->_pushNode($rep);
return $rep->addNumber();
}
function macro_ex_ENDREP(JSM $jsm)
{
$rep = $jsm->_popNode();
if(!($rep instanceof JSM_Rep))
throw new Exception("Not expected ENDREP");
return $rep;
}
/**
* @global @raw_args
*/
function macro_CALL($proc, $raw_args)
{
list($name, $call_args) = explode(",", $raw_args, 2);
$name = trim($name, '"');
$m = $proc->_getMacro($name);
$m->setUserArgs($call_args);
$m->call($proc);
}
/**
* @global @eval_args
*/
function macro_TRACE($jsm, array $eval_res)
{
echo "TRACE: " . $eval_res['value'] . "\n";
}
/**
* @global @raw_args
*/
function macro_LET_IF($jsm, $raw_args)
{
list($let, $cond) = explode(",", $raw_args, 2);
$eval_res = jsm_eval_string($jsm, $cond);
if($eval_res['logical_op'])
{
$items = explode("=", $let, 2);
$eval_res = jsm_eval_string($jsm, $items[1]);
$jsm->setVar(trim($items[0]), $eval_res['value']);
}
}