2080 lines
46 KiB
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']);
|
|
}
|
|
}
|