summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--php/template-test.php64
-rw-r--r--php/template.php370
2 files changed, 434 insertions, 0 deletions
diff --git a/php/template-test.php b/php/template-test.php
new file mode 100644
index 0000000..e7d5587
--- /dev/null
+++ b/php/template-test.php
@@ -0,0 +1,64 @@
+<?php
+
+error_reporting(E_ALL | E_STRICT);
+
+require 'template.php';
+
+# build template string
+$template_str = join("\n", array(
+ # test basic templates
+ "%{greet}, %{name}!",
+
+ # test filters and filters with parameters
+ "Your name hashes to: %{
+ name
+ |
+ hash
+ sha1
+ |
+ uc
+ }",
+
+ # test custom filter
+ "Your custom filtered name is: %{name|custom}",
+
+ # test custom filter with arguments
+ "Your custom_args name is: %{name|custom_args foo bar baz}",
+
+ # test whitespace in filters
+ "random test: %{name | hash sha512 | base64| uc }",
+
+ # test pluralize filter
+ 'pluralize test (0): %{count_0} item%{count_0 | s}',
+ 'pluralize test (1): %{count_1} item%{count_1 | s}',
+ 'pluralize test (10): %{count_10} item%{count_10 | s}',
+)) . "\n";
+
+Luigi\Filters::add(array(
+ 'custom' => function() {
+ return 'custom';
+ },
+
+ 'custom_args' => function($v, $args) {
+ return join(',', array_map(function($arg) use ($v) {
+ return "$arg$v";
+ }, $args));
+ },
+));
+
+# build template
+$t = new Luigi\Template($template_str);
+
+$args = array(
+ 'greet' => 'hello',
+ 'name' => 'paul',
+ 'count_0' => 0,
+ 'count_1' => 1,
+ 'count_10' => 10,
+);
+
+# print results
+echo $t->run($args);
+
+# test static invocation
+echo Luigi\Template::run_once($template_str, $args);
diff --git a/php/template.php b/php/template.php
new file mode 100644
index 0000000..cec1ff6
--- /dev/null
+++ b/php/template.php
@@ -0,0 +1,370 @@
+<?php
+
+namespace Luigi;
+
+const VERSION = '0.4.0';
+
+final class Error extends \Exception {};
+
+final class Filters {
+ private static $filters = null;
+
+ public static function init() {
+ if (self::$filters !== null)
+ return;
+
+ self::$filters = array(
+ 'h' => function($s) {
+ return htmlspecialchars($v);
+ },
+
+ 'u' => function($s) {
+ return urlencode($v);
+ },
+
+ 'json' => function($v) {
+ return json_encode($v);
+ },
+
+ 'hash' => function($v, $args) {
+ $algo = (count($args) == 1) ? $args[0] : 'md5';
+ return hash($algo, $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);
+ },
+ );
+ }
+
+ public static function get($key) {
+ self::init();
+
+ if (!isset(self::$filters[$key]))
+ throw new Error("unknown filter: $key");
+
+ return self::$filters[$key];
+ }
+
+ public static function add(array $filters) {
+ self::init();
+ self::$filters = array_merge(self::$filters, $filters);
+ }
+};
+
+final class LegacyParser {
+ private static $RES = array(
+ 'action' => '/
+ # match opening brace
+ %\{\s*
+
+ # match key
+ ([\w_-]+)
+
+ # match filter(s)
+ ((\s*\|\s*\w+\s*(\([\w\s,-]+\))?)*)
+
+ # match closing brace
+ \}
+
+ # or match up all non-% chars or a single % char
+ | ([^%]* | %)
+ /mx',
+
+ 'filter' => '/
+ # match filter name
+ ([\w_-]+)
+
+ # optional trailing whitespace
+ \s*
+
+ # optional filter arguments
+ (\(([\w\s_,-]+)\))?
+ /mx',
+ );
+
+ 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('error matching template');
+
+ # walk over matches and build list of actions
+ $r = array_map(function($m) {
+ if ($m[1] !== '') {
+ # key and filters
+ return array(
+ 'type' => 'action',
+ 'key' => $m[1],
+ 'filters' => self::parse_filters($m[2]),
+ );
+ } else {
+ # literal text
+ return array(
+ 'type' => 'text',
+ 'text' => $m[5],
+ );
+ }
+ }, $matches);
+
+ # return result
+ return $r;
+ }
+
+ public static function parse_filters($filters) {
+ # split into individual filters
+ $r = array();
+ foreach (preg_split('/\s*\|\s*/', $filters) as $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[1],
+
+ # filter arguments
+ 'args' => (count($md) > 2) ? preg_split(
+ '/\s*,\s*/',
+ trim($md[3])
+ ) : array(),
+ );
+ }
+
+ # return results
+ return $r;
+ }
+};
+
+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);
+
+ # return result
+ return $r;
+ }
+
+ 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(),
+ );
+ }
+
+ # return results
+ return $r;
+ }
+};
+
+final class Template {
+ private $template, $actions, $o;
+
+ public function __construct($template, array $o = array()) {
+ $this->template = $template;
+ $this->o = $o;
+
+ # parse template into list of actions
+ $this->actions = Parser::parse_template($template);
+ }
+
+ public function run(array $args = array()) {
+ # php sucks
+ $me = $this;
+
+ 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 = Filters::get($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));
+ }
+
+ public static function run_once($str, array $args = array()) {
+ $t = new Template($str);
+ return $t->run($args);
+ }
+};
+
+final class Cache {
+ private $templates, $o, $lut = array();
+
+ public function __construct(array $templates, array $o = array()) {
+ $this->templates = $templates;
+ $this->o = $o;
+ }
+
+ public function get($key) {
+ if (!isset($this->lut[$key])) {
+ if (!isset($this->templates[$key]))
+ throw new Error("unknown template: $key");
+
+ # lazy-load template
+ $this->lut[$key] = new Template($this->templates[$key], $this->o);
+ }
+
+ # return result
+ return $this->lut[$key];
+ }
+
+ public function run($key, array $args = array()) {
+ return $this->get($key)->run($args);
+ }
+};