From 153667f0b461e2fc4a45cc06dd63652e08c1ce65 Mon Sep 17 00:00:00 2001 From: whwyy Date: Mon, 1 Dec 2025 07:16:58 +0800 Subject: [PATCH] eee --- src/Blade/BladeCompiler.php | 269 +++++++++++++++++++++++- src/Blade/BladeView.php | 4 + src/Blade/README.md | 43 +++- src/Blade/SYNTAX.md | 405 ++++++++++++++++++++++++++++++++++++ 4 files changed, 715 insertions(+), 6 deletions(-) create mode 100644 src/Blade/SYNTAX.md diff --git a/src/Blade/BladeCompiler.php b/src/Blade/BladeCompiler.php index e6cedb3..9a17aab 100644 --- a/src/Blade/BladeCompiler.php +++ b/src/Blade/BladeCompiler.php @@ -113,20 +113,32 @@ class BladeCompiler // 移除注释 $content = $this->compileComments($content); - // 编译 Echo 语句 + // 编译 Echo 语句(包括 @json) $content = $this->compileEchos($content); // 编译指令 $content = $this->compileDirectives($content); + // 编译条件判断语法糖 + $content = $this->compileConditionalDirectives($content); + // 编译布局和继承(如果未跳过) if (!$skipLayouts) { $content = $this->compileLayouts($content); } - // 编译包含 + // 编译包含和组件 $content = $this->compileIncludes($content); + // 编译 @each 指令 + $content = $this->compileEach($content); + + // 编译栈和推送 + $content = $this->compileStacks($content); + + // 编译表单辅助 + $content = $this->compileFormHelpers($content); + // 编译原始 PHP $content = $this->compilePhp($content); @@ -148,6 +160,7 @@ class BladeCompiler * 编译 Echo 语句 * {{ $var }} => * {!! $var !!} => + * @json($var) => JSON 编码输出 * * @param string $content * @return string @@ -166,6 +179,12 @@ class BladeCompiler return ""; }, $content); + // 编译 @json 指令 + $content = preg_replace_callback('/@json\s*\((.+?)\)/', function ($matches) { + $expression = trim($matches[1]); + return ""; + }, $content); + return $content; } @@ -214,6 +233,25 @@ class BladeCompiler $content = preg_replace('/@break\b/', '', $content); $content = preg_replace('/@continue\b/', '', $content); + // 处理 @lang 指令(语言翻译) + $content = preg_replace_callback('/@lang\s*\([\'"](.+?)[\'"]\s*(?:,\s*\[(.+?)\])?\)/', function ($matches) { + $key = $matches[1]; + $replace = $matches[2] ?? '[]'; + return ""; + }, $content); + + // 处理 @class 指令(条件类名) + $content = preg_replace_callback('/@class\s*\((.+?)\)/', function ($matches) { + $conditions = trim($matches[1]); + return ""; + }, $content); + + // 处理 @style 指令(条件样式) + $content = preg_replace_callback('/@style\s*\((.+?)\)/', function ($matches) { + $styles = trim($matches[1]); + return ""; + }, $content); + return $content; } @@ -321,6 +359,233 @@ class BladeCompiler }, $content); } + /** + * 编译 @each 指令 + * @each('view', $items, 'item', 'empty') + * + * @param string $content + * @return string + */ + protected function compileEach(string $content): string + { + return preg_replace_callback('/@each\s*\([\'"](.+?)[\'"]\s*,\s*(.+?)\s*(?:,\s*[\'"](.+?)[\'"]\s*(?:,\s*[\'"](.+?)[\'"])?)?\)/', function ($matches) { + $view = $matches[1]; + $items = trim($matches[2]); + $itemVar = $matches[3] ?? "'item'"; + $emptyView = $matches[4] ?? null; + + $itemVar = trim($itemVar, "'\""); + + if ($emptyView) { + return " 0): foreach ({$items} as \${$itemVar}): echo \Kiri\Router\Blade\BladeHelper::include('{$view}', ['{$itemVar}' => \${$itemVar}]); endforeach; else: echo \Kiri\Router\Blade\BladeHelper::include('{$emptyView}', []); endif; ?>"; + } + + return " \${$itemVar}]); endforeach; ?>"; + }, $content); + } + + /** + * 编译条件判断语法糖 + * @isset, @empty, @auth, @guest, @hasSection, @unless 等 + * + * @param string $content + * @return string + */ + protected function compileConditionalDirectives(string $content): string + { + // @isset($var) ... @endisset + $content = preg_replace_callback('/@isset\s*\((.+?)\)\s*(.*?)\s*@endisset/s', function ($matches) { + $var = trim($matches[1]); + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @empty($var) ... @endempty + $content = preg_replace_callback('/@empty\s*\((.+?)\)\s*(.*?)\s*@endempty/s', function ($matches) { + $var = trim($matches[1]); + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @auth ... @endauth + $content = preg_replace_callback('/@auth\s*(?:\((.+?)\))?\s*(.*?)\s*@endauth/s', function ($matches) { + $guard = $matches[1] ?? ''; + $body = $matches[2]; + if ($guard) { + return "check()): ?>{$body}"; + } + return "check()): ?>{$body}"; + }, $content); + + // @guest ... @endguest + $content = preg_replace_callback('/@guest\s*(?:\((.+?)\))?\s*(.*?)\s*@endguest/s', function ($matches) { + $guard = $matches[1] ?? ''; + $body = $matches[2]; + if ($guard) { + return "check()): ?>{$body}"; + } + return "check()): ?>{$body}"; + }, $content); + + // @hasSection('name') ... @endhasSection + $content = preg_replace_callback('/@hasSection\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endhasSection/s', function ($matches) { + $name = $matches[1]; + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @sectionMissing('name') ... @endsectionMissing + $content = preg_replace_callback('/@sectionMissing\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endsectionMissing/s', function ($matches) { + $name = $matches[1]; + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @unless($condition) ... @endunless + $content = preg_replace_callback('/@unless\s*\((.+?)\)\s*(.*?)\s*@endunless/s', function ($matches) { + $condition = trim($matches[1]); + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @can('permission') ... @endcan + $content = preg_replace_callback('/@can\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)\s*(.*?)\s*@endcan/s', function ($matches) { + $permission = $matches[1]; + $model = $matches[2] ?? ''; + $body = $matches[3]; + if ($model) { + return "{$body}"; + } + return "{$body}"; + }, $content); + + // @cannot('permission') ... @endcannot + $content = preg_replace_callback('/@cannot\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)\s*(.*?)\s*@endcannot/s', function ($matches) { + $permission = $matches[1]; + $model = $matches[2] ?? ''; + $body = $matches[3]; + if ($model) { + return "{$body}"; + } + return "{$body}"; + }, $content); + + // @forelse($items as $item) ... @empty ... @endforelse + $content = preg_replace_callback('/@forelse\s*\((.+?)\)\s*(.*?)\s*@empty\s*(.*?)\s*@endforelse/s', function ($matches) { + $loop = trim($matches[1]); + $body = $matches[2]; + $empty = $matches[3]; + return " 0): ?>{$body}{$empty}"; + }, $content); + + // @once ... @endonce (只执行一次) + $content = preg_replace_callback('/@once\s*(.*?)\s*@endonce/s', function ($matches) { + static $onceCounter = 0; + $onceCounter++; + $hash = md5($matches[0] . $onceCounter); + $body = $matches[1]; + return "{$body}"; + }, $content); + + // @error('field') ... @enderror + $content = preg_replace_callback('/@error\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@enderror/s', function ($matches) { + $field = $matches[1]; + $body = $matches[2]; + return "has('{$field}')): ?>{$body}"; + }, $content); + + return $content; + } + + /** + * 编译栈和推送 @push, @stack, @prepend + * + * @param string $content + * @return string + */ + protected function compileStacks(string $content): string + { + // @push('name') ... @endpush + $content = preg_replace_callback('/@push\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endpush/s', function ($matches) { + $name = $matches[1]; + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @prepend('name') ... @endprepend + $content = preg_replace_callback('/@prepend\s*\([\'"](.+?)[\'"]\)\s*(.*?)\s*@endprepend/s', function ($matches) { + $name = $matches[1]; + $body = $matches[2]; + return "{$body}"; + }, $content); + + // @stack('name') + $content = preg_replace_callback('/@stack\s*\([\'"](.+?)[\'"]\)/', function ($matches) { + $name = $matches[1]; + return ""; + }, $content); + + return $content; + } + + /** + * 编译表单辅助函数 + * @csrf, @method, @old, @checked, @selected, @disabled + * + * @param string $content + * @return string + */ + protected function compileFormHelpers(string $content): string + { + // @csrf - CSRF token + $content = preg_replace('/@csrf/', '', $content); + + // @method('PUT') - HTTP method spoofing + $content = preg_replace_callback('/@method\s*\([\'"](.+?)[\'"]\)/', function ($matches) { + $method = strtoupper($matches[1]); + return ""; + }, $content); + + // @old('field', 'default') + $content = preg_replace_callback('/@old\s*\([\'"](.+?)[\'"]\s*(?:,\s*(.+?))?\)/', function ($matches) { + $field = $matches[1]; + $default = $matches[2] ?? "''"; + return ""; + }, $content); + + // @checked($condition) + $content = preg_replace_callback('/@checked\s*\((.+?)\)/', function ($matches) { + $condition = trim($matches[1]); + return ""; + }, $content); + + // @selected($condition) + $content = preg_replace_callback('/@selected\s*\((.+?)\)/', function ($matches) { + $condition = trim($matches[1]); + return ""; + }, $content); + + // @disabled($condition) + $content = preg_replace_callback('/@disabled\s*\((.+?)\)/', function ($matches) { + $condition = trim($matches[1]); + return ""; + }, $content); + + // @readonly($condition) + $content = preg_replace_callback('/@readonly\s*\((.+?)\)/', function ($matches) { + $condition = trim($matches[1]); + return ""; + }, $content); + + // @required($condition) + $content = preg_replace_callback('/@required\s*\((.+?)\)/', function ($matches) { + $condition = trim($matches[1]); + return ""; + }, $content); + + return $content; + } + /** * 编译原始 PHP @php ... @endphp * diff --git a/src/Blade/BladeView.php b/src/Blade/BladeView.php index af5b4b9..b647f1f 100644 --- a/src/Blade/BladeView.php +++ b/src/Blade/BladeView.php @@ -49,6 +49,10 @@ class BladeView // 提取数据 extract($this->data, EXTR_SKIP); + // 初始化栈和 section 数组 + $__stacks = []; + $__sections = []; + // 开始输出缓冲 ob_start(); diff --git a/src/Blade/README.md b/src/Blade/README.md index e6f30c2..e4ccf9d 100644 --- a/src/Blade/README.md +++ b/src/Blade/README.md @@ -4,15 +4,50 @@ ## 功能特性 +### 基础功能 - ✅ 变量输出:`{{ $variable }}` 和 `{!! $variable !!}` -- ✅ 控制结构:`@if`, `@elseif`, `@else`, `@endif` -- ✅ 循环:`@foreach`, `@endforeach`, `@for`, `@endfor`, `@while`, `@endwhile` -- ✅ 布局继承:`@extends`, `@section`, `@yield`, `@endsection` -- ✅ 包含视图:`@include` +- ✅ JSON 输出:`@json($data)` +- ✅ 控制结构:`@if`, `@elseif`, `@else`, `@endif`, `@unless` +- ✅ 循环:`@foreach`, `@for`, `@while`, `@forelse` 及其结束指令 +- ✅ Switch:`@switch`, `@case`, `@default`, `@endswitch` +- ✅ 布局继承:`@extends`, `@section`, `@yield`, `@endsection`, `@parent` +- ✅ 包含视图:`@include`, `@each` - ✅ 注释:`{{-- comment --}}` - ✅ 原始 PHP:`@php ... @endphp` - ✅ 编译缓存:自动缓存编译后的模板以提高性能 +### 条件判断语法糖 +- ✅ `@isset` / `@endisset` - 检查变量是否存在 +- ✅ `@empty` / `@endempty` - 检查变量是否为空 +- ✅ `@auth` / `@endauth` - 检查用户是否已认证 +- ✅ `@guest` / `@endguest` - 检查用户是否未认证 +- ✅ `@hasSection` / `@endhasSection` - 检查 section 是否存在 +- ✅ `@sectionMissing` / `@endsectionMissing` - 检查 section 是否缺失 +- ✅ `@can` / `@endcan` - 权限检查 +- ✅ `@cannot` / `@endcannot` - 反向权限检查 + +### 表单辅助 +- ✅ `@csrf` - CSRF token +- ✅ `@method` - HTTP 方法伪装 +- ✅ `@old` - 旧输入值 +- ✅ `@checked` - 复选框选中状态 +- ✅ `@selected` - 下拉框选中状态 +- ✅ `@disabled` - 禁用状态 +- ✅ `@readonly` - 只读状态 +- ✅ `@required` - 必填状态 +- ✅ `@error` / `@enderror` - 错误信息显示 + +### 栈和推送 +- ✅ `@push` / `@endpush` - 推送到栈 +- ✅ `@prepend` / `@endprepend` - 前置推送到栈 +- ✅ `@stack` - 输出栈内容 + +### 其他语法糖 +- ✅ `@once` / `@endonce` - 只执行一次 +- ✅ `@lang` - 语言翻译 +- ✅ `@class` - 条件类名 +- ✅ `@style` - 条件样式 + ## 使用方法 ### 基本用法 diff --git a/src/Blade/SYNTAX.md b/src/Blade/SYNTAX.md new file mode 100644 index 0000000..4ab7d17 --- /dev/null +++ b/src/Blade/SYNTAX.md @@ -0,0 +1,405 @@ +# Blade 语法糖参考手册 + +本文档列出了所有可用的 Blade 语法糖和指令。 + +## 变量输出 + +### 转义输出 +```blade +{{ $variable }} +{{ $user->name }} +{{ $array['key'] }} +``` +自动转义 HTML 特殊字符,防止 XSS 攻击。 + +### 原始输出 +```blade +{!! $htmlContent !!} +``` +不转义,直接输出原始 HTML(谨慎使用)。 + +### JSON 输出 +```blade +@json($data) +``` +将数据编码为 JSON 字符串输出。 + +## 控制结构 + +### 条件判断 + +#### @if / @elseif / @else / @endif +```blade +@if($user->isAdmin()) +

管理员

+@elseif($user->isModerator()) +

版主

+@else +

普通用户

+@endif +``` + +#### @unless / @endunless +```blade +@unless($user->isBanned()) +

用户正常

+@endunless +``` +等同于 `@if(!condition)` + +#### @isset / @endisset +```blade +@isset($variable) +

{{ $variable }}

+@endisset +``` + +#### @empty / @endempty +```blade +@empty($items) +

列表为空

+@endempty +``` + +### 循环 + +#### @foreach / @endforeach +```blade +@foreach($users as $user) +

{{ $user->name }}

+@endforeach +``` + +#### @for / @endfor +```blade +@for($i = 0; $i < 10; $i++) +

Item {{ $i }}

+@endfor +``` + +#### @while / @endwhile +```blade +@while($condition) +

循环内容

+@endwhile +``` + +#### @forelse / @empty / @endforelse +```blade +@forelse($items as $item) +

{{ $item->name }}

+@empty +

没有项目

+@endforelse +``` + +#### @break 和 @continue +```blade +@foreach($items as $item) + @if($item->hidden) + @continue + @endif +

{{ $item->name }}

+ @if($item->id > 100) + @break + @endif +@endforeach +``` + +### Switch 语句 + +```blade +@switch($status) + @case(1) +

待处理

+ @break + @case(2) +

处理中

+ @break + @default +

未知状态

+@endswitch +``` + +## 布局和继承 + +### @extends +```blade +@extends('layouts.app') +``` + +### @section / @endsection +```blade +@section('content') +

页面内容

+@endsection +``` + +### @yield +```blade +@yield('content') +@yield('title', '默认标题') +``` + +### @parent +在子视图的 section 中使用,输出父布局中对应 section 的内容。 + +### @hasSection / @endhasSection +```blade +@hasSection('sidebar') + @yield('sidebar') +@endhasSection +``` + +### @sectionMissing / @endsectionMissing +```blade +@sectionMissing('sidebar') +

没有侧边栏

+@endsectionMissing +``` + +## 包含和组件 + +### @include +```blade +@include('components.header') +@include('components.footer', ['year' => 2024]) +``` + +### @each +```blade +@each('components.item', $items, 'item') +@each('components.item', $items, 'item', 'components.empty') +``` + +## 栈和推送 + +### @push / @endpush +```blade +@push('scripts') + +@endpush +``` + +### @prepend / @endprepend +```blade +@prepend('scripts') + +@endprepend +``` + +### @stack +```blade +@stack('scripts') +``` + +## 表单辅助 + +### @csrf +```blade +
+ @csrf + ... +
+``` + +### @method +```blade +
+ @method('PUT') + ... +
+``` + +### @old +```blade + +``` + +### @checked +```blade +isActive())> +``` + +### @selected +```blade + +``` + +### @disabled +```blade + +``` + +### @readonly +```blade + +``` + +### @required +```blade + +``` + +## 权限和认证 + +### @auth / @endauth +```blade +@auth +

欢迎,{{ auth()->user()->name }}

+@endauth + +@auth('admin') +

管理员面板

+@endauth +``` + +### @guest / @endguest +```blade +@guest + 登录 +@endguest +``` + +### @can / @endcan +```blade +@can('edit', $post) + 编辑 +@endcan +``` + +### @cannot / @endcannot +```blade +@cannot('edit', $post) +

您没有编辑权限

+@endcannot +``` + +## 错误处理 + +### @error / @enderror +```blade +@error('email') +

{{ $message }}

+@enderror +``` + +## 其他语法糖 + +### @once / @endonce +```blade +@once + +@endonce +``` +确保内容只输出一次。 + +### @lang +```blade +@lang('messages.welcome') +@lang('messages.greeting', ['name' => $user->name]) +``` + +### @class +```blade +
$isActive, 'disabled' => $isDisabled])> +``` + +### @style +```blade +
'red', 'font-size' => '14px'])> +``` + +### @php / @endphp +```blade +@php + $count = count($items); + $total = array_sum($prices); +@endphp +``` + +### 注释 +```blade +{{-- 这是注释,不会输出到 HTML --}} +``` + +## 使用示例 + +### 完整的页面模板 +```blade +@extends('layouts.app') + +@section('title', '用户资料') + +@push('styles') + +@endpush + +@section('content') + @auth +

欢迎,{{ auth()->user()->name }}

+ + @if($user->isAdmin()) +

管理员

+ @endif + +
+ @csrf + @method('PUT') + + + @error('name') +

{{ $message }}

+ @enderror + + +
+ +

文章列表

+ @forelse($posts as $post) +
+

{{ $post->title }}

+

{{ $post->content }}

+
+ @empty +

暂无文章

+ @endforelse + @else +

请先登录

+ @endauth +@endsection + +@push('scripts') + +@endpush +``` + +### 布局文件 +```blade + + + + @yield('title', '默认标题') + @stack('styles') + + +
+ @include('components.header') +
+ +
+ @yield('content') +
+ + + + @stack('scripts') + + +``` +