@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.
- package/README.md +69 -0
- package/composer.json +21 -0
- package/package.json +26 -0
- package/src/BarefootJS.php +1705 -0
- package/src/Evaluator.php +968 -0
- package/src/Json.php +67 -0
- package/src/SearchParams.php +77 -0
- package/tests/_harness.php +164 -0
- package/tests/run.php +72 -0
- package/tests/test_eval_vectors.php +119 -0
- package/tests/test_evaluator.php +274 -0
- package/tests/test_helper_vectors.php +334 -0
- package/tests/test_omit.php +57 -0
- package/tests/test_props_attr.php +63 -0
- package/tests/test_query.php +47 -0
- package/tests/test_render_child.php +123 -0
- package/tests/test_search_params.php +56 -0
- package/tests/test_spread_attrs.php +98 -0
- package/tests/test_template_primitives.php +370 -0
- package/tests/vector-divergences.json +42 -0
|
@@ -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='{"note":"it's <b> & co"}'");
|
|
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
|
+
['"', ''', '<', '>', '&'],
|
|
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="<b>"x"</b>"');
|
|
87
|
+
bf_assert_eq(bfs_run(['alt' => 'tom & jerry']), 'alt="tom & 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();
|