@pie-players/pie-players-shared 1.0.0-beta.3 → 1.0.0-beta.5
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/i18n/catalogs.d.ts +5 -0
- package/dist/i18n/catalogs.js +9 -0
- package/dist/i18n/content-language.d.ts +35 -0
- package/dist/i18n/content-language.js +51 -0
- package/dist/i18n/index.d.ts +6 -2
- package/dist/i18n/index.js +5 -2
- package/dist/i18n/language-tags.d.ts +9 -0
- package/dist/i18n/language-tags.js +21 -0
- package/dist/i18n/locale-load-error.d.ts +34 -0
- package/dist/i18n/locale-load-error.js +29 -0
- package/dist/i18n/messages/en-US.d.ts +124 -3
- package/dist/i18n/messages/en-US.js +139 -3
- package/dist/i18n/messages/nl-NL.d.ts +109 -2
- package/dist/i18n/messages/nl-NL.js +115 -8
- package/dist/i18n/provider.d.ts +25 -2
- package/dist/i18n/provider.js +79 -32
- package/dist/i18n/pseudo.d.ts +34 -0
- package/dist/i18n/pseudo.js +106 -0
- package/dist/loaders/esm-adapter.d.ts +10 -0
- package/dist/loaders/esm-adapter.js +88 -15
- package/dist/loaders/index.d.ts +1 -1
- package/dist/loaders/index.js +1 -1
- package/dist/object/index.d.ts +4 -1
- package/dist/object/index.js +8 -1
- package/dist/pie/index.d.ts +1 -0
- package/dist/pie/math-rendering.d.ts +13 -1
- package/dist/pie/math-rendering.js +54 -1
- package/dist/types/index.d.ts +12 -0
- package/dist/ui/focus-trap.d.ts +12 -0
- package/dist/ui/focus-trap.js +39 -1
- package/dist/ui/overlay-containment.d.ts +13 -0
- package/dist/ui/overlay-containment.js +23 -0
- package/dist/ui/overlay-placement.d.ts +28 -0
- package/dist/ui/overlay-placement.js +17 -2
- package/package.json +5 -5
package/dist/i18n/provider.d.ts
CHANGED
|
@@ -57,6 +57,8 @@ export declare class SimpleI18n implements I18nServiceApi {
|
|
|
57
57
|
private catalogs;
|
|
58
58
|
private customMessages;
|
|
59
59
|
private loading;
|
|
60
|
+
/** Counts `setLocale` calls, so a slow load cannot overwrite a later request. */
|
|
61
|
+
private requests;
|
|
60
62
|
private readonly listeners;
|
|
61
63
|
private config;
|
|
62
64
|
constructor(config?: I18nConfig);
|
|
@@ -75,7 +77,9 @@ export declare class SimpleI18n implements I18nServiceApi {
|
|
|
75
77
|
* Resolves the request against the catalogs this provider can serve, loads
|
|
76
78
|
* the winner if it is not resident, then notifies subscribers — in that
|
|
77
79
|
* order, so a component's reactive read re-runs against a catalog that is
|
|
78
|
-
* already in place rather than against the previous locale.
|
|
80
|
+
* already in place rather than against the previous locale. The last call
|
|
81
|
+
* wins: a call whose load finishes after a later call's leaves the locale
|
|
82
|
+
* to that later call.
|
|
79
83
|
*/
|
|
80
84
|
setLocale(locale: LocaleCode): Promise<void>;
|
|
81
85
|
t(key: MessageKeyInput, values?: InterpolationValues): string;
|
|
@@ -169,6 +173,25 @@ export declare function getDefaultI18n(): I18nServiceApi;
|
|
|
169
173
|
* default rather than nothing, which is the graceful default the pull pattern
|
|
170
174
|
* requires.
|
|
171
175
|
*/
|
|
176
|
+
/** The rendering surface `resolveInterfaceI18n` promises. */
|
|
177
|
+
export interface ResolvedInterfaceI18n extends I18nProvider {
|
|
178
|
+
/** Locale-aware number formatting, at the resolved locale. */
|
|
179
|
+
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
180
|
+
/** Locale-aware date formatting, at the resolved locale. */
|
|
181
|
+
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string;
|
|
182
|
+
}
|
|
172
183
|
export declare function resolveInterfaceI18n(source: {
|
|
173
184
|
i18n?: I18nProvider;
|
|
174
|
-
} | null | undefined):
|
|
185
|
+
} | null | undefined): ResolvedInterfaceI18n;
|
|
186
|
+
/**
|
|
187
|
+
* Stamp the interface locale's `lang` and `dir` on a chrome element.
|
|
188
|
+
*
|
|
189
|
+
* The imperative form of the `lang={…} dir={…}` pair a template puts on its
|
|
190
|
+
* root, for the elements a template cannot reach: a shadow host whose chrome
|
|
191
|
+
* is slotted light DOM, a shell built with `document.createElement`, a vanilla
|
|
192
|
+
* element stamping itself. Call it from whatever already re-runs on a locale
|
|
193
|
+
* change and on a catalog load: `getLocale()` moves only once the requested
|
|
194
|
+
* catalog is resident, so a stamp taken at setup alone names the locale that
|
|
195
|
+
* rendered before the load.
|
|
196
|
+
*/
|
|
197
|
+
export declare function stampInterfaceLocale(target: Element, provider: Pick<I18nProvider, "getLocale" | "getDirection">): void;
|
package/dist/i18n/provider.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module @pie-players/pie-players-shared/i18n/provider
|
|
11
11
|
*/
|
|
12
|
-
import { findBestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
12
|
+
import { canonicalLanguageTag, findBestLanguageMatch, normalizeLanguageTag, toLanguageTag, } from "./language-tags.js";
|
|
13
13
|
import enUS from "./messages/en-US.js";
|
|
14
14
|
/** The locale every fallback chain terminates at. */
|
|
15
15
|
export const DEFAULT_LOCALE = "en-US";
|
|
@@ -114,11 +114,14 @@ export class SimpleI18n {
|
|
|
114
114
|
catalogs;
|
|
115
115
|
customMessages;
|
|
116
116
|
loading;
|
|
117
|
+
/** Counts `setLocale` calls, so a slow load cannot overwrite a later request. */
|
|
118
|
+
requests = 0;
|
|
117
119
|
listeners = new Set();
|
|
118
120
|
config;
|
|
119
121
|
constructor(config = {}) {
|
|
120
122
|
this.config = { ...config };
|
|
121
|
-
this.fallbackLocale =
|
|
123
|
+
this.fallbackLocale =
|
|
124
|
+
toLanguageTag(config.fallbackLocale) || DEFAULT_LOCALE;
|
|
122
125
|
this.catalogs = new Map([[DEFAULT_LOCALE, enUS]]);
|
|
123
126
|
this.customMessages = { ...(config.customMessages ?? {}) };
|
|
124
127
|
this.loading = new Map();
|
|
@@ -149,9 +152,12 @@ export class SimpleI18n {
|
|
|
149
152
|
* Resolves the request against the catalogs this provider can serve, loads
|
|
150
153
|
* the winner if it is not resident, then notifies subscribers — in that
|
|
151
154
|
* order, so a component's reactive read re-runs against a catalog that is
|
|
152
|
-
* already in place rather than against the previous locale.
|
|
155
|
+
* already in place rather than against the previous locale. The last call
|
|
156
|
+
* wins: a call whose load finishes after a later call's leaves the locale
|
|
157
|
+
* to that later call.
|
|
153
158
|
*/
|
|
154
159
|
async setLocale(locale) {
|
|
160
|
+
const request = ++this.requests;
|
|
155
161
|
const resolved = this.resolveLocaleTag(locale);
|
|
156
162
|
if (!resolved) {
|
|
157
163
|
// A locale we ship no catalog for is legitimate: the host may have
|
|
@@ -181,6 +187,8 @@ export class SimpleI18n {
|
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
189
|
}
|
|
190
|
+
if (request !== this.requests)
|
|
191
|
+
return;
|
|
184
192
|
this.locale = resolved;
|
|
185
193
|
this.notify();
|
|
186
194
|
}
|
|
@@ -190,7 +198,7 @@ export class SimpleI18n {
|
|
|
190
198
|
this.config.onMissingKey?.(key, this.locale);
|
|
191
199
|
return key;
|
|
192
200
|
}
|
|
193
|
-
return interpolate(message, values);
|
|
201
|
+
return interpolate(message, values, this.locale);
|
|
194
202
|
}
|
|
195
203
|
/**
|
|
196
204
|
* Translate a plural group.
|
|
@@ -207,7 +215,7 @@ export class SimpleI18n {
|
|
|
207
215
|
}
|
|
208
216
|
const category = this.selectPluralCategory(options.count);
|
|
209
217
|
const form = group[category] ?? group.other;
|
|
210
|
-
return interpolate(form, options);
|
|
218
|
+
return interpolate(form, options, this.locale);
|
|
211
219
|
}
|
|
212
220
|
getDirection() {
|
|
213
221
|
return localeDirection(this.locale);
|
|
@@ -389,12 +397,41 @@ function readPath(source, key) {
|
|
|
389
397
|
}
|
|
390
398
|
return current;
|
|
391
399
|
}
|
|
392
|
-
|
|
400
|
+
/**
|
|
401
|
+
* Substitute `{name}` placeholders.
|
|
402
|
+
*
|
|
403
|
+
* A finite numeric value renders through `Intl.NumberFormat` for the active
|
|
404
|
+
* locale, so digits and grouping follow CLDR locale defaults: `ar` renders
|
|
405
|
+
* Arabic-Indic digits, `fr` groups with narrow no-break spaces, `en-US` with
|
|
406
|
+
* commas. Catalog text that spells digits out is untouched.
|
|
407
|
+
*/
|
|
408
|
+
const numberFormats = new Map();
|
|
409
|
+
function formatNumber(value, locale) {
|
|
410
|
+
let formatter = numberFormats.get(locale);
|
|
411
|
+
if (!formatter) {
|
|
412
|
+
try {
|
|
413
|
+
formatter = new Intl.NumberFormat(locale);
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
formatter = new Intl.NumberFormat(DEFAULT_LOCALE);
|
|
417
|
+
}
|
|
418
|
+
numberFormats.set(locale, formatter);
|
|
419
|
+
}
|
|
420
|
+
return formatter.format(value);
|
|
421
|
+
}
|
|
422
|
+
function interpolate(text, values, locale) {
|
|
393
423
|
if (!values)
|
|
394
424
|
return text;
|
|
395
425
|
return text.replace(/\{(\w+)\}/g, (match, name) => {
|
|
396
426
|
const value = values[name];
|
|
397
|
-
|
|
427
|
+
if (value === undefined || value === null)
|
|
428
|
+
return match;
|
|
429
|
+
if (locale !== undefined &&
|
|
430
|
+
typeof value === "number" &&
|
|
431
|
+
Number.isFinite(value)) {
|
|
432
|
+
return formatNumber(value, locale);
|
|
433
|
+
}
|
|
434
|
+
return String(value);
|
|
398
435
|
});
|
|
399
436
|
}
|
|
400
437
|
function deepMerge(target, source) {
|
|
@@ -428,41 +465,51 @@ export function getDefaultI18n() {
|
|
|
428
465
|
defaultProvider = new SimpleI18n();
|
|
429
466
|
return defaultProvider;
|
|
430
467
|
}
|
|
431
|
-
/**
|
|
432
|
-
* Resolve a component's interface-locale provider from a published context.
|
|
433
|
-
*
|
|
434
|
-
* Returns a **fresh** object on every call. A
|
|
435
|
-
* provider is mutable: `setLocale` swaps its catalog in place, so its identity is
|
|
436
|
-
* unchanged before and after a locale load. A reactive `$derived` reading the
|
|
437
|
-
* provider directly therefore never invalidates, and a label rendered while the
|
|
438
|
-
* catalog was still loading stays English forever — the exact silent failure
|
|
439
|
-
* `composition-context.md` records for a context published without a change
|
|
440
|
-
* signal, and it is silent here too because English is a plausible answer.
|
|
441
|
-
*
|
|
442
|
-
* Deriving this instead gives the component a value whose identity tracks the
|
|
443
|
-
* context republish, so the strings built from it re-render. Call it inside
|
|
444
|
-
* `$derived`, never once at setup:
|
|
445
|
-
*
|
|
446
|
-
* ```ts
|
|
447
|
-
* const interfaceI18n = $derived(resolveInterfaceI18n(runtimeContext));
|
|
448
|
-
* ```
|
|
449
|
-
*
|
|
450
|
-
* A context with no provider — no publisher at all — resolves to the English-only
|
|
451
|
-
* default rather than nothing, which is the graceful default the pull pattern
|
|
452
|
-
* requires.
|
|
453
|
-
*/
|
|
454
468
|
export function resolveInterfaceI18n(source) {
|
|
455
469
|
const provider = source?.i18n ?? getDefaultI18n();
|
|
456
470
|
// A thin facade rather than the provider itself: cheap to allocate (a handful
|
|
457
471
|
// of times per session, once per republish) and it carries no state of its own,
|
|
458
|
-
// so it cannot drift from the provider it delegates to.
|
|
472
|
+
// so it cannot drift from the provider it delegates to. The locale formatters
|
|
473
|
+
// are part of the promised surface, so a provider that does not implement
|
|
474
|
+
// them falls through to `Intl` at the locale the provider resolves — the same
|
|
475
|
+
// thing the bundled provider does, and the same graceful default every other
|
|
476
|
+
// optional member here applies.
|
|
459
477
|
return {
|
|
460
|
-
|
|
478
|
+
// Canonical, because components read this for `lang` attributes; the
|
|
479
|
+
// provider's own `getLocale()` keeps the tag as resolved for lookups.
|
|
480
|
+
getLocale: () => {
|
|
481
|
+
const locale = provider.getLocale();
|
|
482
|
+
return canonicalLanguageTag(locale) ?? locale;
|
|
483
|
+
},
|
|
461
484
|
setLocale: (locale) => provider.setLocale(locale),
|
|
462
485
|
t: (key, values) => provider.t(key, values),
|
|
463
486
|
plural: (key, options) => provider.plural?.(key, options) ?? provider.t(key, options),
|
|
464
487
|
getDirection: () => provider.getDirection?.() ?? "ltr",
|
|
465
488
|
hasKey: (key) => provider.hasKey?.(key) ?? false,
|
|
466
489
|
subscribe: (listener) => provider.subscribe?.(listener) ?? (() => { }),
|
|
490
|
+
formatNumber: (value, options) => provider.formatNumber?.(value, options) ??
|
|
491
|
+
new Intl.NumberFormat(provider.getLocale(), options).format(value),
|
|
492
|
+
formatDate: (value, options) => provider.formatDate?.(value, options) ??
|
|
493
|
+
new Intl.DateTimeFormat(provider.getLocale(), options).format(value),
|
|
467
494
|
};
|
|
468
495
|
}
|
|
496
|
+
/**
|
|
497
|
+
* Stamp the interface locale's `lang` and `dir` on a chrome element.
|
|
498
|
+
*
|
|
499
|
+
* The imperative form of the `lang={…} dir={…}` pair a template puts on its
|
|
500
|
+
* root, for the elements a template cannot reach: a shadow host whose chrome
|
|
501
|
+
* is slotted light DOM, a shell built with `document.createElement`, a vanilla
|
|
502
|
+
* element stamping itself. Call it from whatever already re-runs on a locale
|
|
503
|
+
* change and on a catalog load: `getLocale()` moves only once the requested
|
|
504
|
+
* catalog is resident, so a stamp taken at setup alone names the locale that
|
|
505
|
+
* rendered before the load.
|
|
506
|
+
*/
|
|
507
|
+
export function stampInterfaceLocale(target, provider) {
|
|
508
|
+
// `lang` takes the canonical form: a provider honours an unshipped tag as
|
|
509
|
+
// the host spelled it, and `NL-nl` is a valid request but not a valid
|
|
510
|
+
// attribute value. Every `lang` this repository writes goes through the
|
|
511
|
+
// same rule, so chrome and content stamps match in shape.
|
|
512
|
+
const locale = provider.getLocale();
|
|
513
|
+
target.setAttribute("lang", canonicalLanguageTag(locale) ?? locale);
|
|
514
|
+
target.setAttribute("dir", provider.getDirection?.() ?? "ltr");
|
|
515
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pseudo-localization: a transform over the English catalog that keeps every key,
|
|
3
|
+
* plural category and `{placeholder}` intact while making each string accented,
|
|
4
|
+
* bracketed and padded roughly 40% longer.
|
|
5
|
+
*
|
|
6
|
+
* What it is for: the gates check parity, not rendered geometry. A pseudo-locale
|
|
7
|
+
* makes truncation, clipped labels, strings glued together in code and text that
|
|
8
|
+
* was never catalog text visible in any demo or e2e run, before a translator or a
|
|
9
|
+
* reviewer sees a real one. It is the industry's `qps-ploc` (Windows) and
|
|
10
|
+
* `en-XA`/`ar-XB` (Android) practice; PIE uses the Android tags, registered
|
|
11
|
+
* through `customMessages`, never shipped as catalogs.
|
|
12
|
+
*/
|
|
13
|
+
import type { MessageCatalog } from "./types.js";
|
|
14
|
+
/** Expanded Latin pseudo-locale: text grows ~40% and carries accents and brackets. */
|
|
15
|
+
export declare const PSEUDO_LOCALE_EXPANDED = "en-XA";
|
|
16
|
+
/**
|
|
17
|
+
* Right-to-left pseudo-locale: the same transformed English under an `ar` primary
|
|
18
|
+
* subtag, so `localeDirection` resolves RTL and chrome lays out mirrored without a
|
|
19
|
+
* word of Arabic on screen.
|
|
20
|
+
*/
|
|
21
|
+
export declare const PSEUDO_LOCALE_RTL = "ar-XB";
|
|
22
|
+
export interface PseudoLocalizeOptions {
|
|
23
|
+
/** Extra length as a fraction of the original, on top of what accents add. Default 0.4. */
|
|
24
|
+
expansion?: number;
|
|
25
|
+
/** Wrap every string in `[` and `]` so concatenated fragments stand out. Default true. */
|
|
26
|
+
brackets?: boolean;
|
|
27
|
+
/** Map Latin letters to accented look-alikes. Default true. */
|
|
28
|
+
accents?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Produce the pseudo-localized twin of a catalog: same keys, same plural
|
|
32
|
+
* categories, same placeholder sets, longer accented bracketed strings.
|
|
33
|
+
*/
|
|
34
|
+
export declare function pseudoLocalize(catalog: MessageCatalog, options?: PseudoLocalizeOptions): MessageCatalog;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Expanded Latin pseudo-locale: text grows ~40% and carries accents and brackets. */
|
|
2
|
+
export const PSEUDO_LOCALE_EXPANDED = "en-XA";
|
|
3
|
+
/**
|
|
4
|
+
* Right-to-left pseudo-locale: the same transformed English under an `ar` primary
|
|
5
|
+
* subtag, so `localeDirection` resolves RTL and chrome lays out mirrored without a
|
|
6
|
+
* word of Arabic on screen.
|
|
7
|
+
*/
|
|
8
|
+
export const PSEUDO_LOCALE_RTL = "ar-XB";
|
|
9
|
+
const ACCENTS = {
|
|
10
|
+
a: "á",
|
|
11
|
+
A: "Á",
|
|
12
|
+
b: "ƀ",
|
|
13
|
+
B: "Ƀ",
|
|
14
|
+
c: "ç",
|
|
15
|
+
C: "Ç",
|
|
16
|
+
d: "ð",
|
|
17
|
+
D: "Ð",
|
|
18
|
+
e: "é",
|
|
19
|
+
E: "É",
|
|
20
|
+
f: "ƒ",
|
|
21
|
+
F: "Ƒ",
|
|
22
|
+
g: "ğ",
|
|
23
|
+
G: "Ğ",
|
|
24
|
+
h: "ĥ",
|
|
25
|
+
H: "Ĥ",
|
|
26
|
+
i: "í",
|
|
27
|
+
I: "Í",
|
|
28
|
+
j: "ĵ",
|
|
29
|
+
J: "Ĵ",
|
|
30
|
+
k: "ķ",
|
|
31
|
+
K: "Ķ",
|
|
32
|
+
l: "ĺ",
|
|
33
|
+
L: "Ĺ",
|
|
34
|
+
m: "ḿ",
|
|
35
|
+
M: "Ḿ",
|
|
36
|
+
n: "ñ",
|
|
37
|
+
N: "Ñ",
|
|
38
|
+
o: "ó",
|
|
39
|
+
O: "Ó",
|
|
40
|
+
p: "þ",
|
|
41
|
+
P: "Þ",
|
|
42
|
+
q: "ǫ",
|
|
43
|
+
Q: "Ǫ",
|
|
44
|
+
r: "ŕ",
|
|
45
|
+
R: "Ŕ",
|
|
46
|
+
s: "š",
|
|
47
|
+
S: "Š",
|
|
48
|
+
t: "ŧ",
|
|
49
|
+
T: "Ŧ",
|
|
50
|
+
u: "ú",
|
|
51
|
+
U: "Ú",
|
|
52
|
+
v: "ṽ",
|
|
53
|
+
V: "Ṽ",
|
|
54
|
+
w: "ŵ",
|
|
55
|
+
W: "Ŵ",
|
|
56
|
+
x: "ẋ",
|
|
57
|
+
X: "Ẋ",
|
|
58
|
+
y: "ý",
|
|
59
|
+
Y: "Ý",
|
|
60
|
+
z: "ž",
|
|
61
|
+
Z: "Ž",
|
|
62
|
+
};
|
|
63
|
+
const PLACEHOLDER = /(\{\w+\})/g;
|
|
64
|
+
function pseudoText(text, { expansion, accents, brackets }) {
|
|
65
|
+
const segments = text.split(PLACEHOLDER);
|
|
66
|
+
const transformed = segments.map((segment, index) => {
|
|
67
|
+
// A capturing split leaves each placeholder at an odd index, verbatim.
|
|
68
|
+
if (index % 2 === 1 || segment === "")
|
|
69
|
+
return segment;
|
|
70
|
+
let out = segment;
|
|
71
|
+
if (accents) {
|
|
72
|
+
out = out.replace(/[a-zA-Z]/g, (letter) => ACCENTS[letter] ?? letter);
|
|
73
|
+
}
|
|
74
|
+
if (expansion <= 0)
|
|
75
|
+
return out;
|
|
76
|
+
const padding = Math.max(1, Math.ceil(segment.length * expansion));
|
|
77
|
+
return `${out} ${"~".repeat(padding)}`;
|
|
78
|
+
});
|
|
79
|
+
const joined = transformed.join("");
|
|
80
|
+
return brackets ? `[${joined}]` : joined;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Produce the pseudo-localized twin of a catalog: same keys, same plural
|
|
84
|
+
* categories, same placeholder sets, longer accented bracketed strings.
|
|
85
|
+
*/
|
|
86
|
+
export function pseudoLocalize(catalog, options) {
|
|
87
|
+
const settings = {
|
|
88
|
+
expansion: options?.expansion ?? 0.4,
|
|
89
|
+
brackets: options?.brackets ?? true,
|
|
90
|
+
accents: options?.accents ?? true,
|
|
91
|
+
};
|
|
92
|
+
const result = {};
|
|
93
|
+
for (const [key, node] of Object.entries(catalog)) {
|
|
94
|
+
result[key] = pseudoNode(node, settings);
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
function pseudoNode(node, settings) {
|
|
99
|
+
if (typeof node === "string")
|
|
100
|
+
return pseudoText(node, settings);
|
|
101
|
+
const branch = {};
|
|
102
|
+
for (const [key, child] of Object.entries(node)) {
|
|
103
|
+
branch[key] = pseudoNode(child, settings);
|
|
104
|
+
}
|
|
105
|
+
return branch;
|
|
106
|
+
}
|
|
@@ -85,4 +85,14 @@ export declare function resolveEsmAssetRoot(options: {
|
|
|
85
85
|
cdnBaseUrl: string;
|
|
86
86
|
cdnProvider?: EsmCdnProviderOption;
|
|
87
87
|
}): string | undefined;
|
|
88
|
+
/**
|
|
89
|
+
* URL of a raw npm package file, `file` being `<package>@<version>/<path>`, on
|
|
90
|
+
* the ESM CDN a player's `loaderOptions` name, so a page asset the players add,
|
|
91
|
+
* such as a stylesheet, comes from where the elements do. Reads untyped options
|
|
92
|
+
* as a toolkit runtime context carries them; jsDelivr when they set no base.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveEsmPackageFileUrl(loaderOptions: {
|
|
95
|
+
esmCdnUrl?: unknown;
|
|
96
|
+
esmCdnProvider?: unknown;
|
|
97
|
+
} | null | undefined, file: string): string;
|
|
88
98
|
export declare function pickElementClass(module: any, view: string): unknown;
|
|
@@ -19,6 +19,7 @@ import { writeRegistryEntry } from "../pie/registry.js";
|
|
|
19
19
|
import { validateCustomElementTag } from "../pie/tag-names.js";
|
|
20
20
|
import { BundleType, isCustomElementConstructor, Status, } from "../pie/types.js";
|
|
21
21
|
import { parsePackageName } from "../pie/utils.js";
|
|
22
|
+
import { DEFAULT_ESM_CDN_URL } from "./defaults.js";
|
|
22
23
|
import { AdapterFailure, } from "./element-loader-types.js";
|
|
23
24
|
import { isExactSemver } from "./element-package-policy.js";
|
|
24
25
|
import { invalidPackageSpecMessage, isScopedPackageSpec, } from "./element-package-spec.js";
|
|
@@ -141,8 +142,11 @@ export function createEsmBackend(config) {
|
|
|
141
142
|
: undefined;
|
|
142
143
|
const imports = withoutMappedSpecifiers({ ...importMapResult.imports, ...runtimePlan?.imports }, context.doc);
|
|
143
144
|
let injectedMap;
|
|
145
|
+
let map;
|
|
144
146
|
if (Object.keys(imports).length > 0) {
|
|
145
|
-
const
|
|
147
|
+
const scopes = withoutMappedScopes(jsDelivrRootPathScopes(cdnProvider, cdnBaseUrl, context.doc), context.doc);
|
|
148
|
+
map = scopes ? { imports, scopes } : { imports };
|
|
149
|
+
const json = JSON.stringify(map, null, 2);
|
|
146
150
|
// URL resolution maps the shared dependencies and the editor
|
|
147
151
|
// runtime, so it needs import maps too.
|
|
148
152
|
assertImportMapSupported();
|
|
@@ -162,8 +166,8 @@ export function createEsmBackend(config) {
|
|
|
162
166
|
importMappedPackageVersions.set(parsePackageName(pkg).name, pkg);
|
|
163
167
|
}
|
|
164
168
|
}
|
|
165
|
-
if (injectedMap) {
|
|
166
|
-
await applyImportMap(
|
|
169
|
+
if (injectedMap && map) {
|
|
170
|
+
await applyImportMap(map, injectedMap, context.doc);
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
await Promise.all(Object.entries(loadable).map(async ([tag, packageVersion]) => {
|
|
@@ -339,13 +343,13 @@ export function createEsmBackend(config) {
|
|
|
339
343
|
* document that already uses it, and otherwise the browser has either
|
|
340
344
|
* applied it or rejected it.
|
|
341
345
|
*/
|
|
342
|
-
async function applyImportMap(
|
|
346
|
+
async function applyImportMap(map, script, doc) {
|
|
343
347
|
let state = "applied";
|
|
344
348
|
try {
|
|
345
349
|
if (script.type === SHIM_IMPORT_MAP_TYPE) {
|
|
346
|
-
(await importShim(doc)).addImportMap(
|
|
350
|
+
(await importShim(doc)).addImportMap(map);
|
|
347
351
|
}
|
|
348
|
-
else if (await importMapRejected(imports)) {
|
|
352
|
+
else if (await importMapRejected(map.imports)) {
|
|
349
353
|
state = "rejected";
|
|
350
354
|
await importShim(doc);
|
|
351
355
|
}
|
|
@@ -384,7 +388,7 @@ export function createEsmBackend(config) {
|
|
|
384
388
|
*/
|
|
385
389
|
function importShim(doc) {
|
|
386
390
|
importShimRequest ??= importShimLoader().then((shim) => {
|
|
387
|
-
shim.addImportMap(
|
|
391
|
+
shim.addImportMap(documentImportMap(doc));
|
|
388
392
|
return shim;
|
|
389
393
|
}, (err) => {
|
|
390
394
|
importShimRequest = undefined;
|
|
@@ -661,6 +665,24 @@ export function resolveEsmAssetRoot(options) {
|
|
|
661
665
|
? packageJsonUrl.slice(0, -suffix.length) || undefined
|
|
662
666
|
: undefined;
|
|
663
667
|
}
|
|
668
|
+
/**
|
|
669
|
+
* URL of a raw npm package file, `file` being `<package>@<version>/<path>`, on
|
|
670
|
+
* the ESM CDN a player's `loaderOptions` name, so a page asset the players add,
|
|
671
|
+
* such as a stylesheet, comes from where the elements do. Reads untyped options
|
|
672
|
+
* as a toolkit runtime context carries them; jsDelivr when they set no base.
|
|
673
|
+
*/
|
|
674
|
+
export function resolveEsmPackageFileUrl(loaderOptions, file) {
|
|
675
|
+
const configured = loaderOptions?.esmCdnUrl;
|
|
676
|
+
const cdnBaseUrl = (typeof configured === "string" && configured.trim()
|
|
677
|
+
? configured.trim()
|
|
678
|
+
: DEFAULT_ESM_CDN_URL).replace(/\/+$/, "");
|
|
679
|
+
const provider = loaderOptions?.esmCdnProvider;
|
|
680
|
+
const cdnProvider = typeof provider === "string" || isEsmCdnProvider(provider)
|
|
681
|
+
? provider
|
|
682
|
+
: undefined;
|
|
683
|
+
const root = resolveEsmAssetRoot({ cdnBaseUrl, cdnProvider }) ?? cdnBaseUrl;
|
|
684
|
+
return `${root}/${file.replace(/^\/+/, "")}`;
|
|
685
|
+
}
|
|
664
686
|
function resolveCdnProvider(provider, cdnBaseUrl) {
|
|
665
687
|
if (isEsmCdnProvider(provider))
|
|
666
688
|
return provider;
|
|
@@ -681,7 +703,7 @@ function isEsmCdnProvider(value) {
|
|
|
681
703
|
typeof candidate.sharedDependencyUrl === "function");
|
|
682
704
|
}
|
|
683
705
|
function createJsDelivrProvider(cdnBaseUrl, name = "jsdelivr") {
|
|
684
|
-
|
|
706
|
+
const provider = {
|
|
685
707
|
name,
|
|
686
708
|
packageJsonUrl: (packageVersion) => `${cdnBaseUrl}/${packageVersion}/package.json`,
|
|
687
709
|
browserViewUrl: (packageVersion, view) => `${cdnBaseUrl}/${packageVersion}/dist/browser/${view}/index.js`,
|
|
@@ -692,7 +714,31 @@ function createJsDelivrProvider(cdnBaseUrl, name = "jsdelivr") {
|
|
|
692
714
|
return `${cdnBaseUrl}/${dependencyName}@${version}${suffix}`;
|
|
693
715
|
},
|
|
694
716
|
};
|
|
717
|
+
jsDelivrShapedProviders.add(provider);
|
|
718
|
+
return provider;
|
|
695
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* jsDelivr's `+esm` builds import their dependencies by root path
|
|
722
|
+
* (`/npm/react@18.3.1/+esm`), which resolves against the origin that served
|
|
723
|
+
* them. A base mounted anywhere but `/npm` gets a scope mapping those paths
|
|
724
|
+
* back under it, so they load from the configured CDN and resolve to the URLs
|
|
725
|
+
* the shared-dependency entries map, keeping one React on the page.
|
|
726
|
+
*/
|
|
727
|
+
function jsDelivrRootPathScopes(cdnProvider, cdnBaseUrl, doc) {
|
|
728
|
+
if (!jsDelivrShapedProviders.has(cdnProvider))
|
|
729
|
+
return undefined;
|
|
730
|
+
let base;
|
|
731
|
+
try {
|
|
732
|
+
base = new URL(`${cdnBaseUrl}/`, doc.baseURI || undefined);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
const root = new URL("/npm/", base).href;
|
|
738
|
+
return root === base.href ? undefined : { [base.href]: { [root]: base.href } };
|
|
739
|
+
}
|
|
740
|
+
/** Providers built by {@link createJsDelivrProvider}, whatever their name. */
|
|
741
|
+
const jsDelivrShapedProviders = new WeakSet();
|
|
696
742
|
function createEsmShProvider(cdnBaseUrl) {
|
|
697
743
|
const esmBaseUrl = cdnBaseUrl.replace(/\/+$/, "");
|
|
698
744
|
const rawBaseUrl = toRawEsmShBaseUrl(esmBaseUrl);
|
|
@@ -1050,23 +1096,50 @@ async function importMapsSettled(doc) {
|
|
|
1050
1096
|
* for a specifier as it does in the browser.
|
|
1051
1097
|
*/
|
|
1052
1098
|
function documentImports(doc) {
|
|
1099
|
+
return documentImportMap(doc).imports;
|
|
1100
|
+
}
|
|
1101
|
+
/** {@link documentImports}, with the scopes the maps define. */
|
|
1102
|
+
function documentImportMap(doc) {
|
|
1053
1103
|
const imports = Object.create(null);
|
|
1104
|
+
const scopes = Object.create(null);
|
|
1054
1105
|
for (const script of importMapScripts(doc)) {
|
|
1055
1106
|
try {
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1107
|
+
const map = JSON.parse(script.textContent || "{}");
|
|
1108
|
+
addFirstRules(imports, map?.imports);
|
|
1109
|
+
if (!map?.scopes || typeof map.scopes !== "object")
|
|
1058
1110
|
continue;
|
|
1059
|
-
for (const [
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
}
|
|
1111
|
+
for (const [scope, rules] of Object.entries(map.scopes)) {
|
|
1112
|
+
scopes[scope] ??= Object.create(null);
|
|
1113
|
+
addFirstRules(scopes[scope], rules);
|
|
1063
1114
|
}
|
|
1064
1115
|
}
|
|
1065
1116
|
catch {
|
|
1066
1117
|
// The browser rejects a map it cannot parse, so it maps nothing.
|
|
1067
1118
|
}
|
|
1068
1119
|
}
|
|
1069
|
-
return imports;
|
|
1120
|
+
return { imports, scopes };
|
|
1121
|
+
}
|
|
1122
|
+
function addFirstRules(target, rules) {
|
|
1123
|
+
if (!rules || typeof rules !== "object")
|
|
1124
|
+
return;
|
|
1125
|
+
for (const [specifier, url] of Object.entries(rules)) {
|
|
1126
|
+
if (typeof url === "string" && !(specifier in target)) {
|
|
1127
|
+
target[specifier] = url;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
/** {@link withoutMappedSpecifiers} for scope rules; `undefined` when none remain. */
|
|
1132
|
+
function withoutMappedScopes(scopes, doc) {
|
|
1133
|
+
if (!scopes)
|
|
1134
|
+
return undefined;
|
|
1135
|
+
const mapped = documentImportMap(doc).scopes;
|
|
1136
|
+
const remaining = {};
|
|
1137
|
+
for (const [scope, rules] of Object.entries(scopes)) {
|
|
1138
|
+
const kept = Object.entries(rules).filter(([specifier]) => !(specifier in (mapped[scope] ?? {})));
|
|
1139
|
+
if (kept.length > 0)
|
|
1140
|
+
remaining[scope] = Object.fromEntries(kept);
|
|
1141
|
+
}
|
|
1142
|
+
return Object.keys(remaining).length > 0 ? remaining : undefined;
|
|
1070
1143
|
}
|
|
1071
1144
|
/**
|
|
1072
1145
|
* es-module-shims in shim mode: it resolves through the import maps this
|
package/dist/loaders/index.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export { isScopedPackageSpec } from "./element-package-spec.js";
|
|
|
38
38
|
export type { BackendContext, BackendOption, ElementLoaderBackend, ElementTag, EnsureRegisteredOptions, EsmBackendConfig, IifeBackendConfig, RegistrationFailureReason, } from "./element-loader.js";
|
|
39
39
|
export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
|
|
40
40
|
export type { BuiltInEsmCdnProviderName, EsmBackend, EsmCdnProvider, EsmCdnProviderName, EsmCdnProviderOption, ViewConfig, } from "./esm-adapter.js";
|
|
41
|
-
export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
|
|
41
|
+
export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmPackageFileUrl, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
|
|
42
42
|
export type { IifeBackend, IifeBundleRetryStatus, } from "./iife-adapter.js";
|
|
43
43
|
export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
|
|
44
44
|
export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
|
package/dist/loaders/index.js
CHANGED
|
@@ -34,7 +34,7 @@ export { aggregateElements } from "./ElementLoader.js";
|
|
|
34
34
|
export { assertElementPackagesAllowed, ElementPackagePolicyError, } from "./element-package-policy.js";
|
|
35
35
|
export { isScopedPackageSpec } from "./element-package-spec.js";
|
|
36
36
|
export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
|
|
37
|
-
export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
|
|
37
|
+
export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmPackageFileUrl, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
|
|
38
38
|
export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
|
|
39
39
|
export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
|
|
40
40
|
export { createIifeBackend } from "./iife-adapter.js";
|
package/dist/object/index.d.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare const mergeObjectsIgnoringNullUndefined: <T extends object>(...objects: T[]) => T;
|
|
8
8
|
/**
|
|
9
|
-
* Deep clone
|
|
9
|
+
* Deep clone of arrays and plain objects. Any other object — a `Date`, a bson
|
|
10
|
+
* `ObjectId`, a class instance — is carried over by reference: copying its own
|
|
11
|
+
* keys onto a plain object would drop its prototype, and with it the methods
|
|
12
|
+
* that identify and serialise it.
|
|
10
13
|
*/
|
|
11
14
|
export declare const cloneDeep: <T>(value: T) => T;
|
|
12
15
|
/**
|
package/dist/object/index.js
CHANGED
|
@@ -17,7 +17,10 @@ export const mergeObjectsIgnoringNullUndefined = (...objects) => {
|
|
|
17
17
|
}, {});
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
|
-
* Deep clone
|
|
20
|
+
* Deep clone of arrays and plain objects. Any other object — a `Date`, a bson
|
|
21
|
+
* `ObjectId`, a class instance — is carried over by reference: copying its own
|
|
22
|
+
* keys onto a plain object would drop its prototype, and with it the methods
|
|
23
|
+
* that identify and serialise it.
|
|
21
24
|
*/
|
|
22
25
|
export const cloneDeep = (value) => {
|
|
23
26
|
if (value === null || typeof value !== "object") {
|
|
@@ -26,6 +29,10 @@ export const cloneDeep = (value) => {
|
|
|
26
29
|
if (Array.isArray(value)) {
|
|
27
30
|
return value.map(cloneDeep);
|
|
28
31
|
}
|
|
32
|
+
const prototype = Object.getPrototypeOf(value);
|
|
33
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
29
36
|
const copy = {};
|
|
30
37
|
for (const key in value) {
|
|
31
38
|
if (Object.hasOwn(value, key)) {
|
package/dist/pie/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export type { PieElementContext, PieElementContextSource, } from "./element-obse
|
|
|
15
15
|
export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolveConfigureConfiguration, validateAuthoringModels, } from "./authoring.js";
|
|
16
16
|
export type { AuthoringMediaHandlers, AuthoringValidationResult, InitializedConfigureModel, } from "./authoring.js";
|
|
17
17
|
export { initializeMathRendering, setMathRenderer, } from "./math-rendering.js";
|
|
18
|
+
export type { MathRenderingOptions } from "./math-rendering.js";
|
|
18
19
|
export { attachInstrumentationEventBridge } from "./instrumentation-event-bridge.js";
|
|
19
20
|
export type { AttachInstrumentationEventBridgeArgs } from "./instrumentation-event-bridge.js";
|
|
20
21
|
export { resolveInstrumentationProvider } from "./instrumentation-provider-resolution.js";
|
|
@@ -13,6 +13,16 @@ export interface MathRenderingAPI {
|
|
|
13
13
|
unWrapMath?: (wrapped: string) => string;
|
|
14
14
|
mmlToLatex?: (mathml: string) => string;
|
|
15
15
|
}
|
|
16
|
+
/** How the default renderer `initializeMathRendering` installs loads its assets. */
|
|
17
|
+
export interface MathRenderingOptions {
|
|
18
|
+
/**
|
|
19
|
+
* The npm root the renderer's speech rules load from when the page's math
|
|
20
|
+
* options set no `assetRoot`. With neither, jsDelivr. A page that names the
|
|
21
|
+
* rules' directory itself, in `SREfeature.json` or a `text/x-sre-config`
|
|
22
|
+
* block, keeps it.
|
|
23
|
+
*/
|
|
24
|
+
assetRoot?: string;
|
|
25
|
+
}
|
|
16
26
|
/**
|
|
17
27
|
* The page's renderer: the one `initializeMathRendering` installed, or a
|
|
18
28
|
* host's. IIFE elements render with it, and so do ESM elements when the page
|
|
@@ -27,6 +37,8 @@ export declare const getMathRenderer: () => MathRenderingAPI | null;
|
|
|
27
37
|
* - window["@pie-lib/math-rendering"] (standard key)
|
|
28
38
|
* - window["_dll_pie_lib__math_rendering"] (SystemJS/DLL key for IIFE bundles)
|
|
29
39
|
*
|
|
40
|
+
* The first call installs the renderer for the page, so its `options` apply.
|
|
41
|
+
*
|
|
30
42
|
* @example
|
|
31
43
|
* ```typescript
|
|
32
44
|
* // Default MathJax
|
|
@@ -37,7 +49,7 @@ export declare const getMathRenderer: () => MathRenderingAPI | null;
|
|
|
37
49
|
* await initializeMathRendering();
|
|
38
50
|
* ```
|
|
39
51
|
*/
|
|
40
|
-
export declare function initializeMathRendering(): Promise<void>;
|
|
52
|
+
export declare function initializeMathRendering(options?: MathRenderingOptions): Promise<void>;
|
|
41
53
|
/**
|
|
42
54
|
* Set custom math renderer programmatically
|
|
43
55
|
*
|