@pie-players/pie-players-shared 1.0.0-beta.2 → 1.0.0-beta.4
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 +113 -3
- package/dist/i18n/messages/en-US.js +128 -3
- package/dist/i18n/messages/nl-NL.d.ts +103 -2
- package/dist/i18n/messages/nl-NL.js +109 -8
- package/dist/i18n/provider.d.ts +20 -1
- package/dist/i18n/provider.js +71 -31
- 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/loaders/preloaded-registration.d.ts +18 -6
- package/dist/loaders/preloaded-registration.js +44 -9
- 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/security/overwide-regions.d.ts +20 -0
- package/dist/security/overwide-regions.js +65 -0
- package/dist/security/wrap-overwide-images.d.ts +6 -2
- package/dist/security/wrap-overwide-images.js +5 -2
- package/dist/security/wrap-overwide-tables.d.ts +6 -2
- package/dist/security/wrap-overwide-tables.js +5 -2
- package/dist/security/wrap-overwide.d.ts +13 -0
- package/dist/security/wrap-overwide.js +68 -3
- 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 +4 -4
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";
|
|
@@ -118,7 +118,8 @@ export class SimpleI18n {
|
|
|
118
118
|
config;
|
|
119
119
|
constructor(config = {}) {
|
|
120
120
|
this.config = { ...config };
|
|
121
|
-
this.fallbackLocale =
|
|
121
|
+
this.fallbackLocale =
|
|
122
|
+
toLanguageTag(config.fallbackLocale) || DEFAULT_LOCALE;
|
|
122
123
|
this.catalogs = new Map([[DEFAULT_LOCALE, enUS]]);
|
|
123
124
|
this.customMessages = { ...(config.customMessages ?? {}) };
|
|
124
125
|
this.loading = new Map();
|
|
@@ -190,7 +191,7 @@ export class SimpleI18n {
|
|
|
190
191
|
this.config.onMissingKey?.(key, this.locale);
|
|
191
192
|
return key;
|
|
192
193
|
}
|
|
193
|
-
return interpolate(message, values);
|
|
194
|
+
return interpolate(message, values, this.locale);
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
196
197
|
* Translate a plural group.
|
|
@@ -207,7 +208,7 @@ export class SimpleI18n {
|
|
|
207
208
|
}
|
|
208
209
|
const category = this.selectPluralCategory(options.count);
|
|
209
210
|
const form = group[category] ?? group.other;
|
|
210
|
-
return interpolate(form, options);
|
|
211
|
+
return interpolate(form, options, this.locale);
|
|
211
212
|
}
|
|
212
213
|
getDirection() {
|
|
213
214
|
return localeDirection(this.locale);
|
|
@@ -389,12 +390,41 @@ function readPath(source, key) {
|
|
|
389
390
|
}
|
|
390
391
|
return current;
|
|
391
392
|
}
|
|
392
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Substitute `{name}` placeholders.
|
|
395
|
+
*
|
|
396
|
+
* A finite numeric value renders through `Intl.NumberFormat` for the active
|
|
397
|
+
* locale, so digits and grouping follow CLDR locale defaults: `ar` renders
|
|
398
|
+
* Arabic-Indic digits, `fr` groups with narrow no-break spaces, `en-US` with
|
|
399
|
+
* commas. Catalog text that spells digits out is untouched.
|
|
400
|
+
*/
|
|
401
|
+
const numberFormats = new Map();
|
|
402
|
+
function formatNumber(value, locale) {
|
|
403
|
+
let formatter = numberFormats.get(locale);
|
|
404
|
+
if (!formatter) {
|
|
405
|
+
try {
|
|
406
|
+
formatter = new Intl.NumberFormat(locale);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
formatter = new Intl.NumberFormat(DEFAULT_LOCALE);
|
|
410
|
+
}
|
|
411
|
+
numberFormats.set(locale, formatter);
|
|
412
|
+
}
|
|
413
|
+
return formatter.format(value);
|
|
414
|
+
}
|
|
415
|
+
function interpolate(text, values, locale) {
|
|
393
416
|
if (!values)
|
|
394
417
|
return text;
|
|
395
418
|
return text.replace(/\{(\w+)\}/g, (match, name) => {
|
|
396
419
|
const value = values[name];
|
|
397
|
-
|
|
420
|
+
if (value === undefined || value === null)
|
|
421
|
+
return match;
|
|
422
|
+
if (locale !== undefined &&
|
|
423
|
+
typeof value === "number" &&
|
|
424
|
+
Number.isFinite(value)) {
|
|
425
|
+
return formatNumber(value, locale);
|
|
426
|
+
}
|
|
427
|
+
return String(value);
|
|
398
428
|
});
|
|
399
429
|
}
|
|
400
430
|
function deepMerge(target, source) {
|
|
@@ -428,41 +458,51 @@ export function getDefaultI18n() {
|
|
|
428
458
|
defaultProvider = new SimpleI18n();
|
|
429
459
|
return defaultProvider;
|
|
430
460
|
}
|
|
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
461
|
export function resolveInterfaceI18n(source) {
|
|
455
462
|
const provider = source?.i18n ?? getDefaultI18n();
|
|
456
463
|
// A thin facade rather than the provider itself: cheap to allocate (a handful
|
|
457
464
|
// 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.
|
|
465
|
+
// so it cannot drift from the provider it delegates to. The locale formatters
|
|
466
|
+
// are part of the promised surface, so a provider that does not implement
|
|
467
|
+
// them falls through to `Intl` at the locale the provider resolves — the same
|
|
468
|
+
// thing the bundled provider does, and the same graceful default every other
|
|
469
|
+
// optional member here applies.
|
|
459
470
|
return {
|
|
460
|
-
|
|
471
|
+
// Canonical, because components read this for `lang` attributes; the
|
|
472
|
+
// provider's own `getLocale()` keeps the tag as resolved for lookups.
|
|
473
|
+
getLocale: () => {
|
|
474
|
+
const locale = provider.getLocale();
|
|
475
|
+
return canonicalLanguageTag(locale) ?? locale;
|
|
476
|
+
},
|
|
461
477
|
setLocale: (locale) => provider.setLocale(locale),
|
|
462
478
|
t: (key, values) => provider.t(key, values),
|
|
463
479
|
plural: (key, options) => provider.plural?.(key, options) ?? provider.t(key, options),
|
|
464
480
|
getDirection: () => provider.getDirection?.() ?? "ltr",
|
|
465
481
|
hasKey: (key) => provider.hasKey?.(key) ?? false,
|
|
466
482
|
subscribe: (listener) => provider.subscribe?.(listener) ?? (() => { }),
|
|
483
|
+
formatNumber: (value, options) => provider.formatNumber?.(value, options) ??
|
|
484
|
+
new Intl.NumberFormat(provider.getLocale(), options).format(value),
|
|
485
|
+
formatDate: (value, options) => provider.formatDate?.(value, options) ??
|
|
486
|
+
new Intl.DateTimeFormat(provider.getLocale(), options).format(value),
|
|
467
487
|
};
|
|
468
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* Stamp the interface locale's `lang` and `dir` on a chrome element.
|
|
491
|
+
*
|
|
492
|
+
* The imperative form of the `lang={…} dir={…}` pair a template puts on its
|
|
493
|
+
* root, for the elements a template cannot reach: a shadow host whose chrome
|
|
494
|
+
* is slotted light DOM, a shell built with `document.createElement`, a vanilla
|
|
495
|
+
* element stamping itself. Call it from whatever already re-runs on a locale
|
|
496
|
+
* change and on a catalog load: `getLocale()` moves only once the requested
|
|
497
|
+
* catalog is resident, so a stamp taken at setup alone names the locale that
|
|
498
|
+
* rendered before the load.
|
|
499
|
+
*/
|
|
500
|
+
export function stampInterfaceLocale(target, provider) {
|
|
501
|
+
// `lang` takes the canonical form: a provider honours an unshipped tag as
|
|
502
|
+
// the host spelled it, and `NL-nl` is a valid request but not a valid
|
|
503
|
+
// attribute value. Every `lang` this repository writes goes through the
|
|
504
|
+
// same rule, so chrome and content stamps match in shape.
|
|
505
|
+
const locale = provider.getLocale();
|
|
506
|
+
target.setAttribute("lang", canonicalLanguageTag(locale) ?? locale);
|
|
507
|
+
target.setAttribute("dir", provider.getDirection?.() ?? "ltr");
|
|
508
|
+
}
|
|
@@ -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";
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* define the ones content names under another base tag
|
|
12
12
|
* (`defineAuthoredPreloadedTags`) and assert them (`assertRegistered`).
|
|
13
13
|
*
|
|
14
|
+
* pie-elements-ng's `./browser/*` modules export `piePackage`, the
|
|
15
|
+
* `{ name, version }` of the package they were built from, so an entry can
|
|
16
|
+
* give its modules alone. A `package` or `version` it also gives must match,
|
|
17
|
+
* which catches a registration an upgrade left behind.
|
|
18
|
+
*
|
|
14
19
|
* A hosted player takes models from the server and resolves no controller. A
|
|
15
20
|
* player that is not hosted runs the controller's `model()` in the browser, so
|
|
16
21
|
* it needs the `controller` an entry registers; the item player warns about
|
|
@@ -29,13 +34,20 @@ export interface PreloadedElement {
|
|
|
29
34
|
/**
|
|
30
35
|
* Base tag to register, e.g. `pie-element-multiple-choice`. Content can
|
|
31
36
|
* author another base tag for the package, which the players define from
|
|
32
|
-
* this registration.
|
|
37
|
+
* this registration. Defaults to the package name without its `@`, with
|
|
38
|
+
* `-` for `/`.
|
|
39
|
+
*/
|
|
40
|
+
tag?: string;
|
|
41
|
+
/**
|
|
42
|
+
* npm package name, e.g. `@pie-element/multiple-choice`. Defaults to the
|
|
43
|
+
* name in the modules' `piePackage`.
|
|
44
|
+
*/
|
|
45
|
+
package?: string;
|
|
46
|
+
/**
|
|
47
|
+
* The installed version, exact, e.g. `13.4.4`. Defaults to the version in
|
|
48
|
+
* the modules' `piePackage`.
|
|
33
49
|
*/
|
|
34
|
-
|
|
35
|
-
/** npm package name, e.g. `@pie-element/multiple-choice`. */
|
|
36
|
-
package: string;
|
|
37
|
-
/** The installed version, exact, e.g. `13.4.4`. */
|
|
38
|
-
version: string;
|
|
50
|
+
version?: string;
|
|
39
51
|
/** The package's `./browser/delivery` module, or its default export. */
|
|
40
52
|
element: CustomElementConstructor | {
|
|
41
53
|
readonly default: CustomElementConstructor;
|
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* define the ones content names under another base tag
|
|
12
12
|
* (`defineAuthoredPreloadedTags`) and assert them (`assertRegistered`).
|
|
13
13
|
*
|
|
14
|
+
* pie-elements-ng's `./browser/*` modules export `piePackage`, the
|
|
15
|
+
* `{ name, version }` of the package they were built from, so an entry can
|
|
16
|
+
* give its modules alone. A `package` or `version` it also gives must match,
|
|
17
|
+
* which catches a registration an upgrade left behind.
|
|
18
|
+
*
|
|
14
19
|
* A hosted player takes models from the server and resolves no controller. A
|
|
15
20
|
* player that is not hosted runs the controller's `model()` in the browser, so
|
|
16
21
|
* it needs the `controller` an entry registers; the item player warns about
|
|
@@ -23,6 +28,7 @@
|
|
|
23
28
|
import { defineCustomElementSafely } from "../pie/custom-element-define.js";
|
|
24
29
|
import { mathAssetOptionsError, setMathAssetOptions, } from "../pie/math-assets.js";
|
|
25
30
|
import { writeRegistryEntry } from "../pie/registry.js";
|
|
31
|
+
import { validateCustomElementTag } from "../pie/tag-names.js";
|
|
26
32
|
import { BundleType, isCustomElementConstructor, Status } from "../pie/types.js";
|
|
27
33
|
import { parsePackageName } from "../pie/utils.js";
|
|
28
34
|
import { toPackageVersionedTag } from "../pie/versioned-tag.js";
|
|
@@ -69,15 +75,24 @@ export function registerPreloadedElements(elements, options = {}) {
|
|
|
69
75
|
function resolveElement(element, index) {
|
|
70
76
|
const at = `[registerPreloadedElements] entry ${index}`;
|
|
71
77
|
if (!element || typeof element !== "object") {
|
|
72
|
-
throw new Error(`${at}: expected {
|
|
78
|
+
throw new Error(`${at}: expected { element, controller?, tag?, package?, version? }`);
|
|
79
|
+
}
|
|
80
|
+
const identity = moduleIdentity(element, at);
|
|
81
|
+
if (!identity && element.package === undefined && element.version === undefined) {
|
|
82
|
+
throw new Error(`${at}: package and version are required for modules that export no piePackage`);
|
|
73
83
|
}
|
|
74
|
-
const
|
|
84
|
+
const packageName = element.package ?? identity?.name;
|
|
85
|
+
const version = element.version ?? identity?.version;
|
|
75
86
|
if (!isBarePackageName(packageName)) {
|
|
76
87
|
throw new Error(`${at}: package must be a bare package name, got ${JSON.stringify(packageName)}`);
|
|
77
88
|
}
|
|
78
89
|
if (typeof version !== "string" || !isExactSemver(version)) {
|
|
79
90
|
throw new Error(`${at} (${packageName}): version must be the installed version, exact, such as 1.2.3; got ${JSON.stringify(version)}`);
|
|
80
91
|
}
|
|
92
|
+
if (identity && (identity.name !== packageName || identity.version !== version)) {
|
|
93
|
+
throw new Error(`${at}: the modules were built from ${identity.name}@${identity.version}, not ${packageName}@${version}`);
|
|
94
|
+
}
|
|
95
|
+
const tag = element.tag ?? packageName.replace(/^@/, "").replace("/", "-");
|
|
81
96
|
if (typeof tag !== "string" || !tag) {
|
|
82
97
|
throw new Error(`${at} (${packageName}): tag must be the base tag to register, got ${JSON.stringify(tag)}`);
|
|
83
98
|
}
|
|
@@ -95,13 +110,33 @@ function resolveElement(element, index) {
|
|
|
95
110
|
}
|
|
96
111
|
}
|
|
97
112
|
const spec = `${packageName}@${version}`;
|
|
98
|
-
|
|
99
|
-
packageName
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
const tagName = validateCustomElementTag(toPackageVersionedTag(tag, spec), element.tag === undefined
|
|
114
|
+
? `${at} (${packageName}) tag derived from the package name; give tag`
|
|
115
|
+
: `${at} (${packageName}) tag`);
|
|
116
|
+
return { packageName, spec, tagName, elementClass: candidate, controller };
|
|
117
|
+
}
|
|
118
|
+
/** The `piePackage` of the entry's modules, which agree when both export one. */
|
|
119
|
+
function moduleIdentity(element, at) {
|
|
120
|
+
const fromElement = readPackageIdentity(element.element, `${at}: element`);
|
|
121
|
+
const fromController = readPackageIdentity(element.controller, `${at}: controller`);
|
|
122
|
+
if (fromElement &&
|
|
123
|
+
fromController &&
|
|
124
|
+
(fromElement.name !== fromController.name ||
|
|
125
|
+
fromElement.version !== fromController.version)) {
|
|
126
|
+
throw new Error(`${at}: the element module is ${fromElement.name}@${fromElement.version}, ` +
|
|
127
|
+
`but the controller module is ${fromController.name}@${fromController.version}`);
|
|
128
|
+
}
|
|
129
|
+
return fromElement ?? fromController;
|
|
130
|
+
}
|
|
131
|
+
function readPackageIdentity(module, at) {
|
|
132
|
+
if (!module || typeof module !== "object" || !("piePackage" in module)) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const identity = module.piePackage;
|
|
136
|
+
if (typeof identity?.name !== "string" || typeof identity.version !== "string") {
|
|
137
|
+
throw new Error(`${at} module's piePackage must be { name, version }`);
|
|
138
|
+
}
|
|
139
|
+
return { name: identity.name, version: identity.version };
|
|
105
140
|
}
|
|
106
141
|
function resolveController(value) {
|
|
107
142
|
if (!value || typeof value !== "object")
|
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";
|