initial
This commit is contained in:
commit
6c081f3aff
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.idea
|
13
README.md
Normal file
13
README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# pure_php_templates
|
||||
|
||||
This isn't a ready-to-use library but rather a concept of native php templates with:
|
||||
- transparent string escaping
|
||||
- ability to "include" other templates
|
||||
- conditions and loops support
|
||||
|
||||
PHP 8+ is required (it is not strictly necessary, but named arguments makes code much easier to write and understand).
|
||||
|
||||
|
||||
## License
|
||||
|
||||
GPLv3
|
28
htdocs/index.php
Normal file
28
htdocs/index.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
set_include_path(
|
||||
get_include_path().PATH_SEPARATOR.realpath(__DIR__.'/..'));
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
spl_autoload_register(function($class) {
|
||||
$path = __DIR__.'/../lib/'.$class.'.php';
|
||||
if (is_file($path))
|
||||
require_once $path;
|
||||
});
|
||||
|
||||
SkinContext::setRootDirectory(realpath(__DIR__.'/../skin'));
|
||||
|
||||
$skin = new Skin();
|
||||
$skin->title = 'hello world!';
|
||||
|
||||
$cities = [
|
||||
'Moscow',
|
||||
'St. Petersburg',
|
||||
'<b>New York</b>' // potential xss
|
||||
];
|
||||
echo $skin->renderPage('main/index',
|
||||
name: "John",
|
||||
show_cities: true,
|
||||
cities: $cities);
|
19
lib/Skin.php
Normal file
19
lib/Skin.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
class Skin {
|
||||
|
||||
public string $title = 'no title';
|
||||
|
||||
public function renderPage($f, ...$vars): string {
|
||||
$f = str_replace('/', '\\', $f);
|
||||
$ctx = new SkinContext(substr($f, 0, ($pos = strrpos($f, '\\'))));
|
||||
$body = call_user_func_array([$ctx, substr($f, $pos+1)], $vars);
|
||||
|
||||
$layout_ctx = new SkinContext('base');
|
||||
return $layout_ctx->layout(
|
||||
title: $this->title,
|
||||
unsafe_body: $body,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
100
lib/SkinContext.php
Normal file
100
lib/SkinContext.php
Normal file
@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
class SkinContext {
|
||||
|
||||
protected string $ns;
|
||||
protected array $data = [];
|
||||
protected static ?string $root = null;
|
||||
|
||||
public static function setRootDirectory(string $root): void {
|
||||
self::$root = $root;
|
||||
}
|
||||
|
||||
public function __construct(string $namespace) {
|
||||
$this->ns = $namespace;
|
||||
require_once self::$root.'/'.str_replace('\\', '/', $namespace).'.skin.php';
|
||||
}
|
||||
|
||||
public function __call($name, array $arguments) {
|
||||
$plain_args = array_is_list($arguments);
|
||||
|
||||
$fn = '\\skin\\'.$this->ns.'\\'.$name;
|
||||
$refl = new ReflectionFunction($fn);
|
||||
$fparams = $refl->getParameters();
|
||||
assert(count($fparams) == count($arguments)+1, "$fn: invalid number of arguments (".count($fparams)." != ".(count($arguments)+1).")");
|
||||
|
||||
foreach ($fparams as $n => $param) {
|
||||
if ($n == 0)
|
||||
continue; // skip $ctx
|
||||
|
||||
$key = $plain_args ? $n-1 : $param->name;
|
||||
if (!$plain_args && !array_key_exists($param->name, $arguments))
|
||||
throw new InvalidArgumentException('argument '.$param->name.' not found');
|
||||
|
||||
if (is_string($arguments[$key]) || $arguments[$key] instanceof SkinString) {
|
||||
if (is_string($arguments[$key]))
|
||||
$arguments[$key] = new SkinString($arguments[$key]);
|
||||
|
||||
if (($pos = strpos($param->name, '_')) !== false) {
|
||||
$mod_type = match(substr($param->name, 0, $pos)) {
|
||||
'unsafe' => SkinStringModificationType::RAW,
|
||||
'urlencoded' => SkinStringModificationType::URL,
|
||||
default => SkinStringModificationType::HTML
|
||||
};
|
||||
} else {
|
||||
$mod_type = SkinStringModificationType::HTML;
|
||||
}
|
||||
$arguments[$key]->setModType($mod_type);
|
||||
}
|
||||
}
|
||||
|
||||
array_unshift($arguments, $this);
|
||||
return call_user_func_array($fn, $arguments);
|
||||
}
|
||||
|
||||
public function __get(string $name) {
|
||||
$fn = '\\skin\\'.$this->ns.'\\'.$name;
|
||||
if (function_exists($fn))
|
||||
return [$this, $name];
|
||||
|
||||
if (array_key_exists($name, $this->data))
|
||||
return $this->data[$name];
|
||||
}
|
||||
|
||||
public function __set(string $name, $value) {
|
||||
$this->data[$name] = $value;
|
||||
}
|
||||
|
||||
public function if_not($cond, $callback, ...$args) {
|
||||
return $this->_if_condition(!$cond, $callback, ...$args);
|
||||
}
|
||||
|
||||
public function if_true($cond, $callback, ...$args) {
|
||||
return $this->_if_condition($cond, $callback, ...$args);
|
||||
}
|
||||
|
||||
public function if_then_else($cond, $cb1, $cb2) {
|
||||
return $cond ? $this->_return_callback($cb1) : $this->_return_callback($cb2);
|
||||
}
|
||||
|
||||
protected function _if_condition($condition, $callback, ...$args) {
|
||||
if ($condition)
|
||||
return $this->_return_callback($callback, $args);
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function _return_callback($callback, $args = []) {
|
||||
if (is_callable($callback))
|
||||
return call_user_func_array($callback, $args);
|
||||
else if (is_string($callback))
|
||||
return $callback;
|
||||
}
|
||||
|
||||
public function for_each(array $iterable, callable $callback) {
|
||||
$html = '';
|
||||
foreach ($iterable as $k => $v)
|
||||
$html .= call_user_func($callback, $v, $k);
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
21
lib/SkinString.php
Normal file
21
lib/SkinString.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
class SkinString implements Stringable {
|
||||
|
||||
protected SkinStringModificationType $modType;
|
||||
|
||||
public function __construct(protected string $string) {}
|
||||
|
||||
public function setModType(SkinStringModificationType $modType) {
|
||||
$this->modType = $modType;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return match ($this->modType) {
|
||||
SkinStringModificationType::HTML => htmlspecialchars($this->string, ENT_QUOTES, 'UTF-8'),
|
||||
SkinStringModificationType::URL => urlencode($this->string),
|
||||
default => $this->string,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
7
lib/SkinStringModificationType.php
Normal file
7
lib/SkinStringModificationType.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
enum SkinStringModificationType {
|
||||
case RAW;
|
||||
case URL;
|
||||
case HTML;
|
||||
}
|
15
skin/base.skin.php
Normal file
15
skin/base.skin.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace skin\base;
|
||||
|
||||
function layout($ctx, $title, $unsafe_body) {
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<title>{$title}</title>
|
||||
</body>
|
||||
<body>{$unsafe_body}</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
29
skin/main.skin.php
Normal file
29
skin/main.skin.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace skin\main;
|
||||
|
||||
function index($ctx, $name, $show_cities, $cities) {
|
||||
return <<<HTML
|
||||
Hello, {$name}!<br/>
|
||||
|
||||
{$ctx->if_true($show_cities, 'line of truth<br/>')}
|
||||
{$ctx->if_not(false, $ctx->renderIfFalse, '<b>safe<b>', '<b>unsafe<b>')}
|
||||
|
||||
<ul>
|
||||
{$ctx->for_each($cities, fn($city, $i) => $ctx->renderIndexCityItem($city, $i+1))}
|
||||
</ul>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function renderIndexCityItem($ctx, $city, $index) {
|
||||
return <<<HTML
|
||||
<li>{$index} {$city}</li>
|
||||
HTML;
|
||||
}
|
||||
|
||||
function renderIfFalse($ctx, $str, $unsafe_str) {
|
||||
return <<<HTML
|
||||
safe: $str<br/>
|
||||
unsafe: $unsafe_str
|
||||
HTML;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user