diff options
Diffstat (limited to 'php/src')
-rw-r--r-- | php/src/luigi-template.php | 393 |
1 files changed, 393 insertions, 0 deletions
diff --git a/php/src/luigi-template.php b/php/src/luigi-template.php new file mode 100644 index 0000000..69e3fb8 --- /dev/null +++ b/php/src/luigi-template.php @@ -0,0 +1,393 @@ +<?php +declare(strict_types = 1); + +namespace Luigi; + +const VERSION = '0.4.2'; + +class Error extends \Exception {}; + +class UnknownTypeError extends Error { + public $type, + $name; + + public function __construct(string $type, string $name) { + $this->type = $type; + $this->name = $name; + parent::__construct("unknown $type: $name"); + } +}; + +final class UnknownTemplateError extends UnknownTypeError { + public function __construct(string $name) { + parent::__construct('template', $name); + } +}; + +final class UnknownFilterError extends UnknownTypeError { + public function __construct(string $name) { + parent::__construct('filter', $name); + } +}; + +final class UnknownKeyError extends UnknownTypeError { + public function __construct(string $name) { + parent::__construct('key', $name); + } +}; + +final class MissingFilterParameterError extends Error { + public $filter_name; + + public function __construct(string $filter_name) { + $this->filter_name = $filter_name; + parent::__construct("missing required filter parameter for filter $filter_name"); + } +}; + +final class InvalidTemplateError extends Error { + public $template; + + public function __construct(string $template) { + $this->template = $template; + parent::__construct("invalid template: $template"); + } +}; + +namespace Luigi; + +final class RunContext { + public $args, + $filters; + + public function __construct(array $args, array $filters) { + $this->args = $args; + $this->filters = $filters; + } +}; + +final class TemplateFilter { + private $name, + $args; + + public function __construct(string $name, array $args) { + $this->name = $name; + $this->args = $args; + } + + public function run($v, array &$args, array &$filters) { + if (!isset($filters[$this->name])) { + throw new UnknownFilterError($this->name); + } + + # get callback + $cb = $filters[$this->name]; + + # invoke callback, return result + return $cb($v, $this->args, $args); + } +}; + +namespace Luigi\Parser; + +use Luigi\RunContext; +use Luigi + +abstract class Token { + public function run(array $args, array $filters) : string; +}; + +final class LiteralToken extends Token { + private $val; + + public function __construct(string $val) { + $this->val = $val; + } + + public function run(array &$args, array &$filters) : string { + return $this->val; + } +}; + +final class FilterToken extends Token { + private $key, + $filters; + + public function __construct(string $key, array $filters) { + $this->key = $key; + $this->filters = $filters; + } + + public function run(array &$args, array &$filters) : string { + if (!isset($args[$this->key])) { + throw new UnknownKeyError($this->key); + } + + # get initial value + $r = $args[$this->key]; + + if ($this->filters && count($this->filters) > 0) { + # pass value through filters + $r = array_reduce($this->filters, function($r, $f) use (&$args, &$filters) { + return $f->run($r, $args, $filters); + }, $r); + } + + # return result + return $r; + } +}; + +const TOKEN_RE = '/ + # match opening brace + %\{ + + # match optional whitespace + \s* + + # match key + (?<key>[^\s\|\}]+) + + # match filter(s) + (?<filters>(\s*\|(\s*[^\s\|\}]+)+)*) + + # match optional whitespace + \s* + + # match closing brace + \} + + # or match up all non-% chars or a single % char + | (?<text>[^%]* | %) +/mx'; + +const FILTER_RE = '/ + # match filter name + (?<name>\S+) + + # match filter arguments (optional) + (?<args>(\s*\S+)*) + + # optional trailing whitespace + \s* +/mx'; + +const DELIM_FILTERS_RE = '/\s*\|\s*/m'; + +const DELIM_ARGS_RE = '/\s+/m'; + +function parse_filters(string $filters) : array { + # split into individual filters + $r = []; + + foreach (preg_split(DELIM_FILTERS_RE, $filters) as $f) { + # trim whitespace + $f = trim($f); + + # skip empty filters + if (!$f) + continue; + + # match filter + $md = []; + if (!preg_match(FILTER_RE, $f, $md)) { + throw new UnknownFilterError($f); + } + + # add filter to results + $r[] = new TemplateFilter($md['name'], (count($md) > 2) ? preg_split( + DELIM_ARGS_RE, + trim($md['args']) + ) : []); + } + + # return results + return $r; +} + +function parse_template(string $template) : array { + # build list of matches + $matches = []; + $num_matches = preg_match_all(TOKEN_RE, $template, $matches, PREG_SET_ORDER); + + # check for error + if ($num_matches === false) { + throw new InvalidTemplateError($template); + } + + # map matches to tokens + return array_map(function($m) { + if ($m['key'] !== '') { + # filter token + return new FilterToken($m['key'], parse_filters($m['filters'])); + } else { + # literal token + return new LiteralToken($m['text']); + } + }, $matches); +} + +namespace Luigi; + +public $FILTERS = [ + 'h' => function($s) { + return htmlspecialchars($v, ENT_QUOTES); + }, + + 'u' => function($s) { + return urlencode($v); + }, + + 'json' => function($v) { + return json_encode($v); + }, + + 'hash' => function($v, $args) { + if (count($args) !== 1) { + throw new MissingFilterParameterError('hash'); + } + + return hash($args[0], $v); + }, + + 'base64' => function($v) { + return base64_encode($v); + }, + + 'nl2br' => function($v) { + return nl2br($v); + }, + + 'uc' => function($v) { + return strtoupper($v); + }, + + 'lc' => function($v) { + return strtolower($v); + }, + + 'trim' => function($v) { + return trim($v); + }, + + 'rtrim' => function($v) { + return rtrim($v); + }, + + 'ltrim' => function($v) { + return ltrim($v); + }, + + 's' => function($v) { + return ($v == 1) ? '' : 's'; + }, + + 'strlen' => function($v) { + return strlen($v); + }, + + 'count' => function($v) { + return count($v); + }, + + 'key' => function($v, $args) { + if (count($args) !== 1) { + throw new MissingFilterParameterError('key'); + } + + # get key + $key = $args[0]; + + # make sure key exists + if (!isset($v[$key])) { + throw new UnknownKeyError($key); + } + + # return key + return $v[$key]; + }, +]; + +final class Template { + private $template, + $filters, + $tokens; + + public function __construct( + string $template, + array $custom_filters = [] + ) { + global $FILTERS; + + $this->template = $template; + $this->filters = count($custom_filters) ? $custom_filters : $FILTERS; + + # parse template into list of tokens + $this->tokens = Parser\parse_template($template); + } + + public function run(array $args = []) : string { + # php sucks + $me = $this; + + return join('', array_map(function($token) use ($me, $args) { + return $token->run($args, $this->filters); + }, $this->tokens)); + } + + public static function once($str, $args = [], array $filters = []) { + $t = new Template($str, $filters); + return $t->run($args); + } +}; + +public function run( + string $template, + array $args = [], + array $filters = [] +) : string { + $t = new Template($template, $filters); + return $t->run($args); +} + +final class Cache implements \ArrayAccess { + private $templates, + $filters, + $lut = []; + + public function __construct(array $templates, array $filters = []) { + $this->templates = $templates; + $this->filters = $filters; + } + + public function offsetExists($key) : bool { + return isset($this->templates[$key]); + } + + public function offsetGet($key) { + if (isset($this->lut[$key])) { + return $this->lut[$key]; + } else if (isset($this->templates[$key]) { + $this->lut[$key] = new Template($this->templates[$key], $this->filters); + return $this->lut[$key]; + } else { + throw new UnknownTemplateError($key); + } + } + + public function offsetUnset($key) { + delete($this->lut[$key]); + delete($this->templates[$key]); + } + + public function offsetSet($key, $val) : bool { + delete($this->lut[$key]); + $this->templates[$key] = $val; + return isset($this->templates[$key]); + } + + + public function run(string $key, array $args = []) : string { + return $this->offsetGet($key)->run($args); + } +}; |