@optionfactory/fml 9.0.0-rc15 → 9.0.0-rc16
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 +16 -88
- package/dist/client-errors.iife.js +17 -26
- package/dist/client-errors.iife.js.map +1 -1
- package/dist/client-errors.iife.min.js.map +1 -1
- package/dist/custom-elements.json +44 -30
- package/dist/fml.css +20 -17
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +3386 -2037
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ftl.d.mts +395 -224
- package/dist/ftl.iife.js +400 -330
- package/dist/ftl.iife.js.map +1 -1
- package/dist/ftl.iife.min.js +1 -1
- package/dist/ftl.iife.min.js.map +1 -1
- package/dist/ftl.min.mjs +1 -1
- package/dist/ftl.min.mjs.map +1 -1
- package/dist/ftl.mjs +400 -330
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +20 -17
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +2583 -690
- package/dist/ful.iife.js +2850 -1542
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +2850 -1542
- package/dist/ful.mjs.map +1 -1
- package/dist/httpc.d.mts +139 -121
- package/dist/httpc.iife.js +137 -166
- package/dist/httpc.iife.js.map +1 -1
- package/dist/httpc.iife.min.js +1 -1
- package/dist/httpc.iife.min.js.map +1 -1
- package/dist/httpc.min.mjs +1 -1
- package/dist/httpc.min.mjs.map +1 -1
- package/dist/httpc.mjs +137 -166
- package/dist/httpc.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +26 -18
- package/dist/web-types.json +51 -35
- package/package.json +1 -1
package/dist/ftl.mjs
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A Map bounded by entry count, evicting
|
|
3
|
-
*
|
|
4
|
-
* expressions, compiled masks, formatter instances) from growing forever. The
|
|
5
|
-
* capacity is sized so eviction never happens on a sane page: hitting it means
|
|
6
|
-
* dynamically generated keys, where FIFO's worst case (evicting a hot entry)
|
|
7
|
-
* costs one recomputation.
|
|
2
|
+
* A Map bounded by entry count, evicting the oldest entry when full. A hit does
|
|
3
|
+
* not refresh an entry: eviction follows insertion order, not use.
|
|
8
4
|
*/
|
|
9
5
|
class BoundedCache {
|
|
10
6
|
#max;
|
|
@@ -33,6 +29,7 @@ class BoundedCache {
|
|
|
33
29
|
this.#entries.set(key, value);
|
|
34
30
|
return value;
|
|
35
31
|
}
|
|
32
|
+
/** @returns {number} the number of entries held */
|
|
36
33
|
get size() {
|
|
37
34
|
return this.#entries.size;
|
|
38
35
|
}
|
|
@@ -75,10 +72,6 @@ const nodes = {
|
|
|
75
72
|
//
|
|
76
73
|
// https://peggyjs.org/
|
|
77
74
|
|
|
78
|
-
//keyword classification lives here, not in the grammar's ordered choice: the
|
|
79
|
-
//identifier rule consumes the whole word and the action decides, so no keyword
|
|
80
|
-
//can grab a longer identifier's prefix. A Map, not a plain object: `in` on the
|
|
81
|
-
//latter would classify inherited names such as `constructor` as keywords
|
|
82
75
|
const keywords = new Map([
|
|
83
76
|
['true', true],
|
|
84
77
|
['false', false],
|
|
@@ -3250,10 +3243,10 @@ function peg$parse(input, options) {
|
|
|
3250
3243
|
/**
|
|
3251
3244
|
* The one lookup against a data stack: the innermost overlay carrying the name
|
|
3252
3245
|
* wins, `self` is the innermost overlay itself, and a function overlay counts
|
|
3253
|
-
* like an object one.
|
|
3254
|
-
* imperative facades cannot drift from what a template sees.
|
|
3246
|
+
* like an object one.
|
|
3255
3247
|
* @param {any[]} dataStack
|
|
3256
3248
|
* @param {string|symbol} prop
|
|
3249
|
+
* @returns {any} undefined when no overlay carries the name
|
|
3257
3250
|
*/
|
|
3258
3251
|
const resolveInStack = (dataStack, prop) => {
|
|
3259
3252
|
if (prop === 'self') {
|
|
@@ -3317,8 +3310,7 @@ class EvaluatingVisitor {
|
|
|
3317
3310
|
return this.visit(node.lhs) || this.visit(node.rhs);
|
|
3318
3311
|
}
|
|
3319
3312
|
[nodes.nullc](node) {
|
|
3320
|
-
|
|
3321
|
-
return lhs !== null && lhs !== undefined ? lhs : this.visit(node.rhs);
|
|
3313
|
+
return this.visit(node.lhs) ?? this.visit(node.rhs);
|
|
3322
3314
|
}
|
|
3323
3315
|
[nodes.not](node) {
|
|
3324
3316
|
return !this.visit(node.expr);
|
|
@@ -3385,8 +3377,7 @@ class EvaluatingVisitor {
|
|
|
3385
3377
|
return this.visit(node.cond) ? this.visit(node.ifTrue) : this.visit(node.ifFalse);
|
|
3386
3378
|
}
|
|
3387
3379
|
[nodes.elv](node) {
|
|
3388
|
-
|
|
3389
|
-
return cond ? cond : this.visit(node.ifFalse);
|
|
3380
|
+
return this.visit(node.cond) || this.visit(node.ifFalse);
|
|
3390
3381
|
}
|
|
3391
3382
|
[nodes.access](node) {
|
|
3392
3383
|
let prev;
|
|
@@ -3452,16 +3443,23 @@ class EvaluatingVisitor {
|
|
|
3452
3443
|
* spliced into the expression text.
|
|
3453
3444
|
*/
|
|
3454
3445
|
class Expressions {
|
|
3446
|
+
/** Parses the whole text as one expression: `user.name`. */
|
|
3455
3447
|
static MODE_EXPRESSION = Symbol('MODE_EXPRESSION');
|
|
3448
|
+
/**
|
|
3449
|
+
* Parses text with interpolations, `Hello {{ user.name }}`, evaluating to
|
|
3450
|
+
* its parts: `{ type, value }` for each literal run and each `{{ }}` (text),
|
|
3451
|
+
* `{{{ }}}` (html) or `{{{{ }}}}` (node) interpolation, in order.
|
|
3452
|
+
*/
|
|
3456
3453
|
static MODE_TEMPLATED = Symbol('MODE_TEMPLATED');
|
|
3457
3454
|
|
|
3458
3455
|
static #astCache = new BoundedCache(1000);
|
|
3459
3456
|
|
|
3460
3457
|
/**
|
|
3461
|
-
* Parses an expression.
|
|
3458
|
+
* Parses an expression, caching the ast by mode and text.
|
|
3462
3459
|
* @param {string} expression
|
|
3463
|
-
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode]
|
|
3464
|
-
* @returns the ast
|
|
3460
|
+
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] MODE_EXPRESSION when omitted
|
|
3461
|
+
* @returns {any} the ast
|
|
3462
|
+
* @throws a peggy syntax error when the text does not parse
|
|
3465
3463
|
*/
|
|
3466
3464
|
static parse(expression, mode) {
|
|
3467
3465
|
const key = mode?.toString() + expression;
|
|
@@ -3472,12 +3470,12 @@ class Expressions {
|
|
|
3472
3470
|
);
|
|
3473
3471
|
}
|
|
3474
3472
|
/**
|
|
3475
|
-
* Evaluates
|
|
3473
|
+
* Evaluates a parsed ast against the modules and the data stack.
|
|
3476
3474
|
* @param {{[k: string]: any } | null | undefined } modules
|
|
3477
|
-
* @param {any[]} dataStack
|
|
3478
|
-
* @param {any} ast
|
|
3479
|
-
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode]
|
|
3480
|
-
* @returns the
|
|
3475
|
+
* @param {any[]} dataStack innermost overlay last
|
|
3476
|
+
* @param {any} ast what `parse` answered
|
|
3477
|
+
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] the mode it was parsed in
|
|
3478
|
+
* @returns {any} the value, or the list of parts in MODE_TEMPLATED
|
|
3481
3479
|
*/
|
|
3482
3480
|
static evaluate(modules, dataStack, ast, mode) {
|
|
3483
3481
|
return new EvaluatingVisitor(modules, dataStack).visitRoot(ast, mode === Expressions.MODE_TEMPLATED);
|
|
@@ -3485,10 +3483,10 @@ class Expressions {
|
|
|
3485
3483
|
/**
|
|
3486
3484
|
* Parses and evaluates an expression.
|
|
3487
3485
|
* @param {{ [x: string]: any; } | null | undefined} modules
|
|
3488
|
-
* @param {any[]} dataStack
|
|
3486
|
+
* @param {any[]} dataStack innermost overlay last
|
|
3489
3487
|
* @param {string} expression
|
|
3490
|
-
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode]
|
|
3491
|
-
* @returns the
|
|
3488
|
+
* @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] MODE_EXPRESSION when omitted
|
|
3489
|
+
* @returns {any} the value, or the list of parts in MODE_TEMPLATED
|
|
3492
3490
|
*/
|
|
3493
3491
|
static interpret(modules, dataStack, expression, mode) {
|
|
3494
3492
|
return Expressions.evaluate(modules, dataStack, Expressions.parse(expression, mode), mode);
|
|
@@ -3504,14 +3502,31 @@ class Expressions {
|
|
|
3504
3502
|
class ExpressionEvaluator {
|
|
3505
3503
|
#modules;
|
|
3506
3504
|
#dataStack;
|
|
3505
|
+
/**
|
|
3506
|
+
* @param {{ [k: string]: any } | null | undefined} modules
|
|
3507
|
+
* @param {any[]} dataStack innermost overlay last
|
|
3508
|
+
*/
|
|
3507
3509
|
constructor(modules, dataStack) {
|
|
3508
3510
|
this.#modules = modules;
|
|
3509
3511
|
this.#dataStack = dataStack;
|
|
3510
3512
|
}
|
|
3513
|
+
/**
|
|
3514
|
+
* A new evaluator with one more module: `#name:fn()` resolves into it, and an
|
|
3515
|
+
* empty name merges a whole map whose functions resolve bare, as `#fn()`.
|
|
3516
|
+
* @param {string} name
|
|
3517
|
+
* @param {object} value
|
|
3518
|
+
* @returns {ExpressionEvaluator}
|
|
3519
|
+
*/
|
|
3511
3520
|
withModule(name, value) {
|
|
3512
3521
|
const module = name ? { [name]: value } : value;
|
|
3513
3522
|
return new ExpressionEvaluator({ ...this.#modules, ...module }, this.#dataStack);
|
|
3514
3523
|
}
|
|
3524
|
+
/**
|
|
3525
|
+
* A new evaluator with the data pushed as the innermost overlays, the last
|
|
3526
|
+
* one winning a shared name.
|
|
3527
|
+
* @param {...any} data
|
|
3528
|
+
* @returns {ExpressionEvaluator}
|
|
3529
|
+
*/
|
|
3515
3530
|
withOverlay(...data) {
|
|
3516
3531
|
return new ExpressionEvaluator(
|
|
3517
3532
|
this.#modules,
|
|
@@ -3522,26 +3537,36 @@ class ExpressionEvaluator {
|
|
|
3522
3537
|
* Resolves a name against the data stack, with no parse round trip: the
|
|
3523
3538
|
* lookup a template's bare identifier makes, for an imperative caller.
|
|
3524
3539
|
* @param {string} name
|
|
3540
|
+
* @returns {any} undefined when no overlay carries the name
|
|
3525
3541
|
*/
|
|
3526
3542
|
resolve(name) {
|
|
3527
3543
|
return resolveInStack(this.#dataStack, name);
|
|
3528
3544
|
}
|
|
3529
|
-
/**
|
|
3545
|
+
/**
|
|
3546
|
+
* Evaluates an expression against this scope.
|
|
3547
|
+
* @param {string} expression
|
|
3548
|
+
* @returns {any}
|
|
3549
|
+
*/
|
|
3530
3550
|
evaluateExpression(expression) {
|
|
3531
3551
|
return Expressions.interpret(this.#modules, this.#dataStack, expression, Expressions.MODE_EXPRESSION);
|
|
3532
3552
|
}
|
|
3533
|
-
/**
|
|
3553
|
+
/**
|
|
3554
|
+
* Evaluates a templated text, the `{{ }}` form, against this scope.
|
|
3555
|
+
* @param {string} text
|
|
3556
|
+
* @returns {{ type: symbol, value: any }[]} its parts, as MODE_TEMPLATED describes
|
|
3557
|
+
*/
|
|
3534
3558
|
evaluateTemplated(text) {
|
|
3535
3559
|
return Expressions.interpret(this.#modules, this.#dataStack, text, Expressions.MODE_TEMPLATED);
|
|
3536
3560
|
}
|
|
3537
3561
|
}
|
|
3538
3562
|
|
|
3539
|
-
/** Creates and inspects
|
|
3563
|
+
/** Creates, serializes and inspects DocumentFragments. */
|
|
3540
3564
|
class Fragments {
|
|
3541
3565
|
/**
|
|
3542
|
-
*
|
|
3543
|
-
*
|
|
3544
|
-
* @
|
|
3566
|
+
* Parses html into a DocumentFragment adopted by the document, each piece
|
|
3567
|
+
* trimmed and the pieces joined.
|
|
3568
|
+
* @param {...string} html
|
|
3569
|
+
* @returns {DocumentFragment}
|
|
3545
3570
|
*/
|
|
3546
3571
|
static fromHtml(...html) {
|
|
3547
3572
|
const el = document.createElement('template');
|
|
@@ -3549,11 +3574,11 @@ class Fragments {
|
|
|
3549
3574
|
return document.adoptNode(el.content);
|
|
3550
3575
|
}
|
|
3551
3576
|
/**
|
|
3552
|
-
*
|
|
3553
|
-
*
|
|
3577
|
+
* Serializes a fragment, or a single node, to html, consuming it: the nodes
|
|
3578
|
+
* are moved out, not copied, and a fragment is left empty. Pass
|
|
3554
3579
|
* `fragment.cloneNode(true)` to keep the original usable.
|
|
3555
|
-
* @param {DocumentFragment} fragment
|
|
3556
|
-
* @returns {string}
|
|
3580
|
+
* @param {DocumentFragment|Node} fragment
|
|
3581
|
+
* @returns {string}
|
|
3557
3582
|
*/
|
|
3558
3583
|
static toHtml(fragment) {
|
|
3559
3584
|
var el = document.createElement('template');
|
|
@@ -3561,17 +3586,17 @@ class Fragments {
|
|
|
3561
3586
|
return el.innerHTML;
|
|
3562
3587
|
}
|
|
3563
3588
|
/**
|
|
3564
|
-
*
|
|
3589
|
+
* Whether a fragment holds no element and no text but whitespace.
|
|
3565
3590
|
* @param {DocumentFragment} fragment
|
|
3566
|
-
* @returns {boolean}
|
|
3591
|
+
* @returns {boolean}
|
|
3567
3592
|
*/
|
|
3568
3593
|
static isBlank(fragment) {
|
|
3569
3594
|
return fragment.childElementCount === 0 && fragment.textContent.trim() === '';
|
|
3570
3595
|
}
|
|
3571
3596
|
/**
|
|
3572
|
-
*
|
|
3573
|
-
* @param
|
|
3574
|
-
* @returns {DocumentFragment}
|
|
3597
|
+
* Moves the nodes, in order, into a new DocumentFragment.
|
|
3598
|
+
* @param {...Node} nodes
|
|
3599
|
+
* @returns {DocumentFragment}
|
|
3575
3600
|
*/
|
|
3576
3601
|
static from(...nodes) {
|
|
3577
3602
|
const fragment = new DocumentFragment();
|
|
@@ -3581,9 +3606,10 @@ class Fragments {
|
|
|
3581
3606
|
return fragment;
|
|
3582
3607
|
}
|
|
3583
3608
|
/**
|
|
3584
|
-
*
|
|
3609
|
+
* Moves the children of a node into a new DocumentFragment, leaving the
|
|
3610
|
+
* node empty.
|
|
3585
3611
|
* @param {Node} el
|
|
3586
|
-
* @returns {DocumentFragment}
|
|
3612
|
+
* @returns {DocumentFragment}
|
|
3587
3613
|
*/
|
|
3588
3614
|
static fromChildNodes(el) {
|
|
3589
3615
|
const fragment = new DocumentFragment();
|
|
@@ -3594,23 +3620,23 @@ class Fragments {
|
|
|
3594
3620
|
}
|
|
3595
3621
|
}
|
|
3596
3622
|
|
|
3597
|
-
/** Attribute
|
|
3623
|
+
/** Attribute helpers: unique ids, defaults, prefixed forwarding, and writes where a nullish value removes the attribute. */
|
|
3598
3624
|
class Attributes {
|
|
3599
3625
|
static id = 0;
|
|
3600
3626
|
/**
|
|
3601
|
-
*
|
|
3627
|
+
* A page-unique id under the prefix, as `prefix-n`.
|
|
3602
3628
|
* @param {string} prefix
|
|
3603
|
-
* @returns
|
|
3629
|
+
* @returns {string}
|
|
3604
3630
|
*/
|
|
3605
3631
|
static uid(prefix) {
|
|
3606
3632
|
return `${prefix}-${++Attributes.id}`;
|
|
3607
3633
|
}
|
|
3608
3634
|
/**
|
|
3609
|
-
* Sets an attribute
|
|
3635
|
+
* Sets an attribute when the element does not carry it.
|
|
3610
3636
|
* @param {Element} el
|
|
3611
3637
|
* @param {string} k
|
|
3612
3638
|
* @param {string} v
|
|
3613
|
-
* @returns
|
|
3639
|
+
* @returns {string|null} the attribute's value afterwards
|
|
3614
3640
|
*/
|
|
3615
3641
|
static defaultValue(el, k, v) {
|
|
3616
3642
|
if (!el.hasAttribute(k)) {
|
|
@@ -3619,7 +3645,9 @@ class Attributes {
|
|
|
3619
3645
|
return el.getAttribute(k);
|
|
3620
3646
|
}
|
|
3621
3647
|
/**
|
|
3622
|
-
*
|
|
3648
|
+
* Copies every attribute whose name starts with the prefix onto another
|
|
3649
|
+
* element, under the name without the prefix. `<prefix>class` adds its
|
|
3650
|
+
* classes to the target's instead of replacing them.
|
|
3623
3651
|
* @param {string} prefix
|
|
3624
3652
|
* @param {Element} from
|
|
3625
3653
|
* @param {Element} to
|
|
@@ -3642,7 +3670,7 @@ class Attributes {
|
|
|
3642
3670
|
});
|
|
3643
3671
|
}
|
|
3644
3672
|
/**
|
|
3645
|
-
* Sets
|
|
3673
|
+
* Sets an attribute, or removes it when the value is nullish.
|
|
3646
3674
|
* @param {Element} el
|
|
3647
3675
|
* @param {string} attr
|
|
3648
3676
|
* @param {string | null | undefined} value
|
|
@@ -3664,13 +3692,14 @@ class Attributes {
|
|
|
3664
3692
|
*/
|
|
3665
3693
|
class LightSlots {
|
|
3666
3694
|
/**
|
|
3667
|
-
*
|
|
3695
|
+
* Takes an element's children apart into slots, leaving the element empty.
|
|
3696
|
+
* A named child is removed and contributes what `slotFromNode` answers;
|
|
3697
|
+
* children sharing a name are gathered in order. A child with an empty
|
|
3698
|
+
* `slot` belongs to the default slot, in document order among the rest.
|
|
3668
3699
|
* @param {Element} el
|
|
3669
|
-
* @returns the slots
|
|
3700
|
+
* @returns {Record<string, DocumentFragment>} the slots by name, `default` always present
|
|
3670
3701
|
*/
|
|
3671
3702
|
static from(el) {
|
|
3672
|
-
//the platform reads slot="" as the default slot: such children stay in
|
|
3673
|
-
//the document order of the default content, their claim stripped like any named one
|
|
3674
3703
|
for (const child of el.children) {
|
|
3675
3704
|
if (child.matches('[slot=""]')) {
|
|
3676
3705
|
child.removeAttribute('slot');
|
|
@@ -3696,6 +3725,12 @@ class LightSlots {
|
|
|
3696
3725
|
}
|
|
3697
3726
|
return slots;
|
|
3698
3727
|
}
|
|
3728
|
+
/**
|
|
3729
|
+
* What a slotted node contributes: a template's content, the parsed markup
|
|
3730
|
+
* of a `<script type="text/html">`, or the node itself.
|
|
3731
|
+
* @param {Element} el
|
|
3732
|
+
* @returns {Element|DocumentFragment}
|
|
3733
|
+
*/
|
|
3699
3734
|
static slotFromNode(el) {
|
|
3700
3735
|
if (el instanceof HTMLTemplateElement) {
|
|
3701
3736
|
return document.adoptNode(el.content);
|
|
@@ -3707,12 +3742,13 @@ class LightSlots {
|
|
|
3707
3742
|
}
|
|
3708
3743
|
}
|
|
3709
3744
|
|
|
3710
|
-
/** Waits
|
|
3745
|
+
/** Waits on the parser and the document, and queries an element's own children. */
|
|
3711
3746
|
class Nodes {
|
|
3712
3747
|
/**
|
|
3713
|
-
*
|
|
3748
|
+
* Whether the parser has moved past the element: the element, or one of
|
|
3749
|
+
* its ancestors, has a next sibling.
|
|
3714
3750
|
* @param {Element} el
|
|
3715
|
-
* @returns
|
|
3751
|
+
* @returns {boolean}
|
|
3716
3752
|
*/
|
|
3717
3753
|
static isParsed(el) {
|
|
3718
3754
|
for (let c = /** @type {Node | null} */ (el); c; c = c.parentNode) {
|
|
@@ -3724,13 +3760,12 @@ class Nodes {
|
|
|
3724
3760
|
}
|
|
3725
3761
|
|
|
3726
3762
|
/**
|
|
3727
|
-
* Waits for the document's DOMContentLoaded
|
|
3728
|
-
*
|
|
3729
|
-
*
|
|
3730
|
-
*
|
|
3731
|
-
*
|
|
3732
|
-
*
|
|
3733
|
-
* never early.
|
|
3763
|
+
* Waits for the document's DOMContentLoaded. Resolves at once when that
|
|
3764
|
+
* moment already passed, and for a document with no view, which receives
|
|
3765
|
+
* no events. Never resolves early for a document still running its
|
|
3766
|
+
* deferred scripts.
|
|
3767
|
+
* @param {Document} doc
|
|
3768
|
+
* @returns {Promise<void>}
|
|
3734
3769
|
*/
|
|
3735
3770
|
static waitDomContentLoaded(doc) {
|
|
3736
3771
|
if (doc.readyState === 'loading') {
|
|
@@ -3743,7 +3778,6 @@ class Nodes {
|
|
|
3743
3778
|
}
|
|
3744
3779
|
const win = doc.defaultView;
|
|
3745
3780
|
if (win === null) {
|
|
3746
|
-
//a viewless document can receive no event at all
|
|
3747
3781
|
return Promise.resolve();
|
|
3748
3782
|
}
|
|
3749
3783
|
return new Promise((resolve) => {
|
|
@@ -3763,18 +3797,8 @@ class Nodes {
|
|
|
3763
3797
|
* which is the parser having moved past this subtree. The document's
|
|
3764
3798
|
* DOMContentLoaded is the deadline. Resolves immediately for an element
|
|
3765
3799
|
* that is already parsed.
|
|
3766
|
-
*
|
|
3767
|
-
*
|
|
3768
|
-
* predicate reads: an element last among its siblings becomes parsed when
|
|
3769
|
-
* an ancestor gains one, and a childList observer on the parent never sees
|
|
3770
|
-
* that. Formatted markup usually hides the difference, the whitespace
|
|
3771
|
-
* before a closing tag being a text node the parent does gain, so the gap
|
|
3772
|
-
* shows on whitespace-free markup, where the wait fell back to the
|
|
3773
|
-
* deadline. One observer takes many targets, so the cost is one observe()
|
|
3774
|
-
* per level, and it is disconnected at the first checkpoint past the
|
|
3775
|
-
* element either way.
|
|
3776
|
-
* @param {any} el
|
|
3777
|
-
* @returns {Promise<any>}
|
|
3800
|
+
* @param {Element} el
|
|
3801
|
+
* @returns {Promise<Element>} the element
|
|
3778
3802
|
*/
|
|
3779
3803
|
static waitParsed(el) {
|
|
3780
3804
|
if (Nodes.isParsed(el)) {
|
|
@@ -3799,10 +3823,10 @@ class Nodes {
|
|
|
3799
3823
|
}
|
|
3800
3824
|
|
|
3801
3825
|
/**
|
|
3802
|
-
*
|
|
3803
|
-
* @param {Element} el
|
|
3826
|
+
* The first child element matching the selector, descendants excluded.
|
|
3827
|
+
* @param {Element|DocumentFragment} el
|
|
3804
3828
|
* @param {string} selector
|
|
3805
|
-
* @returns
|
|
3829
|
+
* @returns {Element|null}
|
|
3806
3830
|
*/
|
|
3807
3831
|
static queryChildren(el, selector) {
|
|
3808
3832
|
for (const c of el.children) {
|
|
@@ -3813,10 +3837,10 @@ class Nodes {
|
|
|
3813
3837
|
return null;
|
|
3814
3838
|
}
|
|
3815
3839
|
/**
|
|
3816
|
-
*
|
|
3817
|
-
* @param {Element} el
|
|
3840
|
+
* Every child element matching the selector, descendants excluded.
|
|
3841
|
+
* @param {Element|DocumentFragment} el
|
|
3818
3842
|
* @param {string} selector
|
|
3819
|
-
* @returns
|
|
3843
|
+
* @returns {Element[]}
|
|
3820
3844
|
*/
|
|
3821
3845
|
static queryChildrenAll(el, selector) {
|
|
3822
3846
|
const r = [];
|
|
@@ -3873,9 +3897,11 @@ class NodeOperations {
|
|
|
3873
3897
|
|
|
3874
3898
|
/** The `data-tpl-*` commands, each taking the node it is written on and the scope it evaluates in. */
|
|
3875
3899
|
class CommandsHandler {
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3900
|
+
/**
|
|
3901
|
+
* The order the commands run in on one element, each seeing the node as the
|
|
3902
|
+
* ones before left it: tplIf gates before tplWith and tplEach push their
|
|
3903
|
+
* overlay, tplWhen gates after, inside the scope that overlay opened.
|
|
3904
|
+
*/
|
|
3879
3905
|
static ORDERED_COMMANDS = [
|
|
3880
3906
|
'tplIf',
|
|
3881
3907
|
'tplWith',
|
|
@@ -3886,8 +3912,7 @@ class CommandsHandler {
|
|
|
3886
3912
|
'tplRemove',
|
|
3887
3913
|
'tplVerbatim',
|
|
3888
3914
|
];
|
|
3889
|
-
|
|
3890
|
-
//if evaluates in the outer scope, before with/each
|
|
3915
|
+
/** Removes the node when the expression is falsy, evaluated before tplWith and tplEach open their overlay. */
|
|
3891
3916
|
static tplIf(node, expression, ops, evaluator) {
|
|
3892
3917
|
const accept = evaluator.evaluateExpression(expression);
|
|
3893
3918
|
if (!accept) {
|
|
@@ -3902,49 +3927,36 @@ class CommandsHandler {
|
|
|
3902
3927
|
.render();
|
|
3903
3928
|
ops.replace(node, newNode);
|
|
3904
3929
|
}
|
|
3930
|
+
/**
|
|
3931
|
+
* Renders the node once per entry, in place of the node. A Map iterates as
|
|
3932
|
+
* `{ key, value }` entries in its own order, a plain object as `{ key, value }`
|
|
3933
|
+
* entries in `Object.entries` order, and any other iterable as itself; a
|
|
3934
|
+
* value that is none of these throws. Each render sees its entry under
|
|
3935
|
+
* `data-tpl-var`, or as the whole overlay without one. `data-tpl-stat` names
|
|
3936
|
+
* an overlay beneath the entry carrying `index`, `count`, `size`, `first`,
|
|
3937
|
+
* `last`, `even` and `odd`, so an entry property of the same name wins; the
|
|
3938
|
+
* size, and so `last`, is null for a collection that knows neither its
|
|
3939
|
+
* `length` nor its `size`, which is never consumed to learn it.
|
|
3940
|
+
*/
|
|
3905
3941
|
static tplEach(node, expression, ops, evaluator) {
|
|
3906
3942
|
const varName = ops.popData(node, 'tplVar');
|
|
3907
3943
|
const statName = ops.popData(node, 'tplStat');
|
|
3908
3944
|
const template = new Template(node, evaluator);
|
|
3909
3945
|
const evaluated = evaluator.evaluateExpression(expression);
|
|
3910
|
-
//keyed collections iterate as {key, value} entries: a Map in its own
|
|
3911
|
-
//order, a non-iterable plain dict in Object.entries order. Plain alone:
|
|
3912
|
-
//a class instance or a response wrapper standing where an array was
|
|
3913
|
-
//expected still fails loudly below, and any other iterable (a Set, a
|
|
3914
|
-
//generator, an entries() iterator) iterates as itself
|
|
3915
3946
|
const proto = evaluated === null || typeof evaluated !== 'object' ? undefined : Object.getPrototypeOf(evaluated);
|
|
3916
|
-
const
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
: !evaluated?.[Symbol.iterator] && (proto === Object.prototype || proto === null)
|
|
3920
|
-
? Object.entries(evaluated)
|
|
3921
|
-
: null;
|
|
3947
|
+
const plain = !evaluated?.[Symbol.iterator] && (proto === Object.prototype || proto === null);
|
|
3948
|
+
const dict = plain ? Object.entries(evaluated) : null;
|
|
3949
|
+
const keyed = evaluated instanceof Map ? [...evaluated] : dict;
|
|
3922
3950
|
const entries = keyed === null ? evaluated : keyed.map(([key, value]) => ({ key, value }));
|
|
3923
3951
|
if (!entries?.[Symbol.iterator]) {
|
|
3924
3952
|
throw new Error(`Expected an iterable got '${evaluated}'`);
|
|
3925
3953
|
}
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
}
|
|
3930
|
-
ops.remove(node);
|
|
3931
|
-
return;
|
|
3932
|
-
}
|
|
3933
|
-
//the stat rides grouped under its declared name, overlaid below the
|
|
3934
|
-
//item, so data can never collide with its fields and an item property
|
|
3935
|
-
//sharing the stat's very name wins, as data always does. The size is
|
|
3936
|
-
//read where the collection already knows it (arrays and keyed entries
|
|
3937
|
-
//by length, sets and the like by size): an arbitrary iterator is never
|
|
3938
|
-
//consumed to learn it, so its size and last read null, the unknown
|
|
3939
|
-
const size = Array.isArray(entries)
|
|
3940
|
-
? entries.length
|
|
3941
|
-
: typeof entries.length === 'number'
|
|
3942
|
-
? entries.length
|
|
3943
|
-
: typeof entries.size === 'number'
|
|
3944
|
-
? entries.size
|
|
3945
|
-
: null;
|
|
3954
|
+
const measured = statName ? entries : null;
|
|
3955
|
+
const counted = typeof measured?.length === 'number' ? measured.length : measured?.size;
|
|
3956
|
+
const size = typeof counted === 'number' ? counted : null;
|
|
3946
3957
|
let index = 0;
|
|
3947
3958
|
for (const v of entries) {
|
|
3959
|
+
const item = varName ? { [varName]: v } : v;
|
|
3948
3960
|
const stat = {
|
|
3949
3961
|
index,
|
|
3950
3962
|
count: index + 1,
|
|
@@ -3954,16 +3966,13 @@ class CommandsHandler {
|
|
|
3954
3966
|
even: index % 2 === 0,
|
|
3955
3967
|
odd: index % 2 === 1,
|
|
3956
3968
|
};
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
template.withOverlay({ [statName]: stat }, varName ? { [varName]: v } : v).render(),
|
|
3960
|
-
);
|
|
3969
|
+
const layers = statName ? [{ [statName]: stat }, item] : [item];
|
|
3970
|
+
ops.prepend(node, template.withOverlay(...layers).render());
|
|
3961
3971
|
++index;
|
|
3962
3972
|
}
|
|
3963
3973
|
ops.remove(node);
|
|
3964
3974
|
}
|
|
3965
|
-
|
|
3966
|
-
//when evaluates after with/each, in the scope their overlay opened
|
|
3975
|
+
/** Removes the node when the expression is falsy, evaluated in the overlay tplWith or tplEach opened. */
|
|
3967
3976
|
static tplWhen(node, expression, ops, evaluator) {
|
|
3968
3977
|
const accept = evaluator.evaluateExpression(expression);
|
|
3969
3978
|
if (!accept) {
|
|
@@ -3977,12 +3986,8 @@ class CommandsHandler {
|
|
|
3977
3986
|
static tplRemove(node, value, ops, evaluator) {
|
|
3978
3987
|
switch (value.toLowerCase()) {
|
|
3979
3988
|
case 'tag': {
|
|
3980
|
-
const fragment =
|
|
3981
|
-
while (node.firstChild) {
|
|
3982
|
-
fragment.appendChild(node.firstChild);
|
|
3983
|
-
}
|
|
3989
|
+
const fragment = Fragments.fromChildNodes(node);
|
|
3984
3990
|
if ('tplVerbatim' in node.dataset) {
|
|
3985
|
-
//we are removing the parent element so we have to handle the lower priority commands
|
|
3986
3991
|
ops.popData(node, 'tplVerbatim');
|
|
3987
3992
|
ops.replace(node, fragment);
|
|
3988
3993
|
} else {
|
|
@@ -4045,12 +4050,10 @@ class CommandsHandler {
|
|
|
4045
4050
|
}
|
|
4046
4051
|
}
|
|
4047
4052
|
|
|
4048
|
-
// Module-isolated string cache for dataset-to-attribute conversions
|
|
4049
4053
|
const attributeCache = new BoundedCache(1000);
|
|
4050
4054
|
|
|
4051
4055
|
/**
|
|
4052
|
-
*
|
|
4053
|
-
* Uses a cache lookup to avoid repetitive regex/string splitting.
|
|
4056
|
+
* The attribute a `tpl` dataset key targets: `tplAriaLabel` is `aria-label`.
|
|
4054
4057
|
* @param {string} dataSetKey
|
|
4055
4058
|
* @returns {string}
|
|
4056
4059
|
*/
|
|
@@ -4072,38 +4075,40 @@ function toAttr(dataSetKey) {
|
|
|
4072
4075
|
*/
|
|
4073
4076
|
class Template {
|
|
4074
4077
|
/**
|
|
4075
|
-
* Creates a template from
|
|
4078
|
+
* Creates a template from html.
|
|
4076
4079
|
* @param {string} html
|
|
4077
4080
|
* @param {{ [k: string] : any }?} [modules]
|
|
4078
|
-
* @param {...*} data
|
|
4079
|
-
* @returns
|
|
4081
|
+
* @param {...*} data the data stack, innermost last
|
|
4082
|
+
* @returns {Template}
|
|
4080
4083
|
*/
|
|
4081
4084
|
static fromHtml(html, modules, ...data) {
|
|
4082
4085
|
return new Template(Fragments.fromHtml(html), new ExpressionEvaluator(modules, data));
|
|
4083
4086
|
}
|
|
4084
4087
|
|
|
4085
4088
|
/**
|
|
4086
|
-
* Creates a template from the content of the first
|
|
4089
|
+
* Creates a template from the content of the first element matching the
|
|
4090
|
+
* selector, which is adopted: the template element is left empty.
|
|
4087
4091
|
* @param {string} selector for an HTMLTemplateElement
|
|
4088
4092
|
* @param {{ [k: string] : any }?} [modules]
|
|
4089
|
-
* @param {...*} data
|
|
4090
|
-
* @returns
|
|
4093
|
+
* @param {...*} data the data stack, innermost last
|
|
4094
|
+
* @returns {Template}
|
|
4095
|
+
* @throws when the selector matches no template element
|
|
4091
4096
|
*/
|
|
4092
4097
|
static fromSelector(selector, modules, ...data) {
|
|
4093
4098
|
const templateEl = document.querySelector(selector);
|
|
4094
4099
|
if (!(templateEl instanceof HTMLTemplateElement)) {
|
|
4095
4100
|
throw new Error('template selector does not match any template tag');
|
|
4096
4101
|
}
|
|
4097
|
-
|
|
4098
|
-
return new Template(fragment, new ExpressionEvaluator(modules, data));
|
|
4102
|
+
return Template.fromTemplate(templateEl, modules, ...data);
|
|
4099
4103
|
}
|
|
4100
4104
|
|
|
4101
4105
|
/**
|
|
4102
|
-
* Creates a template from the content of
|
|
4106
|
+
* Creates a template from the content of a template element, which is
|
|
4107
|
+
* adopted: the template element is left empty.
|
|
4103
4108
|
* @param {HTMLTemplateElement} templateEl
|
|
4104
4109
|
* @param {{ [k: string] : any }?} [modules]
|
|
4105
|
-
* @param {...*} data
|
|
4106
|
-
* @returns
|
|
4110
|
+
* @param {...*} data the data stack, innermost last
|
|
4111
|
+
* @returns {Template}
|
|
4107
4112
|
*/
|
|
4108
4113
|
static fromTemplate(templateEl, modules, ...data) {
|
|
4109
4114
|
const fragment = document.adoptNode(templateEl.content);
|
|
@@ -4111,11 +4116,12 @@ class Template {
|
|
|
4111
4116
|
}
|
|
4112
4117
|
|
|
4113
4118
|
/**
|
|
4114
|
-
* Creates a template
|
|
4119
|
+
* Creates a template over a fragment, which is imported afresh on every
|
|
4120
|
+
* render and never modified.
|
|
4115
4121
|
* @param {DocumentFragment} fragment
|
|
4116
4122
|
* @param { { [k: string] : any }? } [modules]
|
|
4117
|
-
* @param {...*} data
|
|
4118
|
-
* @returns
|
|
4123
|
+
* @param {...*} data the data stack, innermost last
|
|
4124
|
+
* @returns {Template}
|
|
4119
4125
|
*/
|
|
4120
4126
|
static fromFragment(fragment, modules, ...data) {
|
|
4121
4127
|
return new Template(fragment, new ExpressionEvaluator(modules, data));
|
|
@@ -4135,28 +4141,32 @@ class Template {
|
|
|
4135
4141
|
* Creates a new Template rendering in another scope: the one way to rebind
|
|
4136
4142
|
* a compiled template to a registry's modules and data.
|
|
4137
4143
|
* @param {ExpressionEvaluator} evaluator
|
|
4144
|
+
* @returns {Template}
|
|
4138
4145
|
*/
|
|
4139
4146
|
withEvaluator(evaluator) {
|
|
4140
4147
|
return new Template(this.#fragment, evaluator);
|
|
4141
4148
|
}
|
|
4142
4149
|
/**
|
|
4143
|
-
* Creates a new Template
|
|
4150
|
+
* Creates a new Template over another fragment, in the same scope.
|
|
4144
4151
|
* @param {DocumentFragment} fragment
|
|
4152
|
+
* @returns {Template}
|
|
4145
4153
|
*/
|
|
4146
4154
|
withFragment(fragment) {
|
|
4147
4155
|
return new Template(fragment, this.#evaluator);
|
|
4148
4156
|
}
|
|
4149
4157
|
/**
|
|
4150
|
-
* Creates a new Template with a
|
|
4158
|
+
* Creates a new Template with a module added, as `ExpressionEvaluator.withModule` does.
|
|
4151
4159
|
* @param {string?} name
|
|
4152
4160
|
* @param {{[k: string]: any}} value
|
|
4161
|
+
* @returns {Template}
|
|
4153
4162
|
*/
|
|
4154
4163
|
withModule(name, value) {
|
|
4155
4164
|
return new Template(this.#fragment, this.#evaluator.withModule(name, value));
|
|
4156
4165
|
}
|
|
4157
4166
|
/**
|
|
4158
|
-
* Creates a new Template with
|
|
4167
|
+
* Creates a new Template with the data pushed as the innermost overlays.
|
|
4159
4168
|
* @param {...*} data
|
|
4169
|
+
* @returns {Template}
|
|
4160
4170
|
*/
|
|
4161
4171
|
withOverlay(...data) {
|
|
4162
4172
|
return new Template(this.#fragment, this.#evaluator.withOverlay(...data));
|
|
@@ -4165,7 +4175,7 @@ class Template {
|
|
|
4165
4175
|
* Evaluates an expression in this template's scope, widened by the overlays.
|
|
4166
4176
|
* @param {string} expression
|
|
4167
4177
|
* @param {...*} data
|
|
4168
|
-
* @returns
|
|
4178
|
+
* @returns {any}
|
|
4169
4179
|
*/
|
|
4170
4180
|
evaluateExpression(expression, ...data) {
|
|
4171
4181
|
return this.#evaluator.withOverlay(...data).evaluateExpression(expression);
|
|
@@ -4174,33 +4184,29 @@ class Template {
|
|
|
4174
4184
|
* Evaluates a templated text, the `{{ }}` form, in this template's scope.
|
|
4175
4185
|
* @param {string} text
|
|
4176
4186
|
* @param {...*} data
|
|
4177
|
-
* @returns the parts
|
|
4187
|
+
* @returns {{ type: symbol, value: any }[]} the parts, as `Expressions.MODE_TEMPLATED` describes
|
|
4178
4188
|
*/
|
|
4179
4189
|
evaluateTemplated(text, ...data) {
|
|
4180
4190
|
return this.#evaluator.withOverlay(...data).evaluateTemplated(text);
|
|
4181
4191
|
}
|
|
4182
4192
|
/**
|
|
4183
|
-
*
|
|
4193
|
+
* The scope this template renders in.
|
|
4194
|
+
* @returns {ExpressionEvaluator}
|
|
4184
4195
|
*/
|
|
4185
4196
|
evaluator() {
|
|
4186
4197
|
return this.#evaluator;
|
|
4187
4198
|
}
|
|
4188
4199
|
/**
|
|
4189
|
-
* Renders the template
|
|
4190
|
-
*
|
|
4200
|
+
* Renders the template into a new fragment, applying the `data-tpl-*`
|
|
4201
|
+
* commands and bindings and the `{{ }}` interpolations.
|
|
4202
|
+
* @returns {DocumentFragment}
|
|
4203
|
+
* @throws {RenderError} naming the node and the directive that failed
|
|
4191
4204
|
*/
|
|
4192
4205
|
render() {
|
|
4193
4206
|
try {
|
|
4194
4207
|
const ops = new NodeOperations();
|
|
4195
4208
|
const imported = document.importNode(this.#fragment, true);
|
|
4196
|
-
const fragment =
|
|
4197
|
-
imported.nodeType === Node.DOCUMENT_FRAGMENT_NODE
|
|
4198
|
-
? imported
|
|
4199
|
-
: (() => {
|
|
4200
|
-
const d = new DocumentFragment();
|
|
4201
|
-
d.appendChild(imported);
|
|
4202
|
-
return d;
|
|
4203
|
-
})();
|
|
4209
|
+
const fragment = imported.nodeType === Node.DOCUMENT_FRAGMENT_NODE ? imported : Fragments.from(imported);
|
|
4204
4210
|
const iterator = document.createNodeIterator(
|
|
4205
4211
|
fragment,
|
|
4206
4212
|
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
|
@@ -4226,8 +4232,6 @@ class Template {
|
|
|
4226
4232
|
try {
|
|
4227
4233
|
CommandsHandler[command](el, value, ops, this.#evaluator);
|
|
4228
4234
|
} catch (ex) {
|
|
4229
|
-
//the directive is popped before it runs, so the open tag no
|
|
4230
|
-
//longer carries it: the frame names it and its expression
|
|
4231
4235
|
throw RenderError.wrap(`Error evaluating data-tpl-${toAttr(command)}="${value}"`, el, ex);
|
|
4232
4236
|
}
|
|
4233
4237
|
if (ops.removed(el)) {
|
|
@@ -4250,17 +4254,13 @@ class Template {
|
|
|
4250
4254
|
el.setAttribute(attributeName, evaluated);
|
|
4251
4255
|
}
|
|
4252
4256
|
} catch (ex) {
|
|
4253
|
-
throw RenderError.wrap(`Error evaluating data-tpl-${
|
|
4257
|
+
throw RenderError.wrap(`Error evaluating data-tpl-${attributeName}="${expression}"`, el, ex);
|
|
4254
4258
|
}
|
|
4255
4259
|
}
|
|
4256
4260
|
}
|
|
4257
4261
|
ops.cleanup();
|
|
4258
4262
|
return fragment;
|
|
4259
4263
|
} catch (ex) {
|
|
4260
|
-
//a command, a text node or a nested render already named the node it
|
|
4261
|
-
//failed on: wrapping again would add a frame for the fragment that
|
|
4262
|
-
//contains it, one per level, serializing the whole template into the
|
|
4263
|
-
//message that survives. Only a failure outside those is framed here
|
|
4264
4264
|
if (ex instanceof RenderError) {
|
|
4265
4265
|
throw ex;
|
|
4266
4266
|
}
|
|
@@ -4268,22 +4268,26 @@ class Template {
|
|
|
4268
4268
|
}
|
|
4269
4269
|
}
|
|
4270
4270
|
/**
|
|
4271
|
-
* Renders this template
|
|
4271
|
+
* Renders this template into the element, replacing its children.
|
|
4272
4272
|
* @param {Element} el
|
|
4273
|
+
* @throws {RenderError}
|
|
4273
4274
|
*/
|
|
4274
4275
|
renderTo(el) {
|
|
4275
4276
|
el.replaceChildren(this.render());
|
|
4276
4277
|
}
|
|
4277
4278
|
/**
|
|
4278
|
-
* Renders this template
|
|
4279
|
+
* Renders this template and appends the result to the element.
|
|
4279
4280
|
* @param {Element} el
|
|
4281
|
+
* @throws {RenderError}
|
|
4280
4282
|
*/
|
|
4281
4283
|
appendTo(el) {
|
|
4282
4284
|
el.appendChild(this.render());
|
|
4283
4285
|
}
|
|
4284
4286
|
/**
|
|
4285
|
-
* Renders this template
|
|
4287
|
+
* Renders this template into the first element matching the selector,
|
|
4288
|
+
* replacing its children; does nothing when none matches.
|
|
4286
4289
|
* @param {string} selector
|
|
4290
|
+
* @throws {RenderError}
|
|
4287
4291
|
*/
|
|
4288
4292
|
renderToSelector(selector) {
|
|
4289
4293
|
const el = document.querySelector(selector);
|
|
@@ -4292,8 +4296,10 @@ class Template {
|
|
|
4292
4296
|
}
|
|
4293
4297
|
}
|
|
4294
4298
|
/**
|
|
4295
|
-
* Renders this template
|
|
4299
|
+
* Renders this template and appends the result to the first element
|
|
4300
|
+
* matching the selector; does nothing when none matches.
|
|
4296
4301
|
* @param {string} selector
|
|
4302
|
+
* @throws {RenderError}
|
|
4297
4303
|
*/
|
|
4298
4304
|
appendToSelector(selector) {
|
|
4299
4305
|
const el = document.querySelector(selector);
|
|
@@ -4317,30 +4323,26 @@ class Template {
|
|
|
4317
4323
|
}
|
|
4318
4324
|
|
|
4319
4325
|
/**
|
|
4320
|
-
* A render failure, one frame per nesting level
|
|
4321
|
-
*
|
|
4322
|
-
*
|
|
4323
|
-
*
|
|
4324
|
-
* the
|
|
4325
|
-
* `node`, so a failure costs no clone and a nested failure does not embed the
|
|
4326
|
-
* page in its own message.
|
|
4326
|
+
* A render failure, one frame per nesting level, chained through `cause` from
|
|
4327
|
+
* the outermost frame down to the error that failed the expression. A frame's
|
|
4328
|
+
* message names what was being evaluated and the node's open tag, with the
|
|
4329
|
+
* attributes it still carries: the directive being evaluated is already off
|
|
4330
|
+
* the tag. The subtree is not in the message; `html` serializes it on demand.
|
|
4327
4331
|
*/
|
|
4328
4332
|
class RenderError extends Error {
|
|
4329
|
-
/**
|
|
4330
|
-
* How many frames a chain keeps. The innermost are the specific ones, so a
|
|
4331
|
-
* deeper nesting drops the outer context rather than the failure site: three
|
|
4332
|
-
* frames name the offending node and the two levels that hold it, which is
|
|
4333
|
-
* the path a reader follows without the page arriving with it.
|
|
4334
|
-
*/
|
|
4333
|
+
/** How many frames a chain keeps: a deeper nesting drops the outermost ones and marks the chain `truncated`. */
|
|
4335
4334
|
static FRAMES = 3;
|
|
4336
4335
|
#node;
|
|
4337
4336
|
#depth;
|
|
4338
|
-
/**
|
|
4337
|
+
/** Whether the chain dropped outer frames to stay within FRAMES. */
|
|
4339
4338
|
truncated = false;
|
|
4340
4339
|
/**
|
|
4341
|
-
* Frames a failure, unless the chain already
|
|
4342
|
-
* cause
|
|
4343
|
-
*
|
|
4340
|
+
* Frames a failure, unless the chain already holds FRAMES frames: then the
|
|
4341
|
+
* cause is returned as it is, marked `truncated`.
|
|
4342
|
+
* @param {string} message
|
|
4343
|
+
* @param {Node} nodeOrFragment the node that failed
|
|
4344
|
+
* @param {any} cause
|
|
4345
|
+
* @returns {RenderError}
|
|
4344
4346
|
*/
|
|
4345
4347
|
static wrap(message, nodeOrFragment, cause) {
|
|
4346
4348
|
if (cause instanceof RenderError && cause.depth >= RenderError.FRAMES) {
|
|
@@ -4349,27 +4351,44 @@ class RenderError extends Error {
|
|
|
4349
4351
|
}
|
|
4350
4352
|
return new RenderError(message, nodeOrFragment, cause);
|
|
4351
4353
|
}
|
|
4354
|
+
/**
|
|
4355
|
+
* @param {string} message
|
|
4356
|
+
* @param {Node} nodeOrFragment the node that failed
|
|
4357
|
+
* @param {any} cause
|
|
4358
|
+
*/
|
|
4352
4359
|
constructor(message, nodeOrFragment, cause) {
|
|
4353
4360
|
super(`${message} in \`${RenderError.describe(nodeOrFragment)}\``, { cause });
|
|
4354
4361
|
this.name = 'RenderError';
|
|
4355
4362
|
this.#node = nodeOrFragment;
|
|
4356
4363
|
this.#depth = (cause instanceof RenderError ? cause.depth : 0) + 1;
|
|
4357
4364
|
}
|
|
4358
|
-
/**
|
|
4365
|
+
/**
|
|
4366
|
+
* How many frames this chain carries, this one included.
|
|
4367
|
+
* @returns {number}
|
|
4368
|
+
*/
|
|
4359
4369
|
get depth() {
|
|
4360
4370
|
return this.#depth;
|
|
4361
4371
|
}
|
|
4362
|
-
/**
|
|
4372
|
+
/**
|
|
4373
|
+
* The node the render failed on, live in the fragment being built.
|
|
4374
|
+
* @returns {Node}
|
|
4375
|
+
*/
|
|
4363
4376
|
get node() {
|
|
4364
4377
|
return this.#node;
|
|
4365
4378
|
}
|
|
4366
|
-
/**
|
|
4379
|
+
/**
|
|
4380
|
+
* The failed node's markup, whitespace trimmed, serialized on each read.
|
|
4381
|
+
* @returns {string}
|
|
4382
|
+
*/
|
|
4367
4383
|
get html() {
|
|
4368
4384
|
return RenderError.stringify(this.#node);
|
|
4369
4385
|
}
|
|
4370
4386
|
/**
|
|
4371
4387
|
* What identifies a node in a frame: an element by its open tag, a text node
|
|
4372
|
-
* by its source, a fragment by the open tags of the elements it holds
|
|
4388
|
+
* by its source, a fragment by the open tags of the elements it holds, cut
|
|
4389
|
+
* at 120 characters.
|
|
4390
|
+
* @param {Node} nodeOrFragment
|
|
4391
|
+
* @returns {string}
|
|
4373
4392
|
*/
|
|
4374
4393
|
static describe(nodeOrFragment) {
|
|
4375
4394
|
if (nodeOrFragment.nodeType === Node.TEXT_NODE) {
|
|
@@ -4388,10 +4407,13 @@ class RenderError extends Error {
|
|
|
4388
4407
|
static #ellipsize(text) {
|
|
4389
4408
|
return text.length > 120 ? `${text.slice(0, 120)}…` : text;
|
|
4390
4409
|
}
|
|
4410
|
+
/**
|
|
4411
|
+
* A node's markup with its whitespace-only text dropped and the rest trimmed.
|
|
4412
|
+
* @param {Node} nodeOrFragment left untouched: a clone is serialized
|
|
4413
|
+
* @returns {string}
|
|
4414
|
+
*/
|
|
4391
4415
|
static stringify(nodeOrFragment) {
|
|
4392
|
-
|
|
4393
|
-
t.content.appendChild(RenderError.#cleanup(nodeOrFragment.cloneNode(true)));
|
|
4394
|
-
return t.innerHTML;
|
|
4416
|
+
return Fragments.toHtml(RenderError.#cleanup(nodeOrFragment.cloneNode(true)));
|
|
4395
4417
|
}
|
|
4396
4418
|
static #cleanup(node) {
|
|
4397
4419
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -4413,11 +4435,6 @@ class RenderError extends Error {
|
|
|
4413
4435
|
}
|
|
4414
4436
|
}
|
|
4415
4437
|
|
|
4416
|
-
/**
|
|
4417
|
-
* Tracks the elements waiting to render. `ready` resolves once the queue has
|
|
4418
|
-
* drained and never rejects; the promise kept per element resolves when that
|
|
4419
|
-
* element has rendered and rejects with whatever its upgrade threw.
|
|
4420
|
-
*/
|
|
4421
4438
|
class UpgradeQueue {
|
|
4422
4439
|
#q = new Map();
|
|
4423
4440
|
#readyResolve;
|
|
@@ -4429,12 +4446,6 @@ class UpgradeQueue {
|
|
|
4429
4446
|
this.#start();
|
|
4430
4447
|
});
|
|
4431
4448
|
}
|
|
4432
|
-
/**
|
|
4433
|
-
* Waits for the page's readiness: the promise resolves right after the
|
|
4434
|
-
* ftl:ready event is dispatched, and immediately when that moment already
|
|
4435
|
-
* passed.
|
|
4436
|
-
* @returns {Promise<void>}
|
|
4437
|
-
*/
|
|
4438
4449
|
ready() {
|
|
4439
4450
|
return this.#ready;
|
|
4440
4451
|
}
|
|
@@ -4450,13 +4461,8 @@ class UpgradeQueue {
|
|
|
4450
4461
|
}
|
|
4451
4462
|
enqueue(el) {
|
|
4452
4463
|
if (this.#q.has(el)) {
|
|
4453
|
-
//already upgrading, can happen when disconnecting an element
|
|
4454
|
-
//while it's already queued for upgrade (e.g.: ful-form)
|
|
4455
4464
|
return;
|
|
4456
4465
|
}
|
|
4457
|
-
//one entry, two signals: readiness waits on a signal that only ever
|
|
4458
|
-
//resolves, so nothing here attaches a rejection handler to the upgrade
|
|
4459
|
-
//itself and a component that fails is still reported the way it always was
|
|
4460
4466
|
const { promise: finished, resolve: markFinished } = /** @type {PromiseWithResolvers<void>} */ (
|
|
4461
4467
|
Promise.withResolvers()
|
|
4462
4468
|
);
|
|
@@ -4468,11 +4474,6 @@ class UpgradeQueue {
|
|
|
4468
4474
|
});
|
|
4469
4475
|
this.#q.set(el, { upgrade, finished });
|
|
4470
4476
|
}
|
|
4471
|
-
/**
|
|
4472
|
-
* The one fixed-point loop: drains the accepted entries, including the ones
|
|
4473
|
-
* enqueued while waiting, since a component is only queued once its parent
|
|
4474
|
-
* connects it and a single pass would miss everything nested.
|
|
4475
|
-
*/
|
|
4476
4477
|
async #drain(accept, pick) {
|
|
4477
4478
|
for (;;) {
|
|
4478
4479
|
const pending = Array.from(this.#q)
|
|
@@ -4485,22 +4486,25 @@ class UpgradeQueue {
|
|
|
4485
4486
|
}
|
|
4486
4487
|
}
|
|
4487
4488
|
/**
|
|
4488
|
-
* Waits for the whole queue to drain
|
|
4489
|
-
*
|
|
4490
|
-
*
|
|
4489
|
+
* Waits for the whole queue to drain, the upgrades enqueued meanwhile included.
|
|
4490
|
+
* Never rejects: a failed upgrade counts as drained.
|
|
4491
|
+
* @returns {Promise<void>}
|
|
4491
4492
|
*/
|
|
4492
4493
|
settled() {
|
|
4493
4494
|
return this.#drain(() => true, (entry) => entry.finished);
|
|
4494
4495
|
}
|
|
4495
|
-
/**
|
|
4496
|
+
/**
|
|
4497
|
+
* Waits for the accepted upgrades, the ones enqueued meanwhile included,
|
|
4498
|
+
* rejecting with the first that failed.
|
|
4499
|
+
* @param {(el: Element) => boolean} accept
|
|
4500
|
+
* @returns {Promise<void>}
|
|
4501
|
+
*/
|
|
4496
4502
|
upgraded(accept) {
|
|
4497
4503
|
return this.#drain(accept, (entry) => entry.upgrade);
|
|
4498
4504
|
}
|
|
4499
|
-
/** The pending upgrade of one element, undefined when it is not queued. */
|
|
4500
4505
|
whenUpgraded(el) {
|
|
4501
4506
|
return this.#q.get(el)?.upgrade;
|
|
4502
4507
|
}
|
|
4503
|
-
/** The elements whose upgrade is still pending, in queue order. */
|
|
4504
4508
|
pending() {
|
|
4505
4509
|
return Array.from(this.#q.keys());
|
|
4506
4510
|
}
|
|
@@ -4511,8 +4515,8 @@ class UpgradeQueue {
|
|
|
4511
4515
|
* and attribute mappers. Elements defined before configure() are deferred and
|
|
4512
4516
|
* defined by it; from then on every defineElement takes effect immediately.
|
|
4513
4517
|
* The exported `registry` singleton is the page's own; a `new Registry()` is a
|
|
4514
|
-
* separate instance
|
|
4515
|
-
* singleton alone.
|
|
4518
|
+
* separate instance with its own upgrade queue, which dispatches its own
|
|
4519
|
+
* ftl:ready on the document, while Rendering waits on the singleton alone.
|
|
4516
4520
|
*/
|
|
4517
4521
|
class Registry {
|
|
4518
4522
|
#tagToClass = {};
|
|
@@ -4581,9 +4585,12 @@ class Registry {
|
|
|
4581
4585
|
* Registers a custom element under its tag: the class is augmented with its
|
|
4582
4586
|
* BITS (observed attributes, mappers, templates) and handed to the platform.
|
|
4583
4587
|
* Before configure() the definition is deferred, so import order never
|
|
4584
|
-
* matters.
|
|
4588
|
+
* matters. The element resolves its templates and components through this
|
|
4589
|
+
* registry, whichever one a module imports, and its `static config` is read
|
|
4590
|
+
* here, so a page replacing it does so before the definition.
|
|
4585
4591
|
* @param {string} tag
|
|
4586
4592
|
* @param {*} klass a ParsedElement subclass
|
|
4593
|
+
* @returns {Registry} this registry
|
|
4587
4594
|
*/
|
|
4588
4595
|
defineElement(tag, klass) {
|
|
4589
4596
|
if (!this.#configured) {
|
|
@@ -4601,13 +4608,8 @@ class Registry {
|
|
|
4601
4608
|
* ancestors', so a base class declares what every subclass keeps observing
|
|
4602
4609
|
* (a protocol attribute such as Field's disabled claim) and a leaf refines a
|
|
4603
4610
|
* mapping, or moves a name's position, without repeating the whole list.
|
|
4604
|
-
*
|
|
4605
|
-
*
|
|
4606
|
-
* stop can work, since a registered ancestor carries an own BITS of its own,
|
|
4607
|
-
* and nothing above the elements declares anything.
|
|
4608
|
-
*
|
|
4609
|
-
* Everything deriving a component's attribute vocabulary reads it here, so
|
|
4610
|
-
* the runtime and whatever documents it cannot walk the chain differently.
|
|
4611
|
+
* The observed attributes are applied in the composed order, a re-declared
|
|
4612
|
+
* name taking the position of its last declaration.
|
|
4611
4613
|
* @param {*} klass a ParsedElement subclass
|
|
4612
4614
|
* @returns {{ observed: string[], attributes: string[] }}
|
|
4613
4615
|
*/
|
|
@@ -4623,14 +4625,7 @@ class Registry {
|
|
|
4623
4625
|
* The property an observed attribute drives, by the platform's own
|
|
4624
4626
|
* dash-to-camel rule: the one `dataset` applies, so `clear-invalid-on-change`
|
|
4625
4627
|
* would reach `clearInvalidOnChange` the way `data-clear-invalid-on-change`
|
|
4626
|
-
* reaches `dataset.clearInvalidOnChange`.
|
|
4627
|
-
* unchanged, which is what every observed attribute in the library is.
|
|
4628
|
-
*
|
|
4629
|
-
* It exists because the observed tier is the one that becomes properties:
|
|
4630
|
-
* without it an attribute could only be observed if its name happened to be
|
|
4631
|
-
* a usable identifier, which is why every multiword observed attribute here
|
|
4632
|
-
* used to be squashed into one word while the configuration tier, which
|
|
4633
|
-
* never becomes a property, spelled the same idea with a dash.
|
|
4628
|
+
* reaches `dataset.clearInvalidOnChange`.
|
|
4634
4629
|
*
|
|
4635
4630
|
* Only this direction is mapped. A property never derives its attribute: a
|
|
4636
4631
|
* setter reflects through `reflectTo`, naming the attribute it writes.
|
|
@@ -4643,10 +4638,6 @@ class Registry {
|
|
|
4643
4638
|
#augmentAndDefineElement(tag, klass) {
|
|
4644
4639
|
const { observed, attributes } = Registry.declarationsOf(klass);
|
|
4645
4640
|
const { template, templates, slots, mappers, config } = klass;
|
|
4646
|
-
//a name a subclass re-declares takes the subclass's position, which is
|
|
4647
|
-
//how a field whose value setter reads its own shape attributes declares
|
|
4648
|
-
//that its value lands after them: the order the declarations compose in
|
|
4649
|
-
//is the order the base applies them
|
|
4650
4641
|
const declaredNames = observed.map((a) => a.split(':')[0]);
|
|
4651
4642
|
const observedNames = [...new Set(declaredNames.reverse())].reverse();
|
|
4652
4643
|
const attrToMapper = [...attributes, ...observed].reduce((acc, a) => {
|
|
@@ -4665,20 +4656,13 @@ class Registry {
|
|
|
4665
4656
|
const nameToTemplate = Object.fromEntries(namesAndTemplates);
|
|
4666
4657
|
|
|
4667
4658
|
klass.BITS = {
|
|
4668
|
-
//the defining registry travels with the definition: an element resolves
|
|
4669
|
-
//its templates and its components through the registry that defined it,
|
|
4670
|
-
//not through whichever one a module happened to import
|
|
4671
4659
|
registry: this,
|
|
4672
4660
|
enqueue: (el) => this.#upgradeQueue.enqueue(el),
|
|
4673
4661
|
SLOTS: slots,
|
|
4674
|
-
//the class's own constants, overlaid on its templates as `config`:
|
|
4675
|
-
//read here with the rest of the declaration, so a page replacing them
|
|
4676
|
-
//does it before configure() like every other definition-time choice
|
|
4677
4662
|
CONFIG: config,
|
|
4678
4663
|
OBSERVED: observedNames,
|
|
4679
4664
|
DECLARED: [...new Set([...observedNames, ...attributes.map((a) => a.split(':')[0])])],
|
|
4680
4665
|
ATTR_TO_MAPPER: attrToMapper,
|
|
4681
|
-
//resolved once here rather than at every attribute write
|
|
4682
4666
|
ATTR_TO_PROPERTY: Object.fromEntries(observedNames.map((a) => [a, Registry.propertyOf(a)])),
|
|
4683
4667
|
TEMPLATES: nameToTemplate,
|
|
4684
4668
|
};
|
|
@@ -4690,6 +4674,7 @@ class Registry {
|
|
|
4690
4674
|
* as `#fn`.
|
|
4691
4675
|
* @param {string} name
|
|
4692
4676
|
* @param {object} value
|
|
4677
|
+
* @returns {Registry} this registry
|
|
4693
4678
|
*/
|
|
4694
4679
|
defineModule(name, value) {
|
|
4695
4680
|
const module = name ? { [name]: value } : value;
|
|
@@ -4700,6 +4685,7 @@ class Registry {
|
|
|
4700
4685
|
/**
|
|
4701
4686
|
* Replaces the whole module map.
|
|
4702
4687
|
* @param {object} ms
|
|
4688
|
+
* @returns {Registry} this registry
|
|
4703
4689
|
*/
|
|
4704
4690
|
defineModules(ms) {
|
|
4705
4691
|
this.#modules = ms;
|
|
@@ -4711,6 +4697,7 @@ class Registry {
|
|
|
4711
4697
|
* through component().
|
|
4712
4698
|
* @param {string} name
|
|
4713
4699
|
* @param {*} value
|
|
4700
|
+
* @returns {Registry} this registry
|
|
4714
4701
|
*/
|
|
4715
4702
|
defineComponent(name, value) {
|
|
4716
4703
|
this.#components[name] = value;
|
|
@@ -4719,6 +4706,7 @@ class Registry {
|
|
|
4719
4706
|
/**
|
|
4720
4707
|
* Replaces the data stack the templates evaluate over.
|
|
4721
4708
|
* @param {...any} data
|
|
4709
|
+
* @returns {Registry} this registry
|
|
4722
4710
|
*/
|
|
4723
4711
|
defineData(...data) {
|
|
4724
4712
|
this.#data = data;
|
|
@@ -4728,6 +4716,7 @@ class Registry {
|
|
|
4728
4716
|
/**
|
|
4729
4717
|
* Appends to the data stack, the later entry winning a shared name.
|
|
4730
4718
|
* @param {...any} data
|
|
4719
|
+
* @returns {Registry} this registry
|
|
4731
4720
|
*/
|
|
4732
4721
|
defineOverlay(...data) {
|
|
4733
4722
|
this.#data = [...this.#data, ...data];
|
|
@@ -4739,6 +4728,7 @@ class Registry {
|
|
|
4739
4728
|
* `name:type` declaration.
|
|
4740
4729
|
* @param {string} k
|
|
4741
4730
|
* @param {{ unmarshal(str: string|null, name: string, el: Element): any, marshal(value: any, name: string, el: Element): string|null }} v
|
|
4731
|
+
* @returns {Registry} this registry
|
|
4742
4732
|
*/
|
|
4743
4733
|
defineMapper(k, v) {
|
|
4744
4734
|
this.#mappers[k] = v;
|
|
@@ -4747,6 +4737,7 @@ class Registry {
|
|
|
4747
4737
|
/**
|
|
4748
4738
|
* Hands the registry over to the plugin's configure.
|
|
4749
4739
|
* @param {{ configure(registry: Registry): void }} p
|
|
4740
|
+
* @returns {Registry} this registry
|
|
4750
4741
|
*/
|
|
4751
4742
|
plugin(p) {
|
|
4752
4743
|
p.configure(this);
|
|
@@ -4755,6 +4746,7 @@ class Registry {
|
|
|
4755
4746
|
/**
|
|
4756
4747
|
* Defines every element deferred so far; from then on, defineElement takes
|
|
4757
4748
|
* effect immediately.
|
|
4749
|
+
* @returns {Registry} this registry
|
|
4758
4750
|
*/
|
|
4759
4751
|
configure() {
|
|
4760
4752
|
for (const [tag, klass] of Object.entries(this.#tagToClass)) {
|
|
@@ -4766,23 +4758,36 @@ class Registry {
|
|
|
4766
4758
|
}
|
|
4767
4759
|
/**
|
|
4768
4760
|
* Waits for the queued upgrades the filter accepts, rejecting with the first
|
|
4769
|
-
* that failed
|
|
4761
|
+
* that failed, the upgrades enqueued meanwhile included: the rejecting
|
|
4762
|
+
* barrier Rendering is a facade over.
|
|
4770
4763
|
* @param {(el: Element) => boolean} accept
|
|
4764
|
+
* @returns {Promise<void>}
|
|
4771
4765
|
*/
|
|
4772
4766
|
settle(accept) {
|
|
4773
4767
|
return this.#upgradeQueue.upgraded(accept);
|
|
4774
4768
|
}
|
|
4775
|
-
/**
|
|
4769
|
+
/**
|
|
4770
|
+
* The pending upgrade of one element: resolving when it has rendered,
|
|
4771
|
+
* rejecting with what its upgrade threw.
|
|
4772
|
+
* @param {Element} el
|
|
4773
|
+
* @returns {Promise<void>|undefined} undefined when the element is not queued
|
|
4774
|
+
*/
|
|
4776
4775
|
whenUpgraded(el) {
|
|
4777
4776
|
return this.#upgradeQueue.whenUpgraded(el);
|
|
4778
4777
|
}
|
|
4779
|
-
/**
|
|
4778
|
+
/**
|
|
4779
|
+
* The elements whose upgrade is still pending, in queue order.
|
|
4780
|
+
* @returns {Element[]}
|
|
4781
|
+
*/
|
|
4780
4782
|
pending() {
|
|
4781
4783
|
return this.#upgradeQueue.pending();
|
|
4782
4784
|
}
|
|
4783
4785
|
/**
|
|
4784
|
-
* Waits for
|
|
4785
|
-
* dispatched
|
|
4786
|
+
* Waits for this registry's readiness: resolves right after its ftl:ready
|
|
4787
|
+
* event is dispatched on the document, and immediately when that already
|
|
4788
|
+
* happened. Never rejects: a failed upgrade counts as settled, and its
|
|
4789
|
+
* rejection is left unhandled so it still reaches the console and the
|
|
4790
|
+
* page's error reporting.
|
|
4786
4791
|
* @returns {Promise<void>}
|
|
4787
4792
|
*/
|
|
4788
4793
|
ready() {
|
|
@@ -4795,11 +4800,16 @@ class Registry {
|
|
|
4795
4800
|
* The scope every template on this registry renders in: the modules and the
|
|
4796
4801
|
* data stack, as one value. Replaced whenever either is defined, so a holder
|
|
4797
4802
|
* of an older one keeps rendering against what it was handed.
|
|
4803
|
+
* @returns {ExpressionEvaluator}
|
|
4798
4804
|
*/
|
|
4799
4805
|
evaluator() {
|
|
4800
4806
|
return this.#evaluator;
|
|
4801
4807
|
}
|
|
4802
|
-
/**
|
|
4808
|
+
/**
|
|
4809
|
+
* The component registered under the name.
|
|
4810
|
+
* @param {string} name
|
|
4811
|
+
* @returns {any} undefined when none is
|
|
4812
|
+
*/
|
|
4803
4813
|
component(name) {
|
|
4804
4814
|
return this.#components[name];
|
|
4805
4815
|
}
|
|
@@ -4807,33 +4817,59 @@ class Registry {
|
|
|
4807
4817
|
|
|
4808
4818
|
const registry = new Registry();
|
|
4809
4819
|
|
|
4810
|
-
/** The Template factories, each bound to the page registry's scope. */
|
|
4811
4820
|
/** The Template factories bound to the page's own registry: the same four sources, with its modules and data already applied. */
|
|
4812
4821
|
class Templates {
|
|
4822
|
+
/**
|
|
4823
|
+
* @param {string} html
|
|
4824
|
+
* @returns {Template}
|
|
4825
|
+
*/
|
|
4813
4826
|
static fromHtml(html) {
|
|
4814
4827
|
return Template.fromHtml(html).withEvaluator(registry.evaluator());
|
|
4815
4828
|
}
|
|
4829
|
+
/**
|
|
4830
|
+
* @param {string} selector for an HTMLTemplateElement
|
|
4831
|
+
* @returns {Template}
|
|
4832
|
+
*/
|
|
4816
4833
|
static fromSelector(selector) {
|
|
4817
4834
|
return Template.fromSelector(selector).withEvaluator(registry.evaluator());
|
|
4818
4835
|
}
|
|
4836
|
+
/**
|
|
4837
|
+
* @param {HTMLTemplateElement} templateEl consumed: its content is adopted
|
|
4838
|
+
* @returns {Template}
|
|
4839
|
+
*/
|
|
4819
4840
|
static fromTemplate(templateEl) {
|
|
4820
4841
|
return Template.fromTemplate(templateEl).withEvaluator(registry.evaluator());
|
|
4821
4842
|
}
|
|
4843
|
+
/**
|
|
4844
|
+
* @param {DocumentFragment} fragment
|
|
4845
|
+
* @returns {Template}
|
|
4846
|
+
*/
|
|
4822
4847
|
static fromFragment(fragment) {
|
|
4823
4848
|
return Template.fromFragment(fragment).withEvaluator(registry.evaluator());
|
|
4824
4849
|
}
|
|
4825
4850
|
}
|
|
4826
4851
|
|
|
4827
4852
|
/**
|
|
4828
|
-
*
|
|
4829
|
-
*
|
|
4830
|
-
*
|
|
4831
|
-
* leaves a failed component to its own unhandled-rejection report.
|
|
4853
|
+
* Waits for elements queued on the page's `registry` to render. Unlike
|
|
4854
|
+
* `registry.ready()`, these reject when an awaited upgrade fails. An element
|
|
4855
|
+
* defined by another Registry is awaited through that registry instead.
|
|
4832
4856
|
*/
|
|
4833
4857
|
class Rendering {
|
|
4858
|
+
/**
|
|
4859
|
+
* Waits for the element and its descendants, the ones their own renders
|
|
4860
|
+
* enqueue included.
|
|
4861
|
+
* @param {Element} el
|
|
4862
|
+
* @returns {Promise<void>} rejecting with the first upgrade that failed
|
|
4863
|
+
*/
|
|
4834
4864
|
static waitFor(el) {
|
|
4835
4865
|
return registry.settle((child) => el.contains(child));
|
|
4836
4866
|
}
|
|
4867
|
+
/**
|
|
4868
|
+
* Waits for the element's descendants, the element itself excluded: what a
|
|
4869
|
+
* component awaits from inside its own render.
|
|
4870
|
+
* @param {Element} el
|
|
4871
|
+
* @returns {Promise<void>} rejecting with the first upgrade that failed
|
|
4872
|
+
*/
|
|
4837
4873
|
static waitForChildren(el) {
|
|
4838
4874
|
return registry.settle((child) => el !== child && el.contains(child));
|
|
4839
4875
|
}
|
|
@@ -4847,9 +4883,15 @@ class Rendering {
|
|
|
4847
4883
|
* @property {(val: any, name: string, el: Element) => string|null} marshal
|
|
4848
4884
|
*/
|
|
4849
4885
|
|
|
4886
|
+
/**
|
|
4887
|
+
* The base of every fml element. A subclass declares its attributes, slots and
|
|
4888
|
+
* templates in statics and builds its dom in `render`; the base reads the light
|
|
4889
|
+
* slots, reads the declared attributes through their mappers, renders once, and
|
|
4890
|
+
* then forwards observed attribute writes to the matching properties. An element
|
|
4891
|
+
* resolves templates and components through the registry that defined it.
|
|
4892
|
+
*/
|
|
4850
4893
|
class ParsedElement extends HTMLElement {
|
|
4851
4894
|
static BITS = {
|
|
4852
|
-
//an element the registry never defined still answers the page's own
|
|
4853
4895
|
registry,
|
|
4854
4896
|
enqueue: (el) => {},
|
|
4855
4897
|
SLOTS: false,
|
|
@@ -4873,14 +4915,12 @@ class ParsedElement extends HTMLElement {
|
|
|
4873
4915
|
this.internals = this.attachInternals();
|
|
4874
4916
|
}
|
|
4875
4917
|
/**
|
|
4876
|
-
* The
|
|
4877
|
-
*
|
|
4878
|
-
*
|
|
4879
|
-
*
|
|
4880
|
-
*
|
|
4881
|
-
*
|
|
4882
|
-
* A subclass must not redeclare it: a class field with no initializer
|
|
4883
|
-
* assigns undefined after super() returns, which would wipe it.
|
|
4918
|
+
* The element's ElementInternals, attached by the base for every element: a
|
|
4919
|
+
* subclass never calls attachInternals itself, since a second call throws.
|
|
4920
|
+
* A subclass declaring `static formAssociated` gets the form apis here.
|
|
4921
|
+
* A subclass must not redeclare the field: a class field without an
|
|
4922
|
+
* initializer assigns undefined after super() returns, wiping it.
|
|
4923
|
+
* @type {ElementInternals}
|
|
4884
4924
|
*/
|
|
4885
4925
|
internals;
|
|
4886
4926
|
#parsed = false;
|
|
@@ -4897,26 +4937,36 @@ class ParsedElement extends HTMLElement {
|
|
|
4897
4937
|
#mapper(attr) {
|
|
4898
4938
|
const mapper = this.#bits().ATTR_TO_MAPPER[attr];
|
|
4899
4939
|
if (!mapper) {
|
|
4900
|
-
//an attribute the class never declared has no type to read it as:
|
|
4901
|
-
//say so rather than dying on the missing mapper. Content this
|
|
4902
|
-
//element does not own, such as a custom loader's own configuration,
|
|
4903
|
-
//is read with getAttribute, the platform's own answer
|
|
4904
4940
|
throw new Error(
|
|
4905
4941
|
`${this.constructor.name} declares no attribute '${attr}': declare it in static observed or static attributes, or read it with getAttribute`,
|
|
4906
4942
|
);
|
|
4907
4943
|
}
|
|
4908
4944
|
return mapper;
|
|
4909
4945
|
}
|
|
4946
|
+
/**
|
|
4947
|
+
* Reads an attribute value through the mapper it was declared with.
|
|
4948
|
+
* @param {string} attr
|
|
4949
|
+
* @param {string|null} str
|
|
4950
|
+
* @returns {any}
|
|
4951
|
+
* @throws when the class declares no such attribute; read undeclared ones with getAttribute
|
|
4952
|
+
*/
|
|
4910
4953
|
unmarshal(attr, str) {
|
|
4911
4954
|
return this.#mapper(attr).unmarshal(str, attr, this);
|
|
4912
4955
|
}
|
|
4956
|
+
/**
|
|
4957
|
+
* Writes a value as the attribute string its declared mapper produces.
|
|
4958
|
+
* @param {string} attr
|
|
4959
|
+
* @param {any} value
|
|
4960
|
+
* @returns {string|null} null for a value that removes the attribute
|
|
4961
|
+
* @throws when the class declares no such attribute
|
|
4962
|
+
*/
|
|
4913
4963
|
marshal(attr, value) {
|
|
4914
4964
|
return this.#mapper(attr).marshal(value, attr, this);
|
|
4915
4965
|
}
|
|
4916
4966
|
/**
|
|
4917
|
-
*
|
|
4967
|
+
* The registry that defined this element, the page's own for an undefined one.
|
|
4968
|
+
* @returns {typeof registry}
|
|
4918
4969
|
*/
|
|
4919
|
-
/** The registry that defined this element, the page's own for an undefined one. */
|
|
4920
4970
|
get _registry() {
|
|
4921
4971
|
return this.#bits().registry;
|
|
4922
4972
|
}
|
|
@@ -4924,10 +4974,18 @@ class ParsedElement extends HTMLElement {
|
|
|
4924
4974
|
* The component registered under the name on this element's registry, the
|
|
4925
4975
|
* one every ful loader and mapper resolves through.
|
|
4926
4976
|
* @param {string} name
|
|
4977
|
+
* @returns {any} undefined when none is registered
|
|
4927
4978
|
*/
|
|
4928
4979
|
component(name) {
|
|
4929
4980
|
return this.#bits().registry.component(name);
|
|
4930
4981
|
}
|
|
4982
|
+
/**
|
|
4983
|
+
* One of the class's templates, bound to its registry's scope with the
|
|
4984
|
+
* class's `static config` overlaid as `config`.
|
|
4985
|
+
* @param {string} [name] 'default', the `static template`, when omitted
|
|
4986
|
+
* @returns {Template}
|
|
4987
|
+
* @throws when the class declares no template of that name
|
|
4988
|
+
*/
|
|
4931
4989
|
template(name) {
|
|
4932
4990
|
const target = this.#bits().TEMPLATES[name ?? 'default'];
|
|
4933
4991
|
if (!target) {
|
|
@@ -4947,15 +5005,9 @@ class ParsedElement extends HTMLElement {
|
|
|
4947
5005
|
if (oldValue === newValue) {
|
|
4948
5006
|
return;
|
|
4949
5007
|
}
|
|
4950
|
-
//the mute is the attribute being reflected, not the element: a component
|
|
4951
|
-
//that legitimately changes another observed attribute while one reflects
|
|
4952
|
-
//is a change like any other, and used to be swallowed
|
|
4953
5008
|
if (this.#reflecting.has(attr)) {
|
|
4954
5009
|
return;
|
|
4955
5010
|
}
|
|
4956
|
-
//the properties go live only once the render is done: before that, an
|
|
4957
|
-
//attribute write lands in the observed snapshot and the base applies it
|
|
4958
|
-
//with the rest of the declared state
|
|
4959
5011
|
if (!this.#parsed) {
|
|
4960
5012
|
if (this.#pending !== null && attr in this.#pending) {
|
|
4961
5013
|
this.#pending[attr] = this.unmarshal(attr, newValue);
|
|
@@ -4965,14 +5017,18 @@ class ParsedElement extends HTMLElement {
|
|
|
4965
5017
|
this[this.#bits().ATTR_TO_PROPERTY[attr]] = this.unmarshal(attr, newValue);
|
|
4966
5018
|
}
|
|
4967
5019
|
/**
|
|
4968
|
-
* Upgrades once
|
|
4969
|
-
* to writes made while the render is
|
|
4970
|
-
*
|
|
5020
|
+
* Upgrades once, however often the element is connected: reads the declared
|
|
5021
|
+
* attributes, keeps the observed ones open to writes made while the render is
|
|
5022
|
+
* pending, renders, then applies the observed values to their properties in
|
|
5023
|
+
* the order the registry composed them, a base class's before a subclass's.
|
|
5024
|
+
* Only then do attribute writes forward to properties, and the element gains
|
|
5025
|
+
* the `rendered` custom state. A render that throws leaves the forwarding
|
|
5026
|
+
* shut and the state unset.
|
|
4971
5027
|
*
|
|
4972
5028
|
* An observed attribute drives the property the registry's `propertyOf`
|
|
4973
5029
|
* names, so a hyphenated attribute is authored with its dashes and read as
|
|
4974
|
-
* a camelCase property.
|
|
4975
|
-
*
|
|
5030
|
+
* a camelCase property.
|
|
5031
|
+
* @returns {Promise<void>} rejecting with what the render threw
|
|
4976
5032
|
*/
|
|
4977
5033
|
async upgrade() {
|
|
4978
5034
|
if (this.#started) {
|
|
@@ -4980,9 +5036,6 @@ class ParsedElement extends HTMLElement {
|
|
|
4980
5036
|
}
|
|
4981
5037
|
this.#started = true;
|
|
4982
5038
|
const slots = this.#bits().SLOTS ? LightSlots.from(this) : undefined;
|
|
4983
|
-
//both tiers are read here: the observed ones forward to a property once
|
|
4984
|
-
//the render is done, the rest are the element's configuration, read the
|
|
4985
|
-
//one time and never again
|
|
4986
5039
|
const declared = Object.fromEntries(
|
|
4987
5040
|
this.#bits().DECLARED.map((attribute) => [
|
|
4988
5041
|
attribute,
|
|
@@ -4994,21 +5047,11 @@ class ParsedElement extends HTMLElement {
|
|
|
4994
5047
|
this.#pending = declared;
|
|
4995
5048
|
try {
|
|
4996
5049
|
await this.render({ slots });
|
|
4997
|
-
//the declared state reaches the properties once the dom the setters
|
|
4998
|
-
//drive exists, in the order the registry composed the declarations:
|
|
4999
|
-
//a base class's attributes before the subclass's own
|
|
5000
5050
|
const properties = this.#bits().ATTR_TO_PROPERTY;
|
|
5001
5051
|
for (const name of this.#bits().OBSERVED) {
|
|
5002
5052
|
this[properties[name]] = declared[name];
|
|
5003
5053
|
}
|
|
5004
|
-
//the properties go live once the render is done: from here on, an
|
|
5005
|
-
//attribute write forwards to the property. A render that threw
|
|
5006
|
-
//leaves it shut, so a later attribute write cannot reach setters
|
|
5007
|
-
//that assume pieces the failed render never adopted
|
|
5008
5054
|
this.#parsed = true;
|
|
5009
|
-
//the same moment, said to css. :defined is true from the constructor,
|
|
5010
|
-
//which is before the dom exists, so a guard written against it reveals
|
|
5011
|
-
//an element that has nothing in it yet
|
|
5012
5055
|
this.internals.states.add('rendered');
|
|
5013
5056
|
} finally {
|
|
5014
5057
|
this.#pending = null;
|
|
@@ -5019,8 +5062,9 @@ class ParsedElement extends HTMLElement {
|
|
|
5019
5062
|
* drive, and the base applies the declared state onto the properties as
|
|
5020
5063
|
* soon as it returns. A render needing a declared value while it builds
|
|
5021
5064
|
* reads it through `declared(name)`. The slots are undefined for an element
|
|
5022
|
-
* declaring no slots.
|
|
5023
|
-
* @param {{ slots:
|
|
5065
|
+
* declaring no slots. It may be async; the upgrade awaits it.
|
|
5066
|
+
* @param {{ slots: Record<string, DocumentFragment> | undefined }} c
|
|
5067
|
+
* @returns {void | Promise<void>}
|
|
5024
5068
|
*/
|
|
5025
5069
|
render(c) {}
|
|
5026
5070
|
/**
|
|
@@ -5032,12 +5076,9 @@ class ParsedElement extends HTMLElement {
|
|
|
5032
5076
|
* observed name answers the snapshot while the render is pending, kept
|
|
5033
5077
|
* open to attribute writes landing in that window, and the live attribute
|
|
5034
5078
|
* afterwards, the property being live by then.
|
|
5035
|
-
*
|
|
5036
|
-
* The snapshot exists rather than a read of the dom because an element may
|
|
5037
|
-
* write its own observed attributes while it renders, whether a reflection
|
|
5038
|
-
* or a value the platform normalizes on the way in, and what the author
|
|
5039
|
-
* declared is what the base applies, not what the render left behind.
|
|
5040
5079
|
* @param {string} name
|
|
5080
|
+
* @returns {any}
|
|
5081
|
+
* @throws when the class declares no such attribute
|
|
5041
5082
|
*/
|
|
5042
5083
|
declared(name) {
|
|
5043
5084
|
if (name in this.#frozen) {
|
|
@@ -5048,15 +5089,18 @@ class ParsedElement extends HTMLElement {
|
|
|
5048
5089
|
}
|
|
5049
5090
|
return this.unmarshal(name, this.getAttribute(name));
|
|
5050
5091
|
}
|
|
5051
|
-
/**
|
|
5092
|
+
/**
|
|
5093
|
+
* Whether the element's render completed: the moment its properties went
|
|
5094
|
+
* live, which css reads as `:state(rendered)`. Unlike `:defined`, which holds
|
|
5095
|
+
* from the constructor, it is false until the dom exists.
|
|
5096
|
+
* @returns {boolean}
|
|
5097
|
+
*/
|
|
5052
5098
|
get rendered() {
|
|
5053
5099
|
return this.#parsed;
|
|
5054
5100
|
}
|
|
5055
5101
|
/**
|
|
5056
5102
|
* Projects a property back onto its observed attribute, marshalled through
|
|
5057
|
-
* the mapper the attribute was declared with
|
|
5058
|
-
* reaches its attribute, so a setter never has to know how its own type
|
|
5059
|
-
* serializes.
|
|
5103
|
+
* the mapper the attribute was declared with.
|
|
5060
5104
|
*
|
|
5061
5105
|
* A value the attribute already carries is not written at all, so reflecting
|
|
5062
5106
|
* what an attribute write just delivered ends there rather than looping, and
|
|
@@ -5098,7 +5142,10 @@ const POSITIONAL = /\{(\d+)\}/g;
|
|
|
5098
5142
|
/** @param {any} v @returns {v is Record<string, string>} */
|
|
5099
5143
|
const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
5100
5144
|
|
|
5101
|
-
/**
|
|
5145
|
+
/** @param {any} problem @returns {string|undefined} the reason, unless it is missing or blank */
|
|
5146
|
+
const reasonOf = (problem) =>
|
|
5147
|
+
typeof problem?.reason === 'string' && problem.reason.trim() !== '' ? problem.reason : undefined;
|
|
5148
|
+
|
|
5102
5149
|
const warned = new Set();
|
|
5103
5150
|
const warnOnce = (problem) => {
|
|
5104
5151
|
if (!warned.has(problem)) {
|
|
@@ -5109,11 +5156,7 @@ const warnOnce = (problem) => {
|
|
|
5109
5156
|
|
|
5110
5157
|
const formatters = new BoundedCache(100);
|
|
5111
5158
|
/**
|
|
5112
|
-
* Intl
|
|
5113
|
-
* lookups), while formatting on a built instance is near free: a list rendering
|
|
5114
|
-
* a size per row must not rebuild a NumberFormat per cell, so instances are
|
|
5115
|
-
* memoized by constructor, locale and options.
|
|
5116
|
-
*
|
|
5159
|
+
* An Intl instance, memoized by constructor, locale and options.
|
|
5117
5160
|
* @param {{ new (locale: string | undefined, options: any): any }} ctor
|
|
5118
5161
|
* @param {string | undefined} locale
|
|
5119
5162
|
* @param {any} [options]
|
|
@@ -5139,6 +5182,9 @@ class Localization {
|
|
|
5139
5182
|
*
|
|
5140
5183
|
* @param {string} key
|
|
5141
5184
|
* @param {...any} args
|
|
5185
|
+
* Each problem (a missing key, placeholder or plural form) is warned once
|
|
5186
|
+
* per page.
|
|
5187
|
+
*
|
|
5142
5188
|
* @this {Receiver}
|
|
5143
5189
|
* @returns {string} the message, or the key itself when the translations do not carry it
|
|
5144
5190
|
*/
|
|
@@ -5149,9 +5195,9 @@ class Localization {
|
|
|
5149
5195
|
warnOnce(`missing message "${key}"`);
|
|
5150
5196
|
return key;
|
|
5151
5197
|
}
|
|
5198
|
+
const named = args.length === 1 && isPlainObject(args[0]) ? args[0] : null;
|
|
5152
5199
|
if (isPlainObject(message)) {
|
|
5153
|
-
|
|
5154
|
-
if (typeof named.count !== 'number') {
|
|
5200
|
+
if (typeof named?.count !== 'number') {
|
|
5155
5201
|
warnOnce(`plural message "${key}" needs a numeric {count}`);
|
|
5156
5202
|
message = message.other;
|
|
5157
5203
|
} else {
|
|
@@ -5164,8 +5210,7 @@ class Localization {
|
|
|
5164
5210
|
warnOnce(`plural message "${key}" has no "other" form`);
|
|
5165
5211
|
return key;
|
|
5166
5212
|
}
|
|
5167
|
-
if (
|
|
5168
|
-
const named = args[0];
|
|
5213
|
+
if (named) {
|
|
5169
5214
|
return message.replace(PLACEHOLDER, (literal, name) => {
|
|
5170
5215
|
if (!Object.hasOwn(named, name)) {
|
|
5171
5216
|
warnOnce(`message "${key}" wants {${name}}`);
|
|
@@ -5193,6 +5238,7 @@ class Localization {
|
|
|
5193
5238
|
* @param {Date | number} value
|
|
5194
5239
|
* @param {Intl.DateTimeFormatOptions} [options]
|
|
5195
5240
|
* @this {Receiver}
|
|
5241
|
+
* @returns {string}
|
|
5196
5242
|
*/
|
|
5197
5243
|
static date(value, options) {
|
|
5198
5244
|
return formatter(Intl.DateTimeFormat, this.locale, options).format(value);
|
|
@@ -5204,6 +5250,7 @@ class Localization {
|
|
|
5204
5250
|
* @param {number} value
|
|
5205
5251
|
* @param {Intl.NumberFormatOptions} [options]
|
|
5206
5252
|
* @this {Receiver}
|
|
5253
|
+
* @returns {string}
|
|
5207
5254
|
*/
|
|
5208
5255
|
static number(value, options) {
|
|
5209
5256
|
return formatter(Intl.NumberFormat, this.locale, options).format(value);
|
|
@@ -5216,6 +5263,7 @@ class Localization {
|
|
|
5216
5263
|
*
|
|
5217
5264
|
* @param {number} value
|
|
5218
5265
|
* @this {Receiver}
|
|
5266
|
+
* @returns {string}
|
|
5219
5267
|
*/
|
|
5220
5268
|
static bytes(value) {
|
|
5221
5269
|
const format = formatter(Intl.NumberFormat, this.locale, { maximumFractionDigits: 2 }).format;
|
|
@@ -5231,16 +5279,37 @@ class Localization {
|
|
|
5231
5279
|
return `${format(value)}B`;
|
|
5232
5280
|
}
|
|
5233
5281
|
|
|
5282
|
+
/**
|
|
5283
|
+
* What went wrong, as text for the reader. A value carrying a non-empty
|
|
5284
|
+
* `problems` array reads as their reasons, one per line, a blank or missing
|
|
5285
|
+
* reason reading as the `failure.no-reason` message. Any other value with a
|
|
5286
|
+
* `message` reads as that message; anything else as its string form, and
|
|
5287
|
+
* null or undefined as the empty string.
|
|
5288
|
+
*
|
|
5289
|
+
* @param {any} value what was thrown or rejected with
|
|
5290
|
+
* @this {Receiver}
|
|
5291
|
+
* @returns {string}
|
|
5292
|
+
*/
|
|
5293
|
+
static failure(value) {
|
|
5294
|
+
if (Array.isArray(value?.problems) && value.problems.length > 0) {
|
|
5295
|
+
return value.problems.map((p) => reasonOf(p) ?? Localization.t.call(this, 'failure.no-reason')).join('\n');
|
|
5296
|
+
}
|
|
5297
|
+
if (value?.message !== undefined) {
|
|
5298
|
+
return String(value.message);
|
|
5299
|
+
}
|
|
5300
|
+
return value == null ? '' : String(value);
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5234
5303
|
/**
|
|
5235
5304
|
* An imperative facade over the module functions, resolving the translations
|
|
5236
|
-
* and the locale from the registry overlays on every call
|
|
5305
|
+
* and the locale from the registry overlays on every call, the same lookup a
|
|
5306
|
+
* template makes: a facade taken at module scope, before the plugin
|
|
5307
|
+
* configures, still sees the translations.
|
|
5237
5308
|
*
|
|
5238
5309
|
* @param {{ locale?: string }} [overrides] an explicit locale, winning over the registry one
|
|
5310
|
+
* @returns {{ t: (key: string, ...args: any[]) => string, date: (value: Date | number, options?: Intl.DateTimeFormatOptions) => string, number: (value: number, options?: Intl.NumberFormatOptions) => string, bytes: (value: number) => string, failure: (value: any) => string }}
|
|
5239
5311
|
*/
|
|
5240
5312
|
static of(overrides = {}) {
|
|
5241
|
-
//resolved per call, not per facade: a module-scope `Localization.of()` is
|
|
5242
|
-
//bound before the plugin configures. The registry's own evaluator does the
|
|
5243
|
-
//lookup, so this cannot drift from what a template sees
|
|
5244
5313
|
const resolve = (prop) => registry.evaluator().resolve(prop);
|
|
5245
5314
|
/** @param {any} fn */
|
|
5246
5315
|
const bind =
|
|
@@ -5252,6 +5321,7 @@ class Localization {
|
|
|
5252
5321
|
date: bind(Localization.date),
|
|
5253
5322
|
number: bind(Localization.number),
|
|
5254
5323
|
bytes: bind(Localization.bytes),
|
|
5324
|
+
failure: bind(Localization.failure),
|
|
5255
5325
|
};
|
|
5256
5326
|
}
|
|
5257
5327
|
}
|