@pie-players/pie-players-shared 0.3.73 → 0.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -1
- package/dist/components/vendor/nds/README.md +7 -5
- package/dist/config/profile.d.ts +0 -2
- package/dist/config/profile.js +2 -8
- package/dist/i18n/index.d.ts +2 -3
- package/dist/i18n/index.js +2 -3
- package/dist/i18n/messages/en-US.d.ts +10 -0
- package/dist/i18n/messages/en-US.js +12 -2
- package/dist/i18n/messages/nl-NL.d.ts +6 -0
- package/dist/i18n/messages/nl-NL.js +8 -2
- package/dist/i18n/provider.d.ts +4 -5
- package/dist/i18n/provider.js +4 -5
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -1
- package/dist/instrumentation/index.d.ts +5 -4
- package/dist/instrumentation/index.js +5 -4
- package/dist/instrumentation/new-relic-agent.d.ts +20 -0
- package/dist/instrumentation/new-relic-agent.js +24 -0
- package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +5 -4
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +7 -7
- package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +21 -32
- package/dist/instrumentation/types.d.ts +7 -3
- package/dist/loader-config.d.ts +16 -6
- package/dist/loaders/controller-loading.d.ts +14 -0
- package/dist/loaders/controller-loading.js +12 -0
- package/dist/loaders/defaults.d.ts +10 -0
- package/dist/loaders/defaults.js +10 -0
- package/dist/loaders/element-loader.d.ts +20 -8
- package/dist/loaders/element-loader.js +102 -36
- package/dist/loaders/element-package-policy.d.ts +2 -0
- package/dist/loaders/element-package-policy.js +2 -1
- package/dist/loaders/esm-adapter.d.ts +18 -0
- package/dist/loaders/esm-adapter.js +617 -102
- package/dist/loaders/iife-adapter.d.ts +0 -5
- package/dist/loaders/iife-adapter.js +3 -9
- package/dist/loaders/index.d.ts +10 -3
- package/dist/loaders/index.js +8 -3
- package/dist/loaders/mathjax-version-conflict.d.ts +17 -0
- package/dist/loaders/mathjax-version-conflict.js +51 -0
- package/dist/loaders/module-shim.d.ts +12 -0
- package/dist/loaders/module-shim.js +12 -0
- package/dist/loaders/preloaded-alignment.d.ts +24 -0
- package/dist/loaders/preloaded-alignment.js +57 -0
- package/dist/loaders/preloaded-authored-tags.d.ts +26 -0
- package/dist/loaders/preloaded-authored-tags.js +69 -0
- package/dist/loaders/preloaded-registration.d.ts +56 -0
- package/dist/loaders/preloaded-registration.js +129 -0
- package/dist/media/index.d.ts +69 -0
- package/dist/media/index.js +194 -0
- package/dist/pie/authoring.d.ts +1 -1
- package/dist/pie/authoring.js +25 -18
- package/dist/pie/element-announcements.d.ts +39 -0
- package/dist/pie/element-announcements.js +67 -0
- package/dist/pie/element-observer.d.ts +3 -1
- package/dist/pie/element-observer.js +1 -0
- package/dist/pie/index.d.ts +4 -4
- package/dist/pie/index.js +5 -3
- package/dist/pie/initialization.js +13 -22
- package/dist/pie/initialize-element.d.ts +3 -0
- package/dist/pie/initialize-element.js +3 -4
- package/dist/pie/instrumentation-event-map.js +2 -9
- package/dist/pie/instrumentation-provider-resolution.d.ts +2 -1
- package/dist/pie/instrumentation-provider-resolution.js +0 -1
- package/dist/pie/item-session-contract.d.ts +8 -0
- package/dist/pie/item-session-contract.js +30 -9
- package/dist/pie/markup-math.d.ts +20 -0
- package/dist/pie/markup-math.js +118 -0
- package/dist/pie/math-rendering.d.ts +10 -21
- package/dist/pie/math-rendering.js +50 -35
- package/dist/pie/registry.d.ts +13 -1
- package/dist/pie/registry.js +39 -1
- package/dist/pie/resource-monitor.d.ts +6 -33
- package/dist/pie/resource-monitor.js +14 -213
- package/dist/pie/scoring.d.ts +11 -2
- package/dist/pie/scoring.js +18 -16
- package/dist/pie/session-commit.d.ts +16 -0
- package/dist/pie/session-commit.js +33 -5
- package/dist/pie/session-snapshot.d.ts +80 -0
- package/dist/pie/session-snapshot.js +166 -0
- package/dist/pie/tag-names.d.ts +1 -0
- package/dist/pie/tag-names.js +1 -0
- package/dist/pie/types.d.ts +4 -5
- package/dist/pie/types.js +1 -0
- package/dist/pie/updates.d.ts +2 -2
- package/dist/pie/updates.js +33 -15
- package/dist/pie/versioned-tag.d.ts +4 -0
- package/dist/pie/versioned-tag.js +4 -0
- package/dist/security/index.d.ts +2 -3
- package/dist/security/index.js +2 -3
- package/dist/security/sanitize-item-markup.d.ts +0 -2
- package/dist/security/sanitize-item-markup.js +0 -4
- package/dist/security/sanitize-svg-icon.d.ts +0 -2
- package/dist/security/sanitize-svg-icon.js +0 -5
- package/dist/security/wrap-overwide.d.ts +5 -0
- package/dist/security/wrap-overwide.js +16 -0
- package/dist/timed-media/policy.js +5 -16
- package/dist/types/index.d.ts +30 -311
- package/dist/types/index.js +0 -68
- package/dist/ui/attribute-coercion.d.ts +12 -0
- package/dist/ui/attribute-coercion.js +33 -0
- package/dist/ui/content-styles.d.ts +18 -16
- package/dist/ui/content-styles.js +67 -36
- package/dist/ui/overlay-containment.d.ts +31 -0
- package/dist/ui/overlay-containment.js +63 -0
- package/dist/ui/overlay-placement.d.ts +110 -0
- package/dist/ui/overlay-placement.js +222 -0
- package/dist/ui/pointer-gesture.d.ts +32 -0
- package/dist/ui/pointer-gesture.js +56 -0
- package/dist/ui/pointer-rotate.d.ts +29 -0
- package/dist/ui/pointer-rotate.js +39 -0
- package/package.json +21 -17
- package/dist/pie/component-context.d.ts +0 -21
- package/dist/pie/component-context.js +0 -29
- package/dist/pie/player-initializer.d.ts +0 -54
- package/dist/pie/player-initializer.js +0 -122
- package/dist/security/wrap-model-rich-content.d.ts +0 -1
- package/dist/security/wrap-model-rich-content.js +0 -40
- package/dist/server/npm-auth-env.d.ts +0 -13
- package/dist/server/npm-auth-env.js +0 -64
- package/dist/server/npm-registry.d.ts +0 -7
- package/dist/server/npm-registry.js +0 -59
- package/dist/ui/debug-panel-persistence.d.ts +0 -17
- package/dist/ui/debug-panel-persistence.js +0 -37
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration for the `preloaded` strategy: the host's bundler resolves the
|
|
3
|
+
* element packages, and the players load nothing.
|
|
4
|
+
*
|
|
5
|
+
* Each element is defined under the versioned tag `toPackageVersionedTag`
|
|
6
|
+
* derives from its base tag and the registered version, recorded in
|
|
7
|
+
* `window.PIE_REGISTRY`, and recorded by package in
|
|
8
|
+
* `window.PIE_PRELOADED_ELEMENTS`. The item player and the section player
|
|
9
|
+
* rewrite every authored spec of a registered package to that version
|
|
10
|
+
* (`alignPreloadedElementVersions`), derive the versioned tags from the result,
|
|
11
|
+
* define the ones content names under another base tag
|
|
12
|
+
* (`defineAuthoredPreloadedTags`) and assert them (`assertRegistered`).
|
|
13
|
+
*
|
|
14
|
+
* A hosted player takes models from the server and resolves no controller. A
|
|
15
|
+
* player that is not hosted runs the controller's `model()` in the browser, so
|
|
16
|
+
* it needs the `controller` an entry registers; the item player warns about
|
|
17
|
+
* each tag it renders without one.
|
|
18
|
+
*/
|
|
19
|
+
import { defineCustomElementSafely } from "../pie/custom-element-define.js";
|
|
20
|
+
import { writeRegistryEntry } from "../pie/registry.js";
|
|
21
|
+
import { BundleType, isCustomElementConstructor, Status } from "../pie/types.js";
|
|
22
|
+
import { parsePackageName } from "../pie/utils.js";
|
|
23
|
+
import { toPackageVersionedTag } from "../pie/versioned-tag.js";
|
|
24
|
+
import { isExactSemver } from "./element-package-policy.js";
|
|
25
|
+
import { pickElementClass } from "./esm-adapter.js";
|
|
26
|
+
/**
|
|
27
|
+
* Register host-bundled elements for the `preloaded` strategy.
|
|
28
|
+
*
|
|
29
|
+
* Synchronous: every tag is in `customElements` when the call returns.
|
|
30
|
+
* Idempotent: a tag that is already defined keeps its definition and its
|
|
31
|
+
* registry entry, which only gains a controller it lacked. Every entry is
|
|
32
|
+
* validated before any is registered. A package registers at one version per
|
|
33
|
+
* page, because the players align every authored version of a package to the
|
|
34
|
+
* registered one.
|
|
35
|
+
*/
|
|
36
|
+
export function registerPreloadedElements(elements) {
|
|
37
|
+
if (typeof window === "undefined" || typeof customElements === "undefined") {
|
|
38
|
+
throw new Error("[registerPreloadedElements] requires a browser with customElements");
|
|
39
|
+
}
|
|
40
|
+
const preloaded = preloadedPackageSpecs();
|
|
41
|
+
const resolved = elements.map(resolveElement);
|
|
42
|
+
assertOneVersionPerPackage(resolved, preloaded);
|
|
43
|
+
for (const { packageName, spec, tagName, elementClass, controller } of resolved) {
|
|
44
|
+
defineCustomElementSafely(tagName, class extends elementClass {
|
|
45
|
+
}, `preloaded element tag for ${packageName}`);
|
|
46
|
+
writeRegistryEntry({
|
|
47
|
+
package: spec,
|
|
48
|
+
status: Status.loaded,
|
|
49
|
+
tagName,
|
|
50
|
+
element: elementClass,
|
|
51
|
+
...(controller ? { controller } : {}),
|
|
52
|
+
bundleType: controller ? BundleType.clientPlayer : BundleType.player,
|
|
53
|
+
});
|
|
54
|
+
preloaded[packageName] = spec;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function resolveElement(element, index) {
|
|
58
|
+
const at = `[registerPreloadedElements] entry ${index}`;
|
|
59
|
+
if (!element || typeof element !== "object") {
|
|
60
|
+
throw new Error(`${at}: expected { tag, package, version, element }`);
|
|
61
|
+
}
|
|
62
|
+
const { package: packageName, version, tag } = element;
|
|
63
|
+
if (!isBarePackageName(packageName)) {
|
|
64
|
+
throw new Error(`${at}: package must be a bare package name, got ${JSON.stringify(packageName)}`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof version !== "string" || !isExactSemver(version)) {
|
|
67
|
+
throw new Error(`${at} (${packageName}): version must be the installed version, exact, such as 1.2.3; got ${JSON.stringify(version)}`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof tag !== "string" || !tag) {
|
|
70
|
+
throw new Error(`${at} (${packageName}): tag must be the base tag to register, got ${JSON.stringify(tag)}`);
|
|
71
|
+
}
|
|
72
|
+
const candidate = typeof element.element === "function"
|
|
73
|
+
? element.element
|
|
74
|
+
: pickElementClass(element.element, "delivery");
|
|
75
|
+
if (!isCustomElementConstructor(candidate)) {
|
|
76
|
+
throw new Error(`${at} (${packageName}): element must be the ./browser/delivery module or its default export`);
|
|
77
|
+
}
|
|
78
|
+
let controller;
|
|
79
|
+
if (element.controller !== undefined) {
|
|
80
|
+
controller = resolveController(element.controller);
|
|
81
|
+
if (!controller) {
|
|
82
|
+
throw new Error(`${at} (${packageName}): controller must be the ./browser/controller module or its default export`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const spec = `${packageName}@${version}`;
|
|
86
|
+
return {
|
|
87
|
+
packageName,
|
|
88
|
+
spec,
|
|
89
|
+
tagName: toPackageVersionedTag(tag, spec),
|
|
90
|
+
elementClass: candidate,
|
|
91
|
+
controller,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function resolveController(value) {
|
|
95
|
+
if (!value || typeof value !== "object")
|
|
96
|
+
return undefined;
|
|
97
|
+
const module = value;
|
|
98
|
+
const candidate = (typeof module.model === "function" ? module : module.default);
|
|
99
|
+
return candidate && typeof candidate.model === "function"
|
|
100
|
+
? candidate
|
|
101
|
+
: undefined;
|
|
102
|
+
}
|
|
103
|
+
function assertOneVersionPerPackage(resolved, preloaded) {
|
|
104
|
+
const specs = new Map(Object.entries(preloaded));
|
|
105
|
+
for (const { packageName, spec } of resolved) {
|
|
106
|
+
const existing = specs.get(packageName);
|
|
107
|
+
if (existing !== undefined && existing !== spec) {
|
|
108
|
+
throw new Error(`[registerPreloadedElements] ${packageName} is registered as ${existing}; ` +
|
|
109
|
+
`${spec} cannot also be registered, because the players align every ` +
|
|
110
|
+
"authored version of a package to the registered one");
|
|
111
|
+
}
|
|
112
|
+
specs.set(packageName, spec);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isBarePackageName(value) {
|
|
116
|
+
if (typeof value !== "string" || !value)
|
|
117
|
+
return false;
|
|
118
|
+
try {
|
|
119
|
+
return parsePackageName(value).name === value;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function preloadedPackageSpecs() {
|
|
126
|
+
const host = window;
|
|
127
|
+
host.PIE_PRELOADED_ELEMENTS ??= {};
|
|
128
|
+
return host.PIE_PRELOADED_ELEMENTS;
|
|
129
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation primitives shared by every catalog card whose payload references
|
|
3
|
+
* media.
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* Lives here, behind its own subpath, so a PIE element can validate authored
|
|
13
|
+
* media without importing the assessment toolkit, whose root entry declares
|
|
14
|
+
* side effects and brings the TTS and calculator packages with it. The toolkit
|
|
15
|
+
* root re-exports every name. Nothing here runs at import time.
|
|
16
|
+
*/
|
|
17
|
+
import type { MediaFragmentRange, MediaSource } from "../types/index.js";
|
|
18
|
+
/**
|
|
19
|
+
* The only `MediaAssetRef.version` this build renders.
|
|
20
|
+
*
|
|
21
|
+
* `media-asset-contract.md` requires unknown-version rejection for runtime
|
|
22
|
+
* rendering: bumping to `2` obliges every consumer to accept both for as long as
|
|
23
|
+
* any producer emits `1`, so a card claiming a version this build does not
|
|
24
|
+
* implement must not be rendered on a guess at which fields still mean what they
|
|
25
|
+
* did.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SUPPORTED_MEDIA_ASSET_VERSION = 1;
|
|
28
|
+
/**
|
|
29
|
+
* Whether a media reference claims a version this build cannot render.
|
|
30
|
+
*
|
|
31
|
+
* An absent `version` is accepted rather than rejected — the same posture the
|
|
32
|
+
* rest of this module takes toward absent fields, and the one `media.kind` and
|
|
33
|
+
* `matchesRequestedSignLanguage` already take: only a positive claim of
|
|
34
|
+
* something else is refused. Producers predate the field, and treating its
|
|
35
|
+
* absence as a rejection would drop cards that are otherwise wholly valid.
|
|
36
|
+
*/
|
|
37
|
+
export declare function isUnsupportedMediaAssetVersion(version: unknown): boolean;
|
|
38
|
+
export declare function isSafeMediaSrc(raw: unknown): raw is string;
|
|
39
|
+
export declare function normalizeMediaSources(raw: unknown): MediaSource[];
|
|
40
|
+
export declare function normalizeMediaFragment(raw: unknown): MediaFragmentRange | undefined;
|
|
41
|
+
export declare function trimmedOrUndefined(value: unknown): string | undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Hold a media element to one fragment's range, and return the disposer.
|
|
44
|
+
*
|
|
45
|
+
* The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
|
|
46
|
+
* bound reliably, so every consumer that means "play only this slice" enforces it
|
|
47
|
+
* here instead of reimplementing the pair. Both shipped consumers do —
|
|
48
|
+
* `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
|
|
49
|
+
* ending the clip so the chunk sequence advances — which is why the end action is
|
|
50
|
+
* the caller's and only the arithmetic is shared.
|
|
51
|
+
*
|
|
52
|
+
* The start seek is forward only: a browser that did honour the URI has already
|
|
53
|
+
* positioned past the start, and seeking back would replay audio the learner has
|
|
54
|
+
* heard. `onReachedEnd` may fire more than once and must tolerate it; polling
|
|
55
|
+
* stops while the element is paused, so an element left sitting past its end bound
|
|
56
|
+
* goes quiet rather than being told repeatedly.
|
|
57
|
+
*/
|
|
58
|
+
export declare function enforceMediaFragment(element: HTMLMediaElement, fragment: MediaFragmentRange | undefined, onReachedEnd: () => void): () => void;
|
|
59
|
+
/**
|
|
60
|
+
* Apply a fragment range to a source URL as a Media Fragments URI, so one
|
|
61
|
+
* recording can serve several content nodes.
|
|
62
|
+
*
|
|
63
|
+
* The URI is a hint only: browsers honour both bounds inconsistently, so the
|
|
64
|
+
* caller enforces the range itself with `enforceMediaFragment` — seek forward
|
|
65
|
+
* to the start once metadata is available, and stop at the end.
|
|
66
|
+
* `SignLanguageMediaRegion` and `TTSService.playRecordedAudio` are the two
|
|
67
|
+
* shipped consumers that do so.
|
|
68
|
+
*/
|
|
69
|
+
export declare function applyMediaFragment(src: string, fragment?: MediaFragmentRange): string;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation primitives shared by every catalog card whose payload references
|
|
3
|
+
* media.
|
|
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.
|
|
11
|
+
*
|
|
12
|
+
* Lives here, behind its own subpath, so a PIE element can validate authored
|
|
13
|
+
* media without importing the assessment toolkit, whose root entry declares
|
|
14
|
+
* side effects and brings the TTS and calculator packages with it. The toolkit
|
|
15
|
+
* root re-exports every name. Nothing here runs at import time.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Media source URLs are handed to a media element in the learner's browser. Only
|
|
19
|
+
* schemes such an element can actually fetch are allowed; anything else is
|
|
20
|
+
* dropped so an authored `javascript:` / `file:` URL cannot ride into the DOM.
|
|
21
|
+
* Relative and protocol-relative URLs are allowed — host content is commonly
|
|
22
|
+
* served from the same origin as the player.
|
|
23
|
+
*/
|
|
24
|
+
const DISALLOWED_SRC_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
|
|
25
|
+
const ALLOWED_SRC_SCHEMES = new Set(["http:", "https:", "data:", "blob:"]);
|
|
26
|
+
/**
|
|
27
|
+
* The only `MediaAssetRef.version` this build renders.
|
|
28
|
+
*
|
|
29
|
+
* `media-asset-contract.md` requires unknown-version rejection for runtime
|
|
30
|
+
* rendering: bumping to `2` obliges every consumer to accept both for as long as
|
|
31
|
+
* any producer emits `1`, so a card claiming a version this build does not
|
|
32
|
+
* implement must not be rendered on a guess at which fields still mean what they
|
|
33
|
+
* did.
|
|
34
|
+
*/
|
|
35
|
+
export const SUPPORTED_MEDIA_ASSET_VERSION = 1;
|
|
36
|
+
/**
|
|
37
|
+
* Whether a media reference claims a version this build cannot render.
|
|
38
|
+
*
|
|
39
|
+
* An absent `version` is accepted rather than rejected — the same posture the
|
|
40
|
+
* rest of this module takes toward absent fields, and the one `media.kind` and
|
|
41
|
+
* `matchesRequestedSignLanguage` already take: only a positive claim of
|
|
42
|
+
* something else is refused. Producers predate the field, and treating its
|
|
43
|
+
* absence as a rejection would drop cards that are otherwise wholly valid.
|
|
44
|
+
*/
|
|
45
|
+
export function isUnsupportedMediaAssetVersion(version) {
|
|
46
|
+
return version !== undefined && version !== SUPPORTED_MEDIA_ASSET_VERSION;
|
|
47
|
+
}
|
|
48
|
+
export function isSafeMediaSrc(raw) {
|
|
49
|
+
if (typeof raw !== "string")
|
|
50
|
+
return false;
|
|
51
|
+
const src = raw.trim();
|
|
52
|
+
if (!src)
|
|
53
|
+
return false;
|
|
54
|
+
// Relative ("/video.mp4", "video.mp4") and protocol-relative ("//cdn/x.mp4")
|
|
55
|
+
// forms carry no scheme to check and inherit the document's.
|
|
56
|
+
if (src.startsWith("//") || !DISALLOWED_SRC_SCHEME.test(src))
|
|
57
|
+
return true;
|
|
58
|
+
const scheme = src.slice(0, src.indexOf(":") + 1).toLowerCase();
|
|
59
|
+
return ALLOWED_SRC_SCHEMES.has(scheme);
|
|
60
|
+
}
|
|
61
|
+
export function normalizeMediaSources(raw) {
|
|
62
|
+
if (!Array.isArray(raw))
|
|
63
|
+
return [];
|
|
64
|
+
const sources = [];
|
|
65
|
+
for (const entry of raw) {
|
|
66
|
+
if (!entry || typeof entry !== "object")
|
|
67
|
+
continue;
|
|
68
|
+
const candidate = entry;
|
|
69
|
+
if (!isSafeMediaSrc(candidate.src))
|
|
70
|
+
continue;
|
|
71
|
+
const source = { src: candidate.src.trim() };
|
|
72
|
+
if (typeof candidate.type === "string" && candidate.type.trim()) {
|
|
73
|
+
source.type = candidate.type.trim();
|
|
74
|
+
}
|
|
75
|
+
if (Number.isFinite(candidate.width))
|
|
76
|
+
source.width = candidate.width;
|
|
77
|
+
if (Number.isFinite(candidate.height))
|
|
78
|
+
source.height = candidate.height;
|
|
79
|
+
// Deduplicated by `src`, because the signing region renders `<source>`
|
|
80
|
+
// elements in an `{#each}` keyed on exactly that: an authored card listing
|
|
81
|
+
// one URL twice — the same file under two MIME types is the plausible way —
|
|
82
|
+
// would otherwise throw Svelte's duplicate-key error and take the whole
|
|
83
|
+
// region down rather than degrade. The first entry wins, so authored order
|
|
84
|
+
// still decides which encoding the browser is offered first.
|
|
85
|
+
if (sources.some((existing) => existing.src === source.src))
|
|
86
|
+
continue;
|
|
87
|
+
sources.push(source);
|
|
88
|
+
}
|
|
89
|
+
return sources;
|
|
90
|
+
}
|
|
91
|
+
export function normalizeMediaFragment(raw) {
|
|
92
|
+
if (!raw || typeof raw !== "object")
|
|
93
|
+
return undefined;
|
|
94
|
+
const candidate = raw;
|
|
95
|
+
const start = Number(candidate.startSeconds);
|
|
96
|
+
if (!Number.isFinite(start) || start < 0)
|
|
97
|
+
return undefined;
|
|
98
|
+
const end = Number(candidate.endSeconds);
|
|
99
|
+
// An end at or before the start would produce a zero/negative slice; treat
|
|
100
|
+
// it as "no end" rather than a range that can never play.
|
|
101
|
+
if (!Number.isFinite(end) || end <= start)
|
|
102
|
+
return { startSeconds: start };
|
|
103
|
+
return { startSeconds: start, endSeconds: end };
|
|
104
|
+
}
|
|
105
|
+
export function trimmedOrUndefined(value) {
|
|
106
|
+
if (typeof value !== "string")
|
|
107
|
+
return undefined;
|
|
108
|
+
const trimmed = value.trim();
|
|
109
|
+
return trimmed || undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* How often the end bound is re-checked while a slice is playing. `timeupdate`
|
|
113
|
+
* alone fires about four times a second, which is loose enough to leak a sliver
|
|
114
|
+
* of the next node's recording.
|
|
115
|
+
*/
|
|
116
|
+
const END_CHECK_INTERVAL_MS = 100;
|
|
117
|
+
/** `HTMLMediaElement.HAVE_METADATA`, which not every DOM implementation exposes. */
|
|
118
|
+
const HAVE_METADATA = 1;
|
|
119
|
+
/**
|
|
120
|
+
* Hold a media element to one fragment's range, and return the disposer.
|
|
121
|
+
*
|
|
122
|
+
* The `#t=` URI `applyMediaFragment` writes is a hint browsers honour at neither
|
|
123
|
+
* bound reliably, so every consumer that means "play only this slice" enforces it
|
|
124
|
+
* here instead of reimplementing the pair. Both shipped consumers do —
|
|
125
|
+
* `SignLanguageMediaRegion` pausing at the end, `TTSService.playRecordedAudio`
|
|
126
|
+
* ending the clip so the chunk sequence advances — which is why the end action is
|
|
127
|
+
* the caller's and only the arithmetic is shared.
|
|
128
|
+
*
|
|
129
|
+
* The start seek is forward only: a browser that did honour the URI has already
|
|
130
|
+
* positioned past the start, and seeking back would replay audio the learner has
|
|
131
|
+
* heard. `onReachedEnd` may fire more than once and must tolerate it; polling
|
|
132
|
+
* stops while the element is paused, so an element left sitting past its end bound
|
|
133
|
+
* goes quiet rather than being told repeatedly.
|
|
134
|
+
*/
|
|
135
|
+
export function enforceMediaFragment(element, fragment, onReachedEnd) {
|
|
136
|
+
if (!fragment)
|
|
137
|
+
return () => { };
|
|
138
|
+
const { startSeconds, endSeconds } = fragment;
|
|
139
|
+
let seekToStart;
|
|
140
|
+
if (startSeconds > 0) {
|
|
141
|
+
seekToStart = () => {
|
|
142
|
+
if (element.currentTime < startSeconds) {
|
|
143
|
+
element.currentTime = startSeconds;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
// Seeking before metadata is available only sets the default start
|
|
147
|
+
// position, so wait for it unless it already landed.
|
|
148
|
+
if (element.readyState >= HAVE_METADATA)
|
|
149
|
+
seekToStart();
|
|
150
|
+
else
|
|
151
|
+
element.addEventListener("loadedmetadata", seekToStart);
|
|
152
|
+
}
|
|
153
|
+
let checkEnd;
|
|
154
|
+
let endPoll;
|
|
155
|
+
if (endSeconds !== undefined) {
|
|
156
|
+
checkEnd = () => {
|
|
157
|
+
if (element.currentTime >= endSeconds)
|
|
158
|
+
onReachedEnd();
|
|
159
|
+
};
|
|
160
|
+
element.addEventListener("timeupdate", checkEnd);
|
|
161
|
+
endPoll = setInterval(() => {
|
|
162
|
+
if (!element.paused)
|
|
163
|
+
checkEnd?.();
|
|
164
|
+
}, END_CHECK_INTERVAL_MS);
|
|
165
|
+
}
|
|
166
|
+
return () => {
|
|
167
|
+
if (seekToStart)
|
|
168
|
+
element.removeEventListener("loadedmetadata", seekToStart);
|
|
169
|
+
if (checkEnd)
|
|
170
|
+
element.removeEventListener("timeupdate", checkEnd);
|
|
171
|
+
if (endPoll !== undefined)
|
|
172
|
+
clearInterval(endPoll);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Apply a fragment range to a source URL as a Media Fragments URI, so one
|
|
177
|
+
* recording can serve several content nodes.
|
|
178
|
+
*
|
|
179
|
+
* The URI is a hint only: browsers honour both bounds inconsistently, so the
|
|
180
|
+
* caller enforces the range itself with `enforceMediaFragment` — seek forward
|
|
181
|
+
* to the start once metadata is available, and stop at the end.
|
|
182
|
+
* `SignLanguageMediaRegion` and `TTSService.playRecordedAudio` are the two
|
|
183
|
+
* shipped consumers that do so.
|
|
184
|
+
*/
|
|
185
|
+
export function applyMediaFragment(src, fragment) {
|
|
186
|
+
if (!fragment)
|
|
187
|
+
return src;
|
|
188
|
+
// Never stack a second fragment onto a URL that already carries one — the
|
|
189
|
+
// authored value wins.
|
|
190
|
+
if (src.includes("#"))
|
|
191
|
+
return src;
|
|
192
|
+
const end = fragment.endSeconds !== undefined ? `,${fragment.endSeconds}` : "";
|
|
193
|
+
return `${src}#t=${fragment.startSeconds}${end}`;
|
|
194
|
+
}
|
package/dist/pie/authoring.d.ts
CHANGED
package/dist/pie/authoring.js
CHANGED
|
@@ -105,14 +105,16 @@ export function initializeAuthoringConfigures(config, configuration = {}, option
|
|
|
105
105
|
function resolveValidationController(configureElement) {
|
|
106
106
|
return findPieController(configureElement.localName);
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// A controller's `validate` returns a field → message map. ebsr returns one map
|
|
109
|
+
// per part, so an empty ebsr result is `{ partA: {}, partB: {} }`.
|
|
110
|
+
function hasValidationErrors(errors) {
|
|
111
|
+
if (!isRecord(errors))
|
|
110
112
|
return false;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return
|
|
113
|
+
if (isRecord(errors.partA) && isRecord(errors.partB)) {
|
|
114
|
+
return (Object.keys(errors.partA).length > 0 ||
|
|
115
|
+
Object.keys(errors.partB).length > 0);
|
|
116
|
+
}
|
|
117
|
+
return Object.keys(errors).length > 0;
|
|
116
118
|
}
|
|
117
119
|
export async function validateAuthoringModels(config, configuration = {}, options = {}) {
|
|
118
120
|
const container = options.container ?? document;
|
|
@@ -129,20 +131,25 @@ export async function validateAuthoringModels(config, configuration = {}, option
|
|
|
129
131
|
continue;
|
|
130
132
|
const controller = resolveValidationController(element);
|
|
131
133
|
const elementConfiguration = resolveConfigureConfiguration(configuration, elementTag, String(packageSpec));
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
134
|
+
if (typeof controller?.validate !== "function") {
|
|
135
|
+
validatedModels.push(model);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const errors = await controller.validate(model, elementConfiguration);
|
|
139
|
+
const validatedModel = { ...model, errors };
|
|
140
|
+
validatedModels.push(validatedModel);
|
|
141
|
+
// Configure elements render inline messages from `model.errors`. Skip an
|
|
142
|
+
// unchanged set: some configure elements emit `model.updated` when their
|
|
143
|
+
// model is assigned, and a host validating on every update would loop.
|
|
144
|
+
const configureElement = element;
|
|
145
|
+
if (JSON.stringify(configureElement.model?.errors) !==
|
|
146
|
+
JSON.stringify(errors)) {
|
|
147
|
+
configureElement.model = validatedModel;
|
|
148
|
+
}
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
return {
|
|
145
|
-
hasErrors: validatedModels.some((model) => hasValidationErrors(model.
|
|
152
|
+
hasErrors: validatedModels.some((model) => hasValidationErrors(model.errors)),
|
|
146
153
|
validatedModels,
|
|
147
154
|
};
|
|
148
155
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which delivery-element `session-changed` announcements a renderer forwards.
|
|
3
|
+
*
|
|
4
|
+
* Every update pass hands each element its session again, and an element that
|
|
5
|
+
* announces from its `session` setter announces on every pass, with its own
|
|
6
|
+
* unchanged `complete`. Forwarded, those echoes follow every learner response
|
|
7
|
+
* and let the last element to announce set the item's completion for a host.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* JSON with object keys in sorted order, so a session entry rebuilt with its
|
|
11
|
+
* keys in another order signs the same. An object met a second time is written
|
|
12
|
+
* as `"[Circular]"`. `null` when the value cannot be serialized.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sessionSignature(value: unknown): string | null;
|
|
15
|
+
export interface ElementAnnouncement {
|
|
16
|
+
/** The element that dispatched. */
|
|
17
|
+
element: EventTarget | null;
|
|
18
|
+
/** `complete` from the event detail. */
|
|
19
|
+
complete: unknown;
|
|
20
|
+
/** The session the element holds. */
|
|
21
|
+
session: unknown;
|
|
22
|
+
/** A commit at a teardown, navigation or page-hidden seam. */
|
|
23
|
+
commit: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Admits an announcement when it is the element's first, or when its `complete`
|
|
27
|
+
* or the element's session differs from the last announcement admitted from
|
|
28
|
+
* that element. A commit is always admitted. Keyed by the element instance:
|
|
29
|
+
* `component` repeats across two elements of one type.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createElementAnnouncementFilter(): (announcement: ElementAnnouncement) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether an element holds a session other than its entry of the item's
|
|
34
|
+
* session, by identity and by content. Before the renderer's update pass, that
|
|
35
|
+
* is the placeholder its first initialization pass handed it, and a `complete`
|
|
36
|
+
* it announces describes the placeholder. An element with no readable session
|
|
37
|
+
* holds none.
|
|
38
|
+
*/
|
|
39
|
+
export declare function holdsPlaceholderSession(held: unknown, entry: unknown): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which delivery-element `session-changed` announcements a renderer forwards.
|
|
3
|
+
*
|
|
4
|
+
* Every update pass hands each element its session again, and an element that
|
|
5
|
+
* announces from its `session` setter announces on every pass, with its own
|
|
6
|
+
* unchanged `complete`. Forwarded, those echoes follow every learner response
|
|
7
|
+
* and let the last element to announce set the item's completion for a host.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* JSON with object keys in sorted order, so a session entry rebuilt with its
|
|
11
|
+
* keys in another order signs the same. An object met a second time is written
|
|
12
|
+
* as `"[Circular]"`. `null` when the value cannot be serialized.
|
|
13
|
+
*/
|
|
14
|
+
export function sessionSignature(value) {
|
|
15
|
+
try {
|
|
16
|
+
const seen = new WeakSet();
|
|
17
|
+
const json = JSON.stringify(value, (_key, nested) => {
|
|
18
|
+
if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
|
|
19
|
+
return nested;
|
|
20
|
+
}
|
|
21
|
+
if (seen.has(nested))
|
|
22
|
+
return "[Circular]";
|
|
23
|
+
seen.add(nested);
|
|
24
|
+
const record = nested;
|
|
25
|
+
const sorted = {};
|
|
26
|
+
for (const key of Object.keys(record).sort())
|
|
27
|
+
sorted[key] = record[key];
|
|
28
|
+
return sorted;
|
|
29
|
+
});
|
|
30
|
+
return json ?? "undefined";
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Admits an announcement when it is the element's first, or when its `complete`
|
|
38
|
+
* or the element's session differs from the last announcement admitted from
|
|
39
|
+
* that element. A commit is always admitted. Keyed by the element instance:
|
|
40
|
+
* `component` repeats across two elements of one type.
|
|
41
|
+
*/
|
|
42
|
+
export function createElementAnnouncementFilter() {
|
|
43
|
+
const admitted = new WeakMap();
|
|
44
|
+
return ({ element, complete, session, commit }) => {
|
|
45
|
+
if (!element || typeof element !== "object")
|
|
46
|
+
return true;
|
|
47
|
+
const signature = sessionSignature({ complete, session });
|
|
48
|
+
if (signature === null)
|
|
49
|
+
return true;
|
|
50
|
+
if (!commit && admitted.get(element) === signature)
|
|
51
|
+
return false;
|
|
52
|
+
admitted.set(element, signature);
|
|
53
|
+
return true;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Whether an element holds a session other than its entry of the item's
|
|
58
|
+
* session, by identity and by content. Before the renderer's update pass, that
|
|
59
|
+
* is the placeholder its first initialization pass handed it, and a `complete`
|
|
60
|
+
* it announces describes the placeholder. An element with no readable session
|
|
61
|
+
* holds none.
|
|
62
|
+
*/
|
|
63
|
+
export function holdsPlaceholderSession(held, entry) {
|
|
64
|
+
if (held === undefined || held === entry)
|
|
65
|
+
return false;
|
|
66
|
+
return sessionSignature(held) !== sessionSignature(entry);
|
|
67
|
+
}
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* recently registered, silently never bound.
|
|
21
21
|
*/
|
|
22
22
|
import type { ConfigEntity, Env } from "../types/index.js";
|
|
23
|
-
import type { EventListenersMap } from "./types.js";
|
|
23
|
+
import type { BundleType, EventListenersMap } from "./types.js";
|
|
24
24
|
/**
|
|
25
25
|
* What one registration binds a PIE element with.
|
|
26
26
|
*/
|
|
@@ -29,6 +29,8 @@ export interface PieElementContext {
|
|
|
29
29
|
session: any[];
|
|
30
30
|
env?: Env;
|
|
31
31
|
eventListeners?: EventListenersMap;
|
|
32
|
+
/** The delivering player's bundle type, see `findPieController`. */
|
|
33
|
+
bundleType?: BundleType;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Reads the context to bind with.
|
package/dist/pie/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export { observePieElements, pieElementContextsWithin, } from "./element-observe
|
|
|
14
14
|
export type { PieElementContext, PieElementContextSource, } from "./element-observer.js";
|
|
15
15
|
export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolveConfigureConfiguration, validateAuthoringModels, } from "./authoring.js";
|
|
16
16
|
export type { AuthoringMediaHandlers, AuthoringValidationResult, InitializedConfigureModel, } from "./authoring.js";
|
|
17
|
-
export { initializeMathRendering,
|
|
17
|
+
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";
|
|
@@ -30,12 +30,12 @@ export type { ItemSessionContainer as CanonicalItemSessionContainer, ItemSession
|
|
|
30
30
|
export { ensureHostSessionEntries, hasLearnerResponse, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
|
|
31
31
|
export type { ItemSessionContainer, ItemSessionStorageStrategy, } from "./item-controller-storage.js";
|
|
32
32
|
export { MemoryItemSessionStorage, SessionStorageItemSessionStorage, } from "./item-controller-storage.js";
|
|
33
|
-
export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
|
|
33
|
+
export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, flushPendingSessionNotifications, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
|
|
34
34
|
export type { BindPageLifecycleCommitOptions, CommitPendingSessionsOptions, CommitPendingSessionsResult, SessionCommitReason, } from "./session-commit.js";
|
|
35
|
+
export { createSessionSnapshot, isSessionSnapshotEnabled, isSnapshotIdentitySpecific, sessionSnapshotKey, sessionStorageSnapshotStore, } from "./session-snapshot.js";
|
|
36
|
+
export type { SessionSnapshot, SessionSnapshotConfig, SessionSnapshotIdentity, SessionSnapshotRecord, SessionSnapshotStore, } from "./session-snapshot.js";
|
|
35
37
|
export type { PieLogger } from "./logger.js";
|
|
36
38
|
export { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
37
|
-
export type { ItemData, PiePlayerConfig, PiePlayerElements, } from "./player-initializer.js";
|
|
38
|
-
export { buildApiParams, buildEventListenersMap, ensurePiePlayerLoaded, extractPassageMarkup, fetchItemData, initializePiePlayer, } from "./player-initializer.js";
|
|
39
39
|
export { pieRegistry } from "./registry.js";
|
|
40
40
|
export { findPieController, scorePieItem } from "./scoring.js";
|
|
41
41
|
export type { Entry, EventListeners, EventListenersMap, LoadPieElementsOptions, PieElement, PieRegistry, UpdatePieElementOptions, } from "./types.js";
|
package/dist/pie/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export { initializePiesFromLoadedBundle, loadBundleFromString, loadPieModule, lo
|
|
|
20
20
|
// owns.
|
|
21
21
|
export { observePieElements, pieElementContextsWithin, } from "./element-observer.js";
|
|
22
22
|
export { createAuthoringAssetEventManager, initializeAuthoringConfigures, resolveConfigureConfiguration, validateAuthoringModels, } from "./authoring.js";
|
|
23
|
-
export { initializeMathRendering,
|
|
23
|
+
export { initializeMathRendering, setMathRenderer, } from "./math-rendering.js";
|
|
24
24
|
export { attachInstrumentationEventBridge } from "./instrumentation-event-bridge.js";
|
|
25
25
|
export { resolveInstrumentationProvider } from "./instrumentation-provider-resolution.js";
|
|
26
26
|
// Canonical stage vocabulary (M6). Both `<pie-section-player-*>` and
|
|
@@ -32,9 +32,11 @@ export { ASSESSMENT_INSTRUMENTATION_EVENT_MAP, ITEM_INSTRUMENTATION_EVENT_MAP, S
|
|
|
32
32
|
export { ItemController, normalizeItemSessionContainer, } from "./item-controller.js";
|
|
33
33
|
export { ensureHostSessionEntries, hasLearnerResponse, hasResponseValue, normalizeItemSessionChange, projectSessionIntoHostContainer, } from "./item-session-contract.js";
|
|
34
34
|
export { MemoryItemSessionStorage, SessionStorageItemSessionStorage, } from "./item-controller-storage.js";
|
|
35
|
-
export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
|
|
35
|
+
export { SESSION_COMMIT_METHOD, bindPageLifecycleCommit, commitPendingSessions, flushPendingSessionNotifications, noteSessionBaseline, noteSessionObserved, } from "./session-commit.js";
|
|
36
|
+
// Opt-in device-local snapshot of a committed session, offered back to the host
|
|
37
|
+
// after a crash rather than applied.
|
|
38
|
+
export { createSessionSnapshot, isSessionSnapshotEnabled, isSnapshotIdentitySpecific, sessionSnapshotKey, sessionStorageSnapshotStore, } from "./session-snapshot.js";
|
|
36
39
|
export { createPieLogger, isGlobalDebugEnabled } from "./logger.js";
|
|
37
|
-
export { buildApiParams, buildEventListenersMap, ensurePiePlayerLoaded, extractPassageMarkup, fetchItemData, initializePiePlayer, } from "./player-initializer.js";
|
|
38
40
|
// Registry
|
|
39
41
|
export { pieRegistry } from "./registry.js";
|
|
40
42
|
// Scoring
|