@pie-players/pie-players-shared 1.0.0-beta.0 → 1.0.0-beta.10

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 (122) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -17
  3. package/dist/THIRD-PARTY-LICENSES.txt +51 -0
  4. package/dist/components/vendor/nds/README.md +43 -5
  5. package/dist/components/vendor/nds/nds-icon-button.d.ts +2 -2
  6. package/dist/components/vendor/nds/nds-icon-button.js +2 -2
  7. package/dist/i18n/catalogs.d.ts +25 -13
  8. package/dist/i18n/catalogs.js +30 -22
  9. package/dist/i18n/content-language.d.ts +77 -0
  10. package/dist/i18n/content-language.js +129 -0
  11. package/dist/i18n/index.d.ts +7 -2
  12. package/dist/i18n/index.js +5 -2
  13. package/dist/i18n/language-tags.d.ts +3 -55
  14. package/dist/i18n/language-tags.js +3 -90
  15. package/dist/i18n/locale-load-error.d.ts +35 -0
  16. package/dist/i18n/locale-load-error.js +30 -0
  17. package/dist/i18n/messages/en-US.d.ts +151 -7
  18. package/dist/i18n/messages/en-US.js +168 -7
  19. package/dist/i18n/provider.d.ts +122 -18
  20. package/dist/i18n/provider.js +315 -133
  21. package/dist/i18n/pseudo.d.ts +34 -0
  22. package/dist/i18n/pseudo.js +106 -0
  23. package/dist/i18n/types.d.ts +19 -3
  24. package/dist/index.d.ts +16 -20
  25. package/dist/index.js +19 -19
  26. package/dist/instrumentation/providers/CompositeInstrumentationProvider.js +2 -1
  27. package/dist/item-session-control/index.d.ts +16 -0
  28. package/dist/item-session-control/index.js +5 -0
  29. package/dist/{formative → item-session-control}/mastery.d.ts +5 -5
  30. package/dist/{formative → item-session-control}/mastery.js +3 -3
  31. package/dist/{formative → item-session-control}/outcome.d.ts +3 -3
  32. package/dist/{formative → item-session-control}/outcome.js +2 -2
  33. package/dist/{formative → item-session-control}/policy.d.ts +11 -11
  34. package/dist/{formative → item-session-control}/policy.js +12 -12
  35. package/dist/item-session-control/session.d.ts +19 -0
  36. package/dist/{formative → item-session-control}/session.js +12 -12
  37. package/dist/{formative → item-session-control}/state.d.ts +26 -26
  38. package/dist/{formative → item-session-control}/state.js +13 -13
  39. package/dist/{formative → item-session-control}/types.d.ts +38 -40
  40. package/dist/{formative → item-session-control}/types.js +2 -2
  41. package/dist/loaders/ElementLoader.d.ts +1 -1
  42. package/dist/loaders/element-loader.d.ts +4 -1
  43. package/dist/loaders/element-loader.js +74 -21
  44. package/dist/loaders/element-package-spec.d.ts +16 -0
  45. package/dist/loaders/element-package-spec.js +30 -0
  46. package/dist/loaders/esm-adapter.d.ts +12 -1
  47. package/dist/loaders/esm-adapter.js +113 -19
  48. package/dist/loaders/iife-adapter.d.ts +7 -0
  49. package/dist/loaders/iife-adapter.js +1 -1
  50. package/dist/loaders/index.d.ts +2 -1
  51. package/dist/loaders/index.js +2 -1
  52. package/dist/loaders/preloaded-registration.d.ts +18 -6
  53. package/dist/loaders/preloaded-registration.js +44 -9
  54. package/dist/object/index.d.ts +4 -1
  55. package/dist/object/index.js +8 -1
  56. package/dist/pie/config.d.ts +8 -0
  57. package/dist/pie/config.js +32 -1
  58. package/dist/pie/index.d.ts +2 -0
  59. package/dist/pie/index.js +1 -0
  60. package/dist/pie/math-rendering.d.ts +13 -1
  61. package/dist/pie/math-rendering.js +54 -1
  62. package/dist/pie/updates.d.ts +4 -1
  63. package/dist/pie/updates.js +21 -5
  64. package/dist/pie/versioned-tag.d.ts +10 -0
  65. package/dist/pie/versioned-tag.js +27 -0
  66. package/dist/promise/index.d.ts +10 -0
  67. package/dist/promise/index.js +16 -0
  68. package/dist/security/overwide-regions.js +65 -0
  69. package/dist/security/private-hosts.d.ts +27 -0
  70. package/dist/security/private-hosts.js +85 -0
  71. package/dist/security/validate-style-url.js +2 -2
  72. package/dist/security/wrap-overwide-images.d.ts +6 -2
  73. package/dist/security/wrap-overwide-images.js +5 -2
  74. package/dist/security/wrap-overwide-tables.d.ts +6 -2
  75. package/dist/security/wrap-overwide-tables.js +5 -2
  76. package/dist/security/wrap-overwide.d.ts +13 -0
  77. package/dist/security/wrap-overwide.js +68 -3
  78. package/dist/timed-media/index.d.ts +1 -1
  79. package/dist/timed-media/index.js +1 -1
  80. package/dist/timed-media/policy.d.ts +2 -2
  81. package/dist/timed-media/policy.js +2 -2
  82. package/dist/timed-media/session.d.ts +1 -1
  83. package/dist/timed-media/session.js +1 -1
  84. package/dist/timed-media/types.d.ts +11 -11
  85. package/dist/tools/term-lookup.d.ts +15 -0
  86. package/dist/tools/term-lookup.js +16 -0
  87. package/dist/types/index.d.ts +127 -47
  88. package/dist/types/interchange.d.ts +116 -0
  89. package/dist/types/interchange.js +1 -0
  90. package/dist/ui/content-styles-entry.d.ts +1 -0
  91. package/dist/ui/content-styles-entry.js +2 -0
  92. package/dist/ui/content-styles.d.ts +24 -3
  93. package/dist/ui/content-styles.js +114 -3
  94. package/dist/ui/focus-trap.d.ts +12 -0
  95. package/dist/ui/focus-trap.js +41 -1
  96. package/dist/ui/fontawesome.d.ts +51 -0
  97. package/dist/ui/fontawesome.js +122 -0
  98. package/dist/ui/ime.d.ts +6 -0
  99. package/dist/ui/ime.js +10 -0
  100. package/dist/ui/overlay-containment.d.ts +13 -0
  101. package/dist/ui/overlay-containment.js +23 -0
  102. package/dist/ui/overlay-placement.d.ts +28 -0
  103. package/dist/ui/overlay-placement.js +17 -2
  104. package/package.json +72 -36
  105. package/dist/formative/index.d.ts +0 -16
  106. package/dist/formative/index.js +0 -5
  107. package/dist/formative/session.d.ts +0 -19
  108. package/dist/i18n/messages/nl-NL.d.ts +0 -529
  109. package/dist/i18n/messages/nl-NL.js +0 -529
  110. package/dist/instrumentation/new-relic-agent.d.ts +0 -20
  111. package/dist/loaders/module-shim.d.ts +0 -12
  112. package/dist/pie/authoring-tag.d.ts +0 -6
  113. package/dist/pie/configure-initialization.d.ts +0 -21
  114. package/dist/pie/correct-response-env.d.ts +0 -3
  115. package/dist/pie/element-announcements.d.ts +0 -39
  116. package/dist/pie/elements-rendered.d.ts +0 -23
  117. package/dist/pie/initialize-element.d.ts +0 -26
  118. package/dist/pie/markup-math.d.ts +0 -20
  119. package/dist/pie/resource-monitor.d.ts +0 -217
  120. package/dist/security/sanitize-forbidden-lists.d.ts +0 -8
  121. package/dist/ui/pointer-rotate.d.ts +0 -29
  122. package/dist/ui/safe-storage.d.ts +0 -2
@@ -19,8 +19,10 @@ 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";
25
+ import { invalidPackageSpecMessage, isScopedPackageSpec, } from "./element-package-spec.js";
24
26
  import { forwardMathjaxEvents } from "./mathjax-events.js";
25
27
  import { isPlainRecord } from "../object/index.js";
26
28
  export const BUILT_IN_VIEWS = {
@@ -80,12 +82,30 @@ export function createEsmBackend(config) {
80
82
  async function load(elements, context) {
81
83
  if (!elements || Object.keys(elements).length === 0)
82
84
  return;
85
+ // Refused before any URL is built from the spec; the other elements load.
86
+ const reasons = new Map();
87
+ const loadable = {};
88
+ for (const [tag, pkg] of Object.entries(elements)) {
89
+ if (isScopedPackageSpec(pkg)) {
90
+ loadable[tag] = pkg;
91
+ }
92
+ else {
93
+ reasons.set(tag, {
94
+ kind: "backend-rejected",
95
+ tag,
96
+ cause: invalidPackageSpecMessage(pkg),
97
+ });
98
+ }
99
+ }
100
+ if (Object.keys(loadable).length === 0) {
101
+ throw new AdapterFailure(reasons);
102
+ }
83
103
  if (moduleResolution === "import-map") {
84
104
  assertImportMapSupported();
85
105
  }
86
106
  forwardMathjaxEventsOf(context.doc);
87
107
  const newEntries = {};
88
- for (const [tag, pkg] of Object.entries(elements)) {
108
+ for (const [tag, pkg] of Object.entries(loadable)) {
89
109
  const packageName = parsePackageName(pkg).name;
90
110
  if (moduleResolution === "import-map") {
91
111
  const existingVersion = importMappedPackageVersions.get(packageName);
@@ -122,8 +142,11 @@ export function createEsmBackend(config) {
122
142
  : undefined;
123
143
  const imports = withoutMappedSpecifiers({ ...importMapResult.imports, ...runtimePlan?.imports }, context.doc);
124
144
  let injectedMap;
145
+ let map;
125
146
  if (Object.keys(imports).length > 0) {
126
- 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);
127
150
  // URL resolution maps the shared dependencies and the editor
128
151
  // runtime, so it needs import maps too.
129
152
  assertImportMapSupported();
@@ -143,12 +166,11 @@ export function createEsmBackend(config) {
143
166
  importMappedPackageVersions.set(parsePackageName(pkg).name, pkg);
144
167
  }
145
168
  }
146
- if (injectedMap) {
147
- await applyImportMap(imports, injectedMap, context.doc);
169
+ if (injectedMap && map) {
170
+ await applyImportMap(map, injectedMap, context.doc);
148
171
  }
149
172
  }
150
- const reasons = new Map();
151
- await Promise.all(Object.entries(elements).map(async ([tag, packageVersion]) => {
173
+ await Promise.all(Object.entries(loadable).map(async ([tag, packageVersion]) => {
152
174
  const packageName = parsePackageName(packageVersion).name;
153
175
  let actualTag;
154
176
  try {
@@ -321,13 +343,13 @@ export function createEsmBackend(config) {
321
343
  * document that already uses it, and otherwise the browser has either
322
344
  * applied it or rejected it.
323
345
  */
324
- async function applyImportMap(imports, script, doc) {
346
+ async function applyImportMap(map, script, doc) {
325
347
  let state = "applied";
326
348
  try {
327
349
  if (script.type === SHIM_IMPORT_MAP_TYPE) {
328
- (await importShim(doc)).addImportMap({ imports });
350
+ (await importShim(doc)).addImportMap(map);
329
351
  }
330
- else if (await importMapRejected(imports)) {
352
+ else if (await importMapRejected(map.imports)) {
331
353
  state = "rejected";
332
354
  await importShim(doc);
333
355
  }
@@ -366,7 +388,7 @@ export function createEsmBackend(config) {
366
388
  */
367
389
  function importShim(doc) {
368
390
  importShimRequest ??= importShimLoader().then((shim) => {
369
- shim.addImportMap({ imports: documentImports(doc) });
391
+ shim.addImportMap(documentImportMap(doc));
370
392
  return shim;
371
393
  }, (err) => {
372
394
  importShimRequest = undefined;
@@ -609,9 +631,12 @@ async function fetchPackageMetadata(packageJsonUrl) {
609
631
  const RUNTIME_SUPPORT_PATH = "dist/runtime-support.js";
610
632
  /**
611
633
  * URL of a package's `./runtime-support` module on the ESM CDN the players load
612
- * elements from, or `undefined` when a custom provider names no package root.
634
+ * elements from, or `undefined` when a custom provider names no package root or
635
+ * `packageVersion` is not a scoped package spec.
613
636
  */
614
637
  export function resolveEsmRuntimeSupportUrl(packageVersion, options) {
638
+ if (!isScopedPackageSpec(packageVersion))
639
+ return undefined;
615
640
  const cdnBaseUrl = options.cdnBaseUrl.replace(/\/+$/, "");
616
641
  const provider = resolveCdnProvider(options.cdnProvider, cdnBaseUrl);
617
642
  if (typeof provider.runtimeSupportUrl === "function") {
@@ -640,6 +665,24 @@ export function resolveEsmAssetRoot(options) {
640
665
  ? packageJsonUrl.slice(0, -suffix.length) || undefined
641
666
  : undefined;
642
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
+ }
643
686
  function resolveCdnProvider(provider, cdnBaseUrl) {
644
687
  if (isEsmCdnProvider(provider))
645
688
  return provider;
@@ -660,7 +703,7 @@ function isEsmCdnProvider(value) {
660
703
  typeof candidate.sharedDependencyUrl === "function");
661
704
  }
662
705
  function createJsDelivrProvider(cdnBaseUrl, name = "jsdelivr") {
663
- return {
706
+ const provider = {
664
707
  name,
665
708
  packageJsonUrl: (packageVersion) => `${cdnBaseUrl}/${packageVersion}/package.json`,
666
709
  browserViewUrl: (packageVersion, view) => `${cdnBaseUrl}/${packageVersion}/dist/browser/${view}/index.js`,
@@ -671,7 +714,31 @@ function createJsDelivrProvider(cdnBaseUrl, name = "jsdelivr") {
671
714
  return `${cdnBaseUrl}/${dependencyName}@${version}${suffix}`;
672
715
  },
673
716
  };
717
+ jsDelivrShapedProviders.add(provider);
718
+ return provider;
674
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();
675
742
  function createEsmShProvider(cdnBaseUrl) {
676
743
  const esmBaseUrl = cdnBaseUrl.replace(/\/+$/, "");
677
744
  const rawBaseUrl = toRawEsmShBaseUrl(esmBaseUrl);
@@ -1029,23 +1096,50 @@ async function importMapsSettled(doc) {
1029
1096
  * for a specifier as it does in the browser.
1030
1097
  */
1031
1098
  function documentImports(doc) {
1099
+ return documentImportMap(doc).imports;
1100
+ }
1101
+ /** {@link documentImports}, with the scopes the maps define. */
1102
+ function documentImportMap(doc) {
1032
1103
  const imports = Object.create(null);
1104
+ const scopes = Object.create(null);
1033
1105
  for (const script of importMapScripts(doc)) {
1034
1106
  try {
1035
- const mapped = JSON.parse(script.textContent || "{}")?.imports;
1036
- 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")
1037
1110
  continue;
1038
- for (const [specifier, url] of Object.entries(mapped)) {
1039
- if (typeof url === "string" && !(specifier in imports)) {
1040
- imports[specifier] = url;
1041
- }
1111
+ for (const [scope, rules] of Object.entries(map.scopes)) {
1112
+ scopes[scope] ??= Object.create(null);
1113
+ addFirstRules(scopes[scope], rules);
1042
1114
  }
1043
1115
  }
1044
1116
  catch {
1045
1117
  // The browser rejects a map it cannot parse, so it maps nothing.
1046
1118
  }
1047
1119
  }
1048
- 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;
1049
1143
  }
1050
1144
  /**
1051
1145
  * es-module-shims in shim mode: it resolves through the import maps this
@@ -21,6 +21,7 @@
21
21
  import type { InstrumentationProvider } from "../instrumentation/types.js";
22
22
  import { type IifeBundleRetryConfig } from "../loader-config.js";
23
23
  import { BundleType } from "../pie/types.js";
24
+ import type { ElementMap } from "./ElementLoader.js";
24
25
  import { type ElementLoaderBackend } from "./element-loader-types.js";
25
26
  /**
26
27
  * Bundle-build retry status emitted while a bundle is being built or after
@@ -77,4 +78,10 @@ export type IifeBackendConfig = {
77
78
  instrumentationProvider?: InstrumentationProvider;
78
79
  };
79
80
  export type IifeBackend = ElementLoaderBackend & {};
81
+ /**
82
+ * The legacy IIFE bundle global is keyed by unversioned package name. Two
83
+ * distinct specs for one package therefore cannot coexist in a single bundle:
84
+ * one constructor would silently be reused for both requested versioned tags.
85
+ */
86
+ export declare function findIifeElementMapConflict(elements: ElementMap): string | undefined;
80
87
  export declare function createIifeBackend(config: IifeBackendConfig): IifeBackend;
@@ -31,7 +31,7 @@ import { AdapterFailure, } from "./element-loader-types.js";
31
31
  * distinct specs for one package therefore cannot coexist in a single bundle:
32
32
  * one constructor would silently be reused for both requested versioned tags.
33
33
  */
34
- function findIifeElementMapConflict(elements) {
34
+ export function findIifeElementMapConflict(elements) {
35
35
  const specsByPackageName = new Map();
36
36
  for (const packageSpecValue of Object.values(elements)) {
37
37
  const packageSpec = String(packageSpecValue);
@@ -34,10 +34,11 @@ export type { ElementMap } from "./ElementLoader.js";
34
34
  export { aggregateElements } from "./ElementLoader.js";
35
35
  export type { ElementPackagePolicy } from "./element-package-policy.js";
36
36
  export { assertElementPackagesAllowed, ElementPackagePolicyError, } from "./element-package-policy.js";
37
+ export { isScopedPackageSpec } from "./element-package-spec.js";
37
38
  export type { BackendContext, BackendOption, ElementLoaderBackend, ElementTag, EnsureRegisteredOptions, EsmBackendConfig, IifeBackendConfig, RegistrationFailureReason, } from "./element-loader.js";
38
39
  export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
39
40
  export type { BuiltInEsmCdnProviderName, EsmBackend, EsmCdnProvider, EsmCdnProviderName, EsmCdnProviderOption, ViewConfig, } from "./esm-adapter.js";
40
- 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";
41
42
  export type { IifeBackend, IifeBundleRetryStatus, } from "./iife-adapter.js";
42
43
  export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
43
44
  export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
@@ -32,8 +32,9 @@
32
32
  */
33
33
  export { aggregateElements } from "./ElementLoader.js";
34
34
  export { assertElementPackagesAllowed, ElementPackagePolicyError, } from "./element-package-policy.js";
35
+ export { isScopedPackageSpec } from "./element-package-spec.js";
35
36
  export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
36
- 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";
37
38
  export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
38
39
  export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
39
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")
@@ -6,7 +6,10 @@
6
6
  */
7
7
  export declare const mergeObjectsIgnoringNullUndefined: <T extends object>(...objects: T[]) => T;
8
8
  /**
9
- * Deep clone an object.
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
  /**
@@ -17,7 +17,10 @@ export const mergeObjectsIgnoringNullUndefined = (...objects) => {
17
17
  }, {});
18
18
  };
19
19
  /**
20
- * Deep clone an object.
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)) {
@@ -8,6 +8,14 @@ export type PieConfigContractValidationResult = {
8
8
  valid: boolean;
9
9
  errors: string[];
10
10
  warnings: string[];
11
+ /**
12
+ * Id problems that can bind a model, markup element or session entry to the
13
+ * wrong partner. They do not invalidate the config, since content that
14
+ * carries them has always rendered, but the players report them as errors
15
+ * so they reach instrumentation. Kept apart from `warnings`, which are
16
+ * advisory.
17
+ */
18
+ bindingWarnings: string[];
11
19
  };
12
20
  export declare const validatePieConfigContract: (config: unknown) => PieConfigContractValidationResult;
13
21
  export declare const assertPieConfigContract: (config: unknown) => void;
@@ -6,6 +6,16 @@
6
6
  import { cloneDeep } from "../object/index.js";
7
7
  import { parsePackageName } from "./utils.js";
8
8
  import { parseVersionedTagName, toPackageVersionedTag, } from "./versioned-tag.js";
9
+ const MARKUP_ELEMENT_ID_REGEX = /<([a-zA-Z][a-zA-Z0-9._]*-[a-zA-Z0-9._-]*)\b[^>]*?\sid\s*=\s*(["'])(.*?)\2/g;
10
+ /** How many PIE elements in the markup carry each `id`. */
11
+ function countMarkupElementIds(markup) {
12
+ const counts = new Map();
13
+ for (const match of markup.matchAll(MARKUP_ELEMENT_ID_REGEX)) {
14
+ const id = match[3] ?? "";
15
+ counts.set(id, (counts.get(id) ?? 0) + 1);
16
+ }
17
+ return counts;
18
+ }
9
19
  function collectMarkupElementTags(markup) {
10
20
  const tags = new Set();
11
21
  const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9._-]*)(?=[\t\n\f\r />])/g;
@@ -24,11 +34,13 @@ function collectMarkupElementTags(markup) {
24
34
  export const validatePieConfigContract = (config) => {
25
35
  const errors = [];
26
36
  const warnings = [];
37
+ const bindingWarnings = [];
27
38
  if (!config || typeof config !== "object" || Array.isArray(config)) {
28
39
  return {
29
40
  valid: false,
30
41
  errors: ["Config must be an object."],
31
42
  warnings,
43
+ bindingWarnings,
32
44
  };
33
45
  }
34
46
  const cfg = config;
@@ -45,7 +57,7 @@ export const validatePieConfigContract = (config) => {
45
57
  errors.push("`models` must be an array.");
46
58
  }
47
59
  if (errors.length > 0) {
48
- return { valid: false, errors, warnings };
60
+ return { valid: false, errors, warnings, bindingWarnings };
49
61
  }
50
62
  const elementMap = elements;
51
63
  const modelList = models;
@@ -66,8 +78,21 @@ export const validatePieConfigContract = (config) => {
66
78
  errors.push(`Element "${tagName}" has invalid package spec "${packageSpec}".`);
67
79
  }
68
80
  }
81
+ // Ids bind models, markup elements and session entries one-to-one, and each
82
+ // binding takes the first match.
69
83
  const modelElementTags = new Set();
84
+ const modelIds = new Set();
70
85
  for (const [index, model] of modelList.entries()) {
86
+ const modelId = model?.id;
87
+ if (typeof modelId !== "string" || modelId.trim().length === 0) {
88
+ bindingWarnings.push(`Model at index ${index} has no valid "id", so no markup element binds to it.`);
89
+ }
90
+ else if (modelIds.has(modelId)) {
91
+ bindingWarnings.push(`Duplicate model id "${modelId}"; the markup element binds to the first model with it.`);
92
+ }
93
+ else {
94
+ modelIds.add(modelId);
95
+ }
71
96
  const modelElement = model?.element;
72
97
  if (typeof modelElement !== "string" || modelElement.trim().length === 0) {
73
98
  errors.push(`Model at index ${index} is missing a valid "element" reference.`);
@@ -78,6 +103,11 @@ export const validatePieConfigContract = (config) => {
78
103
  errors.push(`Model element "${modelElement}" is not declared in \`elements\`.`);
79
104
  }
80
105
  }
106
+ for (const [id, count] of countMarkupElementIds(markup)) {
107
+ if (count > 1) {
108
+ bindingWarnings.push(`${count} markup elements share id "${id}"; they bind to one model and one session entry.`);
109
+ }
110
+ }
81
111
  const markupElementTags = collectMarkupElementTags(markup);
82
112
  for (const tagName of markupElementTags) {
83
113
  if (!(tagName in elementMap)) {
@@ -97,6 +127,7 @@ export const validatePieConfigContract = (config) => {
97
127
  valid: errors.length === 0,
98
128
  errors,
99
129
  warnings,
130
+ bindingWarnings,
100
131
  };
101
132
  };
102
133
  export const assertPieConfigContract = (config) => {
@@ -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";
@@ -41,6 +42,7 @@ export { BundleType, defaultPieElementOptions, isCustomElementConstructor, isPie
41
42
  export { attemptCustomElementDefine, defineCustomElementSafely, type AttemptCustomElementDefineOptions, type SafeDefineAttempt, type SafeDefineOutcome, type SafeDefineResult, type SafeDefineStatus, } from "./custom-element-define.js";
42
43
  export { toPrintHashedTag, toViewTag, validateCustomElementTag, VIEW_TAG_SUFFIX, } from "./tag-names.js";
43
44
  export type { PieViewMode } from "./tag-names.js";
45
+ export { runtimeElementTags } from "./versioned-tag.js";
44
46
  export { updatePieElement, updatePieElements, updatePieElementWithRef, } from "./updates.js";
45
47
  export type { ElementOverrides } from "./overrides.js";
46
48
  export { addOrUpdateOverrideInUrl, applyElementOverrides, applyElementVersionOverridesPreserveTags, extractPackageInfo, formatElementOverrideParam, parseElementOverridesFromCurrentUrl, parseElementOverridesFromUrl, } from "./overrides.js";
package/dist/pie/index.js CHANGED
@@ -44,6 +44,7 @@ isPieAvailable, isPieRegistryAvailable, Status, } from "./types.js";
44
44
  // Tag naming helpers
45
45
  export { attemptCustomElementDefine, defineCustomElementSafely, } from "./custom-element-define.js";
46
46
  export { toPrintHashedTag, toViewTag, validateCustomElementTag, VIEW_TAG_SUFFIX, } from "./tag-names.js";
47
+ export { runtimeElementTags } from "./versioned-tag.js";
47
48
  // Updates
48
49
  export { updatePieElement, updatePieElements, updatePieElementWithRef, } from "./updates.js";
49
50
  export { addOrUpdateOverrideInUrl, applyElementOverrides, applyElementVersionOverridesPreserveTags, extractPackageInfo, formatElementOverrideParam, parseElementOverridesFromCurrentUrl, parseElementOverridesFromUrl, } from "./overrides.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
  *