volaro 0.1.0-alpha.12 → 0.1.0-alpha.13
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/__main__.py +10 -6
- package/compiler/validator/vlcheck/checks.py +20 -5
- package/compiler/validator/vlcheck/resolve.py +138 -6
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +37 -10
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.js +1 -1
- package/compiler/vlbuild/vlbuild/emit.py +168 -15
- package/compiler/vlbuild/vlbuild/project.py +4 -2
- package/compiler/vlbuild/vlbuild/server_emit.py +7 -8
- package/language/crib.md +9 -1
- package/language/spec.md +1 -1
- package/language/supported.md +38 -4
- 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.13`), 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": "d6e19b4e656bee01bf90048117fb24b598abdf3b",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "64c762006f1fceda3601d6c6b3a6de4ba99590d93aa14d04a749f318a840aa17"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
d6e19b4e656bee01bf90048117fb24b598abdf3b
|
|
@@ -24,7 +24,7 @@ from .checks import check
|
|
|
24
24
|
from .diagnostics import LineIndex, Severity
|
|
25
25
|
from .lexer import Lexer
|
|
26
26
|
from .parser import Parser
|
|
27
|
-
from .resolve import resolve
|
|
27
|
+
from .resolve import resolve, resolve_page_server
|
|
28
28
|
from .project_config import load_config, ConfigError
|
|
29
29
|
from .diagnostics import Diagnostic
|
|
30
30
|
from .layoutcompose import check_all as check_project_composition
|
|
@@ -59,21 +59,25 @@ def check_source(src: str, filename: str, strict_names: bool = False,
|
|
|
59
59
|
# (spec §10.2): handlers and an `errors` block, no module items. Every
|
|
60
60
|
# other basename parses as an ordinary module.
|
|
61
61
|
base = os.path.basename(filename)
|
|
62
|
+
resolver = resolve
|
|
62
63
|
if base in ("page.server.vl", "_server.vl"):
|
|
63
64
|
if base == "page.server.vl":
|
|
64
65
|
mod = parser.parse_page_server()
|
|
66
|
+
# Handler bodies resolve like service route bodies, against the
|
|
67
|
+
# project's app.vl, the handler's params and the `_server.vl`
|
|
68
|
+
# context providers above the file (resolve.resolve_page_server).
|
|
69
|
+
resolver = resolve_page_server
|
|
65
70
|
else:
|
|
66
71
|
mod = parser.parse_server_ctx()
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
do_resolve = False
|
|
72
|
+
# A `_server.vl` holds only `context` providers; the resolver
|
|
73
|
+
# has no pass for them yet.
|
|
74
|
+
do_resolve = False
|
|
71
75
|
else:
|
|
72
76
|
mod = parser.parse_module()
|
|
73
77
|
diags = (list(lx.diags) + list(parser.diags)
|
|
74
78
|
+ check(mod, filename, li, strict_names=strict_names, release=release))
|
|
75
79
|
if do_resolve:
|
|
76
|
-
diags +=
|
|
80
|
+
diags += resolver(mod, filename, li)
|
|
77
81
|
diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
|
|
78
82
|
return li, diags
|
|
79
83
|
|
|
@@ -105,12 +105,13 @@ _METHOD_FN_FORM = {
|
|
|
105
105
|
"matches": "matches", "match": "matches", "test": "matches",
|
|
106
106
|
"first_char": "first_char", "split": "split", "join": "join",
|
|
107
107
|
"filter": "filter", "map": "map", "take": "take", "count": "count",
|
|
108
|
+
"some": "any", "every": "all",
|
|
108
109
|
"sum": "sum", "sort_by": "sort_by", "group_by": "group_by",
|
|
109
110
|
"chunk": "chunk", "concat": "concat", "append": "append", "push": "append",
|
|
110
111
|
"keys": "keys", "values": "values", "get_or": "get_or",
|
|
111
112
|
}
|
|
112
113
|
_LIST_FNS = {"filter", "map", "take", "count", "sum", "sort_by", "group_by",
|
|
113
|
-
"chunk", "concat", "append", "join"}
|
|
114
|
+
"chunk", "concat", "append", "join", "any", "all"}
|
|
114
115
|
# Other common carry-overs with a Volaro spelling that is not a function call.
|
|
115
116
|
_METHOD_OTHER_FIX = {
|
|
116
117
|
"length": "`{r}.len()`", "size": "`{r}.len()`",
|
|
@@ -153,6 +154,8 @@ STD_BUILT_FNS = frozenset({
|
|
|
153
154
|
"concat", "append",
|
|
154
155
|
# `unique(xs)`: first occurrence of each distinct value, in order
|
|
155
156
|
"unique",
|
|
157
|
+
# `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`
|
|
158
|
+
"any", "all",
|
|
156
159
|
# std.str (spec section 13: split, join, trim, case, replace, contains,
|
|
157
160
|
# regex -- `matches`)
|
|
158
161
|
"join", "split", "first_char", "trim", "upper", "lower", "replace",
|
|
@@ -182,9 +185,7 @@ _FN_NOT_BUILT = {
|
|
|
182
185
|
"index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
|
|
183
186
|
"slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
|
|
184
187
|
"range": "a `for i in 0..n` loop",
|
|
185
|
-
"
|
|
186
|
-
"all": "count(filter(xs, f)) == count(xs)",
|
|
187
|
-
"none": "count(filter(xs, f)) == 0",
|
|
188
|
+
"none": "not any(xs, f)",
|
|
188
189
|
"avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
|
|
189
190
|
"min": "if (a < b) a else b",
|
|
190
191
|
"max": "if (a > b) a else b",
|
|
@@ -235,7 +236,7 @@ CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
|
|
|
235
236
|
_WHERE_PREDICATE_FIX = {
|
|
236
237
|
"lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
|
|
237
238
|
"ne": "`a != b`", "between": "`lo <= x and x <= hi`",
|
|
238
|
-
"any_of": "`
|
|
239
|
+
"any_of": "`any(xs, fn(v) v == x)`",
|
|
239
240
|
}
|
|
240
241
|
# Error codes (spec section 6): a code is constructed only as the argument of
|
|
241
242
|
# `err(...)` in a service or page-server route body, where the build turns
|
|
@@ -246,6 +247,20 @@ _ERROR_CODES = frozenset({
|
|
|
246
247
|
"out_of_range", "decode", "invalid_credentials", "unverified_email",
|
|
247
248
|
"totp_required", "rate_limited",
|
|
248
249
|
})
|
|
250
|
+
# The codes the compiler-owned auth endpoints fail with (`catch
|
|
251
|
+
# weak_password`); vlbuild's emitter reads this set.
|
|
252
|
+
AUTH_ERROR_CODES = frozenset({
|
|
253
|
+
"email_taken", "weak_password", "breached_password", "rate_limited",
|
|
254
|
+
"invalid_credentials", "unverified_email", "totp_required",
|
|
255
|
+
"provider_error", "invalid_token", "expired_token", "invalid_code",
|
|
256
|
+
"no_totp_enrolled",
|
|
257
|
+
})
|
|
258
|
+
# A route body's `catch <head>` tests the thrown error's code when <head> is
|
|
259
|
+
# one of these (or a declared union variant); any other head binds the
|
|
260
|
+
# caught error itself (`catch e`). resolve.py and vlbuild's `_route_try`
|
|
261
|
+
# both read this set, so `err(e)` means the same thing to check and build.
|
|
262
|
+
ROUTE_CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
|
|
263
|
+
| frozenset({"bad_request", "unauthorized"}))
|
|
249
264
|
_ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
|
|
250
265
|
"page.server route body; a view reports a failure by "
|
|
251
266
|
"setting its own state")
|
|
@@ -50,11 +50,12 @@ import os
|
|
|
50
50
|
import re
|
|
51
51
|
|
|
52
52
|
from . import ast_nodes as A
|
|
53
|
-
from .checks import (BUILTINS, CONTEXTUAL_WORDS,
|
|
54
|
-
_PRIM_NAMES)
|
|
53
|
+
from .checks import (BUILTINS, CONTEXTUAL_WORDS, ROUTE_CATCH_CODES,
|
|
54
|
+
STD_BUILT_FNS, _ERROR_CODES, _FN_NOT_BUILT, _PRIM_NAMES)
|
|
55
55
|
from .diagnostics import Diagnostic, LineIndex, Severity
|
|
56
56
|
from .elements import ELEMENT_NAMES
|
|
57
57
|
from .lexer import Lexer
|
|
58
|
+
from .pageroutes import _provider_chain
|
|
58
59
|
from .parser import Parser
|
|
59
60
|
|
|
60
61
|
# std free-function names: the ones this build lowers and the ones it rejects
|
|
@@ -190,6 +191,13 @@ _METHODS = {
|
|
|
190
191
|
_KNOWN = (set(BUILTINS) | set(_PRIM_NAMES) | _STD_EXTRA | _CONTEXTUAL
|
|
191
192
|
| {"true", "false", "nil", "self", "_", "?"})
|
|
192
193
|
|
|
194
|
+
# The type-info a `catch e` binding carries: the caught error itself, the
|
|
195
|
+
# one bare name `err(...)` accepts in a route body (`err(e)` rethrows it).
|
|
196
|
+
_CAUGHT = ("!caught", False)
|
|
197
|
+
_ERR_ARG_FIX = ('an error code call `err(conflict("..."))`, a variant of an '
|
|
198
|
+
'error union you declare `err(invalid_field("title"))`, or '
|
|
199
|
+
'the error a `catch e` arm caught: `err(e)`')
|
|
200
|
+
|
|
193
201
|
|
|
194
202
|
class Sym:
|
|
195
203
|
__slots__ = ("kind", "params", "fields", "variants", "ret", "routes",
|
|
@@ -323,6 +331,9 @@ class Resolver:
|
|
|
323
331
|
self._in_view = False
|
|
324
332
|
self._view_name = ""
|
|
325
333
|
self._vmf_seen: set[str] = set()
|
|
334
|
+
# True while walking a service route or page.server handler body,
|
|
335
|
+
# the only places the build lowers `err(...)` (E-ERR-ARG).
|
|
336
|
+
self._in_route = False
|
|
326
337
|
# Spec §7: `_name` is private to its file. A bare `use ./x` does not
|
|
327
338
|
# bind x's `_` names; remember which module declared each one so a
|
|
328
339
|
# use of it reports E-IMPORT-PRIVATE instead of a bare R-UNDEF.
|
|
@@ -534,7 +545,47 @@ class Resolver:
|
|
|
534
545
|
for seg in r.path.split("/"):
|
|
535
546
|
if seg.startswith(":"):
|
|
536
547
|
sc[seg[1:]] = None
|
|
548
|
+
self._in_route = True
|
|
537
549
|
self._scoped(r.body, [sc])
|
|
550
|
+
self._in_route = False
|
|
551
|
+
|
|
552
|
+
def run_page_server(self, mod: A.PageServerFile) -> list[Diagnostic]:
|
|
553
|
+
"""Resolve a `page.server.vl` file (spec section 10.2) the way a
|
|
554
|
+
service route body is resolved. Before, `check_source` skipped the
|
|
555
|
+
resolver for these files, so an undefined name, a wrong arity or an
|
|
556
|
+
unknown field in a handler passed `volaro check`.
|
|
557
|
+
|
|
558
|
+
In scope: the names the build gives the handler -- the project's
|
|
559
|
+
`app.vl` declarations when it has one (its models, types, unions and
|
|
560
|
+
`api` clients, which the single-entry project build lowers the
|
|
561
|
+
handlers against), the handler's own parameters, `session`,
|
|
562
|
+
`request`, and every `_server.vl` context provider on the file's
|
|
563
|
+
directory path."""
|
|
564
|
+
root = _project_root(self.filename)
|
|
565
|
+
providers = []
|
|
566
|
+
if root is not None:
|
|
567
|
+
entry = os.path.join(root, "app.vl")
|
|
568
|
+
if os.path.isfile(entry):
|
|
569
|
+
self.table.update(_collect_module(entry, self._loading))
|
|
570
|
+
# its parse diagnostics belong to the _server.vl files, which
|
|
571
|
+
# `volaro check` reports on their own
|
|
572
|
+
providers, _diags = _provider_chain(
|
|
573
|
+
os.path.join(root, "app"),
|
|
574
|
+
os.path.dirname(os.path.abspath(self.filename)))
|
|
575
|
+
for h in mod.handlers:
|
|
576
|
+
sc = {p.name: _tref(p.type)[:2] for p in h.params}
|
|
577
|
+
sc["session"] = None
|
|
578
|
+
sc["request"] = None
|
|
579
|
+
for prov in providers:
|
|
580
|
+
sc.setdefault(prov.name, _tref(prov.type)[:2])
|
|
581
|
+
guard = getattr(h, "guard", None)
|
|
582
|
+
if guard is not None and guard.kind == "owner" and isinstance(
|
|
583
|
+
guard.detail, A.Node):
|
|
584
|
+
self._expr(guard.detail, [sc])
|
|
585
|
+
self._in_route = True
|
|
586
|
+
self._scoped(h.body, [sc])
|
|
587
|
+
self._in_route = False
|
|
588
|
+
return self.diags
|
|
538
589
|
|
|
539
590
|
def _load_type(self, s: A.LoadDecl, scopes):
|
|
540
591
|
if isinstance(s.expr, A.Call):
|
|
@@ -603,8 +654,7 @@ class Resolver:
|
|
|
603
654
|
elif isinstance(st, A.Try):
|
|
604
655
|
self._expr(st.expr, scopes)
|
|
605
656
|
for pat, body in st.catches:
|
|
606
|
-
|
|
607
|
-
self._scoped(body, inner)
|
|
657
|
+
self._scoped(body, scopes + [self._catch_scope(pat)])
|
|
608
658
|
elif isinstance(st, (A.Break, A.Skip)):
|
|
609
659
|
pass
|
|
610
660
|
elif isinstance(st, A.StateDecl):
|
|
@@ -729,6 +779,7 @@ class Resolver:
|
|
|
729
779
|
self._expr(v, scopes)
|
|
730
780
|
self._check_call(ex, scopes)
|
|
731
781
|
self._check_view_module_fn(ex, scopes)
|
|
782
|
+
self._check_err_arg(ex, scopes)
|
|
732
783
|
elif isinstance(ex, A.Unary):
|
|
733
784
|
self._expr(ex.operand, scopes)
|
|
734
785
|
elif isinstance(ex, A.Binary):
|
|
@@ -751,8 +802,7 @@ class Resolver:
|
|
|
751
802
|
# `vlbuild` refused it (Testing Lab 2026-09-27, volaro-F2).
|
|
752
803
|
self._expr(ex.expr, scopes)
|
|
753
804
|
for pat, body in ex.catches:
|
|
754
|
-
|
|
755
|
-
self._scoped(body, inner)
|
|
805
|
+
self._scoped(body, scopes + [self._catch_scope(pat)])
|
|
756
806
|
elif isinstance(ex, A.IfExpr):
|
|
757
807
|
self._expr(ex.cond, scopes)
|
|
758
808
|
self._expr(ex.then, scopes)
|
|
@@ -999,6 +1049,69 @@ class Resolver:
|
|
|
999
1049
|
self._arity(Sym("fn", params=[(n, False, not req) for n, req in params]),
|
|
1000
1050
|
ex, f"'auth.{m}'")
|
|
1001
1051
|
|
|
1052
|
+
def _catch_scope(self, pat) -> dict:
|
|
1053
|
+
"""The bindings a `catch` arm adds. An arm headed by an error code or
|
|
1054
|
+
a declared union variant binds the variant's fields; any other head
|
|
1055
|
+
(`catch e`) binds the caught error itself, marked `_CAUGHT` so
|
|
1056
|
+
`err(e)` can rethrow it."""
|
|
1057
|
+
head = getattr(pat, "head", "")
|
|
1058
|
+
binds = list(getattr(pat, "binds", []) or [])
|
|
1059
|
+
sym = self.table.get(head)
|
|
1060
|
+
if (head == "_" or head in ROUTE_CATCH_CODES
|
|
1061
|
+
or (sym is not None and sym.kind == "variant")
|
|
1062
|
+
or binds != [head]):
|
|
1063
|
+
return {b: None for b in binds}
|
|
1064
|
+
return {head: _CAUGHT}
|
|
1065
|
+
|
|
1066
|
+
def _check_err_arg(self, ex: A.Call, scopes) -> None:
|
|
1067
|
+
"""E-ERR-ARG: in a route body the build lowers `err(X)` only when X
|
|
1068
|
+
is a call to an error code (`conflict("x")`) or to a declared union
|
|
1069
|
+
variant (`invalid_field("title")`), or is the error a `catch e` arm
|
|
1070
|
+
bound (`err(e)` rethrows it). Anything else checked clean and then
|
|
1071
|
+
failed the build ("`err(...)` argument must be a standard error
|
|
1072
|
+
..."). A view's `err(...)` is checks.py's E-ERR-CONTEXT, and an
|
|
1073
|
+
undefined name is already R-UNDEF."""
|
|
1074
|
+
if not (self._in_route and isinstance(ex.callee, A.Name)
|
|
1075
|
+
and ex.callee.id == "err"):
|
|
1076
|
+
return
|
|
1077
|
+
if any("err" in sc for sc in scopes) or "err" in self.table:
|
|
1078
|
+
return
|
|
1079
|
+
if len(ex.args) != 1 or ex.named:
|
|
1080
|
+
self._d("E-ERR-ARG",
|
|
1081
|
+
"`err(...)` takes exactly one positional argument, the "
|
|
1082
|
+
"error -- the build cannot lower this call", ex,
|
|
1083
|
+
"use " + _ERR_ARG_FIX)
|
|
1084
|
+
return
|
|
1085
|
+
arg = ex.args[0]
|
|
1086
|
+
if isinstance(arg, A.Call) and isinstance(arg.callee, A.Name):
|
|
1087
|
+
n = arg.callee.id
|
|
1088
|
+
sym = self.table.get(n)
|
|
1089
|
+
if any(n in sc for sc in scopes):
|
|
1090
|
+
what = f"'{n}' is a local binding, not an error constructor"
|
|
1091
|
+
elif sym is not None and sym.kind == "variant":
|
|
1092
|
+
return
|
|
1093
|
+
elif sym is None and n in _ERROR_CODES:
|
|
1094
|
+
return
|
|
1095
|
+
elif sym is None and not self._resolves(n, scopes):
|
|
1096
|
+
return # already R-UNDEF
|
|
1097
|
+
else:
|
|
1098
|
+
what = f"'{n}' is not an error code or a declared union variant"
|
|
1099
|
+
elif isinstance(arg, A.Name):
|
|
1100
|
+
if self._typeof(arg, scopes) == _CAUGHT:
|
|
1101
|
+
return
|
|
1102
|
+
if not self._resolves(arg.id, scopes):
|
|
1103
|
+
return # already R-UNDEF
|
|
1104
|
+
what = (f"'{arg.id}' is not an error a `catch` arm caught"
|
|
1105
|
+
+ (" -- call the error code: "
|
|
1106
|
+
f"`err({arg.id}(\"...\"))`"
|
|
1107
|
+
if arg.id in _ERROR_CODES else ""))
|
|
1108
|
+
else:
|
|
1109
|
+
what = "the argument is not an error constructor call"
|
|
1110
|
+
self._d("E-ERR-ARG",
|
|
1111
|
+
f"{what}, so `err(...)` cannot be built -- the build lowers "
|
|
1112
|
+
"`err(...)` only for an error code, a declared union variant "
|
|
1113
|
+
"or a caught error", ex, "use " + _ERR_ARG_FIX)
|
|
1114
|
+
|
|
1002
1115
|
def _check_view_module_fn(self, ex: A.Call, scopes) -> None:
|
|
1003
1116
|
"""A view -- or a closure / `on` handler / block inside one -- that
|
|
1004
1117
|
calls a module-level `fn` resolves fine, then `vlbuild` refuses it:
|
|
@@ -1187,3 +1300,22 @@ def resolve(mod: A.Module, filename: str, li: LineIndex,
|
|
|
1187
1300
|
layout_params: dict | None = None) -> list[Diagnostic]:
|
|
1188
1301
|
return Resolver(filename, li, page_clients=page_clients,
|
|
1189
1302
|
layout_params=layout_params).run(mod)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _project_root(filename: str) -> str | None:
|
|
1306
|
+
"""The project a page-server file belongs to: the parent of the nearest
|
|
1307
|
+
enclosing `app/` directory, or None for a file outside one."""
|
|
1308
|
+
d = os.path.dirname(os.path.abspath(filename))
|
|
1309
|
+
while True:
|
|
1310
|
+
if os.path.basename(d) == "app":
|
|
1311
|
+
return os.path.dirname(d)
|
|
1312
|
+
parent = os.path.dirname(d)
|
|
1313
|
+
if parent == d:
|
|
1314
|
+
return None
|
|
1315
|
+
d = parent
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def resolve_page_server(mod: A.PageServerFile, filename: str,
|
|
1319
|
+
li: LineIndex) -> list[Diagnostic]:
|
|
1320
|
+
"""The resolver pass for a `page.server.vl` file (`Resolver.run_page_server`)."""
|
|
1321
|
+
return Resolver(filename, li).run_page_server(mod)
|
|
@@ -86,21 +86,43 @@
|
|
|
86
86
|
// of VL.std (test_std_fn_parity in tests/test_build.py checks this file,
|
|
87
87
|
// the minified copy and server.js).
|
|
88
88
|
function asArray(x) { return x == null ? [] : x; }
|
|
89
|
-
// Volaro equality
|
|
90
|
-
//
|
|
91
|
-
// (
|
|
89
|
+
// Volaro equality (spec section 4.3), used by `==` / `!=` (VL.eq below)
|
|
90
|
+
// and `unique`: scalars compare as `===` does, `nil` is null or undefined
|
|
91
|
+
// (a missing optional field is nil), and records and lists compare
|
|
92
|
+
// structurally, equal when their fields or items are. No identity or
|
|
93
|
+
// reference semantics. Only plain objects (a record literal, a decoded
|
|
94
|
+
// JSON value, a database row) and arrays are compared structurally; any
|
|
95
|
+
// other object compares by identity.
|
|
96
|
+
function isPlain(x) {
|
|
97
|
+
if (Array.isArray(x)) return true;
|
|
98
|
+
var p = Object.getPrototypeOf(x);
|
|
99
|
+
return p === Object.prototype || p === null;
|
|
100
|
+
}
|
|
92
101
|
function stdEq(a, b) {
|
|
93
102
|
if (a === b) return true;
|
|
94
|
-
if (a
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
103
|
+
if (a == null || b == null) return a == null && b == null;
|
|
104
|
+
if (typeof a !== "object" || typeof b !== "object"
|
|
105
|
+
|| Array.isArray(a) !== Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
|
|
106
|
+
var i;
|
|
107
|
+
if (Array.isArray(a)) {
|
|
108
|
+
if (a.length !== b.length) return false;
|
|
109
|
+
for (i = 0; i < a.length; i++) if (!stdEq(a[i], b[i])) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// A field that is nil on one side and absent on the other is equal.
|
|
113
|
+
var ka = Object.keys(a), kb = Object.keys(b);
|
|
114
|
+
for (i = 0; i < ka.length; i++) if (!stdEq(a[ka[i]], b[ka[i]])) return false;
|
|
115
|
+
for (i = 0; i < kb.length; i++) {
|
|
116
|
+
if (!Object.prototype.hasOwnProperty.call(a, kb[i]) && b[kb[i]] != null) return false;
|
|
101
117
|
}
|
|
102
118
|
return true;
|
|
103
119
|
}
|
|
120
|
+
// `a == b` where the build cannot tell both sides are scalars. A scalar
|
|
121
|
+
// pair costs one `===` and a typeof test; records and lists go to stdEq.
|
|
122
|
+
VL.eq = function (a, b) {
|
|
123
|
+
return a === b || (a == null ? b == null
|
|
124
|
+
: b != null && typeof a === "object" && typeof b === "object" && stdEq(a, b));
|
|
125
|
+
};
|
|
104
126
|
VL.std = {
|
|
105
127
|
filter: function (xs, f) { return asArray(xs).filter(function (x) { return f(x); }); },
|
|
106
128
|
map: function (xs, f) { return asArray(xs).map(function (x) { return f(x); }); },
|
|
@@ -152,6 +174,11 @@
|
|
|
152
174
|
get_or: function (m, k, dflt) {
|
|
153
175
|
return m && (k in m) ? m[k] : dflt;
|
|
154
176
|
},
|
|
177
|
+
// `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`.
|
|
178
|
+
// `any` of an empty list is false, `all` of an empty list is true. Both
|
|
179
|
+
// stop at the first item that settles the answer.
|
|
180
|
+
any: function (xs, f) { return asArray(xs).some(function (x) { return !!f(x); }); },
|
|
181
|
+
all: function (xs, f) { return asArray(xs).every(function (x) { return !!f(x); }); },
|
|
155
182
|
int_to_float: function (x) { return x; },
|
|
156
183
|
float_to_int: function (x) { return Math.trunc(x); },
|
|
157
184
|
// List growth (spec section 20.9) — the only way to combine or grow a
|
|
@@ -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}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);
|
|
2
|
+
"use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e){if(Array.isArray(e))return!0;var t=Object.getPrototypeOf(e);return t===Object.prototype||null===t}function o(e,t){if(e===t)return!0;if(null==e||null==t)return null==e&&null==t;if("object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t)||!i(e)||!i(t))return!1;var n;if(Array.isArray(e)){if(e.length!==t.length)return!1;for(n=0;n<e.length;n++)if(!o(e[n],t[n]))return!1;return!0}var r=Object.keys(e),l=Object.keys(t);for(n=0;n<r.length;n++)if(!o(e[r[n]],t[r[n]]))return!1;for(n=0;n<l.length;n++)if(!Object.prototype.hasOwnProperty.call(e,l[n])&&null!=t[l[n]])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 l=r[i.op];t=t.filter(function(e){return l(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.eq=function(e,t){return e===t||(null==e?null==t:null!=t&&"object"==typeof e&&"object"==typeof t&&o(e,t))},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},any:function(e,t){return r(e).some(function(e){return!!t(e)})},all:function(e,t){return r(e).every(function(e){return!!t(e)})},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,i=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<i.length;r++)if(o(i[r],e))return;i.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 l in i)if(Object.prototype.hasOwnProperty.call(i,l)){if(void 0===n[l])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+l);i[l]&&"object"==typeof i[l]&&e(n[l],i[l],o+"/"+l)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,l){var a,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=(a=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(a))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!l)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(l){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=l.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 l=["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 u(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){u(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function s(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function c(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function d(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function f(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 v(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 h(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function p(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)}),y(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 y(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return g(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=p(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function g(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),l=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):l.push(e)});var a=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(a[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!a[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=[],f=0;f<t._overlays.length;f++){var h=t._overlays[f];if(h.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!a[g._vlKey]&&g.contains&&g.contains(h.restoreFocus)){d.push({rec:h,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!a[_._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?p(r,e):e;i._vlKey=n,w.push(i)}else{var a=l[S++];w.push(a?p(a,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,l=0;l<e;l++){var u=i[l];u&&null!=u._vlKey&&a[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return v(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var j=A(m);if(j&&"function"==typeof j.focus)try{j.focus({preventScroll:!0})}catch(e){}}for(var C=0;C<d.length;C++)d[C].rec.restoreFallback=A(d[C].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,l[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=u,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||{},u(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",l=r+"e",a=e.input||{},u=a.attrs=a.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(l),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",a,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:l})),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 l=t._mainLabel;if(l&&o.getAttribute){var a=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||a)&&(o.getAttribute("aria-label")!==l&&o.setAttribute("aria-label",l),a||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=c(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 l=i.body||s()&&s().parentNode||null;l&&l.appendChild(o);var a=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:a&&a!==i.body?a:null}),1===t._overlays.length&&d(s(),!0),o},close:function(n){var r=c(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&&(d(s(),!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&&f(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=v(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 l;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return l},o)}),l=i(),null;var a=c(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var s=t._overlays[c(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),f=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(s.panel&&s.panel.parentNode===u&&u.replaceChild){var y=p(s.panel,f);y!==s.panel&&(u.replaceChild(y,s.panel),s.panel=y)}else if(u.replaceChildren)u.replaceChildren(f),s.panel=f;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(f),s.panel=f}return a&&(s._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=s.panel,r=v(n);if(r.length){var i=r[0],o=r[r.length-1],l=e.document.activeElement;t.shiftKey&&l===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||l!==o&&n.contains(l)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",s._onTab),s._onClick=function(e){e.target===u&&h(n)},u.addEventListener&&u.addEventListener("click",s._onClick),s._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||h(n)},e.document.addEventListener&&e.document.addEventListener("keydown",s._onKey),this.focusInto(s.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 l(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var a=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(),l(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),l(r-1)):"Home"===t?(i(),l(0)):"End"===t&&(i(),l(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"},[a,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,a={_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),l=this._loads[e];if(!l||l.key!==o){l=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var a=this;Promise.resolve().then(r).then(function(n){a._loads[e]===l&&(l.pending=!1,l.value=n,a.schedule(),t._onLoadSettled&&t._onLoadSettled(a))},function(n){a._loads[e]===l&&(l.pending=!1,l.error=n,a.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(a,e,n),t._onLoadSettled&&t._onLoadSettled(a))})}return l},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,a=l&&l.activeElement,c=null;a&&a.getAttribute&&a.getAttribute("data-vl-bind")&&(!l.contains||l.contains(a))&&(c={node:a,key:a.getAttribute("data-vl-bind"),start:a.selectionStart,end:a.selectionEnd,direction:a.selectionDirection,scrollLeft:a.scrollLeft,scrollTop:a.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();u(f,i),r.replaceChild?y(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?y(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 h=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[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:a},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){a._composing=!0}),c.addEventListener("compositionend",function(){a._composing=!1,a.schedule()})),a.render(),a},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var m=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(m[n])return m[n];var i=e.document,o=new Promise(function(e,o){var l=i.createElement("script");l.src=r,l.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},l.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(l)});return m[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
|
|
@@ -59,7 +59,9 @@ from vlcheck.elements import (
|
|
|
59
59
|
SPELLCHECK_HOMES, TABINDEX_ALLOWED, TEXT_STYLE_ATTRS,
|
|
60
60
|
UNSAFE_ELEMENTS,
|
|
61
61
|
)
|
|
62
|
-
from vlcheck.checks import
|
|
62
|
+
from vlcheck.checks import (AUTH_ERROR_CODES, ROUTE_CATCH_CODES, STD_BUILT_FNS,
|
|
63
|
+
_ERROR_CODES, _lit_int, _lit_text,
|
|
64
|
+
_rendered_children)
|
|
63
65
|
from vlcheck.lexer import Lexer
|
|
64
66
|
from vlcheck.parser import Parser
|
|
65
67
|
from vlcheck.resolve import (_AUTH_API, AUTH_BUILT, _route_name,
|
|
@@ -169,12 +171,7 @@ WHERE_PREDICATES = {
|
|
|
169
171
|
"contains": 1,
|
|
170
172
|
}
|
|
171
173
|
|
|
172
|
-
_AUTH_ERROR_CODES =
|
|
173
|
-
"email_taken", "weak_password", "breached_password", "rate_limited",
|
|
174
|
-
"invalid_credentials", "unverified_email", "totp_required",
|
|
175
|
-
"provider_error", "invalid_token", "expired_token", "invalid_code",
|
|
176
|
-
"no_totp_enrolled",
|
|
177
|
-
}
|
|
174
|
+
_AUTH_ERROR_CODES = AUTH_ERROR_CODES
|
|
178
175
|
|
|
179
176
|
|
|
180
177
|
def _where_predicate_call(v):
|
|
@@ -286,7 +283,13 @@ _VIEW_STMT_OK = (A.StateDecl, A.DeriveDecl, A.LoadDecl,
|
|
|
286
283
|
_ARITH_OPS = {"+", "-", "*", "/", "%"}
|
|
287
284
|
# std functions whose result is a list (resolve.py E-LIST-OPERATOR)
|
|
288
285
|
_LIST_RESULT_FNS = {"concat", "append", "filter", "map", "take", "sort_by",
|
|
289
|
-
"chunk", "keys", "values", "split"}
|
|
286
|
+
"chunk", "keys", "values", "split", "unique"}
|
|
287
|
+
# std functions whose result is a scalar (a number, a string or a bool).
|
|
288
|
+
_SCALAR_RESULT_FNS = {"count", "sum", "contains", "matches", "join",
|
|
289
|
+
"first_char", "trim", "upper", "lower", "replace",
|
|
290
|
+
"int_to_float", "float_to_int", "any", "all"}
|
|
291
|
+
_SCALAR_OPS = {"+", "-", "*", "/", "%", "<", "<=", ">", ">=", "==", "!=",
|
|
292
|
+
"and", "or"}
|
|
290
293
|
|
|
291
294
|
|
|
292
295
|
def _known_list(e) -> bool:
|
|
@@ -308,6 +311,34 @@ def _known_list(e) -> bool:
|
|
|
308
311
|
return False
|
|
309
312
|
|
|
310
313
|
|
|
314
|
+
def _known_scalar(e, bare_variants=()) -> bool:
|
|
315
|
+
"""True for an expression that is a number, string or bool by its shape
|
|
316
|
+
alone: a non-nil literal, `true` / `false`, a fieldless union variant
|
|
317
|
+
(emitted as its name), `not` / unary `-`, arithmetic, a comparison, a
|
|
318
|
+
logical operator, `.len()` / `.format()`, or a std function that returns
|
|
319
|
+
a scalar (directly or through `|>`). `==` with one side known scalar
|
|
320
|
+
stays JavaScript's `===`; anything else goes through `VL.eq`, which
|
|
321
|
+
compares records and lists structurally (spec section 4.3)."""
|
|
322
|
+
if isinstance(e, A.Literal):
|
|
323
|
+
return e.kind in ("int", "float", "bool", "str", "duration")
|
|
324
|
+
if isinstance(e, A.Name):
|
|
325
|
+
return e.id in ("true", "false") or e.id in bare_variants
|
|
326
|
+
if isinstance(e, A.Unary):
|
|
327
|
+
return True
|
|
328
|
+
if isinstance(e, A.Binary):
|
|
329
|
+
if e.op == "|>":
|
|
330
|
+
r = e.right
|
|
331
|
+
callee = r.callee if isinstance(r, A.Call) else r
|
|
332
|
+
return isinstance(callee, A.Name) and callee.id in _SCALAR_RESULT_FNS
|
|
333
|
+
return e.op in _SCALAR_OPS
|
|
334
|
+
if isinstance(e, A.Call):
|
|
335
|
+
c = e.callee
|
|
336
|
+
if isinstance(c, A.Member):
|
|
337
|
+
return c.name in ("len", "format")
|
|
338
|
+
return isinstance(c, A.Name) and c.id in _SCALAR_RESULT_FNS
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
|
|
311
342
|
def _nested_view_drop(stmts, where):
|
|
312
343
|
"""The first statement in a view's render tree that the emitter would
|
|
313
344
|
not lower, as `(code, message)`, or None. vlcheck's `silent_drops` is
|
|
@@ -412,6 +443,9 @@ class Emitter:
|
|
|
412
443
|
# the name resolves through these frames.
|
|
413
444
|
self._scopes: list[dict[str, str]] = []
|
|
414
445
|
self._local_seq = 0
|
|
446
|
+
# JS names bound to a caught error by a route body's `catch e`
|
|
447
|
+
# (`_route_try`): `err(e)` rethrows that error as it is.
|
|
448
|
+
self._caught: set[str] = set()
|
|
415
449
|
self.views: list[A.ViewDecl] = []
|
|
416
450
|
self.view_names: set[str] = set()
|
|
417
451
|
self.routes: dict[str, A.Route] = {}
|
|
@@ -3540,6 +3574,10 @@ class Emitter:
|
|
|
3540
3574
|
if isinstance(st, A.ExprStmt):
|
|
3541
3575
|
last = tail and i == len(stmts) - 1
|
|
3542
3576
|
parts.append("%s%s;" % ("return " if last else "", self.expr(st.expr)))
|
|
3577
|
+
elif isinstance(st, A.Let) and isinstance(st.init, A.Try):
|
|
3578
|
+
parts.append(self._route_try(st.init, decl, in_loop, let=st))
|
|
3579
|
+
elif isinstance(st, A.Try):
|
|
3580
|
+
parts.append(self._route_try(st, decl, in_loop))
|
|
3543
3581
|
elif isinstance(st, A.Let):
|
|
3544
3582
|
init = self.expr(st.init) if st.init is not None else "undefined"
|
|
3545
3583
|
parts.append("%s %s = %s;" % (decl, self._bind_local(st.name), init))
|
|
@@ -3576,14 +3614,90 @@ class Emitter:
|
|
|
3576
3614
|
"E-LOOP-CONTROL should have caught this" % word)
|
|
3577
3615
|
parts.append("break;" if word == "break" else "continue;")
|
|
3578
3616
|
else:
|
|
3579
|
-
hint = {"Match": " Use if / elif / else instead."
|
|
3580
|
-
"Try": " Propagate with `?`, or check the value with if."}
|
|
3617
|
+
hint = {"Match": " Use if / elif / else instead."}
|
|
3581
3618
|
raise BuildError(
|
|
3582
3619
|
"route body statement `%s` is not lowered by vlbuild -- emitting it "
|
|
3583
3620
|
"would silently skip it.%s" % (type(st).__name__,
|
|
3584
3621
|
hint.get(type(st).__name__, "")))
|
|
3585
3622
|
return parts
|
|
3586
3623
|
|
|
3624
|
+
def _route_try(self, t: A.Try, decl: str, in_loop: bool,
|
|
3625
|
+
let: A.Let | None = None) -> str:
|
|
3626
|
+
"""`try EXPR` / `catch ...` in a route body (spec section 6.2), both
|
|
3627
|
+
forms. It used to fail the build ("route body statement `Try` is
|
|
3628
|
+
not lowered") although `volaro check` accepted it, so the spec's own
|
|
3629
|
+
`catch e` / `return err(e)` example could not be built.
|
|
3630
|
+
|
|
3631
|
+
- An arm headed by an error code (`catch not_found`), a declared
|
|
3632
|
+
union variant or an auth error code runs when the thrown error has
|
|
3633
|
+
that `code`, binding the variant's fields by name. Any other head
|
|
3634
|
+
(`catch e`, `catch _`) catches every error and binds it to `e`;
|
|
3635
|
+
arms after it are unreachable, as in a view handler. An error no
|
|
3636
|
+
arm matches is rethrown.
|
|
3637
|
+
- Statement form: a handled error does not fall through. The arm
|
|
3638
|
+
ends the route: its last expression statement is the route's
|
|
3639
|
+
value (like a route body's own last line); otherwise the route
|
|
3640
|
+
returns nil.
|
|
3641
|
+
- `let x = try EXPR`: `x` is EXPR's value, or the value of the arm
|
|
3642
|
+
that ran (its last expression statement); the statements after
|
|
3643
|
+
the `let` then run as usual.
|
|
3644
|
+
- `err(e)` inside an arm rethrows the caught error unchanged
|
|
3645
|
+
(`_ex_ServerErr`)."""
|
|
3646
|
+
codes = set(ROUTE_CATCH_CODES) | set(self.variant_params)
|
|
3647
|
+
call_js = self.expr(t.expr)
|
|
3648
|
+
out: list[str] = []
|
|
3649
|
+
target = None
|
|
3650
|
+
if let is not None:
|
|
3651
|
+
target = self._bind_local(let.name)
|
|
3652
|
+
out.append("%s %s;" % (decl, target))
|
|
3653
|
+
out.append("try { %s = await %s; } catch (_vlErr) {" % (target, call_js))
|
|
3654
|
+
else:
|
|
3655
|
+
out.append("try { await %s; } catch (_vlErr) {" % call_js)
|
|
3656
|
+
fallback = False
|
|
3657
|
+
for i, (pat, body) in enumerate(t.catches):
|
|
3658
|
+
head = getattr(pat, "head", "")
|
|
3659
|
+
is_code = head in codes
|
|
3660
|
+
if is_code:
|
|
3661
|
+
out.append("%s (_vlErr && _vlErr.code === %s) {" %
|
|
3662
|
+
("if" if i == 0 else "else if", json.dumps(head)))
|
|
3663
|
+
else:
|
|
3664
|
+
out.append("{" if i == 0 else "else {")
|
|
3665
|
+
fallback = True
|
|
3666
|
+
self._push_scope()
|
|
3667
|
+
caught = None
|
|
3668
|
+
try:
|
|
3669
|
+
binds = list(getattr(pat, "binds", []) or [])
|
|
3670
|
+
if is_code:
|
|
3671
|
+
for name in binds:
|
|
3672
|
+
if name != head:
|
|
3673
|
+
out.append("%s %s = _vlErr[%s];" % (
|
|
3674
|
+
decl, self._bind_local(name), json.dumps(name)))
|
|
3675
|
+
elif binds and head != "_":
|
|
3676
|
+
caught = self._bind_local(binds[0])
|
|
3677
|
+
out.append("%s %s = _vlErr;" % (decl, caught))
|
|
3678
|
+
self._caught.add(caught)
|
|
3679
|
+
stmts = list(body)
|
|
3680
|
+
last = stmts[-1] if stmts and isinstance(stmts[-1], A.ExprStmt) else None
|
|
3681
|
+
head_stmts = stmts[:-1] if last is not None else stmts
|
|
3682
|
+
out.extend(self._route_block(head_stmts, decl, in_loop, tail=False))
|
|
3683
|
+
if let is not None:
|
|
3684
|
+
if last is not None:
|
|
3685
|
+
out.append("%s = %s;" % (target, self.expr(last.expr)))
|
|
3686
|
+
elif last is not None:
|
|
3687
|
+
out.append("return %s;" % self.expr(last.expr))
|
|
3688
|
+
elif not (stmts and isinstance(stmts[-1], A.Return)):
|
|
3689
|
+
out.append("return null;")
|
|
3690
|
+
finally:
|
|
3691
|
+
self._caught.discard(caught)
|
|
3692
|
+
self._pop_scope()
|
|
3693
|
+
out.append("}")
|
|
3694
|
+
if fallback:
|
|
3695
|
+
break
|
|
3696
|
+
if not fallback:
|
|
3697
|
+
out.append("else { throw _vlErr; }")
|
|
3698
|
+
out.append("}")
|
|
3699
|
+
return " ".join(out)
|
|
3700
|
+
|
|
3587
3701
|
def expr(self, e) -> str:
|
|
3588
3702
|
if e is None:
|
|
3589
3703
|
return "undefined"
|
|
@@ -3760,16 +3874,28 @@ class Emitter:
|
|
|
3760
3874
|
dispatcher's catch handler forwards every other own field (so
|
|
3761
3875
|
`err(invalid_field("title"))` reaches the client as
|
|
3762
3876
|
`{error: "invalid_field", field: "title"}`, not just a bare code)."""
|
|
3763
|
-
|
|
3877
|
+
# `err(e)` in a route's `catch e` arm (spec section 6.2's own
|
|
3878
|
+
# example): rethrow the caught error unchanged, so its code and
|
|
3879
|
+
# fields reach the error response as they would have with no
|
|
3880
|
+
# `try` at all.
|
|
3881
|
+
if isinstance(inner, A.Name) and self._js_name(inner.id) in self._caught:
|
|
3882
|
+
return "(() => { throw %s; })()" % self._js_name(inner.id)
|
|
3883
|
+
# Every spec error code (vlcheck's `_ERROR_CODES`: `conflict`,
|
|
3884
|
+
# `rejected`, ... -- the checker accepts each as the argument of
|
|
3885
|
+
# `err(...)`), plus the server's own `bad_request` / `unauthorized`.
|
|
3886
|
+
standard = set(_ERROR_CODES) | {"bad_request", "unauthorized"}
|
|
3764
3887
|
if not (isinstance(inner, A.Call) and isinstance(inner.callee, A.Name)
|
|
3765
3888
|
and (inner.callee.id in self.variant_params
|
|
3766
3889
|
or inner.callee.id in standard)):
|
|
3767
3890
|
got = (inner.callee.id if isinstance(inner, A.Call)
|
|
3768
|
-
and isinstance(inner.callee, A.Name)
|
|
3891
|
+
and isinstance(inner.callee, A.Name)
|
|
3892
|
+
else inner.id if isinstance(inner, A.Name)
|
|
3893
|
+
else type(inner).__name__)
|
|
3769
3894
|
raise BuildError(
|
|
3770
|
-
"
|
|
3771
|
-
"
|
|
3772
|
-
"
|
|
3895
|
+
"E-ERR-ARG: `err(...)` takes an error code call "
|
|
3896
|
+
"(`not_found(\"...\")`), a declared error-union variant "
|
|
3897
|
+
"(`invalid_field(\"title\")`), or the error a `catch e` arm "
|
|
3898
|
+
"caught -- `%s` is not lowered" % got)
|
|
3773
3899
|
variant = inner.callee.id
|
|
3774
3900
|
param_names = (self.variant_params[variant]
|
|
3775
3901
|
if variant in self.variant_params else ["message"])
|
|
@@ -3902,8 +4028,35 @@ class Emitter:
|
|
|
3902
4028
|
raise BuildError(
|
|
3903
4029
|
"E-LIST-OPERATOR: '%s' with a list operand is not defined; use "
|
|
3904
4030
|
"concat(a, b) or xs |> append(x)" % e.op)
|
|
4031
|
+
if e.op in ("==", "!="):
|
|
4032
|
+
return self._equality_js(e)
|
|
3905
4033
|
return "(%s %s %s)" % (self.expr(e.left), js, self.expr(e.right))
|
|
3906
4034
|
|
|
4035
|
+
def _equality_js(self, e: A.Binary) -> str:
|
|
4036
|
+
"""`==` / `!=` (spec section 4.3: records and lists compare by value,
|
|
4037
|
+
never by identity). JavaScript's `===` compares objects by identity,
|
|
4038
|
+
so `{a: 1} == {a: 1}` was false and a list of records never matched
|
|
4039
|
+
itself, while `unique` compared the same values structurally. The
|
|
4040
|
+
emitter has no types, so it decides by shape: when either side is a
|
|
4041
|
+
known scalar (`_known_scalar`) the comparison stays `===`; a `nil`
|
|
4042
|
+
literal on either side is a null-or-undefined test (`== null`, since
|
|
4043
|
+
a missing optional field is nil too); anything else calls `VL.eq`,
|
|
4044
|
+
which is a single `===` for two scalars and structural for records
|
|
4045
|
+
and lists. Both targets have `VL.eq` (the shared std block)."""
|
|
4046
|
+
neg = e.op == "!="
|
|
4047
|
+
left, right = self.expr(e.left), self.expr(e.right)
|
|
4048
|
+
nil = lambda x: ((isinstance(x, A.Literal) and x.kind == "nil")
|
|
4049
|
+
or (isinstance(x, A.Name) and x.id == "nil"
|
|
4050
|
+
and self._js_name("nil") is None))
|
|
4051
|
+
if nil(e.left) or nil(e.right):
|
|
4052
|
+
other = right if nil(e.left) else left
|
|
4053
|
+
return "(%s %s null)" % (other, "!=" if neg else "==")
|
|
4054
|
+
bare = {v for v, ps in self.variant_params.items()
|
|
4055
|
+
if not ps and self._js_name(v) is None}
|
|
4056
|
+
if _known_scalar(e.left, bare) or _known_scalar(e.right, bare):
|
|
4057
|
+
return "(%s %s %s)" % (left, "!==" if neg else "===", right)
|
|
4058
|
+
return "%sVL.eq(%s, %s)" % ("!" if neg else "", left, right)
|
|
4059
|
+
|
|
3907
4060
|
def _ex_Unary(self, e: A.Unary) -> str:
|
|
3908
4061
|
return "(%s%s)" % ("!" if e.op == "not" else e.op, self.expr(e.operand))
|
|
3909
4062
|
|
|
@@ -116,11 +116,13 @@ def lower_page_server(root: Path, emitter) -> A.ServiceDecl:
|
|
|
116
116
|
# `build_manifest` parses each page.server.vl but only structurally; the
|
|
117
117
|
# page-server CHECKS (E-PS-WRITE-AUTH, dup verbs/actions, errors-block
|
|
118
118
|
# rules) are what make the synthetic service safe for `precheck`, so run
|
|
119
|
-
# the full per-file pipeline too
|
|
119
|
+
# the full per-file pipeline too, with the resolver `volaro check` runs
|
|
120
|
+
# (names, arity, fields, `err(...)` arguments), so the build reports
|
|
121
|
+
# exactly what the check does.
|
|
120
122
|
for path in sorted({e.source for e in manifest.entries}
|
|
121
123
|
| _page_server_files(root)):
|
|
122
124
|
src = Path(path).read_text(encoding="utf-8")
|
|
123
|
-
_, diags = check_source(src, str(path))
|
|
125
|
+
_, diags = check_source(src, str(path), do_resolve=True)
|
|
124
126
|
for d in diags:
|
|
125
127
|
if d.severity == Severity.ERROR:
|
|
126
128
|
problems.append(f"{d.code}: {d.message} ({d.file})")
|
|
@@ -60,7 +60,7 @@ _DB_METHODS = {"where", "order", "limit", "all", "first", "get", "insert", "upda
|
|
|
60
60
|
# Statements Emitter.route_block lowers (action-34). It raises BuildError
|
|
61
61
|
# for anything else at any nesting depth; this top-level check just fails
|
|
62
62
|
# earlier with the route named.
|
|
63
|
-
_STMT_OK = (A.ExprStmt, A.Let, A.Assign, A.Return, A.If, A.For, A.While)
|
|
63
|
+
_STMT_OK = (A.ExprStmt, A.Let, A.Assign, A.Return, A.If, A.For, A.While, A.Try)
|
|
64
64
|
|
|
65
65
|
# A route's declared params are lowered if every one is a coercible primitive;
|
|
66
66
|
# a record/list/map param stays a hard error — see precheck. `get`/`delete`
|
|
@@ -1330,14 +1330,13 @@ function err(code, message) { const e = new Error(message || code); e.code = cod
|
|
|
1330
1330
|
// `STD_FNS` one function at a time (`matches`, `split`, `filter`, `count`,
|
|
1331
1331
|
// `replace`, `contains`, `lower`, `trim`, `map`, `concat` were each found
|
|
1332
1332
|
// missing by a route that called them).
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1333
|
+
// The block also defines `VL.eq`, the structural `==` a route body's
|
|
1334
|
+
// record and list comparisons compile to.
|
|
1335
|
+
const VL = (function () {
|
|
1336
|
+
const VL = { len: (x) => (x == null ? 0 : x.length) };
|
|
1337
1337
|
%(vl_std)s
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
};
|
|
1338
|
+
return VL;
|
|
1339
|
+
})();
|
|
1341
1340
|
|
|
1342
1341
|
// Coerce one `?name=raw` query-string value or one decoded path segment to
|
|
1343
1342
|
// its declared Volaro scalar type, or throw a 400 (via
|
package/language/crib.md
CHANGED
|
@@ -119,6 +119,10 @@ derive shown = items |> filter(fn(a) q == "" or contains(lower(a.title), lower(t
|
|
|
119
119
|
|
|
120
120
|
# no + on lists, no spread — concat/append are the only way to grow one:
|
|
121
121
|
concat(xs, ys) · xs |> append(x)
|
|
122
|
+
# does some / every item pass? any(xs, f) / all(xs, f) -> bool
|
|
123
|
+
if not (cs |> any(fn(c) c == a.category))
|
|
124
|
+
cs = cs |> append(a.category)
|
|
125
|
+
# == / != compare records and lists by value (fields, items), like unique(xs)
|
|
122
126
|
|
|
123
127
|
# exact pattern validation (std.str's `regex`) -- no implicit anchoring,
|
|
124
128
|
# caller supplies ^/$. An invalid pattern never throws -- matches() always
|
|
@@ -211,7 +215,11 @@ errors # per-file code -> status; `_` required
|
|
|
211
215
|
Handler shape: `method [action] (params) Ret[!Err] [as alias]` + body. One
|
|
212
216
|
unnamed handler per method per file. `?` propagates only errors in the
|
|
213
217
|
handler's declared `!E` set (db failures are always fine — they become the
|
|
214
|
-
opaque internal error); translate anything else with try/catch.
|
|
218
|
+
opaque internal error); translate anything else with try/catch. `err(...)`
|
|
219
|
+
takes an error code call (`err(conflict("taken"))`), a variant of an error
|
|
220
|
+
union declared in `app.vl`, or a caught error: `catch e` then `return err(e)`
|
|
221
|
+
rethrows it. Anything else is `E-ERR-ARG`. `volaro check` resolves these
|
|
222
|
+
files like any other.
|
|
215
223
|
|
|
216
224
|
Views call the structural client — one options object, dynamic segments are
|
|
217
225
|
params: `api.posts.list(limit: 20)` · `api.posts.get(id:)` ·
|
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, 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) |
|
|
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), any, all (`any(xs, f) bool` — true when some item passes `f`, false for an empty list; `all(xs, f) bool` — true when every item passes `f`, true for an empty list) |
|
|
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
|
@@ -423,8 +423,8 @@ audio video source track picture map area
|
|
|
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
425
|
The std free functions this build has are the ones listed under
|
|
426
|
-
"Data / services" below (`concat`, `append`, `unique`, `
|
|
427
|
-
`contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
|
|
426
|
+
"Data / services" below (`concat`, `append`, `unique`, `any`, `all`,
|
|
427
|
+
`matches`, `contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
|
|
428
428
|
`sort_by`, `group_by`, `chunk`, `join`, `split`, `first_char`, `trim`,
|
|
429
429
|
`upper`, `lower`, `keys`, `values`, `get_or`, `int_to_float` and
|
|
430
430
|
`float_to_int`. Any other std name (`sorted`, `first`, `fold`, `merge`,
|
|
@@ -444,6 +444,14 @@ audio video source track picture map area
|
|
|
444
444
|
only the argument of `err(...)`. `not_found("x")` anywhere else is
|
|
445
445
|
`E-ERR-CONTEXT`. A service with no `model` runs its routes in the
|
|
446
446
|
browser, and `err(...)` fails the call there too.
|
|
447
|
+
- The argument of `err(...)` is an error code call (`err(conflict("x"))`,
|
|
448
|
+
any code in the list above), a variant of an error union you declare
|
|
449
|
+
(`err(invalid_field("title"))`; a page.server handler sees the unions
|
|
450
|
+
in the project's `app.vl`), or the error a `catch e` arm caught
|
|
451
|
+
(`err(e)` rethrows it unchanged). Anything else (a string, a local
|
|
452
|
+
value, a record type, a function) is `E-ERR-ARG` in `volaro check` and
|
|
453
|
+
the build. A code with no `errors` entry answers with status 500, like
|
|
454
|
+
any unmapped code.
|
|
447
455
|
- A namespace or ambient object (`db`, `env`, `log`, `route`, `auth`,
|
|
448
456
|
`session`, `request`, `api`, and the unbuilt `analytics`, `events`,
|
|
449
457
|
`ws`, `mailer`, `billing`), a type name (`str(x)`, `int(x)`, …), `ok`,
|
|
@@ -472,9 +480,22 @@ audio video source track picture map area
|
|
|
472
480
|
writes.
|
|
473
481
|
- Route bodies (services and `page.server.vl` handlers) run `let`,
|
|
474
482
|
assignment (`=`, `+=` …), `if`/`elif`/`else`, `for`, `while`, `break`,
|
|
475
|
-
`skip` and `
|
|
476
|
-
statement (`match`
|
|
483
|
+
`skip`, `return` and `try`/`catch`; the last expression is the route's
|
|
484
|
+
value. Any other statement (`match` as a statement, `par`, `spawn`) fails
|
|
477
485
|
the build, at any nesting depth. Use `?` to propagate errors.
|
|
486
|
+
- `try`/`catch` in a route body, both forms. `catch not_found` (an error
|
|
487
|
+
code, or a variant of a union you declare) runs for that code only and
|
|
488
|
+
binds the variant's fields; `catch e` catches anything. An error no arm
|
|
489
|
+
matches propagates. The bare statement form does not fall through: the
|
|
490
|
+
arm ends the route, and its last expression is the route's value.
|
|
491
|
+
`let x = try ...` gives `x` the arm's last expression and carries on.
|
|
492
|
+
`catch e` then `return err(e)` rethrows the caught error as it was.
|
|
493
|
+
- `page.server.vl` handlers are resolved by `volaro check` like service
|
|
494
|
+
routes: an undefined name, a wrong argument count, an unknown field of
|
|
495
|
+
a typed parameter or a bad `err(...)` argument is reported there. In
|
|
496
|
+
scope: the project's `app.vl` declarations, the handler's parameters,
|
|
497
|
+
`session`, `request`, and the `_server.vl` context providers above the
|
|
498
|
+
file.
|
|
478
499
|
- Password auth: `auth.sign_up` / `sign_in` / `sign_out` / `reset_password` /
|
|
479
500
|
`confirm_reset` / `confirm_verify` against compiler-owned endpoints.
|
|
480
501
|
Any other `auth.` name is `E-AUTH-METHOD` in `volaro check` and the build,
|
|
@@ -507,6 +528,19 @@ audio video source track picture map area
|
|
|
507
528
|
- `std.list` `concat` / `append`; `xs |> map|filter(fn(x) …)`. (`fold` is in
|
|
508
529
|
`volaro spec`'s stdlib table but is **not implemented** in this build —
|
|
509
530
|
found stale while documenting `matches` below; corrected here, not fixed.)
|
|
531
|
+
- `std.list` `any(xs, f)` / `all(xs, f)` — whether some / every item
|
|
532
|
+
passes `f` (a `bool`). `any` of an empty list is `false`, `all` of an
|
|
533
|
+
empty list is `true`. Both forms work, in views, handlers and route bodies:
|
|
534
|
+
```vl
|
|
535
|
+
if not (cs |> any(fn(c) c == a.category))
|
|
536
|
+
cs = cs |> append(a.category)
|
|
537
|
+
derive all_done = all(tasks, fn(t) t.done)
|
|
538
|
+
```
|
|
539
|
+
- `==` and `!=` compare records and lists by value, the way `unique` does
|
|
540
|
+
(spec section 4.3): two records are equal when their fields are, two lists
|
|
541
|
+
when their items are, nested records included, in views and route bodies
|
|
542
|
+
alike. `nil` equals a missing optional field. Numbers, strings and bools
|
|
543
|
+
compare as before.
|
|
510
544
|
- `std.list` `unique(xs)` — the first occurrence of each distinct value, in
|
|
511
545
|
list order, as a new list. Scalars compare as `==` does (`1` and `"1"`
|
|
512
546
|
differ); records and lists compare by their fields and items. Works in
|
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.13",
|
|
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",
|