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

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 (86) hide show
  1. package/README.md +88 -75
  2. package/dist/components/vendor/nds/README.md +4 -5
  3. package/dist/config/profile.d.ts +1 -1
  4. package/dist/config/profile.js +1 -1
  5. package/dist/i18n/index.d.ts +1 -1
  6. package/dist/i18n/index.js +1 -1
  7. package/dist/i18n/language-tags.d.ts +9 -1
  8. package/dist/i18n/language-tags.js +11 -1
  9. package/dist/i18n/messages/en-US.d.ts +33 -28
  10. package/dist/i18n/messages/en-US.js +34 -29
  11. package/dist/i18n/messages/nl-NL.d.ts +33 -28
  12. package/dist/i18n/messages/nl-NL.js +34 -29
  13. package/dist/i18n/provider.d.ts +14 -7
  14. package/dist/i18n/provider.js +26 -29
  15. package/dist/i18n/types.d.ts +4 -5
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +0 -1
  18. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +0 -14
  19. package/dist/loader-config.d.ts +0 -2
  20. package/dist/loader-config.js +0 -2
  21. package/dist/loaders/element-loader.js +13 -6
  22. package/dist/loaders/esm-adapter.d.ts +11 -0
  23. package/dist/loaders/esm-adapter.js +74 -45
  24. package/dist/loaders/iife-adapter.d.ts +3 -3
  25. package/dist/loaders/iife-adapter.js +36 -35
  26. package/dist/loaders/index.d.ts +4 -2
  27. package/dist/loaders/index.js +2 -1
  28. package/dist/loaders/mathjax-events.d.ts +22 -0
  29. package/dist/loaders/mathjax-events.js +65 -0
  30. package/dist/loaders/preloaded-registration.d.ts +18 -2
  31. package/dist/loaders/preloaded-registration.js +14 -2
  32. package/dist/media/index.d.ts +4 -6
  33. package/dist/media/index.js +6 -8
  34. package/dist/object/index.d.ts +8 -3
  35. package/dist/object/index.js +8 -6
  36. package/dist/pie/asset-handler.d.ts +0 -17
  37. package/dist/pie/asset-handler.js +1 -1
  38. package/dist/pie/authoring.js +5 -7
  39. package/dist/pie/elements-rendered.d.ts +23 -0
  40. package/dist/pie/elements-rendered.js +96 -0
  41. package/dist/pie/index.d.ts +4 -6
  42. package/dist/pie/index.js +4 -7
  43. package/dist/pie/initialization.d.ts +13 -14
  44. package/dist/pie/initialization.js +61 -75
  45. package/dist/pie/instrumentation-event-bridge.js +23 -1
  46. package/dist/pie/instrumentation-event-map.js +11 -18
  47. package/dist/pie/item-controller.js +4 -0
  48. package/dist/pie/item-session-contract.js +7 -9
  49. package/dist/pie/logger.d.ts +6 -0
  50. package/dist/pie/logger.js +10 -0
  51. package/dist/pie/math-assets.d.ts +48 -0
  52. package/dist/pie/math-assets.js +81 -0
  53. package/dist/pie/resource-monitor.d.ts +34 -32
  54. package/dist/pie/resource-monitor.js +152 -231
  55. package/dist/pie/scoring.d.ts +1 -6
  56. package/dist/pie/scoring.js +3 -8
  57. package/dist/pie/session-commit.js +1 -1
  58. package/dist/pie/stages.d.ts +11 -47
  59. package/dist/pie/stages.js +11 -47
  60. package/dist/pie/updates.js +5 -4
  61. package/dist/pie/utils.d.ts +0 -2
  62. package/dist/pie/utils.js +0 -2
  63. package/dist/security/sanitize-item-markup.d.ts +8 -4
  64. package/dist/security/sanitize-item-markup.js +52 -6
  65. package/dist/security/sanitize-svg-icon.js +2 -2
  66. package/dist/security/wrap-overwide-images.d.ts +1 -1
  67. package/dist/security/wrap-overwide-images.js +20 -1
  68. package/dist/security/wrap-overwide.d.ts +8 -0
  69. package/dist/security/wrap-overwide.js +47 -3
  70. package/dist/timed-media/media-element-source.js +1 -2
  71. package/dist/types/index.d.ts +88 -153
  72. package/dist/types/index.js +2 -2
  73. package/dist/ui/attribute-coercion.d.ts +5 -0
  74. package/dist/ui/attribute-coercion.js +6 -4
  75. package/dist/ui/content-styles.d.ts +6 -10
  76. package/dist/ui/content-styles.js +8 -13
  77. package/dist/ui/first-focusable.d.ts +0 -1
  78. package/dist/ui/first-focusable.js +0 -1
  79. package/dist/ui/focus-trap.js +2 -3
  80. package/dist/ui/scope-css.d.ts +23 -26
  81. package/dist/ui/scope-css.js +23 -26
  82. package/package.json +6 -4
  83. package/dist/loaders/mathjax-version-conflict.d.ts +0 -17
  84. package/dist/loaders/mathjax-version-conflict.js +0 -51
  85. package/dist/pie/stage-tracker.d.ts +0 -50
  86. package/dist/pie/stage-tracker.js +0 -105
@@ -21,7 +21,8 @@ import { BundleType, isCustomElementConstructor, Status, } from "../pie/types.js
21
21
  import { parsePackageName } from "../pie/utils.js";
22
22
  import { AdapterFailure, } from "./element-loader-types.js";
23
23
  import { isExactSemver } from "./element-package-policy.js";
24
- import { forwardMathjaxVersionConflicts } from "./mathjax-version-conflict.js";
24
+ import { forwardMathjaxEvents } from "./mathjax-events.js";
25
+ import { isPlainRecord } from "../object/index.js";
25
26
  export const BUILT_IN_VIEWS = {
26
27
  delivery: { subpath: "", tagSuffix: "" },
27
28
  author: { subpath: "/author", tagSuffix: "-config", fallback: "delivery" },
@@ -41,8 +42,8 @@ export function createEsmBackend(config) {
41
42
  const editorRuntimeVariants = new Map();
42
43
  /** Packages that do not publish the browser ESM exports this backend loads. */
43
44
  const unpublishedPackages = new Map();
44
- /** Windows whose MathJax version conflicts this backend forwards. */
45
- const mathjaxConflictViews = new WeakSet();
45
+ /** Windows whose MathJax events this backend forwards. */
46
+ const mathjaxEventViews = new WeakSet();
46
47
  let sharedDependencyVersions = {};
47
48
  let importer = defaultImporter;
48
49
  let packageMetadataLoader = defaultPackageMetadataLoader;
@@ -82,7 +83,7 @@ export function createEsmBackend(config) {
82
83
  if (moduleResolution === "import-map") {
83
84
  assertImportMapSupported();
84
85
  }
85
- forwardMathjaxVersionConflictsOf(context.doc);
86
+ forwardMathjaxEventsOf(context.doc);
86
87
  const newEntries = {};
87
88
  for (const [tag, pkg] of Object.entries(elements)) {
88
89
  const packageName = parsePackageName(pkg).name;
@@ -123,10 +124,15 @@ export function createEsmBackend(config) {
123
124
  let injectedMap;
124
125
  if (Object.keys(imports).length > 0) {
125
126
  const json = JSON.stringify({ imports }, null, 2);
127
+ // URL resolution maps the shared dependencies and the editor
128
+ // runtime, so it needs import maps too.
126
129
  assertImportMapSupported();
127
130
  injectedMap = injectImportMap(json, context.doc, runtimePlan?.mappedRuntime);
128
131
  importMapObserver?.(json, context.doc);
129
132
  }
133
+ // Recorded before the map settles: a load that starts meanwhile skips
134
+ // these packages, builds on the shared dependency versions this map
135
+ // fixed, and its imports wait for the map in `importModule`.
130
136
  for (const [pkg, variant] of runtimePlan?.served ?? []) {
131
137
  editorRuntimeVariants.set(pkg, variant);
132
138
  }
@@ -152,7 +158,7 @@ export function createEsmBackend(config) {
152
158
  reasons.set(tag, {
153
159
  kind: "define-failed",
154
160
  tag,
155
- cause: err instanceof Error ? err.message : String(err),
161
+ cause: errorMessage(err),
156
162
  });
157
163
  return;
158
164
  }
@@ -189,9 +195,7 @@ export function createEsmBackend(config) {
189
195
  kind: "module-load-failed",
190
196
  tag,
191
197
  specifier: fallbackSpecifier,
192
- cause: fallbackErr instanceof Error
193
- ? fallbackErr.message
194
- : String(fallbackErr),
198
+ cause: errorMessage(fallbackErr),
195
199
  });
196
200
  return;
197
201
  }
@@ -201,7 +205,7 @@ export function createEsmBackend(config) {
201
205
  kind: "module-load-failed",
202
206
  tag,
203
207
  specifier,
204
- cause: err instanceof Error ? err.message : String(err),
208
+ cause: errorMessage(err),
205
209
  });
206
210
  return;
207
211
  }
@@ -225,6 +229,8 @@ export function createEsmBackend(config) {
225
229
  return;
226
230
  }
227
231
  try {
232
+ // `customElements` rejects a constructor already defined under
233
+ // another tag, so each versioned tag gets its own subclass.
228
234
  defineCustomElementSafely(actualTag, class extends ElementClass {
229
235
  }, `element tag for ${packageName}`);
230
236
  }
@@ -232,35 +238,13 @@ export function createEsmBackend(config) {
232
238
  reasons.set(tag, {
233
239
  kind: "define-failed",
234
240
  tag,
235
- cause: err instanceof Error ? err.message : String(err),
241
+ cause: errorMessage(err),
236
242
  });
237
243
  return;
238
244
  }
239
- let controller = null;
240
- if (loadControllers) {
241
- const servedVariant = editorRuntimeVariants.get(packageVersion);
242
- const variantController = servedVariant?.views.controller;
243
- if (servedVariant && variantController) {
244
- try {
245
- const controllerModule = await importModule(cdnProvider.browserViewUrl(packageVersion, variantController), context.doc);
246
- controller = controllerModule?.default ?? controllerModule;
247
- }
248
- catch (err) {
249
- reportEditorRuntimeFallback(packageVersion, servedVariant.declaration, servedVariant.runtime, `its editor-runtime controller failed to load: ${errorMessage(err)}`);
250
- }
251
- }
252
- if (!controller) {
253
- const controllerSpecifier = resolveControllerSpecifier(packageName, packageVersion, moduleResolution, cdnProvider);
254
- try {
255
- const controllerModule = await importModule(controllerSpecifier, context.doc);
256
- controller = controllerModule?.default ?? controllerModule;
257
- }
258
- catch {
259
- // Controllers are best-effort; element registration is what the
260
- // primitive verifies.
261
- }
262
- }
263
- }
245
+ const controller = loadControllers
246
+ ? await importController(packageName, packageVersion, context.doc)
247
+ : null;
264
248
  writeRegistryEntry({
265
249
  package: packageVersion,
266
250
  status: Status.loaded,
@@ -293,6 +277,35 @@ export function createEsmBackend(config) {
293
277
  return undefined;
294
278
  }
295
279
  }
280
+ /**
281
+ * The package's controller: its editor-runtime variant's when one is served
282
+ * and loads, else the package's own. Controllers are best-effort; element
283
+ * registration is what the primitive verifies.
284
+ */
285
+ async function importController(packageName, packageVersion, doc) {
286
+ let controller = null;
287
+ const servedVariant = editorRuntimeVariants.get(packageVersion);
288
+ const variantController = servedVariant?.views.controller;
289
+ if (servedVariant && variantController) {
290
+ try {
291
+ const controllerModule = await importModule(cdnProvider.browserViewUrl(packageVersion, variantController), doc);
292
+ controller = controllerModule?.default ?? controllerModule;
293
+ if (controller)
294
+ return controller;
295
+ }
296
+ catch (err) {
297
+ reportEditorRuntimeFallback(packageVersion, servedVariant.declaration, servedVariant.runtime, `its editor-runtime controller failed to load: ${errorMessage(err)}`);
298
+ }
299
+ }
300
+ const controllerSpecifier = resolveControllerSpecifier(packageName, packageVersion, moduleResolution, cdnProvider);
301
+ try {
302
+ const controllerModule = await importModule(controllerSpecifier, doc);
303
+ return controllerModule?.default ?? controllerModule;
304
+ }
305
+ catch {
306
+ return controller;
307
+ }
308
+ }
296
309
  /**
297
310
  * Imports through the browser, or through es-module-shims once the document
298
311
  * has rejected an import map, after every map a backend added has settled.
@@ -481,12 +494,12 @@ export function createEsmBackend(config) {
481
494
  return undefined;
482
495
  return provider;
483
496
  }
484
- function forwardMathjaxVersionConflictsOf(doc) {
497
+ function forwardMathjaxEventsOf(doc) {
485
498
  const view = doc.defaultView;
486
- if (!view || mathjaxConflictViews.has(view))
499
+ if (!view || mathjaxEventViews.has(view))
487
500
  return;
488
- mathjaxConflictViews.add(view);
489
- forwardMathjaxVersionConflicts(view, getInstrumentationProvider);
501
+ mathjaxEventViews.add(view);
502
+ forwardMathjaxEvents(view, getInstrumentationProvider);
490
503
  }
491
504
  function reportSharedDependencyConflict(attributes, message = `[pie-esm] Shared dependency version conflict resolved for ${String(attributes.dependencyName)}`) {
492
505
  if (typeof console !== "undefined" && console.warn) {
@@ -547,7 +560,8 @@ export function mapEsmViewElements(elements, view = "delivery", viewConfig) {
547
560
  ]));
548
561
  }
549
562
  function defaultImporter(specifier) {
550
- // @vite-ignore — dynamic import resolved at runtime.
563
+ // Host bundlers must leave this import alone: the specifier is a CDN URL or
564
+ // import-map specifier known only at runtime.
551
565
  return import(/* webpackIgnore: true */ /* @vite-ignore */ specifier);
552
566
  }
553
567
  /**
@@ -608,6 +622,24 @@ export function resolveEsmRuntimeSupportUrl(packageVersion, options) {
608
622
  ? `${packageJsonUrl.slice(0, -"package.json".length)}${RUNTIME_SUPPORT_PATH}`
609
623
  : undefined;
610
624
  }
625
+ /** A package the probe below names, so a provider URL ends in its `<package>@<version>` path. */
626
+ const ROOT_PROBE_PACKAGE = "@pie-element/shared-math-rendering-mathjax@0.0.0";
627
+ /**
628
+ * The npm root the ESM CDN the players load elements from serves raw package
629
+ * files under, `<root>/<package>@<version>/<path>`: the base URL for jsDelivr,
630
+ * raw.esm.sh for esm.sh, and for a custom provider whatever its
631
+ * `packageJsonUrl` puts before the package. `undefined` when that URL names
632
+ * no such root.
633
+ */
634
+ export function resolveEsmAssetRoot(options) {
635
+ const cdnBaseUrl = options.cdnBaseUrl.replace(/\/+$/, "");
636
+ const provider = resolveCdnProvider(options.cdnProvider, cdnBaseUrl);
637
+ const suffix = `/${ROOT_PROBE_PACKAGE}/package.json`;
638
+ const packageJsonUrl = provider.packageJsonUrl(ROOT_PROBE_PACKAGE);
639
+ return packageJsonUrl.endsWith(suffix)
640
+ ? packageJsonUrl.slice(0, -suffix.length) || undefined
641
+ : undefined;
642
+ }
611
643
  function resolveCdnProvider(provider, cdnBaseUrl) {
612
644
  if (isEsmCdnProvider(provider))
613
645
  return provider;
@@ -854,9 +886,6 @@ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._~-]*\/)?[a-z0-9][a-z0-9._~-]*$/;
854
886
  /** Slash-separated segments, none starting with a dot. */
855
887
  const BROWSER_VIEW_PATH = /^[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$/;
856
888
  const BARE_SPECIFIER = /^(?:@[A-Za-z0-9_-][A-Za-z0-9._-]*\/)?[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[A-Za-z0-9_-][A-Za-z0-9._-]*)*$/;
857
- function isRecord(value) {
858
- return typeof value === "object" && value !== null && !Array.isArray(value);
859
- }
860
889
  /**
861
890
  * A package's `pie.browserEditorRuntime`: `undefined` when it declares none,
862
891
  * `null` when the declaration is malformed.
@@ -865,7 +894,7 @@ function readEditorRuntimeDeclaration(metadata) {
865
894
  const declared = metadata?.pie?.browserEditorRuntime;
866
895
  if (declared === undefined)
867
896
  return undefined;
868
- if (!isRecord(declared) || !isRecord(declared.views))
897
+ if (!isPlainRecord(declared) || !isPlainRecord(declared.views))
869
898
  return null;
870
899
  const { name, version } = declared;
871
900
  if (typeof name !== "string" || !PACKAGE_NAME.test(name))
@@ -889,7 +918,7 @@ function readEditorRuntimeDeclaration(metadata) {
889
918
  */
890
919
  function readBrowserModules(metadata) {
891
920
  const declared = metadata.pie?.browserModules;
892
- if (!isRecord(declared))
921
+ if (!isPlainRecord(declared))
893
922
  return null;
894
923
  const modules = Object.entries(declared);
895
924
  const valid = modules.length > 0 &&
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IIFE backend adapter for the ElementLoader primitive.
3
3
  *
4
- * Loads PIE elements from IIFE bundles served by the PIE build service,
4
+ * Loads PIE elements from IIFE bundles served by the bundle host,
5
5
  * then registers them with `customElements`. On any per-tag failure
6
6
  * (missing package, non-constructor element class, define failure),
7
7
  * throws `AdapterFailure` with a structured `reasons` map that the
@@ -38,7 +38,7 @@ export type IifeBundleRetryStatus = {
38
38
  };
39
39
  export type IifeBackendConfig = {
40
40
  kind: "iife";
41
- /** Base URL for the PIE bundle service. */
41
+ /** Base URL of the bundle host. */
42
42
  bundleHost: string;
43
43
  /** Bundle variant to request (player / client-player / editor). */
44
44
  bundleType?: BundleType;
@@ -54,7 +54,7 @@ export type IifeBackendConfig = {
54
54
  /**
55
55
  * Retry policy for transient bundle load failures.
56
56
  *
57
- * When the bundle service is building a bundle on demand, the first
57
+ * When the bundle host is building a bundle on demand, the first
58
58
  * few `<script src>` requests can fail. This policy polls until the
59
59
  * bundle is available or `timeoutMs` elapses. Default matches
60
60
  * `DEFAULT_IIFE_BUNDLE_RETRY_CONFIG`.
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * IIFE backend adapter for the ElementLoader primitive.
3
3
  *
4
- * Loads PIE elements from IIFE bundles served by the PIE build service,
4
+ * Loads PIE elements from IIFE bundles served by the bundle host,
5
5
  * then registers them with `customElements`. On any per-tag failure
6
6
  * (missing package, non-constructor element class, define failure),
7
7
  * throws `AdapterFailure` with a structured `reasons` map that the
@@ -76,7 +76,7 @@ export function createIifeBackend(config) {
76
76
  /**
77
77
  * Wrap the single-shot script loader with poll-until-ready retry.
78
78
  *
79
- * The PIE bundle service may return a transient failure while it
79
+ * The bundle host may return a transient failure while it
80
80
  * builds a bundle on demand. Retry with fixed-interval polling until
81
81
  * the bundle is ready or the cumulative deadline elapses. Emits
82
82
  * retry lifecycle events to the host via `onBundleRetryStatus` and to
@@ -111,7 +111,7 @@ export function createIifeBackend(config) {
111
111
  catch (error) {
112
112
  const elapsedMs = Date.now() - startedAt;
113
113
  const remainingMs = timeoutMs - elapsedMs;
114
- const reason = error instanceof Error ? error.message : String(error);
114
+ const reason = errorMessage(error);
115
115
  if (remainingMs <= 0) {
116
116
  const timeoutError = new Error(`IIFE bundle load timed out after ${timeoutMs}ms: ${url}`);
117
117
  config.onBundleRetryStatus?.({
@@ -201,45 +201,33 @@ export function createIifeBackend(config) {
201
201
  return;
202
202
  const conflict = findIifeElementMapConflict(elements);
203
203
  if (conflict) {
204
- const reasons = new Map();
205
- for (const tag of Object.keys(elements)) {
206
- reasons.set(tag, {
207
- kind: "backend-rejected",
208
- tag,
209
- cause: conflict,
210
- });
211
- }
212
- throw new AdapterFailure(reasons);
204
+ throw failureForEveryTag(elements, (tag) => ({
205
+ kind: "backend-rejected",
206
+ tag,
207
+ cause: conflict,
208
+ }));
213
209
  }
214
210
  const bundleUrl = buildBundleUrl(elements, bundleType, config);
215
211
  try {
216
212
  await ensureBundleLoaded(bundleUrl, context.doc);
217
213
  }
218
214
  catch (err) {
219
- const cause = err instanceof Error ? err.message : String(err);
220
- const reasons = new Map();
221
- for (const tag of Object.keys(elements)) {
222
- reasons.set(tag, {
223
- kind: "bundle-load-failed",
224
- tag,
225
- url: bundleUrl,
226
- cause,
227
- });
228
- }
229
- throw new AdapterFailure(reasons);
215
+ const cause = errorMessage(err);
216
+ throw failureForEveryTag(elements, (tag) => ({
217
+ kind: "bundle-load-failed",
218
+ tag,
219
+ url: bundleUrl,
220
+ cause,
221
+ }));
230
222
  }
231
223
  const pieModule = readPieModule();
232
224
  if (!pieModule) {
233
- const reasons = new Map();
234
- for (const tag of Object.keys(elements)) {
235
- reasons.set(tag, {
236
- kind: "bundle-load-failed",
237
- tag,
238
- url: bundleUrl,
239
- cause: "window.pie.default missing after bundle load",
240
- });
241
- }
242
- throw new AdapterFailure(reasons);
225
+ throw failureForEveryTag(elements, (tag) => ({
226
+ kind: "bundle-load-failed",
227
+ tag,
228
+ url: bundleUrl,
229
+ cause: "window.pie.default missing after bundle load",
230
+ }));
243
231
  }
244
232
  const reasons = new Map();
245
233
  const isEditorBundle = bundleType === BundleType.editor;
@@ -282,11 +270,13 @@ export function createIifeBackend(config) {
282
270
  reasons.set(tag, {
283
271
  kind: "define-failed",
284
272
  tag,
285
- cause: err instanceof Error ? err.message : String(err),
273
+ cause: errorMessage(err),
286
274
  });
287
275
  continue;
288
276
  }
289
277
  try {
278
+ // `customElements` rejects a constructor already defined under
279
+ // another tag, so each versioned tag gets its own subclass.
290
280
  defineCustomElementSafely(actualTag, class extends ElementClass {
291
281
  }, `element tag for ${packageName}`);
292
282
  }
@@ -294,7 +284,7 @@ export function createIifeBackend(config) {
294
284
  reasons.set(tag, {
295
285
  kind: "define-failed",
296
286
  tag,
297
- cause: err instanceof Error ? err.message : String(err),
287
+ cause: errorMessage(err),
298
288
  });
299
289
  continue;
300
290
  }
@@ -320,6 +310,17 @@ export function createIifeBackend(config) {
320
310
  };
321
311
  }
322
312
  // ─── Helpers ─────────────────────────────────────────────────────────────────
313
+ /** A failure of the whole request, reported once per tag. */
314
+ function failureForEveryTag(elements, reasonFor) {
315
+ const reasons = new Map();
316
+ for (const tag of Object.keys(elements)) {
317
+ reasons.set(tag, reasonFor(tag));
318
+ }
319
+ return new AdapterFailure(reasons);
320
+ }
321
+ function errorMessage(err) {
322
+ return err instanceof Error ? err.message : String(err);
323
+ }
323
324
  function defaultLoadBundleScript(url, doc) {
324
325
  return new Promise((resolve, reject) => {
325
326
  const script = doc.createElement("script");
@@ -37,12 +37,14 @@ export { assertElementPackagesAllowed, ElementPackagePolicyError, } from "./elem
37
37
  export type { BackendContext, BackendOption, ElementLoaderBackend, ElementTag, EnsureRegisteredOptions, EsmBackendConfig, IifeBackendConfig, RegistrationFailureReason, } from "./element-loader.js";
38
38
  export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
39
39
  export type { BuiltInEsmCdnProviderName, EsmBackend, EsmCdnProvider, EsmCdnProviderName, EsmCdnProviderOption, ViewConfig, } from "./esm-adapter.js";
40
- export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
40
+ export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
41
41
  export type { IifeBackend, IifeBundleRetryStatus, } from "./iife-adapter.js";
42
+ export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
42
43
  export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
43
44
  export { createIifeBackend } from "./iife-adapter.js";
44
45
  export { resolveLoadControllers } from "./controller-loading.js";
45
- export type { PreloadedController, PreloadedElement, } from "./preloaded-registration.js";
46
+ export type { PreloadedController, PreloadedElement, PreloadedRegistrationOptions, } from "./preloaded-registration.js";
47
+ export type { MathAssetOptions } from "../pie/math-assets.js";
46
48
  export { registerPreloadedElements } from "./preloaded-registration.js";
47
49
  export type { PreloadedElementSpecs } from "./preloaded-alignment.js";
48
50
  export { alignPreloadedElementVersions } from "./preloaded-alignment.js";
@@ -33,7 +33,8 @@
33
33
  export { aggregateElements } from "./ElementLoader.js";
34
34
  export { assertElementPackagesAllowed, ElementPackagePolicyError, } from "./element-package-policy.js";
35
35
  export { AdapterFailure, ElementAssertionError, ElementLoaderError, assertRegistered, describeRegistrationFailures, ensureRegistered, } from "./element-loader.js";
36
- export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
36
+ export { BUILT_IN_VIEWS, createEsmBackend, mapEsmViewElements, resolveEsmAssetRoot, resolveEsmRuntimeSupportUrl, } from "./esm-adapter.js";
37
+ export { forwardMathjaxEvents, MATHJAX_NO_ASSET_ROOT_EVENT, MATHJAX_VERSION_CONFLICT_EVENT, } from "./mathjax-events.js";
37
38
  export { DEFAULT_BUNDLE_HOST, DEFAULT_ESM_CDN_URL } from "./defaults.js";
38
39
  export { createIifeBackend } from "./iife-adapter.js";
39
40
  export { resolveLoadControllers } from "./controller-loading.js";
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Forwards the MathJax adapter's page events to instrumentation.
3
+ *
4
+ * Every copy of the MathJax 4 adapter that browser ESM elements render with
5
+ * dispatches these on `window`, once per condition per page:
6
+ * `pie-mathjax-version-conflict` when MathJax 3 runs on the same page — an
7
+ * unsupported page, see
8
+ * docs/item-player/loading-strategies.md#one-mathjax-version-per-page — and
9
+ * `pie-mathjax-no-asset-root` when a copy has nowhere to load MathJax's files
10
+ * from, see docs/item-player/loading-strategies.md#mathjax-assets. The event
11
+ * names are repeated here because players do not depend on the element
12
+ * packages.
13
+ */
14
+ import type { InstrumentationProvider } from "../instrumentation/types.js";
15
+ export declare const MATHJAX_VERSION_CONFLICT_EVENT = "pie-mathjax-version-conflict";
16
+ export declare const MATHJAX_NO_ASSET_ROOT_EVENT = "pie-mathjax-no-asset-root";
17
+ /**
18
+ * Tracks each MathJax event on `target` with the provider `getProvider`
19
+ * returns when it fires, once per provider. Returns the function that stops
20
+ * forwarding.
21
+ */
22
+ export declare function forwardMathjaxEvents(target: EventTarget, getProvider: () => InstrumentationProvider | undefined): () => void;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Forwards the MathJax adapter's page events to instrumentation.
3
+ *
4
+ * Every copy of the MathJax 4 adapter that browser ESM elements render with
5
+ * dispatches these on `window`, once per condition per page:
6
+ * `pie-mathjax-version-conflict` when MathJax 3 runs on the same page — an
7
+ * unsupported page, see
8
+ * docs/item-player/loading-strategies.md#one-mathjax-version-per-page — and
9
+ * `pie-mathjax-no-asset-root` when a copy has nowhere to load MathJax's files
10
+ * from, see docs/item-player/loading-strategies.md#mathjax-assets. The event
11
+ * names are repeated here because players do not depend on the element
12
+ * packages.
13
+ */
14
+ export const MATHJAX_VERSION_CONFLICT_EVENT = "pie-mathjax-version-conflict";
15
+ export const MATHJAX_NO_ASSET_ROOT_EVENT = "pie-mathjax-no-asset-root";
16
+ /** The detail fields each event is tracked with. */
17
+ const TRACKED_FIELDS = {
18
+ [MATHJAX_VERSION_CONFLICT_EVENT]: ["condition", "docsUrl"],
19
+ [MATHJAX_NO_ASSET_ROOT_EVENT]: ["effect", "docsUrl"],
20
+ };
21
+ // Every player on a page listens, and each player bundle carries its own copy
22
+ // of this module, so the providers that tracked an event are kept on the page.
23
+ const TRACKED = Symbol.for("@pie-players/pie-players-shared/mathjax-version-conflict-tracked");
24
+ function providersThatTracked(event) {
25
+ const registry = globalThis;
26
+ registry[TRACKED] ??= new WeakMap();
27
+ let providers = registry[TRACKED].get(event);
28
+ if (!providers) {
29
+ providers = new WeakSet();
30
+ registry[TRACKED].set(event, providers);
31
+ }
32
+ return providers;
33
+ }
34
+ /**
35
+ * Tracks each MathJax event on `target` with the provider `getProvider`
36
+ * returns when it fires, once per provider. Returns the function that stops
37
+ * forwarding.
38
+ */
39
+ export function forwardMathjaxEvents(target, getProvider) {
40
+ const forward = (event) => {
41
+ const provider = getProvider();
42
+ if (!provider)
43
+ return;
44
+ const tracked = providersThatTracked(event);
45
+ if (tracked.has(provider))
46
+ return;
47
+ tracked.add(provider);
48
+ const detail = event.detail;
49
+ const fields = (detail && typeof detail === "object" ? detail : {});
50
+ try {
51
+ provider.trackEvent(event.type, Object.fromEntries(TRACKED_FIELDS[event.type].map((field) => [field, fields[field]])));
52
+ }
53
+ catch {
54
+ // Swallow: instrumentation must never break rendering.
55
+ }
56
+ };
57
+ for (const type of Object.keys(TRACKED_FIELDS)) {
58
+ target.addEventListener(type, forward);
59
+ }
60
+ return () => {
61
+ for (const type of Object.keys(TRACKED_FIELDS)) {
62
+ target.removeEventListener(type, forward);
63
+ }
64
+ };
65
+ }
@@ -15,7 +15,12 @@
15
15
  * player that is not hosted runs the controller's `model()` in the browser, so
16
16
  * it needs the `controller` an entry registers; the item player warns about
17
17
  * each tag it renders without one.
18
+ *
19
+ * Bundled by the host, the elements' copies of the MathJax adapter find no npm
20
+ * root in their module URLs, so the registration also takes where they load
21
+ * MathJax's fonts and speech from (`options.math`): a root, or each file's URL.
18
22
  */
23
+ import { type MathAssetOptions } from "../pie/math-assets.js";
19
24
  /** A controller module: the `model()` a player that is not hosted runs. */
20
25
  export type PreloadedController = {
21
26
  readonly model: (...args: any[]) => unknown;
@@ -43,6 +48,16 @@ export interface PreloadedElement {
43
48
  readonly default: PreloadedController;
44
49
  };
45
50
  }
51
+ export interface PreloadedRegistrationOptions {
52
+ /**
53
+ * Where the elements' MathJax loads its fonts, its speech worker and, on the
54
+ * adapter's npm build, MathJax itself from, and whether math is in the tab
55
+ * order, written to `window['@pie-lib/math-rendering@2'].opts`. With neither
56
+ * an `assetRoot` nor the fonts in `assetUrls`, math renders without web fonts
57
+ * and speech.
58
+ */
59
+ math?: MathAssetOptions;
60
+ }
46
61
  /**
47
62
  * Register host-bundled elements for the `preloaded` strategy.
48
63
  *
@@ -51,6 +66,7 @@ export interface PreloadedElement {
51
66
  * registry entry, which only gains a controller it lacked. Every entry is
52
67
  * validated before any is registered. A package registers at one version per
53
68
  * page, because the players align every authored version of a package to the
54
- * registered one.
69
+ * registered one. `options.math` replaces the page's math asset options it
70
+ * sets, and keeps the others.
55
71
  */
56
- export declare function registerPreloadedElements(elements: readonly PreloadedElement[]): void;
72
+ export declare function registerPreloadedElements(elements: readonly PreloadedElement[], options?: PreloadedRegistrationOptions): void;
@@ -15,8 +15,13 @@
15
15
  * player that is not hosted runs the controller's `model()` in the browser, so
16
16
  * it needs the `controller` an entry registers; the item player warns about
17
17
  * each tag it renders without one.
18
+ *
19
+ * Bundled by the host, the elements' copies of the MathJax adapter find no npm
20
+ * root in their module URLs, so the registration also takes where they load
21
+ * MathJax's fonts and speech from (`options.math`): a root, or each file's URL.
18
22
  */
19
23
  import { defineCustomElementSafely } from "../pie/custom-element-define.js";
24
+ import { mathAssetOptionsError, setMathAssetOptions, } from "../pie/math-assets.js";
20
25
  import { writeRegistryEntry } from "../pie/registry.js";
21
26
  import { BundleType, isCustomElementConstructor, Status } from "../pie/types.js";
22
27
  import { parsePackageName } from "../pie/utils.js";
@@ -31,15 +36,22 @@ import { pickElementClass } from "./esm-adapter.js";
31
36
  * registry entry, which only gains a controller it lacked. Every entry is
32
37
  * validated before any is registered. A package registers at one version per
33
38
  * page, because the players align every authored version of a package to the
34
- * registered one.
39
+ * registered one. `options.math` replaces the page's math asset options it
40
+ * sets, and keeps the others.
35
41
  */
36
- export function registerPreloadedElements(elements) {
42
+ export function registerPreloadedElements(elements, options = {}) {
37
43
  if (typeof window === "undefined" || typeof customElements === "undefined") {
38
44
  throw new Error("[registerPreloadedElements] requires a browser with customElements");
39
45
  }
40
46
  const preloaded = preloadedPackageSpecs();
41
47
  const resolved = elements.map(resolveElement);
42
48
  assertOneVersionPerPackage(resolved, preloaded);
49
+ const mathError = options.math === undefined ? undefined : mathAssetOptionsError(options.math);
50
+ if (mathError) {
51
+ throw new Error(`[registerPreloadedElements] options.math: ${mathError}`);
52
+ }
53
+ if (options.math)
54
+ setMathAssetOptions(options.math);
43
55
  for (const { packageName, spec, tagName, elementClass, controller } of resolved) {
44
56
  defineCustomElementSafely(tagName, class extends elementClass {
45
57
  }, `preloaded element tag for ${packageName}`);
@@ -2,12 +2,10 @@
2
2
  * Validation primitives shared by every catalog card whose payload references
3
3
  * media.
4
4
  *
5
- * Extracted from `sign-language-cards.ts` when `spoken` cards gained a recorded
6
- * audio form: both card types take authored, wire-facing URLs and hand them to a
7
- * media element in the learner's browser, so both need the same scheme
8
- * allow-list and the same "treat as absent, never as partially valid" posture.
9
- * Two copies of a URL allow-list is one copy that gets a fix and one that does
10
- * not.
5
+ * `sign-language` and recorded `spoken` cards both hand authored, wire-facing
6
+ * URLs to a media element in the learner's browser, so both take the same
7
+ * scheme allow-list and the same "treat as absent, never as partially valid"
8
+ * posture from here.
11
9
  *
12
10
  * Lives here, behind its own subpath, so a PIE element can validate authored
13
11
  * media without importing the assessment toolkit, whose root entry declares
@@ -2,12 +2,10 @@
2
2
  * Validation primitives shared by every catalog card whose payload references
3
3
  * media.
4
4
  *
5
- * Extracted from `sign-language-cards.ts` when `spoken` cards gained a recorded
6
- * audio form: both card types take authored, wire-facing URLs and hand them to a
7
- * media element in the learner's browser, so both need the same scheme
8
- * allow-list and the same "treat as absent, never as partially valid" posture.
9
- * Two copies of a URL allow-list is one copy that gets a fix and one that does
10
- * not.
5
+ * `sign-language` and recorded `spoken` cards both hand authored, wire-facing
6
+ * URLs to a media element in the learner's browser, so both take the same
7
+ * scheme allow-list and the same "treat as absent, never as partially valid"
8
+ * posture from here.
11
9
  *
12
10
  * Lives here, behind its own subpath, so a PIE element can validate authored
13
11
  * media without importing the assessment toolkit, whose root entry declares
@@ -21,7 +19,7 @@
21
19
  * Relative and protocol-relative URLs are allowed — host content is commonly
22
20
  * served from the same origin as the player.
23
21
  */
24
- const DISALLOWED_SRC_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
22
+ const URL_SCHEME_PREFIX = /^[a-z][a-z0-9+.-]*:/i;
25
23
  const ALLOWED_SRC_SCHEMES = new Set(["http:", "https:", "data:", "blob:"]);
26
24
  /**
27
25
  * The only `MediaAssetRef.version` this build renders.
@@ -53,7 +51,7 @@ export function isSafeMediaSrc(raw) {
53
51
  return false;
54
52
  // Relative ("/video.mp4", "video.mp4") and protocol-relative ("//cdn/x.mp4")
55
53
  // forms carry no scheme to check and inherit the document's.
56
- if (src.startsWith("//") || !DISALLOWED_SRC_SCHEME.test(src))
54
+ if (src.startsWith("//") || !URL_SCHEME_PREFIX.test(src))
57
55
  return true;
58
56
  const scheme = src.slice(0, src.indexOf(":") + 1).toLowerCase();
59
57
  return ALLOWED_SRC_SCHEMES.has(scheme);