summaryrefslogtreecommitdiff
path: root/php/src
diff options
context:
space:
mode:
Diffstat (limited to 'php/src')
-rw-r--r--php/src/luigi-template.php393
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);
+ }
+};