@pie-players/pie-players-shared 0.3.74 → 0.3.75

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 (42) hide show
  1. package/README.md +3 -1
  2. package/dist/i18n/messages/en-US.d.ts +1 -24
  3. package/dist/i18n/messages/en-US.js +1 -24
  4. package/dist/i18n/messages/nl-NL.d.ts +1 -24
  5. package/dist/i18n/messages/nl-NL.js +1 -24
  6. package/dist/i18n/types.d.ts +4 -5
  7. package/dist/index.d.ts +1 -1
  8. package/dist/loaders/esm-adapter.d.ts +11 -0
  9. package/dist/loaders/esm-adapter.js +26 -8
  10. package/dist/loaders/index.d.ts +4 -2
  11. package/dist/loaders/index.js +2 -1
  12. package/dist/loaders/mathjax-events.d.ts +22 -0
  13. package/dist/loaders/mathjax-events.js +65 -0
  14. package/dist/loaders/preloaded-registration.d.ts +18 -2
  15. package/dist/loaders/preloaded-registration.js +14 -2
  16. package/dist/pie/elements-rendered.d.ts +25 -0
  17. package/dist/pie/elements-rendered.js +96 -0
  18. package/dist/pie/index.d.ts +4 -6
  19. package/dist/pie/index.js +4 -7
  20. package/dist/pie/instrumentation-event-bridge.js +23 -1
  21. package/dist/pie/instrumentation-event-map.js +11 -18
  22. package/dist/pie/item-controller.js +4 -0
  23. package/dist/pie/logger.d.ts +6 -0
  24. package/dist/pie/logger.js +10 -0
  25. package/dist/pie/math-assets.d.ts +48 -0
  26. package/dist/pie/math-assets.js +81 -0
  27. package/dist/pie/scoring.d.ts +1 -6
  28. package/dist/pie/scoring.js +3 -8
  29. package/dist/pie/session-commit.js +1 -1
  30. package/dist/pie/stages.d.ts +10 -47
  31. package/dist/pie/stages.js +10 -47
  32. package/dist/security/sanitize-item-markup.d.ts +4 -0
  33. package/dist/security/sanitize-item-markup.js +48 -2
  34. package/dist/security/wrap-overwide-images.js +19 -0
  35. package/dist/security/wrap-overwide.d.ts +8 -0
  36. package/dist/security/wrap-overwide.js +47 -3
  37. package/dist/types/index.d.ts +37 -112
  38. package/package.json +4 -3
  39. package/dist/loaders/mathjax-version-conflict.d.ts +0 -17
  40. package/dist/loaders/mathjax-version-conflict.js +0 -51
  41. package/dist/pie/stage-tracker.d.ts +0 -50
  42. package/dist/pie/stage-tracker.js +0 -105
package/README.md CHANGED
@@ -128,7 +128,9 @@ jsDelivr is the default npm CDN provider. Hosts can opt into `esm.sh` with
128
128
  Provider names are open-ended: custom/internal providers can pass their own name
129
129
  when they follow the jsDelivr-compatible package-file URL layout, or pass a
130
130
  provider object when package artifacts and shared dependencies need different
131
- route builders.
131
+ route builders. Under `esm` the item player's own MathJax takes the provider's
132
+ package-file root as its asset root when the page sets none; see
133
+ [MathJax assets](../../docs/item-player/loading-strategies.md#mathjax-assets).
132
134
 
133
135
  `dependencies` and `peerDependencies` are not used as fallback runtime contracts.
134
136
  If multiple elements request different minor or patch versions of a shared
@@ -393,30 +393,6 @@ declare const enUS: {
393
393
  textToSpeech: {
394
394
  name: string;
395
395
  description: string;
396
- toolA11y: string;
397
- title: string;
398
- initializing: string;
399
- initFailed: string;
400
- selectText: string;
401
- speed: string;
402
- play: string;
403
- pause: string;
404
- resume: string;
405
- stop: string;
406
- speaking: string;
407
- paused: string;
408
- charactersSelected: {
409
- one: string;
410
- other: string;
411
- };
412
- rate: {
413
- slow: string;
414
- slower: string;
415
- normal: string;
416
- faster: string;
417
- fast: string;
418
- veryFast: string;
419
- };
420
396
  /** Inline reading controls and their live-region announcements. */
421
397
  inline: {
422
398
  controlsA11y: string;
@@ -538,6 +514,7 @@ declare const enUS: {
538
514
  resolvedTools: string;
539
515
  provenanceSummary: string;
540
516
  perToolDecisions: string;
517
+ policyDiagnostics: string;
541
518
  profileReadOnly: string;
542
519
  };
543
520
  /** TTS settings panel. */
@@ -407,30 +407,6 @@ const enUS = {
407
407
  textToSpeech: {
408
408
  name: "Text to Speech",
409
409
  description: "Read content aloud",
410
- toolA11y: "Text-to-speech tool",
411
- title: "Text-to-Speech",
412
- initializing: "Initializing…",
413
- initFailed: "Failed to initialize text-to-speech.",
414
- selectText: "Select text on the page to read it aloud.",
415
- speed: "Speed:",
416
- play: "Play",
417
- pause: "Pause",
418
- resume: "Resume",
419
- stop: "Stop",
420
- speaking: "Speaking…",
421
- paused: "Paused",
422
- charactersSelected: {
423
- one: "{count} character selected",
424
- other: "{count} characters selected",
425
- },
426
- rate: {
427
- slow: "Slow",
428
- slower: "Slower",
429
- normal: "Normal",
430
- faster: "Faster",
431
- fast: "Fast",
432
- veryFast: "Very Fast",
433
- },
434
410
  /** Inline reading controls and their live-region announcements. */
435
411
  inline: {
436
412
  controlsA11y: "Reading controls",
@@ -552,6 +528,7 @@ const enUS = {
552
528
  resolvedTools: "Resolved Tools (toolkit)",
553
529
  provenanceSummary: "Tool Policy Provenance Summary",
554
530
  perToolDecisions: "Per-Tool Decisions",
531
+ policyDiagnostics: "Policy Diagnostics",
555
532
  profileReadOnly: "PNP Profile (read-only)",
556
533
  },
557
534
  /** TTS settings panel. */
@@ -321,30 +321,6 @@ declare const nlNL: {
321
321
  textToSpeech: {
322
322
  name: string;
323
323
  description: string;
324
- toolA11y: string;
325
- title: string;
326
- initializing: string;
327
- initFailed: string;
328
- selectText: string;
329
- speed: string;
330
- play: string;
331
- pause: string;
332
- resume: string;
333
- stop: string;
334
- speaking: string;
335
- paused: string;
336
- charactersSelected: {
337
- one: string;
338
- other: string;
339
- };
340
- rate: {
341
- slow: string;
342
- slower: string;
343
- normal: string;
344
- faster: string;
345
- fast: string;
346
- veryFast: string;
347
- };
348
324
  inline: {
349
325
  controlsA11y: string;
350
326
  playbackSpeedA11y: string;
@@ -450,6 +426,7 @@ declare const nlNL: {
450
426
  resolvedTools: string;
451
427
  provenanceSummary: string;
452
428
  perToolDecisions: string;
429
+ policyDiagnostics: string;
453
430
  profileReadOnly: string;
454
431
  };
455
432
  tts: {
@@ -321,30 +321,6 @@ const nlNL = {
321
321
  textToSpeech: {
322
322
  name: "Voorlezen",
323
323
  description: "Inhoud laten voorlezen",
324
- toolA11y: "Voorleeshulp",
325
- title: "Voorlezen",
326
- initializing: "Wordt gestart…",
327
- initFailed: "Voorlezen kon niet worden gestart.",
328
- selectText: "Selecteer tekst op de pagina om die te laten voorlezen.",
329
- speed: "Snelheid:",
330
- play: "Voorlezen",
331
- pause: "Pauzeren",
332
- resume: "Hervatten",
333
- stop: "Stoppen",
334
- speaking: "Aan het voorlezen…",
335
- paused: "Gepauzeerd",
336
- charactersSelected: {
337
- one: "{count} teken geselecteerd",
338
- other: "{count} tekens geselecteerd",
339
- },
340
- rate: {
341
- slow: "Langzaam",
342
- slower: "Langzamer",
343
- normal: "Normaal",
344
- faster: "Sneller",
345
- fast: "Snel",
346
- veryFast: "Zeer snel",
347
- },
348
324
  inline: {
349
325
  controlsA11y: "Voorleesknoppen",
350
326
  playbackSpeedA11y: "Voorleessnelheid",
@@ -450,6 +426,7 @@ const nlNL = {
450
426
  resolvedTools: "Toegekende hulpmiddelen (toolkit)",
451
427
  provenanceSummary: "Overzicht van herkomst van het hulpmiddelenbeleid",
452
428
  perToolDecisions: "Beslissingen per hulpmiddel",
429
+ policyDiagnostics: "Diagnose van het hulpmiddelenbeleid",
453
430
  profileReadOnly: "PNP-profiel (alleen lezen)",
454
431
  },
455
432
  tts: {
@@ -52,11 +52,10 @@ type PluralCategoryKey = keyof PluralTranslation;
52
52
  * Dot-notation keys of a catalog shape.
53
53
  *
54
54
  * A plural group terminates the path at the group itself: `plural()` is called
55
- * with `tools.textToSpeech.charactersSelected`, never with
56
- * `…charactersSelected.other`. A group is recognized by every one of its keys
57
- * being a CLDR plural category — the containment has to run this way round,
58
- * because a group carrying only `one`/`other` does not contain the whole
59
- * category union.
55
+ * with `player.formative.triesLeft`, never with `…triesLeft.other`. A group is
56
+ * recognized by every one of its keys being a CLDR plural category — the
57
+ * containment has to run this way round, because a group carrying only
58
+ * `one`/`other` does not contain the whole category union.
60
59
  */
61
60
  type NestedKeys<T> = {
62
61
  [K in keyof T & string]: T[K] extends string ? K : keyof T[K] extends PluralCategoryKey ? K : T[K] extends object ? K | `${K}.${NestedKeys<T[K]>}` : K;
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export * from "./security/index.js";
6
6
  export * from "./loaders/index.js";
7
7
  export * from "./object/index.js";
8
8
  export * from "./pie/index.js";
9
- export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env, FormativeDeliveryPolicy, FormativeItemPolicy, TimedMediaSectionData, ItemConfig, ItemEntity, ItemSession, OutcomeResponse, PassageEntity, PieController, PieContent, PieDefaultModel, PieItemElement, PieModel, PieUpdateSession, RubricBlock, TestPart, } from "./types/index.js";
9
+ export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env, FormativeDeliveryPolicy, FormativeItemPolicy, TimedMediaSectionData, ItemConfig, ItemEntity, ItemSession, OutcomeResponse, PassageEntity, PieController, PieContent, PieDefaultModel, PieItemElement, PieModel, PieUpdateSession, ReferencedItemEntity, ReferencedPassageEntity, RubricBlock, TestPart, } from "./types/index.js";
10
10
  export { editorPostFix } from "./types/index.js";
11
11
  export * from "./ui/attribute-coercion.js";
12
12
  export * from "./ui/content-styles.js";
@@ -73,4 +73,15 @@ export declare function resolveEsmRuntimeSupportUrl(packageVersion: string, opti
73
73
  cdnBaseUrl: string;
74
74
  cdnProvider?: EsmCdnProviderOption;
75
75
  }): string | undefined;
76
+ /**
77
+ * The npm root the ESM CDN the players load elements from serves raw package
78
+ * files under, `<root>/<package>@<version>/<path>`: the base URL for jsDelivr,
79
+ * raw.esm.sh for esm.sh, and for a custom provider whatever its
80
+ * `packageJsonUrl` puts before the package. `undefined` when that URL names
81
+ * no such root.
82
+ */
83
+ export declare function resolveEsmAssetRoot(options: {
84
+ cdnBaseUrl: string;
85
+ cdnProvider?: EsmCdnProviderOption;
86
+ }): string | undefined;
76
87
  export declare function pickElementClass(module: any, view: string): unknown;
@@ -21,7 +21,7 @@ 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
25
  export const BUILT_IN_VIEWS = {
26
26
  delivery: { subpath: "", tagSuffix: "" },
27
27
  author: { subpath: "/author", tagSuffix: "-config", fallback: "delivery" },
@@ -41,8 +41,8 @@ export function createEsmBackend(config) {
41
41
  const editorRuntimeVariants = new Map();
42
42
  /** Packages that do not publish the browser ESM exports this backend loads. */
43
43
  const unpublishedPackages = new Map();
44
- /** Windows whose MathJax version conflicts this backend forwards. */
45
- const mathjaxConflictViews = new WeakSet();
44
+ /** Windows whose MathJax events this backend forwards. */
45
+ const mathjaxEventViews = new WeakSet();
46
46
  let sharedDependencyVersions = {};
47
47
  let importer = defaultImporter;
48
48
  let packageMetadataLoader = defaultPackageMetadataLoader;
@@ -82,7 +82,7 @@ export function createEsmBackend(config) {
82
82
  if (moduleResolution === "import-map") {
83
83
  assertImportMapSupported();
84
84
  }
85
- forwardMathjaxVersionConflictsOf(context.doc);
85
+ forwardMathjaxEventsOf(context.doc);
86
86
  const newEntries = {};
87
87
  for (const [tag, pkg] of Object.entries(elements)) {
88
88
  const packageName = parsePackageName(pkg).name;
@@ -481,12 +481,12 @@ export function createEsmBackend(config) {
481
481
  return undefined;
482
482
  return provider;
483
483
  }
484
- function forwardMathjaxVersionConflictsOf(doc) {
484
+ function forwardMathjaxEventsOf(doc) {
485
485
  const view = doc.defaultView;
486
- if (!view || mathjaxConflictViews.has(view))
486
+ if (!view || mathjaxEventViews.has(view))
487
487
  return;
488
- mathjaxConflictViews.add(view);
489
- forwardMathjaxVersionConflicts(view, getInstrumentationProvider);
488
+ mathjaxEventViews.add(view);
489
+ forwardMathjaxEvents(view, getInstrumentationProvider);
490
490
  }
491
491
  function reportSharedDependencyConflict(attributes, message = `[pie-esm] Shared dependency version conflict resolved for ${String(attributes.dependencyName)}`) {
492
492
  if (typeof console !== "undefined" && console.warn) {
@@ -608,6 +608,24 @@ export function resolveEsmRuntimeSupportUrl(packageVersion, options) {
608
608
  ? `${packageJsonUrl.slice(0, -"package.json".length)}${RUNTIME_SUPPORT_PATH}`
609
609
  : undefined;
610
610
  }
611
+ /** A package the probe below names, so a provider URL ends in its `<package>@<version>` path. */
612
+ const ROOT_PROBE_PACKAGE = "@pie-element/shared-math-rendering-mathjax@0.0.0";
613
+ /**
614
+ * The npm root the ESM CDN the players load elements from serves raw package
615
+ * files under, `<root>/<package>@<version>/<path>`: the base URL for jsDelivr,
616
+ * raw.esm.sh for esm.sh, and for a custom provider whatever its
617
+ * `packageJsonUrl` puts before the package. `undefined` when that URL names
618
+ * no such root.
619
+ */
620
+ export function resolveEsmAssetRoot(options) {
621
+ const cdnBaseUrl = options.cdnBaseUrl.replace(/\/+$/, "");
622
+ const provider = resolveCdnProvider(options.cdnProvider, cdnBaseUrl);
623
+ const suffix = `/${ROOT_PROBE_PACKAGE}/package.json`;
624
+ const packageJsonUrl = provider.packageJsonUrl(ROOT_PROBE_PACKAGE);
625
+ return packageJsonUrl.endsWith(suffix)
626
+ ? packageJsonUrl.slice(0, -suffix.length) || undefined
627
+ : undefined;
628
+ }
611
629
  function resolveCdnProvider(provider, cdnBaseUrl) {
612
630
  if (isEsmCdnProvider(provider))
613
631
  return provider;
@@ -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}`);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * When the PIE elements of a player have rendered.
3
+ *
4
+ * An element renders after its model and session are assigned, in a task of
5
+ * its own: legacy React elements debounce the render and commit through the
6
+ * React scheduler, so the commit lands tens of milliseconds after the
7
+ * assignment. No event marks it in both element generations — `model-set` is
8
+ * dispatched by about half of them, some before rendering — so this reads the
9
+ * DOM.
10
+ *
11
+ * An element has rendered once it holds content, and so has every custom
12
+ * element defined below it: an `ebsr` paints its parts, which then render
13
+ * themselves. Some elements render nothing, a rubric for a student among them,
14
+ * so an element still empty when the subtree has had no mutation for
15
+ * `quietMs` counts as rendered. A render slower than that window, with no
16
+ * mutation before it, is missed rather than waited for.
17
+ */
18
+ import type { ConfigEntity } from "../types/index.js";
19
+ /** Longer than the 50ms render debounce legacy elements use, plus a scheduler task. */
20
+ export declare const ELEMENT_RENDER_QUIET_MS = 200;
21
+ /**
22
+ * Resolves once every element of `configs` in `root` has rendered, or when
23
+ * `signal` aborts. Never rejects.
24
+ */
25
+ export declare function elementsRendered(root: Element, configs: ConfigEntity[], signal: AbortSignal, quietMs?: number): Promise<void>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * When the PIE elements of a player have rendered.
3
+ *
4
+ * An element renders after its model and session are assigned, in a task of
5
+ * its own: legacy React elements debounce the render and commit through the
6
+ * React scheduler, so the commit lands tens of milliseconds after the
7
+ * assignment. No event marks it in both element generations — `model-set` is
8
+ * dispatched by about half of them, some before rendering — so this reads the
9
+ * DOM.
10
+ *
11
+ * An element has rendered once it holds content, and so has every custom
12
+ * element defined below it: an `ebsr` paints its parts, which then render
13
+ * themselves. Some elements render nothing, a rubric for a student among them,
14
+ * so an element still empty when the subtree has had no mutation for
15
+ * `quietMs` counts as rendered. A render slower than that window, with no
16
+ * mutation before it, is missed rather than waited for.
17
+ */
18
+ /** Longer than the 50ms render debounce legacy elements use, plus a scheduler task. */
19
+ export const ELEMENT_RENDER_QUIET_MS = 200;
20
+ function hasContent(element) {
21
+ if (element.firstElementChild || element.shadowRoot?.firstChild)
22
+ return true;
23
+ return Boolean(element.textContent?.trim());
24
+ }
25
+ function isDefinedCustomElement(element) {
26
+ return (element.localName.includes("-") &&
27
+ customElements.get(element.localName) !== undefined);
28
+ }
29
+ function hasRendered(element) {
30
+ if (!element.isConnected)
31
+ return true;
32
+ if (!hasContent(element))
33
+ return false;
34
+ for (const nested of element.querySelectorAll("*")) {
35
+ if (isDefinedCustomElement(nested) && !hasContent(nested))
36
+ return false;
37
+ }
38
+ return true;
39
+ }
40
+ /** The elements of `configs` in `root`, found as `updatePieElements` finds them. */
41
+ function renderedElements(root, configs) {
42
+ const elements = [];
43
+ for (const config of configs) {
44
+ const ids = new Set((config.models ?? []).map((model) => model?.id));
45
+ for (const tag of Object.keys(config.elements ?? {})) {
46
+ // An undefined tag renders nothing until its bundle loads.
47
+ if (!customElements.get(tag))
48
+ continue;
49
+ for (const element of root.querySelectorAll(tag)) {
50
+ if (ids.has(element.id))
51
+ elements.push(element);
52
+ }
53
+ }
54
+ }
55
+ return elements;
56
+ }
57
+ /**
58
+ * Resolves once every element of `configs` in `root` has rendered, or when
59
+ * `signal` aborts. Never rejects.
60
+ */
61
+ export function elementsRendered(root, configs, signal, quietMs = ELEMENT_RENDER_QUIET_MS) {
62
+ const elements = renderedElements(root, configs);
63
+ const allRendered = () => elements.every(hasRendered);
64
+ if (signal.aborted ||
65
+ allRendered() ||
66
+ typeof MutationObserver === "undefined") {
67
+ return Promise.resolve();
68
+ }
69
+ return new Promise((resolve) => {
70
+ let quiet;
71
+ const observer = new MutationObserver(() => {
72
+ if (allRendered()) {
73
+ done();
74
+ return;
75
+ }
76
+ armQuiet();
77
+ });
78
+ function armQuiet() {
79
+ clearTimeout(quiet);
80
+ quiet = setTimeout(done, quietMs);
81
+ }
82
+ function done() {
83
+ clearTimeout(quiet);
84
+ observer.disconnect();
85
+ signal.removeEventListener("abort", done);
86
+ resolve();
87
+ }
88
+ observer.observe(root, {
89
+ childList: true,
90
+ subtree: true,
91
+ characterData: true,
92
+ });
93
+ signal.addEventListener("abort", done);
94
+ armQuiet();
95
+ });
96
+ }
@@ -18,16 +18,14 @@ export { initializeMathRendering, setMathRenderer, } from "./math-rendering.js";
18
18
  export { attachInstrumentationEventBridge } from "./instrumentation-event-bridge.js";
19
19
  export type { AttachInstrumentationEventBridgeArgs } from "./instrumentation-event-bridge.js";
20
20
  export { resolveInstrumentationProvider } from "./instrumentation-provider-resolution.js";
21
- export { STAGES, applicableStages, stageOrdinal, } from "./stages.js";
22
- export type { LoadingCompleteDetail, Stage, StageChangeDetail, StageSourceCe, StageStatus, } from "./stages.js";
23
- export { createStageTracker } from "./stage-tracker.js";
24
- export type { CreateStageTrackerOptions, StageTracker, } from "./stage-tracker.js";
21
+ export { STAGES } from "./stages.js";
22
+ export type { LoadingCompleteDetail, Stage, StageChangeDetail, StageStatus, } from "./stages.js";
25
23
  export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, ITEM_INSTRUMENTATION_EVENT_MAP, SECTION_INSTRUMENTATION_EVENT_MAP, TOOLKIT_INSTRUMENTATION_EVENT_MAP, } from "./instrumentation-event-map.js";
26
24
  export type { InstrumentationEventMapping } from "./instrumentation-event-map.js";
27
25
  export type { ItemControllerOptions } from "./item-controller.js";
28
26
  export { ItemController, normalizeItemSessionContainer, } from "./item-controller.js";
29
27
  export type { ItemSessionContainer as CanonicalItemSessionContainer, ItemSessionUpdateIntent, NormalizedItemSessionChange, } from "./item-session-contract.js";
30
- export { ensureHostSessionEntries, hasLearnerResponse, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
28
+ export { ensureHostSessionEntries, hasLearnerResponse, hasResponseField, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
31
29
  export type { ItemSessionContainer, ItemSessionStorageStrategy, } from "./item-controller-storage.js";
32
30
  export { MemoryItemSessionStorage, SessionStorageItemSessionStorage, } from "./item-controller-storage.js";
33
31
  export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, flushPendingSessionNotifications, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
@@ -35,7 +33,7 @@ export type { BindPageLifecycleCommitOptions, CommitPendingSessionsOptions, Comm
35
33
  export { createSessionSnapshot, isSessionSnapshotEnabled, isSnapshotIdentitySpecific, sessionSnapshotKey, sessionStorageSnapshotStore, } from "./session-snapshot.js";
36
34
  export type { SessionSnapshot, SessionSnapshotConfig, SessionSnapshotIdentity, SessionSnapshotRecord, SessionSnapshotStore, } from "./session-snapshot.js";
37
35
  export type { PieLogger } from "./logger.js";
38
- export { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
36
+ export { createPieLogger, isGlobalDebugEnabled, isTtsDebugEnabled, } from "./logger.js";
39
37
  export { pieRegistry } from "./registry.js";
40
38
  export { findPieController, scorePieItem } from "./scoring.js";
41
39
  export type { Entry, EventListeners, EventListenersMap, LoadPieElementsOptions, PieElement, PieRegistry, UpdatePieElementOptions, } from "./types.js";