barefootjs 0.18.0__py3-none-any.whl
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.
- barefootjs/__init__.py +57 -0
- barefootjs/backend_jinja.py +125 -0
- barefootjs/evaluator.py +787 -0
- barefootjs/runtime.py +1334 -0
- barefootjs/search_params.py +89 -0
- barefootjs-0.18.0.dist-info/METADATA +18 -0
- barefootjs-0.18.0.dist-info/RECORD +9 -0
- barefootjs-0.18.0.dist-info/WHEEL +5 -0
- barefootjs-0.18.0.dist-info/top_level.txt +1 -0
barefootjs/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""barefootjs -- Python runtime for the @barefootjs/jinja adapter.
|
|
2
|
+
|
|
3
|
+
Python port of the Perl `BarefootJS` runtime (`packages/adapter-perl`),
|
|
4
|
+
targeting Jinja2 as the template engine (mirroring `packages/adapter-xslate`'s
|
|
5
|
+
Text::Xslate backend). Generated `.jinja` templates call the `BarefootJS`
|
|
6
|
+
instance as a `bf` object: `{{ bf.scope_attr() }}`, `{{ bf.json(x) }}`,
|
|
7
|
+
`{{ bf.spread_attrs(bag) }}`.
|
|
8
|
+
|
|
9
|
+
Quick start::
|
|
10
|
+
|
|
11
|
+
from barefootjs import BarefootJS, JinjaBackend
|
|
12
|
+
|
|
13
|
+
backend = JinjaBackend(paths=["templates"])
|
|
14
|
+
bf = BarefootJS(None, {"backend": backend})
|
|
15
|
+
html = backend.render_named("counter", bf, {"count": 0})
|
|
16
|
+
|
|
17
|
+
Package layout (mirrors the Perl distribution's module layering):
|
|
18
|
+
|
|
19
|
+
runtime.py -- the engine-agnostic `bf` object (port of BarefootJS.pm)
|
|
20
|
+
evaluator.py -- ParsedExpr evaluator for the `*_eval` helpers
|
|
21
|
+
(port of BarefootJS/Evaluator.pm)
|
|
22
|
+
search_params.py -- searchParams() SSR reader (port of
|
|
23
|
+
BarefootJS/SearchParams.pm)
|
|
24
|
+
backend_jinja.py -- the Jinja2 engine backend (port of
|
|
25
|
+
BarefootJS::Backend::Xslate)
|
|
26
|
+
|
|
27
|
+
Only dependency: `jinja2` (which brings in `markupsafe`). Everything else is
|
|
28
|
+
stdlib.
|
|
29
|
+
|
|
30
|
+
Run the test suite with:
|
|
31
|
+
|
|
32
|
+
python3 -m unittest discover -s packages/adapter-jinja/python/tests -t packages/adapter-jinja/python
|
|
33
|
+
|
|
34
|
+
(equivalently, from `packages/adapter-jinja/python`:
|
|
35
|
+
`python3 -m unittest discover -s tests`.)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from .backend_jinja import JinjaBackend, default_json_encoder
|
|
39
|
+
from .evaluator import eval_json
|
|
40
|
+
from .evaluator import evaluate as evaluate_expr
|
|
41
|
+
from .runtime import BarefootJS, jinja_ident, js_bool_str, js_number, js_string, js_truthy, mangle_ident
|
|
42
|
+
from .search_params import SearchParams
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
"BarefootJS",
|
|
46
|
+
"JinjaBackend",
|
|
47
|
+
"SearchParams",
|
|
48
|
+
"default_json_encoder",
|
|
49
|
+
"eval_json",
|
|
50
|
+
"evaluate_expr",
|
|
51
|
+
"jinja_ident",
|
|
52
|
+
"mangle_ident",
|
|
53
|
+
"js_bool_str",
|
|
54
|
+
"js_number",
|
|
55
|
+
"js_string",
|
|
56
|
+
"js_truthy",
|
|
57
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Python port of packages/adapter-xslate/lib/BarefootJS/Backend/Xslate.pm.
|
|
2
|
+
|
|
3
|
+
Jinja2 rendering backend for the `barefootjs` runtime.
|
|
4
|
+
|
|
5
|
+
The engine-agnostic runtime logic (the JS-compat value helpers, array/string
|
|
6
|
+
methods, hydration markers, child rendering) lives in `runtime.BarefootJS`.
|
|
7
|
+
This backend supplies the four engine-specific operations the runtime
|
|
8
|
+
delegates to, targeting Jinja2 syntax:
|
|
9
|
+
|
|
10
|
+
encode_json(data) -> JSON string (injectable encoder)
|
|
11
|
+
mark_raw(str) -> a value Jinja emits verbatim (no re-escaping)
|
|
12
|
+
materialize(value) -> resolve a captured-children value to a string
|
|
13
|
+
render_named(name, bf, vars) -> render `<name>.jinja` with `bf` + vars bound
|
|
14
|
+
|
|
15
|
+
Pair it with the `@barefootjs/jinja` compile-time adapter, which emits Jinja2
|
|
16
|
+
`.jinja` templates that call the runtime as a `bf` object: `{{ bf.scope_attr()
|
|
17
|
+
}}`, `{{ bf.json(x) }}`, `{{ bf.spread_attrs(bag) }}`. Jinja auto-escapes
|
|
18
|
+
`{{ ... }}` interpolations (the Environment is built with `autoescape=True`);
|
|
19
|
+
helpers that emit markup return `mark_raw` (`markupsafe.Markup`) values so
|
|
20
|
+
they render unescaped, mirroring Kolon's `| mark_raw` / Mojo EP's `<%==` vs
|
|
21
|
+
`<%=` distinction.
|
|
22
|
+
|
|
23
|
+
Unlike a web-framework plugin, this has no dependency on a specific web
|
|
24
|
+
framework: a plain `jinja2.Environment` renders templates from a path, so it
|
|
25
|
+
runs under any WSGI app (Flask, Plack-equivalent) or none at all.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json as _json
|
|
31
|
+
import math
|
|
32
|
+
from typing import Any, Callable, Optional, Sequence
|
|
33
|
+
|
|
34
|
+
from jinja2 import ChainableUndefined, Environment, FileSystemLoader
|
|
35
|
+
from markupsafe import Markup
|
|
36
|
+
|
|
37
|
+
from .runtime import jinja_ident
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _prepare_for_json(value: Any) -> Any:
|
|
41
|
+
"""Recursively replace non-finite floats with `None` so they encode as
|
|
42
|
+
JSON `null` -- matches `JSON.stringify(NaN)` / `JSON.stringify(Infinity)`
|
|
43
|
+
at ANY depth (a strictly more complete carve-out than the Go/Perl
|
|
44
|
+
backends' documented top-level-only handling, and still spec-compliant:
|
|
45
|
+
the template-helpers.md `json` entry only requires the top level).
|
|
46
|
+
Anything else JSON can't encode (cycles, non-serialisable objects) is
|
|
47
|
+
left for `json.dumps` to raise on -- the failure policy mirrors Go/Perl:
|
|
48
|
+
user-data marshalling bubbles errors loudly rather than silently
|
|
49
|
+
producing an empty payload."""
|
|
50
|
+
if isinstance(value, float):
|
|
51
|
+
if math.isnan(value) or math.isinf(value):
|
|
52
|
+
return None
|
|
53
|
+
return value
|
|
54
|
+
if isinstance(value, dict):
|
|
55
|
+
return {k: _prepare_for_json(v) for k, v in value.items()}
|
|
56
|
+
if isinstance(value, (list, tuple)):
|
|
57
|
+
return [_prepare_for_json(v) for v in value]
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def default_json_encoder(data: Any) -> str:
|
|
62
|
+
"""`sort_keys=True` keeps key order deterministic (matching the Xslate
|
|
63
|
+
backend's `JSON::PP->canonical` -- see `render.t`'s "canonical key
|
|
64
|
+
order" assertion, ported verbatim in this package's render test).
|
|
65
|
+
`separators=(',', ':')` matches `JSON.stringify`'s compact form."""
|
|
66
|
+
return _json.dumps(
|
|
67
|
+
_prepare_for_json(data), separators=(",", ":"), sort_keys=True, allow_nan=False
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class JinjaBackend:
|
|
72
|
+
"""Jinja2 rendering backend. Accepts a pre-built `Environment`, or builds
|
|
73
|
+
one from `paths` (a list of template directories) plus optional
|
|
74
|
+
`environment_options`. `json_encoder` overrides the default canonical
|
|
75
|
+
(sorted-key) encoder."""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
*,
|
|
80
|
+
env: Optional[Environment] = None,
|
|
81
|
+
paths: Optional[Sequence[str]] = None,
|
|
82
|
+
json_encoder: Optional[Callable[[Any], str]] = None,
|
|
83
|
+
environment_options: Optional[dict] = None,
|
|
84
|
+
):
|
|
85
|
+
self._json_encoder = json_encoder or default_json_encoder
|
|
86
|
+
if env is not None:
|
|
87
|
+
self._env = env
|
|
88
|
+
else:
|
|
89
|
+
options = dict(environment_options or {})
|
|
90
|
+
options.setdefault("autoescape", True)
|
|
91
|
+
options.setdefault("undefined", ChainableUndefined)
|
|
92
|
+
self._env = Environment(loader=FileSystemLoader(list(paths or [])), **options)
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def env(self) -> Environment:
|
|
96
|
+
return self._env
|
|
97
|
+
|
|
98
|
+
def encode_json(self, data: Any) -> str:
|
|
99
|
+
return self._json_encoder(data)
|
|
100
|
+
|
|
101
|
+
def mark_raw(self, s: Any) -> Markup:
|
|
102
|
+
"""Mark a string as already-safe so Jinja emits it verbatim (no
|
|
103
|
+
auto-escape)."""
|
|
104
|
+
return Markup("" if s is None else s)
|
|
105
|
+
|
|
106
|
+
def materialize(self, value: Any) -> Any:
|
|
107
|
+
"""JSX children captured by the adapter resolve to a string here.
|
|
108
|
+
Jinja's `{% set children %}...{% endset %}` set-block already
|
|
109
|
+
produces a rendered `Markup` string directly (unlike Mojo's `begin
|
|
110
|
+
%>...<% end`, which yields a CODE ref) -- `materialize` still
|
|
111
|
+
supports a callable for parity with the Perl port's contract and any
|
|
112
|
+
lazy-render composition a caller builds on top of this backend."""
|
|
113
|
+
return value() if callable(value) else value
|
|
114
|
+
|
|
115
|
+
def render_named(self, template_name: str, child_bf: Any, variables: Optional[dict]) -> str:
|
|
116
|
+
"""Render `<name>.jinja` with `child_bf` bound as the `bf` variable
|
|
117
|
+
for the nested render, plus the supplied template vars. Keyword
|
|
118
|
+
mangling (`jinja_ident`) is applied here -- the one point every
|
|
119
|
+
props dict is turned into template variables -- so a prop literally
|
|
120
|
+
named e.g. `class` or `none` doesn't collide with a Jinja/Python
|
|
121
|
+
reserved word."""
|
|
122
|
+
template = self._env.get_template(f"{template_name}.jinja")
|
|
123
|
+
mangled = {jinja_ident(k): v for k, v in (variables or {}).items()}
|
|
124
|
+
mangled["bf"] = child_bf
|
|
125
|
+
return template.render(**mangled)
|