summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md13
-rw-r--r--htdocs/index.php28
-rw-r--r--lib/Skin.php19
-rw-r--r--lib/SkinContext.php100
-rw-r--r--lib/SkinString.php21
-rw-r--r--lib/SkinStringModificationType.php7
-rw-r--r--skin/base.skin.php15
-rw-r--r--skin/main.skin.php29
9 files changed, 233 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..485dee6
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.idea
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a27e6f0
--- /dev/null
+++ b/README.md
@@ -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 \ No newline at end of file
diff --git a/htdocs/index.php b/htdocs/index.php
new file mode 100644
index 0000000..84c4ff8
--- /dev/null
+++ b/htdocs/index.php
@@ -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);
diff --git a/lib/Skin.php b/lib/Skin.php
new file mode 100644
index 0000000..e04aa5b
--- /dev/null
+++ b/lib/Skin.php
@@ -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,
+ );
+ }
+
+}
diff --git a/lib/SkinContext.php b/lib/SkinContext.php
new file mode 100644
index 0000000..b4192c3
--- /dev/null
+++ b/lib/SkinContext.php
@@ -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;
+ }
+
+}
diff --git a/lib/SkinString.php b/lib/SkinString.php
new file mode 100644
index 0000000..ef43090
--- /dev/null
+++ b/lib/SkinString.php
@@ -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,
+ };
+ }
+
+} \ No newline at end of file
diff --git a/lib/SkinStringModificationType.php b/lib/SkinStringModificationType.php
new file mode 100644
index 0000000..61959ff
--- /dev/null
+++ b/lib/SkinStringModificationType.php
@@ -0,0 +1,7 @@
+<?php
+
+enum SkinStringModificationType {
+ case RAW;
+ case URL;
+ case HTML;
+} \ No newline at end of file
diff --git a/skin/base.skin.php b/skin/base.skin.php
new file mode 100644
index 0000000..d5e02e2
--- /dev/null
+++ b/skin/base.skin.php
@@ -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;
+}
diff --git a/skin/main.skin.php b/skin/main.skin.php
new file mode 100644
index 0000000..f44f739
--- /dev/null
+++ b/skin/main.skin.php
@@ -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;
+} \ No newline at end of file