aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--php/composer.json2
-rw-r--r--php/src/Template.php604
-rw-r--r--php/tests/TemplateTest.php16
3 files changed, 601 insertions, 21 deletions
diff --git a/php/composer.json b/php/composer.json
index 9cf4a1e..f0bdb51 100644
--- a/php/composer.json
+++ b/php/composer.json
@@ -22,7 +22,7 @@
"autoload": {
"psr-4": {
- "Luigi\\": "src/"
+ "Pablotron\\Luigi\\": "src/"
}
},
diff --git a/php/src/Template.php b/php/src/Template.php
index da905d7..34dfe09 100644
--- a/php/src/Template.php
+++ b/php/src/Template.php
@@ -1,16 +1,51 @@
<?php
-declare(strict_types = 1);
+/**
+ * Fast string templating library for JavaScript, PHP, Ruby, and Java.
+ *
+ * @author Paul Duncan <pabs@pablotron.org>
+ * @copyright 2010-2018 Paul Duncan <pabs@pablotron.org>
+ * @license MIT
+ * @package Pablotron\Luigi
+ */
-namespace Luigi;
+declare(strict_types = 1);
+/**
+ * Luigi Template namespace.
+ *
+ * @api
+ */
+namespace Pablotron\Luigi;
+
+/**
+ * Current version of Luigi Template.
+ *
+ * @api
+ */
const VERSION = '0.4.2';
-class Error extends \Exception {};
-
-class UnknownTypeError extends Error {
+/**
+ * Base class for all exceptions raised by Luigi Template.
+ */
+class LuigiError extends \Exception {};
+
+/**
+ * Base class for all all unknown type errors.
+ */
+class UnknownTypeError extends LuigiError {
+ /**
+ * @var string $type Unknown item type name ("method", "template", etc).
+ * @var string $name Unknown item name.
+ */
public $type,
$name;
+ /**
+ * Create a new UnknownTypeError error.
+ *
+ * @param string $type Unknown item type name.
+ * @param string $name Unknown item name.
+ */
public function __construct(string $type, string $name) {
$this->type = $type;
$this->name = $name;
@@ -18,61 +53,153 @@ class UnknownTypeError extends Error {
}
};
+/**
+ * Thrown when attempting to get an unknown template from a Cache.
+ *
+ * @see Cache
+ */
final class UnknownTemplateError extends UnknownTypeError {
+ /**
+ * Create a new UnknownTemplateError.
+ *
+ * @param string $name Unknown template name.
+ */
public function __construct(string $name) {
parent::__construct('template', $name);
}
};
+/**
+ * Thrown when attempting to apply an unknown filter.
+ */
final class UnknownFilterError extends UnknownTypeError {
+ /**
+ * Create a new UnknownFilterError.
+ *
+ * @param string $name Unknown filter name.
+ */
public function __construct(string $name) {
parent::__construct('filter', $name);
}
};
+/**
+ * Thrown when attempting to get an unknown key.
+ */
final class UnknownKeyError extends UnknownTypeError {
+ /**
+ * Create a new UnknownKeyError.
+ *
+ * @param string $name Unknown key name.
+ */
public function __construct(string $name) {
parent::__construct('key', $name);
}
};
-final class MissingFilterParameterError extends Error {
+/**
+ * Thrown when trying to use a filter with with a missing parameter.
+ */
+final class MissingFilterParameterError extends LuigiError {
+ /** @var string $filter_name Name of filter. */
public $filter_name;
+ /**
+ * Create a new MissingFilterParameterError.
+ *
+ * @param string $filter_name Name of filter.
+ */
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 {
+/**
+ * Thrown when trying to parse an invalid template string.
+ */
+final class InvalidTemplateError extends LuigiError {
+ /** @var string $template Template string. */
public $template;
+ /**
+ * Create a new InvalidTemplateError.
+ *
+ * @param string $template Template string.
+ */
public function __construct(string $template) {
$this->template = $template;
parent::__construct("invalid template: $template");
}
};
+/**
+ * Wrapper for context during Template#run().
+ *
+ * @internal
+ */
final class RunContext {
+ /**
+ * @var array $args Hash of arguments.
+ * @var array $filters Hash of filters.
+ */
public $args,
$filters;
+/**
+ * Create a RunContext.
+ *
+ * @internal
+ *
+ * @param array $args Arguments hash.
+ * @param array $filters Filters hash.
+ */
public function __construct(array $args, array $filters) {
$this->args = $args;
$this->filters = $filters;
}
};
+/**
+ * Parsed filter name and arguments.
+ *
+ * @internal
+ */
final class TemplateFilter {
+ /**
+ * @var string $name Filter name.
+ * @var array $args Filter arguments.
+ */
private $name,
$args;
+ /**
+ * Create a TemplateFilter.
+ *
+ * @internal
+ *
+ * @param string $name Filter name.
+ * @param array $args Filter arguments.
+ */
public function __construct(string $name, array $args) {
$this->name = $name;
$this->args = $args;
}
+ /**
+ * Run filter on given value, arguments, and filter set.
+ *
+ * @internal
+ *
+ * @param string $v Input value.
+ * @param array $args Hash passed to Template#run().
+ * @param array $filters Hash of filters.
+ *
+ * @return mixed Filter result.
+ *
+ * @throws UnknownFilterError If this filter does not exist in filter
+ * hash.
+ */
public function run($v, array &$args, array &$filters) {
if (!isset($filters[$this->name])) {
throw new UnknownFilterError($this->name);
@@ -86,32 +213,97 @@ final class TemplateFilter {
}
};
-
+/**
+ * Abstract base class for parser tokens.
+ *
+ * @internal
+ */
abstract class Token {
+ /**
+ * Apply this token.
+ *
+ * @internal
+ *
+ * @param RunContext $ctx Run context.
+ *
+ * @return string
+ */
public abstract function run(RunContext &$ctx) : string;
};
+/**
+ * Literal parser token.
+ *
+ * @internal
+ */
final class LiteralToken extends Token {
+ /** @var string $val Literal value. */
private $val;
+ /**
+ * Create a new LiteralToken.
+ *
+ * @internal
+ *
+ * @param string $val Literal string.
+ */
public function __construct(string $val) {
$this->val = $val;
}
+ /**
+ * Returns the literal value.
+ *
+ * @internal
+ *
+ * @param RunContext $ctx Run context.
+ *
+ * @return string Literal value.
+ */
public function run(RunContext &$ctx) : string {
return $this->val;
}
};
+/**
+ * Filter parser token.
+ *
+ * @internal
+ */
final class FilterToken extends Token {
+ /**
+ * @var string $key Argument name.
+ * @var array $filters Array of TemplateFilter instances.
+ */
private $key,
$filters;
+ /**
+ * Create a new LiteralToken.
+ *
+ * @internal
+ *
+ * @param string $key Argument name.
+ * @param array $filters Array of TemplateFilter instances.
+ */
public function __construct(string $key, array $filters) {
$this->key = $key;
$this->filters = $filters;
}
+ /**
+ * Get key from arguments hash and apply filters to it, then return
+ * the result.
+ *
+ * @internal
+ *
+ * @param RunContext $ctx Run context.
+ *
+ * @return string Filtered result.
+ *
+ * @throws UnknownKeyError If the given key does not exist in the
+ * arguments hash.
+ */
public function run(RunContext &$ctx) : string {
# check key
if (!isset($ctx->args[$this->key])) {
@@ -133,6 +325,11 @@ final class FilterToken extends Token {
}
};
+/**
+ * Token parser regular expression.
+ *
+ * @internal
+ */
const TOKEN_RE = '/
# match opening brace
%\{
@@ -156,6 +353,11 @@ const TOKEN_RE = '/
| (?<text>[^%]* | %)
/mx';
+/**
+ * Filter parser regular expression.
+ *
+ * @internal
+ */
const FILTER_RE = '/
# match filter name
(?<name>\S+)
@@ -167,10 +369,32 @@ const FILTER_RE = '/
\s*
/mx';
+/**
+ * Filter delimiter regular expression.
+ *
+ * @internal
+ */
const DELIM_FILTERS_RE = '/\s*\|\s*/m';
+/**
+ * Filter argument delimiter regular expression.
+ *
+ * @internal
+ */
const DELIM_ARGS_RE = '/\s+/m';
+/**
+ * Parse a string containing a filter clause into an array of
+ * TemplateFilter instances.
+ *
+ * @internal
+ *
+ * @param string $filters Input filter string.
+ *
+ * @return array Array of TemplateFilter instances.
+ *
+ * @throws UnknownFilterError if a filter clause could not be parsed.
+ */
function parse_filters(string $filters) : array {
# split into individual filters
$r = [];
@@ -200,6 +424,17 @@ function parse_filters(string $filters) : array {
return $r;
}
+/**
+ * Parse a template string into an array of Token instances.
+ *
+ * @internal
+ *
+ * @param string $template Input template string.
+ *
+ * @return array Array of Token instances.
+ *
+ * @throws InvalidTemplateError if the template could not be parsed.
+ */
function parse_template(string $template) : array {
# build list of matches
$matches = [];
@@ -226,9 +461,64 @@ function parse_template(string $template) : array {
}, []);
}
+/**
+ * Static class containing global filter map.
+ *
+ * The built-in default filters are:
+ *
+ * * `h`: HTML-escape input string (including quotes).
+ * * `u`: URL-escape input string.
+ * * `json`: JSON-encode input value.
+ * * `hash`: Hash input value. Requires hash algorithm as parameter.
+ * * `base64`: Base64-encode input string.
+ * * `nl2br`: Convert newlines to `\<br/\>` elements.
+ * * `uc`: Upper-case input string.
+ * * `lc`: Lower-case input string.
+ * * `trim`: Trim leading and trailing whitespace from input string.
+ * * `ltrim`: Trim leading whitespace from input string.
+ * * `rtrim`: Trim trailing whitespace from input string.
+ * * `s`: Return '' if input number is 1, and 's' otherwise (used for
+ * pluralization).
+ * * `strlen`: Return the length of the input string.
+ * * `count`: Return the number of elements in the input array.
+ * * `key`: Get the given key from the input array. Requires key as a
+ * parameter.
+ *
+ * You can add your own filters to the default set of filters by
+ * modifying `\Luigi\Filters::$FILTERS`, like so:
+ *
+ * # add a filter named "my-filter"
+ * \Luigi\Filters['my-filter'] = function($s) {
+ * # filter input string
+ * return "foo-{$s}-bar";
+ * };
+ *
+ * # use custom filter in template
+ * echo Template::once('%{some-key | my-filter}', [
+ * 'some-key' => 'example',
+ * ]) . "\n";
+ *
+ * # prints "foo-example-bar"
+ *
+ * @api
+ */
class Filters {
+ /**
+ * @var array $FILTERS Global filter map.
+ *
+ * @api
+ */
public static $FILTERS = null;
+ /**
+ * Initialize global filter map.
+ *
+ * Called internally by LuigiTemplate.
+ *
+ * @internal
+ *
+ * @return void
+ */
public static function init() : void {
# prevent double initialization
if (self::$FILTERS !== null)
@@ -315,13 +605,62 @@ class Filters {
}
};
+# initialize filters
Filters::init();
+/**
+ * Template object.
+ *
+ * Parse a template string into a Template instance, and then apply the
+ * Template via the Template#run() method.
+ *
+ * Example:
+ *
+ * # load template class
+ * use \Pablotron\Luigi\Template;
+ *
+ * # create template
+ * $tmpl = new Template('hello %{name}');
+ *
+ * # run template and print result
+ * echo $tmpl->run([
+ * 'name' => 'Paul',
+ * ]) . "\n";
+ *
+ * # prints "hello Paul"
+ *
+ * @api
+ *
+ */
final class Template {
- private $template,
- $filters,
+ /** @var string $template Input template string. */
+ public $template;
+
+ /**
+ * @var array $filters Filter map.
+ * @var array $tokens Parsed template tokens.
+ */
+ private $filters,
$tokens;
+ /**
+ * Create a new Template object.
+ *
+ * Parse a template string into tokens.
+ *
+ * Example:
+ *
+ * # load template class
+ * use \Pablotron\Luigi\Template;
+ *
+ * # create template
+ * $tmpl = new Template('hello %{name}');
+ *
+ * @api
+ *
+ * @param string $template Template string.
+ * @param array $custom_filters Custom filter map (optional).
+ */
public function __construct(
string $template,
array $custom_filters = []
@@ -333,6 +672,33 @@ final class Template {
$this->tokens = parse_template($template);
}
+ /**
+ * Apply given arguments to template and return the result as a
+ * string.
+ *
+ * Example:
+ *
+ * # load template class
+ * use \Pablotron\Luigi\Template;
+ *
+ * # create template
+ * $tmpl = new Template('hello %{name}');
+ *
+ * # run template and print result
+ * echo $tmpl->run([
+ * 'name' => 'Paul',
+ * ]) . "\n";
+ *
+ * # prints "hello Paul"
+ *
+ * @api
+ *
+ * @param array $args Template arguments.
+ *
+ * @return string Expanded template.
+ *
+ * @throws UnknownKeyError If a referenced key is missing from $args.
+ */
public function run(array $args = []) : string {
# create run context
$ctx = new RunContext($args, $this->filters);
@@ -342,6 +708,53 @@ final class Template {
}, $this->tokens));
}
+ /**
+ * Return the input template string.
+ *
+ * Example:
+ *
+ * # load template class
+ * use \Pablotron\Luigi\Template;
+ *
+ * # create template
+ * $tmpl = new Template('hello %{name}');
+ *
+ * # print template string
+ * echo $tmpl . "\n";
+ *
+ * # prints "hello %{name}"
+ *
+ * @api
+ *
+ * @return string Input template string.
+ */
+ public function __toString() : string {
+ return $this->template;
+ }
+
+ /**
+ * Parse template string, expand it using given arguments, and return
+ * the result as a string.
+ *
+ * Example:
+ *
+ * # load template class
+ * use \Pablotron\Luigi\Template;
+ *
+ * # create template, run it, and print result
+ * echo Template::once('foo-%{some-key}-bar', [
+ * 'some-key' => 'example',
+ * ]) . "\n";
+ *
+ * # prints "foo-example-bar"
+ * @api
+ *
+ * @param string $template Template string.
+ * @param array $args Template arguments.
+ * @param array $filters Custom filter map (optional).
+ *
+ * @return string Expanded template.
+ */
public static function once(
string $template,
array $args = [],
@@ -352,21 +765,107 @@ final class Template {
}
};
+/**
+ * Lazy-loading template cache.
+ *
+ * Group a set of templates together and only parse them on an as-needed
+ * basis.
+ *
+ * @api
+ */
final class Cache implements \ArrayAccess {
+ /**
+ * @var array $templates Map of keys to template strings.
+ * @var array $filters Filter map (optional).
+ * @var array $lut Parsed template cache.
+ */
private $templates,
$filters,
$lut = [];
+ /**
+ * Create a new template cache.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create template cache
+ * $cache = new Cache([
+ * 'hi' => 'hi %{name}!',
+ * ]);
+ *
+ * @api
+ *
+ * @param array $templates Map of template keys to template strings.
+ * @param array $filters Custom filter map (optional).
+ */
public function __construct(array $templates, array $filters = []) {
$this->templates = $templates;
$this->filters = (count($filters) > 0) ? $filters : Filters::$FILTERS;
}
+ /**
+ * Returns true if the given template key exists in this cache.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create cache
+ * $cache = new Cache([
+ * 'hi' => 'hi %{name}!',
+ * ]);
+ *
+ * # get template 'hi' from cache
+ * foreach (array('hi', 'nope') as $tmpl_key) {
+ * echo "$key: " . (isset($cache[$key]) ? 'Yes' : 'No') . "\n"
+ * }
+ *
+ * # prints "hi: Yes" and "nope: No"
+ * @api
+ *
+ * @param string $key Template key.
+ *
+ * @return bool Returns true if the template key exists.
+ */
public function offsetExists($key) : bool {
return isset($this->templates[$key]);
}
- public function offsetGet($key) {
+ /**
+ * Get the template associated with the given template key.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create cache
+ * $cache = new Cache([
+ * 'hi' => 'hi %{name}!',
+ * ]);
+ *
+ * # get template 'hi' from cache
+ * $tmpl = $cache['hi'];
+ *
+ * echo $tmpl->run([
+ * 'name' => 'Paul',
+ * ]);
+ *
+ * # prints "hi Paul!"
+ *
+ * @api
+ *
+ * @param string $key Template key.
+ *
+ * @return Template Returns template associated with this key.
+ *
+ * @throws UnknownTemplateError if the given template does not exist.
+ */
+ public function offsetGet($key) : Template {
if (isset($this->lut[$key])) {
return $this->lut[$key];
} else if (isset($this->templates[$key])) {
@@ -377,17 +876,98 @@ final class Cache implements \ArrayAccess {
}
}
+ /**
+ * Remove the template associated with the given template key.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create cache
+ * $cache = new Cache([
+ * 'hi' => 'hi %{name}!',
+ * ]);
+ *
+ * # remove template 'hi' from cache
+ * unset($cache['hi']);
+ *
+ * echo $cache['hi']->run([
+ * 'name' => 'Paul',
+ * ]);
+ *
+ * # throws UnknownTemplateError
+ *
+ * @api
+ *
+ * @param array $key Template key.
+ *
+ * @return void
+ */
public function offsetUnset($key) : void {
unset($this->lut[$key]);
unset($this->templates[$key]);
}
+ /**
+ * Set the template associated with the given template key.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create cache
+ * $cache = new Cache();
+ *
+ * # add template to cache as 'hash-name'
+ * $cache['hash-name'] = 'hashed name: %{name | hash sha1}';
+ *
+ * echo $cache['hash-name']->run([
+ * 'name' => 'Paul',
+ * ]);
+ *
+ * # prints "sha1 of name: c3687ab9880c26dfe7ab966a8a1701b5e017c2ff"
+ *
+ * @api
+ *
+ * @param string $key Template key.
+ * @param string $val Template string.
+ *
+ * @return void
+ */
public function offsetSet($key, $val) : void {
unset($this->lut[$key]);
$this->templates[$key] = $val;
}
-
+ /**
+ * Run template with the given arguments and return the result.
+ *
+ * Example:
+ *
+ * # load cache class
+ * use \Pablotron\Luigi\Cache;
+ *
+ * # create cache
+ * $cache = new Cache([
+ * 'my-template' => 'hello %{name | uc}',
+ * ]);
+ *
+ * # run template
+ * echo $cache->run('my-template', [
+ * 'name' => 'Paul',
+ * ]);
+ *
+ * # prints "hello PAUL"
+ *
+ * @api
+ *
+ * @param string $key Template key.
+ * @param array $args Template arguments.
+ *
+ * @return string Returns the expanded template result.
+ */
public function run(string $key, array $args = []) : string {
return $this->offsetGet($key)->run($args);
}
diff --git a/php/tests/TemplateTest.php b/php/tests/TemplateTest.php
index 18e5a6e..b6e72c9 100644
--- a/php/tests/TemplateTest.php
+++ b/php/tests/TemplateTest.php
@@ -3,7 +3,7 @@ declare(strict_types = 1);
namespace Pablotron\Luigi\Tests;
use \PHPUnit\Framework\TestCase;
-use \Luigi\Template;
+use \Pablotron\Luigi\Template;
final class TemplateTest extends TestCase {
public function testNewTemplate() : void {
@@ -89,12 +89,12 @@ final class TemplateTest extends TestCase {
}
public function testCustomGlobalFilter() : void {
- \Luigi\Filters::$FILTERS['barify'] = function($s) {
+ \Pablotron\Luigi\Filters::$FILTERS['barify'] = function($s) {
return 'BAR';
};
# create template cache
- $cache = new \Luigi\Cache([
+ $cache = new \Pablotron\Luigi\Cache([
'foo' => 'foo%{bar|barify}',
]);
@@ -124,7 +124,7 @@ final class TemplateTest extends TestCase {
public function testCache() : void {
# create template cache
- $cache = new \Luigi\Cache([
+ $cache = new \Pablotron\Luigi\Cache([
'foo' => 'foo%{bar}',
]);
@@ -148,7 +148,7 @@ final class TemplateTest extends TestCase {
}
public function testUnknownKeyError() : void {
- $this->expectException(\Luigi\UnknownKeyError::class);
+ $this->expectException(\Pablotron\Luigi\UnknownKeyError::class);
# run template
$r = Template::once('foo%{unknown-key}', [
@@ -157,7 +157,7 @@ final class TemplateTest extends TestCase {
}
public function testUnknownFilterError() : void {
- $this->expectException(\Luigi\UnknownFilterError::class);
+ $this->expectException(\Pablotron\Luigi\UnknownFilterError::class);
# run template
$r = Template::once('foo%{bar|unknown-filter}', [
@@ -166,10 +166,10 @@ final class TemplateTest extends TestCase {
}
public function testUnknownTemplateError() : void {
- $this->expectException(\Luigi\UnknownTemplateError::class);
+ $this->expectException(\Pablotron\Luigi\UnknownTemplateError::class);
# create cache
- $cache = new \Luigi\Cache([
+ $cache = new \Pablotron\Luigi\Cache([
'foo' => 'foo%{bar}',
]);