diff --git a/composer.json b/composer.json
index 69fa819..75506ba 100644
--- a/composer.json
+++ b/composer.json
@@ -32,7 +32,7 @@
"iidestiny/flysystem-oss": "^4.0",
"czproject/git-php": "^4.2",
"localheinz/diff": "^1.1",
- "topthink/think-template": "^3.0"
+ "psr/simple-cache": ">=1.0"
},
"require-dev": {
"symfony/var-dumper": "^4.2"
diff --git a/composer.lock b/composer.lock
index ba60511..8f1d6fd 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "cf930a793fc0588ec1f01c7e8bfb5ea6",
+ "content-hash": "9ef28b8bf889c61ae5c86815724ebffd",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@@ -2417,47 +2417,6 @@
"source": "https://github.com/top-think/think-orm/tree/v3.0.13"
},
"time": "2023-09-01T09:08:49+00:00"
- },
- {
- "name": "topthink/think-template",
- "version": "v3.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/top-think/think-template.git",
- "reference": "4352d2cf627abfb8b49f830686c25c02f59c23f2"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/top-think/think-template/zipball/4352d2cf627abfb8b49f830686c25c02f59c23f2",
- "reference": "4352d2cf627abfb8b49f830686c25c02f59c23f2",
- "shasum": ""
- },
- "require": {
- "php": ">=8.0.0",
- "psr/simple-cache": ">=1.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "think\\": "src"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "Apache-2.0"
- ],
- "authors": [
- {
- "name": "liu21st",
- "email": "liu21st@gmail.com"
- }
- ],
- "description": "the php template engine",
- "support": {
- "issues": "https://github.com/top-think/think-template/issues",
- "source": "https://github.com/top-think/think-template/tree/v3.0.0"
- },
- "time": "2023-02-25T12:11:14+00:00"
}
],
"packages-dev": [
diff --git a/config/view.php b/config/view.php
index e460907..9307b1e 100644
--- a/config/view.php
+++ b/config/view.php
@@ -26,7 +26,9 @@ return [
// 标签库标签结束标记
'taglib_end' => '}',
// 模板缓存
- 'display_cache' => true,
+ 'display_cache' => false,
+ // 模板编译缓存
+ 'tpl_cache' => true,
// 字符替换
'tpl_replace_string' => [
'__STATIC__' => Env::get('adminsystem.static_path', '/static'),
diff --git a/extend/think/Template.php b/extend/think/Template.php
new file mode 100644
index 0000000..77aa218
--- /dev/null
+++ b/extend/think/Template.php
@@ -0,0 +1,1317 @@
+
+// +----------------------------------------------------------------------
+declare(strict_types=1);
+
+namespace think;
+
+use Exception;
+use Psr\SimpleCache\CacheInterface;
+use think\template\contract\DriverInterface;
+
+/**
+ * ThinkPHP分离出来的模板引擎
+ * 支持XML标签和普通标签的模板解析
+ * 编译型模板引擎 支持动态缓存.
+ */
+class Template
+{
+ /**
+ * 模板变量.
+ * @var array
+ */
+ protected array $data = [];
+
+ /**
+ * 模板配置参数.
+ * @var array
+ */
+ protected array $config = [
+ 'view_suffix' => 'html', // 默认模板文件后缀
+ 'view_depr' => DIRECTORY_SEPARATOR,
+ 'cache_path' => '',
+ 'cache_suffix' => 'php', // 默认模板缓存后缀
+ 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数
+ 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码
+ 'tpl_begin' => '{', // 模板引擎普通标签开始标记
+ 'tpl_end' => '}', // 模板引擎普通标签结束标记
+ 'strip_space' => false, // 是否去除模板文件里面的html空格与换行
+ 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译
+ 'compile_type' => 'file', // 模板编译类型
+ 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变
+ 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒)
+ 'layout_on' => false, // 布局模板开关
+ 'layout_name' => 'layout', // 布局模板入口文件
+ 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识
+ 'taglib_begin' => '{', // 标签库标签开始标记
+ 'taglib_end' => '}', // 标签库标签结束标记
+ 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测
+ 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序
+ 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔
+ 'display_cache' => false, // 模板渲染缓存
+ 'cache_id' => '', // 模板缓存ID
+ 'tpl_replace_string' => [],
+ 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别
+ 'default_filter' => 'htmlentities', // 默认过滤方法 用于普通标签输出
+ ];
+
+ /**
+ * 保留内容信息.
+ * @var array
+ */
+ private array $literal = [];
+
+ /**
+ * 扩展解析规则.
+ * @var array
+ */
+ private array $extend = [];
+
+ /**
+ * 模板包含信息.
+ * @var array
+ */
+ private array $includeFile = [];
+
+ /**
+ * 模板存储对象
+ * @var DriverInterface
+ */
+ protected DriverInterface $storage;
+
+ /**
+ * 查询缓存对象
+ * @var CacheInterface|null
+ */
+ protected ?CacheInterface $cache;
+
+ /**
+ * 架构函数.
+ * @param array $config
+ */
+ public function __construct(array $config = [])
+ {
+ $this->config = array_merge($this->config, $config);
+
+ $this->config['taglib_begin_origin'] = $this->config['taglib_begin'];
+ $this->config['taglib_end_origin'] = $this->config['taglib_end'];
+
+ $this->config['taglib_begin'] = preg_quote($this->config['taglib_begin'], '/');
+ $this->config['taglib_end'] = preg_quote($this->config['taglib_end'], '/');
+ $this->config['tpl_begin'] = preg_quote($this->config['tpl_begin'], '/');
+ $this->config['tpl_end'] = preg_quote($this->config['tpl_end'], '/');
+
+ // 初始化模板编译存储器
+ $type = $this->config['compile_type'] ?: 'File';
+ $class = str_contains($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type);
+
+ $this->storage = new $class();
+ }
+
+ /**
+ * 模板变量赋值
+ * @param array $vars 模板变量
+ * @return $this
+ */
+ public function assign(array $vars = []): static
+ {
+ $this->data = array_merge($this->data, $vars);
+
+ return $this;
+ }
+
+ /**
+ * 模板引擎参数赋值
+ * @param string $name
+ * @param mixed $value
+ */
+ public function __set(string $name, $value)
+ {
+ $this->config[$name] = $value;
+ }
+
+ /**
+ * 设置缓存对象
+ * @param CacheInterface $cache 缓存对象
+ * @return void
+ */
+ public function setCache(CacheInterface $cache): void
+ {
+ $this->cache = $cache;
+ }
+
+ /**
+ * 模板引擎配置.
+ * @param array $config
+ * @return $this
+ */
+ public function config(array $config): static
+ {
+ $this->config = array_merge($this->config, $config);
+
+ return $this;
+ }
+
+ /**
+ * 获取模板引擎配置项.
+ * @param string $name
+ * @return mixed
+ */
+ public function getConfig(string $name)
+ {
+ return $this->config[$name] ?? null;
+ }
+
+ /**
+ * 模板变量获取.
+ * @param string $name 变量名
+ * @return mixed
+ */
+ public function get(string $name = '')
+ {
+ if ('' == $name) {
+ return $this->data;
+ }
+
+ $data = $this->data;
+
+ foreach (explode('.', $name) as $key => $val) {
+ if (isset($data[$val])) {
+ $data = $data[$val];
+ } else {
+ $data = null;
+ break;
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * 扩展模板解析规则.
+ * @param string $rule 解析规则
+ * @param callable|null $callback 解析规则回调
+ * @return void
+ */
+ public function extend(string $rule, callable $callback = null): void
+ {
+ $this->extend[$rule] = $callback;
+ }
+
+ /**
+ * 渲染模板文件.
+ * @param string $template 模板文件
+ * @param array $vars 模板变量
+ * @return void
+ */
+ public function fetch(string $template, array $vars = []): void
+ {
+ if ($vars) {
+ $this->data = array_merge($this->data, $vars);
+ }
+
+ if ($this->isCache($this->config['cache_id'])) {
+ // 读取渲染缓存
+ echo $this->cache->get($this->config['cache_id']);
+
+ return;
+ }
+
+ $template = $this->parseTemplateFile($template);
+
+ if ($template) {
+ $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_on'] . $this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
+
+ if (!$this->checkCache($cacheFile)) {
+ // 缓存无效 重新模板编译
+ $content = file_get_contents($template);
+ $this->compiler($content, $cacheFile);
+ }
+
+ // 页面缓存
+ ob_start();
+ ob_implicit_flush(false);
+
+ // 读取编译存储
+ $this->storage->read($cacheFile, $this->data);
+
+ // 获取并清空缓存
+ $content = ob_get_clean();
+
+ if (!empty($this->config['cache_id']) && $this->config['display_cache'] && null !== $this->cache) {
+ // 缓存页面输出
+ $this->cache->set($this->config['cache_id'], $content, $this->config['cache_time']);
+ }
+
+ echo $content;
+ }
+ }
+
+ /**
+ * 检查编译缓存是否存在.
+ * @param string $cacheId 缓存的id
+ * @return bool
+ */
+ public function isCache(string $cacheId): bool
+ {
+ if ($cacheId && null !== $this->cache && $this->config['display_cache']) {
+ // 缓存页面输出
+ return $this->cache->has($cacheId);
+ }
+
+ return false;
+ }
+
+ /**
+ * 渲染模板内容.
+ * @param string $content 模板内容
+ * @param array $vars 模板变量
+ * @return void
+ */
+ public function display(string $content, array $vars = []): void
+ {
+ if ($vars) {
+ $this->data = array_merge($this->data, $vars);
+ }
+
+ $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
+
+ if (!$this->checkCache($cacheFile)) {
+ // 缓存无效 模板编译
+ $this->compiler($content, $cacheFile);
+ }
+
+ // 读取编译存储
+ $this->storage->read($cacheFile, $this->data);
+ }
+
+ /**
+ * 设置布局
+ * @param bool|string $name 布局模板名称 false 则关闭布局
+ * @param string $replace 布局模板内容替换标识
+ * @return $this
+ */
+ public function layout(bool|string $name, string $replace = ''): static
+ {
+ if (false === $name) {
+ // 关闭布局
+ $this->config['layout_on'] = false;
+ } else {
+ // 开启布局
+ $this->config['layout_on'] = true;
+
+ // 名称必须为字符串
+ if (is_string($name)) {
+ $this->config['layout_name'] = $name;
+ }
+
+ if (!empty($replace)) {
+ $this->config['layout_item'] = $replace;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
+ * 检查编译缓存是否有效,如果无效则需要重新编译.
+ * @param string $cacheFile 缓存文件名
+ * @return bool
+ */
+ private function checkCache(string $cacheFile): bool
+ {
+ if (!$this->config['tpl_cache'] || !is_file($cacheFile) || !$handle = @fopen($cacheFile, 'r')) {
+ return false;
+ }
+
+ // 读取第一行
+ $line = fgets($handle);
+
+ if (false === $line) {
+ return false;
+ }
+
+ preg_match('/\/\*(.+?)\*\//', $line, $matches);
+
+ if (!isset($matches[1])) {
+ return false;
+ }
+
+ $includeFile = unserialize($matches[1]);
+
+ if (!is_array($includeFile)) {
+ return false;
+ }
+
+ // 检查模板文件是否有更新
+ foreach ($includeFile as $path => $time) {
+ if (is_file($path) && filemtime($path) > $time) {
+ // 模板文件如果有更新则缓存需要更新
+ return false;
+ }
+ }
+
+ // 检查编译存储是否有效
+ return $this->storage->check($cacheFile, $this->config['cache_time']);
+ }
+
+ /**
+ * 编译模板文件内容.
+ * @param string $content 模板内容
+ * @param string $cacheFile 缓存文件名
+ * @return void
+ */
+ private function compiler(string &$content, string $cacheFile): void
+ {
+ // 判断是否启用布局
+ if ($this->config['layout_on']) {
+ if (str_contains($content, '{__NOLAYOUT__}')) {
+ // 可以单独定义不使用布局
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ } else {
+ // 读取布局模板
+ $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
+
+ if ($layoutFile) {
+ // 替换布局的主体内容
+ $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
+ }
+ }
+ } else {
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ }
+
+ // 模板解析
+ $this->parse($content);
+
+ if ($this->config['strip_space']) {
+ /* 去除html空格与换行 */
+ $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
+ $replace = ['><', '>'];
+ $content = preg_replace($find, $replace, $content);
+ }
+
+ // 优化生成的php代码
+ $content = preg_replace('/\?>\s*<\?php\s(?!echo\b|\bend)/s', '', $content);
+
+ // 模板过滤输出
+ $replace = $this->config['tpl_replace_string'];
+ $content = str_replace(array_keys($replace), array_values($replace), $content);
+
+ // 添加安全代码及模板引用记录
+ $content = 'includeFile) . '*/ ?>' . "\n" . $content;
+ // 编译存储
+ $this->storage->write($cacheFile, $content);
+
+ $this->includeFile = [];
+ }
+
+ /**
+ * 模板解析入口
+ * 支持普通标签和TagLib解析 支持自定义标签库.
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ public function parse(string &$content): void
+ {
+ // 内容为空不解析
+ if (empty($content)) {
+ return;
+ }
+
+ // 替换literal标签内容
+ $this->parseLiteral($content);
+
+ // 解析继承
+ $this->parseExtend($content);
+
+ // 解析布局
+ $this->parseLayout($content);
+
+ // 检查include语法
+ $this->parseInclude($content);
+
+ // 替换包含文件中literal标签内容
+ $this->parseLiteral($content);
+
+ // 检查PHP语法
+ $this->parsePhp($content);
+
+ // 获取需要引入的标签库列表
+ // 标签库只需要定义一次,允许引入多个一次
+ // 一般放在文件的最前面
+ // 格式:
+ // 当TAGLIB_LOAD配置为true时才会进行检测
+ if ($this->config['taglib_load']) {
+ $tagLibs = $this->getIncludeTagLib($content);
+
+ if (!empty($tagLibs)) {
+ // 对导入的TagLib进行解析
+ foreach ($tagLibs as $tagLibName) {
+ $this->parseTagLib($tagLibName, $content);
+ }
+ }
+ }
+
+ // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
+ if ($this->config['taglib_pre_load']) {
+ $tagLibs = explode(',', $this->config['taglib_pre_load']);
+
+ foreach ($tagLibs as $tag) {
+ $this->parseTagLib($tag, $content);
+ }
+ }
+
+ // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
+ $tagLibs = explode(',', $this->config['taglib_build_in']);
+
+ foreach ($tagLibs as $tag) {
+ $this->parseTagLib($tag, $content, true);
+ }
+
+ // 解析普通模板标签 {$tagName}
+ $this->parseTag($content);
+
+ // 还原被替换的Literal标签
+ $this->parseLiteral($content, true);
+ }
+
+ /**
+ * 检查PHP语法.
+ * @param string $content 要解析的模板内容
+ * @return void
+ * @throws Exception
+ */
+ private function parsePhp(string &$content): void
+ {
+ // 短标签的情况要将' . "\n", $content);
+
+ // PHP语法检查
+ if ($this->config['tpl_deny_php'] && false !== strpos($content, 'getRegex('layout'), $content, $matches)) {
+ // 替换Layout标签
+ $content = str_replace($matches[0], '', $content);
+ // 解析Layout标签
+ $array = $this->parseAttr($matches[0]);
+
+ if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {
+ // 读取布局模板
+ $layoutFile = $this->parseTemplateFile($array['name']);
+
+ if ($layoutFile) {
+ $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];
+ // 替换布局的主体内容
+ $content = str_replace($replace, $content, file_get_contents($layoutFile));
+ }
+ }
+ } else {
+ $content = str_replace('{__NOLAYOUT__}', '', $content);
+ }
+ }
+
+ /**
+ * 解析模板中的include标签.
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseInclude(string &$content): void
+ {
+ $regex = $this->getRegex('include');
+ $func = function ($template) use (&$func, &$regex, &$content) {
+ if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $array = $this->parseAttr($match[0]);
+ $file = $array['file'];
+ unset($array['file']);
+
+ // 分析模板文件名并读取内容
+ $parseStr = $this->parseTemplateName($file);
+
+ foreach ($array as $k => $v) {
+ // 以$开头字符串转换成模板变量
+ if (str_starts_with($v, '$')) {
+ $v = $this->get(substr($v, 1));
+ }
+
+ $parseStr = str_replace('[' . $k . ']', $v, $parseStr);
+ }
+
+ $content = str_replace($match[0], $parseStr, $content);
+ // 再次对包含文件进行模板分析
+ $func($parseStr);
+ }
+ unset($matches);
+ }
+ };
+
+ // 替换模板中的include标签
+ $func($content);
+ }
+
+ /**
+ * 解析模板中的extend标签.
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseExtend(string &$content): void
+ {
+ $regex = $this->getRegex('extend');
+ $array = $blocks = $baseBlocks = [];
+ $extend = '';
+
+ $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
+ if (preg_match($regex, $template, $matches)) {
+ if (!isset($array[$matches['name']])) {
+ $array[$matches['name']] = 1;
+ // 读取继承模板
+ $extend = $this->parseTemplateName($matches['name']);
+
+ // 递归检查继承
+ $func($extend);
+
+ // 取得block标签内容
+ $blocks = array_merge($blocks, $this->parseBlock($template));
+
+ return;
+ }
+ } else {
+ // 取得顶层模板block标签内容
+ $baseBlocks = $this->parseBlock($template, true);
+
+ if (empty($extend)) {
+ // 无extend标签但有block标签的情况
+ $extend = $template;
+ }
+ }
+ };
+
+ $func($content);
+
+ if (!empty($extend)) {
+ if ($baseBlocks) {
+ $children = [];
+ foreach ($baseBlocks as $name => $val) {
+ $replace = $val['content'];
+
+ if (!empty($children[$name])) {
+ // 如果包含有子block标签
+ foreach ($children[$name] as $key) {
+ $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace);
+ }
+ }
+
+ if (isset($blocks[$name])) {
+ // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
+ $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);
+
+ if (!empty($val['parent'])) {
+ // 如果不是最顶层的block标签
+ $parent = $val['parent'];
+
+ if (isset($blocks[$parent])) {
+ $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']);
+ }
+
+ $blocks[$name]['content'] = $replace;
+ $children[$parent][] = $name;
+
+ continue;
+ }
+ } elseif (!empty($val['parent'])) {
+ // 如果子标签没有被继承则用原值
+ $children[$val['parent']][] = $name;
+ $blocks[$name] = $val;
+ }
+
+ if (!$val['parent']) {
+ // 替换模板中的顶级block标签
+ $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend);
+ }
+ }
+ }
+
+ $content = $extend;
+ unset($blocks, $baseBlocks);
+ }
+ }
+
+ /**
+ * 替换页面中的literal标签.
+ * @param string $content 模板内容
+ * @param bool $restore 是否为还原
+ * @return void
+ */
+ private function parseLiteral(string &$content, bool $restore = false): void
+ {
+ $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal');
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+ if (!$restore) {
+ $count = count($this->literal);
+
+ // 替换literal标签
+ foreach ($matches as $match) {
+ $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
+ $content = str_replace($match[0], "", $content);
+ $count++;
+ }
+ } else {
+ // 还原literal标签
+ foreach ($matches as $match) {
+ $content = str_replace($match[0], $this->literal[$match[1]], $content);
+ }
+
+ // 清空literal记录
+ $this->literal = [];
+ }
+
+ unset($matches);
+ }
+ }
+
+ /**
+ * 获取模板中的block标签.
+ * @param string $content 模板内容
+ * @param bool $sort 是否排序
+ * @return array
+ */
+ private function parseBlock(string &$content, bool $sort = false): array
+ {
+ $regex = $this->getRegex('block');
+ $result = [];
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $right = $keys = [];
+
+ foreach ($matches as $match) {
+ if (empty($match['name'][0])) {
+ if (count($right) > 0) {
+ $tag = array_pop($right);
+ $start = $tag['offset'] + strlen($tag['tag']);
+ $length = $match[0][1] - $start;
+
+ $result[$tag['name']] = [
+ 'begin' => $tag['tag'],
+ 'content' => substr($content, $start, $length),
+ 'end' => $match[0][0],
+ 'parent' => count($right) ? end($right)['name'] : '',
+ ];
+
+ $keys[$tag['name']] = $match[0][1];
+ }
+ } else {
+ // 标签头压入栈
+ $right[] = [
+ 'name' => $match[2][0],
+ 'offset' => $match[0][1],
+ 'tag' => $match[0][0],
+ ];
+ }
+ }
+
+ unset($right, $matches);
+
+ if ($sort) {
+ // 按block标签结束符在模板中的位置排序
+ array_multisort($keys, $result);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 搜索模板页面中包含的 TagLib 库,并返回列表.
+ * @param string $content 模板内容
+ * @return array
+ */
+ private function getIncludeTagLib(string &$content): array
+ {
+ // 搜索是否有TagLib标签
+ if (preg_match($this->getRegex('taglib'), $content, $matches)) {
+ // 替换TagLib标签
+ $content = str_replace($matches[0], '', $content);
+
+ return explode(',', $matches['name']);
+ }
+
+ return [];
+ }
+
+ /**
+ * TagLib库解析.
+ * @param string $tagLib 要解析的标签库
+ * @param string $content 要解析的模板内容
+ * @param bool $hide 是否隐藏标签库前缀
+ * @return void
+ */
+ public function parseTagLib(string $tagLib, string &$content, bool $hide = false): void
+ {
+ if (str_contains($tagLib, '\\')) {
+ // 支持指定标签库的命名空间
+ $className = $tagLib;
+ $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1);
+ } else {
+ $className = '\\think\\template\\taglib\\' . ucwords($tagLib);
+ }
+
+ $tLib = new $className($this);
+
+ $tLib->parseTag($content, $hide ? '' : $tagLib);
+ }
+
+ /**
+ * 分析标签属性.
+ * @param string $str 属性字符串
+ * @param string|null $name 不为空时返回指定的属性名
+ * @return array
+ */
+ public function parseAttr(string $str, string $name = null): array
+ {
+ $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is';
+ $array = [];
+
+ if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $array[$match['name']] = $match['value'];
+ }
+ unset($matches);
+ }
+
+ if (!empty($name) && isset($array[$name])) {
+ return $array[$name];
+ }
+
+ return $array;
+ }
+
+ /**
+ * 模板标签解析
+ * 格式: {TagName:args [|content] }.
+ * @param string $content 要解析的模板内容
+ * @return void
+ */
+ private function parseTag(string &$content): void
+ {
+ $regex = $this->getRegex('tag');
+
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
+ foreach ($matches as $match) {
+ $str = stripslashes($match[1]);
+ $flag = substr($str, 0, 1);
+
+ switch ($flag) {
+ case '$':
+ // 解析模板变量 格式 {$varName}
+ // 是否带有?号
+ if (false !== $pos = strpos($str, '?')) {
+ $array = preg_split('/([!=]={1,2}|(?<]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
+ $name = $array[0];
+
+ $this->parseVar($name);
+ //$this->parseVarFunction($name);
+
+ $str = trim(substr($str, $pos + 1));
+ $this->parseVar($str);
+ $first = substr($str, 0, 1);
+
+ if (strpos($name, ')')) {
+ // $name为对象或是自动识别,或者含有函数
+ if (isset($array[1])) {
+ $this->parseVar($array[2]);
+ $name .= $array[1] . $array[2];
+ }
+
+ switch ($first) {
+ case '?':
+ $this->parseVarFunction($name);
+ $str = '';
+ break;
+ case '=':
+ $str = '';
+ break;
+ default:
+ $str = '';
+ }
+ } else {
+ if (isset($array[1])) {
+ $this->parseVar($array[2]);
+ $express = $name . $array[1] . $array[2];
+ } else {
+ $express = false;
+ }
+
+ if (in_array($first, ['?', '=', ':'])) {
+ $str = trim(substr($str, 1));
+ if (str_starts_with($str, '$')) {
+ $str = $this->parseVarFunction($str);
+ }
+ }
+
+ // $name为数组
+ switch ($first) {
+ case '?':
+ // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx
+ $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>';
+ break;
+ case '=':
+ // {$varname?='xxx'} $varname为真时才输出xxx
+ $str = '';
+ break;
+ case ':':
+ // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx
+ $str = 'parseVarFunction($name) . ' : ' . $str . '; ?>';
+ break;
+ default:
+ if (strpos($str, ':')) {
+ // {$varname ? 'a' : 'b'} $varname为真时输出a,否则输出b
+ $array = explode(':', $str, 2);
+
+ $array[0] = str_starts_with(trim($array[0]), '$') ? $this->parseVarFunction($array[0]) : $array[0];
+ $array[1] = str_starts_with(trim($array[1]), '$') ? $this->parseVarFunction($array[1]) : $array[1];
+
+ $str = implode(' : ', $array);
+ }
+ $str = '';
+ }
+ }
+ } else {
+ $this->parseVar($str);
+ $this->parseVarFunction($str);
+ $str = '';
+ }
+ break;
+ case ':':
+ // 输出某个函数的结果
+ $str = substr($str, 1);
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '~':
+ // 执行某个函数
+ $str = substr($str, 1);
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '-':
+ case '+':
+ // 输出计算
+ $this->parseVar($str);
+ $str = '';
+ break;
+ case '/':
+ // 注释标签
+ $flag2 = substr($str, 1, 1);
+ if ('/' == $flag2 || ('*' == $flag2 && str_ends_with(rtrim($str), '*/'))) {
+ $str = '';
+ }
+ break;
+ default:
+ // 未识别的标签直接返回
+ $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end'];
+ break;
+ }
+
+ $content = str_replace($match[0], $str, $content);
+ }
+
+ unset($matches);
+ }
+ }
+
+ /**
+ * 模板变量解析,支持使用函数
+ * 格式: {$varname|function1|function2=arg1,arg2}.
+ * @param string $varStr 变量数据
+ * @return void
+ */
+ public function parseVar(string &$varStr): void
+ {
+ $varStr = trim($varStr);
+
+ if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
+ static $_varParseList = [];
+
+ while ($matches[0]) {
+ $match = array_pop($matches[0]);
+
+ //如果已经解析过该变量字串,则直接返回变量值
+ if (isset($_varParseList[$match[0]])) {
+ $parseStr = $_varParseList[$match[0]];
+ } else {
+ if (strpos($match[0], '.')) {
+ $vars = explode('.', $match[0]);
+ $first = array_shift($vars);
+
+ if (isset($this->extend[$first])) {
+ $callback = $this->extend[$first];
+ $parseStr = $callback($vars);
+ } elseif ('$Request' == $first) {
+ // 输出请求变量
+ $parseStr = $this->parseRequestVar($vars);
+ } elseif ('$Think' == $first) {
+ // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
+ $parseStr = $this->parseThinkVar($vars);
+ } else {
+ switch ($this->config['tpl_var_identify']) {
+ case 'array': // 识别为数组
+ $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']';
+ break;
+ case 'obj': // 识别为对象
+ $parseStr = $first . '->' . implode('->', $vars);
+ break;
+ default: // 自动判断数组或对象
+ $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')';
+ }
+ }
+ } else {
+ $parseStr = str_replace(':', '->', $match[0]);
+ }
+
+ $_varParseList[$match[0]] = $parseStr;
+ }
+
+ $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0]));
+ }
+ unset($matches);
+ }
+ }
+
+ /**
+ * 对模板中使用了函数的变量进行解析
+ * 格式 {$varname|function1|function2=arg1,arg2}.
+ * @param string $varStr 变量字符串
+ * @param bool $autoescape 自动转义
+ * @return string
+ */
+ public function parseVarFunction(string &$varStr, bool $autoescape = true): string
+ {
+ if (!$autoescape && !str_contains($varStr, '|')) {
+ return $varStr;
+ } elseif ($autoescape && !preg_match('/\|(\s)?raw(\||\s)?/i', $varStr)) {
+ $varStr .= '|' . $this->config['default_filter'];
+ }
+
+ static $_varFunctionList = [];
+
+ $_key = md5($varStr);
+
+ //如果已经解析过该变量字串,则直接返回变量值
+ if (isset($_varFunctionList[$_key])) {
+ $varStr = $_varFunctionList[$_key];
+ } else {
+ $varArray = explode('|', $varStr);
+
+ // 取得变量名称
+ $name = trim(array_shift($varArray));
+
+ // 对变量使用函数
+ $length = count($varArray);
+
+ // 取得模板禁止使用函数列表
+ $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']);
+
+ for ($i = 0; $i < $length; $i++) {
+ $args = explode('=', $varArray[$i], 2);
+
+ // 模板函数过滤
+ $fun = trim($args[0]);
+ if (in_array($fun, $template_deny_funs)) {
+ continue;
+ }
+
+ switch (strtolower($fun)) {
+ case 'raw':
+ break;
+ case 'date':
+ $name = 'date(' . $args[1] . ',!is_numeric(' . $name . ')? strtotime(' . $name . ') : ' . $name . ')';
+ break;
+ case 'first':
+ $name = 'current(' . $name . ')';
+ break;
+ case 'last':
+ $name = 'end(' . $name . ')';
+ break;
+ case 'upper':
+ $name = 'strtoupper(' . $name . ')';
+ break;
+ case 'lower':
+ $name = 'strtolower(' . $name . ')';
+ break;
+ case 'format':
+ $name = 'sprintf(' . $args[1] . ',' . $name . ')';
+ break;
+ case 'default': // 特殊模板函数
+ if (!str_contains($name, '(')) {
+ $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')';
+ } else {
+ $name = '(' . $name . ' ?: ' . $args[1] . ')';
+ }
+ break;
+ default: // 通用模板函数
+ if (isset($args[1])) {
+ if (str_contains($args[1], '###')) {
+ $args[1] = str_replace('###', $name, $args[1]);
+ $name = "$fun($args[1])";
+ } else {
+ $name = "$fun($name,$args[1])";
+ }
+ } else {
+ if (!empty($args[0])) {
+ $name = "$fun($name)";
+ }
+ }
+ }
+ }
+
+ $_varFunctionList[$_key] = $name;
+ $varStr = $name;
+ }
+
+ return $varStr;
+ }
+
+ /**
+ * 请求变量解析
+ * 格式 以 $Request. 打头的变量属于请求变量.
+ * @param array $vars 变量数组
+ * @return string
+ */
+ public function parseRequestVar(array $vars): string
+ {
+ $type = strtoupper(trim(array_shift($vars)));
+ $param = implode('.', $vars);
+
+ switch ($type) {
+ case 'SERVER':
+ $parseStr = '$_SERVER[\'' . $param . '\']';
+ break;
+ case 'GET':
+ $parseStr = '$_GET[\'' . $param . '\']';
+ break;
+ case 'POST':
+ $parseStr = '$_POST[\'' . $param . '\']';
+ break;
+ case 'COOKIE':
+ $parseStr = '$_COOKIE[\'' . $param . '\']';
+ break;
+ case 'SESSION':
+ $parseStr = '$_SESSION[\'' . $param . '\']';
+ break;
+ case 'ENV':
+ $parseStr = '$_ENV[\'' . $param . '\']';
+ break;
+ case 'REQUEST':
+ $parseStr = '$_REQUEST[\'' . $param . '\']';
+ break;
+ default:
+ $parseStr = '\'\'';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * 特殊模板变量解析
+ * 格式 以 $Think. 打头的变量属于特殊模板变量.
+ * @param array $vars 变量数组
+ * @return string
+ */
+ public function parseThinkVar(array $vars): string
+ {
+ $type = strtoupper(trim(array_shift($vars)));
+ $param = implode('.', $vars);
+
+ return match ($type) {
+ 'CONST' => strtoupper($param),
+ 'NOW' => "date('Y-m-d g:i a',time())",
+ 'LDELIM' => '\'' . ltrim($this->config['tpl_begin'], '\\') . '\'',
+ 'RDELIM' => '\'' . ltrim($this->config['tpl_end'], '\\') . '\'',
+ default => defined($type) ? $type : '\'\'',
+ };
+ }
+
+ /**
+ * 分析加载的模板文件并读取内容 支持多个模板文件读取.
+ * @param string $templateName 模板文件名
+ * @return string
+ */
+ private function parseTemplateName(string $templateName): string
+ {
+ $array = explode(',', $templateName);
+ $parseStr = '';
+
+ foreach ($array as $templateName) {
+ if (empty($templateName)) {
+ continue;
+ }
+
+ if (str_starts_with($templateName, '$')) {
+ //支持加载变量文件名
+ $templateName = $this->get(substr($templateName, 1));
+ }
+
+ $template = $this->parseTemplateFile($templateName);
+
+ if ($template) {
+ // 获取模板文件内容
+ $parseStr .= file_get_contents($template);
+ }
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * 解析模板文件名.
+ * @param string $template 文件名
+ * @return string
+ */
+ private function parseTemplateFile(string $template): string
+ {
+ if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
+ if (!str_starts_with($template, '/')) {
+ $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
+ } else {
+ $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1));
+ }
+
+ if (str_starts_with($template, '@' . DIRECTORY_SEPARATOR)) {
+ // 以@/开头,则从框架跟目录定位
+ $app_path = $this->getConfig('view_root_path');
+ $base_app_path = $this->getConfig('view_root_path');
+ $view_app_path = $this->getConfig('view_root_path');
+
+ $template = str_replace('@' . DIRECTORY_SEPARATOR, '', $template);
+ } elseif (str_starts_with($template, '@')) {
+ // 以@开头,则从控制器的视图文件定位
+ $app_path = $this->getConfig('base_view_path');
+ $base_app_path = $this->getConfig('base_view_path');
+ $view_app_path = $this->getConfig('base_view_path');
+
+ $template = str_replace('@', '', $template);
+ } else {
+ $app_path = $this->getConfig('app_path');
+ $base_app_path = $this->getConfig('base_app_path');
+ $view_app_path = $this->getConfig('view_app_path');
+ }
+
+ $template_file_path = $template . '.' . ltrim($this->config['view_suffix'], '.');
+
+ if (is_file($app_path . $template_file_path)) {
+ // 优先app下的view
+ $template_file_path = $app_path . $template_file_path;
+ } elseif (is_file($base_app_path . $template_file_path)) {
+ // 查找extend下的view
+ $template_file_path = $base_app_path . $template_file_path;
+ } else {
+ // 查找根目录下的view
+ $template_file_path = $view_app_path . $template_file_path;
+ }
+
+ $template = $template_file_path;
+ }
+
+ if (is_file($template)) {
+ // 记录模板文件的更新时间
+ $this->includeFile[$template] = filemtime($template);
+
+ return $template;
+ }
+
+ throw new Exception('template not exists:' . $template);
+ }
+
+ /**
+ * 按标签生成正则.
+ * @param string $tagName 标签名
+ * @return string
+ */
+ private function getRegex(string $tagName): string
+ {
+ $regex = '';
+ if ('tag' == $tagName) {
+ $begin = $this->config['tpl_begin'];
+ $end = $this->config['tpl_end'];
+
+ if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {
+ $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end;
+ } else {
+ $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;
+ }
+ } else {
+ $begin = $this->config['taglib_begin'];
+ $end = $this->config['taglib_end'];
+ $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1;
+
+ switch ($tagName) {
+ case 'block':
+ if ($single) {
+ $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end;
+ } else {
+ $regex = $begin . '(?:' . $tagName . '\b\s+(?>(?:(?!name=).)*)\bname=([\'\"])(?P[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end;
+ }
+ break;
+ case 'literal':
+ if ($single) {
+ $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')';
+ $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)';
+ $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+ } else {
+ $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')';
+ $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)';
+ $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
+ }
+ break;
+ case 'restoreliteral':
+ $regex = '';
+ break;
+ case 'include':
+ $name = 'file';
+ case 'taglib':
+ case 'layout':
+ case 'extend':
+ if (empty($name)) {
+ $name = 'name';
+ }
+ if ($single) {
+ $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end;
+ } else {
+ $regex = $begin . $tagName . '\b\s+(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end;
+ }
+ break;
+ }
+ }
+
+ return '/' . $regex . '/is';
+ }
+
+ public function __debugInfo()
+ {
+ $data = get_object_vars($this);
+ unset($data['storage']);
+
+ return $data;
+ }
+}
diff --git a/extend/think/facade/Template.php b/extend/think/facade/Template.php
new file mode 100644
index 0000000..69cf492
--- /dev/null
+++ b/extend/think/facade/Template.php
@@ -0,0 +1,83 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\facade;
+
+if (class_exists('think\Facade')) {
+ class Facade extends \think\Facade
+ {}
+} else {
+ class Facade
+ {
+ /**
+ * 始终创建新的对象实例
+ * @var bool
+ */
+ protected static $alwaysNewInstance;
+
+ protected static $instance;
+
+ /**
+ * 获取当前Facade对应类名
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {}
+
+ /**
+ * 创建Facade实例
+ * @static
+ * @access protected
+ * @return object
+ */
+ protected static function createFacade()
+ {
+ $class = static::getFacadeClass() ?: 'think\Template';
+
+ if (static::$alwaysNewInstance) {
+ return new $class();
+ }
+
+ if (!self::$instance) {
+ self::$instance = new $class();
+ }
+
+ return self::$instance;
+
+ }
+
+ // 调用实际类的方法
+ public static function __callStatic($method, $params)
+ {
+ return call_user_func_array([static::createFacade(), $method], $params);
+ }
+ }
+}
+
+/**
+ * @see \think\Template
+ * @mixin \think\Template
+ */
+class Template extends Facade
+{
+ protected static $alwaysNewInstance = true;
+
+ /**
+ * 获取当前Facade对应类名(或者已经绑定的容器对象标识)
+ * @access protected
+ * @return string
+ */
+ protected static function getFacadeClass()
+ {
+ return 'think\Template';
+ }
+}
diff --git a/extend/think/template/TagLib.php b/extend/think/template/TagLib.php
new file mode 100644
index 0000000..4203b77
--- /dev/null
+++ b/extend/think/template/TagLib.php
@@ -0,0 +1,341 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template;
+
+use Exception;
+use think\Template;
+
+/**
+ * ThinkPHP标签库TagLib解析基类
+ * @category Think
+ * @package Think
+ * @subpackage Template
+ * @author liu21st
+ */
+class TagLib
+{
+
+ /**
+ * 标签库定义XML文件
+ * @var string
+ * @access protected
+ */
+ protected $xml = '';
+ protected $tags = []; // 标签定义
+ /**
+ * 标签库名称
+ * @var string
+ * @access protected
+ */
+ protected $tagLib = '';
+
+ /**
+ * 标签库标签列表
+ * @var array
+ * @access protected
+ */
+ protected $tagList = [];
+
+ /**
+ * 标签库分析数组
+ * @var array
+ * @access protected
+ */
+ protected $parse = [];
+
+ /**
+ * 标签库是否有效
+ * @var bool
+ * @access protected
+ */
+ protected $valid = false;
+
+ protected $comparison = [' nheq ' => ' !== ', ' heq ' => ' === ', ' neq ' => ' != ', ' eq ' => ' == ', ' egt ' => ' >= ', ' gt ' => ' > ', ' elt ' => ' <= ', ' lt ' => ' < '];
+
+ /**
+ * 架构函数
+ * @access public
+ * @param Template $tpl 模板引擎对象
+ */
+ public function __construct(protected Template $tpl)
+ {
+ }
+
+ /**
+ * 按签标库替换页面中的标签
+ * @access public
+ * @param string $content 模板内容
+ * @param string $lib 标签库名
+ * @return void
+ */
+ public function parseTag(string &$content, string $lib = ''): void
+ {
+ $tags = [];
+ $lib = $lib ? strtolower($lib) . ':' : '';
+
+ foreach ($this->tags as $name => $val) {
+ $close = !isset($val['close']) || $val['close'] ? 1 : 0;
+ $tags[$close][$lib . $name] = $name;
+ if (isset($val['alias'])) {
+ // 别名设置
+ $array = (array) $val['alias'];
+ foreach (explode(',', $array[0]) as $v) {
+ $tags[$close][$lib . $v] = $name;
+ }
+ }
+ }
+
+ // 闭合标签
+ if (!empty($tags[1])) {
+ $nodes = [];
+ $regex = $this->getRegex(array_keys($tags[1]), 1);
+ if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
+ $right = [];
+ foreach ($matches as $match) {
+ if ('' == $match[1][0]) {
+ $name = strtolower($match[2][0]);
+ // 如果有没闭合的标签头则取出最后一个
+ if (!empty($right[$name])) {
+ // $match[0][1]为标签结束符在模板中的位置
+ $nodes[$match[0][1]] = [
+ 'name' => $name,
+ 'begin' => array_pop($right[$name]), // 标签开始符
+ 'end' => $match[0], // 标签结束符
+ ];
+ }
+ } else {
+ // 标签头压入栈
+ $right[strtolower($match[1][0])][] = $match[0];
+ }
+ }
+ unset($right, $matches);
+ // 按标签在模板中的位置从后向前排序
+ krsort($nodes);
+ }
+
+ $break = '';
+ if ($nodes) {
+ $beginArray = [];
+ // 标签替换 从后向前
+ foreach ($nodes as $pos => $node) {
+ // 对应的标签名
+ $name = $tags[1][$node['name']];
+ $alias = $lib . $name != $node['name'] ? ($lib ? strstr($node['name'], $lib) : $node['name']) : '';
+
+ // 解析标签属性
+ $attrs = $this->parseAttr($node['begin'][0], $name, $alias);
+ $method = 'tag' . $name;
+
+ // 读取标签库中对应的标签内容 replace[0]用来替换标签头,replace[1]用来替换标签尾
+ $replace = explode($break, $this->$method($attrs, $break));
+
+ if (count($replace) > 1) {
+ while ($beginArray) {
+ $begin = end($beginArray);
+ // 判断当前标签尾的位置是否在栈中最后一个标签头的后面,是则为子标签
+ if ($node['end'][1] > $begin['pos']) {
+ break;
+ } else {
+ // 不为子标签时,取出栈中最后一个标签头
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ // 替换标签尾部
+ $content = substr_replace($content, $replace[1], $node['end'][1], strlen($node['end'][0]));
+ // 把标签头压入栈
+ $beginArray[] = ['pos' => $node['begin'][1], 'len' => strlen($node['begin'][0]), 'str' => $replace[0]];
+ }
+ }
+
+ while ($beginArray) {
+ $begin = array_pop($beginArray);
+ // 替换标签头部
+ $content = substr_replace($content, $begin['str'], $begin['pos'], $begin['len']);
+ }
+ }
+ }
+ // 自闭合标签
+ if (!empty($tags[0])) {
+ $regex = $this->getRegex(array_keys($tags[0]), 0);
+ $content = preg_replace_callback($regex, function ($matches) use (&$tags, &$lib) {
+ // 对应的标签名
+ $name = $tags[0][strtolower($matches[1])];
+ $alias = $lib . $name != $matches[1] ? ($lib ? strstr($matches[1], $lib) : $matches[1]) : '';
+ // 解析标签属性
+ $attrs = $this->parseAttr($matches[0], $name, $alias);
+ $method = 'tag' . $name;
+ return $this->$method($attrs, '');
+ }, $content);
+ }
+ }
+
+ /**
+ * 按标签生成正则
+ * @access public
+ * @param array|string $tags 标签名
+ * @param boolean $close 是否为闭合标签
+ * @return string
+ */
+ public function getRegex($tags, bool $close): string
+ {
+ $begin = $this->tpl->getConfig('taglib_begin');
+ $end = $this->tpl->getConfig('taglib_end');
+ $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
+ $tagName = is_array($tags) ? implode('|', $tags) : $tags;
+
+ if ($single) {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>[^' . $end . ']*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>[^' . $end . ']*)' . $end;
+ }
+ } else {
+ if ($close) {
+ // 如果是闭合标签
+ $regex = $begin . '(?:(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)|\/(' . $tagName . '))' . $end;
+ } else {
+ $regex = $begin . '(' . $tagName . ')\b(?>(?:(?!' . $end . ').)*)' . $end;
+ }
+ }
+
+ return '/' . $regex . '/is';
+ }
+
+ /**
+ * 分析标签属性 正则方式
+ * @access public
+ * @param string $str 标签属性字符串
+ * @param string $name 标签名
+ * @param string $alias 别名
+ * @return array
+ */
+ public function parseAttr(string $str, string $name, string $alias = ''): array
+ {
+ $regex = '/\s+(?>(?P[\w-]+)\s*)=(?>\s*)([\"\'])(?P(?:(?!\\2).)*)\\2/is';
+ $result = [];
+
+ if (preg_match_all($regex, $str, $matches)) {
+ foreach ($matches['name'] as $key => $val) {
+ $result[$val] = $matches['value'][$key];
+ }
+
+ if (!isset($this->tags[$name])) {
+ // 检测是否存在别名定义
+ foreach ($this->tags as $key => $val) {
+ if (isset($val['alias'])) {
+ $array = (array) $val['alias'];
+ if (in_array($name, explode(',', $array[0]))) {
+ $tag = $val;
+ $type = !empty($array[1]) ? $array[1] : 'type';
+ $result[$type] = $name;
+ break;
+ }
+ }
+ }
+ } else {
+ $tag = $this->tags[$name];
+ // 设置了标签别名
+ if (!empty($alias) && isset($tag['alias'])) {
+ $type = !empty($tag['alias'][1]) ? $tag['alias'][1] : 'type';
+ $result[$type] = $alias;
+ }
+ }
+
+ if (!empty($tag['must'])) {
+ $must = explode(',', $tag['must']);
+ foreach ($must as $name) {
+ if (!isset($result[$name])) {
+ throw new Exception('tag attr must:' . $name);
+ }
+ }
+ }
+ } else {
+ // 允许直接使用表达式的标签
+ if (!empty($this->tags[$name]['expression'])) {
+ static $_taglibs;
+ if (!isset($_taglibs[$name])) {
+ $_taglibs[$name][0] = strlen($this->tpl->getConfig('taglib_begin_origin') . $name);
+ $_taglibs[$name][1] = strlen($this->tpl->getConfig('taglib_end_origin'));
+ }
+ $result['expression'] = substr($str, $_taglibs[$name][0], -$_taglibs[$name][1]);
+ // 清除自闭合标签尾部/
+ $result['expression'] = rtrim($result['expression'], '/');
+ $result['expression'] = trim($result['expression']);
+ } elseif (empty($this->tags[$name]) || !empty($this->tags[$name]['attr'])) {
+ throw new Exception('tag error:' . $name);
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * 解析条件表达式
+ * @access public
+ * @param string $condition 表达式标签内容
+ * @return string
+ */
+ public function parseCondition(string $condition): string
+ {
+ if (strpos($condition, ':')) {
+ $condition = ' ' . substr(strstr($condition, ':'), 1);
+ }
+
+ $condition = str_ireplace(array_keys($this->comparison), array_values($this->comparison), $condition);
+ $this->tpl->parseVar($condition);
+
+ return $condition;
+ }
+
+ /**
+ * 自动识别构建变量
+ * @access public
+ * @param string $name 变量描述
+ * @return string
+ */
+ public function autoBuildVar(string &$name): string
+ {
+ $flag = substr($name, 0, 1);
+
+ if (':' == $flag) {
+ // 以:开头为函数调用,解析前去掉:
+ $name = substr($name, 1);
+ } elseif ('$' != $flag && preg_match('/[a-zA-Z_]/', $flag)) {
+ // XXX: 这句的写法可能还需要改进
+ // 常量不需要解析
+ if (defined($name)) {
+ return $name;
+ }
+
+ // 不以$开头并且也不是常量,自动补上$前缀
+ $name = '$' . $name;
+ }
+
+ $this->tpl->parseVar($name);
+ $this->tpl->parseVarFunction($name, false);
+
+ return $name;
+ }
+
+ /**
+ * 获取标签列表
+ * @access public
+ * @return array
+ */
+ public function getTags(): array
+ {
+ return $this->tags;
+ }
+}
diff --git a/extend/think/template/contract/DriverInterface.php b/extend/think/template/contract/DriverInterface.php
new file mode 100644
index 0000000..e1c52da
--- /dev/null
+++ b/extend/think/template/contract/DriverInterface.php
@@ -0,0 +1,35 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\driver;
+
+use Exception;
+use think\template\contract\DriverInterface;
+
+class File implements DriverInterface
+{
+ protected $cacheFile;
+
+ /**
+ * 写入编译缓存
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param string $content 缓存的内容
+ * @return void
+ */
+ public function write(string $cacheFile, string $content): void
+ {
+ // 检测模板目录
+ $dir = dirname($cacheFile);
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ // 生成模板缓存文件
+ if (false === file_put_contents($cacheFile, $content)) {
+ throw new Exception('cache write error:' . $cacheFile, 11602);
+ }
+ }
+
+ /**
+ * 读取编译编译
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param array $vars 变量数组
+ * @return void
+ */
+ public function read(string $cacheFile, array $vars = []): void
+ {
+ $this->cacheFile = $cacheFile;
+
+ if (!empty($vars) && is_array($vars)) {
+ // 模板阵列变量分解成为独立变量
+ extract($vars, EXTR_OVERWRITE);
+ }
+
+ //载入模版缓存文件
+ include $this->cacheFile;
+ }
+
+ /**
+ * 检查编译缓存是否有效
+ * @access public
+ * @param string $cacheFile 缓存的文件名
+ * @param int $cacheTime 缓存时间
+ * @return bool
+ */
+ public function check(string $cacheFile, int $cacheTime): bool
+ {
+ // 缓存文件不存在, 直接返回false
+ if (!file_exists($cacheFile)) {
+ return false;
+ }
+
+ if (0 != $cacheTime && time() > filemtime($cacheFile) + $cacheTime) {
+ // 缓存是否在有效期
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/extend/think/template/exception/TemplateNotFoundException.php b/extend/think/template/exception/TemplateNotFoundException.php
new file mode 100644
index 0000000..193e69c
--- /dev/null
+++ b/extend/think/template/exception/TemplateNotFoundException.php
@@ -0,0 +1,33 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\exception;
+
+class TemplateNotFoundException extends \RuntimeException
+{
+ protected $template;
+
+ public function __construct(string $message, string $template = '')
+ {
+ $this->message = $message;
+ $this->template = $template;
+ }
+
+ /**
+ * 获取模板文件
+ * @access public
+ * @return string
+ */
+ public function getTemplate(): string
+ {
+ return $this->template;
+ }
+}
diff --git a/extend/think/template/taglib/Cx.php b/extend/think/template/taglib/Cx.php
new file mode 100644
index 0000000..01d7ddb
--- /dev/null
+++ b/extend/think/template/taglib/Cx.php
@@ -0,0 +1,715 @@
+
+// +----------------------------------------------------------------------
+
+namespace think\template\taglib;
+
+use think\template\TagLib;
+
+/**
+ * CX标签库解析类
+ * @category Think
+ * @package Think
+ * @subpackage Driver.Taglib
+ * @author liu21st
+ */
+class Cx extends Taglib
+{
+
+ // 标签定义
+ protected $tags = [
+ // 标签定义: attr 属性列表 close 是否闭合(0 或者1 默认1) alias 标签别名 level 嵌套层次
+ 'php' => ['attr' => ''],
+ 'volist' => ['attr' => 'name,id,offset,length,key,mod', 'alias' => 'iterate'],
+ 'foreach' => ['attr' => 'name,id,item,key,offset,length,mod', 'expression' => true],
+ 'if' => ['attr' => 'condition', 'expression' => true],
+ 'elseif' => ['attr' => 'condition', 'close' => 0, 'expression' => true],
+ 'else' => ['attr' => '', 'close' => 0],
+ 'switch' => ['attr' => 'name', 'expression' => true],
+ 'case' => ['attr' => 'value,break', 'expression' => true],
+ 'default' => ['attr' => '', 'close' => 0],
+ 'compare' => ['attr' => 'name,value,type', 'alias' => ['eq,equal,notequal,neq,gt,lt,egt,elt,heq,nheq', 'type']],
+ 'range' => ['attr' => 'name,value,type', 'alias' => ['in,notin,between,notbetween', 'type']],
+ 'empty' => ['attr' => 'name'],
+ 'notempty' => ['attr' => 'name'],
+ 'present' => ['attr' => 'name'],
+ 'notpresent' => ['attr' => 'name'],
+ 'defined' => ['attr' => 'name'],
+ 'notdefined' => ['attr' => 'name'],
+ 'load' => ['attr' => 'file,href,type,value,basepath', 'close' => 0, 'alias' => ['import,css,js', 'type']],
+ 'assign' => ['attr' => 'name,value', 'close' => 0],
+ 'define' => ['attr' => 'name,value', 'close' => 0],
+ 'for' => ['attr' => 'start,end,name,comparison,step'],
+ 'url' => ['attr' => 'link,vars,suffix,domain', 'close' => 0, 'expression' => true],
+ 'function' => ['attr' => 'name,vars,use,call'],
+ ];
+
+ /**
+ * php标签解析
+ * 格式:
+ * {php}echo $name{/php}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPhp(array $tag, string $content): string
+ {
+ $parseStr = '';
+ return $parseStr;
+ }
+
+ /**
+ * volist标签解析 循环输出数据集
+ * 格式:
+ * {volist name="userList" id="user" empty=""}
+ * {user.username}
+ * {user.email}
+ * {/volist}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagVolist(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $id = $tag['id'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $key = !empty($tag['key']) ? $tag['key'] : 'i';
+ $mod = isset($tag['mod']) ? $tag['mod'] : '2';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+ // 允许使用函数设定数据集 {$vo.name}
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= '$_result=' . $name . ';';
+ $name = '$_result';
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): $' . $key . ' = 0;';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ $parseStr .= '$__LIST__ = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $parseStr .= ' $__LIST__ = ' . $name . ';';
+ }
+
+ $parseStr .= 'if( count($__LIST__)==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+ $parseStr .= 'foreach($__LIST__ as $key=>$' . $id . '): ';
+ $parseStr .= '$mod = ($' . $key . ' % ' . $mod . ' );';
+ $parseStr .= '++$' . $key . ';?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * foreach标签解析 循环输出数据集
+ * 格式:
+ * {foreach name="userList" id="user" key="key" index="i" mod="2" offset="3" length="5" empty=""}
+ * {user.username}
+ * {/foreach}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagForeach(array $tag, string $content): string
+ {
+ // 直接使用表达式
+ if (!empty($tag['expression'])) {
+ $expression = ltrim(rtrim($tag['expression'], ')'), '(');
+ $expression = $this->autoBuildVar($expression);
+ $parseStr = '';
+ $parseStr .= $content;
+ $parseStr .= '';
+ return $parseStr;
+ }
+
+ $name = $tag['name'];
+ $key = !empty($tag['key']) ? $tag['key'] : 'key';
+ $item = !empty($tag['id']) ? $tag['id'] : $tag['item'];
+ $empty = isset($tag['empty']) ? $tag['empty'] : '';
+ $offset = !empty($tag['offset']) && is_numeric($tag['offset']) ? intval($tag['offset']) : 0;
+ $length = !empty($tag['length']) && is_numeric($tag['length']) ? intval($tag['length']) : 'null';
+
+ $parseStr = 'autoBuildVar($name);
+ $parseStr .= $var . '=' . $name . '; ';
+ $name = $var;
+ } else {
+ $name = $this->autoBuildVar($name);
+ }
+
+ $parseStr .= 'if(is_array(' . $name . ') || ' . $name . ' instanceof \think\Collection || ' . $name . ' instanceof \think\Paginator): ';
+
+ // 设置了输出数组长度
+ if (0 != $offset || 'null' != $length) {
+ if (!isset($var)) {
+ $var = '$_' . uniqid();
+ }
+ $parseStr .= $var . ' = is_array(' . $name . ') ? array_slice(' . $name . ',' . $offset . ',' . $length . ', true) : ' . $name . '->slice(' . $offset . ',' . $length . ', true); ';
+ } else {
+ $var = &$name;
+ }
+
+ $parseStr .= 'if( count(' . $var . ')==0 ) : echo "' . $empty . '" ;';
+ $parseStr .= 'else: ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ $parseStr .= '$' . $index . '=0; ';
+ }
+
+ $parseStr .= 'foreach(' . $var . ' as $' . $key . '=>$' . $item . '): ';
+
+ // 设置了索引项
+ if (isset($tag['index'])) {
+ $index = $tag['index'];
+ if (isset($tag['mod'])) {
+ $mod = (int) $tag['mod'];
+ $parseStr .= '$mod = ($' . $index . ' % ' . $mod . '); ';
+ }
+ $parseStr .= '++$' . $index . '; ';
+ }
+
+ $parseStr .= '?>';
+ // 循环体中的内容
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * if标签解析
+ * 格式:
+ * {if condition=" $a eq 1"}
+ * {elseif condition="$a eq 2" /}
+ * {else /}
+ * {/if}
+ * 表达式支持 eq neq gt egt lt elt == > >= < <= or and || &&
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagIf(array $tag, string $content): string
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * elseif标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagElseif(array $tag, string $content): string
+ {
+ $condition = !empty($tag['expression']) ? $tag['expression'] : $tag['condition'];
+ $condition = $this->parseCondition($condition);
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * else标签解析
+ * 格式:见if标签
+ * @access public
+ * @param array $tag 标签属性
+ * @return string
+ */
+ public function tagElse(array $tag): string
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * switch标签解析
+ * 格式:
+ * {switch name="a.name"}
+ * {case value="1" break="false"}1{/case}
+ * {case value="2" }2{/case}
+ * {default /}other
+ * {/switch}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagSwitch(array $tag, string $content): string
+ {
+ $name = !empty($tag['expression']) ? $tag['expression'] : $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * case标签解析 需要配合switch才有效
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCase(array $tag, string $content): string
+ {
+ $value = isset($tag['expression']) ? $tag['expression'] : $tag['value'];
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $value = 'case ' . $value . ':';
+ } elseif (strpos($value, '|')) {
+ $values = explode('|', $value);
+ $value = '';
+ foreach ($values as $val) {
+ $value .= 'case "' . addslashes($val) . '":';
+ }
+ } else {
+ $value = 'case "' . $value . '":';
+ }
+
+ $parseStr = '' . $content;
+ $isBreak = isset($tag['break']) ? $tag['break'] : '';
+
+ if ('' == $isBreak || $isBreak) {
+ $parseStr .= '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * default标签解析 需要配合switch才有效
+ * 使用: {default /}ddfdf
+ * @access public
+ * @param array $tag 标签属性
+ * @return string
+ */
+ public function tagDefault(array $tag): string
+ {
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * compare标签解析
+ * 用于值的比较 支持 eq neq gt lt egt elt heq nheq 默认是eq
+ * 格式: {compare name="" type="eq" value="" }content{/compare}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagCompare(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'eq'; // 比较类型
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ } else {
+ $value = '\'' . $value . '\'';
+ }
+
+ switch ($type) {
+ case 'equal':
+ $type = 'eq';
+ break;
+ case 'notequal':
+ $type = 'neq';
+ break;
+ }
+ $type = $this->parseCondition(' ' . $type . ' ');
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * range标签解析
+ * 如果某个变量存在于某个范围 则输出内容 type= in 表示在范围内 否则表示在范围外
+ * 格式: {range name="var|function" value="val" type='in|notin' }content{/range}
+ * example: {range name="a" value="1,2,3" type='in' }content{/range}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagRange(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $value = $tag['value'];
+ $type = isset($tag['type']) ? $tag['type'] : 'in'; // 比较类型
+
+ $name = $this->autoBuildVar($name);
+ $flag = substr($value, 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ $str = 'is_array(' . $value . ')?' . $value . ':explode(\',\',' . $value . ')';
+ } else {
+ $value = '"' . $value . '"';
+ $str = 'explode(\',\',' . $value . ')';
+ }
+
+ if ('between' == $type) {
+ $parseStr = '= $_RANGE_VAR_[0] && ' . $name . '<= $_RANGE_VAR_[1]):?>' . $content . '';
+ } elseif ('notbetween' == $type) {
+ $parseStr = '$_RANGE_VAR_[1]):?>' . $content . '';
+ } else {
+ $fun = ('in' == $type) ? 'in_array' : '!in_array';
+ $parseStr = '' . $content . '';
+ }
+
+ return $parseStr;
+ }
+
+ /**
+ * present标签解析
+ * 如果某个变量已经设置 则输出内容
+ * 格式: {present name="" }content{/present}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagPresent(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notpresent标签解析
+ * 如果某个变量没有设置,则输出内容
+ * 格式: {notpresent name="" }content{/notpresent}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotpresent(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * empty标签解析
+ * 如果某个变量为empty 则输出内容
+ * 格式: {empty name="" }content{/empty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagEmpty(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty())): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * notempty标签解析
+ * 如果某个变量不为empty 则输出内容
+ * 格式: {notempty name="" }content{/notempty}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagNotempty(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $name = $this->autoBuildVar($name);
+ $parseStr = 'isEmpty()))): ?>' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否已经定义了该常量
+ * {defined name='TXT'}已定义{/defined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagDefined(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * 判断是否没有定义了该常量
+ * {notdefined name='TXT'}已定义{/notdefined}
+ * @access public
+ * @param array $tag
+ * @param string $content
+ * @return string
+ */
+ public function tagNotdefined(array $tag, string $content): string
+ {
+ $name = $tag['name'];
+ $parseStr = '' . $content . '';
+
+ return $parseStr;
+ }
+
+ /**
+ * load 标签解析 {load file="/static/js/base.js" /}
+ * 格式:{load file="/static/css/base.css" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagLoad(array $tag, string $content): string
+ {
+ $file = isset($tag['file']) ? $tag['file'] : $tag['href'];
+ $type = isset($tag['type']) ? strtolower($tag['type']) : '';
+
+ $parseStr = '';
+ $endStr = '';
+
+ // 判断是否存在加载条件 允许使用函数判断(默认为isset)
+ if (isset($tag['value'])) {
+ $name = $tag['value'];
+ $name = $this->autoBuildVar($name);
+ $name = 'isset(' . $name . ')';
+ $parseStr .= '';
+ $endStr = '';
+ }
+
+ // 文件方式导入
+ $array = explode(',', $file);
+
+ foreach ($array as $val) {
+ $type = strtolower(substr(strrchr($val, '.'), 1));
+ switch ($type) {
+ case 'js':
+ $parseStr .= '';
+ break;
+ case 'css':
+ $parseStr .= '';
+ break;
+ case 'php':
+ $parseStr .= '';
+ break;
+ }
+ }
+
+ return $parseStr . $endStr;
+ }
+
+ /**
+ * assign标签解析
+ * 在模板中给某个变量赋值 支持变量赋值
+ * 格式: {assign name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagAssign(array $tag, string $content): string
+ {
+ $name = $this->autoBuildVar($tag['name']);
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * define标签解析
+ * 在模板中定义常量 支持变量赋值
+ * 格式: {define name="" value="" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagDefine(array $tag, string $content): string
+ {
+ $name = '\'' . $tag['name'] . '\'';
+ $flag = substr($tag['value'], 0, 1);
+
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($tag['value']);
+ } else {
+ $value = '\'' . $tag['value'] . '\'';
+ }
+
+ $parseStr = '';
+
+ return $parseStr;
+ }
+
+ /**
+ * for标签解析
+ * 格式:
+ * {for start="" end="" comparison="" step="" name=""}
+ * content
+ * {/for}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFor(array $tag, string $content): string
+ {
+ //设置默认值
+ $start = 0;
+ $end = 0;
+ $step = 1;
+ $comparison = 'lt';
+ $name = 'i';
+ $rand = rand(); //添加随机数,防止嵌套变量冲突
+
+ //获取属性
+ foreach ($tag as $key => $value) {
+ $value = trim($value);
+ $flag = substr($value, 0, 1);
+ if ('$' == $flag || ':' == $flag) {
+ $value = $this->autoBuildVar($value);
+ }
+
+ switch ($key) {
+ case 'start':
+ $start = $value;
+ break;
+ case 'end':
+ $end = $value;
+ break;
+ case 'step':
+ $step = $value;
+ break;
+ case 'comparison':
+ $comparison = $value;
+ break;
+ case 'name':
+ $name = $value;
+ break;
+ }
+ }
+
+ $parseStr = 'parseCondition('$' . $name . ' ' . $comparison . ' $__FOR_END_' . $rand . '__') . ';$' . $name . '+=' . $step . '){ ?>';
+ $parseStr .= $content;
+ $parseStr .= '';
+
+ return $parseStr;
+ }
+
+ /**
+ * url函数的tag标签
+ * 格式:{url link="模块/控制器/方法" vars="参数" suffix="true或者false 是否带有后缀" domain="true或者false 是否携带域名" /}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagUrl(array $tag, string $content): string
+ {
+ $url = isset($tag['link']) ? $tag['link'] : '';
+ $vars = isset($tag['vars']) ? $tag['vars'] : '';
+ $suffix = isset($tag['suffix']) ? $tag['suffix'] : 'true';
+ $domain = isset($tag['domain']) ? $tag['domain'] : 'false';
+
+ return '';
+ }
+
+ /**
+ * function标签解析 匿名函数,可实现递归
+ * 使用:
+ * {function name="func" vars="$data" call="$list" use="&$a,&$b"}
+ * {if is_array($data)}
+ * {foreach $data as $val}
+ * {~func($val) /}
+ * {/foreach}
+ * {else /}
+ * {$data}
+ * {/if}
+ * {/function}
+ * @access public
+ * @param array $tag 标签属性
+ * @param string $content 标签内容
+ * @return string
+ */
+ public function tagFunction(array $tag, string $content): string
+ {
+ $name = !empty($tag['name']) ? $tag['name'] : 'func';
+ $vars = !empty($tag['vars']) ? $tag['vars'] : '';
+ $call = !empty($tag['call']) ? $tag['call'] : '';
+ $use = ['&$' . $name];
+
+ if (!empty($tag['use'])) {
+ foreach (explode(',', $tag['use']) as $val) {
+ $use[] = '&' . ltrim(trim($val), '&');
+ }
+ }
+
+ $parseStr = '' . $content . '' : '?>';
+
+ return $parseStr;
+ }
+}
diff --git a/extend/think/view/driver/Think.php b/extend/think/view/driver/Think.php
index df65af6..19f0df6 100644
--- a/extend/think/view/driver/Think.php
+++ b/extend/think/view/driver/Think.php
@@ -29,8 +29,6 @@ class Think
'auto_rule' => 1,
// 视图目录名
'view_dir_name' => 'view',
- // 模板起始路径
- 'view_path' => '',
// 模板文件后缀
'view_suffix' => 'html',
// 模板文件名分隔符
@@ -138,7 +136,7 @@ class Think
$request = $this->app['request'];
// 获取视图根目录
- if (strpos($template, '@')) {
+ if (strpos($template, '@') !== false) {
// 跨模块调用
list($app, $template) = explode('@', $template);
} else {
@@ -147,9 +145,30 @@ class Think
$view = $this->config['view_dir_name'];
- $app_path = $this->app->getAppPath() . $view . DIRECTORY_SEPARATOR;
- $base_app_path = $this->app->getRootPath() . 'extend' . DIRECTORY_SEPARATOR . 'base' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . $view . DIRECTORY_SEPARATOR;
- $view_app_path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
+ if (empty($app)) {
+ if (str_starts_with($template, '/')) {
+ $app_path = $this->app->getRootPath();
+ $base_app_path = $this->app->getRootPath();
+ $view_app_path = $this->app->getRootPath();
+ } else {
+ // $app不应该为空,如果为空,说明是@开头的用法,此时定位当前应用控制器下的用法
+ $app = $this->app->http->getName();
+
+ $app_path = $this->app->getAppPath() . $view . DIRECTORY_SEPARATOR;
+ $base_app_path = $this->app->getRootPath() . 'extend' . DIRECTORY_SEPARATOR . 'base' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . $view . DIRECTORY_SEPARATOR;
+ $view_app_path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
+ }
+ } else {
+ $app_path = $this->app->getAppPath() . $view . DIRECTORY_SEPARATOR;
+ $base_app_path = $this->app->getRootPath() . 'extend' . DIRECTORY_SEPARATOR . 'base' . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . $view . DIRECTORY_SEPARATOR;
+ $view_app_path = $this->app->getRootPath() . $view . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : '');
+ }
+
+ $this->template->view_root_path = $this->app->getRootPath();
+
+ $this->template->app_path = $app_path;
+ $this->template->base_app_path = $base_app_path;
+ $this->template->view_app_path = $view_app_path;
$depr = $this->config['view_depr'];
@@ -186,23 +205,19 @@ class Think
$view_file_path = ltrim($template, '/') . '.' . ltrim($this->config['view_suffix'], '.');
- $view_path = '';
$file_path = '';
if (is_file($app_path . $view_file_path)) {
// 优先app下的view
- $view_path = $app_path;
$file_path = $app_path . $view_file_path;
} elseif (is_file($base_app_path . $view_file_path)) {
// 查找extend下的view
- $view_path = $base_app_path;
$file_path = $base_app_path . $view_file_path;
} else {
// 查找根目录下的view
- $view_path = $view_app_path;
$file_path = $view_app_path . $view_file_path;
}
- $this->template->view_path = $view_path;
+ $this->template->base_view_path = dirname($file_path) . DIRECTORY_SEPARATOR;
return $file_path;
}