@barefootjs/php 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.
@@ -0,0 +1,63 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * `BarefootJS::props_attr` -- the `bf-p` hydration-payload attribute.
7
+ *
8
+ * The encoded JSON is embedded in a SINGLE-quoted attribute, so it must be
9
+ * attribute-escaped: a raw `'` inside a string value (e.g. a blog paragraph)
10
+ * terminates the attribute early and the client hydrates from truncated JSON
11
+ * (empty island text; found via the shared blog-ssr e2e). Same fix across
12
+ * the Perl, Python, Ruby, Rust, and PHP runtimes (#2086) -- keep the five
13
+ * tests in sync.
14
+ */
15
+
16
+ require_once __DIR__ . '/_harness.php';
17
+ bf_require_runtime();
18
+ bf_reset();
19
+
20
+ use Barefoot\BarefootJS;
21
+ use Barefoot\Json;
22
+
23
+ $backend = new class {
24
+ public function encode_json($value): string
25
+ {
26
+ return Json::canonicalEncode($value);
27
+ }
28
+ };
29
+
30
+ function bfp_with($props): BarefootJS
31
+ {
32
+ global $backend;
33
+ $bf = new BarefootJS(null, ['backend' => $backend]);
34
+ if ($props !== null) {
35
+ $bf->_props($props);
36
+ }
37
+ return $bf;
38
+ }
39
+
40
+ bf_test('empty props emit nothing', function () {
41
+ bf_assert_eq(bfp_with(null)->props_attr(), '');
42
+ bf_assert_eq(bfp_with([])->props_attr(), '');
43
+ });
44
+
45
+ bf_test('json is attribute-escaped', function () {
46
+ $attr = bfp_with(['note' => "it's <b> & co"])->props_attr();
47
+ bf_assert_eq($attr, " bf-p='{&#34;note&#34;:&#34;it&#39;s &lt;b&gt; &amp; co&#34;}'");
48
+ });
49
+
50
+ bf_test('attribute round-trips through entity decoding', function () {
51
+ $attr = bfp_with(['note' => "it's <b> & co"])->props_attr();
52
+ if (!preg_match("/bf-p='([^']*)'/", $attr, $m)) {
53
+ throw new RuntimeException('bf-p attribute not found in: ' . $attr);
54
+ }
55
+ $decoded = str_replace(
56
+ ['&#34;', '&#39;', '&lt;', '&gt;', '&amp;'],
57
+ ['"', "'", '<', '>', '&'],
58
+ $m[1]
59
+ );
60
+ bf_assert_eq(json_decode($decoded, true), ['note' => "it's <b> & co"]);
61
+ });
62
+
63
+ return bf_finish();
@@ -0,0 +1,47 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * `BarefootJS::query` -- ported from packages/adapter-perl/t/query.t (see
7
+ * also the Python port's test_query.py).
8
+ *
9
+ * The full CROSS-BACKEND behaviour (control flow + form-encoding parity
10
+ * with the browser's URLSearchParams) is defined ONCE in the shared golden
11
+ * helper vectors and run by test_helper_vectors.php. This file keeps a few
12
+ * representative cases for always-on coverage plus the PHP-runtime-SPECIFIC
13
+ * defensive behaviour the golden vectors can't express: a `null` value
14
+ * (JSON has no `undefined`; this runtime coerces `null` to '' and omits the
15
+ * empty pair, mirroring the Perl/Python ports' documented `undef`/`None`
16
+ * handling).
17
+ */
18
+
19
+ require_once __DIR__ . '/_harness.php';
20
+ bf_require_runtime();
21
+ bf_reset();
22
+
23
+ use Barefoot\BarefootJS;
24
+
25
+ $bf = new BarefootJS(null, ['backend' => null]);
26
+
27
+ bf_test('order preserved; repeated key overwrites at first position', function () use ($bf) {
28
+ bf_assert_eq(
29
+ $bf->query('/blog', true, 'sort', 'title', true, 'tag', 'go', true, 'sort', 'date'),
30
+ '/blog?sort=date&tag=go'
31
+ );
32
+ });
33
+
34
+ bf_test('form encoding: tilde, star, space', function () use ($bf) {
35
+ bf_assert_eq($bf->query('/s', true, 't', 'a~b *c'), '/s?t=a%7Eb+*c');
36
+ });
37
+
38
+ bf_test('array value appends one pair per non-empty member', function () use ($bf) {
39
+ bf_assert_eq($bf->query('/list', true, 'tag', ['a', '', 'b']), '/list?tag=a&tag=b');
40
+ });
41
+
42
+ bf_test('null value coerces to empty and is omitted', function () use ($bf) {
43
+ bf_assert_eq($bf->query('/list', true, 'tag', null), '/list');
44
+ bf_assert_eq($bf->query('/list', true, 'tag', null, true, 'keep', 'me'), '/list?keep=me');
45
+ });
46
+
47
+ return bf_finish();
@@ -0,0 +1,123 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * `render_child` renderer-invocation contract, ported from
7
+ * packages/adapter-perl/t/render_child.t (see also the Python port's
8
+ * test_render_child.py).
9
+ *
10
+ * Renderer contract (#1897): the renderer is invoked with `(props,
11
+ * invoking_bf)` so nested renders can chain scope/slot identity off the
12
+ * caller, not the registrant. Uses a StubBackend (no engine dependency).
13
+ */
14
+
15
+ require_once __DIR__ . '/_harness.php';
16
+ bf_require_runtime();
17
+ bf_reset();
18
+
19
+ use Barefoot\BarefootJS;
20
+
21
+ $stubBackend = new class {
22
+ public function materialize($value)
23
+ {
24
+ return is_callable($value) ? $value() : $value;
25
+ }
26
+
27
+ /** Identity -- this stub is engine-agnostic (no reserved-word set of its
28
+ * own). The Twig-specific reserved-word mangling behaviour (`for` ->
29
+ * `for_`) is exercised end-to-end via the real `TwigBackend` in
30
+ * `packages/adapter-twig/php/tests/test_render.php`. */
31
+ public function ident(string $name): string
32
+ {
33
+ return $name;
34
+ }
35
+ };
36
+
37
+ function bfrc_new_bf(): BarefootJS
38
+ {
39
+ global $stubBackend;
40
+ return new BarefootJS(null, ['backend' => $stubBackend]);
41
+ }
42
+
43
+ bf_test('renderer receives the invoking instance', function () {
44
+ $bf = bfrc_new_bf();
45
+ $bf->_scope_id('Root_test');
46
+
47
+ $seen = [];
48
+ $probe = function ($props, $caller) use (&$seen) {
49
+ $seen['props'] = $props;
50
+ $seen['caller'] = $caller;
51
+ return 'ok';
52
+ };
53
+ $bf->register_child_renderer('probe', $probe);
54
+
55
+ bf_assert_eq($bf->render_child('probe', 'value', 1), 'ok');
56
+ bf_assert_eq($seen['props']['value'], 1);
57
+ bf_assert($seen['caller'] === $bf, 'expected the caller to be the invoking bf');
58
+
59
+ // A nested invocation from a different instance passes THAT instance.
60
+ $child = bfrc_new_bf();
61
+ $child->_scope_id('Root_test_s0');
62
+ $child->_child_renderers($bf->_child_renderers());
63
+ $child->render_child('probe');
64
+ bf_assert($seen['caller'] === $child, 'expected the caller to be the nested child instance');
65
+ });
66
+
67
+ bf_test('renderer exceptions propagate', function () {
68
+ $bf = bfrc_new_bf();
69
+ $bf->register_child_renderer('boom', function ($props, $caller) {
70
+ throw new \RuntimeException('renderer exploded');
71
+ });
72
+
73
+ try {
74
+ $bf->render_child('boom');
75
+ bf_assert(false, 'expected an exception to propagate');
76
+ } catch (\RuntimeException $e) {
77
+ bf_assert_eq($e->getMessage(), 'renderer exploded');
78
+ }
79
+ });
80
+
81
+ bf_test('single-array form', function () {
82
+ // Mirrors the Perl port's hashref form for callers that can't splat a
83
+ // hash into positional/keyword args.
84
+ $bf = bfrc_new_bf();
85
+ $seen = [];
86
+ $bf->register_child_renderer('probe', function ($props, $caller) use (&$seen) {
87
+ $seen['props'] = $props;
88
+ return 'ok';
89
+ });
90
+ $bf->render_child('probe', ['value' => 42]);
91
+ bf_assert_eq($seen['props']['value'], 42);
92
+ });
93
+
94
+ bf_test('missing renderer raises', function () {
95
+ $bf = bfrc_new_bf();
96
+ try {
97
+ $bf->render_child('missing');
98
+ bf_assert(false, 'expected a RuntimeException');
99
+ } catch (\RuntimeException $e) {
100
+ bf_assert(str_contains($e->getMessage(), 'missing'), 'expected the message to name the missing renderer');
101
+ }
102
+ });
103
+
104
+ bf_test('prop keys are routed through backend->ident (identity in this stub)', function () {
105
+ // `render_child` delegates key-mangling to the backend's `ident()` (the
106
+ // fifth backend-contract method) rather than hard-coding an
107
+ // engine-specific reserved-word set here. This stub's `ident()` is the
108
+ // identity function, so keys pass through unchanged -- the Twig-specific
109
+ // mangling behaviour (`for` -> `for_`) is exercised end-to-end via the
110
+ // real `TwigBackend` in
111
+ // packages/adapter-twig/php/tests/test_render.php.
112
+ $bf = bfrc_new_bf();
113
+ $seen = [];
114
+ $bf->register_child_renderer('probe', function ($props, $caller) use (&$seen) {
115
+ $seen['props'] = $props;
116
+ return 'ok';
117
+ });
118
+ $bf->render_child('probe', ['for' => 'x', 'id' => 'y']);
119
+ bf_assert_eq($seen['props']['for'], 'x');
120
+ bf_assert_eq($seen['props']['id'], 'y');
121
+ });
122
+
123
+ return bf_finish();
@@ -0,0 +1,56 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * `BarefootJS::SearchParams` -- PHP-specific concerns, ported from
7
+ * packages/adapter-perl/t/search_params.t (see also the Python port's
8
+ * test_search_params.py).
9
+ *
10
+ * The cross-language VALUE semantics of `get` are owned by the
11
+ * language-independent golden vectors (`search_params_get` in
12
+ * test_helper_vectors.php), so Go/Perl/Python/PHP parity there is
13
+ * mechanical. This file covers only what those value vectors can't: the
14
+ * lazy factory seam, lenient parsing (never raises), and UTF-8 decoding.
15
+ */
16
+
17
+ require_once __DIR__ . '/_harness.php';
18
+ bf_require_runtime();
19
+ bf_reset();
20
+
21
+ use Barefoot\BarefootJS;
22
+ use Barefoot\SearchParams;
23
+
24
+ bf_test('lazy factory', function () {
25
+ $sp = BarefootJS::search_params('sort=price');
26
+ bf_assert($sp instanceof SearchParams, 'expected a SearchParams instance');
27
+ bf_assert_eq($sp->get('sort'), 'price');
28
+ bf_assert(BarefootJS::search_params() instanceof SearchParams, 'expected a SearchParams instance for the default query');
29
+ });
30
+
31
+ bf_test('null-ish composition coalesces only an absent key', function () {
32
+ // The adapter lowers `searchParams().get(k) ?? d` to Twig's native `??`,
33
+ // which coalesces only `null` (not a bare `or`, which would also
34
+ // default a present-but-empty value) -- so an absent key falls back to
35
+ // the author's default while a present-but-empty value keeps ''.
36
+ $absent = BarefootJS::search_params('other=x');
37
+ $got = $absent->get('sort');
38
+ bf_assert_eq($got ?? 'none', 'none');
39
+
40
+ $empty = BarefootJS::search_params('sort=');
41
+ $got = $empty->get('sort');
42
+ bf_assert_eq($got ?? 'none', '');
43
+ });
44
+
45
+ bf_test('UTF-8 percent-decoding', function () {
46
+ $sp = BarefootJS::search_params('q=%E2%9C%93');
47
+ bf_assert_eq($sp->get('q'), "\xE2\x9C\x93"); // U+2713 CHECK MARK
48
+ });
49
+
50
+ bf_test('lenient parsing never raises', function () {
51
+ BarefootJS::search_params(''); // should not raise
52
+ bf_assert_eq(BarefootJS::search_params('&&&')->get('x'), null);
53
+ bf_assert_eq(BarefootJS::search_params('=novalue')->get('x'), null);
54
+ });
55
+
56
+ return bf_finish();
@@ -0,0 +1,98 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * `BarefootJS::spread_attrs` -- ported from
7
+ * packages/adapter-perl/t/spread_attrs.t (see also the Python port's
8
+ * test_spread_attrs.py).
9
+ *
10
+ * JSX intrinsic-element spread runtime helper (#1407 follow-up). Mirrors
11
+ * the JS `spreadAttrs` runtime and the Go/Perl/Python adapters' equivalents
12
+ * so SSR output stays byte-equal across every adapter -- cross-adapter
13
+ * parity regressions surface here first.
14
+ */
15
+
16
+ require_once __DIR__ . '/_harness.php';
17
+ bf_require_runtime();
18
+ bf_reset();
19
+
20
+ use Barefoot\BarefootJS;
21
+
22
+ $backend = new class {
23
+ public function mark_raw($s)
24
+ {
25
+ return $s;
26
+ }
27
+ };
28
+
29
+ function bfs_run($bag): string
30
+ {
31
+ global $backend;
32
+ $bf = new BarefootJS(null, ['backend' => $backend]);
33
+ return (string) $bf->spread_attrs($bag);
34
+ }
35
+
36
+ bf_test('basic shapes', function () {
37
+ bf_assert_eq(bfs_run(null), '');
38
+ bf_assert_eq(bfs_run([]), '');
39
+ bf_assert_eq(bfs_run('not a hash'), '');
40
+ bf_assert_eq(bfs_run(['id' => 'a']), 'id="a"');
41
+ });
42
+
43
+ bf_test('alphabetic key order', function () {
44
+ bf_assert_eq(bfs_run(['id' => 'a', 'class' => 'on']), 'class="on" id="a"');
45
+ });
46
+
47
+ bf_test('key remapping', function () {
48
+ bf_assert_eq(bfs_run(['className' => 'foo']), 'class="foo"');
49
+ bf_assert_eq(bfs_run(['htmlFor' => 'x']), 'for="x"');
50
+ bf_assert_eq(bfs_run(['dataPriority' => 'high']), 'data-priority="high"');
51
+ // SVG XML attrs are case-sensitive -- preserve verbatim.
52
+ bf_assert_eq(bfs_run(['viewBox' => '0 0 10 10']), 'viewBox="0 0 10 10"');
53
+ bf_assert_eq(bfs_run(['clipPathUnits' => 'userSpaceOnUse']), 'clipPathUnits="userSpaceOnUse"');
54
+ // JS-reference parity (#1411): a leading uppercase letter emits a leading dash.
55
+ bf_assert_eq(bfs_run(['XData' => 'x']), '-x-data="x"');
56
+ });
57
+
58
+ bf_test('event handlers -- JS predicate parity', function () {
59
+ bf_assert_eq(bfs_run(['onClick' => 'fn', 'id' => 'a']), 'id="a"');
60
+ bf_assert_eq(bfs_run(['on_custom' => 'fn', 'id' => 'a']), 'id="a"');
61
+ bf_assert_eq(bfs_run(['on0' => 'fn', 'id' => 'a']), 'id="a"');
62
+ bf_assert_eq(bfs_run(['oncology' => 'x']), 'oncology="x"');
63
+ });
64
+
65
+ bf_test('children skipped, ref passed through', function () {
66
+ bf_assert_eq(bfs_run(['children' => 'x', 'id' => 'a']), 'id="a"');
67
+ // JS `spreadAttrs` does NOT filter `ref` (`applyRestAttrs` does -- a
68
+ // separate divergence).
69
+ bf_assert_eq(bfs_run(['ref' => 'x', 'id' => 'a']), 'id="a" ref="x"');
70
+ });
71
+
72
+ bf_test('boolean values', function () {
73
+ // Contract: callers MUST pass a real PHP bool for boolean attributes --
74
+ // no sentinel object is needed (PHP has a real boolean type).
75
+ bf_assert_eq(bfs_run(['hidden' => true, 'id' => 'a']), 'hidden id="a"');
76
+ bf_assert_eq(bfs_run(['hidden' => false, 'id' => 'a']), 'id="a"');
77
+ // Plain numeric 0 renders as a value (matches `tabindex="0"`).
78
+ bf_assert_eq(bfs_run(['tabindex' => 0]), 'tabindex="0"');
79
+ });
80
+
81
+ bf_test('nullish skip', function () {
82
+ bf_assert_eq(bfs_run(['a' => null, 'b' => 'x']), 'b="x"');
83
+ });
84
+
85
+ bf_test('HTML escape', function () {
86
+ bf_assert_eq(bfs_run(['title' => '<b>"x"</b>']), 'title="&lt;b&gt;&#34;x&#34;&lt;/b&gt;"');
87
+ bf_assert_eq(bfs_run(['alt' => 'tom & jerry']), 'alt="tom &amp; jerry"');
88
+ });
89
+
90
+ bf_test('style object lowering', function () {
91
+ bf_assert_eq(
92
+ bfs_run(['style' => ['backgroundColor' => 'red', 'color' => 'white']]),
93
+ 'style="background-color:red;color:white"'
94
+ );
95
+ bf_assert_eq(bfs_run(['style' => 'color:red']), 'style="color:red"');
96
+ });
97
+
98
+ return bf_finish();