@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.
Files changed (72) hide show
  1. package/README.md +73 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/blade-adapter.d.ts +537 -0
  5. package/dist/adapter/blade-adapter.d.ts.map +1 -0
  6. package/dist/adapter/boolean-result.d.ts +76 -0
  7. package/dist/adapter/boolean-result.d.ts.map +1 -0
  8. package/dist/adapter/emit-context.d.ts +105 -0
  9. package/dist/adapter/emit-context.d.ts.map +1 -0
  10. package/dist/adapter/expr/array-method.d.ts +74 -0
  11. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  12. package/dist/adapter/expr/emitters.d.ts +176 -0
  13. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  14. package/dist/adapter/expr/operand.d.ts +25 -0
  15. package/dist/adapter/expr/operand.d.ts.map +1 -0
  16. package/dist/adapter/index.d.ts +6 -0
  17. package/dist/adapter/index.d.ts.map +1 -0
  18. package/dist/adapter/index.js +189060 -0
  19. package/dist/adapter/lib/blade-naming.d.ts +128 -0
  20. package/dist/adapter/lib/blade-naming.d.ts.map +1 -0
  21. package/dist/adapter/lib/constants.d.ts +21 -0
  22. package/dist/adapter/lib/constants.d.ts.map +1 -0
  23. package/dist/adapter/lib/ir-scope.d.ts +48 -0
  24. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  25. package/dist/adapter/lib/types.d.ts +27 -0
  26. package/dist/adapter/lib/types.d.ts.map +1 -0
  27. package/dist/adapter/memo/seed.d.ts +84 -0
  28. package/dist/adapter/memo/seed.d.ts.map +1 -0
  29. package/dist/adapter/props/prop-classes.d.ts +34 -0
  30. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  31. package/dist/adapter/spread/spread-codegen.d.ts +72 -0
  32. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  33. package/dist/adapter/value/parsed-literal.d.ts +27 -0
  34. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  35. package/dist/build.d.ts +28 -0
  36. package/dist/build.d.ts.map +1 -0
  37. package/dist/build.js +189080 -0
  38. package/dist/conformance-pins.d.ts +12 -0
  39. package/dist/conformance-pins.d.ts.map +1 -0
  40. package/dist/index.d.ts +9 -0
  41. package/dist/index.d.ts.map +1 -0
  42. package/dist/index.js +189079 -0
  43. package/package.json +67 -0
  44. package/php/composer.json +32 -0
  45. package/php/src/BladeBackend.php +197 -0
  46. package/php/src/naming.php +84 -0
  47. package/php/tests/test_render.php +159 -0
  48. package/src/__tests__/blade-adapter-unit.test.ts +392 -0
  49. package/src/__tests__/blade-adapter.test.ts +52 -0
  50. package/src/__tests__/blade-counter.test.ts +63 -0
  51. package/src/__tests__/blade-query-href.test.ts +113 -0
  52. package/src/__tests__/blade-spread-attrs.test.ts +235 -0
  53. package/src/adapter/analysis/component-tree.ts +119 -0
  54. package/src/adapter/blade-adapter.ts +1912 -0
  55. package/src/adapter/boolean-result.ts +168 -0
  56. package/src/adapter/emit-context.ts +117 -0
  57. package/src/adapter/expr/array-method.ts +345 -0
  58. package/src/adapter/expr/emitters.ts +636 -0
  59. package/src/adapter/expr/operand.ts +35 -0
  60. package/src/adapter/index.ts +6 -0
  61. package/src/adapter/lib/blade-naming.ts +180 -0
  62. package/src/adapter/lib/constants.ts +33 -0
  63. package/src/adapter/lib/ir-scope.ts +83 -0
  64. package/src/adapter/lib/types.ts +30 -0
  65. package/src/adapter/memo/seed.ts +135 -0
  66. package/src/adapter/props/prop-classes.ts +66 -0
  67. package/src/adapter/spread/spread-codegen.ts +179 -0
  68. package/src/adapter/value/parsed-literal.ts +75 -0
  69. package/src/build.ts +37 -0
  70. package/src/conformance-pins.ts +86 -0
  71. package/src/index.ts +9 -0
  72. 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
+ * (`&quot;`/`&#039;`), same byte-form difference as Twig's default escaper
60
+ * relative to Perl/Go/markupsafe's numeric forms (`&#34;`/`&#39;`) -- 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, '&lt;x&gt;'), "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: &quot;/&#039; (htmlspecialchars,
153
+ // ENT_QUOTES); the runtime's own spread_attrs escaper: &#34;/&#39;
154
+ // (numeric, both sides).
155
+ bf_assert(str_contains($out, '&amp;'), "expected an escaped '&' in: {$out}");
156
+ bf_assert(str_contains($out, 'title="quote&#34; and &#39;apos&#39;"'), "expected numeric-entity escaping from spread_attrs in: {$out}");
157
+ });
158
+
159
+ return bf_finish();