commit ae9c32f278a89cb5a78fe1817af620ed894f3043 Author: Pavel Shevaev Date: Mon May 16 14:18:02 2022 +0300 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/jsm.inc.php b/jsm.inc.php new file mode 100644 index 0000000..ba8481a --- /dev/null +++ b/jsm.inc.php @@ -0,0 +1,2059 @@ +base_dir = rtrim(jsm_normalize_path($base_dir, true/*nix*/), '/'); + $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->_processIncludes($file, $txt); + $txt = $this->_removeBlockComments($txt); + $txt = $this->_removeLineComments($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() + { + $m = end($this->cur_modules); + if(!$m) + throw new Exception("No current modules"); + return $m; + } + + function getCurrentModule() + { + 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 _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*(?base_dir, '', $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])); + + $full_path = jsm_normalize_path($full_path, true/*nix*/); + $rel_path = ltrim(str_replace($this->base_dir, '', $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(array $m, $curr_file) + { + $file_name = $m[1]; + $dir = ($file_name[0] == '/') ? $this->base_dir : dirname($curr_file); + $file = jsm_normalize_path($dir . '/' . $m[1]); + + $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) + { + 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*%>~', + function($m) use($self, $file) { return $self->_includesCallback($m, $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($base_dir, $file) + { + $deps = array(); + self::_getDeps($base_dir, $file, $deps); + return $deps; + } + + static private function _extractDeps($txt) + { + $dep_files = array(); + if(preg_match_all('~<%\s*((?:INC)|(?:BHL))\s*\(\s*"([^\n]+)~', $txt, $ms)) + { + foreach($ms[1] as $idx => $type) + { + $raw_dep = $ms[2][$idx]; + + $is_bhl = $type === "BHL"; + if($is_bhl) + { + $bhl_imps = explode(',', str_replace('"', '', $raw_dep)); + foreach($bhl_imps as $bhl_imp) + { + if($bhl_imp) + $dep_files[$bhl_imp . '.bhl'] = true; + } + } + else + { + $dep_file = substr($raw_dep, 0, strpos($raw_dep, '"')); + $dep_files[$dep_file] = false; + } + } + } + return $dep_files; + } + + static private function _getDeps($base_dir, $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 => $is_bhl) + { + $dir = ($inc[0] == '/') ? $base_dir : dirname($file); + $dep_file = jsm_normalize_path($dir . "/" . $inc); + $local_deps[] = array($is_bhl, $dep_file); + $cache[$file] = $local_deps; + } + } + else + $local_deps = $cache[$file]; + + foreach($local_deps as $local_dep) + { + list($is_bhl, $dep_file) = $local_dep; + + if(in_array($dep_file, $deps)) + continue; + $deps[] = $dep_file; + + //NOTE: bhl deps are shallow + if(!$is_bhl) + { + try + { + self::_getDeps($base_dir, $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); + + 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 node 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 node(prev. node " . $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_builtin; + 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_builtin = strpos($doc, '@builtin') !== 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(!$this->is_builtin && !$jsm->_isIncluded($this->refl->getFileName())) + throw new Exception("Not found"); + + $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); + return false; + } + } +} + +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+'), $c); $binary = false; break; + case '-': $token = $this->_getFuncToken(($binary ? '-' : 'u-'), $c); $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, null, $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_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(); + $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; + } + + if($is_float) + return 1*substr($this->in, $start, $this->c - $start); + else + return (int)(1*substr($this->in, $start, $this->c - $start)); + } + + 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 ? '...' : ''); + } +} + +////////////////////////////////////////////////////// + +/** + * @builtin @raw_args + */ +function macro_DEP($jsm) +{} + +/** + * @builtin @raw_args + */ +function macro_INC($jsm, $file) +{ + $m = $jsm->getModule($file); + $m->node->call($jsm); +} + +/** + * @builtin @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']); +} + +/** + * @builtin @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 +/** + * @builtin @raw_args + */ +function macro_SVAL($jsm, $txt_args) +{ + return $jsm->getVar($txt_args); +} + +/** + * @builtin + */ +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; +} + +/** + * @builtin @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); +} + +/** + * @builtin @eval_args + */ +function macro_TRACE($jsm, array $eval_res) +{ + echo "TRACE: " . $eval_res['value'] . "\n"; +} + +/** + * @builtin @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']); + } +}