Skip to content

Commit 3950e0f

Browse files
committed
add Template
1 parent d694bd2 commit 3950e0f

2 files changed

Lines changed: 181 additions & 0 deletions

File tree

src/Template.php

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<?php
2+
namespace MintyPHP;
3+
4+
class Template {
5+
6+
public static function render($template, $data, $functions = array()) {
7+
$tokens = Template::tokenize($template);
8+
$tree = Template::createSyntaxTree($tokens);
9+
return Template::renderChildren($tree,$data,$functions);
10+
}
11+
12+
private static function createNode($type,$expression) {
13+
return (object)array('type'=>$type,'expression'=>$expression,'children'=>array());
14+
}
15+
16+
private static function tokenize($template) {
17+
$parts = ['',$template];
18+
$tokens = [];
19+
while (true) {
20+
$parts = explode('{{',$parts[1],2);
21+
$tokens[] = $parts[0];
22+
if (count($parts)!=2) {
23+
break;
24+
}
25+
$parts = explode('}}',$parts[1],2);
26+
$tokens[] = $parts[0];
27+
if (count($parts)!=2) {
28+
break;
29+
}
30+
}
31+
return $tokens;
32+
}
33+
34+
private static function createSyntaxTree(&$tokens) {
35+
$root = Template::createNode('root',false);
36+
$current = $root;
37+
$stack = array();
38+
foreach ($tokens as $i=>$token) {
39+
if ($i%2==1) {
40+
if ($token=='endif') {
41+
$type = 'endif';
42+
$expression = false;
43+
} elseif ($token=='endfor') {
44+
$type = 'endfor';
45+
$expression = false;
46+
} elseif (substr($token,0,3)=='if:') {
47+
$type = 'if';
48+
$expression = substr($token,3);
49+
} elseif (substr($token,0,4)=='for:') {
50+
$type = 'for';
51+
$expression = substr($token,4);
52+
} else {
53+
$type = 'var';
54+
$expression = $token;
55+
}
56+
if (in_array($type,array('endif','endfor'))) {
57+
$current = array_pop($stack);
58+
} else {
59+
$node = Template::createNode($type,$expression);
60+
array_push($current->children,$node);
61+
if (in_array($type,array('if','for'))) {
62+
array_push($stack,$current);
63+
$current = $node;
64+
}
65+
}
66+
} else {
67+
array_push($current->children,Template::createNode('lit',$token));
68+
}
69+
}
70+
return $root;
71+
}
72+
73+
private static function renderChildren($node,&$data,&$functions) {
74+
$result = '';
75+
foreach ($node->children as $child) {
76+
switch($child->type) {
77+
case 'if': $result .= Template::renderIfNode($child,$data,$functions); break;
78+
case 'for': $result .= Template::renderForNode($child,$data,$functions); break;
79+
case 'var': $result .= Template::renderVarNode($child,$data,$functions); break;
80+
case 'lit': $result .= $child->expression; break;
81+
}
82+
}
83+
return $result;
84+
}
85+
86+
private static function renderIfNode(&$node, &$data, &$functions) {
87+
$parts = explode('|',$node->expression);
88+
$path = array_shift($parts);
89+
try {
90+
$value = Template::resolvePath($path,$data);
91+
$value = Template::applyFunctions($value,$parts,$functions);
92+
} catch (\Throwable $e) {
93+
return '{{if:'.$node->expression.'!!'.$e->getMessage().'}}';
94+
}
95+
$result = '';
96+
if ($value) {
97+
$result .= Template::renderChildren($node,$data,$functions);
98+
}
99+
return $result;
100+
}
101+
102+
private static function renderForNode(&$node, &$data, &$functions) {
103+
$parts = explode('|',$node->expression);
104+
$path = array_shift($parts);
105+
$path = explode(':',$path,2);
106+
if (count($path)!=2) {
107+
return '{{for:'.$node->expression.'!!'."for must have 'for:var:array' format".'}}';
108+
}
109+
list($var,$path) = $path;
110+
try {
111+
$value = Template::resolvePath($path,$data);
112+
$value = Template::applyFunctions($value,$parts,$functions);
113+
} catch (\Throwable $e) {
114+
return '{{for:'.$node->expression.'!!'.$e->getMessage().'}}';
115+
}
116+
if (!is_array($value)) {
117+
return '{{for:'.$node->expression.'!!'."expression must evaluate to an array".'}}';
118+
}
119+
$result = '';
120+
foreach ($value as $v) {
121+
$data = array_merge($data,[$var=>$v]);
122+
$result .= Template::renderChildren($node,$data,$functions);
123+
}
124+
return $result;
125+
}
126+
127+
private static function renderVarNode(&$node, &$data, &$functions) {
128+
$parts = explode('|',$node->expression);
129+
$path = array_shift($parts);
130+
try {
131+
$value = Template::resolvePath($path,$data);
132+
$value = Template::applyFunctions($value,$parts,$functions);
133+
} catch (\Throwable $e) {
134+
return '{{'.$node->expression.'!!'.$e->getMessage().'}}';
135+
}
136+
return $value;
137+
}
138+
139+
private static function resolvePath(&$path, &$data) {
140+
$current = &$data;
141+
foreach (explode('.',$path) as $p) {
142+
if (!isset($current[$p])) {
143+
throw new \Exception("path '$p' not found");
144+
}
145+
$current = &$current[$p];
146+
}
147+
return $current;
148+
}
149+
150+
private static function applyFunctions(&$value, &$parts, &$functions) {
151+
foreach ($parts as $part) {
152+
$function = explode('(',rtrim($part,')'));
153+
$f = $function[0];
154+
$arguments = isset($function[1])?explode(',',$function[1]):array();
155+
array_unshift($arguments,$value);
156+
if (isset($functions[$f])) {
157+
$value = call_user_func_array($functions[$f],$arguments);
158+
} else {
159+
throw new \Exception("function '$f' not found");
160+
}
161+
}
162+
return $value;
163+
}
164+
165+
}

src/Tests/TemplateTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
namespace MintyPHP\Tests;
3+
4+
use MintyPHP\Template;
5+
6+
class TemplateTest extends \PHPUnit\Framework\TestCase
7+
{
8+
public function testRender() {
9+
$this->assertEquals("hello World",Template::render('hello {{name|capitalize}}', ['name'=>'world'], ['capitalize'=>'ucfirst']));
10+
$this->assertEquals("hello {{name|failure!!function 'failure' not found}}",Template::render('hello {{name|failure}}', ['name'=>'world'], ['capitalize'=>'ucfirst']));
11+
$this->assertEquals("hello m is 3",Template::render('hello {{if:n.m|eq(3)}}m is 3{{endif}}', ['n'=>['m'=>3]], ['eq'=>function($a,$b){return $a==$b;}]));
12+
$this->assertEquals("hello 1980-05-13",Template::render('hello {{name|dateFormat(Y-m-d)}}', ['name'=>'May 13, 1980'], ['dateFormat'=>function($date, $format) { return date($format, strtotime($date)); }]));
13+
$this->assertEquals("test 1 2 3",Template::render('test{{for:i:counts}} {{i}}{{endfor}}', ['counts'=>[1,2,3]]));
14+
$this->assertEquals("test (-1,-1) (-1,1) (1,-1) (1,1)",Template::render('test{{for:x:steps}}{{for:y:steps}} ({{x}},{{y}}){{endfor}}{{endfor}}', ['steps'=>[-1,1]]));
15+
}
16+
}

0 commit comments

Comments
 (0)