@barefootjs/blade 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/blade-adapter.d.ts +537 -0
- package/dist/adapter/blade-adapter.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +76 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +105 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +74 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +176 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189060 -0
- package/dist/adapter/lib/blade-naming.d.ts +128 -0
- package/dist/adapter/lib/blade-naming.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +48 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +84 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +34 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +72 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +27 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189080 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189079 -0
- package/package.json +67 -0
- package/php/composer.json +32 -0
- package/php/src/BladeBackend.php +197 -0
- package/php/src/naming.php +84 -0
- package/php/tests/test_render.php +159 -0
- package/src/__tests__/blade-adapter-unit.test.ts +392 -0
- package/src/__tests__/blade-adapter.test.ts +52 -0
- package/src/__tests__/blade-counter.test.ts +63 -0
- package/src/__tests__/blade-query-href.test.ts +113 -0
- package/src/__tests__/blade-spread-attrs.test.ts +235 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/blade-adapter.ts +1912 -0
- package/src/adapter/boolean-result.ts +168 -0
- package/src/adapter/emit-context.ts +117 -0
- package/src/adapter/expr/array-method.ts +345 -0
- package/src/adapter/expr/emitters.ts +636 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/blade-naming.ts +180 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +83 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +135 -0
- package/src/adapter/props/prop-classes.ts +66 -0
- package/src/adapter/spread/spread-codegen.ts +179 -0
- package/src/adapter/value/parsed-literal.ts +75 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +86 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +782 -0
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@barefootjs/blade",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Laravel Blade (PHP) adapter for BarefootJS — compiles IR to .blade.php templates and ships the PHP BarefootJS rendering runtime on illuminate/view standalone",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./adapter": {
|
|
14
|
+
"types": "./dist/adapter/index.d.ts",
|
|
15
|
+
"import": "./dist/adapter/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./test-render": {
|
|
18
|
+
"bun": "./src/test-render.ts"
|
|
19
|
+
},
|
|
20
|
+
"./build": {
|
|
21
|
+
"types": "./dist/build.d.ts",
|
|
22
|
+
"import": "./dist/build.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"src",
|
|
28
|
+
"php/src",
|
|
29
|
+
"php/composer.json",
|
|
30
|
+
"php/tests",
|
|
31
|
+
"README.md"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "bun run build:js && bun run build:types",
|
|
35
|
+
"build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
|
|
36
|
+
"build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
|
|
37
|
+
"test": "bun test",
|
|
38
|
+
"clean": "rm -rf dist",
|
|
39
|
+
"prepack": "node ../../scripts/swap-publish-config.mjs pack",
|
|
40
|
+
"postpack": "node ../../scripts/swap-publish-config.mjs unpack"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"blade",
|
|
44
|
+
"laravel",
|
|
45
|
+
"php",
|
|
46
|
+
"barefoot",
|
|
47
|
+
"ssr"
|
|
48
|
+
],
|
|
49
|
+
"author": "kobaken <kentafly88@gmail.com>",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/piconic-ai/barefootjs",
|
|
54
|
+
"directory": "packages/adapter-blade"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@barefootjs/shared": "0.17.1"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"@barefootjs/jsx": ">=0.2.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@barefootjs/adapter-tests": "0.1.0",
|
|
64
|
+
"@barefootjs/jsx": "0.17.1",
|
|
65
|
+
"typescript": "^5.0.0"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "barefootjs/blade",
|
|
3
|
+
"description": "Laravel Blade rendering backend for the BarefootJS PHP runtime (barefootjs/php) -- BladeBackend implements the engine backend contract on illuminate/view standalone (Filesystem + Dispatcher + EngineResolver/BladeCompiler + FileViewFinder + Factory), ported from packages/adapter-twig/php/src/TwigBackend.php (see also the packages/adapter-jinja/python port's JinjaBackend).",
|
|
4
|
+
"type": "library",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"authors": [
|
|
7
|
+
{
|
|
8
|
+
"name": "kobaken",
|
|
9
|
+
"email": "kentafly88@gmail.com"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"require": {
|
|
13
|
+
"php": ">=8.2",
|
|
14
|
+
"illuminate/view": "^12.0",
|
|
15
|
+
"barefootjs/php": "@dev"
|
|
16
|
+
},
|
|
17
|
+
"repositories": [
|
|
18
|
+
{
|
|
19
|
+
"type": "path",
|
|
20
|
+
"url": "../../adapter-php"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"autoload": {
|
|
24
|
+
"psr-4": {
|
|
25
|
+
"Barefoot\\": "src/"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src/naming.php"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"minimum-stability": "stable"
|
|
32
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Barefoot;
|
|
6
|
+
|
|
7
|
+
use Illuminate\Contracts\View\Factory as ViewFactory;
|
|
8
|
+
use Illuminate\Events\Dispatcher;
|
|
9
|
+
use Illuminate\Filesystem\Filesystem;
|
|
10
|
+
use Illuminate\Support\HtmlString;
|
|
11
|
+
use Illuminate\View\Compilers\BladeCompiler;
|
|
12
|
+
use Illuminate\View\Engines\CompilerEngine;
|
|
13
|
+
use Illuminate\View\Engines\EngineResolver;
|
|
14
|
+
use Illuminate\View\Engines\PhpEngine;
|
|
15
|
+
use Illuminate\View\Factory;
|
|
16
|
+
use Illuminate\View\FileViewFinder;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Laravel Blade rendering backend for the `BarefootJS` runtime -- port of
|
|
20
|
+
* packages/adapter-twig/php/src/TwigBackend.php (itself a port of
|
|
21
|
+
* packages/adapter-jinja/python/barefootjs/backend_jinja.py's `JinjaBackend`).
|
|
22
|
+
*
|
|
23
|
+
* The engine-agnostic runtime logic (JS-compat value helpers, array/string
|
|
24
|
+
* methods, hydration markers, child rendering) lives in `BarefootJS`
|
|
25
|
+
* (`packages/adapter-php`). This backend supplies the five engine-specific
|
|
26
|
+
* operations the runtime delegates to, targeting Blade syntax, wired onto
|
|
27
|
+
* `illuminate/view` used STANDALONE -- no Laravel application/container is
|
|
28
|
+
* required, only the pieces `Illuminate\View\ViewServiceProvider` wires up
|
|
29
|
+
* (`Filesystem`, an event `Dispatcher`, an `EngineResolver` registering a
|
|
30
|
+
* `blade` engine (`CompilerEngine` over a `BladeCompiler`), a
|
|
31
|
+
* `FileViewFinder`, and the `Factory` that ties them together):
|
|
32
|
+
*
|
|
33
|
+
* encode_json(data) -> JSON string (injectable encoder)
|
|
34
|
+
* mark_raw(str) -> a value Blade emits verbatim (no re-escaping)
|
|
35
|
+
* materialize(value) -> resolve a captured-children value to a string
|
|
36
|
+
* render_named(name, bf, vars) -> render `<name>.blade.php` with `bf` + vars bound
|
|
37
|
+
* ident(name) -> mangle a template-variable name for this engine
|
|
38
|
+
*
|
|
39
|
+
* Pair it with the `@barefootjs/blade` compile-time adapter, which emits
|
|
40
|
+
* `.blade.php` templates that call the runtime as a `$bf` object:
|
|
41
|
+
* `{{ $bf->scope_attr() }}`, `{{ $bf->json($x) }}`, `{{ $bf->spread_attrs($bag) }}`.
|
|
42
|
+
*
|
|
43
|
+
* Escaping note (empirically verified against illuminate/view 12.x, see the
|
|
44
|
+
* adapter design doc). Blade's plain `{{ $x }}` echo compiles to
|
|
45
|
+
* `<?php echo e($x); ?>`. `Illuminate\Support\e()` special-cases any value
|
|
46
|
+
* implementing `Illuminate\Contracts\Support\Htmlable` -- it calls
|
|
47
|
+
* `->toHtml()` and returns it VERBATIM, skipping `htmlspecialchars()`
|
|
48
|
+
* entirely; every other value is escaped via `htmlspecialchars($value,
|
|
49
|
+
* ENT_QUOTES, ..., $doubleEncode = true)`. `mark_raw` below returns an
|
|
50
|
+
* `Illuminate\Support\HtmlString`, which implements `Htmlable` -- so a
|
|
51
|
+
* value this backend has marked raw (e.g. `BarefootJS::spread_attrs`'s
|
|
52
|
+
* return value) passes through a plain `{{ }}` unescaped for free, with no
|
|
53
|
+
* `{!! !!}` needed at THAT call site; call sites that receive an ordinary
|
|
54
|
+
* PHP string this runtime does NOT wrap (e.g. `hydration_attrs()`,
|
|
55
|
+
* `text_start()`/`text_end()`, `comment()`, `scope_comment()`,
|
|
56
|
+
* `render_child()`'s return value) must use the compiler's `{!! ... !!}`
|
|
57
|
+
* raw-echo form instead (mirrors Twig's explicit `| raw` filter at those
|
|
58
|
+
* same call sites). `ENT_QUOTES` emits NAMED entity forms
|
|
59
|
+
* (`"`/`'`), same byte-form difference as Twig's default escaper
|
|
60
|
+
* relative to Perl/Go/markupsafe's numeric forms (`"`/`'`) -- the
|
|
61
|
+
* adapter-tests harness's `normalizeHTML` canonicalizes entity forms before
|
|
62
|
+
* comparison, so this is conformance-equivalent, not a special case. The
|
|
63
|
+
* runtime's OWN escape paths that bypass autoescaping via `mark_raw`
|
|
64
|
+
* (`BarefootJS::spread_attrs`, and the inline quote-escaping in
|
|
65
|
+
* `hydration_attrs`/`data_key_attr`) still emit numeric entities directly,
|
|
66
|
+
* ported unchanged from `BarefootJS.pm`'s `_html_escape`.
|
|
67
|
+
*/
|
|
68
|
+
final class BladeBackend
|
|
69
|
+
{
|
|
70
|
+
private ViewFactory $factory;
|
|
71
|
+
|
|
72
|
+
/** @var callable(mixed): string */
|
|
73
|
+
private $jsonEncoder;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Options-shaped constructor, mirroring `TwigBackend`'s
|
|
77
|
+
* `(paths: [...], cache_dir: ..., json_encoder: ...)` assoc-array
|
|
78
|
+
* calling convention (PHP has no keyword arguments for arrays, so one
|
|
79
|
+
* options bag is the canonical shape across every backend in this
|
|
80
|
+
* repo).
|
|
81
|
+
*
|
|
82
|
+
* @param array{
|
|
83
|
+
* paths?: list<string>,
|
|
84
|
+
* cache_dir?: string|null,
|
|
85
|
+
* json_encoder?: callable|null,
|
|
86
|
+
* factory?: ViewFactory|null,
|
|
87
|
+
* } $options `paths`: template directories (`FileViewFinder`);
|
|
88
|
+
* `cache_dir`: directory for Blade's compiled-template cache (defaults
|
|
89
|
+
* to a subdirectory of `sys_get_temp_dir()`, created if missing);
|
|
90
|
+
* `json_encoder`: overrides the default canonical (sorted-key)
|
|
91
|
+
* encoder; `factory`: a pre-built `Illuminate\Contracts\View\Factory`
|
|
92
|
+
* (when given, `paths`/`cache_dir` are ignored and the caller owns
|
|
93
|
+
* wiring the `blade` engine — used by integrations that already run a
|
|
94
|
+
* full Laravel application and want to reuse its view Factory).
|
|
95
|
+
*/
|
|
96
|
+
public function __construct(array $options = [])
|
|
97
|
+
{
|
|
98
|
+
$this->jsonEncoder = $options['json_encoder'] ?? [self::class, 'defaultJsonEncoder'];
|
|
99
|
+
|
|
100
|
+
if (isset($options['factory'])) {
|
|
101
|
+
$this->factory = $options['factory'];
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
$paths = $options['paths'] ?? [];
|
|
106
|
+
$cacheDir = $options['cache_dir'] ?? (sys_get_temp_dir() . '/barefootjs-blade-cache');
|
|
107
|
+
if (!is_dir($cacheDir)) {
|
|
108
|
+
mkdir($cacheDir, 0777, true);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
$files = new Filesystem();
|
|
112
|
+
$compiler = new BladeCompiler($files, $cacheDir);
|
|
113
|
+
$resolver = new EngineResolver();
|
|
114
|
+
$resolver->register('blade', static fn () => new CompilerEngine($compiler, $files));
|
|
115
|
+
$resolver->register('php', static fn () => new PhpEngine($files));
|
|
116
|
+
|
|
117
|
+
$finder = new FileViewFinder($files, $paths);
|
|
118
|
+
$events = new Dispatcher();
|
|
119
|
+
|
|
120
|
+
$this->factory = new Factory($resolver, $finder, $events);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public function factory(): ViewFactory
|
|
124
|
+
{
|
|
125
|
+
return $this->factory;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Thin delegation to the shared canonical encoder (`packages/adapter-php`'s
|
|
130
|
+
* `Barefoot\Json::canonicalEncode`) -- kept as a static method on this
|
|
131
|
+
* class (rather than removed outright) because tests/integrations
|
|
132
|
+
* reference `BladeBackend::defaultJsonEncoder` directly.
|
|
133
|
+
*/
|
|
134
|
+
public static function defaultJsonEncoder($data): string
|
|
135
|
+
{
|
|
136
|
+
return Json::canonicalEncode($data);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public function encode_json($data): string
|
|
140
|
+
{
|
|
141
|
+
return ($this->jsonEncoder)($data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Mark a string as already-safe so a plain `{{ }}` echo emits it
|
|
145
|
+
* verbatim (no auto-escape) -- see the file header for how
|
|
146
|
+
* `Illuminate\Support\e()`'s `Htmlable` fast path makes this work
|
|
147
|
+
* without a `{!! !!}` at the call site. */
|
|
148
|
+
public function mark_raw($s): HtmlString
|
|
149
|
+
{
|
|
150
|
+
return new HtmlString($s === null ? '' : (string) $s);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** JSX children captured by the adapter resolve to a string (or an
|
|
154
|
+
* `HtmlString`) here -- the ONE uniform children/fallback capture
|
|
155
|
+
* mechanism (`@php(ob_start())` ... `@php($NAME = $bf->backend->mark_raw(ob_get_clean()))`,
|
|
156
|
+
* see `blade-adapter.ts`'s file header) already produces a rendered
|
|
157
|
+
* value directly, but `materialize` still supports a callable for
|
|
158
|
+
* parity with the Perl/Python/Twig ports' contract and any lazy-render
|
|
159
|
+
* composition built on top of this backend. */
|
|
160
|
+
public function materialize($value)
|
|
161
|
+
{
|
|
162
|
+
return is_callable($value) ? $value() : $value;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Render `<name>.blade.php` with `$childBf` bound as the `bf` variable
|
|
167
|
+
* for the nested render, plus the supplied template vars. Reserved-word
|
|
168
|
+
* mangling (`blade_ident`) is applied here -- the ONE point every props
|
|
169
|
+
* value is turned into template variables -- so a prop literally named
|
|
170
|
+
* e.g. `bf` or `this` doesn't collide with the render-time PHP scope
|
|
171
|
+
* (see `naming.php`'s docstring for the full rationale and reserved set).
|
|
172
|
+
*/
|
|
173
|
+
public function render_named(string $name, $childBf, $variables): string
|
|
174
|
+
{
|
|
175
|
+
$varsArr = $variables instanceof \stdClass ? get_object_vars($variables) : (is_array($variables) ? $variables : []);
|
|
176
|
+
$mangled = [];
|
|
177
|
+
foreach ($varsArr as $k => $v) {
|
|
178
|
+
$mangled[blade_ident((string) $k)] = $v;
|
|
179
|
+
}
|
|
180
|
+
$mangled['bf'] = $childBf;
|
|
181
|
+
return $this->factory->make($name, $mangled)->render();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Mangle a template-variable name for Blade -- delegates to
|
|
186
|
+
* `blade_ident` (`naming.php`, engine-specific, frozen reserved-word
|
|
187
|
+
* set). Called by `BarefootJS::render_child` (the runtime,
|
|
188
|
+
* `packages/adapter-php`) so the ONE mangling point for props turned
|
|
189
|
+
* into `render_child` template variables stays engine-pluggable rather
|
|
190
|
+
* than hard-coding Blade's reserved-word set into the engine-agnostic
|
|
191
|
+
* runtime.
|
|
192
|
+
*/
|
|
193
|
+
public function ident(string $name): string
|
|
194
|
+
{
|
|
195
|
+
return blade_ident($name);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Barefoot;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reserved-word mangling for Blade template variable names -- SIBLING of
|
|
9
|
+
* `packages/adapter-twig/php/src/naming.php` (`twig_ident`/`TWIG_RESERVED_WORDS`)
|
|
10
|
+
* but with a COMPLETELY DIFFERENT reserved-word set, because the underlying
|
|
11
|
+
* mechanism is different: Twig resolves a bare `name` through its own
|
|
12
|
+
* expression-language variable lookup, so Twig KEYWORDS (`for`, `filter`,
|
|
13
|
+
* `if`, `in`, ...) collide with template syntax and must be mangled. Blade
|
|
14
|
+
* compiles a template straight to a plain PHP file and binds template
|
|
15
|
+
* variables as REAL PHP variables via `extract()` (illuminate/view's
|
|
16
|
+
* `PhpEngine`/`CompilerEngine`) -- so Twig's keyword list is IRRELEVANT here
|
|
17
|
+
* (`$for`, `$filter`, `$if` are all perfectly legal PHP variable names).
|
|
18
|
+
*
|
|
19
|
+
* What DOES collide for Blade is anything the render-time PHP SCOPE already
|
|
20
|
+
* binds to a name other than the incoming prop, or any name PHP itself
|
|
21
|
+
* forbids as a variable:
|
|
22
|
+
*
|
|
23
|
+
* - `bf` -- the runtime binding this adapter's every `bf.method(...)`
|
|
24
|
+
* call compiles to `$bf->method(...)`; a prop named `bf`
|
|
25
|
+
* would silently overwrite the runtime handle.
|
|
26
|
+
* - `this` -- PHP does not allow assigning to `$this` at all outside an
|
|
27
|
+
* object context (`Cannot re-assign $this` fatal error).
|
|
28
|
+
* - `__env` -- illuminate/view's `Factory` binds `$__env` in every
|
|
29
|
+
* compiled view's scope (used by `@section`/`@yield`/etc).
|
|
30
|
+
* - `__data` -- `PhpEngine::evaluatePath()` / `Illuminate\View\View` use
|
|
31
|
+
* `$__data` as the internal name for the variable bag before
|
|
32
|
+
* `extract()`; a same-named prop would collide with the
|
|
33
|
+
* extraction mechanism itself.
|
|
34
|
+
* - `__path` -- same layer, `PhpEngine::evaluatePath()` binds `$__path` to
|
|
35
|
+
* the compiled-template file path being included.
|
|
36
|
+
* - `app` -- Blade/illuminate conventionally expose the container as
|
|
37
|
+
* `$app` in many integrations' base view scope; kept reserved
|
|
38
|
+
* defensively even though this adapter's standalone
|
|
39
|
+
* `Factory` (no framework container) does not bind it itself.
|
|
40
|
+
* - `loop` -- Blade's `@foreach` directive injects a `$loop` variable
|
|
41
|
+
* (iteration metadata) into the loop body scope; this
|
|
42
|
+
* adapter does not itself rely on Blade's `$loop`, but a
|
|
43
|
+
* same-named prop landing in an ancestor scope could still
|
|
44
|
+
* be shadowed by a NESTED `@foreach`'s own `$loop` -- reserved
|
|
45
|
+
* to avoid ANY ambiguity.
|
|
46
|
+
*
|
|
47
|
+
* `blade_ident(name)` is the single mangling point: every props dict handed
|
|
48
|
+
* to a Blade template as top-level variables (`BladeBackend::render_named`,
|
|
49
|
+
* `BarefootJS::render_child`'s prop passing) is mangled through this
|
|
50
|
+
* function first, so a prop with one of the names above doesn't collide with
|
|
51
|
+
* the Blade/illuminate render-time scope. The mangling MUST match, byte for
|
|
52
|
+
* byte, `packages/adapter-blade/src/adapter/lib/blade-naming.ts`'s
|
|
53
|
+
* `BLADE_RESERVED_WORDS` list and `bladeIdent()` -- the TS side additionally
|
|
54
|
+
* runs a parity test against this file's list.
|
|
55
|
+
*
|
|
56
|
+
* This file is registered as a composer "files" autoload entry (loaded
|
|
57
|
+
* unconditionally on every request via a plain `require`, not
|
|
58
|
+
* `require_once`) AND is `require_once`'d directly by
|
|
59
|
+
* `php/tests/_harness.php::bf_require_runtime()` for the Blade-independent
|
|
60
|
+
* test files. `define()`/`function_exists()` guards (rather than a bare
|
|
61
|
+
* top-level `const`/`function`, which cannot appear inside a conditional in
|
|
62
|
+
* PHP) make loading this file twice -- regardless of which mechanism gets
|
|
63
|
+
* there first -- a safe no-op instead of a "Cannot redeclare" fatal error.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
if (!defined(__NAMESPACE__ . '\\BLADE_RESERVED_WORDS')) {
|
|
67
|
+
/** @var list<string> BLADE_RESERVED_WORDS */
|
|
68
|
+
define(__NAMESPACE__ . '\\BLADE_RESERVED_WORDS', [
|
|
69
|
+
'bf', 'this', '__env', '__data', '__path', 'app', 'loop',
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!function_exists(__NAMESPACE__ . '\\blade_ident')) {
|
|
74
|
+
/**
|
|
75
|
+
* Mangle a JS identifier (prop name, signal getter, loop param, ...)
|
|
76
|
+
* into a Blade-safe (i.e. render-time-PHP-scope-safe) variable name:
|
|
77
|
+
* reserved words get a trailing `_` suffix, everything else passes
|
|
78
|
+
* through unchanged.
|
|
79
|
+
*/
|
|
80
|
+
function blade_ident(string $name): string
|
|
81
|
+
{
|
|
82
|
+
return in_array($name, BLADE_RESERVED_WORDS, true) ? $name . '_' : $name;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Real end-to-end Blade render, modeled on packages/adapter-xslate/t/render.t
|
|
7
|
+
* and the Python port's test_render.py.
|
|
8
|
+
*
|
|
9
|
+
* Writes a `.blade.php` template equivalent to what the `@barefootjs/blade`
|
|
10
|
+
* compile-time adapter emits, then renders it through the runtime +
|
|
11
|
+
* BladeBackend. Exercises scope markers, hydration attrs, text slots via
|
|
12
|
+
* HTML comment markers, autoescaping, `spread_attrs`, and (being the one
|
|
13
|
+
* place a REAL BladeBackend is available) Blade-specific reserved-word
|
|
14
|
+
* mangling end-to-end.
|
|
15
|
+
*
|
|
16
|
+
* Needs `illuminate/view` installed (`composer install`); skips gracefully
|
|
17
|
+
* with a notice (not a failure) when `vendor/autoload.php` is absent, per
|
|
18
|
+
* the design doc.
|
|
19
|
+
*
|
|
20
|
+
* This is the only test file left in this package (the engine-agnostic
|
|
21
|
+
* runtime's test suite moved to `packages/adapter-php/tests`, see #2100) --
|
|
22
|
+
* it requires that package's `_harness.php` directly via a relative path
|
|
23
|
+
* rather than keeping a local copy or a single-file `run.php` here, and
|
|
24
|
+
* still runs standalone via `php test_render.php` (`bf_finish()` prints +
|
|
25
|
+
* `exit()`s when `BF_RUNNER` is undefined, which it is here).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
require_once __DIR__ . '/../../../adapter-php/tests/_harness.php';
|
|
29
|
+
bf_require_runtime();
|
|
30
|
+
bf_reset();
|
|
31
|
+
|
|
32
|
+
$vendorAutoload = __DIR__ . '/../vendor/autoload.php';
|
|
33
|
+
if (!is_file($vendorAutoload)) {
|
|
34
|
+
bf_skip_file('illuminate/view not installed -- run `composer install --working-dir=' . dirname(__DIR__) . '` to enable this test');
|
|
35
|
+
return bf_finish();
|
|
36
|
+
}
|
|
37
|
+
require_once $vendorAutoload;
|
|
38
|
+
|
|
39
|
+
use Barefoot\BarefootJS;
|
|
40
|
+
use Barefoot\BladeBackend;
|
|
41
|
+
|
|
42
|
+
$tmpDir = sys_get_temp_dir() . '/barefoot-blade-render-test-' . bin2hex(random_bytes(6));
|
|
43
|
+
mkdir($tmpDir, 0777, true);
|
|
44
|
+
register_shutdown_function(function () use ($tmpDir) {
|
|
45
|
+
foreach (glob($tmpDir . '/*') ?: [] as $f) {
|
|
46
|
+
@unlink($f);
|
|
47
|
+
}
|
|
48
|
+
@rmdir($tmpDir);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Blade idiom note: helpers that return already-safe HTML syntax but are NOT
|
|
52
|
+
// wrapped by the backend's `mark_raw` (`scope_attr`, `hydration_attrs`,
|
|
53
|
+
// `text_start`, `text_end`, `comment`, `scope_comment`) need the explicit
|
|
54
|
+
// `{!! ... !!}` raw-echo form at the call site -- the Blade-syntax
|
|
55
|
+
// equivalent of Twig's `| raw` filter / Kolon's `| mark_raw` pipe.
|
|
56
|
+
// `spread_attrs` already returns an `Illuminate\Support\HtmlString` value
|
|
57
|
+
// (via `backend.mark_raw`), which `Illuminate\Support\e()` passes through
|
|
58
|
+
// unescaped when handed to plain `{{ }}` (the `Htmlable` fast path), so it
|
|
59
|
+
// needs no `{!! !!}`.
|
|
60
|
+
$widgetTemplate = '<div bf-s="{{ $bf->scope_attr() }}" {!! $bf->hydration_attrs() !!}>'
|
|
61
|
+
. "count: {!! \$bf->text_start('s0') !!}{{ \$count }}{!! \$bf->text_end() !!} "
|
|
62
|
+
. '<span {{ $bf->spread_attrs($attrs) }}>{{ $label }}</span>'
|
|
63
|
+
. '</div>';
|
|
64
|
+
|
|
65
|
+
file_put_contents($tmpDir . '/widget.blade.php', $widgetTemplate);
|
|
66
|
+
|
|
67
|
+
$backend = new BladeBackend(['paths' => [$tmpDir]]);
|
|
68
|
+
$bf = new BarefootJS(null, ['backend' => $backend]);
|
|
69
|
+
$bf->_scope_id('Widget_test');
|
|
70
|
+
|
|
71
|
+
$vars = ['count' => 7, 'label' => '<x>', 'attrs' => ['id' => 'n', 'class' => 'c']];
|
|
72
|
+
|
|
73
|
+
bf_test('scope and hydration markers', function () use ($backend, $bf, $vars) {
|
|
74
|
+
$out = $backend->render_named('widget', $bf, $vars);
|
|
75
|
+
bf_assert(str_contains($out, 'bf-s="Widget_test" bf-r=""'), "got: {$out}");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
bf_test('reactive text slot with comment markers', function () use ($backend, $bf, $vars) {
|
|
79
|
+
$out = $backend->render_named('widget', $bf, $vars);
|
|
80
|
+
bf_assert(str_contains($out, 'count: <!--bf:s0-->7<!--/-->'), "got: {$out}");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
bf_test('plain interpolation is autoescaped', function () use ($backend, $bf, $vars) {
|
|
84
|
+
$out = $backend->render_named('widget', $bf, $vars);
|
|
85
|
+
bf_assert(str_contains($out, '<x>'), "got: {$out}");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
bf_test('spread_attrs renders raw with sorted keys', function () use ($backend, $bf, $vars) {
|
|
89
|
+
$out = $backend->render_named('widget', $bf, $vars);
|
|
90
|
+
bf_assert(str_contains($out, '<span class="c" id="n">'), "got: {$out}");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
bf_test('backend unit operations', function () use ($backend) {
|
|
94
|
+
bf_assert_eq($backend->materialize('plain'), 'plain');
|
|
95
|
+
bf_assert_eq($backend->materialize(fn () => 'lazy'), 'lazy');
|
|
96
|
+
$encoded = $backend->encode_json(['b' => 2, 'a' => 1]);
|
|
97
|
+
bf_assert_eq($encoded, '{"a":1,"b":2}'); // canonical (sorted) key order
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
bf_test('render_named mangles reserved-word props', function () use ($backend, $bf, $tmpDir) {
|
|
101
|
+
file_put_contents($tmpDir . '/kw.blade.php', '{{ $bf_ }}-{{ $this_ }}');
|
|
102
|
+
$out = $backend->render_named('kw', $bf, ['bf' => 'X', 'this' => 'Y']);
|
|
103
|
+
bf_assert_eq($out, 'X-Y');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
bf_test('render_child end-to-end (parent -> renderer -> render_named)', function () use ($backend, $bf, $tmpDir) {
|
|
107
|
+
file_put_contents($tmpDir . '/parent.blade.php', "parent:{!! \$bf->render_child('child', 'bf', 'c1', 'label', 'hi') !!}");
|
|
108
|
+
file_put_contents($tmpDir . '/child.blade.php', '[{{ $bf_ }}:{{ $label }}]');
|
|
109
|
+
|
|
110
|
+
$childRenderer = function ($props, $caller) use ($backend) {
|
|
111
|
+
$childBf = new BarefootJS(null, ['backend' => $backend]);
|
|
112
|
+
return $backend->render_named('child', $childBf, $props);
|
|
113
|
+
};
|
|
114
|
+
$bf->register_child_renderer('child', $childRenderer);
|
|
115
|
+
$out = $backend->render_named('parent', $bf, []);
|
|
116
|
+
bf_assert_eq($out, 'parent:[c1:hi]');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
bf_test('render_child mangles reserved-word props via backend->ident (Blade-specific)', function () use ($backend, $bf) {
|
|
120
|
+
// `BarefootJS::render_child` (packages/adapter-php) delegates key
|
|
121
|
+
// mangling to the backend's `ident()` rather than hard-coding Blade's
|
|
122
|
+
// reserved-word set. This probes that delegation directly (a renderer
|
|
123
|
+
// that reads its raw `$props`, not routed back through
|
|
124
|
+
// `render_named`'s OWN mangling pass) so the contract is verified in
|
|
125
|
+
// isolation, not just as an accidental side effect of `render_named`
|
|
126
|
+
// mangling everything a second time. Uses `bf` and `this`, the
|
|
127
|
+
// Blade-specific reserved words (naming.php's BLADE_RESERVED_WORDS,
|
|
128
|
+
// frozen per the design doc) -- Blade templates bind raw PHP variables
|
|
129
|
+
// via `extract()`, so Twig/Jinja keywords like `for`/`class` are NOT
|
|
130
|
+
// reserved here; instead the render-time PHP scope itself (`$bf`, and
|
|
131
|
+
// PHP's own `$this` restriction) is what a prop name must avoid.
|
|
132
|
+
$seen = [];
|
|
133
|
+
$bf->register_child_renderer('probe', function ($props, $caller) use (&$seen) {
|
|
134
|
+
$seen['props'] = $props;
|
|
135
|
+
return 'ok';
|
|
136
|
+
});
|
|
137
|
+
$bf->render_child('probe', 'bf', 'x', 'this', 'y');
|
|
138
|
+
bf_assert_eq($seen['props']['bf_'], 'x');
|
|
139
|
+
bf_assert_eq($seen['props']['this_'], 'y');
|
|
140
|
+
bf_assert(!array_key_exists('bf', $seen['props']), 'expected the raw "bf" key to be gone after mangling');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Custom-escaper sanity check (design doc §2): plain `{{ }}` interpolation
|
|
144
|
+
// uses Blade's OWN default `e()` escaper (named entities), while the
|
|
145
|
+
// runtime's mark_raw-wrapped helpers (spread_attrs) use numeric entities.
|
|
146
|
+
// Both are exercised here on a `"` and a `'`.
|
|
147
|
+
bf_test('autoescape vs. spread_attrs escaping on quote characters', function () use ($tmpDir) {
|
|
148
|
+
file_put_contents($tmpDir . '/quotes.blade.php', '{{ $raw }} | <span {{ $bf->spread_attrs($attrs) }}></span>');
|
|
149
|
+
$backend2 = new BladeBackend(['paths' => [$tmpDir]]);
|
|
150
|
+
$bf2 = new BarefootJS(null, ['backend' => $backend2]);
|
|
151
|
+
$out = $backend2->render_named('quotes', $bf2, ['raw' => "he said \"hi\" & 'bye'", 'attrs' => ['title' => "quote\" and 'apos'"]]);
|
|
152
|
+
// Blade's default `e()` escaper: "/' (htmlspecialchars,
|
|
153
|
+
// ENT_QUOTES); the runtime's own spread_attrs escaper: "/'
|
|
154
|
+
// (numeric, both sides).
|
|
155
|
+
bf_assert(str_contains($out, '&'), "expected an escaped '&' in: {$out}");
|
|
156
|
+
bf_assert(str_contains($out, 'title="quote" and 'apos'"'), "expected numeric-entity escaping from spread_attrs in: {$out}");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return bf_finish();
|