volaro 0.1.0-alpha.11 → 0.1.0-alpha.12
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 +1 -1
- package/compiler/SOURCE_INFO.json +2 -2
- package/compiler/SOURCE_REV +1 -1
- package/compiler/validator/vlcheck/checks.py +275 -56
- package/compiler/validator/vlcheck/resolve.py +8 -13
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +41 -0
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.js +1 -1
- package/compiler/vlbuild/vlbuild/emit.py +26 -41
- package/compiler/vlbuild/vlbuild/server_emit.py +35 -68
- package/language/spec.md +1 -1
- package/language/supported.md +36 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**Limited alpha** (this is `0.1.0-alpha.
|
|
3
|
+
**Limited alpha** (this is `0.1.0-alpha.12`), published under both the
|
|
4
4
|
`latest` and `alpha` dist-tags: `npm install volaro`.
|
|
5
5
|
|
|
6
6
|
Volaro is an experimental application language intended for AI authoring and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"revision": "
|
|
2
|
+
"revision": "92972bae88b0cd71b621a8c21dbeae052dc8a3c9",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "09ae1f5782b061471ba112a8820cd9a08f4519ecff84eb418a5062f8ed23c467"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
92972bae88b0cd71b621a8c21dbeae052dc8a3c9
|
|
@@ -124,64 +124,186 @@ _METHOD_OTHER_FIX = {
|
|
|
124
124
|
# Names a page-server / service body has in scope that are ambient objects,
|
|
125
125
|
# not user values.
|
|
126
126
|
_AMBIENT_RECEIVERS = {"session", "request"}
|
|
127
|
-
#
|
|
128
|
-
#
|
|
129
|
-
#
|
|
127
|
+
# The std free-function surface, in one place. Every std function name the
|
|
128
|
+
# checker knows is in exactly one of the two tables below:
|
|
129
|
+
#
|
|
130
|
+
# STD_BUILT_FNS -- vlbuild lowers `name(...)` to `VL.std.name(...)`.
|
|
131
|
+
# vlbuild's `emit.STD_FNS` IS this set (imported, not
|
|
132
|
+
# copied), and `VL.std` in `assets/vlrt.js` defines each
|
|
133
|
+
# one; server.js embeds that same `VL.std` block, so a
|
|
134
|
+
# route body gets the identical function.
|
|
135
|
+
# _FN_NOT_BUILT -- a name the spec, the corpus or an agent uses that this
|
|
136
|
+
# build has no function for. A bare call where the build
|
|
137
|
+
# emits it (a view or a service route) is E-FN-NOT-BUILT
|
|
138
|
+
# with this fix line; a module-level `fn` body is never
|
|
139
|
+
# emitted into either, so it is not judged there.
|
|
140
|
+
#
|
|
141
|
+
# resolve.py's known-name list is derived from these two, so a name cannot be
|
|
142
|
+
# "known" without being classified. `test_std_fn_parity` in vlbuild's
|
|
143
|
+
# tests/test_build.py calls every known name through the real checker and
|
|
144
|
+
# requires each accepted one to exist in the emitter, `vlrt.js`, its minified
|
|
145
|
+
# copy and server.js. This replaced five hand-synced tables: `unique` was
|
|
146
|
+
# known to the resolver, missing from the other four, and built to a bare
|
|
147
|
+
# `unique(...)` that crashed every page (Testing Lab search brief, Example
|
|
148
|
+
# Demo 2.3).
|
|
149
|
+
STD_BUILT_FNS = frozenset({
|
|
150
|
+
# std.list
|
|
151
|
+
"filter", "map", "take", "count", "sum", "sort_by", "group_by", "chunk",
|
|
152
|
+
# list growth (spec section 20.9): the only way to combine or grow a list
|
|
153
|
+
"concat", "append",
|
|
154
|
+
# `unique(xs)`: first occurrence of each distinct value, in order
|
|
155
|
+
"unique",
|
|
156
|
+
# std.str (spec section 13: split, join, trim, case, replace, contains,
|
|
157
|
+
# regex -- `matches`)
|
|
158
|
+
"join", "split", "first_char", "trim", "upper", "lower", "replace",
|
|
159
|
+
"contains", "matches",
|
|
160
|
+
# std.map
|
|
161
|
+
"keys", "values", "get_or",
|
|
162
|
+
# numeric conversion
|
|
163
|
+
"int_to_float", "float_to_int",
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
_FOLD_FIX = "a `for` loop over a `let mut` accumulator"
|
|
130
167
|
_FN_NOT_BUILT = {
|
|
131
|
-
|
|
132
|
-
"fold":
|
|
168
|
+
# in the spec (section 13 or its examples), not built
|
|
169
|
+
"fold": _FOLD_FIX,
|
|
133
170
|
"merge": "a record literal naming every field",
|
|
134
171
|
"to_int": "no string-to-int conversion is built yet; keep the value an int",
|
|
172
|
+
"now": ("a `model` field default (`created_at time = now()`); a view or "
|
|
173
|
+
"route cannot read the clock in this build"),
|
|
174
|
+
# names the checker has accepted, not built
|
|
175
|
+
"sorted": "sort_by(xs, fn(x) x)",
|
|
176
|
+
"first": "xs[0] (check xs.len() > 0 first)",
|
|
177
|
+
"reduce": _FOLD_FIX,
|
|
178
|
+
"flatten": _FOLD_FIX + " and `concat(acc, x)`",
|
|
179
|
+
"reverse": _FOLD_FIX + " and `concat([x], acc)`",
|
|
180
|
+
"zip": "a `for i in 0..xs.len()` loop reading `xs[i]` and `ys[i]`",
|
|
181
|
+
"enumerate": "a `for i in 0..xs.len()` loop reading `xs[i]`",
|
|
182
|
+
"index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
|
|
183
|
+
"slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
|
|
184
|
+
"range": "a `for i in 0..n` loop",
|
|
185
|
+
"any": "count(filter(xs, f)) > 0",
|
|
186
|
+
"all": "count(filter(xs, f)) == count(xs)",
|
|
187
|
+
"none": "count(filter(xs, f)) == 0",
|
|
188
|
+
"avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
|
|
189
|
+
"min": "if (a < b) a else b",
|
|
190
|
+
"max": "if (a > b) a else b",
|
|
191
|
+
"abs": "if (x < 0) -x else x",
|
|
192
|
+
"round": "float_to_int(x + 0.5) for a non-negative x",
|
|
193
|
+
"floor": "float_to_int(x) for a non-negative x",
|
|
194
|
+
"ceil": "float_to_int(x) + 1 when x has a fraction, for a non-negative x",
|
|
195
|
+
"starts_with": "matches(s, \"^prefix\")",
|
|
196
|
+
"ends_with": "matches(s, \"suffix$\")",
|
|
197
|
+
"pad_start": "string interpolation: `\"{pad}{s}\"`",
|
|
198
|
+
"pad_end": "string interpolation: `\"{s}{pad}\"`",
|
|
199
|
+
"repeat": _FOLD_FIX + " building the string",
|
|
200
|
+
"uuid4": "let the database assign the id (`id uuid pk`)",
|
|
201
|
+
"random": "no random source is built yet",
|
|
135
202
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
#
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# has no lowering for). Duplicated from `vlbuild/emit.py`'s `STD_FNS` (plus
|
|
142
|
-
# `now`, whose one working lowering — a `model` field default — lives in
|
|
143
|
-
# `server_emit.py`'s `_sql_default`) rather than imported: `vlcheck` has no
|
|
144
|
-
# dependency on `vlbuild` (the dependency runs the other way), so the two
|
|
145
|
-
# packages cannot share this table directly. Keep in sync by hand; a mismatch
|
|
146
|
-
# only makes this check too strict or too loose, never silently wrong, since
|
|
147
|
-
# `vlbuild` is the actual authority on what it lowers.
|
|
203
|
+
assert not (STD_BUILT_FNS & set(_FN_NOT_BUILT))
|
|
204
|
+
BUILTINS.update(STD_BUILT_FNS, _FN_NOT_BUILT)
|
|
205
|
+
# The names `use std.X { ... }` may import: every built function, plus `now`,
|
|
206
|
+
# whose one working lowering -- a `model` field default -- is server_emit.py's
|
|
207
|
+
# `_sql_default`.
|
|
148
208
|
#
|
|
149
209
|
# Found reproducing the login-page benchmark's Bug 3 (`test-only`
|
|
150
|
-
# `login-page/volaro/NOTES.md`): `use std.http { post }`
|
|
151
|
-
# clean and
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
210
|
+
# `login-page/volaro/NOTES.md`): `use std.http { post }` passed `vlcheck`
|
|
211
|
+
# clean and compiled to a bare, undefined `post(...)` call. `std.http` has no
|
|
212
|
+
# member here; nothing under it can pass this check.
|
|
213
|
+
_STD_SUPPORTED_NAMES = STD_BUILT_FNS | {"now"}
|
|
214
|
+
|
|
215
|
+
# Bare words the spec uses in argument position (`.order(created_at: desc)`,
|
|
216
|
+
# `auth.start_oauth(google)`). They resolve as names; resolve.py reads this
|
|
217
|
+
# set.
|
|
218
|
+
CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
|
|
219
|
+
|
|
220
|
+
# Every other name the checker knows is not a function the build lowers, so
|
|
221
|
+
# a bare call to one compiles to an undefined (or non-function) JavaScript
|
|
222
|
+
# callee and throws when the page or route runs. Each is classified here by
|
|
223
|
+
# the one place it may be called, if any; `_check_builtin_call` rejects the
|
|
224
|
+
# call everywhere else in emitted code. `test_bare_call_parity` (vlbuild
|
|
225
|
+
# tests/test_build.py, gate 2) calls every known name in every emitted
|
|
226
|
+
# context and requires the checker to reject it or the build to lower it,
|
|
227
|
+
# and `_unclassified_names()` must stay empty, so a name added to BUILTINS
|
|
228
|
+
# cannot slip through unjudged. (Testing Lab, Example Demo 2.3: `unique`
|
|
229
|
+
# was the std case; these are the rest of the class.)
|
|
230
|
+
#
|
|
231
|
+
# `where()` predicates (spec section 13.1): a predicate is only a
|
|
232
|
+
# `.where(field: pred(x))` value (or a branch of an `if` that is one). The
|
|
233
|
+
# fix is the operator that tests the same thing on a value. `contains` is
|
|
234
|
+
# also a std.str function, so it is built everywhere.
|
|
235
|
+
_WHERE_PREDICATE_FIX = {
|
|
236
|
+
"lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
|
|
237
|
+
"ne": "`a != b`", "between": "`lo <= x and x <= hi`",
|
|
238
|
+
"any_of": "`count(filter(xs, fn(v) v == x)) > 0`",
|
|
239
|
+
}
|
|
240
|
+
# Error codes (spec section 6): a code is constructed only as the argument of
|
|
241
|
+
# `err(...)` in a service or page-server route body, where the build turns
|
|
242
|
+
# it into the route's error response. `catch not_found` and a match arm name
|
|
243
|
+
# a code without calling it.
|
|
244
|
+
_ERROR_CODES = frozenset({
|
|
245
|
+
"not_found", "forbidden", "conflict", "rejected", "unexpected",
|
|
246
|
+
"out_of_range", "decode", "invalid_credentials", "unverified_email",
|
|
247
|
+
"totp_required", "rate_limited",
|
|
248
|
+
})
|
|
249
|
+
_ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
|
|
250
|
+
"page.server route body; a view reports a failure by "
|
|
251
|
+
"setting its own state")
|
|
252
|
+
# Names that are never called: namespaces and ambient objects (call a member
|
|
253
|
+
# instead), type names (no conversion function by that name), contextual
|
|
254
|
+
# words, and names the spec's examples use that no build provides.
|
|
255
|
+
_NO_BUILD_NAMESPACE = "no `%s` namespace is built yet"
|
|
256
|
+
_NOT_CALLABLE = {
|
|
257
|
+
# namespaces / ambient objects
|
|
258
|
+
"env": "read a member: `env.NAME`",
|
|
259
|
+
"session": "read a member: `session.user`",
|
|
260
|
+
"request": "read a member: `request.headers`",
|
|
261
|
+
"route": "`route.to(\"/path\")`",
|
|
262
|
+
"auth": "a section 11.3 call: `auth.sign_in(email: e, password: p)`",
|
|
263
|
+
"db": "a query: `db.posts.where(...).all()?`",
|
|
264
|
+
"log": "`log.info(\"message\", value)`",
|
|
265
|
+
"api": "an `api` block endpoint: `client.endpoint(...)`",
|
|
266
|
+
"analytics": _NO_BUILD_NAMESPACE % "analytics",
|
|
267
|
+
"events": _NO_BUILD_NAMESPACE % "events",
|
|
268
|
+
"ws": _NO_BUILD_NAMESPACE % "ws",
|
|
269
|
+
"mailer": _NO_BUILD_NAMESPACE % "mailer",
|
|
270
|
+
"billing": _NO_BUILD_NAMESPACE % "billing",
|
|
271
|
+
# result constructor: a `T!` success is the plain value (spec section 6)
|
|
272
|
+
"ok": "return the value itself; a successful `T!` is the plain value",
|
|
273
|
+
# names from the spec's examples, not built-in functions
|
|
274
|
+
"create_guest": "no `create_guest` is built in; build the value inline",
|
|
275
|
+
"guest_user": "no `guest_user` is built in; build the value inline",
|
|
276
|
+
# contextual words
|
|
277
|
+
"asc": "an `.order(field: asc)` value, not a call",
|
|
278
|
+
"desc": "an `.order(field: desc)` value, not a call",
|
|
279
|
+
"google": "an `auth.start_oauth(google)` argument, not a call",
|
|
280
|
+
"github": "an `auth.start_oauth(github)` argument, not a call",
|
|
281
|
+
# type names
|
|
282
|
+
"str": "string interpolation: `\"{x}\"`",
|
|
283
|
+
"int": "float_to_int(x)",
|
|
284
|
+
"float": "int_to_float(x)",
|
|
285
|
+
"bool": "a comparison: `x != 0`, `s != \"\"`",
|
|
286
|
+
"bytes": "no conversion to `bytes` is built",
|
|
287
|
+
"time": "a `time` field value; no conversion to `time` is built",
|
|
288
|
+
"dur": "no conversion to `dur` is built",
|
|
289
|
+
"uuid": "let the database assign the id (`id uuid pk`)",
|
|
290
|
+
"json": "no conversion to `json` is built",
|
|
291
|
+
"nil": "the value `nil`, not a call",
|
|
182
292
|
}
|
|
183
293
|
|
|
184
294
|
|
|
295
|
+
def _unclassified_names() -> set[str]:
|
|
296
|
+
"""Known names with no bare-call rule: must stay empty (asserted below
|
|
297
|
+
and by test_bare_call_parity)."""
|
|
298
|
+
classified = (set(STD_BUILT_FNS) | set(_FN_NOT_BUILT) | set(_METHOD_ONLY)
|
|
299
|
+
| set(_WHERE_PREDICATE_FIX) | _ERROR_CODES | {"err"}
|
|
300
|
+
| set(_NOT_CALLABLE) | set(ELEMENT_NAMES))
|
|
301
|
+
return (set(BUILTINS) | set(_PRIM_NAMES) | CONTEXTUAL_WORDS) - classified
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
assert not _unclassified_names(), _unclassified_names()
|
|
305
|
+
|
|
306
|
+
|
|
185
307
|
def _lit_text(v):
|
|
186
308
|
"""The literal text of a plain string literal, or None when `v` is not one
|
|
187
309
|
(a bare word, an expression, or an *interpolated* string -- whose runtime
|
|
@@ -324,6 +446,17 @@ class Checker:
|
|
|
324
446
|
# (module-level fn bodies)
|
|
325
447
|
self._module_names: set[str] = set()
|
|
326
448
|
self._emitted = True
|
|
449
|
+
# which emitted code is being walked: "view" (a view's state,
|
|
450
|
+
# derives, render tree and handlers) or "route" (a service route,
|
|
451
|
+
# a page.server handler, a _server.vl provider). `err(...)` builds
|
|
452
|
+
# only in a route.
|
|
453
|
+
self._ctx = "route"
|
|
454
|
+
# payload-carrying union variants this module declares, and the
|
|
455
|
+
# calls whose position makes them legal (a `where()` predicate
|
|
456
|
+
# value, the error constructor inside `err(...)`)
|
|
457
|
+
self._variants: set[str] = set()
|
|
458
|
+
self._views: set[str] = set()
|
|
459
|
+
self._exempt_calls: set[int] = set()
|
|
327
460
|
# names of `fn`-typed record fields in this module: `x.on_pick(id)`
|
|
328
461
|
# calls one, which E-METHOD-UNKNOWN must not report
|
|
329
462
|
self._fn_fields: set[str] = set()
|
|
@@ -2973,10 +3106,15 @@ class Checker:
|
|
|
2973
3106
|
if nm:
|
|
2974
3107
|
top.add(nm)
|
|
2975
3108
|
self._module_names.add(nm)
|
|
3109
|
+
if isinstance(it, A.ViewDecl):
|
|
3110
|
+
self._views.add(nm)
|
|
2976
3111
|
if isinstance(it, A.Use):
|
|
2977
3112
|
for n in it.names:
|
|
2978
3113
|
top.add(n)
|
|
2979
|
-
|
|
3114
|
+
# `use std.time { now }` names the std function, not an
|
|
3115
|
+
# author's own: its call is still judged below
|
|
3116
|
+
if not it.path.startswith("std."):
|
|
3117
|
+
self._module_names.add(n)
|
|
2980
3118
|
base = it.path.replace("./", "").replace("../", "")
|
|
2981
3119
|
base = base.split("/")[-1].split(".")[-1]
|
|
2982
3120
|
if base:
|
|
@@ -2987,6 +3125,7 @@ class Checker:
|
|
|
2987
3125
|
elif isinstance(it, A.TypeDecl) and it.kind == "union":
|
|
2988
3126
|
for v in it.variants:
|
|
2989
3127
|
top.add(v.name)
|
|
3128
|
+
self._variants.add(v.name)
|
|
2990
3129
|
elif isinstance(it, (A.TypeDecl, A.ModelDecl)):
|
|
2991
3130
|
pass
|
|
2992
3131
|
|
|
@@ -3006,7 +3145,9 @@ class Checker:
|
|
|
3006
3145
|
scope.bind(s.name, "derive")
|
|
3007
3146
|
elif isinstance(s, A.LoadDecl):
|
|
3008
3147
|
scope.bind(s.name, "load result")
|
|
3148
|
+
self._ctx = "view"
|
|
3009
3149
|
self._scoped(it.body, [top, scope], fallible=True)
|
|
3150
|
+
self._ctx = "route"
|
|
3010
3151
|
elif isinstance(it, A.ServiceDecl):
|
|
3011
3152
|
for r in it.routes:
|
|
3012
3153
|
scope = _Scope((p.name for p in r.params), "route parameter")
|
|
@@ -3121,23 +3262,100 @@ class Checker:
|
|
|
3121
3262
|
def _check_builtin_call(self, call, scopes: list[set]) -> None:
|
|
3122
3263
|
"""A bare call to an allowlisted name the build cannot lower compiles
|
|
3123
3264
|
to an undefined JS function (action-48). Skipped when the name is the
|
|
3124
|
-
author's own: a local binding or a module declaration or
|
|
3265
|
+
author's own: a local binding or a module declaration or a
|
|
3266
|
+
non-`std` import."""
|
|
3125
3267
|
callee = call.callee
|
|
3126
|
-
if not isinstance(callee, A.Name):
|
|
3268
|
+
if not isinstance(callee, A.Name) or id(call) in self._exempt_calls:
|
|
3127
3269
|
return
|
|
3128
3270
|
nm = callee.id
|
|
3129
|
-
if nm in
|
|
3271
|
+
if nm in STD_BUILT_FNS:
|
|
3272
|
+
return
|
|
3273
|
+
# `session` / `request` are bound in a view's or handler's scope, but
|
|
3274
|
+
# they are the ambient objects, never the author's own function
|
|
3275
|
+
ambient = nm in _AMBIENT_RECEIVERS
|
|
3276
|
+
throws = "compiles to a call the built page or route cannot make, and throws when it runs"
|
|
3277
|
+
if (nm in self._views and self._emitted
|
|
3278
|
+
and not any(nm in s for s in scopes[1:])):
|
|
3279
|
+
# a view is rendered by a line of its own (an element); as a
|
|
3280
|
+
# value, `Row(1)` calls the emitted render function with no
|
|
3281
|
+
# context and throws
|
|
3282
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3283
|
+
"'%s' is a view, not a function -- as a value `%s(...)` %s"
|
|
3284
|
+
% (nm, nm, throws), call,
|
|
3285
|
+
"render it as its own line in the view body: `%s(...)`" % nm)
|
|
3286
|
+
return
|
|
3287
|
+
if not ambient and (nm in self._module_names
|
|
3288
|
+
or any(nm in s for s in scopes[1:])):
|
|
3130
3289
|
return
|
|
3131
3290
|
if nm in _METHOD_ONLY:
|
|
3132
3291
|
self._d("E-METHOD-AS-FN", Severity.ERROR,
|
|
3133
3292
|
"'%s' is a method, not a function -- `%s(...)` compiles to "
|
|
3134
3293
|
"an undefined function and throws when the page runs"
|
|
3135
3294
|
% (nm, nm), call, "call it on the value: `%s`" % _METHOD_ONLY[nm])
|
|
3136
|
-
elif
|
|
3295
|
+
elif not self._emitted:
|
|
3296
|
+
# a module-level `fn` body: the build refuses module functions
|
|
3297
|
+
# (E-VIEW-MODULE-FN), so nothing here is emitted
|
|
3298
|
+
return
|
|
3299
|
+
elif nm in _FN_NOT_BUILT:
|
|
3137
3300
|
self._d("E-FN-NOT-BUILT", Severity.ERROR,
|
|
3138
|
-
"'%s' is
|
|
3139
|
-
"the call compiles
|
|
3301
|
+
"'%s' is a std name this build of Volaro has no function "
|
|
3302
|
+
"for -- the call compiles to an undefined function and "
|
|
3303
|
+
"throws when it runs" % nm,
|
|
3140
3304
|
call, "use %s" % _FN_NOT_BUILT[nm])
|
|
3305
|
+
elif nm == "err":
|
|
3306
|
+
if self._ctx == "view":
|
|
3307
|
+
self._d("E-ERR-CONTEXT", Severity.ERROR,
|
|
3308
|
+
"`err(...)` fails a route; a view has no route to fail "
|
|
3309
|
+
"-- in a view it %s" % throws, call,
|
|
3310
|
+
"use " + _ERR_ROUTE_FIX)
|
|
3311
|
+
elif nm in _ERROR_CODES or nm in self._variants:
|
|
3312
|
+
what = ("the error code '%s'" % nm if nm in _ERROR_CODES
|
|
3313
|
+
and nm not in self._variants
|
|
3314
|
+
else "the union variant '%s'" % nm)
|
|
3315
|
+
self._d("E-ERR-CONTEXT", Severity.ERROR,
|
|
3316
|
+
"%s is built only as the argument of `err(...)` in a route "
|
|
3317
|
+
"body -- here `%s(...)` %s" % (what, nm, throws), call,
|
|
3318
|
+
"use " + _ERR_ROUTE_FIX + ("" if nm in _ERROR_CODES else
|
|
3319
|
+
"; for data, a record type with the same fields"))
|
|
3320
|
+
elif nm in _WHERE_PREDICATE_FIX:
|
|
3321
|
+
self._d("E-WHERE-PREDICATE", Severity.ERROR,
|
|
3322
|
+
"'%s' is a `where()` predicate (spec section 13.1), built "
|
|
3323
|
+
"only as a `.where(field: %s(...))` value -- here it %s"
|
|
3324
|
+
% (nm, nm, throws), call,
|
|
3325
|
+
"compare the values with %s, or move it into "
|
|
3326
|
+
"`.where(field: %s(x))`" % (_WHERE_PREDICATE_FIX[nm], nm))
|
|
3327
|
+
elif nm in _NOT_CALLABLE:
|
|
3328
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3329
|
+
"'%s' is not a function -- `%s(...)` %s" % (nm, nm, throws),
|
|
3330
|
+
call, "use %s" % _NOT_CALLABLE[nm])
|
|
3331
|
+
elif nm in ELEMENT_NAMES:
|
|
3332
|
+
self._d("E-NOT-A-FUNCTION", Severity.ERROR,
|
|
3333
|
+
"'%s' is an element, not a function -- as a value "
|
|
3334
|
+
"`%s(...)` %s" % (nm, nm, throws), call,
|
|
3335
|
+
"write the element as its own line in the view body: `%s ...`"
|
|
3336
|
+
% nm)
|
|
3337
|
+
|
|
3338
|
+
def _mark_positional_calls(self, call) -> None:
|
|
3339
|
+
"""Exempt the calls whose position gives them meaning: a predicate as
|
|
3340
|
+
a `.where(field: ...)` value (either branch of an `if` value too), and
|
|
3341
|
+
the error constructor that is the argument of `err(...)`."""
|
|
3342
|
+
callee = call.callee
|
|
3343
|
+
if isinstance(callee, A.Member) and callee.name == "where":
|
|
3344
|
+
stack = [v for _, v in call.named]
|
|
3345
|
+
while stack:
|
|
3346
|
+
v = stack.pop()
|
|
3347
|
+
if isinstance(v, A.IfExpr):
|
|
3348
|
+
stack += [v.then, v.orelse]
|
|
3349
|
+
elif (isinstance(v, A.Call) and isinstance(v.callee, A.Name)
|
|
3350
|
+
and v.callee.id in _WHERE_PREDICATE_FIX):
|
|
3351
|
+
self._exempt_calls.add(id(v))
|
|
3352
|
+
elif isinstance(callee, A.Name) and callee.id == "err":
|
|
3353
|
+
# in a view the `err(...)` itself is the one error reported
|
|
3354
|
+
for a in call.args:
|
|
3355
|
+
if (isinstance(a, A.Call) and isinstance(a.callee, A.Name)
|
|
3356
|
+
and (a.callee.id in _ERROR_CODES
|
|
3357
|
+
or a.callee.id in self._variants)):
|
|
3358
|
+
self._exempt_calls.add(id(a))
|
|
3141
3359
|
|
|
3142
3360
|
@staticmethod
|
|
3143
3361
|
def _value_receiver(obj, scopes: list[set]) -> bool:
|
|
@@ -3331,6 +3549,7 @@ class Checker:
|
|
|
3331
3549
|
self._expr(ex.obj, scopes, fallible)
|
|
3332
3550
|
self._expr(ex.index, scopes, fallible)
|
|
3333
3551
|
elif isinstance(ex, A.Call):
|
|
3552
|
+
self._mark_positional_calls(ex)
|
|
3334
3553
|
self._check_builtin_call(ex, scopes)
|
|
3335
3554
|
self._check_value_method(ex, scopes)
|
|
3336
3555
|
callee = ex.callee
|
|
@@ -50,30 +50,25 @@ import os
|
|
|
50
50
|
import re
|
|
51
51
|
|
|
52
52
|
from . import ast_nodes as A
|
|
53
|
-
from .checks import BUILTINS,
|
|
53
|
+
from .checks import (BUILTINS, CONTEXTUAL_WORDS, STD_BUILT_FNS, _FN_NOT_BUILT,
|
|
54
|
+
_PRIM_NAMES)
|
|
54
55
|
from .diagnostics import Diagnostic, LineIndex, Severity
|
|
55
56
|
from .elements import ELEMENT_NAMES
|
|
56
57
|
from .lexer import Lexer
|
|
57
58
|
from .parser import Parser
|
|
58
59
|
|
|
59
|
-
# std
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"reverse", "sorted", "unique", "flatten", "concat", "zip", "enumerate",
|
|
64
|
-
"range", "any", "all", "none", "contains", "index_of", "reduce", "slice",
|
|
65
|
-
"float_to_int", "trim", "upper", "lower", "starts_with", "ends_with",
|
|
66
|
-
"replace", "pad_start", "pad_end", "repeat", "now", "uuid4", "random",
|
|
67
|
-
"matches",
|
|
68
|
-
}
|
|
60
|
+
# std free-function names: the ones this build lowers and the ones it rejects
|
|
61
|
+
# with E-FN-NOT-BUILT (checks.py owns both tables, so a name can only become
|
|
62
|
+
# known here by being classified there). Names only -- no arity is checked.
|
|
63
|
+
_STD_EXTRA = set(STD_BUILT_FNS) | set(_FN_NOT_BUILT)
|
|
69
64
|
# std.list / std.str functions whose result is a list (E-LIST-OPERATOR);
|
|
70
65
|
# only when the name is not the module's own declaration or a local binding
|
|
71
66
|
_LIST_STD_FNS = {"concat", "append", "filter", "map", "take", "sort_by",
|
|
72
|
-
"chunk", "keys", "values", "split"}
|
|
67
|
+
"chunk", "keys", "values", "split", "unique"}
|
|
73
68
|
_ARITH = {"+", "-", "*", "/", "%"}
|
|
74
69
|
_COMPOUND_ARITH = {"+=", "-=", "*=", "/="}
|
|
75
70
|
# bare-word identifiers the spec uses in argument position (not bindings)
|
|
76
|
-
_CONTEXTUAL =
|
|
71
|
+
_CONTEXTUAL = set(CONTEXTUAL_WORDS)
|
|
77
72
|
# Spec §3.4: a component's case carries meaning.
|
|
78
73
|
# lowercase -> a std.ui *primitive element*, exactly this set (also in
|
|
79
74
|
# `checks.BUILTINS`, which is how a bare lowercase element resolves);
|
|
@@ -78,7 +78,29 @@
|
|
|
78
78
|
};
|
|
79
79
|
|
|
80
80
|
// -- std helpers referenced by transpiled `derive` / pipelines ------------
|
|
81
|
+
// @vl-std-begin
|
|
82
|
+
// server_emit.py copies the lines between the two marker lines into
|
|
83
|
+
// server.js, so a route body runs these same functions.
|
|
84
|
+
// Keep the block self-contained: JavaScript built-ins and names defined
|
|
85
|
+
// inside it only. Every name in vlcheck's STD_BUILT_FNS must be a member
|
|
86
|
+
// of VL.std (test_std_fn_parity in tests/test_build.py checks this file,
|
|
87
|
+
// the minified copy and server.js).
|
|
81
88
|
function asArray(x) { return x == null ? [] : x; }
|
|
89
|
+
// Volaro equality for `unique`: scalars as `==` compares them (`===`), and
|
|
90
|
+
// records and lists structurally, equal when their fields or items are
|
|
91
|
+
// (spec section 4.3: no identity or reference semantics).
|
|
92
|
+
function stdEq(a, b) {
|
|
93
|
+
if (a === b) return true;
|
|
94
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object"
|
|
95
|
+
|| Array.isArray(a) !== Array.isArray(b)) return false;
|
|
96
|
+
var ka = Object.keys(a);
|
|
97
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
98
|
+
for (var i = 0; i < ka.length; i++) {
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(b, ka[i])
|
|
100
|
+
|| !stdEq(a[ka[i]], b[ka[i]])) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
82
104
|
VL.std = {
|
|
83
105
|
filter: function (xs, f) { return asArray(xs).filter(function (x) { return f(x); }); },
|
|
84
106
|
map: function (xs, f) { return asArray(xs).map(function (x) { return f(x); }); },
|
|
@@ -137,6 +159,24 @@
|
|
|
137
159
|
// `state` reassignment style elsewhere (`title = ""`, not a mutating call).
|
|
138
160
|
concat: function (a, b) { return asArray(a).concat(asArray(b)); },
|
|
139
161
|
append: function (xs, x) { return asArray(xs).concat([x]); },
|
|
162
|
+
// `unique(xs)`: the first occurrence of each distinct value, in list
|
|
163
|
+
// order, as a new list. Scalars go through a Set (same verdicts as `===`,
|
|
164
|
+
// except NaN, which never equals itself and so is always kept); records
|
|
165
|
+
// and lists are compared structurally against the ones already kept.
|
|
166
|
+
unique: function (xs) {
|
|
167
|
+
var out = [], seen = new Set(), objs = [];
|
|
168
|
+
asArray(xs).forEach(function (x) {
|
|
169
|
+
if (x !== null && typeof x === "object") {
|
|
170
|
+
for (var i = 0; i < objs.length; i++) if (stdEq(objs[i], x)) return;
|
|
171
|
+
objs.push(x);
|
|
172
|
+
} else if (x === x) {
|
|
173
|
+
if (seen.has(x)) return;
|
|
174
|
+
seen.add(x);
|
|
175
|
+
}
|
|
176
|
+
out.push(x);
|
|
177
|
+
});
|
|
178
|
+
return out;
|
|
179
|
+
},
|
|
140
180
|
// std.str's `regex` (spec section 13's stdlib table) as a whole-purpose
|
|
141
181
|
// string-vs-pattern test -- the general capability the reconstructed
|
|
142
182
|
// `.split()`/`.len()`/indexing email-shape check (found needed by a
|
|
@@ -155,6 +195,7 @@
|
|
|
155
195
|
catch (e) { return false; }
|
|
156
196
|
}
|
|
157
197
|
};
|
|
198
|
+
// @vl-std-end
|
|
158
199
|
VL.len = function (x) { return x == null ? 0 : x.length; };
|
|
159
200
|
|
|
160
201
|
// A failed T! carries a stable `code` (spec section 6.3).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/* minified vlrt.js; readable source in the volaro package */
|
|
2
|
-
"use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var i=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function o(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function a(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){a(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function l(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function u(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function s(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function c(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function d(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function f(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function v(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),h(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function h(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return p(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=v(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function p(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var f=[],h=0;h<t._overlays.length;h++){var p=t._overlays[h];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){f.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?v(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?v(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return d(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<f.length;x++)f[x].rec.restoreFallback=A(f[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,i[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var a=r(e),l=[],u=Object.create(null),s=0;s<a.length;s++){var c=i(a[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(a[s],s);o(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=a,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},a(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=u(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||l()&&l().parentNode||null;a&&a.appendChild(o);var c=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:c&&c!==i.body?c:null}),1===t._overlays.length&&s(l(),!0),o},close:function(n){var r=u(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(s(l(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&c(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=d(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=u(n)<0,s=this.open(n);s.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[u(n)],h="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":h,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:h},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===s&&s.replaceChild){var y=v(c.panel,p);y!==c.panel&&(s.replaceChild(y,c.panel),c.panel=y)}else if(s.replaceChildren)s.replaceChildren(p),c.panel=p;else{for(;s.firstChild;)s.removeChild(s.firstChild);s.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=d(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},s.addEventListener&&s.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===s&&f(n)},s.addEventListener&&s.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||f(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var l="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:l,signal:l?l.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,l=e.document,u=l&&l.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!l.contains||l.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();a(f,i),r.replaceChild?h(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?h(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&l){var v=l.contains&&l.contains(c.node)?c.node:null;if(!v&&l.querySelectorAll){var p=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===p.length&&(v=p[0])}if(v&&l.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var y=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(y[n])return y[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return y[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
|
|
2
|
+
"use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e,t){if(e===t)return!0;if(null===e||null===t||"object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t))return!1;var n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(var r=0;r<n.length;r++)if(!Object.prototype.hasOwnProperty.call(t,n[r])||!i(e[n[r]],t[n[r]]))return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,o=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<o.length;r++)if(i(o[r],e))return;o.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var o=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function l(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){l(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function u(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function s(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function c(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function d(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function f(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function v(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function h(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),p(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function p(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return y(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=h(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function y(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],v=0;v<t._overlays.length;v++){var p=t._overlays[v];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){d.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?h(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?h(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return f(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<d.length;x++)d[x].rec.restoreFallback=A(d[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,o[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=l,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},l(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=s(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||u()&&u().parentNode||null;a&&a.appendChild(o);var l=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:l&&l!==i.body?l:null}),1===t._overlays.length&&c(u(),!0),o},close:function(n){var r=s(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(c(u(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&d(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=f(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=s(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[s(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===u&&u.replaceChild){var y=h(c.panel,p);y!==c.panel&&(u.replaceChild(y,c.panel),c.panel=y)}else if(u.replaceChildren)u.replaceChildren(p),c.panel=p;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=f(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===u&&v(n)},u.addEventListener&&u.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||v(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var a="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:a,signal:a?a.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,a=e.document,u=a&&a.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!a.contains||a.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();l(f,i),r.replaceChild?p(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?p(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&a){var v=a.contains&&a.contains(c.node)?c.node:null;if(!v&&a.querySelectorAll){var h=Array.prototype.filter.call(a.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&a.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var g=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(g[n])return g[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return g[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
|
|
@@ -59,7 +59,7 @@ from vlcheck.elements import (
|
|
|
59
59
|
SPELLCHECK_HOMES, TABINDEX_ALLOWED, TEXT_STYLE_ATTRS,
|
|
60
60
|
UNSAFE_ELEMENTS,
|
|
61
61
|
)
|
|
62
|
-
from vlcheck.checks import _lit_int, _lit_text, _rendered_children
|
|
62
|
+
from vlcheck.checks import STD_BUILT_FNS, _lit_int, _lit_text, _rendered_children
|
|
63
63
|
from vlcheck.lexer import Lexer
|
|
64
64
|
from vlcheck.parser import Parser
|
|
65
65
|
from vlcheck.resolve import (_AUTH_API, AUTH_BUILT, _route_name,
|
|
@@ -117,44 +117,16 @@ _ATTR_TARGETS = {
|
|
|
117
117
|
"alt": frozenset({"img", "area"}),
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
# std free functions that map straight onto the runtime's VL.std
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
# `xs |> append(x)`.
|
|
131
|
-
"concat", "append",
|
|
132
|
-
# std.str's `regex` (spec section 13): a whole-purpose string-vs-pattern
|
|
133
|
-
# test, `matches(s, pattern)` -- see `VL.std.matches` (assets/vlrt.js)
|
|
134
|
-
# for the exact semantics (never throws; caller supplies its own
|
|
135
|
-
# anchors). The smallest general capability that permits exact
|
|
136
|
-
# validation against an arbitrary frozen pattern, found needed when a
|
|
137
|
-
# hand-rolled `.split()`-based email-shape check only caught a literal
|
|
138
|
-
# space, not the full whitespace class a real pattern's `\s` covers.
|
|
139
|
-
"matches",
|
|
140
|
-
# std.str's `replace(s, target, replacement)` (spec section 13 already
|
|
141
|
-
# names it as part of std.str's row; this build had not implemented it
|
|
142
|
-
# until feature 17 (export) needed literal-substring replace-all for
|
|
143
|
-
# RFC 4180 CSV field escaping -- see checks.py's matching entry for the
|
|
144
|
-
# full reproduce-then-fix note. Keep in sync with checks.py,
|
|
145
|
-
# assets/vlrt.js, and server_emit.py's VL.std shim.
|
|
146
|
-
"replace",
|
|
147
|
-
# std.str's `contains(s, part) bool`: case-sensitive substring test (an
|
|
148
|
-
# empty `part` is true). Before this, a view had no way to ask "does
|
|
149
|
-
# this text contain that text": `contains(...)` existed only as a
|
|
150
|
-
# `where()` predicate, so a bare `contains(a, b)` in a derive built
|
|
151
|
-
# clean and threw `ReferenceError: contains is not defined` (Testing
|
|
152
|
-
# Lab search brief, Example Demo 2.3). A `where(col: contains(x))`
|
|
153
|
-
# value never reaches this table: `_where_value_js` lowers predicate
|
|
154
|
-
# calls to their `{op, v}` marker before generic call lowering. Keep in
|
|
155
|
-
# sync with checks.py, assets/vlrt.js and server_emit.py's VL.std shim.
|
|
156
|
-
"contains",
|
|
157
|
-
}
|
|
120
|
+
# std free functions that map straight onto the runtime's VL.std.*. The set is
|
|
121
|
+
# vlcheck's `STD_BUILT_FNS`, imported rather than copied, so the checker can
|
|
122
|
+
# never accept a std call this emitter does not lower. Add a function there
|
|
123
|
+
# and to `VL.std` in assets/vlrt.js (server.js embeds the same block); the
|
|
124
|
+
# parity test in tests/test_build.py (`test_std_fn_parity`) fails until both
|
|
125
|
+
# exist. Per-function history (why `matches`, `replace`, `contains`,
|
|
126
|
+
# `concat`/`append` were added) is in `git log -S` on this file. A
|
|
127
|
+
# `where(col: contains(x))` value never reaches this table: `_where_value_js`
|
|
128
|
+
# lowers predicate calls to their `{op, v}` marker first.
|
|
129
|
+
STD_FNS = STD_BUILT_FNS
|
|
158
130
|
|
|
159
131
|
# attr name -> CSS property, for attrs whose value is a length in px
|
|
160
132
|
PX_KEYS = {
|
|
@@ -460,6 +432,7 @@ class Emitter:
|
|
|
460
432
|
self.page: A.PageDecl | None = None # spec §8.11 `page title:` declaration
|
|
461
433
|
self.service: A.ServiceDecl | None = None
|
|
462
434
|
self.server_mode = False # a model backs a service route
|
|
435
|
+
self._route_depth = 0 # >0 inside route_block (a route body)
|
|
463
436
|
self.state_names: set[str] = set()
|
|
464
437
|
self.load_names: set[str] = set()
|
|
465
438
|
self._counter = 0
|
|
@@ -3553,9 +3526,11 @@ class Emitter:
|
|
|
3553
3526
|
Assignment is plain: a route has no view `state`, so `_emit_assign`
|
|
3554
3527
|
(which turns a state name into `ctx.setState`) must not be used."""
|
|
3555
3528
|
self._push_scope()
|
|
3529
|
+
self._route_depth += 1
|
|
3556
3530
|
try:
|
|
3557
3531
|
return self._route_block(stmts, decl, in_loop, tail)
|
|
3558
3532
|
finally:
|
|
3533
|
+
self._route_depth -= 1
|
|
3559
3534
|
self._pop_scope()
|
|
3560
3535
|
|
|
3561
3536
|
def _route_block(self, stmts, decl: str, in_loop: bool, tail: bool) -> list[str]:
|
|
@@ -3693,6 +3668,11 @@ class Emitter:
|
|
|
3693
3668
|
return segments
|
|
3694
3669
|
|
|
3695
3670
|
def _parse_sub_expr(self, text: str):
|
|
3671
|
+
# The `{expr}` text is still inside the outer string literal, so a
|
|
3672
|
+
# string argument in it is written with escaped quotes:
|
|
3673
|
+
# `"{join(cats, \",\")}"`. Undo that one level (`\"` and `\\`)
|
|
3674
|
+
# before lexing; lexed raw, `\",\"` became the separator `",")`.
|
|
3675
|
+
text = re.sub(r'\\(["\\])', r"\1", text)
|
|
3696
3676
|
toks = Lexer(text, self.filename).lex()
|
|
3697
3677
|
return Parser(toks, text, self.filename).parse_expr()
|
|
3698
3678
|
|
|
@@ -3866,8 +3846,13 @@ class Emitter:
|
|
|
3866
3846
|
if isinstance(callee, A.Name) and callee.id in STD_FNS:
|
|
3867
3847
|
return "VL.std.%s(%s)" % (callee.id, ", ".join(self.expr(a) for a in e.args))
|
|
3868
3848
|
|
|
3869
|
-
|
|
3870
|
-
|
|
3849
|
+
# `err(...)` in a route body, on either target: the real server, or
|
|
3850
|
+
# the browser stub a service with no backing model runs as (there it
|
|
3851
|
+
# was emitted as a bare `err(...)` call that threw ReferenceError
|
|
3852
|
+
# instead of failing the call). vlcheck's E-ERR-CONTEXT rejects it
|
|
3853
|
+
# in a view.
|
|
3854
|
+
if ((self.server_mode or self._route_depth) and isinstance(callee, A.Name)
|
|
3855
|
+
and callee.id == "err" and len(e.args) == 1 and not e.named):
|
|
3871
3856
|
return self._ex_ServerErr(e.args[0])
|
|
3872
3857
|
|
|
3873
3858
|
args = [self.expr(a) for a in e.args]
|
|
@@ -37,6 +37,7 @@ from __future__ import annotations
|
|
|
37
37
|
|
|
38
38
|
import json
|
|
39
39
|
import re
|
|
40
|
+
from pathlib import Path
|
|
40
41
|
|
|
41
42
|
from vlcheck import ast_nodes as A
|
|
42
43
|
|
|
@@ -124,6 +125,25 @@ def _sql_default(node):
|
|
|
124
125
|
return None
|
|
125
126
|
|
|
126
127
|
|
|
128
|
+
_VLRT = Path(__file__).resolve().parent / "assets" / "vlrt.js"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def vl_std_js() -> str:
|
|
132
|
+
"""The browser runtime's `VL.std` block (assets/vlrt.js, the lines between
|
|
133
|
+
its `// @vl-std-begin` and `// @vl-std-end` marker lines), for server.js.
|
|
134
|
+
One source for both targets, so a std function a view can call is never
|
|
135
|
+
missing from a route body. A missing or duplicated marker is a broken
|
|
136
|
+
compiler install, not a user error."""
|
|
137
|
+
lines = _VLRT.read_text(encoding="utf-8").splitlines()
|
|
138
|
+
marks = [i for i, ln in enumerate(lines)
|
|
139
|
+
if ln.strip() in ("// @vl-std-begin", "// @vl-std-end")]
|
|
140
|
+
if (len(marks) != 2 or lines[marks[0]].strip() != "// @vl-std-begin"
|
|
141
|
+
or lines[marks[1]].strip() != "// @vl-std-end"):
|
|
142
|
+
raise BuildError("internal: assets/vlrt.js must have exactly one "
|
|
143
|
+
"`// @vl-std-begin` / `// @vl-std-end` pair")
|
|
144
|
+
return "\n".join(lines[marks[0] + 1:marks[1]])
|
|
145
|
+
|
|
146
|
+
|
|
127
147
|
class ServerEmitter:
|
|
128
148
|
def __init__(self, module: A.Module, models: dict, service: A.ServiceDecl | None,
|
|
129
149
|
filename: str = "<input>", auth: A.AuthDecl | None = None,
|
|
@@ -801,6 +821,7 @@ class ServerEmitter:
|
|
|
801
821
|
self._auth_session_setting("idle_timeout"), 12 * 3600),
|
|
802
822
|
"session_same_site": "Lax" if oauth_declared else "Strict",
|
|
803
823
|
"spa_fallback": "true" if self.spa_fallback else "false",
|
|
824
|
+
"vl_std": vl_std_js(),
|
|
804
825
|
}
|
|
805
826
|
|
|
806
827
|
|
|
@@ -1300,76 +1321,22 @@ function readBody(req) {
|
|
|
1300
1321
|
function err(code, message) { const e = new Error(message || code); e.code = code; return e; }
|
|
1301
1322
|
|
|
1302
1323
|
// Route bodies are lowered by the same Emitter as the browser target (see
|
|
1303
|
-
// module docstring), so a
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
//
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1324
|
+
// module docstring), so a route body emits the same `VL.len(...)` and
|
|
1325
|
+
// `VL.std.<name>(...)` calls a view does -- but server.js is a standalone
|
|
1326
|
+
// Node script with no VL runtime loaded. `VL.std` below is the browser
|
|
1327
|
+
// runtime's own block (assets/vlrt.js, between its @vl-std-begin/end
|
|
1328
|
+
// markers), copied in at build time, so every std function a view can call
|
|
1329
|
+
// exists here too. It used to be a hand-ported subset that fell behind
|
|
1330
|
+
// `STD_FNS` one function at a time (`matches`, `split`, `filter`, `count`,
|
|
1331
|
+
// `replace`, `contains`, `lower`, `trim`, `map`, `concat` were each found
|
|
1332
|
+
// missing by a route that called them).
|
|
1312
1333
|
const VL = {
|
|
1313
1334
|
len: (x) => (x == null ? 0 : x.length),
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
// pre-existing gap this file's own comment above already names ("add the
|
|
1320
|
-
// next VL.* function here only when a feature actually calls it
|
|
1321
|
-
// server-side"); ported verbatim from vlrt.js's `VL.std.matches` (never
|
|
1322
|
-
// throws -- a malformed pattern fails closed, same contract both
|
|
1323
|
-
// targets share).
|
|
1324
|
-
std: {
|
|
1325
|
-
matches: (s, pattern) => {
|
|
1326
|
-
try { return new RegExp(String(pattern)).test(String(s)); }
|
|
1327
|
-
catch (e) { return false; }
|
|
1328
|
-
},
|
|
1329
|
-
// action-43: a route body splitting a compound (created_at, id) cursor
|
|
1330
|
-
// string (`cursor |> split("_")`) hit the same class of gap `matches`
|
|
1331
|
-
// did above -- ported verbatim from vlrt.js's `VL.std.split`.
|
|
1332
|
-
split: (s, sep) => String(s).split(sep),
|
|
1333
|
-
// feature 16 (bulk operation): a route body computing `missing_ids`
|
|
1334
|
-
// (which requested ids a scoped update actually matched) hit the same
|
|
1335
|
-
// class of gap `matches`/`split` did above -- `VL.std.filter`/
|
|
1336
|
-
// `VL.std.count` compile clean but threw `TypeError: VL.std.filter is
|
|
1337
|
-
// not a function` at request time (a generic try/catch upstream
|
|
1338
|
-
// flattened it to an opaque 500 "internal error", reproduced directly
|
|
1339
|
-
// against the unmodified shim before this fix). Ported verbatim from
|
|
1340
|
-
// vlrt.js's own `filter`/`count` (same null-safe `asArray` contract).
|
|
1341
|
-
filter: (xs, f) => (xs == null ? [] : xs).filter((x) => f(x)),
|
|
1342
|
-
count: (xs) => (xs == null ? [] : xs).length,
|
|
1343
|
-
// feature 17 (export): a route body building a CSV line needs
|
|
1344
|
-
// literal-substring replace-ALL to double an embedded '"' per RFC
|
|
1345
|
-
// 4180 -- same pre-existing shim gap `matches`/`split`/`filter`/
|
|
1346
|
-
// `count` above were each added for in turn. Ported verbatim from
|
|
1347
|
-
// vlrt.js's `VL.std.replace` (split+join, not regex).
|
|
1348
|
-
replace: (s, target, repl) => String(s).split(String(target)).join(String(repl)),
|
|
1349
|
-
// std.str's substring test, ported verbatim from vlrt.js's
|
|
1350
|
-
// `VL.std.contains`. A `where(col: contains(x))` value is the SQL LIKE
|
|
1351
|
-
// predicate instead and never calls this.
|
|
1352
|
-
contains: (s, part) => String(s).indexOf(String(part)) !== -1,
|
|
1353
|
-
// The case-insensitive search the crib teaches
|
|
1354
|
-
// (`contains(lower(a.title), lower(trim(q)))`) threw `VL.std.lower is
|
|
1355
|
-
// not a function` in a route body. Ported verbatim from vlrt.js.
|
|
1356
|
-
lower: (s) => String(s).toLowerCase(),
|
|
1357
|
-
trim: (s) => String(s).trim(),
|
|
1358
|
-
// feature 17 (export): the same route body maps each matched Article
|
|
1359
|
-
// to its own escaped CSV line (`matching |> map(fn(a) ...)`) -- built
|
|
1360
|
-
// clean (vlcheck/precheck do not check the shim's contents, same gap
|
|
1361
|
-
// class as every entry above), then threw `TypeError: VL.std.map is
|
|
1362
|
-
// not a function` at request time on the real running server,
|
|
1363
|
-
// reproduced directly before this fix. Ported verbatim from vlrt.js's
|
|
1364
|
-
// own `map` (same null-safe `asArray` contract as `filter`).
|
|
1365
|
-
map: (xs, f) => (xs == null ? [] : xs).map((x) => f(x)),
|
|
1366
|
-
// feature 17 (export): prepending the CSV header line to the mapped
|
|
1367
|
-
// data rows (`concat([header], data_rows)`) hit the same shim gap --
|
|
1368
|
-
// reproduced directly (`TypeError: VL.std.concat is not a function`)
|
|
1369
|
-
// before this entry existed. Ported verbatim from vlrt.js's own
|
|
1370
|
-
// `concat`.
|
|
1371
|
-
concat: (a, b) => (a == null ? [] : a).concat(b == null ? [] : b),
|
|
1372
|
-
},
|
|
1335
|
+
std: (function () {
|
|
1336
|
+
const VL = {};
|
|
1337
|
+
%(vl_std)s
|
|
1338
|
+
return VL.std;
|
|
1339
|
+
})(),
|
|
1373
1340
|
};
|
|
1374
1341
|
|
|
1375
1342
|
// Coerce one `?name=raw` query-string value or one decoded path segment to
|
package/language/spec.md
CHANGED
|
@@ -1702,7 +1702,7 @@ The library is small on purpose. A large surface is a large thing to remember an
|
|
|
1702
1702
|
| `std.db` | query builder, migrations, transactions |
|
|
1703
1703
|
| `std.time` | `now`, formatting, parsing, arithmetic on `time` and `dur` |
|
|
1704
1704
|
| `std.str` | split, join, trim, case, replace, contains (`contains(s, part) bool` — case-sensitive substring test, an empty `part` is `true`; distinct from the §13.1 `where()` predicate of the same name), regex (`matches(s, pattern) bool` — an unanchored whole-`RegExp`-source test, never throws; §6.2's model of "the compiler-owned surface" — see `volaro supported` for exactly what this build ships of this row) |
|
|
1705
|
-
| `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append |
|
|
1705
|
+
| `std.list` | map, filter, fold, sort_by, group_by, take, chunk, concat, append, unique (`unique(xs) [T]` — the first occurrence of each distinct value, in list order; values compare by §4.3 equality, so two records are the same when their fields are equal) |
|
|
1706
1706
|
| `std.map` | keys, values, merge, get_or |
|
|
1707
1707
|
| `std.json` | encode, decode, pointer access |
|
|
1708
1708
|
| `std.crypto` | random bytes, hashing, timing-safe compare |
|
package/language/supported.md
CHANGED
|
@@ -422,9 +422,35 @@ audio video source track picture map area
|
|
|
422
422
|
bare call is `E-METHOD-AS-FN`. `reload()` and `loading()` act on a
|
|
423
423
|
`load`, so they are valid only on a name the view's own `load` binds
|
|
424
424
|
(`E-LOAD-METHOD`); `loading()` is true while its fetch is in flight.
|
|
425
|
-
The
|
|
426
|
-
|
|
427
|
-
`
|
|
425
|
+
The std free functions this build has are the ones listed under
|
|
426
|
+
"Data / services" below (`concat`, `append`, `unique`, `matches`,
|
|
427
|
+
`contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
|
|
428
|
+
`sort_by`, `group_by`, `chunk`, `join`, `split`, `first_char`, `trim`,
|
|
429
|
+
`upper`, `lower`, `keys`, `values`, `get_or`, `int_to_float` and
|
|
430
|
+
`float_to_int`. Any other std name (`sorted`, `first`, `fold`, `merge`,
|
|
431
|
+
`to_int`, `now()` outside a `model` default, `reverse`, `min`, `max`,
|
|
432
|
+
`starts_with`, …) is `E-FN-NOT-BUILT` in a view or route, and its
|
|
433
|
+
`fix:` line gives the working form.
|
|
434
|
+
- Other names the checker knows are callable only in one position, or not
|
|
435
|
+
at all. Each rejection has a `fix:` line.
|
|
436
|
+
- `where()` predicates (`lt`, `lte`, `gt`, `gte`, `ne`, `between`,
|
|
437
|
+
`any_of`) are only `.where(field: gt(x))` values, including either
|
|
438
|
+
branch of an `if` value. Anywhere else, `gt(x)` is
|
|
439
|
+
`E-WHERE-PREDICATE`; compare with the operator (`a > b`).
|
|
440
|
+
`contains` is also a std.str function, so it works everywhere.
|
|
441
|
+
- `err(...)` works only in a `service` or page.server route body. In a
|
|
442
|
+
view it is `E-ERR-CONTEXT`. An error code (`not_found`, `forbidden`,
|
|
443
|
+
`conflict`, …) or a payload variant of a union the file declares is
|
|
444
|
+
only the argument of `err(...)`. `not_found("x")` anywhere else is
|
|
445
|
+
`E-ERR-CONTEXT`. A service with no `model` runs its routes in the
|
|
446
|
+
browser, and `err(...)` fails the call there too.
|
|
447
|
+
- A namespace or ambient object (`db`, `env`, `log`, `route`, `auth`,
|
|
448
|
+
`session`, `request`, `api`, and the unbuilt `analytics`, `events`,
|
|
449
|
+
`ws`, `mailer`, `billing`), a type name (`str(x)`, `int(x)`, …), `ok`,
|
|
450
|
+
`asc`/`desc`, `google`/`github`, the spec example names `create_guest`
|
|
451
|
+
and `guest_user`, and an element or view used as a value
|
|
452
|
+
(`derive y = span(xs)`, `derive r = Row(1)`) are `E-NOT-A-FUNCTION`. A successful `T!` is the plain
|
|
453
|
+
value, so write `xs`, not `ok(xs)`. `str(x)` is `"{x}"`.
|
|
428
454
|
- `len`, `format`, `reload` and `loading` are the only methods a value
|
|
429
455
|
has. String and list operations are
|
|
430
456
|
std functions: `lower(x)`, `trim(x)`, `xs |> filter(f)`,
|
|
@@ -481,6 +507,13 @@ audio video source track picture map area
|
|
|
481
507
|
- `std.list` `concat` / `append`; `xs |> map|filter(fn(x) …)`. (`fold` is in
|
|
482
508
|
`volaro spec`'s stdlib table but is **not implemented** in this build —
|
|
483
509
|
found stale while documenting `matches` below; corrected here, not fixed.)
|
|
510
|
+
- `std.list` `unique(xs)` — the first occurrence of each distinct value, in
|
|
511
|
+
list order, as a new list. Scalars compare as `==` does (`1` and `"1"`
|
|
512
|
+
differ); records and lists compare by their fields and items. Works in
|
|
513
|
+
views, handlers and route bodies, and through the pipeline:
|
|
514
|
+
```vl
|
|
515
|
+
derive cats = pub |> map(fn(a) a.category) |> unique()
|
|
516
|
+
```
|
|
484
517
|
- `std.str` `matches(s, pattern)` — a whole-string-vs-regex test, `std.str`'s
|
|
485
518
|
`regex` entry in `volaro spec`'s stdlib table (section 13). `pattern` is a
|
|
486
519
|
plain regex source string, no delimiters, no flags; the caller supplies
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volaro",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.12",
|
|
4
4
|
"description": "Volaro \u2014 an application language written to be authored by an AI agent and read by a person. Ships the language reference and a working compiler for a supported subset (volaro check / build / dev). CLI: `volaro` (alias `vl`).",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|