2
0
mirror of https://github.com/boostorg/website.git synced 2026-01-24 06:22:15 +00:00
Files
website/common/code/boost_simple_template.php
2016-05-17 09:10:10 +01:00

242 lines
8.2 KiB
PHP

<?php
require_once(__DIR__.'/boost.php');
/* Simple template library inspired by mustache.
* Does not implement:
*
* Lambdas
* Partials
*
* Doesn't claim to be at all compatible with Mustache, just that it should be
* easy to switch to a proper Mustache implementation in the future.
*/
class BoostSimpleTemplate {
static function render($template, $params) {
echo self::render_to_string($template, $params);
}
static function render_to_string($template, $params) {
$parsed_template = self::parse_template($template);
return self::interpret($parsed_template, $params);
}
static function parse_template($template) {
$template_parts = array();
$operator_stack = array();
$scope_stack = array();
$template_stack = array();
$last_offset = 0;
$open_delim = '{{';
$close_delim = '}}';
while(preg_match("@
(?P<leading_whitespace>^[ \\t]*)?
(?P<tag>{$open_delim}(?:
!.*?{$close_delim} |
(?P<symbol_operator>[#/^&]?)\\s*(?P<symbol>[\\w?!/.-]*)\\s*{$close_delim} |
{\\s*(?P<unescaped>[\\w?!\\/.-]+)\\s*}{$close_delim} |
=\\s*(?P<open_delim>[^=\\s]+?)\\s*(?P<close_delim>[^=\\s]+?)\\s*={$close_delim} |
(?P<error>)
))
(?P<trailing_whitespace>[ \\t]*(?:\\r?\\n|\\Z))?
@xsm",
$template, $match, PREG_OFFSET_CAPTURE, $last_offset)) {
$operator = null;
if (array_key_exists('error', $match) && $match['error'][1] != -1) {
throw new BoostSimpleTemplateException("Invalid/unsupported tag", $match['tag'][1]);
}
else if (!empty($match['unescaped'][0])) {
$operator = '&';
$symbol = $match['unescaped'][0];
}
else if (!empty($match['open_delim'][0])) {
$operator = '=';
$symbol = null;
}
else if(!empty($match['symbol'][0])) {
$operator = $match['symbol_operator'][0] ?: '$';
$symbol = $match['symbol'][0];
}
if ($operator != '&' && $operator != '$' && $match['leading_whitespace'][1] != -1 && array_key_exists('trailing_whitespace', $match) && $match['trailing_whitespace'][1] != -1) {
$text_offset = $last_offset;
$text_length = $match[0][1] - $text_offset;
$last_offset = $match[0][1] + strlen($match[0][0]);
}
else {
$text_offset = $last_offset;
$text_length = $match['tag'][1] - $text_offset;
$last_offset = $match['tag'][1] + strlen($match['tag'][0]);
}
if ($text_length) {
$template_parts[] = substr($template, $text_offset, $text_length);
}
switch($operator) {
case null:
break;
case '#':
case '^':
$operator_stack[] = $operator;
$scope_stack[] = $symbol;
$template_stack[] = $template_parts;
$template_parts = array();
break;
case '/':
if (array_pop($scope_stack) !== $symbol) {
throw new BoostSimpleTemplateException("Mismatched close tag", $match['tag'][1]);
}
$parent_template_parts = array_pop($template_stack);
$parent_template_parts[] = array(
'type' => array_pop($operator_stack),
'symbol' => $symbol,
'contents' => $template_parts,
);
$template_parts = $parent_template_parts;
break;
case '$':
case '&':
$template_parts[] = array(
'type' => $operator,
'symbol' => $symbol,
);
break;
case '=':
$open_delim = preg_quote($match['open_delim'][0], '@');
$close_delim = preg_quote($match['close_delim'][0], '@');
break;
default:
assert(false); exit(1);
}
}
if ($scope_stack) {
// Would probably be better to store the offset of the opening tag.
throw new BoostSimpleTemplateException("Unclosed tag: ".end($scope_stack), strlen($template));
}
$end = substr($template, $last_offset);
if ($end) { $template_parts[] = $end; }
return $template_parts;
}
static function interpret($template_array, $params) {
$output = '';
foreach($template_array as $template_part) {
if (is_string($template_part)) {
$output .= $template_part;
} else {
$value = self::lookup($params, $template_part['symbol']);
switch($template_part['type']) {
case '$':
if ($value) {
$output .= html_encode($value);
}
break;
case '&':
if ($value) {
$output .= $value;
}
break;
case '#':
if ($value) {
$output .= self::interpret_nested_content(
$template_part['contents'],
$params,
$value);
}
break;
case '^':
if (!$value) {
$output .= self::interpret(
$template_part['contents'],
$params);
}
break;
default:
assert(false); exit(1);
}
}
}
return $output;
}
static function lookup($params, $symbol) {
// Q: What if that symbol starts with a '.'?
if ($symbol == '.') {
$symbol = array('.');
}
else {
$symbol = explode('.', $symbol);
}
$value = $params;
foreach($symbol as $symbol_part) {
if (is_array($value) && array_key_exists($symbol_part, $value)) {
$value = $value[$symbol_part];
}
// TODO: What if the object has a magic property that doesn't actually exist?
// Or a function call or some such craziness?
else if (is_object($value) && property_exists($value, $symbol_part)) {
$value = $value->$symbol_part;
}
else {
return null;
}
}
return $value;
}
static function interpret_nested_content($template_array, $params, $value) {
if (is_object($value)) {
return self::interpret($template_array, array_merge($params, (array) $value));
}
if (is_array($value)) {
// Just checking the first key to see if this is a list or object.
// Should probably check every key?
reset($value);
if (is_int(key($value))) {
$output = '';
foreach($value as $x) {
if (is_object($x)) {
$child_params = array_merge($params, (array) $x);
}
else if (is_array($x)) {
$child_params = array_merge($params, $x);
}
else {
$child_params = $params;
}
$child_params['.'] = $x;
$output .= self::interpret($template_array, $child_params);
}
return $output;
}
else {
return self::interpret($template_array, array_merge($params, $value));
}
}
else {
return self::interpret($template_array, $params);
}
}
}
class BoostSimpleTemplateException extends \RuntimeException {
var $offset;
function __construct($message, $offset = null) {
parent::__construct($message);
$this->offset = $offset;
}
}