aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--php/luigi-template.php542
1 files changed, 326 insertions, 216 deletions
diff --git a/php/luigi-template.php b/php/luigi-template.php
index df4f38e..69e3fb8 100644
--- a/php/luigi-template.php
+++ b/php/luigi-template.php
@@ -1,283 +1,393 @@
<?php
+declare(strict_types = 1);
namespace Luigi;
-const VERSION = '0.4.0';
+const VERSION = '0.4.2';
-final class Error extends \Exception {};
+class Error extends \Exception {};
-final class Filters {
- private static $filters = null;
+class UnknownTypeError extends Error {
+ public $type,
+ $name;
- public static function init() {
- if (self::$filters !== null)
- return;
+ 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);
+ }
+};
- self::$filters = array(
- 'h' => function($s) {
- return htmlspecialchars($v);
- },
+final class UnknownFilterError extends UnknownTypeError {
+ public function __construct(string $name) {
+ parent::__construct('filter', $name);
+ }
+};
- 'u' => function($s) {
- return urlencode($v);
- },
+final class UnknownKeyError extends UnknownTypeError {
+ public function __construct(string $name) {
+ parent::__construct('key', $name);
+ }
+};
- 'json' => function($v) {
- return json_encode($v);
- },
+final class MissingFilterParameterError extends Error {
+ public $filter_name;
- 'hash' => function($v, $args) {
- $algo = (count($args) == 1) ? $args[0] : 'md5';
- return hash($algo, $v);
- },
+ public function __construct(string $filter_name) {
+ $this->filter_name = $filter_name;
+ parent::__construct("missing required filter parameter for filter $filter_name");
+ }
+};
- 'base64' => function($v) {
- return base64_encode($v);
- },
+final class InvalidTemplateError extends Error {
+ public $template;
- 'nl2br' => function($v) {
- return nl2br($v);
- },
+ public function __construct(string $template) {
+ $this->template = $template;
+ parent::__construct("invalid template: $template");
+ }
+};
- 'uc' => function($v) {
- return strtoupper($v);
- },
+namespace Luigi;
- 'lc' => function($v) {
- return strtolower($v);
- },
+final class RunContext {
+ public $args,
+ $filters;
- 'trim' => function($v) {
- return trim($v);
- },
+ public function __construct(array $args, array $filters) {
+ $this->args = $args;
+ $this->filters = $filters;
+ }
+};
- 'rtrim' => function($v) {
- return rtrim($v);
- },
+final class TemplateFilter {
+ private $name,
+ $args;
- 'ltrim' => function($v) {
- return ltrim($v);
- },
+ public function __construct(string $name, array $args) {
+ $this->name = $name;
+ $this->args = $args;
+ }
- 's' => function($v) {
- return ($v == 1) ? '' : 's';
- },
+ public function run($v, array &$args, array &$filters) {
+ if (!isset($filters[$this->name])) {
+ throw new UnknownFilterError($this->name);
+ }
- 'strlen' => function($v) {
- return strlen($v);
- },
+ # get callback
+ $cb = $filters[$this->name];
- 'count' => function($v) {
- return count($v);
- },
- );
+ # invoke callback, return result
+ return $cb($v, $this->args, $args);
}
+};
- public static function get($key) {
- self::init();
+namespace Luigi\Parser;
+
+use Luigi\RunContext;
+use Luigi
+
+abstract class Token {
+ public function run(array $args, array $filters) : string;
+};
- if (!isset(self::$filters[$key]))
- throw new Error("unknown filter: $key");
+final class LiteralToken extends Token {
+ private $val;
- return self::$filters[$key];
+ public function __construct(string $val) {
+ $this->val = $val;
}
- public static function add(array $filters) {
- self::init();
- self::$filters = array_merge(self::$filters, $filters);
+ public function run(array &$args, array &$filters) : string {
+ return $this->val;
}
};
-final class Parser {
- private static $RES = array(
- 'action' => '/
- # 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',
-
- 'filter' => '/
- # match filter name
- (?<name>\S+)
-
- # match filter arguments (optional)
- (?<args>(\s*\S+)*)
-
- # optional trailing whitespace
- \s*
- /mx',
-
- 'delim_filters' => '/\s*\|\s*/m',
- 'delim_args' => '/\s+/m',
- );
-
- public static function parse_template($template) {
- # build list of matches
- $matches = array();
- $num_matches = preg_match_all(
- self::$RES['action'],
- $template,
- $matches,
- PREG_SET_ORDER
- );
-
- # check for error
- if ($num_matches === false)
- throw new Error("invalid template: $template");
-
- # walk over matches and build list of actions
- $r = array_map(function($m) {
- if ($m['key'] !== '') {
- # key and filters
- return array(
- 'type' => 'action',
- 'key' => $m['key'],
- 'filters' => self::parse_filters($m['filters']),
- );
- } else {
- # literal text
- return array(
- 'type' => 'text',
- 'text' => $m['text'],
- );
- }
- }, $matches);
+final class FilterToken extends Token {
+ private $key,
+ $filters;
- # return result
- return $r;
+ public function __construct(string $key, array $filters) {
+ $this->key = $key;
+ $this->filters = $filters;
}
- public static function parse_filters($filters) {
- # split into individual filters
- $r = array();
- foreach (preg_split(self::$RES['delim_filters'], $filters) as $f) {
- # trim whitespace
- $f = trim($f);
-
- # skip empty filters
- if (!$f)
- continue;
-
- # match filter
- $md = array();
- if (!preg_match(self::$RES['filter'], $f, $md))
- throw new Error("invalid filter: $f");
-
- # add filter to results
- $r[] = array(
- # filter name
- 'name' => $md['name'],
-
- # filter arguments
- 'args' => (count($md) > 2) ? preg_split(
- self::$RES['delim_args'],
- trim($md['args'])
- ) : array(),
- );
+ 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 results
+ # return result
return $r;
}
};
-final class Template {
- private $template, $filters, $actions
+const TOKEN_RE = '/
+ # match opening brace
+ %\{
- public function __construct($template, $filters = null) {
- $this->template = $template;
- $this->filters = $filters;
+ # match optional whitespace
+ \s*
+
+ # match key
+ (?<key>[^\s\|\}]+)
+
+ # match filter(s)
+ (?<filters>(\s*\|(\s*[^\s\|\}]+)+)*)
- # parse template into list of actions
- $this->actions = Parser::parse_template($template);
+ # 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'])
+ ) : []);
}
- public function run(array $args = array()) {
- # php sucks
- $me = $this;
+ # return results
+ return $r;
+}
- return join('', array_map(function($row) use ($me, $args) {
- if ($row['type'] == 'text') {
- # literal text
- return $row['text'];
- } else if ($row['type'] == 'action') {
- # template key (possibly with filters)
-
- # check value
- if (!isset($args[$row['key']]))
- throw new Error("unknown key: {$row['key']}");
-
- # pass value through filters and return result
- return array_reduce($row['filters'], function($r, $f) use ($me, $args) {
- # get filter
- $fn = $me->get_filter($f['name']);
-
- # call filter and return result
- return call_user_func($fn, $r, $f['args'], $args, $me);
- }, $args[$row['key']]);
- } else {
- # should never be reached
- throw new Error("unknown action type: {$row['type']}");
- }
- }, $this->actions));
+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);
}
- public function get_filter($key) {
- if ($this->filters) {
- # use custom filters
- return $this->filters->get($key);
+ # map matches to tokens
+ return array_map(function($m) {
+ if ($m['key'] !== '') {
+ # filter token
+ return new FilterToken($m['key'], parse_filters($m['filters']));
} else {
- # default to built-in filters
- return Filters::get($key);
+ # 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 static function run_once($str, $args = array(), $filters = null) {
+ 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);
}
};
-final class Cache {
- private $templates, $filters, $lut = array();
-
- public function __construct(array $templates, $filters = null) {
+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;
- $this->o = $o;
}
- public function get($key) {
- if (!isset($this->lut[$key])) {
- if (!isset($this->templates[$key]))
- throw new Error("unknown template: $key");
+ public function offsetExists($key) : bool {
+ return isset($this->templates[$key]);
+ }
- # lazy-load template
+ 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);
}
+ }
- # return result
- return $this->lut[$key];
+ public function offsetUnset($key) {
+ delete($this->lut[$key]);
+ delete($this->templates[$key]);
}
- public function run($key, array $args = array()) {
- return $this->get($key)->run($args);
+ 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);
}
};