@optionfactory/fml 9.0.0-rc2 → 9.0.0-rc20

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.
Files changed (44) hide show
  1. package/README.md +16 -89
  2. package/dist/client-errors.iife.js +23 -37
  3. package/dist/client-errors.iife.js.map +1 -1
  4. package/dist/client-errors.iife.min.js +1 -1
  5. package/dist/client-errors.iife.min.js.map +1 -1
  6. package/dist/custom-elements.json +459 -36
  7. package/dist/fml.css +24 -19
  8. package/dist/fml.css.map +1 -1
  9. package/dist/fml.iife.js +4646 -1779
  10. package/dist/fml.iife.js.map +1 -1
  11. package/dist/fml.iife.min.js +1 -1
  12. package/dist/fml.iife.min.js.map +1 -1
  13. package/dist/ftl.d.mts +395 -224
  14. package/dist/ftl.iife.js +402 -332
  15. package/dist/ftl.iife.js.map +1 -1
  16. package/dist/ftl.iife.min.js +1 -1
  17. package/dist/ftl.iife.min.js.map +1 -1
  18. package/dist/ftl.min.mjs +1 -1
  19. package/dist/ftl.min.mjs.map +1 -1
  20. package/dist/ftl.mjs +402 -332
  21. package/dist/ftl.mjs.map +1 -1
  22. package/dist/ful.css +24 -19
  23. package/dist/ful.css.map +1 -1
  24. package/dist/ful.d.mts +2801 -519
  25. package/dist/ful.iife.js +4096 -1275
  26. package/dist/ful.iife.js.map +1 -1
  27. package/dist/ful.iife.min.js +1 -1
  28. package/dist/ful.iife.min.js.map +1 -1
  29. package/dist/ful.min.mjs +1 -1
  30. package/dist/ful.min.mjs.map +1 -1
  31. package/dist/ful.mjs +4093 -1277
  32. package/dist/ful.mjs.map +1 -1
  33. package/dist/httpc.d.mts +139 -121
  34. package/dist/httpc.iife.js +137 -166
  35. package/dist/httpc.iife.js.map +1 -1
  36. package/dist/httpc.iife.min.js +1 -1
  37. package/dist/httpc.iife.min.js.map +1 -1
  38. package/dist/httpc.min.mjs +1 -1
  39. package/dist/httpc.min.mjs.map +1 -1
  40. package/dist/httpc.mjs +137 -166
  41. package/dist/httpc.mjs.map +1 -1
  42. package/dist/vscode.html-custom-data.json +215 -27
  43. package/dist/web-types.json +463 -36
  44. 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 in insertion order: not an LRU (a hit
6
- * does not refresh an entry), just a cap keeping unbounded key spaces (parsed
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. Every lookup that resolves a name goes through this, so the
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
- const lhs = this.visit(node.lhs);
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
- const cond = this.visit(node.cond);
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 an expression.
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 result
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 result
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
- /** Evaluates an expression against this scope. */
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
- /** Evaluates a templated text, the `{{ }}` form, against this scope. */
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 the DocumentFragments a Template renders into. */
3566
+ /** Creates, serializes and inspects DocumentFragments. */
3543
3567
  class Fragments {
3544
3568
  /**
3545
- * Creates a DocumentFragment from an string.
3546
- * @param {...string} html
3547
- * @returns {DocumentFragment} the fragment
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
- * Creates a string representation (HTML) of a DocumentFragment, consuming it:
3556
- * the nodes are moved out, not copied, and the fragment is left empty. Pass
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} the html
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
- * Checks if a fragment contains only blank text nodes
3592
+ * Whether a fragment holds no element and no text but whitespace.
3568
3593
  * @param {DocumentFragment} fragment
3569
- * @returns {boolean} true if the fragment is blank
3594
+ * @returns {boolean}
3570
3595
  */
3571
3596
  static isBlank(fragment) {
3572
3597
  return fragment.childElementCount === 0 && fragment.textContent.trim() === '';
3573
3598
  }
3574
3599
  /**
3575
- * Creates a DocumentFragment from nodes.
3576
- * @param {...Node} nodes
3577
- * @returns {DocumentFragment} the fragment
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
- * Creates a DocumentFragment from childNodes of an element.
3612
+ * Moves the children of a node into a new DocumentFragment, leaving the
3613
+ * node empty.
3588
3614
  * @param {Node} el
3589
- * @returns {DocumentFragment} the fragment
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 reads and writes where a nullish value removes the attribute instead of setting it to the string 'null'. */
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
- * Creates a unique id with the given prefix.
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 if not present.
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
- * Forwards prefixed attributes from an element to another (removing the prefix).
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 the value of an attribute. nullish values remove the attribute.
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
- * Extracts light slots from an element. For non default slots in a template tag, the content is extracted.
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 for the parser and for the document, for elements that upgrade before their own markup is complete. */
3748
+ /** Waits on the parser and the document, and queries an element's own children. */
3714
3749
  class Nodes {
3715
3750
  /**
3716
- * Checks if an element is already parsed.
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, resolving immediately when
3731
- * that moment already passed. 'interactive' alone cannot tell a document
3732
- * still waiting for deferred scripts (the event comes, however late) from
3733
- * a module imported past it (the event is gone): the two events order
3734
- * themselves, DOMContentLoaded always precedes load, so racing the two
3735
- * resolves with DCL whenever it is still coming and with load otherwise,
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
- * Every ancestor is watched, not only the parent, because that is what the
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
- * Returns the first child of the element element (if exists) matching the selector.
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
- * Returns all children of the element matching the selector.
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
- //the order is the semantics: each command sees the node as the ones before
3880
- //it left it, so tplIf gates before tplWith/tplEach push their overlay and
3881
- //tplWhen gates after (inside the sub-render, where the overlay is in scope)
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
- //same body as tplWhen: the two differ only in ORDERED_COMMANDS position,
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 keyed =
3920
- evaluated instanceof Map
3921
- ? [...evaluated]
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
- if (!statName) {
3930
- for (const v of entries) {
3931
- ops.prepend(node, template.withOverlay(varName ? { [varName]: v } : v).render());
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
- ops.prepend(
3961
- node,
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
- //same body as tplIf: the two differ only in ORDERED_COMMANDS position,
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 = new DocumentFragment();
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
- * Converts a tpl camelCase dataset key into a kebab-case attribute name.
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 a string.
4081
+ * Creates a template from html.
4079
4082
  * @param {string} html
4080
4083
  * @param {{ [k: string] : any }?} [modules]
4081
- * @param {...*} data
4082
- * @returns the template
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 template element matching the selector.
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 the template
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
- const fragment = document.adoptNode(templateEl.content);
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 an HTMLTemplateElement.
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 the template
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 from a DocumentFragment.
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 the template
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 replacing the fragment.
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 new module added.
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 new a data overlay added to the stack.
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 the evaluated expression result
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 the text evaluates to
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
- * Returns an expression evaluator with bound modules and dataStack.
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
- * @returns a DocumentFragment
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-${toAttr(dataSetKey)}="${expression}"`, el, ex);
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 on the Element (replacing children).
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 appending the resulting fragment to the Element.
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 on the first Element matching the selector (replacing children), if exists.
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 appending the resulting fragment to the Element matching the selector, if exists.
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. Each frame names the node it
4324
- * failed on and what was being evaluated there, so the chain reads as the path
4325
- * from the template's root down to the offending expression. The frame carries
4326
- * the node's identification only, an open tag rather than its whole subtree:
4327
- * the markup is serialized on demand through `html`, and the live node stays on
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
- /** true when the budget dropped the outer frames of this chain */
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 spent its budget: then the
4345
- * cause travels on, marked so a report can say the outer context was
4346
- * dropped.
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
- /** How many frames this chain carries, this one included. */
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
- /** The node the render failed on, live: it keeps its place in the fragment being built. */
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
- /** The node's markup, serialized when asked for rather than on every failure. */
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
- const t = document.createElement('template');
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. Never rejects: a component that fails
4492
- * does not hold the others back, and readiness means the queue drained rather
4493
- * than that everything worked.
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
- /** Waits for the accepted upgrades, rejecting with the first that failed. */
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, and Rendering and the ftl:ready machinery wait on the
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
- * The walk stops where the platform's own class hierarchy begins: no earlier
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`. A single-word name is returned
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: the rejecting barrier Rendering is a facade over.
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
- /** The pending upgrade of one element, undefined when it is not queued. */
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
- /** The elements whose upgrade is still pending, in queue order. */
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 the page's readiness: the same moment the ftl:ready event is
4788
- * dispatched at, resolving immediately when that moment already passed.
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
- /** The component registered under the name, undefined when none is. */
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
- * Awaitable rendering barriers over the registry's upgrade queue. These are
4832
- * the rejecting waits: a failed upgrade among the awaited components rejects
4833
- * the wait, where registry.ready() only ever means the queue drained and
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 platform's window into the element's own state. The base attaches it
4880
- * for every element, so a subclass never calls attachInternals itself: the
4881
- * platform allows it once, and a second call throws. Form association is a
4882
- * property of the definition rather than of who attached, so a subclass
4883
- * declaring `static formAssociated` still gets the form apis here.
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
- * @param {string} [name] - The name of the template target, defaults to 'default'
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: reads the declared attributes, keeps the observed half open
4972
- * to writes made while the render is pending, applies them to the properties
4973
- * when the render returns, and only then lets an attribute write forward.
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. A single-word attribute is its own property name,
4978
- * which is what every observed attribute in the library is.
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: any }} c
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
- /** Whether the element's render completed: the moment its properties went live. */
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: the one place a property
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
- /** every l10n problem is reported once per session: a missing key in a row template must not flood the console */
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 construction is the expensive part of formatting (locale parsing, CLDR
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,19 +5185,22 @@ 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
  */
5148
5194
  static t(key, ...args) {
5149
5195
  const messages = this.l10n ?? {};
5150
- let message = messages[key];
5196
+ let message = Object.hasOwn(messages, key) ? messages[key] : undefined;
5151
5197
  if (message === undefined) {
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
- const named = args.length === 1 && isPlainObject(args[0]) ? args[0] : {};
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,10 +5213,9 @@ var ftl = (function (exports) {
5167
5213
  warnOnce(`plural message "${key}" has no "other" form`);
5168
5214
  return key;
5169
5215
  }
5170
- if (args.length === 1 && isPlainObject(args[0])) {
5171
- const named = args[0];
5216
+ if (named) {
5172
5217
  return message.replace(PLACEHOLDER, (literal, name) => {
5173
- if (!(name in named)) {
5218
+ if (!Object.hasOwn(named, name)) {
5174
5219
  warnOnce(`message "${key}" wants {${name}}`);
5175
5220
  return literal;
5176
5221
  }
@@ -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
  }