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