@optionfactory/fml 9.0.0-rc30 → 9.0.0-rc32

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/dist/ftl.mjs CHANGED
@@ -2847,6 +2847,8 @@ const resolveInStack = (dataStack, prop) => {
2847
2847
  return undefined;
2848
2848
  };
2849
2849
 
2850
+ const BOUND = Symbol('BOUND');
2851
+
2850
2852
  /**
2851
2853
  * Evaluates a parsed expression against a scope. One instance per evaluation:
2852
2854
  * it holds the modules and the data stack that names resolve against, and keeps
@@ -2855,9 +2857,11 @@ const resolveInStack = (dataStack, prop) => {
2855
2857
  class EvaluatingVisitor {
2856
2858
  #modules;
2857
2859
  #dataStack;
2858
- constructor(modules, dataStack) {
2860
+ #bound;
2861
+ constructor(modules, dataStack, bound) {
2859
2862
  this.#modules = modules;
2860
2863
  this.#dataStack = dataStack;
2864
+ this.#bound = bound ?? dataStack.length;
2861
2865
  }
2862
2866
  #resolve(prop) {
2863
2867
  return resolveInStack(this.#dataStack, prop);
@@ -2881,8 +2885,9 @@ class EvaluatingVisitor {
2881
2885
  #cached_resolve_proxy;
2882
2886
  #resolve_proxy() {
2883
2887
  if (!this.#cached_resolve_proxy) {
2888
+ const bound = (name) => resolveInStack(this.#dataStack.slice(0, this.#bound), name);
2884
2889
  this.#cached_resolve_proxy = new Proxy(this.#dataStack, {
2885
- get: (target, prop) => this.#resolve(prop),
2890
+ get: (target, prop) => (prop === BOUND ? bound : this.#resolve(prop)),
2886
2891
  });
2887
2892
  }
2888
2893
  return this.#cached_resolve_proxy;
@@ -3035,6 +3040,16 @@ class Expressions {
3035
3040
  * `{{{ }}}` (html) or `{{{{ }}}}` (node) interpolation, in order.
3036
3041
  */
3037
3042
  static MODE_TEMPLATED = Symbol('MODE_TEMPLATED');
3043
+ /**
3044
+ * The key under which the receiver of a module function answers a lookup
3045
+ * against the bound data alone: `this[Expressions.BOUND](name)` resolves
3046
+ * the name in the layers the scope was created with (a registry's data,
3047
+ * the data handed to a Template factory) and never in an overlay pushed
3048
+ * afterwards, by `data-tpl-each`, `data-tpl-with` or `withOverlay`. It is
3049
+ * how a module reads what the page configured without the rendered data
3050
+ * being able to supply or replace it. No name on the data stack reaches it.
3051
+ */
3052
+ static BOUND = BOUND;
3038
3053
 
3039
3054
  static #astCache = new BoundedCache(1000);
3040
3055
 
@@ -3059,10 +3074,11 @@ class Expressions {
3059
3074
  * @param {any[]} dataStack innermost overlay last
3060
3075
  * @param {any} ast what `parse` answered
3061
3076
  * @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] the mode it was parsed in
3077
+ * @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
3062
3078
  * @returns {any} the value, or the list of parts in MODE_TEMPLATED
3063
3079
  */
3064
- static evaluate(modules, dataStack, ast, mode) {
3065
- return new EvaluatingVisitor(modules, dataStack).visitRoot(ast, mode === Expressions.MODE_TEMPLATED);
3080
+ static evaluate(modules, dataStack, ast, mode, bound) {
3081
+ return new EvaluatingVisitor(modules, dataStack, bound).visitRoot(ast, mode === Expressions.MODE_TEMPLATED);
3066
3082
  }
3067
3083
  /**
3068
3084
  * Parses and evaluates an expression.
@@ -3070,10 +3086,11 @@ class Expressions {
3070
3086
  * @param {any[]} dataStack innermost overlay last
3071
3087
  * @param {string} expression
3072
3088
  * @param {(typeof Expressions.MODE_EXPRESSION | typeof Expressions.MODE_TEMPLATED)?} [mode] MODE_EXPRESSION when omitted
3089
+ * @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
3073
3090
  * @returns {any} the value, or the list of parts in MODE_TEMPLATED
3074
3091
  */
3075
- static interpret(modules, dataStack, expression, mode) {
3076
- return Expressions.evaluate(modules, dataStack, Expressions.parse(expression, mode), mode);
3092
+ static interpret(modules, dataStack, expression, mode, bound) {
3093
+ return Expressions.evaluate(modules, dataStack, Expressions.parse(expression, mode), mode, bound);
3077
3094
  }
3078
3095
  }
3079
3096
 
@@ -3082,17 +3099,24 @@ class Expressions {
3082
3099
  * that a bare identifier resolves against, as a single value. Adding an overlay
3083
3100
  * returns a new evaluator instead of modifying this one, so a template can add
3084
3101
  * data for one subtree without affecting the rest of the render.
3102
+ *
3103
+ * The layers an evaluator is created with are its bound data; the overlays
3104
+ * pushed afterwards are not. A bare identifier resolves against both, while a
3105
+ * module function can ask for the bound data alone through `Expressions.BOUND`.
3085
3106
  */
3086
3107
  class ExpressionEvaluator {
3087
3108
  #modules;
3088
3109
  #dataStack;
3110
+ #bound;
3089
3111
  /**
3090
3112
  * @param {{ [k: string]: any } | null | undefined} modules
3091
3113
  * @param {any[]} dataStack innermost overlay last
3114
+ * @param {number} [bound] how many layers, from the outermost, are the bound data; all of them when omitted
3092
3115
  */
3093
- constructor(modules, dataStack) {
3116
+ constructor(modules, dataStack, bound) {
3094
3117
  this.#modules = modules;
3095
3118
  this.#dataStack = dataStack;
3119
+ this.#bound = bound ?? dataStack.length;
3096
3120
  }
3097
3121
  /**
3098
3122
  * A new evaluator with one more module: `#name:fn()` resolves into it, and an
@@ -3103,11 +3127,11 @@ class ExpressionEvaluator {
3103
3127
  */
3104
3128
  withModule(name, value) {
3105
3129
  const module = name ? { [name]: value } : value;
3106
- return new ExpressionEvaluator({ ...this.#modules, ...module }, this.#dataStack);
3130
+ return new ExpressionEvaluator({ ...this.#modules, ...module }, this.#dataStack, this.#bound);
3107
3131
  }
3108
3132
  /**
3109
3133
  * A new evaluator with the data pushed as the innermost overlays, the last
3110
- * one winning a shared name.
3134
+ * one winning a shared name. The bound data stays what it was.
3111
3135
  * @param {...any} data
3112
3136
  * @returns {ExpressionEvaluator}
3113
3137
  */
@@ -3115,6 +3139,7 @@ class ExpressionEvaluator {
3115
3139
  return new ExpressionEvaluator(
3116
3140
  this.#modules,
3117
3141
  data.length === 0 ? this.#dataStack : [...this.#dataStack, ...data],
3142
+ this.#bound,
3118
3143
  );
3119
3144
  }
3120
3145
  /**
@@ -3132,7 +3157,13 @@ class ExpressionEvaluator {
3132
3157
  * @returns {any}
3133
3158
  */
3134
3159
  evaluateExpression(expression) {
3135
- return Expressions.interpret(this.#modules, this.#dataStack, expression, Expressions.MODE_EXPRESSION);
3160
+ return Expressions.interpret(
3161
+ this.#modules,
3162
+ this.#dataStack,
3163
+ expression,
3164
+ Expressions.MODE_EXPRESSION,
3165
+ this.#bound,
3166
+ );
3136
3167
  }
3137
3168
  /**
3138
3169
  * Evaluates a templated text, the `{{ }}` form, against this scope.
@@ -3140,7 +3171,7 @@ class ExpressionEvaluator {
3140
3171
  * @returns {{ type: symbol, value: any }[]} its parts, as MODE_TEMPLATED describes
3141
3172
  */
3142
3173
  evaluateTemplated(text) {
3143
- return Expressions.interpret(this.#modules, this.#dataStack, text, Expressions.MODE_TEMPLATED);
3174
+ return Expressions.interpret(this.#modules, this.#dataStack, text, Expressions.MODE_TEMPLATED, this.#bound);
3144
3175
  }
3145
3176
  }
3146
3177
 
@@ -4036,14 +4067,22 @@ class UpgradeQueue {
4036
4067
  #ready = new Promise((resolve) => {
4037
4068
  this.#readyResolve = resolve;
4038
4069
  });
4070
+ #configured = /** @type {PromiseWithResolvers<void>} */ (Promise.withResolvers());
4039
4071
  constructor() {
4040
- Nodes.waitDomContentLoaded(document).then(() => {
4072
+ Promise.all([Nodes.waitDomContentLoaded(document), this.#configured.promise]).then(() => {
4041
4073
  this.#start();
4042
4074
  });
4043
4075
  }
4044
4076
  ready() {
4045
4077
  return this.#ready;
4046
4078
  }
4079
+ /**
4080
+ * Marks the registry as configured: until then nothing is defined, so an
4081
+ * empty queue says nothing about the page and readiness is held back.
4082
+ */
4083
+ configured() {
4084
+ this.#configured.resolve();
4085
+ }
4047
4086
  async #start() {
4048
4087
  await this.settled();
4049
4088
  document.dispatchEvent(
@@ -4086,7 +4125,10 @@ class UpgradeQueue {
4086
4125
  * @returns {Promise<void>}
4087
4126
  */
4088
4127
  settled() {
4089
- return this.#drain(() => true, (entry) => entry.finished);
4128
+ return this.#drain(
4129
+ () => true,
4130
+ (entry) => entry.finished,
4131
+ );
4090
4132
  }
4091
4133
  /**
4092
4134
  * Waits for the accepted upgrades, the ones enqueued meanwhile included,
@@ -4340,7 +4382,9 @@ class Registry {
4340
4382
  }
4341
4383
  /**
4342
4384
  * Defines every element deferred so far; from then on, defineElement takes
4343
- * effect immediately.
4385
+ * effect immediately. Readiness waits for the first call: a page that
4386
+ * configures late (translations fetched first) gets its ftl:ready once the
4387
+ * elements this defines have upgraded, not at DOMContentLoaded.
4344
4388
  * @returns {Registry} this registry
4345
4389
  */
4346
4390
  configure() {
@@ -4349,6 +4393,7 @@ class Registry {
4349
4393
  delete this.#tagToClass[tag];
4350
4394
  }
4351
4395
  this.#configured = true;
4396
+ this.#upgradeQueue.configured();
4352
4397
  return this;
4353
4398
  }
4354
4399
  /**
@@ -4380,9 +4425,11 @@ class Registry {
4380
4425
  /**
4381
4426
  * Waits for this registry's readiness: resolves right after its ftl:ready
4382
4427
  * event is dispatched on the document, and immediately when that already
4383
- * happened. Never rejects: a failed upgrade counts as settled, and its
4384
- * rejection is left unhandled so it still reaches the console and the
4385
- * page's error reporting.
4428
+ * happened. The event is dispatched once DOMContentLoaded has passed,
4429
+ * configure() has been called and the queue has drained, so a registry
4430
+ * that is never configured is never ready. Never rejects: a failed
4431
+ * upgrade counts as settled, and its rejection is left unhandled so it
4432
+ * still reaches the console and the page's error reporting.
4386
4433
  * @returns {Promise<void>}
4387
4434
  */
4388
4435
  ready() {
@@ -4726,12 +4773,27 @@ class ParsedElement extends HTMLElement {
4726
4773
  */
4727
4774
 
4728
4775
  /**
4729
- * The receiver contract of the module functions: bound to a proxy over the
4730
- * data stack when called from a template, or to a plain object by Localization.of().
4776
+ * The receiver contract of the module functions: a plain object carrying the
4777
+ * translations and the locale, as Localization.of() binds them, or the proxy
4778
+ * over the data stack a template calls them with.
4731
4779
  *
4732
4780
  * @typedef {{ l10n?: Messages, locale?: string }} Receiver
4733
4781
  */
4734
4782
 
4783
+ /**
4784
+ * The translations or the locale of a receiver. From a template they are read
4785
+ * from the bound data alone (the registry's overlays, or the data handed to
4786
+ * the Template factory): the rows and responses a render pushes on top are
4787
+ * what arrives later, and must not supply or replace the page's own messages.
4788
+ * @param {any} receiver
4789
+ * @param {'l10n' | 'locale'} name
4790
+ * @returns {any}
4791
+ */
4792
+ const configured = (receiver, name) => {
4793
+ const bound = receiver[Expressions.BOUND];
4794
+ return typeof bound === 'function' ? bound(name) : receiver[name];
4795
+ };
4796
+
4735
4797
  const PLACEHOLDER = /\{(\w+)\}/g;
4736
4798
  const POSITIONAL = /\{(\d+)\}/g;
4737
4799
  /** @param {any} v @returns {v is Record<string, string>} */
@@ -4766,7 +4828,9 @@ const formatter = (ctor, locale, options) => {
4766
4828
  * The translations as a template module: `#l10n:t()` for messages, plus date,
4767
4829
  * number and bytes formatting in the page's locale. The template form and the
4768
4830
  * imperative `of()` facade both resolve through the registry's scope, so they
4769
- * always produce the same result for the same key.
4831
+ * always produce the same result for the same key. A template reads `l10n`
4832
+ * and `locale` from its bound data only, the most recent layer winning: an
4833
+ * overlay pushed while rendering (a row, a response) never reaches them.
4770
4834
  */
4771
4835
  class Localization {
4772
4836
  /**
@@ -4785,7 +4849,7 @@ class Localization {
4785
4849
  * @returns {string} the message, or the key itself when the translations do not carry it
4786
4850
  */
4787
4851
  static t(key, ...args) {
4788
- const messages = this.l10n ?? {};
4852
+ const messages = configured(this, 'l10n') ?? {};
4789
4853
  let message = Object.hasOwn(messages, key) ? messages[key] : undefined;
4790
4854
  if (message === undefined) {
4791
4855
  warnOnce(`missing message "${key}"`);
@@ -4797,7 +4861,7 @@ class Localization {
4797
4861
  warnOnce(`plural message "${key}" needs a numeric {count}`);
4798
4862
  message = message.other;
4799
4863
  } else {
4800
- const locale = this.locale ?? navigator?.language ?? 'en';
4864
+ const locale = configured(this, 'locale') ?? navigator?.language ?? 'en';
4801
4865
  const category = formatter(Intl.PluralRules, locale).select(named.count);
4802
4866
  message = message[category] ?? message.other;
4803
4867
  }
@@ -4837,7 +4901,7 @@ class Localization {
4837
4901
  * @returns {string}
4838
4902
  */
4839
4903
  static date(value, options) {
4840
- return formatter(Intl.DateTimeFormat, this.locale, options).format(value);
4904
+ return formatter(Intl.DateTimeFormat, configured(this, 'locale'), options).format(value);
4841
4905
  }
4842
4906
 
4843
4907
  /**
@@ -4849,7 +4913,7 @@ class Localization {
4849
4913
  * @returns {string}
4850
4914
  */
4851
4915
  static number(value, options) {
4852
- return formatter(Intl.NumberFormat, this.locale, options).format(value);
4916
+ return formatter(Intl.NumberFormat, configured(this, 'locale'), options).format(value);
4853
4917
  }
4854
4918
 
4855
4919
  /**
@@ -4862,7 +4926,7 @@ class Localization {
4862
4926
  * @returns {string}
4863
4927
  */
4864
4928
  static bytes(value) {
4865
- const format = formatter(Intl.NumberFormat, this.locale, { maximumFractionDigits: 2 }).format;
4929
+ const format = formatter(Intl.NumberFormat, configured(this, 'locale'), { maximumFractionDigits: 2 }).format;
4866
4930
  if (value >= 1024 * 1024 * 1024) {
4867
4931
  return `${format(value / 1024 / 1024 / 1024)}GiB`;
4868
4932
  }