@pie-players/pie-assessment-toolkit 0.3.67 → 0.3.69
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 +81 -9
- package/dist/attempt/AssessmentSession.d.ts +7 -27
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-pe5szfyx.js +46 -0
- package/dist/components/chunks/ItemToolBar-rd7te9r0.js +51 -0
- package/dist/components/item-toolbar-element.js +1 -0
- package/dist/components/pie-assessment-toolkit-element.js +1 -0
- package/dist/components/section-toolbar-element.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +28 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/policy/core/compose-decision.js +14 -0
- package/dist/policy/core/decision-types.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +8 -0
- package/dist/policy/sources/PnpPolicySource.js +19 -10
- package/dist/runtime/SectionRuntimeEngine.d.ts +56 -0
- package/dist/runtime/SectionRuntimeEngine.js +66 -1
- package/dist/runtime/core/engine-resolver.d.ts +25 -1
- package/dist/runtime/registration-events.d.ts +52 -0
- package/dist/runtime/registration-events.js +2 -0
- package/dist/services/AccessibilityCatalogResolver.js +21 -5
- package/dist/services/I18nService.d.ts +28 -100
- package/dist/services/I18nService.js +43 -233
- package/dist/services/TTSService.d.ts +12 -0
- package/dist/services/TTSService.js +18 -17
- package/dist/services/ToolRegistry.d.ts +120 -2
- package/dist/services/ToolRegistry.js +64 -0
- package/dist/services/ToolkitCoordinator.d.ts +39 -37
- package/dist/services/ToolkitCoordinator.js +40 -0
- package/dist/services/audio-handoff.d.ts +39 -0
- package/dist/services/audio-handoff.js +58 -0
- package/dist/services/catalog-media.d.ts +35 -4
- package/dist/services/catalog-media.js +92 -3
- package/dist/services/framework-error.d.ts +15 -1
- package/dist/services/interfaces.d.ts +28 -0
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/section-controller-types.d.ts +218 -7
- package/dist/services/selection-action.d.ts +49 -0
- package/dist/services/selection-action.js +10 -0
- package/dist/services/spoken-audio-cards.js +5 -1
- package/dist/services/tool-context.d.ts +6 -5
- package/dist/services/tool-context.js +205 -155
- package/dist/services/tool-icons.d.ts +18 -0
- package/dist/services/tool-icons.js +31 -0
- package/dist/services/tool-providers/CortexToolProvider.d.ts +18 -0
- package/dist/services/tool-providers/CortexToolProvider.js +32 -0
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +13 -101
- package/dist/services/tool-providers/DesmosToolProvider.js +14 -145
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +21 -0
- package/dist/services/tool-providers/GeoGebraToolProvider.js +32 -0
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +35 -0
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +95 -0
- package/dist/services/tool-providers/index.d.ts +4 -0
- package/dist/services/tool-providers/index.js +2 -0
- package/dist/services/tool-request.d.ts +106 -0
- package/dist/services/tool-request.js +127 -0
- package/dist/services/toolbar-items.d.ts +6 -0
- package/dist/tools/client.d.ts +0 -2
- package/dist/tools/client.js +0 -4
- package/dist/tools/internal.d.ts +7 -1
- package/dist/tools/internal.js +8 -1
- package/dist/tools/tool-surface-host.d.ts +57 -0
- package/dist/tools/tool-surface-host.js +610 -0
- package/dist/tools/types.d.ts +1 -66
- package/package.json +22 -12
- package/dist/components/chunks/ItemToolBar-8jgdz50p.js +0 -51
- package/dist/components/chunks/ItemToolBar-cvs646j3.js +0 -36
- package/dist/tools/calculators/desmos-provider.d.ts +0 -46
- package/dist/tools/calculators/desmos-provider.js +0 -393
- package/dist/tools/library-loader.d.ts +0 -62
- package/dist/tools/library-loader.js +0 -261
|
@@ -63,7 +63,7 @@ export interface ToolPolicyDecisionRequest {
|
|
|
63
63
|
* (already used by `tool-config-validation.ts`) covers config-time
|
|
64
64
|
* misconfiguration; this channel covers per-decision conflicts.
|
|
65
65
|
*/
|
|
66
|
-
export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool-policy.placementMissing";
|
|
66
|
+
export type ToolPolicyDiagnosticCode = "tool-policy.requiredToolBlocked" | "tool-policy.placementMissing" | "tool-policy.unknownSupportId";
|
|
67
67
|
/**
|
|
68
68
|
* Which host gate removed a profile-mandated tool. Surfaced inside
|
|
69
69
|
* `ToolPolicyDiagnostic.details` for `tool-policy.requiredToolBlocked`
|
|
@@ -65,6 +65,14 @@ export interface PnpPolicyResult {
|
|
|
65
65
|
* support id).
|
|
66
66
|
*/
|
|
67
67
|
decisions: PnpPolicyDecisionEvent[];
|
|
68
|
+
/**
|
|
69
|
+
* Support ids named by a profile, district policy or item that no
|
|
70
|
+
* registration claims. Nothing resolves them, so they are silently inert —
|
|
71
|
+
* the engine turns each into a `tool-policy.unknownSupportId` diagnostic so a
|
|
72
|
+
* host sending its own label instead of an AfA/QTI feature id learns of it
|
|
73
|
+
* rather than seeing a tool quietly fail to appear.
|
|
74
|
+
*/
|
|
75
|
+
unmappedSupportIds: Set<string>;
|
|
68
76
|
/** Configuration sources the engine should attach to its provenance. */
|
|
69
77
|
sources: {
|
|
70
78
|
assessment?: {
|
|
@@ -64,6 +64,7 @@ export class PnpPolicySource {
|
|
|
64
64
|
mandatedToolIds: new Set(),
|
|
65
65
|
perToolFlags: new Map(),
|
|
66
66
|
decisions: [],
|
|
67
|
+
unmappedSupportIds: new Set(),
|
|
67
68
|
sources: {},
|
|
68
69
|
};
|
|
69
70
|
if (settings?.districtPolicy || settings?.testAdministration) {
|
|
@@ -99,7 +100,7 @@ export class PnpPolicySource {
|
|
|
99
100
|
resolveSupport(supportId, ctx, out) {
|
|
100
101
|
// 1. District block (absolute veto)
|
|
101
102
|
if (ctx.districtPolicy?.blockedTools?.includes(supportId)) {
|
|
102
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
103
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
103
104
|
out.blockedToolIds.add(toolId);
|
|
104
105
|
out.decisions.push({
|
|
105
106
|
precedence: 1,
|
|
@@ -114,7 +115,7 @@ export class PnpPolicySource {
|
|
|
114
115
|
}
|
|
115
116
|
// 2. Test administration override
|
|
116
117
|
if (ctx.testAdmin?.toolOverrides?.[supportId] === false) {
|
|
117
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
118
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
118
119
|
out.blockedToolIds.add(toolId);
|
|
119
120
|
out.decisions.push({
|
|
120
121
|
precedence: 2,
|
|
@@ -129,7 +130,7 @@ export class PnpPolicySource {
|
|
|
129
130
|
}
|
|
130
131
|
// 3. Item restriction (per-item block)
|
|
131
132
|
if (ctx.itemSettings?.restrictedTools?.includes(supportId)) {
|
|
132
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
133
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
133
134
|
out.blockedToolIds.add(toolId);
|
|
134
135
|
out.decisions.push({
|
|
135
136
|
precedence: 3,
|
|
@@ -144,7 +145,7 @@ export class PnpPolicySource {
|
|
|
144
145
|
}
|
|
145
146
|
// 4. Item requirement (forces enable)
|
|
146
147
|
if (ctx.itemSettings?.requiredTools?.includes(supportId)) {
|
|
147
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
148
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
148
149
|
out.mandatedToolIds.add(toolId);
|
|
149
150
|
out.perToolFlags.set(toolId, {
|
|
150
151
|
required: true,
|
|
@@ -166,7 +167,7 @@ export class PnpPolicySource {
|
|
|
166
167
|
}
|
|
167
168
|
// 5. District requirement
|
|
168
169
|
if (ctx.districtPolicy?.requiredTools?.includes(supportId)) {
|
|
169
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
170
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
170
171
|
out.mandatedToolIds.add(toolId);
|
|
171
172
|
out.perToolFlags.set(toolId, {
|
|
172
173
|
required: true,
|
|
@@ -188,7 +189,7 @@ export class PnpPolicySource {
|
|
|
188
189
|
}
|
|
189
190
|
// 6. PNP prohibitions and supports (student needs)
|
|
190
191
|
if (ctx.pnp?.prohibitedSupports?.includes(supportId)) {
|
|
191
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
192
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
192
193
|
out.blockedToolIds.add(toolId);
|
|
193
194
|
out.decisions.push({
|
|
194
195
|
precedence: 6,
|
|
@@ -202,7 +203,7 @@ export class PnpPolicySource {
|
|
|
202
203
|
return;
|
|
203
204
|
}
|
|
204
205
|
if (ctx.pnp?.supports?.includes(supportId)) {
|
|
205
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
206
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
206
207
|
out.perToolFlags.set(toolId, {
|
|
207
208
|
required: false,
|
|
208
209
|
alwaysAvailable: true,
|
|
@@ -226,7 +227,7 @@ export class PnpPolicySource {
|
|
|
226
227
|
// the same way as every other branch — PR 5's debugger surfaces
|
|
227
228
|
// index provenance trails by tool id and would otherwise show
|
|
228
229
|
// orphaned support-id entries here (M8 PR 1 R3 S1).
|
|
229
|
-
const toolId = this.mapSupportToToolId(supportId);
|
|
230
|
+
const toolId = this.mapSupportToToolId(supportId, out);
|
|
230
231
|
out.decisions.push({
|
|
231
232
|
precedence: 6,
|
|
232
233
|
rule: "pnp-support",
|
|
@@ -263,10 +264,18 @@ export class PnpPolicySource {
|
|
|
263
264
|
*
|
|
264
265
|
* `tests/policy/PnpPolicySource.test.ts` locks this mapping behavior.
|
|
265
266
|
*/
|
|
266
|
-
mapSupportToToolId(supportId) {
|
|
267
|
+
mapSupportToToolId(supportId, out) {
|
|
267
268
|
const toolIds = this.toolRegistry.getToolsByPNPSupport(supportId);
|
|
268
|
-
if (toolIds.size === 0)
|
|
269
|
+
if (toolIds.size === 0) {
|
|
270
|
+
// An empty registry means there is nothing to check the id against, not
|
|
271
|
+
// that the id is wrong — reporting it there would fire on every support
|
|
272
|
+
// id for a host that supplies no registry, which
|
|
273
|
+
// `tool-config-validation` already warns about once.
|
|
274
|
+
if (this.toolRegistry.getAllTools().length > 0) {
|
|
275
|
+
out.unmappedSupportIds.add(supportId);
|
|
276
|
+
}
|
|
269
277
|
return supportId;
|
|
278
|
+
}
|
|
270
279
|
return Array.from(toolIds)[0];
|
|
271
280
|
}
|
|
272
281
|
/**
|
|
@@ -48,6 +48,7 @@ import type { SectionEngineInput } from "./core/engine-input.js";
|
|
|
48
48
|
import type { SectionEngineOutput } from "./core/engine-output.js";
|
|
49
49
|
import { type EffectiveRuntime, type RuntimeInputs } from "./core/engine-resolver.js";
|
|
50
50
|
import { type SectionEngineState } from "./core/engine-state.js";
|
|
51
|
+
import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
|
|
51
52
|
import type { RuntimeRegistrationDetail } from "./registration-events.js";
|
|
52
53
|
import { RuntimeRegistry } from "./RuntimeRegistry.js";
|
|
53
54
|
/**
|
|
@@ -95,6 +96,27 @@ interface RuntimeController extends SectionControllerHandle {
|
|
|
95
96
|
} | null;
|
|
96
97
|
subscribe?: (listener: (event: SectionControllerEvent) => void) => () => void;
|
|
97
98
|
navigateToItem?: (index: number) => unknown;
|
|
99
|
+
attachMediaTimeSource?: (source: MediaTimeSource, options?: {
|
|
100
|
+
origin?: "native-adapter" | "host";
|
|
101
|
+
renderableId?: string;
|
|
102
|
+
}) => void;
|
|
103
|
+
detachMediaTimeSource?: (options?: {
|
|
104
|
+
origin?: "native-adapter" | "host";
|
|
105
|
+
}) => void;
|
|
106
|
+
pauseMediaForCompetingAudio?: () => boolean;
|
|
107
|
+
}
|
|
108
|
+
/** What a media-time-source registration reaching the engine has to say. */
|
|
109
|
+
export interface SectionRuntimeMediaTimeSourceAction {
|
|
110
|
+
renderableId: string;
|
|
111
|
+
action: "attach" | "detach";
|
|
112
|
+
source?: MediaTimeSource;
|
|
113
|
+
origin?: "native-adapter" | "host";
|
|
114
|
+
}
|
|
115
|
+
/** What a formative action reaching the engine has to say. */
|
|
116
|
+
export interface SectionRuntimeFormativeAction {
|
|
117
|
+
itemId: string;
|
|
118
|
+
action: "check" | "retry";
|
|
119
|
+
outcomes?: unknown[];
|
|
98
120
|
}
|
|
99
121
|
/**
|
|
100
122
|
* Initialize args used by the toolkit CE to resolve the coordinator-backed
|
|
@@ -109,6 +131,14 @@ interface EngineInitArgs {
|
|
|
109
131
|
attemptId?: string;
|
|
110
132
|
createDefaultController: () => Promise<RuntimeController> | RuntimeController;
|
|
111
133
|
onCompositionChanged?: (composition: unknown) => void;
|
|
134
|
+
/**
|
|
135
|
+
* The controller event that caused a republish, for the few events the toolkit
|
|
136
|
+
* has to act on rather than merely propagate — a timed-media policy that cannot
|
|
137
|
+
* be enforced becomes a framework warning, and malformed cue data becomes a
|
|
138
|
+
* framework error. Every other event reaches hosts through the composition
|
|
139
|
+
* republish and the coordinator's own subscriptions.
|
|
140
|
+
*/
|
|
141
|
+
onControllerEvent?: (event: SectionControllerEvent) => void;
|
|
112
142
|
}
|
|
113
143
|
/**
|
|
114
144
|
* Engine-side `attachHost` args. The host element, framework-error bus,
|
|
@@ -273,6 +303,32 @@ export declare class SectionRuntimeEngine {
|
|
|
273
303
|
}): void;
|
|
274
304
|
updateItemSession(itemId: string, session: unknown): unknown;
|
|
275
305
|
navigateToItem(index: number): unknown;
|
|
306
|
+
/**
|
|
307
|
+
* Route a learner's formative action to the controller.
|
|
308
|
+
*
|
|
309
|
+
* Canonicalized here for the same reason `updateItemSession` is: the runtime
|
|
310
|
+
* id a card dispatches with is not necessarily the identifier the controller
|
|
311
|
+
* keys state by.
|
|
312
|
+
*/
|
|
313
|
+
handleFormativeAction(action: SectionRuntimeFormativeAction): void;
|
|
314
|
+
/**
|
|
315
|
+
* Bind or release the section's Media Time Source.
|
|
316
|
+
*
|
|
317
|
+
* A pass-through, like `handleFormativeAction`: the engine routes, the
|
|
318
|
+
* controller decides. `renderableId` travels with the attach because whether the
|
|
319
|
+
* registering renderable is the one `stimulusRef` names is the controller's call
|
|
320
|
+
* — it validated `stimulusRef` in the first place.
|
|
321
|
+
*/
|
|
322
|
+
handleMediaTimeSource(action: SectionRuntimeMediaTimeSourceAction): void;
|
|
323
|
+
/**
|
|
324
|
+
* Silence media audio because something else is about to speak.
|
|
325
|
+
*
|
|
326
|
+
* A pass-through like `handleMediaTimeSource`, and for the same reason: which
|
|
327
|
+
* source is authoritative and whether it reports `canPause` are the controller's
|
|
328
|
+
* to know. Returns whether media audio is now silent, or `true` where there is
|
|
329
|
+
* no timed-media controller to ask — nothing is playing.
|
|
330
|
+
*/
|
|
331
|
+
requestMediaPauseForCompetingAudio(): boolean;
|
|
276
332
|
persist(): Promise<void>;
|
|
277
333
|
hydrate(): Promise<void>;
|
|
278
334
|
getRegistry(): RuntimeRegistry;
|
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
import { SectionEngineAdapter, } from "./adapter/SectionEngineAdapter.js";
|
|
42
42
|
import { resolveSectionEngineRuntimeState, } from "./core/engine-resolver.js";
|
|
43
43
|
import { createInitialEngineState, } from "./core/engine-state.js";
|
|
44
|
+
import { createPieLogger, isGlobalDebugEnabled, } from "@pie-players/pie-players-shared";
|
|
44
45
|
import { RuntimeRegistry } from "./RuntimeRegistry.js";
|
|
45
46
|
import { createRuntimeId } from "./runtime-id.js";
|
|
47
|
+
const logger = createPieLogger("section-runtime-engine", () => isGlobalDebugEnabled());
|
|
46
48
|
export class SectionRuntimeEngine {
|
|
47
49
|
registry = new RuntimeRegistry();
|
|
48
50
|
runtimeId = createRuntimeId("section-engine");
|
|
@@ -209,7 +211,17 @@ export class SectionRuntimeEngine {
|
|
|
209
211
|
this.replayRegisteredShellsIntoController(resolved);
|
|
210
212
|
args.onCompositionChanged?.(resolved.getCompositionModel?.());
|
|
211
213
|
this.unsubscribeController =
|
|
212
|
-
resolved.subscribe?.(() => {
|
|
214
|
+
resolved.subscribe?.((event) => {
|
|
215
|
+
// Isolated deliberately: the controller's emit loop catches per
|
|
216
|
+
// listener, so a diagnostic handler that throws would take the
|
|
217
|
+
// composition republish down with it and every cue and Try would stop
|
|
218
|
+
// reaching the cards — a failure with no symptom except a warning.
|
|
219
|
+
try {
|
|
220
|
+
args.onControllerEvent?.(event);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
logger.warn("onControllerEvent handler threw", error);
|
|
224
|
+
}
|
|
213
225
|
args.onCompositionChanged?.(resolved.getCompositionModel?.());
|
|
214
226
|
}) || null;
|
|
215
227
|
}
|
|
@@ -333,6 +345,59 @@ export class SectionRuntimeEngine {
|
|
|
333
345
|
navigateToItem(index) {
|
|
334
346
|
return this.controller?.navigateToItem?.(index) ?? null;
|
|
335
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Route a learner's formative action to the controller.
|
|
350
|
+
*
|
|
351
|
+
* Canonicalized here for the same reason `updateItemSession` is: the runtime
|
|
352
|
+
* id a card dispatches with is not necessarily the identifier the controller
|
|
353
|
+
* keys state by.
|
|
354
|
+
*/
|
|
355
|
+
handleFormativeAction(action) {
|
|
356
|
+
if (!action?.itemId)
|
|
357
|
+
return;
|
|
358
|
+
const canonicalId = this.getCanonicalItemId(action.itemId);
|
|
359
|
+
if (action.action === "retry") {
|
|
360
|
+
this.controller?.retryFormativeItem?.({ itemId: canonicalId });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
this.controller?.recordFormativeTry?.({
|
|
364
|
+
itemId: canonicalId,
|
|
365
|
+
outcomes: action.outcomes,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Bind or release the section's Media Time Source.
|
|
370
|
+
*
|
|
371
|
+
* A pass-through, like `handleFormativeAction`: the engine routes, the
|
|
372
|
+
* controller decides. `renderableId` travels with the attach because whether the
|
|
373
|
+
* registering renderable is the one `stimulusRef` names is the controller's call
|
|
374
|
+
* — it validated `stimulusRef` in the first place.
|
|
375
|
+
*/
|
|
376
|
+
handleMediaTimeSource(action) {
|
|
377
|
+
if (action?.action === "detach") {
|
|
378
|
+
this.controller?.detachMediaTimeSource?.({
|
|
379
|
+
origin: action.origin ?? "host",
|
|
380
|
+
});
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
if (!action?.source)
|
|
384
|
+
return;
|
|
385
|
+
this.controller?.attachMediaTimeSource?.(action.source, {
|
|
386
|
+
origin: action.origin ?? "host",
|
|
387
|
+
renderableId: action.renderableId,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Silence media audio because something else is about to speak.
|
|
392
|
+
*
|
|
393
|
+
* A pass-through like `handleMediaTimeSource`, and for the same reason: which
|
|
394
|
+
* source is authoritative and whether it reports `canPause` are the controller's
|
|
395
|
+
* to know. Returns whether media audio is now silent, or `true` where there is
|
|
396
|
+
* no timed-media controller to ask — nothing is playing.
|
|
397
|
+
*/
|
|
398
|
+
requestMediaPauseForCompetingAudio() {
|
|
399
|
+
return this.controller?.pauseMediaForCompetingAudio?.() ?? true;
|
|
400
|
+
}
|
|
336
401
|
async persist() {
|
|
337
402
|
await this.controller?.persist?.();
|
|
338
403
|
}
|
|
@@ -68,6 +68,18 @@ export type RuntimeConfig = {
|
|
|
68
68
|
* markup (the default). Purely visual — no engine effect.
|
|
69
69
|
*/
|
|
70
70
|
ndsIcons?: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Interface locale: a BCP-47 tag naming the language the player renders its own
|
|
73
|
+
* UI in. Purely presentational — no engine effect.
|
|
74
|
+
*
|
|
75
|
+
* Distinct from content language, which describes the authored item and
|
|
76
|
+
* travels on `env`. QTI 3's implementation guide states the independence
|
|
77
|
+
* directly: a candidate may choose an interface language which may or may not
|
|
78
|
+
* also be the language of the content.
|
|
79
|
+
*
|
|
80
|
+
* Unset renders `en-US`. POSIX (`nl_NL`) and bare (`nl`) forms both resolve.
|
|
81
|
+
*/
|
|
82
|
+
locale?: string;
|
|
71
83
|
toolConfigStrictness?: ToolConfigStrictness;
|
|
72
84
|
onFrameworkError?: FrameworkErrorHandler;
|
|
73
85
|
onStageChange?: StageChangeHandler;
|
|
@@ -120,7 +132,7 @@ export declare function resolveRuntime(args: {
|
|
|
120
132
|
createSectionController: unknown;
|
|
121
133
|
isolation: string;
|
|
122
134
|
env: Record<string, unknown>;
|
|
123
|
-
toolConfigStrictness: "off" | "
|
|
135
|
+
toolConfigStrictness: "off" | "error" | "warn";
|
|
124
136
|
onFrameworkError: FrameworkErrorHandler | undefined;
|
|
125
137
|
onStageChange: StageChangeHandler | undefined;
|
|
126
138
|
onLoadingComplete: LoadingCompleteHandler | undefined;
|
|
@@ -134,6 +146,18 @@ export declare function resolveRuntime(args: {
|
|
|
134
146
|
* markup (the default). Purely visual — no engine effect.
|
|
135
147
|
*/
|
|
136
148
|
ndsIcons?: boolean;
|
|
149
|
+
/**
|
|
150
|
+
* Interface locale: a BCP-47 tag naming the language the player renders its own
|
|
151
|
+
* UI in. Purely presentational — no engine effect.
|
|
152
|
+
*
|
|
153
|
+
* Distinct from content language, which describes the authored item and
|
|
154
|
+
* travels on `env`. QTI 3's implementation guide states the independence
|
|
155
|
+
* directly: a candidate may choose an interface language which may or may not
|
|
156
|
+
* also be the language of the content.
|
|
157
|
+
*
|
|
158
|
+
* Unset renders `en-US`. POSIX (`nl_NL`) and bare (`nl`) forms both resolve.
|
|
159
|
+
*/
|
|
160
|
+
locale?: string;
|
|
137
161
|
};
|
|
138
162
|
/**
|
|
139
163
|
* Effective runtime returned by `resolveRuntime`. The shape is exposed
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { MediaTimeSource } from "@pie-players/pie-players-shared/timed-media";
|
|
1
2
|
export declare const PIE_REGISTER_EVENT = "pie-register";
|
|
2
3
|
export declare const PIE_UNREGISTER_EVENT = "pie-unregister";
|
|
3
4
|
export declare const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed";
|
|
4
5
|
export declare const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
|
|
5
6
|
export declare const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
|
|
6
7
|
export declare const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
|
|
8
|
+
export declare const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
|
|
9
|
+
export declare const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
|
|
7
10
|
export type RuntimeRegistrationKind = "item" | "passage";
|
|
8
11
|
export interface RuntimeRegistrationDetail {
|
|
9
12
|
kind: RuntimeRegistrationKind;
|
|
@@ -35,3 +38,52 @@ export interface InternalItemPlayerErrorDetail {
|
|
|
35
38
|
contentKind?: string;
|
|
36
39
|
error: unknown;
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* A learner's formative action, dispatched by the component that owns the
|
|
43
|
+
* control and the item player node — the only place that can call
|
|
44
|
+
* `provideScore()`. It reports outcomes rather than interpreting them; the
|
|
45
|
+
* section controller derives correctness and owns the state.
|
|
46
|
+
*
|
|
47
|
+
* `outcomes` is the array `pie-item-player.provideScore()` returned, verbatim,
|
|
48
|
+
* including the `undefined` slots it leaves for models with no element or
|
|
49
|
+
* controller.
|
|
50
|
+
*/
|
|
51
|
+
export interface InternalFormativeActionDetail {
|
|
52
|
+
itemId: string;
|
|
53
|
+
canonicalItemId?: string;
|
|
54
|
+
action: "check" | "retry";
|
|
55
|
+
outcomes?: unknown[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* A Media Time Source becoming available or going away.
|
|
59
|
+
*
|
|
60
|
+
* The one seam through which a timed-media section reaches media, and
|
|
61
|
+
* deliberately the *only* one: the stimulus card dispatches this with a native
|
|
62
|
+
* `<video>` adapter, and a host wrapping a third-party player dispatches the same
|
|
63
|
+
* event with its own port. One code path, two producers — which is what keeps
|
|
64
|
+
* "a host can supply its own media element without shipping a PIE element" true
|
|
65
|
+
* rather than aspirational.
|
|
66
|
+
*
|
|
67
|
+
* `source` carries a live object, not serializable data. That is fine and
|
|
68
|
+
* intended: this event never crosses a realm, exactly like the element reference
|
|
69
|
+
* on `pie-register`.
|
|
70
|
+
*/
|
|
71
|
+
export interface InternalMediaTimeSourceDetail {
|
|
72
|
+
/** The renderable that owns the media, for matching against `stimulusRef`. */
|
|
73
|
+
renderableId: string;
|
|
74
|
+
action: "attach" | "detach";
|
|
75
|
+
source?: MediaTimeSource;
|
|
76
|
+
/**
|
|
77
|
+
* Which producer this came from. `"native-adapter"` is the stimulus card
|
|
78
|
+
* wrapping a media element it found in its own subtree; anything else is a host
|
|
79
|
+
* wiring its own player, and omitting the field reads as `"host"` because a
|
|
80
|
+
* caller constructing this event by hand is one.
|
|
81
|
+
*
|
|
82
|
+
* Load-bearing for precedence: the card re-runs its discovery whenever its
|
|
83
|
+
* content changes, so without this a host that supplied a third-party port would
|
|
84
|
+
* have it silently replaced by the native element mid-session — and the
|
|
85
|
+
* capabilities would flip back with it, which is exactly the "appears to enforce"
|
|
86
|
+
* failure this contract exists to prevent.
|
|
87
|
+
*/
|
|
88
|
+
origin?: "native-adapter" | "host";
|
|
89
|
+
}
|
|
@@ -4,3 +4,5 @@ export const PIE_INTERNAL_ITEM_SESSION_CHANGED_EVENT = "pie-item-session-changed
|
|
|
4
4
|
export const PIE_ITEM_SESSION_CHANGED_EVENT = "item-session-changed";
|
|
5
5
|
export const PIE_INTERNAL_CONTENT_LOADED_EVENT = "pie-content-loaded";
|
|
6
6
|
export const PIE_INTERNAL_ITEM_PLAYER_ERROR_EVENT = "pie-item-player-error";
|
|
7
|
+
export const PIE_INTERNAL_FORMATIVE_ACTION_EVENT = "pie-formative-action";
|
|
8
|
+
export const PIE_INTERNAL_MEDIA_TIME_SOURCE_EVENT = "pie-media-time-source";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { languageTagLookupSequence, normalizeLanguageTag, } from "@pie-players/pie-players-shared/i18n/language-tags";
|
|
1
2
|
import { catalogOwnerContextFor, collectOwnerCatalogRegistrations, } from "./catalog-owner.js";
|
|
2
3
|
import { sanitizeSsmlString } from "./SSMLExtractor.js";
|
|
3
4
|
/**
|
|
@@ -537,14 +538,25 @@ export class AccessibilityCatalogResolver {
|
|
|
537
538
|
*/
|
|
538
539
|
findMatchingCard(catalog, options) {
|
|
539
540
|
const { type, language, useFallback = true, form } = options;
|
|
540
|
-
// Language rungs, most specific first: requested language, then the
|
|
541
|
-
// language, then any.
|
|
541
|
+
// Language rungs, most specific first: the requested language, then the
|
|
542
|
+
// default language, then any.
|
|
543
|
+
//
|
|
544
|
+
// Each requested tag expands into its RFC 4647 lookup sequence, so `es-MX`
|
|
545
|
+
// tries `es-mx` and then `es` before falling through to the default. Matching
|
|
546
|
+
// was `===`, which made a POSIX `es_ES` card — what the Learnosity transform
|
|
547
|
+
// emits — unreachable for an `es-ES` request except through the final
|
|
548
|
+
// no-constraint rung, i.e. by accident.
|
|
542
549
|
const languageRungs = [];
|
|
550
|
+
const pushLookupRungs = (tag) => {
|
|
551
|
+
for (const step of languageTagLookupSequence(tag)) {
|
|
552
|
+
languageRungs.push((card) => normalizeLanguageTag(card.language) === step);
|
|
553
|
+
}
|
|
554
|
+
};
|
|
543
555
|
if (language) {
|
|
544
|
-
|
|
556
|
+
pushLookupRungs(language);
|
|
545
557
|
}
|
|
546
558
|
if (useFallback) {
|
|
547
|
-
|
|
559
|
+
pushLookupRungs(this.defaultLanguage);
|
|
548
560
|
languageRungs.push(() => true);
|
|
549
561
|
}
|
|
550
562
|
for (const matchesLanguage of languageRungs) {
|
|
@@ -586,7 +598,11 @@ export class AccessibilityCatalogResolver {
|
|
|
586
598
|
// asking what alternates exist under-reported them.
|
|
587
599
|
const claimed = new Set();
|
|
588
600
|
const add = (card, source) => {
|
|
589
|
-
|
|
601
|
+
// The language part is normalized, so a POSIX `es_ES` card and a BCP-47
|
|
602
|
+
// `es-ES` card collapse to one entry here exactly as they now collapse on
|
|
603
|
+
// the resolution path. Keying on the raw string would report two
|
|
604
|
+
// alternates where resolution can only ever return one.
|
|
605
|
+
const key = `${card.catalog}|${normalizeLanguageTag(card.language)}|${catalogCardForm(card)}`;
|
|
590
606
|
if (claimed.has(key))
|
|
591
607
|
return;
|
|
592
608
|
claimed.add(key);
|
|
@@ -1,119 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* I18nService
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* The toolkit's view of the shared i18n provider: a delegating wrapper over
|
|
5
|
+
* `SimpleI18n` from `players-shared` that adds toolkit-scoped logging and wires
|
|
6
|
+
* the lazy catalog loaders.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Pluralization: ICU-style plural rules
|
|
12
|
-
* - RTL detection: Automatic based on locale
|
|
13
|
-
* - Fallback to English for missing keys
|
|
8
|
+
* It was once a near-verbatim second copy of that class — same fields, same
|
|
9
|
+
* lookup, same plural selection. Duplicate implementations of one contract drift
|
|
10
|
+
* silently, because nothing fails when only one of them is fixed. Keep this a
|
|
11
|
+
* wrapper.
|
|
14
12
|
*
|
|
15
13
|
* Part of PIE Assessment Toolkit.
|
|
16
14
|
*/
|
|
17
|
-
import type { I18nConfig, I18nServiceApi,
|
|
18
|
-
export type { I18nConfig, I18nServiceApi,
|
|
15
|
+
import type { I18nConfig, I18nProvider, I18nServiceApi, InterpolationValues, LocaleCode, MessageCatalog, MessageKeyInput, PluralOptions, TextDirection } from "@pie-players/pie-players-shared/i18n";
|
|
16
|
+
export type { I18nConfig, I18nProvider, I18nServiceApi, InterpolationValues, LocaleCode, MessageCatalog, MessageKeyInput, PluralOptions, TextDirection, };
|
|
19
17
|
/**
|
|
20
|
-
*
|
|
18
|
+
* Toolkit-scoped i18n service.
|
|
21
19
|
*
|
|
22
|
-
*
|
|
20
|
+
* Serves every locale this repository ships, English resident and the rest
|
|
21
|
+
* lazily loaded.
|
|
23
22
|
*/
|
|
24
23
|
export declare class I18nService implements I18nServiceApi {
|
|
25
|
-
private
|
|
26
|
-
private fallbackLocale;
|
|
27
|
-
private direction;
|
|
28
|
-
private translations;
|
|
29
|
-
private listeners;
|
|
30
|
-
private loadingPromises;
|
|
31
|
-
private config;
|
|
24
|
+
private readonly i18n;
|
|
32
25
|
constructor(config?: I18nConfig);
|
|
33
26
|
/**
|
|
34
|
-
*
|
|
35
|
-
*/
|
|
36
|
-
initialize(config: I18nConfig): Promise<void>;
|
|
37
|
-
/**
|
|
38
|
-
* Translate a key with optional interpolation
|
|
39
|
-
*
|
|
40
|
-
* @param key Translation key (e.g., 'common.save')
|
|
41
|
-
* @param params Optional parameters for interpolation
|
|
42
|
-
* @returns Translated string
|
|
43
|
-
*/
|
|
44
|
-
t(key: string, params?: Record<string, any>): string;
|
|
45
|
-
/**
|
|
46
|
-
* Translate with pluralization
|
|
27
|
+
* Set the locale, loading its catalog first.
|
|
47
28
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* @returns Translated string with plural form
|
|
52
|
-
*/
|
|
53
|
-
tn(key: string, count: number, params?: Record<string, any>): string;
|
|
54
|
-
/**
|
|
55
|
-
* Get current locale
|
|
29
|
+
* Never falls back to the browser locale: a rendered-string change reaches a
|
|
30
|
+
* host's live delivery on their next install with no build signal on their
|
|
31
|
+
* side, so the locale has to be something the host asked for.
|
|
56
32
|
*/
|
|
33
|
+
initialize(config: I18nConfig): Promise<void>;
|
|
34
|
+
t(key: MessageKeyInput, values?: InterpolationValues): string;
|
|
35
|
+
plural(key: MessageKeyInput, options: PluralOptions): string;
|
|
57
36
|
getLocale(): string;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
61
|
-
* @param locale Locale code (e.g., 'en', 'es', 'zh', 'ar')
|
|
62
|
-
*/
|
|
63
|
-
setLocale(locale: string): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Get current text direction
|
|
66
|
-
*/
|
|
67
|
-
getDirection(): "ltr" | "rtl";
|
|
68
|
-
/**
|
|
69
|
-
* Get available locales
|
|
70
|
-
*/
|
|
37
|
+
setLocale(locale: LocaleCode): Promise<void>;
|
|
38
|
+
getDirection(): TextDirection;
|
|
71
39
|
getAvailableLocales(): string[];
|
|
72
|
-
|
|
73
|
-
* Check if locale is loaded
|
|
74
|
-
*/
|
|
75
|
-
isLocaleLoaded(locale: string): boolean;
|
|
76
|
-
/**
|
|
77
|
-
* Subscribe to locale/translation changes
|
|
78
|
-
* Returns unsubscribe function
|
|
79
|
-
*/
|
|
40
|
+
isLocaleLoaded(locale: LocaleCode): boolean;
|
|
80
41
|
subscribe(listener: () => void): () => void;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
* Notify all listeners of state change
|
|
87
|
-
*/
|
|
88
|
-
private notifyListeners;
|
|
89
|
-
/**
|
|
90
|
-
* Apply locale and notify listeners
|
|
91
|
-
*/
|
|
92
|
-
private applyLocale;
|
|
93
|
-
/**
|
|
94
|
-
* Apply direction to DOM
|
|
95
|
-
*/
|
|
96
|
-
private applyDOMDirection;
|
|
97
|
-
/**
|
|
98
|
-
* Load translations for a locale
|
|
99
|
-
*/
|
|
100
|
-
private loadTranslationsForLocale;
|
|
101
|
-
/**
|
|
102
|
-
* Get translation for a key
|
|
103
|
-
*/
|
|
104
|
-
private getTranslation;
|
|
105
|
-
/**
|
|
106
|
-
* Interpolate variables in translation string
|
|
107
|
-
* Replaces {variable} with params.variable
|
|
108
|
-
*/
|
|
109
|
-
private interpolate;
|
|
110
|
-
/**
|
|
111
|
-
* Select plural form based on count and locale
|
|
112
|
-
* Simplified implementation - real implementation would use Intl.PluralRules
|
|
113
|
-
*/
|
|
114
|
-
private selectPluralForm;
|
|
115
|
-
/**
|
|
116
|
-
* Detect browser locale
|
|
117
|
-
*/
|
|
118
|
-
private detectBrowserLocale;
|
|
42
|
+
hasKey(key: MessageKeyInput): boolean;
|
|
43
|
+
addCustomMessages(locale: string, messages: MessageCatalog): void;
|
|
44
|
+
withLocale(locale: LocaleCode): I18nProvider;
|
|
45
|
+
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
|
46
|
+
formatDate(date: Date, options?: Intl.DateTimeFormatOptions): string;
|
|
119
47
|
}
|