@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.
Files changed (43) hide show
  1. package/dist/i18n/catalogs.d.ts +5 -0
  2. package/dist/i18n/catalogs.js +9 -0
  3. package/dist/i18n/content-language.d.ts +35 -0
  4. package/dist/i18n/content-language.js +51 -0
  5. package/dist/i18n/index.d.ts +6 -2
  6. package/dist/i18n/index.js +5 -2
  7. package/dist/i18n/language-tags.d.ts +9 -0
  8. package/dist/i18n/language-tags.js +21 -0
  9. package/dist/i18n/locale-load-error.d.ts +34 -0
  10. package/dist/i18n/locale-load-error.js +29 -0
  11. package/dist/i18n/messages/en-US.d.ts +113 -3
  12. package/dist/i18n/messages/en-US.js +128 -3
  13. package/dist/i18n/messages/nl-NL.d.ts +103 -2
  14. package/dist/i18n/messages/nl-NL.js +109 -8
  15. package/dist/i18n/provider.d.ts +20 -1
  16. package/dist/i18n/provider.js +71 -31
  17. package/dist/i18n/pseudo.d.ts +34 -0
  18. package/dist/i18n/pseudo.js +106 -0
  19. package/dist/loaders/esm-adapter.d.ts +10 -0
  20. package/dist/loaders/esm-adapter.js +88 -15
  21. package/dist/loaders/index.d.ts +1 -1
  22. package/dist/loaders/index.js +1 -1
  23. package/dist/loaders/preloaded-registration.d.ts +18 -6
  24. package/dist/loaders/preloaded-registration.js +44 -9
  25. package/dist/pie/index.d.ts +1 -0
  26. package/dist/pie/math-rendering.d.ts +13 -1
  27. package/dist/pie/math-rendering.js +54 -1
  28. package/dist/security/overwide-regions.d.ts +20 -0
  29. package/dist/security/overwide-regions.js +65 -0
  30. package/dist/security/wrap-overwide-images.d.ts +6 -2
  31. package/dist/security/wrap-overwide-images.js +5 -2
  32. package/dist/security/wrap-overwide-tables.d.ts +6 -2
  33. package/dist/security/wrap-overwide-tables.js +5 -2
  34. package/dist/security/wrap-overwide.d.ts +13 -0
  35. package/dist/security/wrap-overwide.js +68 -3
  36. package/dist/types/index.d.ts +12 -0
  37. package/dist/ui/focus-trap.d.ts +12 -0
  38. package/dist/ui/focus-trap.js +39 -1
  39. package/dist/ui/overlay-containment.d.ts +13 -0
  40. package/dist/ui/overlay-containment.js +23 -0
  41. package/dist/ui/overlay-placement.d.ts +28 -0
  42. package/dist/ui/overlay-placement.js +17 -2
  43. package/package.json +4 -4
@@ -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 = toLanguageTag(config.fallbackLocale) || DEFAULT_LOCALE;
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
- function interpolate(text, values) {
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
- return value === undefined || value === null ? match : String(value);
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
- getLocale: () => provider.getLocale(),
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 json = JSON.stringify({ imports }, null, 2);
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(imports, injectedMap, context.doc);
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(imports, script, doc) {
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({ imports });
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({ imports: documentImports(doc) });
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
- return {
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 mapped = JSON.parse(script.textContent || "{}")?.imports;
1057
- if (!mapped || typeof mapped !== "object")
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 [specifier, url] of Object.entries(mapped)) {
1060
- if (typeof url === "string" && !(specifier in imports)) {
1061
- imports[specifier] = url;
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
@@ -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";
@@ -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
- tag: string;
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 { tag, package, version, element }`);
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 { package: packageName, version, tag } = element;
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
- return {
99
- packageName,
100
- spec,
101
- tagName: toPackageVersionedTag(tag, spec),
102
- elementClass: candidate,
103
- controller,
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")
@@ -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";