text = $text; $this->name = $name; $this->off = 0; $this->len = mb_strlen($text); } private function next($n = 1) { if($n <= 0) { throw new \InvalidArgumentException("\$n must be > 0"); } $c = mb_substr($this->text, $this->off, $n); $this->off = min($this->off + $n, $this->len); return $c; } private function eof() { return ($this->off == $this->len); } private function back($n = 1) { if($n <= 0) { throw new \InvalidArgumentException("\$n must be > 0"); } $this->off = max($this->off - $n, 0); } private function search_off($needle) { return mb_strpos($this->text, $needle, $this->off); } private function search_multi($needles) { $oldoff = $this->off; $minoff = $this->len; $which = NULL; foreach($needles as $key => $needle) { if(($off = $this->search_off($needle)) === false) { continue; } if($off < $minoff) { $minoff = $off; $which = $key; } } $this->off = $minoff + (($which === NULL) ? 0 : mb_strlen((string) $needles[$which])); return array($which, $minoff, mb_substr($this->text, $oldoff, $minoff - $oldoff), $oldoff); } private function search($needle) { $oldoff = $this->off; $off = $this->search_off($needle); if($off === false) { $this->off = $this->len; return array(false, mb_substr($this->text, $oldoff), $oldoff); } $this->off = $off + mb_strlen($needle); return array($off, mb_substr($this->text, $oldoff, $off - $oldoff), $oldoff); } private function take_while($cb) { $s = ""; while($c = $this->next()) { if(!call_user_func($cb, $c)) { $this->back(); return $s; } $s .= $c; } return $s; } private function skip_ws() { $this->take_while("ctype_space"); } private function get_name() { $off = $this->off; $name = $this->take_while(function($c) { return ctype_alnum($c) || ($c == "_"); }); if(mb_strlen($name) == 0) { throw new ParseCompileError("Expected a name (alphanumeric chars + '_', at least one char)", $this->name, $off); } return $name; } /* * Function: parse * Parses the input into an AST. * * You only need this function, if you want to manually trnascompile a template. * * Parameters: * $text - The input code. * $name - The name of the template. * * Returns: * An array of objects. * * Throws: * */ public static function parse($text, $name) { $obj = new self($text, $name); $res = $obj->parse_text( self::ESCAPES_DEFAULT, /* Escapes */ self::PARSE_SHORT | self::PARSE_TAG /* Flags */ ); return self::tidyup_ast($res[0]); } private static function tidyup_ast($ast) { $out = array(); $prevtext = NULL; $first = true; foreach($ast as $node) { if($node instanceof TextNode) { if($prevtext === NULL) { $prevtext = $node; } else { $prevtext->text .= $node->text; } } else { if($prevtext !== NULL) { if($first) { $prevtext->text = ltrim($prevtext->text); } if($prevtext->text != "") { $out[] = $prevtext; } } $prevtext = NULL; $first = false; if($node instanceof TagNode) { $node->sub = self::tidyup_ast($node->sub); foreach($node->params as $k => &$v) { $v = self::tidyup_ast($v); } unset($v); } else { /* VariableNode */ foreach($node->arrayfields as &$v) { $v = self::tidyup_ast($v); } unset($v); } $out[] = $node; } } if($prevtext !== NULL) { if($first) { $prevtext->text = ltrim($prevtext->text); } if($prevtext->text != "") { $out[] = $prevtext; } } return $out; } private function parse_text($escapes, $flags, $breakon = NULL, $separator = NULL, $nullaction = NULL, $opentag = NULL, $openedat = -1) { $elems = array(); $astlist = array(); $needles = array( "commentopen" => "", "rawopen" => "", "escape" => '\\', "varcurlyopen" => '${', "var" => '$', ); if($flags & self::PARSE_TAG) { $needles["tagopen"] = 'search_multi($needles); $astlist[] = new TextNode($this->name, $offbefore, $before); switch($which) { case NULL: if($nullaction === NULL) { $elems[] = $astlist; return $elems; } else { call_user_func($nullaction); } break; case "commentopen": list($off, $before, $offbefore) = $this->search(""); if($off === false) { throw new ParseCompileError("ste:comment was not closed", $this->name, $offbefore); } break; case "rawopen": $off_start = $off; list($off, $before, $offbefore) = $this->search(""); if($off === false) { throw new ParseCompileError("ste:rawtext was not closed", $this->name, $off_start); } $astlist[] = new TextNode($this->name, $off_start, $before); break; case "tagopen": $astlist[] = $this->parse_tag($off); break; case "closetagopen": $off_start = $off; $name = $this->get_name(); $this->skip_ws(); $off = $this->off; if($this->next() != ">") { throw new ParseCompileError("Expected '>' in closing ste-Tag", $this->name, $off); } if($opentag === NULL) { throw new ParseCompileError("Found closing ste:$name tag, but no tag was opened", $this->name, $off_start); } if($opentag != $name) { throw new ParseCompileError("Open ste:$opentag was not closed", $this->name, $openedat); } $elems[] = $astlist; return $elems; case "escape": $c = $this->next(); if(mb_strpos($escapes, $c) !== false) { $astlist[] = new TextNode($this->name, $off, $c); } else { $astlist[] = new TextNode($this->name, $off, '\\'); $this->back(); } break; case "shortifopen": $shortelems = $this->parse_short("?{", $off); if(count($shortelems) != 3) { throw new ParseCompileError("A short if tag must have the form ?{..|..|..}", $this->name, $off); } list($cond, $then, $else) = $shortelems; $thentag = new TagNode($this->name, $off); $thentag->name = "then"; $thentag->sub = $then; $elsetag = new TagNode($this->name, $off); $elsetag->name = "else"; $elsetag->sub = $else; $iftag = new TagNode($this->name, $off); $iftag->name = "if"; $iftag->sub = $cond; $iftag->sub[] = $thentag; $iftag->sub[] = $elsetag; $astlist[] = $iftag; break; case "shortcompopen": $shortelems = $this->parse_short("~{", $off); if(count($shortelems) != 3) { throw new ParseCompileError("A short comparasion tag must have the form ~{..|..|..}", $this->name, $off); } // TODO: What will happen, if a tag was in one of the elements? list($a, $op, $b) = $shortelems; $cmptag = new TagNode($this->name, $off); $cmptag->name = "cmp"; $cmptag->params["text_a"] = $a; $cmptag->params["op"] = $op; $cmptag->params["text_b"] = $b; $astlist[] = $cmptag; break; case "sep": $elems[] = $astlist; $astlist = array(); break; case "varcurlyopen": $astlist[] = $this->parse_var($off, true); break; case "var": $astlist[] = $this->parse_var($off, false); break; case "break": $elems[] = $astlist; return $elems; } } $elems[] = $astlist; return $elems; } private function parse_short($shortname, $openedat) { $tplname = $this->name; return $this->parse_text( self::ESCAPES_DEFAULT, /* Escapes */ self::PARSE_SHORT | self::PARSE_TAG, /* Flags */ '}', /* Break on */ '|', /* Separator */ function() use ($shortname, $tplname, $openedat) { /* NULL action */ throw new ParseCompileError("Unclosed $shortname", $tplname, $openedat); }, NULL, /* Open tag */ $openedat /* Opened at */ ); } private function parse_var($openedat, $curly) { $varnode = new VariableNode($this->name, $openedat); $varnode->name = $this->get_name(); if(!$this->eof()) { $varnode->arrayfields = $this->parse_array(); } if(($curly) && ($this->next() != "}")) { throw new ParseCompileError("Unclosed '\${'", $this->name, $openedat); } return $varnode; } private function parse_array() { $tplname = $this->name; $arrayfields = array(); while($this->next() == "[") { $openedat = $this->off - 1; $res = $this->parse_text( self::ESCAPES_DEFAULT, /* Escapes */ 0, /* Flags */ ']', /* Break on */ NULL, /* Separator */ function() use ($tplname, $openedat) { /* NULL action */ throw new ParseCompileError("Unclosed array access '[...]'", $tplname, $openedat); }, NULL, /* Open tag */ $openedat /* Opened at */ ); $arrayfields[] = $res[0]; } $this->back(); return $arrayfields; } private function parse_tag($openedat) { $tplname = $this->name; $this->skip_ws(); $tag = new TagNode($this->name, $openedat); $name = $tag->name = $this->get_name(); $tag->params = array(); $tag->sub = array(); for(;;) { $this->skip_ws(); switch($this->next()) { case '/': /* Self-closing tag */ $this->skip_ws(); if($this->next() != '>') { throw new ParseCompileError("Unclosed opening )", $this->name, $openedat); } return $tag; case '>': $sub = $this->parse_text( self::ESCAPES_DEFAULT, /* Escapes */ self::PARSE_SHORT | self::PARSE_TAG, /* Flags */ NULL, /* Break on */ NULL, /* Separator */ function() use ($name, $tplname, $openedat) { /* NULL action */ throw new ParseCompileError("Open ste:$name tag was not closed", $tplname, $openedat); }, $tag->name, /* Open tag */ $openedat /* Opened at */ ); $tag->sub = $sub[0]; return $tag; default: $this->back(); $param = $this->get_name(); $this->skip_ws(); if($this->next() != '=') { throw new ParseCompileError("Expected '=' after tag parameter name", $this->name, $this->off - 1); } $this->skip_ws(); $quot = $this->next(); if(($quot != '"') && ($quot != "'")) { throw new ParseCompileError("Expected ' or \" after '=' of tag parameter", $this->name, $this->off - 1); } $off = $this->off - 1; $paramval = $this->parse_text( self::ESCAPES_DEFAULT . $quot, /* Escapes */ 0, /* Flags */ $quot, /* Break on */ NULL, /* Separator */ function() use ($quot, $tplname, $off) { /* NULL action */ throw new ParseCompileError("Open tag parameter value ($quot) was not closed", $tplname, $off); }, NULL, /* Open tag */ $off /* Opened at */ ); $tag->params[$param] = $paramval[0]; } } } }