From bd1ad76000b86ee4e4f7cd39ae08c223df1acd56 Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 18 Apr 2020 13:34:19 -0400 Subject: [PATCH] Work on a new template. --- includes/class_template.php | 693 +++++++++++++++++++++++++++++++ includes/class_template_test.php | 47 +++ 2 files changed, 740 insertions(+) create mode 100644 includes/class_template.php create mode 100644 includes/class_template_test.php diff --git a/includes/class_template.php b/includes/class_template.php new file mode 100644 index 0000000..1eca911 --- /dev/null +++ b/includes/class_template.php @@ -0,0 +1,693 @@ +_compiler = new Compiler(); + } + + public function compiler() { return $this->_compiler; } + + public function load($template_name) + { + if (isset($this->_cached[$template_name])) { + return $this->_cached[$template_name]; + } + + $template = $this->_query($template_name); + if ($template) { + return $template; + } + + $template = $this->_load($template_name); + if ($template) { + $this->_store($template_name, $template); + return $template; + } + } + + public function cache(array $template_names) + { + } + + private function _query($template_name) + { + } + + private function _load($template_name) + { + $file = $template_name; + $parser = new Parser($file); + $lexer = new Lexer($parser->parse()); + return $this->_compiler->compile($lexer->lex()); + } +} + +class Template +{ + private $_name; + private $_template; + private $_data = []; + + private static $_executing = null; + + public function __construct($name, $template) + { + $this->_name = $name; + $this->_template = $template; + } + + public function set($key, $value) + { + $this->_data[$key] = $value; + } + + public function replace($data) + { + $this->_data = $data; + } + + public function execute() + { + $old_executing = self::$_executing; + self::$_executing = $this; + + $lambda = function($template) { + eval('$rendered = ' . $template . ';'); + return $rendered; + }; + $result = $lambda($this->_template); + + self::$_executing = $old_executing; + + return $result; + } + + static public function get($key) + { + return self::$_executing->_data[$key]; + } +} + +const TOKEN_CONTENT = 'T_CONTENT'; +const TOKEN_FUNCTION_START = 'T_FUNC_START'; +const TOKEN_FUNCTION_NAME = 'T_FUNC_NAME'; +const TOKEN_FUNCTION_ARG_NAME = 'T_FUNC_ARG_NAME'; +const TOKEN_FUNCTION_ARG_SEP = 'T_FUNC_ARG_SEP'; +const TOKEN_FUNCTION_ARG_VALUE = 'T_FUNC_ARG_VALUE'; +const TOKEN_FUNCTION_CLOSE = 'T_FUNC_CLOSE'; +const TOKEN_FUNCTION_END = 'T_FUNC_END'; + +/** + * The Parser is responsible for turning a string template into an array of + * parsed tokens. + */ +class Parser +{ + const START_TOKEN = ' The parsed token stream. */ + private $_tokens = []; + + public function __construct($stream) + { + $this->_stream = $stream; + $this->_length = strlen($stream); + } + + public function parse() + { + $this->_pos = $this->_start = 0; + $this->_tokens = []; + + while ($this->_pos < $this->_length) { + $last_token = $this->_lastToken(); + $c = $this->_stream[$this->_pos]; + + if ($last_token == TOKEN_FUNCTION_START) { + if (ctype_space($c) || $c == self::SELF_CLOSE || $c == self::CLOSE) { + $this->_pushToken(TOKEN_FUNCTION_NAME, $this->_start, $this->_pos); + if ($c == self::SELF_CLOSE) { + ++$this->_pos; + } else if ($c != self::CLOSE) { + $this->_start = ++$this->_pos; + $this->_scanWhitespace(); + } + continue; + } else if (!ctype_alnum($c)) { + throw new TemplateParseError("Invalid character '$c' in function identifier."); + } + } else if ($last_token == TOKEN_FUNCTION_NAME || $last_token == TOKEN_FUNCTION_ARG_VALUE) { + if ($c == self::CLOSE) { + $last_token_obj = end($this->_tokens); + $this->_pushToken(TOKEN_FUNCTION_CLOSE, $this->_pos, $this->_pos + 1); + if ($this->_stream[$this->_pos - 1] == self::SELF_CLOSE) { + // Self-closing functions insert an end function automatically. + array_push($this->_tokens, new Token(TOKEN_FUNCTION_END, self::END_TOKEN)); + array_push($this->_tokens, new Token(TOKEN_FUNCTION_NAME, $last_token_obj->value)); + array_push($this->_tokens, new Token(TOKEN_FUNCTION_CLOSE, self::CLOSE)); + } + $this->_start = ++$this->_pos; + continue; + } else if ($c == self::ARG_SEP) { + $this->_pushToken(TOKEN_FUNCTION_ARG_NAME, $this->_start, $this->_pos); + $this->_pushToken(TOKEN_FUNCTION_ARG_SEP, $this->_pos, $this->_pos + 1); + $this->_start = ++$this->_pos; + $this->_scanWhitespace(); + continue; + } else if (!ctype_alnum($c)) { + var_dump($this); + throw new TemplateParseError("Invalid character '$c' in function argument."); + } + } else if ($last_token == TOKEN_FUNCTION_ARG_SEP) { + if ($c != self::ARG_VALUE) { + throw new TemplateParseError("Expected '\"' got '$c'."); + } + ++$this->_pos; + $this->_scanValue(); + $this->_pushToken(TOKEN_FUNCTION_ARG_VALUE, $this->_start, $this->_pos); + ++$this->_pos; + $this->_scanWhitespace(); + $this->_start = $this->_pos; + continue; + } else if ($last_token == TOKEN_FUNCTION_END) { + if ($c == self::CLOSE) { + $this->_pushToken(TOKEN_FUNCTION_NAME, $this->_start, $this->_pos); + array_push($this->_tokens, new Token(TOKEN_FUNCTION_CLOSE, self::CLOSE)); + $this->_start = ++$this->_pos; + continue; + } else if (!ctype_alnum($c)) { + throw new TemplateParseError("Invalid character '$c' in close function identifier."); + } + } else if ($last_token == TOKEN_FUNCTION_CLOSE || $last_token == TOKEN_CONTENT) { + if (substr_compare($this->_stream, self::START_TOKEN, $this->_pos, self::START_TOKEN_LEN) == 0) { + $this->_parseContentToken(); + array_push($this->_tokens, new Token(TOKEN_FUNCTION_START, self::START_TOKEN)); + $this->_pos += self::START_TOKEN_LEN; + $this->_start = $this->_pos; + continue; + } else if (substr_compare($this->_stream, self::END_TOKEN, $this->_pos, self::END_TOKEN_LEN) == 0) { + $this->_parseContentToken(); + array_push($this->_tokens, new Token(TOKEN_FUNCTION_END, self::END_TOKEN)); + $this->_pos += self::END_TOKEN_LEN; + $this->_start = $this->_pos; + continue; + } + } + + ++$this->_pos; + } + + $last_token = $this->_lastToken(); + if ($last_token != TOKEN_CONTENT && $last_token != TOKEN_FUNCTION_CLOSE) { + throw new TemplateParseError("Unterminated teplate expression, have $last_token"); + } + + $this->_parseContentToken(); + return $this->_tokens; + } + + private function _pushToken($type, $start, $end) + { + array_push($this->_tokens, new Token($type, $this->_substr($start, $end))); + } + + private function _lastToken() + { + return empty($this->_tokens) ? TOKEN_CONTENT : end($this->_tokens)->type; + } + + private function _scanWhitespace() + { + while ($this->_pos < $this->_length) { + if (!ctype_space($this->_stream[$this->_pos])) { + break; + } + ++$this->_pos; + ++$this->_start; + } + } + + private function _scanValue() + { + $this->_start = $this->_pos; + while ($this->_pos < $this->_length) { + $c = $this->_stream[$this->_pos]; + if ($c == self::ARG_VALUE && $this->_stream[$this->_pos - 1] != '\\') { + return; + } + ++$this->_pos; + } + throw new TemplateParseError('Unexpected end of argument value.'); + } + + private function _parseContentToken() + { + if ($this->_start == $this->_pos) { + return; + } + $this->_pushToken(TOKEN_CONTENT, $this->_start, $this->_pos); + $this->_start = $this->_pos; + } + + private function _substr($start, $end) + { + return substr($this->_stream, $start, $end - $start); + } +} + +class Lexer +{ + private $_tokens; + private $_stack = []; + + public function __construct($tokens) + { + $this->_tokens = $tokens; + } + + public function lex() + { + $template = new TemplateNode(); + array_push($this->_stack, $template); + + $prev_token_type = TOKEN_CONTENT; + foreach ($this->_tokens as $token) { + $top = end($this->_stack); + $bad_token_msg = "Unexpected token type {$token->type}."; + + switch ($token->type) { + case TOKEN_CONTENT: + $top->appendChild(new ContentNode($token->value)); + break; + case TOKEN_FUNCTION_START: + if ($prev_token_type != TOKEN_CONTENT && $prev_token_type != TOKEN_FUNCTION_CLOSE) { + throw new TemplateLexError($bad_token_msg); + } + array_push($this->_stack, new FunctionNode()); + break; + case TOKEN_FUNCTION_NAME: + if ($prev_token_type == TOKEN_FUNCTION_START) { + $top->setName($token->value); + } else if ($prev_token_type == TOKEN_FUNCTION_END) { + if ($top->name() != $token->value) { + throw new TemplateLexError("Invalid nesting: cannot close '{$top->name()}' with '{$token->value}'."); + } + $func = array_pop($this->_stack); + $top = end($this->_stack); + $top->appendChild($func); + } else { + throw new TemplateLexError($bad_token_msg); + } + break; + case TOKEN_FUNCTION_ARG_NAME: + if ($prev_token_type != TOKEN_FUNCTION_NAME && $prev_token_type != TOKEN_FUNCTION_ARG_VALUE) { + throw new TemplateLexError($bad_token_msg); + } + $arg = new ArgumentNode(); + $arg->setKey($token->value); + array_push($this->_stack, $arg); + break; + case TOKEN_FUNCTION_ARG_SEP: + if ($prev_token_type != TOKEN_FUNCTION_ARG_NAME || !($top instanceof ArgumentNode)) { + throw new TemplateLexError($bad_token_msg); + } + break; + case TOKEN_FUNCTION_ARG_VALUE: + if ($prev_token_type != TOKEN_FUNCTION_ARG_SEP || !($top instanceof ArgumentNode)) { + throw new TemplateLexError($bad_token_msg); + } + $arg = array_pop($this->_stack); + $arg->setValue($token->value); + + $func = end($this->_stack); + if (!($func instanceof FunctionNode)) { + throw new TemplateLexError($bad_token_msg); + } + $func->addArgument($arg); + break; + case TOKEN_FUNCTION_CLOSE: + if ($prev_token_type != TOKEN_FUNCTION_NAME && $prev_token_type != TOKEN_FUNCTION_ARG_VALUE) { + throw new TemplateLexError($bad_token_msg); + } + break; + case TOKEN_FUNCTION_END: + if ($prev_token_type != TOKEN_CONTENT && $prev_token_type != TOKEN_FUNCTION_CLOSE) { + throw new TemplateLexError($bad_token_msg); + } + break; + } + + $prev_token_type = $token->type; + } + + if (array_pop($this->_stack) !== $template) { + throw new TemplateLexError('Invalid nesting.'); + } + + return $template; + } +} + +class Compiler +{ + private $_functions = []; + + public function registerDefaults() + { + $this->registerFunction(new VarFunction()); + $this->registerFunction(new IfFunction()); + $this->registerFunction(new PhraseFunction()); + } + + public function registerFunction(TemplateFunction $func) + { + $name = $func->name(); + if (isset($this->_functions[$name])) { + throw new TemplateCompileError("A TemplateFunction has already been registered for '$name'."); + } + $this->_functions[$name] = $func; + } + + public function compile(TemplateNode $template) + { + $output = []; + foreach ($template->children() as $child) { + $output[] = $this->compileNode($child); + } + return implode(' . ', $output); + } + + public function compileNode(Node $node) + { + if ($node instanceof ContentNode) { + return '\'' . $this->singleQuoteEscape($node->value()) . '\''; + } else if ($node instanceof FunctionNode) { + $name = $node->name(); + if (!isset($this->_functions[$name])) { + throw new TemplateCompileError("No function registered for name '$name'."); + } + return $this->_functions[$name]->compile($this, $node); + } else { + throw new TemplateCompileError("Unexpected node type: $node."); + } + } + + public function singleQuoteEscape($value) + { + return str_replace("'", "\\'", $value); + } + + public function doubleQuoteEscape($value) + { + return str_replace('"', '\\"', $value); + } +} + +class VarFunction implements TemplateFunction +{ + public function name() { return 'v'; } + + public function compile(Compiler $compiler, FunctionNode $node) + { + $children = $node->children(); + if (count($children) != 1 || !($children[0] instanceof ContentNode)) { + throw new TemplateCompileError('Template function v must only have 1 content child.'); + } + + $args = $node->arguments(); + if (count($args) > 1) { + throw new TemplateCompileError('Template function v can only have 1 argument.'); + } + + if (count($args) == 1 && !isset($args['as'])) { + throw new TemplateCompileError('Template function v only uses an "as" argument.'); + } + + $as = isset($args['as']) ? $args['as']->value() : 'string'; + $value = $compiler->singleQuoteEscape($children[0]->value()); + $get_value = "\\Bugdar\\Template\\Template::get('$value')"; + + switch ($as) { + case 'int': return "intval($get_value)"; + case 'float': return "floatval($get_value)"; + case 'json': return "json_encode($get_value)"; + case 'string': return "htmlspecialchars($get_value)"; + case 'raw': return $get_value; + default: + throw new TemplateCompileError('Unknown value "as" type "' . $as . '".'); + } + } +} + +class IfFunction implements TemplateFunction +{ + public function name() { return 'if'; } + + public function compile(Compiler $compiler, FunctionNode $node) + { + $children = $node->children(); + + $else_index = -1; + foreach ($children as $i => $child) { + if (($child instanceof FunctionNode) && $child->name() == 'else') { + if ($else_index !== -1) { + throw new TemplateCompileError('More than one else function specified.'); + } + $else_index = $i; + } + } + + $args = $node->arguments(); + if (count($args) != 1 || !isset($args['condition'])) { + throw new TemplateCompileError('Template function if requires exactly 1 argument named condition.'); + } + + $true_children = $children; + $false_children = []; + if ($else_index !== -1) { + $true_children = array_slice($children, 0, $else_index); + $false_children = array_slice($children, $else_index + 1); + } + + $true_branch = $this->_compileBranch($compiler, $true_children); + $false_branch = $this->_compileBranch($compiler, $false_children); + + return ' ((' . $args['condition']->value() . ') ? (' . $true_branch . ') : (' . $false_branch . ')) '; + } + + private function _compileBranch($compiler, $children) + { + if (empty($children)) { + return "''"; + } + + $output = []; + foreach ($children as $child) { + $output[] = $compiler->compileNode($child); + } + return implode(' . ', $output); + } +} + +class PhraseFunction implements TemplateFunction +{ + public function name() { return 'p'; } + + public function compile(Compiler $compiler, FunctionNode $node) + { + $children = $node->children(); + if (sizeof($node->children()) != 1) { + throw new TemplateCompileError("Template function p requires exactly 1 child."); + } + if (!($children[0] instanceof ContentNode)) { + throw new TemplateCompileError("Template function p only suports content children."); + } + + $content = $compiler->compileNode($children[0]); + $string = " T($content) "; + + $args = $node->arguments(); + if (empty($args)) { + return $string; + } + + $args = $this->_compileArgs($compiler, $args); + + return " sprintf($string, " . implode(', ', $args) . ') '; + } + + private function _compileArgs(Compiler $compiler, $args) + { + for ($i = 1; $i <= sizeof($args); ++$i) { + if (!isset($args[$i])) { + throw new TemplateCompileError("Template function p missing argument $i."); + } + } + sort($args); + + $compiled = []; + foreach ($args as $arg) { + $compiled[] = '"' . $compiler->doubleQuoteEscape($arg->value()) . '"'; + } + + return $compiled; + } +} + +interface TemplateFunction +{ + public function name(); + + public function compile(Compiler $compiler, FunctionNode $node); +} + +class Token +{ + /** @var int The TOKEN_ type. */ + public $type; + + /** @var any The value/content of the token. Type dependent. */ + public $value; + + public function __construct($type, $value=NULL) + { + $this->type = $type; + $this->value = $value; + } +} + +abstract class Node +{ + private $_children = []; + + public function appendChild(Node $child) + { + $this->_children[] = $child; + } + + public function children() { return $this->_children; } +} + +class TemplateNode extends Node {} + +class ContentNode extends Node +{ + private $_value; + + public function __construct($value) + { + $this->_value = $value; + } + + public function value() { return $this->_value; } + + public function appendChild(Node $child) + { + throw new TemplateLexError('ContentNodes cannot have children.'); + } +} + +class FunctionNode extends Node +{ + private $_name; + private $_args = []; + + public function setName($name) + { + if ($this->_name !== NULL) { + throw new TemplateLexError('Name already set.'); + } + $this->_name = $name; + } + public function name() { return $this->_name; } + + public function addArgument(ArgumentNode $arg) + { + if (isset($this->_args[$arg->key()])) { + throw new TemplateLexError("Duplicate argument '{$arg->key()}' in function '{$this->_name}'."); + } + $this->_args[$arg->key()] = $arg; + } + public function arguments() { return $this->_args; } +} + +class ArgumentNode extends Node +{ + private $_key; + private $_value; + + public function setKey($key) + { + if ($this->_key !== NULL) { + throw new TemplateLexError('Key already set.'); + } + $this->_key = $key; + } + public function key() { return $this->_key; } + + public function setValue($value) + { + if ($this->_value !== NULL) { + throw new TemplateLexError('Value already set.'); + } + $this->_value = $value; + } + public function value() { return $this->_value; } +} diff --git a/includes/class_template_test.php b/includes/class_template_test.php new file mode 100644 index 0000000..1794ccd --- /dev/null +++ b/includes/class_template_test.php @@ -0,0 +1,47 @@ +'; + +$parser = new Bugdar\Template\Parser( + 'Hello world truefalse Hello again %1$s' . + ' moo ' . + 'Test %1$s %2$s' +); +$tokens = $parser->parse(); +var_dump($parser); + +$lexer = new Bugdar\Template\Lexer($tokens); +$ast = $lexer->lex(); +print_r($ast); + +$compiler = new Bugdar\Template\Compiler(); +$compiler->registerDefaults(); +$template = $compiler->compile($ast); +var_dump($template); + +function T($x) { return $x; } + +$tpl = new Bugdar\Template\Template('demo', $template); +$tpl->set('moo', 'Injected Variable!bold'); +var_dump($tpl->execute()); -- 2.43.5