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*(?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']); } }