Przeglądaj źródła

Move Dirk to the core of the engine

To further expand the functionality of the template engine.
Visman 1 rok temu
rodzic
commit
a0521b376c
2 zmienionych plików z 675 dodań i 63 usunięć
  1. 197 63
      app/Core/View.php
  2. 478 0
      app/Core/View/Compiler.php

+ 197 - 63
app/Core/View.php

@@ -5,65 +5,66 @@
  * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  * @license   The MIT License (MIT)
  */
+/**
+ * based on Dirk <https://github.com/artoodetoo/dirk>
+ *
+ * @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
+ * @license   The MIT License (MIT)
+ */
 
 declare(strict_types=1);
 
 namespace ForkBB\Core;
 
-use R2\Templating\Dirk;
+use ForkBB\Core\View\Compiler;
 use ForkBB\Models\Page;
 use RuntimeException;
 
-class View extends Dirk
+class View
 {
-    public function __construct (string $cache, string $views)
-    {
-        $config = [
-            'views'     => $views,
-            'cache'     => $cache,
-            'ext'       => '.forkbb.php',
-            'echo'      => '\\htmlspecialchars((string) %s, \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\')',
-            'separator' => '/',
-        ];
-        $this->compilers[] = 'Transformations';
-
-        parent::__construct($config);
-    }
+    protected string $ext = '.forkbb.php';
 
-    /**
-     * Трансформация скомпилированного шаблона
-     */
-    protected function compileTransformations(string $value): string
-    {
-        if (\str_starts_with($value, '<?xml ')) {
-            $value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
-        }
+    protected ?Compiler $compilerObj;
+    protected string    $compilerClass = Compiler::class;
 
-        $perfix = <<<'EOD'
-<?php
+    protected string $cache;
+    protected string $defaultDir;
+    protected string $defaultHash;
 
-declare(strict_types=1);
+    protected array $other      = [];
+    protected array $composers  = [];
+    protected array $blocks     = [];
+    protected array $blockStack = [];
+    protected array $templates  = [];
 
-use function \ForkBB\{__, num, dt, size};
+    public function __construct(string|array $config, mixed $views)
+    {
+        if (\is_array($config)) {
+            $this->cache      = $config['cache'];
+            $this->defaultDir = $config['defaultDir'];
 
-?>
-EOD;
+            $this->other[\hash('md5', $config['userDir'])] = [$config['userDir'], 10];
 
-        if (false === \strpos($value, '<!-- inline -->')) {
-            return $perfix . $value;
+            if (! empty($config['composers'])) {
+                foreach ($config['composers'] as $name => $composer) {
+                    $this->composer($name, $composer);
+                }
+            }
+
+            if (! empty($config['compiler'])) {
+                $this->compilerClass = $config['compiler'];
+            }
+        } else {
+            // для rev. 68 и ниже
+            $this->cache       = $config;
+            $this->defaultDir  = $views;
         }
 
-        return $perfix . \preg_replace_callback(
-            '%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
-            function ($matches) {
-                return \preg_replace('%\h*\R\s*%', '', $matches[1]);
-            },
-            $value
-        );
+        $this->defaultHash = \hash('md5', $this->defaultDir);
     }
 
     /**
-     * Return result of templating
+     * Возвращает отображение страницы $p или null
      */
     public function rendering(Page $p): ?string
     {
@@ -76,8 +77,10 @@ EOD;
         $p->prepare();
 
         $this->templates[] = $p->nameTpl;
+
         while ($_name = \array_shift($this->templates)) {
             $this->beginBlock('content');
+
             foreach ($this->composers as $_cname => $_cdata) {
                 if (\preg_match($_cname, $_name)) {
                     foreach ($_cdata as $_citem) {
@@ -85,7 +88,9 @@ EOD;
                     }
                 }
             }
-            require($this->prepare($_name));
+
+            require $this->prepare($_name);
+
             $this->endBlock(true);
         }
 
@@ -94,29 +99,6 @@ EOD;
         return $this->block('content');
     }
 
-    /**
-     * Compile echos
-     */
-    protected function compileEchos(string $value): string
-    {
-        $value = \preg_replace_callback(
-            '%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
-            function($matches) {
-                $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
-
-                return $matches[1]
-                    ? \substr($matches[0], 1)
-                    : '<?= \\htmlspecialchars((string) '
-                        . $this->compileEchoDefaults($matches[2])
-                        . ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
-                        . $whitespace;
-            },
-            $value
-        );
-
-        return parent::compileEchos($value);
-    }
-
     /**
      * Отправляет HTTP заголовки
      */
@@ -128,4 +110,156 @@ EOD;
             }
         }
     }
+
+    /**
+     * Возвращает отображение шаблона $name
+     */
+    public function fetch(string $name, array $data = []): string
+    {
+        $this->templates[] = $name;
+
+        if (! empty($data)) {
+            \extract($data);
+        }
+
+        while ($_name = \array_shift($this->templates)) {
+            $this->beginBlock('content');
+
+            foreach ($this->composers as $_cname => $_cdata) {
+                if (\preg_match($_cname, $_name)) {
+                    foreach ($_cdata as $_citem) {
+                        \extract((\is_callable($_citem) ? $_citem($this) : $_citem) ?: []);
+                    }
+                }
+            }
+
+            require $this->prepare($_name);
+
+            $this->endBlock(true);
+        }
+
+        return $this->block('content');
+    }
+
+    /**
+     * Add view composer
+     * @param mixed $name     template name or array of names
+     * @param mixed $composer data in the same meaning as for fetch() call, or callable returning such data
+     */
+    public function composer(string|array $name, mixed $composer): void
+    {
+        if (\is_array($name)) {
+            foreach ($name as $n) {
+                $this->composer($n, $composer);
+            }
+        } else {
+            $p = '~^'
+                . \str_replace('\*', '[^' . $this->separator . ']+', \preg_quote($name, $this->separator . '~'))
+                . '$~';
+            $this->composers[$p][] = $composer;
+        }
+    }
+
+    /**
+     * Подготавливает файл для подключения
+     */
+    protected function prepare(string $name): string
+    {
+        $st = \preg_replace('%\W%', '-', $name);
+
+        foreach ($this->other as $hash => $cur) {
+            if (\file_exist($tpl = "{$cur[0]}/{$name}{$this->ext}")) {
+                $php = "{$this->cache}/_{$st}-{$hash}.php";
+
+                if (
+                    ! \file_exists($php)
+                    || \filemtime($tpl) > \filemtime($php)
+                ) {
+                    $this->create($php, $tpl);
+                }
+
+                return $php;
+            }
+        }
+
+        $hash = $this->defaultHash;
+        $tpl  = "{$this->defaultDir}/{$name}{$this->ext}";
+        $php  = "{$this->cache}/_{$st}-{$hash}.php";
+
+        if (
+            ! \file_exists($php)
+            || \filemtime($tpl) > \filemtime($php)
+        ) {
+            $this->create($php, $tpl);
+        }
+
+        return $php;
+    }
+
+    /**
+     * Генерирует $php файл на основе шаблона $tpl
+     */
+    protected function create(string $php, string $tpl): void
+    {
+        if (empty($this->compilerObj)) {
+            $this->compilerObj = new $this->compilerClass();
+        }
+
+        $text = $this->compilerObj->create(\file_get_contents($tpl), \hash('fnv1a32', $tpl));
+
+        if (false === \file_put_contents($php, $text, \LOCK_EX)) {
+            throw new RuntimeException("Failed to write {$php} file");
+        }
+
+        if (\function_exists('\\opcache_invalidate')) {
+            \opcache_invalidate($php, true);
+        }
+    }
+
+    /**
+     * Задает родительский шаблон
+     */
+    protected function extend(string $name): void
+    {
+        $this->templates[] = $name;
+    }
+
+    /**
+     * Возвращает содержимое блока или $default
+     */
+    protected function block(string $name, string $default = ''): string
+    {
+        return \array_key_exists($name, $this->blocks)
+            ? $this->blocks[$name]
+            : $default;
+    }
+
+    /**
+     * Задает начало блока
+     */
+    protected function beginBlock(string $name): void
+    {
+        $this->blockStack[] = $name;
+
+        \ob_start();
+    }
+
+    /**
+     * Задает конец блока
+     */
+    protected function endBlock(bool $overwrite = false): string
+    {
+        $name = \array_pop($this->blockStack);
+
+        if (
+            $overwrite
+            || ! \array_key_exists($name, $this->blocks)
+        ) {
+            $this->blocks[$name] = \ob_get_clean();
+        } else {
+            $this->blocks[$name] .= \ob_get_clean();
+        }
+
+        return $name;
+    }
 }

+ 478 - 0
app/Core/View/Compiler.php

@@ -0,0 +1,478 @@
+<?php
+/**
+ * This file is part of the ForkBB <https://github.com/forkbb>.
+ *
+ * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
+ * @license   The MIT License (MIT)
+ */
+/**
+ * based on Dirk <https://github.com/artoodetoo/dirk>
+ *
+ * @copyright (c) 2015 artoodetoo <i.am@artoodetoo.org, https://github.com/artoodetoo>
+ * @license   The MIT License (MIT)
+ */
+
+declare(strict_types=1);
+
+namespace ForkBB\Core\View;
+
+use RuntimeException;
+
+class Compiler
+{
+    protected string $shortID;
+    protected int    $loopsCounter = 0;
+    protected array  $compilers    = [
+        'Statements',
+        'Comments',
+        'Echos',
+        'Transformations',
+    ];
+
+    public function __construct()
+    {
+    }
+
+    /**
+     * Генерирует php код на основе шаблона из $text
+     */
+    public function create(string $text, string $hash): string
+    {
+        $this->shortID = $hash;
+
+        foreach ($this->compilers as $type) {
+            $text = $this->{'compile' . $type}($text);
+        }
+
+        return $text;
+    }
+
+    /**
+     * Обрабатывает операторы начинающиеся с @
+     */
+    protected function compileStatements(string $value): string
+    {
+        return \preg_replace_callback(
+            '%[ \t]*+\B@(\w+)(?: [ \t]*( \( ( (?>[^()]+) | (?2) )* \) ) )?%x',
+            function($match) {
+                if (\method_exists($this, $method = 'compile' . \ucfirst($match[1]))) {
+                    return $this->$method($match[2] ?? '');
+                } else {
+                    return $match[0];
+                }
+            },
+            $value
+        );
+    }
+
+    /**
+     * Обрабатывает комментарии
+     */
+    protected function compileComments(string $value): string
+    {
+        return \preg_replace('%\{\{--(.*?)--\}\}%s', '<?php /*$1*/ ?>', $value);
+    }
+
+    /**
+     * Обрабатывает вывод информации
+     */
+    protected function compileEchos(string $value): string
+    {
+        // {{! !}}
+        $value = \preg_replace_callback(
+            '%(@)?\{\{!\s*(.+?)\s*!\}\}(\r?\n)?%s',
+            function($matches) {
+                $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
+
+                return $matches[1]
+                    ? \substr($matches[0], 1)
+                    : '<?= \\htmlspecialchars((string) '
+                        . $this->compileEchoDefaults($matches[2])
+                        . ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\', false) ?>'
+                        . $whitespace;
+            },
+            $value
+        );
+
+        // {!! !!}
+        $value = \preg_replace_callback(
+            '%\{\!!\s*(.+?)\s*!!\}(\r?\n)?%s',
+            function($matches) {
+                $whitespace = empty($matches[2]) ? '' : $matches[2] . $matches[2];
+
+                return '<?= '
+                    . $this->compileEchoDefaults($matches[1])
+                    . ' ?>'
+                    . $whitespace;
+            },
+            $value
+        );
+
+        // {{ }}
+        $value = \preg_replace_callback(
+            '%(@)?\{\{\s*(.+?)\s*\}\}(\r?\n)?%s',
+            function($matches) {
+                $whitespace = empty($matches[3]) ? '' : $matches[3] . $matches[3];
+
+                return $matches[1]
+                    ? \substr($matches[0], 1)
+                    : '<?= \\htmlspecialchars((string) '
+                        . $this->compileEchoDefaults($matches[2])
+                        . ', \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE, \'UTF-8\') ?>'
+                        . $whitespace;
+            },
+            $value
+        );
+
+        return $value;
+    }
+
+    /**
+     * Трансформирует скомпилированный шаблон
+     */
+    protected function compileTransformations(string $value): string
+    {
+        if (\str_starts_with($value, '<?xml ')) {
+            $value = \str_replace(' \\ENT_HTML5 | \\ENT_QUOTES | \\ENT_SUBSTITUTE,', ' \\ENT_XML1,', $value);
+        }
+
+        $perfix = <<<'EOD'
+<?php
+
+declare(strict_types=1);
+
+use function \ForkBB\{__, num, dt, size};
+
+?>
+EOD;
+
+        if (false === \strpos($value, '<!-- inline -->')) {
+            return $perfix . $value;
+        }
+
+        return $perfix . \preg_replace_callback(
+            '%<!-- inline -->([^<]*(?:<(?!!-- endinline -->)[^<]*)*+)(?:<!-- endinline -->)?%',
+            function ($matches) {
+                return \preg_replace('%\h*\R\s*%', '', $matches[1]);
+            },
+            $value
+        );
+    }
+
+    /**
+     * Обрабатывает значение по умолчанию для вывода информации
+     */
+    public function compileEchoDefaults(string $value): string
+    {
+        return \preg_replace('%^(?=\$)(.+?)(?:\s+or\s+)(.+?)$%s', '($1 ?? $2)', $value);
+    }
+
+    /**
+     * @if()
+     */
+    protected function compileIf(string $expression): string
+    {
+        if (\preg_match('%^\(\s*(\!\s*)?(\$[\w>-]+\[(?:\w+|[\'"]\w+[\'"])\])\s*\)$%', $expression, $matches)) {
+            if (empty($matches[1])) {
+                return "<?php if (! empty{$expression}): ?>";
+            } else {
+                return "<?php if (empty({$matches[2]})): ?>";
+            }
+        } else {
+            return "<?php if {$expression}: ?>";
+        }
+    }
+
+    /**
+     * @elseif()
+     */
+    protected function compileElseif(string $expression): string
+    {
+        return "<?php elseif {$expression}: ?>";
+    }
+
+    /**
+     * @else
+     */
+    protected function compileElse(): string
+    {
+        return "<?php else: ?>";
+    }
+
+    /**
+     * @endif
+     */
+    protected function compileEndif(): string
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * @isset()
+     */
+    protected function compileIsset(string $expression): string
+    {
+        return "<?php if (isset{$expression}): ?>";
+    }
+
+    /**
+     * @endisset
+     */
+    protected function compileEndisset(): string
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * @endempty
+     */
+    protected function compileEndempty(): string
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * @unless()
+     */
+    protected function compileUnless(string $expression): string
+    {
+        return "<?php if (! $expression): ?>";
+    }
+
+    /**
+     * @endunless
+     */
+    protected function compileEndunless(): string
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * @for()
+     */
+    protected function compileFor(string $expression): string
+    {
+        return "<?php for {$expression}: ?>";
+    }
+
+    /**
+     * @endfor
+     */
+    protected function compileEndfor(): string
+    {
+        return "<?php endfor; ?>";
+    }
+
+    /**
+     * @foreach()
+     */
+    protected function compileForeach(string $expression): string
+    {
+        ++$this->loopsCounter;
+
+        return "<?php \$__iter{$this->shortID}_{$this->loopsCounter} = 0; "
+             . "foreach {$expression}: "
+             . "++\$__iter{$this->shortID}_{$this->loopsCounter}; ?>";
+    }
+
+    /**
+     * @endforeach
+     */
+    protected function compileEndforeach(): string
+    {
+        --$this->loopsCounter;
+
+        return "<?php endforeach; ?>";
+    }
+
+    /**
+     * @iteration
+     */
+    protected function compileIteration(): string
+    {
+        return "((int) \$__iter{$this->shortID}_{$this->loopsCounter})";
+    }
+
+    /**
+     * @forelse()
+     */
+    protected function compileForelse(string $expression): string
+    {
+        ++$this->loopsCounter;
+
+        return "<?php \$__iter{$this->shortID}_{$this->loopsCounter} = 0; "
+             . "foreach {$expression}: "
+             . "++\$__iter{$this->shortID}_{$this->loopsCounter}; ?>";
+    }
+
+    /**
+     * @empty / @empty()
+     */
+    protected function compileEmpty(string $expression): string
+    {
+        if (
+            isset($expression[0])
+            && '(' == $expression[0]
+        ) {
+            return "<?php if (empty{$expression}): ?>";
+        } else {
+            $s = "<?php endforeach; if (0 === \$__iter{$this->shortID}_{$this->loopsCounter}): ?>";
+
+            --$this->loopsCounter;
+
+            return $s;
+        }
+    }
+
+    /**
+     * @endforelse
+     */
+    protected function compileEndforelse(): string
+    {
+        return "<?php endif; ?>";
+    }
+
+    /**
+     * @while()
+     */
+    protected function compileWhile(string $expression): string
+    {
+        return "<?php while {$expression}: ?>";
+    }
+
+    /**
+     * @endwhile
+     */
+    protected function compileEndwhile(): string
+    {
+        return "<?php endwhile; ?>";
+    }
+
+    /**
+     * @extends()
+     */
+    protected function compileExtends(string $expression): string
+    {
+        if (
+            isset($expression[0])
+            && '(' == $expression[0]
+        ) {
+            $expression = \substr($expression, 1, -1);
+        }
+
+        return "<?php \$this->extend({$expression}); ?>";
+    }
+
+    /**
+     * @include()
+     */
+    protected function compileInclude(string $expression): string
+    {
+        if (
+            isset($expression[0])
+            && '(' == $expression[0]
+        ) {
+            $expression = \substr($expression, 1, -1);
+        }
+
+        return "<?php include \$this->prepare({$expression}); ?>";
+    }
+
+    /**
+     * @yield()
+     */
+    protected function compileYield(string $expression): string
+    {
+        return "<?= \$this->block{$expression}; ?>";
+    }
+
+    /**
+     * @section()
+     */
+    protected function compileSection(string $expression): string
+    {
+        return "<?php \$this->beginBlock{$expression}; ?>";
+    }
+
+    /**
+     * @endsection
+     */
+    protected function compileEndsection(): string
+    {
+        return "<?php \$this->endBlock(); ?>";
+    }
+
+    /**
+     * @show()
+     */
+    protected function compileShow(): string
+    {
+        return "<?= \$this->block(\$this->endBlock()); ?>";
+    }
+
+    /**
+     * @append
+     */
+    protected function compileAppend(): string
+    {
+        return "<?php \$this->endBlock(); ?>";
+    }
+
+    /**
+     * @stop
+     */
+    protected function compileStop(): string
+    {
+        return "<?php \$this->endBlock(); ?>";
+    }
+
+    /**
+     * @overwrite
+     */
+    protected function compileOverwrite(): string
+    {
+        return "<?php \$this->endBlock(true); ?>";
+    }
+
+    /**
+     * @switch()
+     */
+    protected function compileSwitch(string $expression): string
+    {
+        return "<?php switch {$expression}: ?>";
+    }
+
+    /**
+     * @case()
+     */
+    protected function compileCase(string $expression): string
+    {
+        $expression = \substr($expression, 1, -1);
+
+        return "<?php case {$expression}: ?>";
+    }
+
+    /**
+     * @default
+     */
+    protected function compileDefault(): string
+    {
+        return "<?php default: ?>";
+    }
+
+    /**
+     * @endswitch
+     */
+    protected function compileEndswitch(): string
+    {
+        return "<?php endswitch; ?>";
+    }
+
+    /**
+     * @break
+     */
+    protected function compileBreak(): string
+    {
+        return "<?php break; ?>";
+    }
+}