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 +1 -1
- package/compiler/SOURCE_INFO.json +2 -2
- package/compiler/SOURCE_REV +1 -1
- package/compiler/validator/vlcheck/__main__.py +10 -6
- package/compiler/validator/vlcheck/checks.py +66 -14
- package/compiler/validator/vlcheck/resolve.py +138 -6
- package/compiler/vlbuild/vlbuild/assets/vlrt.js +37 -10
- package/compiler/vlbuild/vlbuild/assets/vlrt.min.js +1 -1
- package/compiler/vlbuild/vlbuild/emit.py +194 -22
- package/compiler/vlbuild/vlbuild/project.py +4 -2
- package/compiler/vlbuild/vlbuild/server_emit.py +7 -8
- package/language/crib.md +17 -1
- package/language/spec.md +5 -3
- package/language/supported.md +46 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Volaro
|
|
2
2
|
|
|
3
|
-
**Limited alpha** (this is `0.1.0-alpha.
|
|
3
|
+
**Limited alpha** (this is `0.1.0-alpha.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": "
|
|
2
|
+
"revision": "e01dc1d92881e38143a73b21dfdf0660cb515906",
|
|
3
3
|
"source": "git",
|
|
4
4
|
"dirty": false,
|
|
5
|
-
"content_sha256": "
|
|
5
|
+
"content_sha256": "6476761bc8a3508734fee9684b80e0efe0ae151a6a40b5f5586bf1595e3a960d"
|
|
6
6
|
}
|
package/compiler/SOURCE_REV
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
do_resolve = False
|
|
72
|
+
# A `_server.vl` holds only `context` providers; the resolver
|
|
73
|
+
# has no pass for them yet.
|
|
74
|
+
do_resolve = False
|
|
71
75
|
else:
|
|
72
76
|
mod = parser.parse_module()
|
|
73
77
|
diags = (list(lx.diags) + list(parser.diags)
|
|
74
78
|
+ check(mod, filename, li, strict_names=strict_names, release=release))
|
|
75
79
|
if do_resolve:
|
|
76
|
-
diags +=
|
|
80
|
+
diags += resolver(mod, filename, li)
|
|
77
81
|
diags.sort(key=lambda d: (d.start, 0 if d.severity == Severity.ERROR else 1, d.code))
|
|
78
82
|
return li, diags
|
|
79
83
|
|
|
@@ -105,12 +105,13 @@ _METHOD_FN_FORM = {
|
|
|
105
105
|
"matches": "matches", "match": "matches", "test": "matches",
|
|
106
106
|
"first_char": "first_char", "split": "split", "join": "join",
|
|
107
107
|
"filter": "filter", "map": "map", "take": "take", "count": "count",
|
|
108
|
+
"some": "any", "every": "all",
|
|
108
109
|
"sum": "sum", "sort_by": "sort_by", "group_by": "group_by",
|
|
109
110
|
"chunk": "chunk", "concat": "concat", "append": "append", "push": "append",
|
|
110
111
|
"keys": "keys", "values": "values", "get_or": "get_or",
|
|
111
112
|
}
|
|
112
113
|
_LIST_FNS = {"filter", "map", "take", "count", "sum", "sort_by", "group_by",
|
|
113
|
-
"chunk", "concat", "append", "join"}
|
|
114
|
+
"chunk", "concat", "append", "join", "any", "all"}
|
|
114
115
|
# Other common carry-overs with a Volaro spelling that is not a function call.
|
|
115
116
|
_METHOD_OTHER_FIX = {
|
|
116
117
|
"length": "`{r}.len()`", "size": "`{r}.len()`",
|
|
@@ -153,6 +154,8 @@ STD_BUILT_FNS = frozenset({
|
|
|
153
154
|
"concat", "append",
|
|
154
155
|
# `unique(xs)`: first occurrence of each distinct value, in order
|
|
155
156
|
"unique",
|
|
157
|
+
# `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`
|
|
158
|
+
"any", "all",
|
|
156
159
|
# std.str (spec section 13: split, join, trim, case, replace, contains,
|
|
157
160
|
# regex -- `matches`)
|
|
158
161
|
"join", "split", "first_char", "trim", "upper", "lower", "replace",
|
|
@@ -182,9 +185,7 @@ _FN_NOT_BUILT = {
|
|
|
182
185
|
"index_of": "a `for i in 0..xs.len()` loop that compares `xs[i]`",
|
|
183
186
|
"slice": "take(xs, n) for a prefix, or a `for i in a..b` loop",
|
|
184
187
|
"range": "a `for i in 0..n` loop",
|
|
185
|
-
"
|
|
186
|
-
"all": "count(filter(xs, f)) == count(xs)",
|
|
187
|
-
"none": "count(filter(xs, f)) == 0",
|
|
188
|
+
"none": "not any(xs, f)",
|
|
188
189
|
"avg": "sum(xs) / count(xs) (check count(xs) > 0 first)",
|
|
189
190
|
"min": "if (a < b) a else b",
|
|
190
191
|
"max": "if (a > b) a else b",
|
|
@@ -235,7 +236,7 @@ CONTEXTUAL_WORDS = frozenset({"asc", "desc", "google", "github"})
|
|
|
235
236
|
_WHERE_PREDICATE_FIX = {
|
|
236
237
|
"lt": "`a < b`", "lte": "`a <= b`", "gt": "`a > b`", "gte": "`a >= b`",
|
|
237
238
|
"ne": "`a != b`", "between": "`lo <= x and x <= hi`",
|
|
238
|
-
"any_of": "`
|
|
239
|
+
"any_of": "`any(xs, fn(v) v == x)`",
|
|
239
240
|
}
|
|
240
241
|
# Error codes (spec section 6): a code is constructed only as the argument of
|
|
241
242
|
# `err(...)` in a service or page-server route body, where the build turns
|
|
@@ -246,6 +247,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,
|
|
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
|
-
|
|
607
|
-
self._scoped(body, inner)
|
|
657
|
+
self._scoped(body, scopes + [self._catch_scope(pat)])
|
|
608
658
|
elif isinstance(st, (A.Break, A.Skip)):
|
|
609
659
|
pass
|
|
610
660
|
elif isinstance(st, A.StateDecl):
|
|
@@ -729,6 +779,7 @@ class Resolver:
|
|
|
729
779
|
self._expr(v, scopes)
|
|
730
780
|
self._check_call(ex, scopes)
|
|
731
781
|
self._check_view_module_fn(ex, scopes)
|
|
782
|
+
self._check_err_arg(ex, scopes)
|
|
732
783
|
elif isinstance(ex, A.Unary):
|
|
733
784
|
self._expr(ex.operand, scopes)
|
|
734
785
|
elif isinstance(ex, A.Binary):
|
|
@@ -751,8 +802,7 @@ class Resolver:
|
|
|
751
802
|
# `vlbuild` refused it (Testing Lab 2026-09-27, volaro-F2).
|
|
752
803
|
self._expr(ex.expr, scopes)
|
|
753
804
|
for pat, body in ex.catches:
|
|
754
|
-
|
|
755
|
-
self._scoped(body, inner)
|
|
805
|
+
self._scoped(body, scopes + [self._catch_scope(pat)])
|
|
756
806
|
elif isinstance(ex, A.IfExpr):
|
|
757
807
|
self._expr(ex.cond, scopes)
|
|
758
808
|
self._expr(ex.then, scopes)
|
|
@@ -999,6 +1049,69 @@ class Resolver:
|
|
|
999
1049
|
self._arity(Sym("fn", params=[(n, False, not req) for n, req in params]),
|
|
1000
1050
|
ex, f"'auth.{m}'")
|
|
1001
1051
|
|
|
1052
|
+
def _catch_scope(self, pat) -> dict:
|
|
1053
|
+
"""The bindings a `catch` arm adds. An arm headed by an error code or
|
|
1054
|
+
a declared union variant binds the variant's fields; any other head
|
|
1055
|
+
(`catch e`) binds the caught error itself, marked `_CAUGHT` so
|
|
1056
|
+
`err(e)` can rethrow it."""
|
|
1057
|
+
head = getattr(pat, "head", "")
|
|
1058
|
+
binds = list(getattr(pat, "binds", []) or [])
|
|
1059
|
+
sym = self.table.get(head)
|
|
1060
|
+
if (head == "_" or head in CATCH_CODES
|
|
1061
|
+
or (sym is not None and sym.kind == "variant")
|
|
1062
|
+
or binds != [head]):
|
|
1063
|
+
return {b: None for b in binds}
|
|
1064
|
+
return {head: _CAUGHT}
|
|
1065
|
+
|
|
1066
|
+
def _check_err_arg(self, ex: A.Call, scopes) -> None:
|
|
1067
|
+
"""E-ERR-ARG: in a route body the build lowers `err(X)` only when X
|
|
1068
|
+
is a call to an error code (`conflict("x")`) or to a declared union
|
|
1069
|
+
variant (`invalid_field("title")`), or is the error a `catch e` arm
|
|
1070
|
+
bound (`err(e)` rethrows it). Anything else checked clean and then
|
|
1071
|
+
failed the build ("`err(...)` argument must be a standard error
|
|
1072
|
+
..."). A view's `err(...)` is checks.py's E-ERR-CONTEXT, and an
|
|
1073
|
+
undefined name is already R-UNDEF."""
|
|
1074
|
+
if not (self._in_route and isinstance(ex.callee, A.Name)
|
|
1075
|
+
and ex.callee.id == "err"):
|
|
1076
|
+
return
|
|
1077
|
+
if any("err" in sc for sc in scopes) or "err" in self.table:
|
|
1078
|
+
return
|
|
1079
|
+
if len(ex.args) != 1 or ex.named:
|
|
1080
|
+
self._d("E-ERR-ARG",
|
|
1081
|
+
"`err(...)` takes exactly one positional argument, the "
|
|
1082
|
+
"error -- the build cannot lower this call", ex,
|
|
1083
|
+
"use " + _ERR_ARG_FIX)
|
|
1084
|
+
return
|
|
1085
|
+
arg = ex.args[0]
|
|
1086
|
+
if isinstance(arg, A.Call) and isinstance(arg.callee, A.Name):
|
|
1087
|
+
n = arg.callee.id
|
|
1088
|
+
sym = self.table.get(n)
|
|
1089
|
+
if any(n in sc for sc in scopes):
|
|
1090
|
+
what = f"'{n}' is a local binding, not an error constructor"
|
|
1091
|
+
elif sym is not None and sym.kind == "variant":
|
|
1092
|
+
return
|
|
1093
|
+
elif sym is None and n in _ERROR_CODES:
|
|
1094
|
+
return
|
|
1095
|
+
elif sym is None and not self._resolves(n, scopes):
|
|
1096
|
+
return # already R-UNDEF
|
|
1097
|
+
else:
|
|
1098
|
+
what = f"'{n}' is not an error code or a declared union variant"
|
|
1099
|
+
elif isinstance(arg, A.Name):
|
|
1100
|
+
if self._typeof(arg, scopes) == _CAUGHT:
|
|
1101
|
+
return
|
|
1102
|
+
if not self._resolves(arg.id, scopes):
|
|
1103
|
+
return # already R-UNDEF
|
|
1104
|
+
what = (f"'{arg.id}' is not an error a `catch` arm caught"
|
|
1105
|
+
+ (" -- call the error code: "
|
|
1106
|
+
f"`err({arg.id}(\"...\"))`"
|
|
1107
|
+
if arg.id in _ERROR_CODES else ""))
|
|
1108
|
+
else:
|
|
1109
|
+
what = "the argument is not an error constructor call"
|
|
1110
|
+
self._d("E-ERR-ARG",
|
|
1111
|
+
f"{what}, so `err(...)` cannot be built -- the build lowers "
|
|
1112
|
+
"`err(...)` only for an error code, a declared union variant "
|
|
1113
|
+
"or a caught error", ex, "use " + _ERR_ARG_FIX)
|
|
1114
|
+
|
|
1002
1115
|
def _check_view_module_fn(self, ex: A.Call, scopes) -> None:
|
|
1003
1116
|
"""A view -- or a closure / `on` handler / block inside one -- that
|
|
1004
1117
|
calls a module-level `fn` resolves fine, then `vlbuild` refuses it:
|
|
@@ -1187,3 +1300,22 @@ def resolve(mod: A.Module, filename: str, li: LineIndex,
|
|
|
1187
1300
|
layout_params: dict | None = None) -> list[Diagnostic]:
|
|
1188
1301
|
return Resolver(filename, li, page_clients=page_clients,
|
|
1189
1302
|
layout_params=layout_params).run(mod)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
def _project_root(filename: str) -> str | None:
|
|
1306
|
+
"""The project a page-server file belongs to: the parent of the nearest
|
|
1307
|
+
enclosing `app/` directory, or None for a file outside one."""
|
|
1308
|
+
d = os.path.dirname(os.path.abspath(filename))
|
|
1309
|
+
while True:
|
|
1310
|
+
if os.path.basename(d) == "app":
|
|
1311
|
+
return os.path.dirname(d)
|
|
1312
|
+
parent = os.path.dirname(d)
|
|
1313
|
+
if parent == d:
|
|
1314
|
+
return None
|
|
1315
|
+
d = parent
|
|
1316
|
+
|
|
1317
|
+
|
|
1318
|
+
def resolve_page_server(mod: A.PageServerFile, filename: str,
|
|
1319
|
+
li: LineIndex) -> list[Diagnostic]:
|
|
1320
|
+
"""The resolver pass for a `page.server.vl` file (`Resolver.run_page_server`)."""
|
|
1321
|
+
return Resolver(filename, li).run_page_server(mod)
|
|
@@ -86,21 +86,43 @@
|
|
|
86
86
|
// of VL.std (test_std_fn_parity in tests/test_build.py checks this file,
|
|
87
87
|
// the minified copy and server.js).
|
|
88
88
|
function asArray(x) { return x == null ? [] : x; }
|
|
89
|
-
// Volaro equality
|
|
90
|
-
//
|
|
91
|
-
// (
|
|
89
|
+
// Volaro equality (spec section 4.3), used by `==` / `!=` (VL.eq below)
|
|
90
|
+
// and `unique`: scalars compare as `===` does, `nil` is null or undefined
|
|
91
|
+
// (a missing optional field is nil), and records and lists compare
|
|
92
|
+
// structurally, equal when their fields or items are. No identity or
|
|
93
|
+
// reference semantics. Only plain objects (a record literal, a decoded
|
|
94
|
+
// JSON value, a database row) and arrays are compared structurally; any
|
|
95
|
+
// other object compares by identity.
|
|
96
|
+
function isPlain(x) {
|
|
97
|
+
if (Array.isArray(x)) return true;
|
|
98
|
+
var p = Object.getPrototypeOf(x);
|
|
99
|
+
return p === Object.prototype || p === null;
|
|
100
|
+
}
|
|
92
101
|
function stdEq(a, b) {
|
|
93
102
|
if (a === b) return true;
|
|
94
|
-
if (a
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
103
|
+
if (a == null || b == null) return a == null && b == null;
|
|
104
|
+
if (typeof a !== "object" || typeof b !== "object"
|
|
105
|
+
|| Array.isArray(a) !== Array.isArray(b) || !isPlain(a) || !isPlain(b)) return false;
|
|
106
|
+
var i;
|
|
107
|
+
if (Array.isArray(a)) {
|
|
108
|
+
if (a.length !== b.length) return false;
|
|
109
|
+
for (i = 0; i < a.length; i++) if (!stdEq(a[i], b[i])) return false;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
// A field that is nil on one side and absent on the other is equal.
|
|
113
|
+
var ka = Object.keys(a), kb = Object.keys(b);
|
|
114
|
+
for (i = 0; i < ka.length; i++) if (!stdEq(a[ka[i]], b[ka[i]])) return false;
|
|
115
|
+
for (i = 0; i < kb.length; i++) {
|
|
116
|
+
if (!Object.prototype.hasOwnProperty.call(a, kb[i]) && b[kb[i]] != null) return false;
|
|
101
117
|
}
|
|
102
118
|
return true;
|
|
103
119
|
}
|
|
120
|
+
// `a == b` where the build cannot tell both sides are scalars. A scalar
|
|
121
|
+
// pair costs one `===` and a typeof test; records and lists go to stdEq.
|
|
122
|
+
VL.eq = function (a, b) {
|
|
123
|
+
return a === b || (a == null ? b == null
|
|
124
|
+
: b != null && typeof a === "object" && typeof b === "object" && stdEq(a, b));
|
|
125
|
+
};
|
|
104
126
|
VL.std = {
|
|
105
127
|
filter: function (xs, f) { return asArray(xs).filter(function (x) { return f(x); }); },
|
|
106
128
|
map: function (xs, f) { return asArray(xs).map(function (x) { return f(x); }); },
|
|
@@ -152,6 +174,11 @@
|
|
|
152
174
|
get_or: function (m, k, dflt) {
|
|
153
175
|
return m && (k in m) ? m[k] : dflt;
|
|
154
176
|
},
|
|
177
|
+
// `any(xs, f)` / `all(xs, f)`: whether some / every item passes `f`.
|
|
178
|
+
// `any` of an empty list is false, `all` of an empty list is true. Both
|
|
179
|
+
// stop at the first item that settles the answer.
|
|
180
|
+
any: function (xs, f) { return asArray(xs).some(function (x) { return !!f(x); }); },
|
|
181
|
+
all: function (xs, f) { return asArray(xs).every(function (x) { return !!f(x); }); },
|
|
155
182
|
int_to_float: function (x) { return x; },
|
|
156
183
|
float_to_int: function (x) { return Math.trunc(x); },
|
|
157
184
|
// List growth (spec section 20.9) — the only way to combine or grow a
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/* minified vlrt.js; readable source in the volaro package */
|
|
2
|
-
"use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e,t){if(e===t)return!0;if(null===e||null===t||"object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t))return!1;var n=Object.keys(e);if(n.length!==Object.keys(t).length)return!1;for(var r=0;r<n.length;r++)if(!Object.prototype.hasOwnProperty.call(t,n[r])||!i(e[n[r]],t[n[r]]))return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var a=r[i.op];t=t.filter(function(e){return a(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,o=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<o.length;r++)if(i(o[r],e))return;o.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var a in i)if(Object.prototype.hasOwnProperty.call(i,a)){if(void 0===n[a])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+a);i[a]&&"object"==typeof i[a]&&e(n[a],i[a],o+"/"+a)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,a){var l,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(l=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(l))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!a)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(a){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=a.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var o=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function l(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){l(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function u(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function s(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function c(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function d(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function f(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function v(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function h(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),p(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function p(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return y(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=h(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function y(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),a=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):a.push(e)});var l=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(l[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!l[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],v=0;v<t._overlays.length;v++){var p=t._overlays[v];if(p.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!l[g._vlKey]&&g.contains&&g.contains(p.restoreFocus)){d.push({rec:p,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!l[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?h(r,e):e;i._vlKey=n,w.push(i)}else{var l=a[S++];w.push(l?h(l,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,a=0;a<e;a++){var u=i[a];u&&null!=u._vlKey&&l[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return f(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var C=A(m);if(C&&"function"==typeof C.focus)try{C.focus({preventScroll:!0})}catch(e){}}for(var x=0;x<d.length;x++)d[x].rec.restoreFallback=A(d[x].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,o[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=l,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},l(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",a=r+"e",l=e.input||{},u=l.attrs=l.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(a),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",l,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:a})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var a=t._mainLabel;if(a&&o.getAttribute){var l=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||l)&&(o.getAttribute("aria-label")!==a&&o.setAttribute("aria-label",a),l||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=s(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var a=i.body||u()&&u().parentNode||null;a&&a.appendChild(o);var l=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:l&&l!==i.body?l:null}),1===t._overlays.length&&c(u(),!0),o},close:function(n){var r=s(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(c(u(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&d(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=f(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var a;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return a},o)}),a=i(),null;var l=s(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var c=t._overlays[s(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),p=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(c.panel&&c.panel.parentNode===u&&u.replaceChild){var y=h(c.panel,p);y!==c.panel&&(u.replaceChild(y,c.panel),c.panel=y)}else if(u.replaceChildren)u.replaceChildren(p),c.panel=p;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(p),c.panel=p}return l&&(c._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=c.panel,r=f(n);if(r.length){var i=r[0],o=r[r.length-1],a=e.document.activeElement;t.shiftKey&&a===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||a!==o&&n.contains(a)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",c._onTab),c._onClick=function(e){e.target===u&&v(n)},u.addEventListener&&u.addEventListener("click",c._onClick),c._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||v(n)},e.document.addEventListener&&e.document.addEventListener("keydown",c._onKey),this.focusInto(c.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function a(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var l=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),a(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),a(r-1)):"Home"===t?(i(),a(0)):"End"===t&&(i(),a(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[l,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var a="undefined"!=typeof AbortController?new AbortController:null,u={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:a,signal:a?a.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),a=this._loads[e];if(!a||a.key!==o){a=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var l=this;Promise.resolve().then(r).then(function(n){l._loads[e]===a&&(a.pending=!1,a.value=n,l.schedule(),t._onLoadSettled&&t._onLoadSettled(l))},function(n){l._loads[e]===a&&(a.pending=!1,a.error=n,l.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(l,e,n),t._onLoadSettled&&t._onLoadSettled(l))})}return a},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,a=e.document,u=a&&a.activeElement,c=null;u&&u.getAttribute&&u.getAttribute("data-vl-bind")&&(!a.contains||a.contains(u))&&(c={node:u,key:u.getAttribute("data-vl-bind"),start:u.selectionStart,end:u.selectionEnd,direction:u.selectionDirection,scrollLeft:u.scrollLeft,scrollTop:u.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();l(f,i),r.replaceChild?p(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?p(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&a){var v=a.contains&&a.contains(c.node)?c.node:null;if(!v&&a.querySelectorAll){var h=Array.prototype.filter.call(a.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&a.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:u},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){u._composing=!0}),c.addEventListener("compositionend",function(){u._composing=!1,u.schedule()})),u.render(),u},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var g=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(g[n])return g[n];var i=e.document,o=new Promise(function(e,o){var a=i.createElement("script");a.src=r,a.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},a.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(a)});return g[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
|
|
2
|
+
"use strict";!function(e){var t={},n=null;function r(e){return null==e?[]:e}function i(e){if(Array.isArray(e))return!0;var t=Object.getPrototypeOf(e);return t===Object.prototype||null===t}function o(e,t){if(e===t)return!0;if(null==e||null==t)return null==e&&null==t;if("object"!=typeof e||"object"!=typeof t||Array.isArray(e)!==Array.isArray(t)||!i(e)||!i(t))return!1;var n;if(Array.isArray(e)){if(e.length!==t.length)return!1;for(n=0;n<e.length;n++)if(!o(e[n],t[n]))return!1;return!0}var r=Object.keys(e),l=Object.keys(t);for(n=0;n<r.length;n++)if(!o(e[r[n]],t[r[n]]))return!1;for(n=0;n<l.length;n++)if(!Object.prototype.hasOwnProperty.call(e,l[n])&&null!=t[l[n]])return!1;return!0}t.fetchData=function(){return n||(n=e.fetch("data.json",{cache:"no-store"}).then(function(e){if(!e.ok)throw new Error("HTTP "+e.status+" loading data.json");return e.json()})),n},t.db={},t.qb=function(e){var t=(e||[]).slice(),n={where:function(e){var r={lt:function(e,t){return e<t},lte:function(e,t){return e<=t},gt:function(e,t){return e>t},gte:function(e,t){return e>=t},ne:function(e,t){return e!==t}};return Object.keys(e||{}).forEach(function(n){var i=e[n];if(null!=i&&""!==i)if(i&&"object"==typeof i&&"string"==typeof i.op)if("between"===i.op)t=t.filter(function(e){return e[n]>=i.v[0]&&e[n]<=i.v[1]});else if("any_of"===i.op){var o=i.v||[];t=t.filter(function(e){return-1!==o.indexOf(e[n])})}else{var l=r[i.op];t=t.filter(function(e){return l(e[n],i.v)})}else t=t.filter(function(e){return e[n]===i})}),n},order:function(e){var r=Object.keys(e||{})[0];if(r){var i="desc"===e[r]?-1:1;t.sort(function(e,t){return e[r]<t[r]?-i:e[r]>t[r]?i:0})}return n},limit:function(e){return t=t.slice(0,e),n},all:function(){return t},first:function(){return t.length?t[0]:null}};return n},t.eq=function(e,t){return e===t||(null==e?null==t:null!=t&&"object"==typeof e&&"object"==typeof t&&o(e,t))},t.std={filter:function(e,t){return r(e).filter(function(e){return t(e)})},map:function(e,t){return r(e).map(function(e){return t(e)})},take:function(e,t){return r(e).slice(0,t)},chunk:function(e,t){for(var n=r(e),i=[],o=0;o<n.length;o+=t)i.push(n.slice(o,o+t));return i},count:function(e){return r(e).length},sum:function(e){return r(e).reduce(function(e,t){return e+(t||0)},0)},sort_by:function(e,t){return r(e).slice().sort(function(e,n){var r=t(e),i=t(n);return r<i?-1:r>i?1:0})},group_by:function(e,t){var n={};return r(e).forEach(function(e){var r=t(e);(n[r]=n[r]||[]).push(e)}),n},join:function(e,t){return r(e).join(void 0===t?"":t)},split:function(e,t){return String(e).split(t)},replace:function(e,t,n){return String(e).split(String(t)).join(String(n))},contains:function(e,t){return-1!==String(e).indexOf(String(t))},first_char:function(e){return String(e).slice(0,1)},trim:function(e){return String(e).trim()},upper:function(e){return String(e).toUpperCase()},lower:function(e){return String(e).toLowerCase()},keys:function(e){return Object.keys(e||{})},values:function(e){return Object.values(e||{})},get_or:function(e,t,n){return e&&t in e?e[t]:n},any:function(e,t){return r(e).some(function(e){return!!t(e)})},all:function(e,t){return r(e).every(function(e){return!!t(e)})},int_to_float:function(e){return e},float_to_int:function(e){return Math.trunc(e)},concat:function(e,t){return r(e).concat(r(t))},append:function(e,t){return r(e).concat([t])},unique:function(e){var t=[],n=new Set,i=[];return r(e).forEach(function(e){if(null!==e&&"object"==typeof e){for(var r=0;r<i.length;r++)if(o(i[r],e))return;i.push(e)}else if(e==e){if(n.has(e))return;n.add(e)}t.push(e)}),t},matches:function(e,t){try{return new RegExp(String(t)).test(String(e))}catch(e){return!1}}},t.len=function(e){return null==e?0:e.length},t.err=function(e,t){var n=new Error(t||e);return n.code=e,n},t.decode=function(e,n,r){return function e(n,i,o){if(null==n||"object"!=typeof n)throw t.err("decode.missing_field",(r||"response")+": expected an object at "+(o||"/"));for(var l in i)if(Object.prototype.hasOwnProperty.call(i,l)){if(void 0===n[l])throw t.err("decode.missing_field",(r||"response")+": missing "+o+"/"+l);i[l]&&"object"==typeof i[l]&&e(n[l],i[l],o+"/"+l)}return n}(e,n||{},"")},t.api=async function(n,r,i,o,l){var a,u,s,c={method:n,headers:{},credentials:"same-origin"};if(void 0!==i&&(c.headers["Content-Type"]="application/json",c.body=JSON.stringify(i)),/^(POST|PUT|PATCH|DELETE)$/.test(n)&&e.document){var d=(a=new RegExp("(?:^|;\\s*)"+(o||"vl_csrf").replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]+)"),(u=(e.document&&e.document.cookie||"").match(a))?decodeURIComponent(u[1]):null);d&&(c.headers["X-CSRF-Token"]=d)}try{s=await e.fetch(r,c)}catch(e){if(!l)throw e;var f=t.err("transport.network",n+" "+r+" -> network failure");throw f.kind="transport",f}if(!s.ok){var v=null;try{v=await s.json()}catch(e){}if(l){if(!v||"object"!=typeof v.error||null===v.error||"string"!=typeof v.error.code){var h=t.err("transport.malformed",n+" "+r+" -> "+s.status);throw h.kind="transport",h}var p=v.error.code,y=t.err(p,n+" "+r+" -> "+s.status),g=v.error.fields;throw y.fields=g&&"object"==typeof g?g:{},y.kind=l.indexOf(p)>=0?"domain":s.status>=500?"internal":"framework",y}var m=v&&v.error?v.error:404===s.status?"not_found":"http."+s.status,b=t.err(m,n+" "+r+" -> "+s.status);throw v&&"object"==typeof v&&Object.keys(v).forEach(function(e){"error"!==e&&(b[e]=v[e])}),b}return 204===s.status?null:s.json()},t.authApi=async function(n,r,i){async function o(){var n=await e.fetch("/__vl/auth/csrf",{method:"GET",credentials:"same-origin",cache:"no-store"});if(!n.ok)throw t.err("http."+n.status,"could not establish CSRF token")}await o();try{return await t.api(n,r,i,"vl_csrf_pre")}catch(e){if(e&&"forbidden"===e.code)return await o(),t.api(n,r,i,"vl_csrf_pre");throw e}};var l=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function a(e,n){if(null!=e&&!0!==e&&!1!==e){if(Array.isArray(e))throw t.err("render.multi_root_row","a keyed list row must be a single element; key "+JSON.stringify(n)+" maps to "+e.length+" sibling nodes -- wrap the row in one `col` / `row`");e.nodeType&&(e._vlKey=n)}}function u(e,t){null!=t&&!1!==t&&!0!==t&&(Array.isArray(t)?t.forEach(function(t){u(e,t)}):"undefined"!=typeof Node&&t instanceof Node||t&&t.nodeType?e.appendChild(t):e.appendChild(document.createTextNode(String(t))))}function s(){return t._appEl||e.document&&e.document.getElementById("vl-root")||null}function c(e){for(var n=0;n<t._overlays.length;n++)if(t._overlays[n].id===e)return n;return-1}function d(e,t){if(e)try{t?(e.inert=!0,e.setAttribute("aria-hidden","true")):(e.inert=!1,e.removeAttribute("aria-hidden"))}catch(e){}}function f(e,t){if(!e||!t)return!1;if("boolean"==typeof e.isConnected)return e.isConnected;if("function"==typeof t.contains)return t.contains(e);for(var n=e;n.parentNode;)n=n.parentNode;return n===t||n===t.body||n===t.documentElement}function v(e){if(!e||!e.querySelectorAll)return[];for(var t=e.querySelectorAll("a[href],button,input,select,textarea,[tabindex]"),n=[],r=0;r<t.length;r++){var i=t[r];!i.disabled&&(null==i.tabIndex||i.tabIndex>=0||"-1"!==i.getAttribute("tabindex"))&&n.push(i)}return n}function h(e){var n=t.overlay._dismiss[e];"function"==typeof n&&n()}function p(e,t){if(!e||e.nodeType!==t.nodeType||e.nodeName!==t.nodeName)return t;if(3===t.nodeType)return e.nodeValue!==t.nodeValue&&(e.nodeValue=t.nodeValue),e;if(1!==t.nodeType||!e.attributes||!t.attributes)return t;if(e.getAttribute("data-vl-bind")!==t.getAttribute("data-vl-bind"))return t;if(!t._vlHandlers||!e._vlHandlers)return t;Object.keys(e._vlHandlers).forEach(function(t){e.removeEventListener(t,e._vlHandlers[t])}),Object.keys(t._vlHandlers).forEach(function(n){e.addEventListener(n,t._vlHandlers[n])}),e._vlHandlers=t._vlHandlers;var n="DETAILS"===e.nodeName?"open":null;return Array.from(e.attributes).forEach(function(r){r.name!==n&&(t.hasAttribute(r.name)||e.removeAttribute(r.name))}),Array.from(t.attributes).forEach(function(t){t.name!==n&&e.getAttribute(t.name)!==t.value&&e.setAttribute(t.name,t.value)}),y(e,Array.from(t.childNodes)),t.hasAttribute&&t.hasAttribute("value")&&e.value!==t.value&&(e.value=t.value),t.hasAttribute&&t.hasAttribute("data-vl-bind")&&e.checked!==t.checked&&(e.checked=t.checked),e}function y(e,t){for(var n=0;n<t.length;n++)if(t[n]&&null!=t[n]._vlKey)return g(e,t);for(t.forEach(function(t,n){var r=e.childNodes[n],i=p(r,t);r?i!==r&&e.replaceChild(i,r):e.appendChild(i)});e.childNodes.length>t.length;)e.removeChild(e.lastChild)}function g(n,r){var i=Array.prototype.slice.call(n.childNodes),o=Object.create(null),l=[];i.forEach(function(e){e&&null!=e._vlKey?e._vlKey in o||(o[e._vlKey]=e):l.push(e)});var a=Object.create(null);r.forEach(function(e){e&&null!=e._vlKey&&(a[e._vlKey]=!0)}),Object.keys(o).forEach(function(e){if(!a[e]){var n=o[e];n&&n._vlPfx&&t.pruneInstPrefix(n._vlHost,n._vlPfx)}});var u=e.document,s=u&&u.activeElement,c=null;s&&"number"==typeof s.selectionStart&&(c={start:s.selectionStart,end:s.selectionEnd,dir:s.selectionDirection});for(var d=[],f=0;f<t._overlays.length;f++){var h=t._overlays[f];if(h.restoreFocus)for(var y=0;y<i.length;y++){var g=i[y];if(g&&null!=g._vlKey&&!a[g._vlKey]&&g.contains&&g.contains(h.restoreFocus)){d.push({rec:h,index:y});break}}}var m=-1;if(s)for(var b=0;b<i.length;b++){var _=i[b];if(_&&null!=_._vlKey&&!a[_._vlKey]&&_.contains&&_.contains(s)){m=b;break}}var S=0,k=Object.create(null),w=[];r.forEach(function(e){if(e&&null!=e._vlKey){var n=e._vlKey;if(k[n])throw t.err("render.duplicate_key","list key "+JSON.stringify(n)+" is used by more than one row; keys must be unique within a list");k[n]=!0;var r=o[n],i=r?p(r,e):e;i._vlKey=n,w.push(i)}else{var a=l[S++];w.push(a?p(a,e):e)}});for(var E=0;E<w.length;E++)n.childNodes[E]!==w[E]&&n.insertBefore(w[E],n.childNodes[E]||null);for(;n.childNodes.length>w.length;)n.removeChild(n.lastChild);if(s&&u&&u.contains&&u.contains(s)&&u.activeElement!==s&&"function"==typeof s.focus){try{s.focus({preventScroll:!0})}catch(e){}if(c&&"function"==typeof s.setSelectionRange)try{s.setSelectionRange(c.start,c.end,c.dir)}catch(e){}}function A(e){for(var t=[],r=0;r<n.childNodes.length;r++)n.childNodes[r]&&null!=n.childNodes[r]._vlKey&&t.push(n.childNodes[r]);if(!t.length)return null;for(var o=0,l=0;l<e;l++){var u=i[l];u&&null!=u._vlKey&&a[u._vlKey]&&o++}var s=t[Math.min(o,t.length-1)];return v(s)[0]||s}if(m>=0&&u&&(!u.contains||!u.contains(s))){var j=A(m);if(j&&"function"==typeof j.focus)try{j.focus({preventScroll:!0})}catch(e){}}for(var C=0;C<d.length;C++)d[C].rec.restoreFallback=A(d[C].index)}t.fmt=function(e,t){var n=e instanceof Date?e:new Date(e);return isNaN(n.getTime())?String(e):String(t||"yyyy-MM-dd").replace(/yyyy/g,n.getFullYear()).replace(/MMM/g,l[n.getMonth()]).replace(/MM/g,("0"+(n.getMonth()+1)).slice(-2)).replace(/dd/g,("0"+n.getDate()).slice(-2)).replace(/\bd\b/g,n.getDate())},t.range=function(e,t,n){for(var r=[],i=e;n?i<=t:i<t;i++)r.push(i);return r},t.each=function(e,t){return r(e).map(function(e,n){return t(e,n)})},t.keyed=function(e,n,i){for(var o=r(e),l=[],u=Object.create(null),s=0;s<o.length;s++){var c=i(o[s],s);if(null==c)throw t.err("render.missing_key","list key expression produced "+String(c)+" for item "+s+"; a keyed list needs a stable, defined key per row");if(u[c=String(c)])throw t.err("render.duplicate_key","list key "+JSON.stringify(c)+" is used by more than one row; keys must be unique within a list");u[c]=!0;var d=n(o[s],s);a(d,c),l.push(d)}return l},t.escKey=function(e){return String(e).replace(/\\/g,"\\\\").replace(/\//g,"\\/")},t.tagInst=function(e,t,n){return n&&n.nodeType&&(n._vlPfx=e,n._vlHost=t),n},t.pruneInstPrefix=function(e,t){e&&t&&["_state","_mounted","_loads"].forEach(function(n){var r=e[n];r&&Object.keys(r).forEach(function(e){0===e.indexOf(t)&&delete r[e]})})},t.appendKids=u,t.testIdsEnabled=!1,t._testFrame=null,t._testScopes=[],t.withTestScope=function(e,n){if("string"!=typeof e||!/^[a-z][a-z0-9-]{0,79}$/.test(e))throw t.err("render.invalid_test_scope","test_scope must be a public lowercase slug");t._testScopes.push(e);try{return n()}finally{t._testScopes.pop()}},t.testId=function(e){var n=t._testScopes.concat([e]).join("/");if(t._testFrame){if(t._testFrame.ids.has(n))throw t.err("render.duplicate_test_id","duplicate automation ID in the next rendered document");t._testFrame.ids.add(n)}return n},t.linkSafeSchemes={http:1,https:1,mailto:1,tel:1},t.linkHref=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.linkSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked link scheme "+i[1].toLowerCase()+":"),"about:blank#blocked"):n},t.fetchSafeSchemes={http:1,https:1},t.fetchUrl=function(e){if(null==e)return"";var n=String(e),r=n.replace(/[\u0000-\u0020]/g,""),i=/^([A-Za-z][A-Za-z0-9+\-.]*):/.exec(r);return i?t.fetchSafeSchemes[i[1].toLowerCase()]?n:("undefined"!=typeof console&&console.warn&&console.warn("VL: blocked resource scheme "+i[1].toLowerCase()+":"),""):n},t.fetchSrcset=function(e){if(null==e)return"";for(var n=String(e),r=n.split(","),i=0;i<r.length;i++){var o=r[i].trim().split(/\s+/)[0];if(o&&""===t.fetchUrl(o))return""}return n},t.detailsToggle=function(e){return function(t){var n=t.currentTarget;if(n&&n.isConnected){if(!0===n._vlToggleSeeded)return e.call(this,t);n._vlToggleSeeded=!0}}},t.el=function(e,t,n){var r=document.createElement(e);return(t=t||{}).class&&(r.className=t.class),t.style&&Object.keys(t.style).forEach(function(e){r.style[e]=t.style[e]}),t.attrs&&Object.keys(t.attrs).forEach(function(e){var n=t.attrs[e];return"checked"===e?(r.checked=!!n,void(n?r.setAttribute("checked",""):r.removeAttribute("checked"))):"muted"===e?(r.muted=!!n,r.defaultMuted=!!n,void(n?r.setAttribute("muted",""):r.removeAttribute("muted"))):void(null!=n&&!1!==n&&r.setAttribute(e,!0===n?"":String(n)))}),void 0!==t.text&&null!==t.text&&(r.textContent=String(t.text)),"details"===e&&(r._vlToggleSeeded=!r.hasAttribute("open")),t.on&&Object.keys(t.on).forEach(function(e){r.addEventListener(e,t.on[e])}),r._vlHandlers=t.on||{},u(r,n),t.attrs&&"value"in t.attrs&&null!==t.attrs.value&&void 0!==t.attrs.value&&!1!==t.attrs.value&&(r.value=t.attrs.value),r},t.Spinner=function(){return t.el("div",{class:"vl-spinner",text:"loading…"},[])},t.live=function(e,n,r){if(null!=e&&"string"!=typeof e)throw t.err("render.invalid_status","status/error message must be a string or nil");r=Object.assign({},r||{},{role:"status","aria-live":"polite","aria-atomic":"true"});var i=t.el("span",{class:n||"vl-status",attrs:r},[]);return i.appendChild(document.createTextNode(e||"")),i},t._fieldSeq=0,t.field=function(e){var n=e.name;if(null==n||""===String(n).trim())throw t.err("render.empty_input_name","an input's accessible name ("+(e.aria?"aria_label:":"label:")+") is "+(null==n?String(n):JSON.stringify(String(n)))+" at render time; a control must always have a non-empty name");n=String(n);var r="vlf"+ ++t._fieldSeq,i=r+"c",o=r+"h",l=r+"e",a=e.input||{},u=a.attrs=a.attrs||{};u.id=i;var s=void 0!==e.help&&null!==e.help,c=e.hasError?e.error:void 0,d=null!=c&&""!==c,f=[];s&&f.push(o),d&&f.push(l),f.length?u["aria-describedby"]=f.join(" "):delete u["aria-describedby"],e.hasError&&(u["aria-invalid"]=d?"true":null),e.aria&&(u["aria-label"]=n);var v=[];return e.aria||v.push(t.el("label",{class:"vl-label",attrs:{for:i},text:n},[])),v.push(t.el(e.tag||"input",a,e.children||[])),s&&v.push(t.el("span",{class:"vl-help",attrs:{id:o},text:e.help},[])),e.hasError&&v.push(t.live(c,"vl-error",{id:l})),t.el("div",{class:"vl-field"},v)},t.reqAlt=function(e){if(null==e||""===String(e).trim())throw t.err("render.empty_alt","an img `alt:` resolved to "+(null==e?String(e):JSON.stringify(String(e)))+" at render time; use `decorative` for a presentational image or give real alternative text");return String(e)},t.setTitle=function(n){if(null==n||""===String(n).trim())throw t.err("render.empty_page_title","the page `title:` resolved to "+(null==n?String(n):JSON.stringify(String(n)))+" at load time; a page must have a non-empty title");var r=e.document;return r&&(r.title=String(n)),String(n)},t.wireSkipLink=function(){var n=e.document;if(n&&n.querySelector&&n.body){var r=n.querySelector("nav.vl-skip-nav"),i=n.querySelector("a.vl-skip"),o=n.querySelector("main");if(o){o.id||(o.id="vl-main"),o.getAttribute&&null==o.getAttribute("tabindex")&&o.setAttribute("tabindex","-1");var l=t._mainLabel;if(l&&o.getAttribute){var a=null!=o.getAttribute("data-vl-route-label");(null==o.getAttribute("aria-label")||a)&&(o.getAttribute("aria-label")!==l&&o.setAttribute("aria-label",l),a||o.setAttribute("data-vl-route-label",""))}if(i)i.getAttribute("href")!=="#"+o.id&&i.setAttribute("href","#"+o.id);else{var u=t.el("a",{class:"vl-skip",attrs:{href:"#"+o.id},text:"Skip to main content"},[]);u.addEventListener("click",function(t){t.preventDefault&&t.preventDefault();var r=n.querySelector("main")||n.getElementById("vl-main");r&&r.focus&&r.focus();try{r&&r.id&&e.history&&e.history.replaceState&&e.history.replaceState(null,"","#"+r.id)}catch(e){}}),r||(r=t.el("nav",{class:"vl-skip-nav",attrs:{"aria-label":"Skip navigation"}},[]),n.body.insertBefore(r,n.body.firstChild)),r.appendChild(u)}}else{var s=r||i;s&&s.parentNode&&s.parentNode.removeChild(s)}}},t.queryProps=function(t){var n={};try{var r=e.location,i=new URLSearchParams(r&&r.search||"");(t||[]).forEach(function(e){i.has(e)&&(n[e]=i.get(e))})}catch(e){}return n},t._overlays=[],t._appEl=null,t.overlay={open:function(n){var r=c(n);if(r>=0)return t._overlays[r].el;var i=e.document,o=i.createElement("div");o.className="vl-overlay",o.setAttribute("data-overlay",String(n));var l=i.body||s()&&s().parentNode||null;l&&l.appendChild(o);var a=i.activeElement;return t._overlays.push({id:n,el:o,restoreFocus:a&&a!==i.body?a:null}),1===t._overlays.length&&d(s(),!0),o},close:function(n){var r=c(n);if(!(r<0)){var i=t._overlays.splice(r,1)[0],o=e.document;i._onClick&&i.el.removeEventListener&&i.el.removeEventListener("click",i._onClick),i._onTab&&i.el.removeEventListener&&i.el.removeEventListener("keydown",i._onTab),i._onKey&&o&&o.removeEventListener&&o.removeEventListener("keydown",i._onKey),delete t.overlay._dismiss[n],i.el&&i.el.parentNode&&i.el.parentNode.removeChild(i.el),0===t._overlays.length&&(d(s(),!1),function(e,t){for(var n=[e.restoreFocus,e.restoreFallback,t&&t.getElementById&&t.getElementById("vl-main")],r=0;r<n.length;r++){var i=n[r];if(i&&f(i,t)&&"function"==typeof i.focus){try{i.focus()}catch(e){}if(!t||t.activeElement===i)return}}var o=e.restoreFocus;if(o&&"function"==typeof o.focus)try{o.focus()}catch(e){}}(i,o))}},focusInto:function(e){if(e){var t=v(e)[0]||e;if(t===e&&(null==e.tabIndex||e.tabIndex<0)&&(e.tabIndex=-1),"function"==typeof t.focus)try{t.focus()}catch(e){}}},count:function(){return t._overlays.length},_dismiss:Object.create(null),_live:null,beginFrame:function(){this._live=Object.create(null)},endFrame:function(){var e=this._live;if(this._live=null,e)for(var n=t._overlays.length-1;n>=0;n--){var r=t._overlays[n].id;r in e||this.close(r)}},paint:function(n,r,i,o){var l;if(t._testFrame)return t._testFrame.portals.push(function(){t.overlay.paint(n,r,function(){return l},o)}),l=i(),null;var a=c(n)<0,u=this.open(n);u.className="vl-overlay vl-overlay-dialog",this._live&&(this._live[n]=!0),this._dismiss[n]=o;var s=t._overlays[c(n)],d="vl-dlg-title-"+String(n).replace(/[^\w-]/g,"_"),f=t.el("div",{class:"vl-dialog",attrs:{role:"dialog","aria-modal":"true","aria-labelledby":d,tabindex:"-1"}},[t.el("h2",{class:"vl-dialog-title",attrs:{id:d},text:r},[]),i()]);if(s.panel&&s.panel.parentNode===u&&u.replaceChild){var y=p(s.panel,f);y!==s.panel&&(u.replaceChild(y,s.panel),s.panel=y)}else if(u.replaceChildren)u.replaceChildren(f),s.panel=f;else{for(;u.firstChild;)u.removeChild(u.firstChild);u.appendChild(f),s.panel=f}return a&&(s._onTab=function(t){if("Tab"===(t.key||"")||9===t.keyCode){var n=s.panel,r=v(n);if(r.length){var i=r[0],o=r[r.length-1],l=e.document.activeElement;t.shiftKey&&l===i?(t.preventDefault&&t.preventDefault(),o.focus()):t.shiftKey||l!==o&&n.contains(l)||(t.preventDefault&&t.preventDefault(),i.focus())}else t.preventDefault&&t.preventDefault()}},u.addEventListener&&u.addEventListener("keydown",s._onTab),s._onClick=function(e){e.target===u&&h(n)},u.addEventListener&&u.addEventListener("click",s._onClick),s._onKey=function(e){"Escape"!==(e.key||"")&&27!==e.keyCode||h(n)},e.document.addEventListener&&e.document.addEventListener("keydown",s._onKey),this.focusInto(s.panel)),null}},t.route={to:function(t){if("string"==typeof t&&t)try{if("?"===t[0]||"#"===t[0]){var n=e.history;return void(n&&n.replaceState&&n.replaceState(null,"",t))}var r=e.location;r&&r.assign&&r.assign(t)}catch(e){}}},t.tabs=function(e){for(var n=(e=e||{}).tabs||[],r=0,i=0;i<n.length;i++)if(n[i].id===e.selected){r=i;break}var o=n.length?n[r].id:null;function l(t){n.length&&e.onSelect&&e.onSelect(n[(t+n.length)%n.length].id)}var a=t.el("div",{class:"vl-tablist",attrs:{role:"tablist"},on:{keydown:function(e){var t=e.key||"",i=function(){e.preventDefault&&e.preventDefault()};"ArrowRight"===t||"ArrowDown"===t?(i(),l(r+1)):"ArrowLeft"===t||"ArrowUp"===t?(i(),l(r-1)):"Home"===t?(i(),l(0)):"End"===t&&(i(),l(n.length-1))}}},n.map(function(n){var r=n.id===o;return t.el("button",{class:"vl-tab",attrs:{role:"tab",id:"vl-tab-"+n.id,"aria-selected":r?"true":"false","aria-controls":"vl-panel-"+n.id,tabindex:r?"0":"-1"},text:n.label,on:{click:function(){e.onSelect&&e.onSelect(n.id)}}},[])})),u=n[r],s=t.el("div",{class:"vl-tabpanel",attrs:{role:"tabpanel",id:"vl-panel-"+o,"aria-labelledby":"vl-tab-"+o,tabindex:"0"}},u?[u.panel()]:[]);return t.el("div",{class:"vl-tabs"},[a,s])},t.subctx=function(e,t){var n=e._host,r=(e._pfx||"")+(t||"");return{_pfx:r,_host:n,signal:n.signal,state:function(e,t){return n.state(r+e,t)},setState:function(e,t){return n.setState(r+e,t)},load:function(e,t,i,o){return n.load(r+e,t,i,o)},reload:function(e){return n.reload(r+e)},loading:function(e){return n.loading(r+e)},mountOnce:function(e,t){return n.mountOnce(r+e,t)}}},t.mount=function(n,r,i){var o=i||{};t._appEl=r;var l="undefined"!=typeof AbortController?new AbortController:null,a={_state:Object.create(null),_loads:Object.create(null),_mounted:Object.create(null),_scheduled:!1,_composing:!1,_abortController:l,signal:l?l.signal:void 0,state:function(e,t){return e in this._state||(this._state[e]=t),this._state[e]},mountOnce:function(e,t){this._mounted[e]||(this._mounted[e]=!0,t())},setState:function(e,t){return this._state[e]=t,this.schedule(),t},load:function(e,n,r,i){var o=JSON.stringify(void 0===n?null:n),l=this._loads[e];if(!l||l.key!==o){l=this._loads[e]={key:o,pending:!0,error:null,value:void 0};var a=this;Promise.resolve().then(r).then(function(n){a._loads[e]===l&&(l.pending=!1,l.value=n,a.schedule(),t._onLoadSettled&&t._onLoadSettled(a))},function(n){a._loads[e]===l&&(l.pending=!1,l.error=n,a.schedule(),!i&&t._onUnhandledLoadError&&t._onUnhandledLoadError(a,e,n),t._onLoadSettled&&t._onLoadSettled(a))})}return l},reload:function(e){delete this._loads[e],this.schedule()},loading:function(e){var t=this._loads[e];return!t||t.pending},schedule:function(){var e=this;e._scheduled||(e._scheduled=!0,queueMicrotask(function(){e._scheduled=!1,e.render()}))},render:function(){if(!this._composing){var i,l=e.document,a=l&&l.activeElement,c=null;a&&a.getAttribute&&a.getAttribute("data-vl-bind")&&(!l.contains||l.contains(a))&&(c={node:a,key:a.getAttribute("data-vl-bind"),start:a.selectionStart,end:a.selectionEnd,direction:a.selectionDirection,scrollLeft:a.scrollLeft,scrollTop:a.scrollTop}),t.overlay.beginFrame(),t.testIdsEnabled&&(t._testFrame={ids:new Set,portals:[]});try{i=n(o,s);var d=t._testFrame;t._testFrame=null,d&&d.portals.forEach(function(e){e()})}catch(e){if(t._testFrame=null,t._testScopes=[],e&&/^render\.(duplicate_test_id|invalid_test_scope)$/.test(e.code||""))throw t.overlay._live=null,e;throw r.replaceChildren(t.el("pre",{class:"vl-error",text:String(e&&e.stack||e)},[])),e}if(Array.isArray(i)){var f=document.createDocumentFragment();u(f,i),r.replaceChild?y(r,Array.from(f.childNodes)):r.replaceChildren(f)}else r.replaceChild?y(r,[i]):r.replaceChildren(i);if(t.overlay.endFrame(),t.wireSkipLink(),c&&l){var v=l.contains&&l.contains(c.node)?c.node:null;if(!v&&l.querySelectorAll){var h=Array.prototype.filter.call(l.querySelectorAll("[data-vl-bind]"),function(e){return e.getAttribute("data-vl-bind")===c.key});1===h.length&&(v=h[0])}if(v&&l.activeElement!==v){if(v.focus({preventScroll:!0}),null!=c.start&&"function"==typeof v.setSelectionRange)try{v.setSelectionRange(c.start,c.end,c.direction)}catch(e){}v.scrollLeft=c.scrollLeft,v.scrollTop=c.scrollTop}}}}},s=t.subctx({_pfx:"",_host:a},""),c=e.document;return c&&c.addEventListener&&(c.addEventListener("compositionstart",function(){a._composing=!0}),c.addEventListener("compositionend",function(){a._composing=!1,a.schedule()})),a.render(),a},t._onUnhandledLoadError=null,t._onLoadSettled=null,t._mainLabel=null,t._modules=Object.create(null);var m=Object.create(null);t.registerModule=function(e,n){t._modules[e]=n},t.loadModule=function(n,r){if(t._modules[n])return Promise.resolve(t._modules[n]);if(m[n])return m[n];var i=e.document,o=new Promise(function(e,o){var l=i.createElement("script");l.src=r,l.onload=function(){t._modules[n]?e(t._modules[n]):o(new Error("module '"+n+"' did not register itself (loaded "+r+")"))},l.onerror=function(){o(new Error("failed to load module '"+n+"' from "+r))},i.head.appendChild(l)});return m[n]=o,o},e.VL=t}("undefined"!=typeof window?window:globalThis);
|
|
@@ -59,7 +59,9 @@ from vlcheck.elements import (
|
|
|
59
59
|
SPELLCHECK_HOMES, TABINDEX_ALLOWED, TEXT_STYLE_ATTRS,
|
|
60
60
|
UNSAFE_ELEMENTS,
|
|
61
61
|
)
|
|
62
|
-
from vlcheck.checks import
|
|
62
|
+
from vlcheck.checks import (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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
"
|
|
176
|
-
|
|
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 =
|
|
3196
|
+
is_variant = self._is_catch_code(head)
|
|
3143
3197
|
if is_variant:
|
|
3144
|
-
out.append("%s (
|
|
3145
|
-
|
|
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 =
|
|
3239
|
+
is_variant = self._is_catch_code(head)
|
|
3186
3240
|
if is_variant:
|
|
3187
|
-
out.append("%s (
|
|
3188
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
"
|
|
3771
|
-
"
|
|
3772
|
-
"
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1333
|
+
// The block also defines `VL.eq`, the structural `==` a route body's
|
|
1334
|
+
// record and list comparisons compile to.
|
|
1335
|
+
const VL = (function () {
|
|
1336
|
+
const VL = { len: (x) => (x == null ? 0 : x.length) };
|
|
1337
1337
|
%(vl_std)s
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
};
|
|
1338
|
+
return VL;
|
|
1339
|
+
})();
|
|
1341
1340
|
|
|
1342
1341
|
// Coerce one `?name=raw` query-string value or one decoded path segment to
|
|
1343
1342
|
// its declared Volaro scalar type, or throw a 400 (via
|
package/language/crib.md
CHANGED
|
@@ -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 |
|
package/language/supported.md
CHANGED
|
@@ -423,8 +423,8 @@ audio video source track picture map area
|
|
|
423
423
|
`load`, so they are valid only on a name the view's own `load` binds
|
|
424
424
|
(`E-LOAD-METHOD`); `loading()` is true while its fetch is in flight.
|
|
425
425
|
The std free functions this build has are the ones listed under
|
|
426
|
-
"Data / services" below (`concat`, `append`, `unique`, `
|
|
427
|
-
`contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
|
|
426
|
+
"Data / services" below (`concat`, `append`, `unique`, `any`, `all`,
|
|
427
|
+
`matches`, `contains`, `replace`), plus `filter`, `map`, `take`, `count`, `sum`,
|
|
428
428
|
`sort_by`, `group_by`, `chunk`, `join`, `split`, `first_char`, `trim`,
|
|
429
429
|
`upper`, `lower`, `keys`, `values`, `get_or`, `int_to_float` and
|
|
430
430
|
`float_to_int`. Any other std name (`sorted`, `first`, `fold`, `merge`,
|
|
@@ -444,6 +444,14 @@ audio video source track picture map area
|
|
|
444
444
|
only the argument of `err(...)`. `not_found("x")` anywhere else is
|
|
445
445
|
`E-ERR-CONTEXT`. A service with no `model` runs its routes in the
|
|
446
446
|
browser, and `err(...)` fails the call there too.
|
|
447
|
+
- The argument of `err(...)` is an error code call (`err(conflict("x"))`,
|
|
448
|
+
any code in the list above), a variant of an error union you declare
|
|
449
|
+
(`err(invalid_field("title"))`; a page.server handler sees the unions
|
|
450
|
+
in the project's `app.vl`), or the error a `catch e` arm caught
|
|
451
|
+
(`err(e)` rethrows it unchanged). Anything else (a string, a local
|
|
452
|
+
value, a record type, a function) is `E-ERR-ARG` in `volaro check` and
|
|
453
|
+
the build. A code with no `errors` entry answers with status 500, like
|
|
454
|
+
any unmapped code.
|
|
447
455
|
- A namespace or ambient object (`db`, `env`, `log`, `route`, `auth`,
|
|
448
456
|
`session`, `request`, `api`, and the unbuilt `analytics`, `events`,
|
|
449
457
|
`ws`, `mailer`, `billing`), a type name (`str(x)`, `int(x)`, …), `ok`,
|
|
@@ -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 `
|
|
476
|
-
statement (`match`
|
|
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.
|
|
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",
|