@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/custom-elements.json +15 -1
- package/dist/fml.css +6 -6
- package/dist/fml.css.map +1 -1
- package/dist/fml.iife.js +910 -105
- package/dist/fml.iife.js.map +1 -1
- package/dist/fml.iife.min.js +1 -1
- package/dist/fml.iife.min.js.map +1 -1
- package/dist/ftl.d.mts +32 -9
- package/dist/ftl.iife.js +89 -25
- package/dist/ftl.iife.js.map +1 -1
- package/dist/ftl.iife.min.js +1 -1
- package/dist/ftl.iife.min.js.map +1 -1
- package/dist/ftl.min.mjs +1 -1
- package/dist/ftl.min.mjs.map +1 -1
- package/dist/ftl.mjs +89 -25
- package/dist/ftl.mjs.map +1 -1
- package/dist/ful.css +6 -6
- package/dist/ful.css.map +1 -1
- package/dist/ful.d.mts +125 -62
- package/dist/ful.iife.js +821 -79
- package/dist/ful.iife.js.map +1 -1
- package/dist/ful.iife.min.js +1 -1
- package/dist/ful.iife.min.js.map +1 -1
- package/dist/ful.min.mjs +1 -1
- package/dist/ful.min.mjs.map +1 -1
- package/dist/ful.mjs +823 -80
- package/dist/ful.mjs.map +1 -1
- package/dist/vscode.html-custom-data.json +9 -1
- package/dist/web-types.json +18 -2
- package/package.json +2 -2
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
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:
|
|
4730
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|