volaro 0.1.0-alpha.12 → 0.1.0-alpha.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Volaro
2
2
 
3
- **Limited alpha** (this is `0.1.0-alpha.12`), published under both the
3
+ **Limited alpha** (this is `0.1.0-alpha.14`), 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": "92972bae88b0cd71b621a8c21dbeae052dc8a3c9",
2
+ "revision": "e01dc1d92881e38143a73b21dfdf0660cb515906",
3
3
  "source": "git",
4
4
  "dirty": false,
5
- "content_sha256": "09ae1f5782b061471ba112a8820cd9a08f4519ecff84eb418a5062f8ed23c467"
5
+ "content_sha256": "6476761bc8a3508734fee9684b80e0efe0ae151a6a40b5f5586bf1595e3a960d"
6
6
  }
@@ -1 +1 @@
1
- 92972bae88b0cd71b621a8c21dbeae052dc8a3c9
1
+ e01dc1d92881e38143a73b21dfdf0660cb515906
@@ -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
- # The resolver walks module items (services, views, use graphs); a
68
- # page-server file has none of those, so skip it here. Folder-param
69
- # matching against the route manifest is a later stage.
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 += resolve(mod, filename, li)
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
- "any": "count(filter(xs, f)) > 0",
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": "`count(filter(xs, fn(v) v == x)) > 0`",
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,23 @@ _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
+ # `catch <head>` tests the thrown error's code when <head> is one of these
259
+ # (or a declared union variant), in a route body and in a view handler
260
+ # alike; any other head binds the caught error itself (`catch e`). The code
261
+ # matches when it equals <head> or starts with "<head>." (`catch decode`
262
+ # catches `decode.missing_field`). resolve.py, the W-CATCH-NO-FALLBACK
263
+ # check and vlbuild's `_route_try` / `_handler_try` all read this set, so a
264
+ # catch arm means the same thing to check and build, client and server.
265
+ CATCH_CODES = (_ERROR_CODES | AUTH_ERROR_CODES
266
+ | frozenset({"bad_request", "unauthorized"}))
249
267
  _ERR_ROUTE_FIX = ("`return err(not_found(\"...\"))` in a `service` or "
250
268
  "page.server route body; a view reports a failure by "
251
269
  "setting its own state")
@@ -399,6 +417,28 @@ def _walk(node):
399
417
  yield from _walk(getattr(node, f))
400
418
 
401
419
 
420
+ def _h1_path(nodes) -> list:
421
+ """The `h1` elements one render of `nodes` can show together, in source
422
+ order: siblings add up, while the branches of an `if` / `match` are
423
+ alternatives, so only the branch with the most `h1`s counts. Anything
424
+ else is searched whole, as before (a `list` body counts once)."""
425
+ out: list = []
426
+ for n in nodes or ():
427
+ if isinstance(n, A.Element):
428
+ out += ([n] if n.name == "h1" else []) + _h1_path(n.body)
429
+ elif isinstance(n, A.If):
430
+ branches = ([n.then] + [body for _c, body in n.elifs]
431
+ + [n.orelse])
432
+ out += max((_h1_path(b) for b in branches), key=len)
433
+ elif isinstance(n, A.Match):
434
+ out += max((_h1_path(arm[-1]) for arm in n.arms),
435
+ key=len, default=[])
436
+ else:
437
+ out += [x for x in _walk(n)
438
+ if isinstance(x, A.Element) and x.name == "h1"]
439
+ return out
440
+
441
+
402
442
  def _rendered_children(el):
403
443
  """Yield the element children `el` actually renders, in source order.
404
444
 
@@ -2383,7 +2423,6 @@ class Checker:
2383
2423
  if not isinstance(it, A.ViewDecl):
2384
2424
  continue
2385
2425
  prev = 0
2386
- first_h1 = None
2387
2426
  navs: list = []
2388
2427
  mains: list = []
2389
2428
  # action-45.3: `aside` (role=complementary) has the same problem
@@ -2401,14 +2440,6 @@ class Checker:
2401
2440
  "screen-reader users navigate by level; don't "
2402
2441
  "skip one" % (prev, lvl), node,
2403
2442
  "use h%d here, or restructure the section" % (prev + 1))
2404
- if lvl == 1:
2405
- if first_h1 is not None:
2406
- self._d("W-HEADING-MULTI-H1", Severity.WARNING,
2407
- "a view renders more than one `h1`; a page "
2408
- "usually has exactly one top-level heading",
2409
- node, "make the later one `h2`")
2410
- else:
2411
- first_h1 = node
2412
2443
  prev = lvl
2413
2444
  elif node.name == "nav":
2414
2445
  navs.append(node)
@@ -2417,6 +2448,14 @@ class Checker:
2417
2448
  mains.append(node)
2418
2449
  elif node.name in landmarks:
2419
2450
  landmarks[node.name].append(node)
2451
+ # Branches of an `if` / `match` render one at a time, so a view
2452
+ # with `h1 "Not found"` in one branch and `h1 found.title` in
2453
+ # the other renders one `h1`: count the branch with the most.
2454
+ for node in _h1_path(it.body)[1:]:
2455
+ self._d("W-HEADING-MULTI-H1", Severity.WARNING,
2456
+ "a view renders more than one `h1`; a page "
2457
+ "usually has exactly one top-level heading",
2458
+ node, "make the later one `h2`")
2420
2459
  for lname, nodes in landmarks.items():
2421
2460
  if lname == "nav" or len(nodes) < 2:
2422
2461
  continue # `nav` keeps its own E-NAV-NAME below
@@ -3515,6 +3554,19 @@ class Checker:
3515
3554
  "'?' inside 'try' is redundant -- the catch arms below "
3516
3555
  "already handle the error, so nothing is propagated", t.expr,
3517
3556
  "remove the trailing '?'")
3557
+ heads = [getattr(pat, "head", "") for pat, _body in t.catches]
3558
+ if (self._ctx == "view" and heads
3559
+ and all(h in CATCH_CODES or h in self._variants for h in heads)):
3560
+ # Every arm names a code, so any other error is rethrown out of
3561
+ # the handler. Through 0.1.0-alpha.13 a view handler's
3562
+ # `catch not_found` caught every error; code written against
3563
+ # that now lets the others through. A warning, not an error:
3564
+ # rethrowing can be what the author wants.
3565
+ self._d("W-CATCH-NO-FALLBACK", Severity.WARNING,
3566
+ "every `catch` arm here names an error code, so any other "
3567
+ "error (a 500, a network failure) is rethrown out of the "
3568
+ "handler", t,
3569
+ "add a final `catch e` arm to handle the rest")
3518
3570
  self._expr(t.expr, scopes, fallible)
3519
3571
  for pat, body in t.catches:
3520
3572
  inner = scopes + [_Scope(getattr(pat, "binds", []), "catch binding")]
@@ -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, STD_BUILT_FNS, _FN_NOT_BUILT,
54
- _PRIM_NAMES)
53
+ from .checks import (BUILTINS, CONTEXTUAL_WORDS, 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
- inner = scopes + [{b: None for b in getattr(pat, "binds", [])}]
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
- inner = scopes + [{b: None for b in getattr(pat, "binds", [])}]
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 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 for `unique`: scalars as `==` compares them (`===`), and
90
- // records and lists structurally, equal when their fields or items are
91
- // (spec section 4.3: no identity or reference semantics).
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 === null || b === null || typeof a !== "object" || typeof b !== "object"
95
- || Array.isArray(a) !== Array.isArray(b)) return false;
96
- var ka = Object.keys(a);
97
- if (ka.length !== Object.keys(b).length) return false;
98
- for (var i = 0; i < ka.length; i++) {
99
- if (!Object.prototype.hasOwnProperty.call(b, ka[i])
100
- || !stdEq(a[ka[i]], b[ka[i]])) return false;
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 STD_BUILT_FNS, _lit_int, _lit_text, _rendered_children
62
+ from vlcheck.checks import (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,16 @@ 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
+
175
+
176
+ def _catch_code_test(head: str) -> str:
177
+ """The JS condition a `catch <code>` arm tests (spec section 6.2), in a
178
+ view handler and a route body alike: the caught error's `code` is
179
+ `head` or starts with `head.`, so `catch decode` also catches the
180
+ runtime's `decode.missing_field`."""
181
+ return ('_vlErr && typeof _vlErr.code === "string" && (_vlErr.code === %s'
182
+ ' || _vlErr.code.indexOf(%s) === 0)'
183
+ % (json.dumps(head), json.dumps(head + ".")))
178
184
 
179
185
 
180
186
  def _where_predicate_call(v):
@@ -286,7 +292,13 @@ _VIEW_STMT_OK = (A.StateDecl, A.DeriveDecl, A.LoadDecl,
286
292
  _ARITH_OPS = {"+", "-", "*", "/", "%"}
287
293
  # std functions whose result is a list (resolve.py E-LIST-OPERATOR)
288
294
  _LIST_RESULT_FNS = {"concat", "append", "filter", "map", "take", "sort_by",
289
- "chunk", "keys", "values", "split"}
295
+ "chunk", "keys", "values", "split", "unique"}
296
+ # std functions whose result is a scalar (a number, a string or a bool).
297
+ _SCALAR_RESULT_FNS = {"count", "sum", "contains", "matches", "join",
298
+ "first_char", "trim", "upper", "lower", "replace",
299
+ "int_to_float", "float_to_int", "any", "all"}
300
+ _SCALAR_OPS = {"+", "-", "*", "/", "%", "<", "<=", ">", ">=", "==", "!=",
301
+ "and", "or"}
290
302
 
291
303
 
292
304
  def _known_list(e) -> bool:
@@ -308,6 +320,34 @@ def _known_list(e) -> bool:
308
320
  return False
309
321
 
310
322
 
323
+ def _known_scalar(e, bare_variants=()) -> bool:
324
+ """True for an expression that is a number, string or bool by its shape
325
+ alone: a non-nil literal, `true` / `false`, a fieldless union variant
326
+ (emitted as its name), `not` / unary `-`, arithmetic, a comparison, a
327
+ logical operator, `.len()` / `.format()`, or a std function that returns
328
+ a scalar (directly or through `|>`). `==` with one side known scalar
329
+ stays JavaScript's `===`; anything else goes through `VL.eq`, which
330
+ compares records and lists structurally (spec section 4.3)."""
331
+ if isinstance(e, A.Literal):
332
+ return e.kind in ("int", "float", "bool", "str", "duration")
333
+ if isinstance(e, A.Name):
334
+ return e.id in ("true", "false") or e.id in bare_variants
335
+ if isinstance(e, A.Unary):
336
+ return True
337
+ if isinstance(e, A.Binary):
338
+ if e.op == "|>":
339
+ r = e.right
340
+ callee = r.callee if isinstance(r, A.Call) else r
341
+ return isinstance(callee, A.Name) and callee.id in _SCALAR_RESULT_FNS
342
+ return e.op in _SCALAR_OPS
343
+ if isinstance(e, A.Call):
344
+ c = e.callee
345
+ if isinstance(c, A.Member):
346
+ return c.name in ("len", "format")
347
+ return isinstance(c, A.Name) and c.id in _SCALAR_RESULT_FNS
348
+ return False
349
+
350
+
311
351
  def _nested_view_drop(stmts, where):
312
352
  """The first statement in a view's render tree that the emitter would
313
353
  not lower, as `(code, message)`, or None. vlcheck's `silent_drops` is
@@ -412,6 +452,9 @@ class Emitter:
412
452
  # the name resolves through these frames.
413
453
  self._scopes: list[dict[str, str]] = []
414
454
  self._local_seq = 0
455
+ # JS names bound to a caught error by a route body's `catch e`
456
+ # (`_route_try`): `err(e)` rethrows that error as it is.
457
+ self._caught: set[str] = set()
415
458
  self.views: list[A.ViewDecl] = []
416
459
  self.view_names: set[str] = set()
417
460
  self.routes: dict[str, A.Route] = {}
@@ -3133,16 +3176,27 @@ class Emitter:
3133
3176
  return ["var %s = _vlErr;" % self._bind_local(binds[0])]
3134
3177
  return []
3135
3178
 
3179
+ def _is_catch_code(self, head: str) -> bool:
3180
+ """`catch <head>` tests the error's code (an error code, an auth
3181
+ error code or a declared union variant); any other head (`catch
3182
+ e`, `catch _`) catches every error. One rule for view handlers and
3183
+ route bodies: `vlcheck.checks.CATCH_CODES`."""
3184
+ return head in CATCH_CODES or head in self.variant_params
3185
+
3136
3186
  def _handler_try(self, st: A.Try, in_loop: bool = False) -> str:
3137
- """Lower Volaro's non-fall-through handled errors (§6.2)."""
3187
+ """Lower Volaro's non-fall-through handled errors (§6.2). An arm
3188
+ headed by an error code runs for that code only (it was a
3189
+ catch-all for every code but the auth ones and declared variants,
3190
+ so `catch not_found` swallowed every error); an error no arm
3191
+ matches is rethrown."""
3138
3192
  out = ["try { await %s; } catch (_vlErr) {" % self.expr(st.expr)]
3139
3193
  fallback = False
3140
3194
  for i, (pat, body) in enumerate(st.catches):
3141
3195
  head = getattr(pat, "head", "")
3142
- is_variant = head in _AUTH_ERROR_CODES or head in self.variant_params
3196
+ is_variant = self._is_catch_code(head)
3143
3197
  if is_variant:
3144
- out.append("%s (_vlErr && _vlErr.code === %s) {" %
3145
- ("if" if i == 0 else "else if", json.dumps(head)))
3198
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3199
+ _catch_code_test(head)))
3146
3200
  else:
3147
3201
  out.append("{" if i == 0 else "else {")
3148
3202
  fallback = True
@@ -3182,10 +3236,10 @@ class Emitter:
3182
3236
  fallback = False
3183
3237
  for i, (pat, body) in enumerate(try_node.catches):
3184
3238
  head = getattr(pat, "head", "")
3185
- is_variant = head in _AUTH_ERROR_CODES or head in self.variant_params
3239
+ is_variant = self._is_catch_code(head)
3186
3240
  if is_variant:
3187
- out.append("%s (_vlErr && _vlErr.code === %s) {" %
3188
- ("if" if i == 0 else "else if", json.dumps(head)))
3241
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3242
+ _catch_code_test(head)))
3189
3243
  else:
3190
3244
  out.append("{" if i == 0 else "else {")
3191
3245
  fallback = True
@@ -3540,6 +3594,10 @@ class Emitter:
3540
3594
  if isinstance(st, A.ExprStmt):
3541
3595
  last = tail and i == len(stmts) - 1
3542
3596
  parts.append("%s%s;" % ("return " if last else "", self.expr(st.expr)))
3597
+ elif isinstance(st, A.Let) and isinstance(st.init, A.Try):
3598
+ parts.append(self._route_try(st.init, decl, in_loop, let=st))
3599
+ elif isinstance(st, A.Try):
3600
+ parts.append(self._route_try(st, decl, in_loop))
3543
3601
  elif isinstance(st, A.Let):
3544
3602
  init = self.expr(st.init) if st.init is not None else "undefined"
3545
3603
  parts.append("%s %s = %s;" % (decl, self._bind_local(st.name), init))
@@ -3576,14 +3634,89 @@ class Emitter:
3576
3634
  "E-LOOP-CONTROL should have caught this" % word)
3577
3635
  parts.append("break;" if word == "break" else "continue;")
3578
3636
  else:
3579
- hint = {"Match": " Use if / elif / else instead.",
3580
- "Try": " Propagate with `?`, or check the value with if."}
3637
+ hint = {"Match": " Use if / elif / else instead."}
3581
3638
  raise BuildError(
3582
3639
  "route body statement `%s` is not lowered by vlbuild -- emitting it "
3583
3640
  "would silently skip it.%s" % (type(st).__name__,
3584
3641
  hint.get(type(st).__name__, "")))
3585
3642
  return parts
3586
3643
 
3644
+ def _route_try(self, t: A.Try, decl: str, in_loop: bool,
3645
+ let: A.Let | None = None) -> str:
3646
+ """`try EXPR` / `catch ...` in a route body (spec section 6.2), both
3647
+ forms. It used to fail the build ("route body statement `Try` is
3648
+ not lowered") although `volaro check` accepted it, so the spec's own
3649
+ `catch e` / `return err(e)` example could not be built.
3650
+
3651
+ - An arm headed by an error code (`catch not_found`), a declared
3652
+ union variant or an auth error code runs when the thrown error's
3653
+ `code` is that head or starts with `head.`, binding the variant's
3654
+ fields by name. Any other head (`catch e`, `catch _`) catches
3655
+ every error and binds it to `e`; arms after it are unreachable,
3656
+ as in a view handler. An error no arm matches is rethrown.
3657
+ - Statement form: a handled error does not fall through. The arm
3658
+ ends the route: its last expression statement is the route's
3659
+ value (like a route body's own last line); otherwise the route
3660
+ returns nil.
3661
+ - `let x = try EXPR`: `x` is EXPR's value, or the value of the arm
3662
+ that ran (its last expression statement); the statements after
3663
+ the `let` then run as usual.
3664
+ - `err(e)` inside an arm rethrows the caught error unchanged
3665
+ (`_ex_ServerErr`)."""
3666
+ call_js = self.expr(t.expr)
3667
+ out: list[str] = []
3668
+ target = None
3669
+ if let is not None:
3670
+ target = self._bind_local(let.name)
3671
+ out.append("%s %s;" % (decl, target))
3672
+ out.append("try { %s = await %s; } catch (_vlErr) {" % (target, call_js))
3673
+ else:
3674
+ out.append("try { await %s; } catch (_vlErr) {" % call_js)
3675
+ fallback = False
3676
+ for i, (pat, body) in enumerate(t.catches):
3677
+ head = getattr(pat, "head", "")
3678
+ is_code = self._is_catch_code(head)
3679
+ if is_code:
3680
+ out.append("%s (%s) {" % ("if" if i == 0 else "else if",
3681
+ _catch_code_test(head)))
3682
+ else:
3683
+ out.append("{" if i == 0 else "else {")
3684
+ fallback = True
3685
+ self._push_scope()
3686
+ caught = None
3687
+ try:
3688
+ binds = list(getattr(pat, "binds", []) or [])
3689
+ if is_code:
3690
+ for name in binds:
3691
+ if name != head:
3692
+ out.append("%s %s = _vlErr[%s];" % (
3693
+ decl, self._bind_local(name), json.dumps(name)))
3694
+ elif binds and head != "_":
3695
+ caught = self._bind_local(binds[0])
3696
+ out.append("%s %s = _vlErr;" % (decl, caught))
3697
+ self._caught.add(caught)
3698
+ stmts = list(body)
3699
+ last = stmts[-1] if stmts and isinstance(stmts[-1], A.ExprStmt) else None
3700
+ head_stmts = stmts[:-1] if last is not None else stmts
3701
+ out.extend(self._route_block(head_stmts, decl, in_loop, tail=False))
3702
+ if let is not None:
3703
+ if last is not None:
3704
+ out.append("%s = %s;" % (target, self.expr(last.expr)))
3705
+ elif last is not None:
3706
+ out.append("return %s;" % self.expr(last.expr))
3707
+ elif not (stmts and isinstance(stmts[-1], A.Return)):
3708
+ out.append("return null;")
3709
+ finally:
3710
+ self._caught.discard(caught)
3711
+ self._pop_scope()
3712
+ out.append("}")
3713
+ if fallback:
3714
+ break
3715
+ if not fallback:
3716
+ out.append("else { throw _vlErr; }")
3717
+ out.append("}")
3718
+ return " ".join(out)
3719
+
3587
3720
  def expr(self, e) -> str:
3588
3721
  if e is None:
3589
3722
  return "undefined"
@@ -3760,16 +3893,28 @@ class Emitter:
3760
3893
  dispatcher's catch handler forwards every other own field (so
3761
3894
  `err(invalid_field("title"))` reaches the client as
3762
3895
  `{error: "invalid_field", field: "title"}`, not just a bare code)."""
3763
- standard = {"not_found", "bad_request", "unauthorized", "forbidden"}
3896
+ # `err(e)` in a route's `catch e` arm (spec section 6.2's own
3897
+ # example): rethrow the caught error unchanged, so its code and
3898
+ # fields reach the error response as they would have with no
3899
+ # `try` at all.
3900
+ if isinstance(inner, A.Name) and self._js_name(inner.id) in self._caught:
3901
+ return "(() => { throw %s; })()" % self._js_name(inner.id)
3902
+ # Every spec error code (vlcheck's `_ERROR_CODES`: `conflict`,
3903
+ # `rejected`, ... -- the checker accepts each as the argument of
3904
+ # `err(...)`), plus the server's own `bad_request` / `unauthorized`.
3905
+ standard = set(_ERROR_CODES) | {"bad_request", "unauthorized"}
3764
3906
  if not (isinstance(inner, A.Call) and isinstance(inner.callee, A.Name)
3765
3907
  and (inner.callee.id in self.variant_params
3766
3908
  or inner.callee.id in standard)):
3767
3909
  got = (inner.callee.id if isinstance(inner, A.Call)
3768
- and isinstance(inner.callee, A.Name) else type(inner).__name__)
3910
+ and isinstance(inner.callee, A.Name)
3911
+ else inner.id if isinstance(inner, A.Name)
3912
+ else type(inner).__name__)
3769
3913
  raise BuildError(
3770
- "server target: `err(...)` argument must be a standard error "
3771
- "or a declared error-union variant constructor — `%s` is not "
3772
- "lowered" % got)
3914
+ "E-ERR-ARG: `err(...)` takes an error code call "
3915
+ "(`not_found(\"...\")`), a declared error-union variant "
3916
+ "(`invalid_field(\"title\")`), or the error a `catch e` arm "
3917
+ "caught -- `%s` is not lowered" % got)
3773
3918
  variant = inner.callee.id
3774
3919
  param_names = (self.variant_params[variant]
3775
3920
  if variant in self.variant_params else ["message"])
@@ -3902,8 +4047,35 @@ class Emitter:
3902
4047
  raise BuildError(
3903
4048
  "E-LIST-OPERATOR: '%s' with a list operand is not defined; use "
3904
4049
  "concat(a, b) or xs |> append(x)" % e.op)
4050
+ if e.op in ("==", "!="):
4051
+ return self._equality_js(e)
3905
4052
  return "(%s %s %s)" % (self.expr(e.left), js, self.expr(e.right))
3906
4053
 
4054
+ def _equality_js(self, e: A.Binary) -> str:
4055
+ """`==` / `!=` (spec section 4.3: records and lists compare by value,
4056
+ never by identity). JavaScript's `===` compares objects by identity,
4057
+ so `{a: 1} == {a: 1}` was false and a list of records never matched
4058
+ itself, while `unique` compared the same values structurally. The
4059
+ emitter has no types, so it decides by shape: when either side is a
4060
+ known scalar (`_known_scalar`) the comparison stays `===`; a `nil`
4061
+ literal on either side is a null-or-undefined test (`== null`, since
4062
+ a missing optional field is nil too); anything else calls `VL.eq`,
4063
+ which is a single `===` for two scalars and structural for records
4064
+ and lists. Both targets have `VL.eq` (the shared std block)."""
4065
+ neg = e.op == "!="
4066
+ left, right = self.expr(e.left), self.expr(e.right)
4067
+ nil = lambda x: ((isinstance(x, A.Literal) and x.kind == "nil")
4068
+ or (isinstance(x, A.Name) and x.id == "nil"
4069
+ and self._js_name("nil") is None))
4070
+ if nil(e.left) or nil(e.right):
4071
+ other = right if nil(e.left) else left
4072
+ return "(%s %s null)" % (other, "!=" if neg else "==")
4073
+ bare = {v for v, ps in self.variant_params.items()
4074
+ if not ps and self._js_name(v) is None}
4075
+ if _known_scalar(e.left, bare) or _known_scalar(e.right, bare):
4076
+ return "(%s %s %s)" % (left, "!==" if neg else "===", right)
4077
+ return "%sVL.eq(%s, %s)" % ("!" if neg else "", left, right)
4078
+
3907
4079
  def _ex_Unary(self, e: A.Unary) -> str:
3908
4080
  return "(%s%s)" % ("!" if e.op == "not" else e.op, self.expr(e.operand))
3909
4081
 
@@ -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
- const VL = {
1334
- len: (x) => (x == null ? 0 : x.length),
1335
- std: (function () {
1336
- const VL = {};
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
- return VL.std;
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
@@ -49,6 +49,8 @@ run either way goes *before* the `try`; fire-and-forget uses `spawn`.
49
49
  try auth.sign_in(email: email, password: password)
50
50
  catch invalid_credentials
51
51
  error = "Wrong email or password." # no `return` needed
52
+ catch e
53
+ error = "Something went wrong."
52
54
  route.to("/") # reached only when nothing was caught
53
55
  ```
54
56
  `let v = try EXPR catch ...` above is different: it binds a value (whichever arm
@@ -59,6 +61,12 @@ fall-through bug this rule exists to prevent — put that logic in each `catch`
59
61
  arm (or in an `if`/`else` reading a flag the `catch` set, declared `let mut`)
60
62
  instead.
61
63
 
64
+ `catch not_found` (an error code, an auth code like `invalid_credentials`, or a
65
+ variant you declared) runs for that code only; `catch decode` also catches
66
+ `decode.missing_field`. Any other name (`catch e`) or `catch _` catches
67
+ everything, so it goes last. An error no arm matches is rethrown: in a view
68
+ handler, end the arms with `catch e` or check warns `W-CATCH-NO-FALLBACK`.
69
+
62
70
  `if C then else E` is also an expression (`derive x = if ok "a" else "b"`):
63
71
  `else` **required**, **binary only** (nest for 3+: `if a X else (if b Y else Z)`),
64
72
  each branch **one simple expression**, and the whole `derive` on **one line**
@@ -119,6 +127,10 @@ derive shown = items |> filter(fn(a) q == "" or contains(lower(a.title), lower(t
119
127
 
120
128
  # no + on lists, no spread — concat/append are the only way to grow one:
121
129
  concat(xs, ys) · xs |> append(x)
130
+ # does some / every item pass? any(xs, f) / all(xs, f) -> bool
131
+ if not (cs |> any(fn(c) c == a.category))
132
+ cs = cs |> append(a.category)
133
+ # == / != compare records and lists by value (fields, items), like unique(xs)
122
134
 
123
135
  # exact pattern validation (std.str's `regex`) -- no implicit anchoring,
124
136
  # caller supplies ^/$. An invalid pattern never throws -- matches() always
@@ -211,7 +223,11 @@ errors # per-file code -> status; `_` required
211
223
  Handler shape: `method [action] (params) Ret[!Err] [as alias]` + body. One
212
224
  unnamed handler per method per file. `?` propagates only errors in the
213
225
  handler's declared `!E` set (db failures are always fine — they become the
214
- opaque internal error); translate anything else with try/catch.
226
+ opaque internal error); translate anything else with try/catch. `err(...)`
227
+ takes an error code call (`err(conflict("taken"))`), a variant of an error
228
+ union declared in `app.vl`, or a caught error: `catch e` then `return err(e)`
229
+ rethrows it. Anything else is `E-ERR-ARG`. `volaro check` resolves these
230
+ files like any other.
215
231
 
216
232
  Views call the structural client — one options object, dynamic segments are
217
233
  params: `api.posts.list(limit: 20)` · `api.posts.get(id:)` ·
package/language/spec.md CHANGED
@@ -413,7 +413,9 @@ catch e
413
413
  return err(e)
414
414
  ```
415
415
 
416
- `?` requires the enclosing function to be fallible with a compatible error type. `??` takes a value of the success type. `try`/`catch` matches on the error union with the same exhaustiveness rules as `match`.
416
+ `?` requires the enclosing function to be fallible with a compatible error type. `??` takes a value of the success type. `try`/`catch` matches on the error union with the same exhaustiveness rules as `match`; `volaro check` does not enforce them yet.
417
+
418
+ A `catch` arm headed by an error code (`catch not_found`), an auth error code or a declared union variant runs when the error's `code` is that name or starts with that name and a dot, so `catch decode` also catches `decode.missing_field` (§9). Any other head (`catch e`, `catch _`) catches every error, and arms after it never run. An error no arm matches is rethrown, in a view handler and in a route body alike. In a view handler, a `try` whose arms all name codes is the warning `W-CATCH-NO-FALLBACK`, because a 500 or a network failure then leaves the handler: end it with `catch e` to handle the rest.
417
419
 
418
420
  **A handled error does not fall through.** When a `catch` arm runs, the statements after the `try` block do **not** execute — the `try` statement is complete. This is the opposite of the usual imperative reading and it is chosen deliberately: the fall-through version produced a login form that showed an error message *and* redirected to the success page, which was the single most common bug in early Volaro examples.
419
421
 
@@ -1063,7 +1065,7 @@ page title:"Weekly Report"
1063
1065
 
1064
1066
  `title:` is an expression evaluated at module scope (a literal, or something a module-level `fn` / config produces — not view state). It becomes the document `<title>` when it is a literal, and is assigned to `document.title` at boot regardless, so a full navigation to another page picks up that page's own title. `page` with no `title:`, or a statically empty one, is `E-PAGE-TITLE`; a build that renders a page with no `page` declaration at all is refused (`E-PAGE-TITLE` at build time). A `title:` expression that resolves to empty at load time raises `render.empty_page_title`. One `page` per module (`E-PAGE-DUP`).
1065
1067
 
1066
- **Headings.** `h1`…`h6` are `std.ui` primitives that emit the real HTML heading elements. The level is **semantic and independent of visual size** — `h2 "…" size:28` is a second-level heading that happens to be large. Within one view, a heading that jumps more than one level below the previous one is `W-HEADING-SKIP`, and a second `h1` is `W-HEADING-MULTI-H1` — warnings, not errors, and scoped to a single view so a reusable component that legitimately starts at `h2` or `h3` under some page's `h1` is never flagged.
1068
+ **Headings.** `h1`…`h6` are `std.ui` primitives that emit the real HTML heading elements. The level is **semantic and independent of visual size** — `h2 "…" size:28` is a second-level heading that happens to be large. Within one view, a heading that jumps more than one level below the previous one is `W-HEADING-SKIP`, and a second `h1` the view can render at the same time is `W-HEADING-MULTI-H1` (one `h1` in each branch of an `if` is one `h1`) — warnings, not errors, and scoped to a single view so a reusable component that legitimately starts at `h2` or `h3` under some page's `h1` is never flagged.
1067
1069
 
1068
1070
  **Landmarks.** `main` and `nav` are `std.ui` primitives emitting `<main>` / `<nav>`. There is one `main` region per page (`E-MAIN-DUP` for two in a view). A `nav` takes `aria_label:` (emitted as `aria-label`); when a view has more than one `nav`, each **must** carry a distinct name (`E-NAV-NAME`) so assistive tech can tell them apart. These rules hold on the **composed** document, not just within one view: a `main` in the page-root view and in a view it renders (or a view whose `main` is reused) is `E-MAIN-COMPOSED` — every `main` emits `id="vl-main"`, so a second one is also a duplicate id and an ambiguous skip target; two `<nav>`s assistive tech cannot tell apart — both unnamed, or sharing one `aria_label:` — from different composed views are likewise `E-NAV-NAME`; and one static `id:` rendered by more than one element is `E-ID-DUP`. The composed pass follows component instantiation within a module from the page root and treats `if` / `else` branches as mutually exclusive (a landmark in both branches counts once); it does not evaluate conditions, and cross-file `use` composition is not yet followed.
1069
1071
 
@@ -1702,7 +1704,7 @@ The library is small on purpose. A large surface is a large thing to remember an
1702
1704
  | `std.db` | query builder, migrations, transactions |
1703
1705
  | `std.time` | `now`, formatting, parsing, arithmetic on `time` and `dur` |
1704
1706
  | `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) |
1707
+ | `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
1708
  | `std.map` | keys, values, merge, get_or |
1707
1709
  | `std.json` | encode, decode, pointer access |
1708
1710
  | `std.crypto` | random bytes, hashing, timing-safe compare |
@@ -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`, `matches`,
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`,
@@ -464,6 +472,13 @@ audio video source track picture map area
464
472
  prop or field (`on_retry()`) are not value methods and are unaffected.
465
473
  - `try call()?` is a warning (`W-TRY-PROPAGATE`): the `catch` arms already
466
474
  handle the error, so the `?` does nothing. Drop it.
475
+ - `catch not_found` in a view handler (an error code, an auth error code
476
+ or a declared variant) runs for that code only, the same as in a route
477
+ body; `catch decode` also catches `decode.missing_field`. `catch e` /
478
+ `catch _` catches anything. An error no arm matches is rethrown. Through
479
+ 0.1.0-alpha.13, `catch not_found` and the other standard codes caught
480
+ every error in a view handler. A handler `try` whose arms all name codes
481
+ is a warning (`W-CATCH-NO-FALLBACK`): add a final `catch e`.
467
482
 
468
483
  ### Data / services
469
484
  - `service` with `db.<model>` query builder (`where` / `order` / `limit` /
@@ -472,9 +487,23 @@ audio video source track picture map area
472
487
  writes.
473
488
  - Route bodies (services and `page.server.vl` handlers) run `let`,
474
489
  assignment (`=`, `+=` …), `if`/`elif`/`else`, `for`, `while`, `break`,
475
- `skip` and `return`; the last expression is the route's value. Any other
476
- statement (`match` or bare `try` as a statement, `par`, `spawn`) fails
490
+ `skip`, `return` and `try`/`catch`; the last expression is the route's
491
+ value. Any other statement (`match` as a statement, `par`, `spawn`) fails
477
492
  the build, at any nesting depth. Use `?` to propagate errors.
493
+ - `try`/`catch` in a route body, both forms. `catch not_found` (an error
494
+ code, or a variant of a union you declare) runs for that code (or a
495
+ `not_found.` code) only and binds the variant's fields; `catch e`
496
+ catches anything. An error no arm
497
+ matches propagates. The bare statement form does not fall through: the
498
+ arm ends the route, and its last expression is the route's value.
499
+ `let x = try ...` gives `x` the arm's last expression and carries on.
500
+ `catch e` then `return err(e)` rethrows the caught error as it was.
501
+ - `page.server.vl` handlers are resolved by `volaro check` like service
502
+ routes: an undefined name, a wrong argument count, an unknown field of
503
+ a typed parameter or a bad `err(...)` argument is reported there. In
504
+ scope: the project's `app.vl` declarations, the handler's parameters,
505
+ `session`, `request`, and the `_server.vl` context providers above the
506
+ file.
478
507
  - Password auth: `auth.sign_up` / `sign_in` / `sign_out` / `reset_password` /
479
508
  `confirm_reset` / `confirm_verify` against compiler-owned endpoints.
480
509
  Any other `auth.` name is `E-AUTH-METHOD` in `volaro check` and the build,
@@ -507,6 +536,19 @@ audio video source track picture map area
507
536
  - `std.list` `concat` / `append`; `xs |> map|filter(fn(x) …)`. (`fold` is in
508
537
  `volaro spec`'s stdlib table but is **not implemented** in this build —
509
538
  found stale while documenting `matches` below; corrected here, not fixed.)
539
+ - `std.list` `any(xs, f)` / `all(xs, f)` — whether some / every item
540
+ passes `f` (a `bool`). `any` of an empty list is `false`, `all` of an
541
+ empty list is `true`. Both forms work, in views, handlers and route bodies:
542
+ ```vl
543
+ if not (cs |> any(fn(c) c == a.category))
544
+ cs = cs |> append(a.category)
545
+ derive all_done = all(tasks, fn(t) t.done)
546
+ ```
547
+ - `==` and `!=` compare records and lists by value, the way `unique` does
548
+ (spec section 4.3): two records are equal when their fields are, two lists
549
+ when their items are, nested records included, in views and route bodies
550
+ alike. `nil` equals a missing optional field. Numbers, strings and bools
551
+ compare as before.
510
552
  - `std.list` `unique(xs)` — the first occurrence of each distinct value, in
511
553
  list order, as a new list. Scalars compare as `==` does (`1` and `"1"`
512
554
  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.12",
3
+ "version": "0.1.0-alpha.14",
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",