@pie-players/pie-players-shared 1.0.0-beta.7 → 1.0.0-beta.9
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 +16 -14
- package/dist/i18n/catalogs.d.ts +20 -13
- package/dist/i18n/catalogs.js +22 -23
- package/dist/i18n/language-tags.d.ts +23 -6
- package/dist/i18n/language-tags.js +74 -9
- package/dist/i18n/messages/en-US.d.ts +21 -4
- package/dist/i18n/messages/en-US.js +23 -4
- package/dist/i18n/types.d.ts +7 -2
- package/dist/index.d.ts +1 -1
- package/dist/item-session-control/index.d.ts +16 -0
- package/dist/item-session-control/index.js +5 -0
- package/dist/{formative → item-session-control}/mastery.d.ts +5 -5
- package/dist/{formative → item-session-control}/mastery.js +3 -3
- package/dist/{formative → item-session-control}/outcome.d.ts +3 -3
- package/dist/{formative → item-session-control}/outcome.js +2 -2
- package/dist/{formative → item-session-control}/policy.d.ts +11 -11
- package/dist/{formative → item-session-control}/policy.js +12 -12
- package/dist/item-session-control/session.d.ts +19 -0
- package/dist/{formative → item-session-control}/session.js +12 -12
- package/dist/{formative → item-session-control}/state.d.ts +26 -26
- package/dist/{formative → item-session-control}/state.js +13 -13
- package/dist/{formative → item-session-control}/types.d.ts +38 -40
- package/dist/{formative → item-session-control}/types.js +2 -2
- package/dist/security/private-hosts.d.ts +27 -0
- package/dist/security/private-hosts.js +85 -0
- package/dist/timed-media/index.d.ts +1 -1
- package/dist/timed-media/index.js +1 -1
- package/dist/timed-media/policy.d.ts +2 -2
- package/dist/timed-media/policy.js +2 -2
- package/dist/timed-media/session.d.ts +1 -1
- package/dist/timed-media/session.js +1 -1
- package/dist/timed-media/types.d.ts +11 -11
- package/dist/tools/term-lookup.d.ts +15 -0
- package/dist/tools/term-lookup.js +16 -0
- package/dist/types/index.d.ts +32 -18
- package/dist/types/interchange.d.ts +66 -4
- package/dist/ui/fontawesome.d.ts +51 -0
- package/dist/ui/fontawesome.js +122 -0
- package/package.json +15 -6
- package/dist/formative/index.d.ts +0 -16
- package/dist/formative/index.js +0 -5
- package/dist/formative/session.d.ts +0 -19
- package/dist/i18n/messages/nl-NL.d.ts +0 -639
- package/dist/i18n/messages/nl-NL.js +0 -639
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hostname classification for a URL a server fetches on someone else's behalf,
|
|
3
|
+
* such as an SSML `<audio src>` a TTS provider will request or an asset URL an
|
|
4
|
+
* upstream service returns. Every such check reads this module, so no two
|
|
5
|
+
* copies of it can drift apart.
|
|
6
|
+
*
|
|
7
|
+
* IP literals are classified by `ipaddr.js`, whose ranges follow the IANA IPv4
|
|
8
|
+
* and IPv6 Special-Purpose Address Registries (RFC 6890). A host is private
|
|
9
|
+
* unless it is a public unicast address or a name: loopback, private, link-local,
|
|
10
|
+
* unique-local, carrier-grade NAT, multicast, broadcast, documentation and
|
|
11
|
+
* reserved ranges, and the IPv6 forms that embed an IPv4 address (NAT64, 6to4,
|
|
12
|
+
* Teredo), all count. `ipaddr.js` also reads the numeric IPv4 forms (`2130706433`,
|
|
13
|
+
* `0x7f000001`, `0177.0.0.1`) and unmaps IPv4-mapped IPv6.
|
|
14
|
+
*
|
|
15
|
+
* Only the literal is checked. A public name that resolves to a private address
|
|
16
|
+
* passes; guarding that takes a check of the resolved address at connect time.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import ipaddr from "ipaddr.js";
|
|
21
|
+
/**
|
|
22
|
+
* Cloud instance-metadata hostnames. They hand out credentials. No registry
|
|
23
|
+
* lists them; each is the provider's documented name.
|
|
24
|
+
*/
|
|
25
|
+
const CLOUD_METADATA_HOSTNAMES = new Set([
|
|
26
|
+
"metadata.google.internal",
|
|
27
|
+
"metadata.azure.internal",
|
|
28
|
+
"metadata.packet.net",
|
|
29
|
+
"metadata.oci.oraclecloud.com",
|
|
30
|
+
]);
|
|
31
|
+
/** Cloud instance-metadata addresses: the link-local one most clouds use, and AWS's IPv6 one. */
|
|
32
|
+
const CLOUD_METADATA_ADDRESSES = ["169.254.169.254", "fd00:ec2::254"].map((address) => ipaddr.parse(address));
|
|
33
|
+
/** Lower-cased, without brackets or a trailing dot. */
|
|
34
|
+
function normalizeHostname(hostname) {
|
|
35
|
+
return hostname
|
|
36
|
+
.replace(/^\[|\]$/g, "")
|
|
37
|
+
.replace(/\.$/, "")
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The address `host` is a literal for, IPv4-mapped (`::ffff:a.b.c.d`) and
|
|
42
|
+
* IPv4-compatible (`::a.b.c.d`) IPv6 reduced to IPv4, or `null` for a name.
|
|
43
|
+
*/
|
|
44
|
+
function parseAddress(host) {
|
|
45
|
+
if (!ipaddr.isValid(host))
|
|
46
|
+
return null;
|
|
47
|
+
const address = ipaddr.process(host);
|
|
48
|
+
if (address.kind() !== "ipv6")
|
|
49
|
+
return address;
|
|
50
|
+
// `ipaddr.js` classifies deprecated IPv4-compatible addresses as unicast;
|
|
51
|
+
// read the IPv4 address in their low 32 bits instead.
|
|
52
|
+
const { parts } = address;
|
|
53
|
+
const isIpv4Compatible = parts.slice(0, 6).every((part) => part === 0) &&
|
|
54
|
+
(parts[6] !== 0 || parts[7] > 1);
|
|
55
|
+
return isIpv4Compatible
|
|
56
|
+
? new ipaddr.IPv4([
|
|
57
|
+
parts[6] >> 8,
|
|
58
|
+
parts[6] & 0xff,
|
|
59
|
+
parts[7] >> 8,
|
|
60
|
+
parts[7] & 0xff,
|
|
61
|
+
])
|
|
62
|
+
: address;
|
|
63
|
+
}
|
|
64
|
+
/** Whether `hostname` is a cloud instance-metadata endpoint. */
|
|
65
|
+
export function isCloudMetadataHostname(hostname) {
|
|
66
|
+
const host = normalizeHostname(hostname);
|
|
67
|
+
if (CLOUD_METADATA_HOSTNAMES.has(host))
|
|
68
|
+
return true;
|
|
69
|
+
const address = parseAddress(host);
|
|
70
|
+
return (address !== null &&
|
|
71
|
+
CLOUD_METADATA_ADDRESSES.some((metadata) => metadata.kind() === address.kind() &&
|
|
72
|
+
metadata.toNormalizedString() === address.toNormalizedString()));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether `hostname` is anything other than a public unicast address or a
|
|
76
|
+
* public name: `localhost` and its subdomains (RFC 6761), or an IP literal in
|
|
77
|
+
* any special-purpose range.
|
|
78
|
+
*/
|
|
79
|
+
export function isPrivateHostname(hostname) {
|
|
80
|
+
const host = normalizeHostname(hostname);
|
|
81
|
+
if (host === "localhost" || host.endsWith(".localhost"))
|
|
82
|
+
return true;
|
|
83
|
+
const address = parseAddress(host);
|
|
84
|
+
return address !== null && address.range() !== "unicast";
|
|
85
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure apart from one adapter factory that is inert until called, so the contract
|
|
6
6
|
* is testable without a browser and an adapter can import it without pulling in a
|
|
7
7
|
* player. Live cue state belongs to `SectionController` in
|
|
8
|
-
* `@pie-players/pie-section-player`, which is where the equivalent
|
|
8
|
+
* `@pie-players/pie-section-player`, which is where the equivalent Try state
|
|
9
9
|
* lives — one rollup over one item set, in one package.
|
|
10
10
|
*
|
|
11
11
|
* Contract: `docs/prds/timed-media-section-contract.md`.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Pure apart from one adapter factory that is inert until called, so the contract
|
|
6
6
|
* is testable without a browser and an adapter can import it without pulling in a
|
|
7
7
|
* player. Live cue state belongs to `SectionController` in
|
|
8
|
-
* `@pie-players/pie-section-player`, which is where the equivalent
|
|
8
|
+
* `@pie-players/pie-section-player`, which is where the equivalent Try state
|
|
9
9
|
* lives — one rollup over one item set, in one package.
|
|
10
10
|
*
|
|
11
11
|
* Contract: `docs/prds/timed-media-section-contract.md`.
|
|
@@ -28,9 +28,9 @@ export declare function normalizeTimedMediaSectionData(args: {
|
|
|
28
28
|
itemIdentifiers: readonly string[];
|
|
29
29
|
resolveStimulusRenderableId: (stimulusRef: string) => string | undefined;
|
|
30
30
|
/**
|
|
31
|
-
* The Try budget each item's resolved
|
|
31
|
+
* The Try budget each item's resolved item session control policy leaves, so a gate on
|
|
32
32
|
* correctness can be refused where it would strand the learner. Supplied by the
|
|
33
|
-
* caller because
|
|
33
|
+
* caller because item session control policy resolution is that contract's, not this one's.
|
|
34
34
|
*/
|
|
35
35
|
resolveItemTryBudget: (itemIdentifier: string) => TimedMediaItemTryBudget;
|
|
36
36
|
}): TimedMediaValidationResult;
|
|
@@ -240,7 +240,7 @@ export function normalizeTimedMediaSectionData(args) {
|
|
|
240
240
|
// A correctness gate is only passable while the learner still has a Try
|
|
241
241
|
// to spend. Over an item with a finite budget the gate becomes
|
|
242
242
|
// unpassable the moment the budget runs out, and over an item that does
|
|
243
|
-
// not
|
|
243
|
+
// not enable item session control there is never a Try at all — in both cases
|
|
244
244
|
// nothing the learner can do releases playback, and no host action
|
|
245
245
|
// releases it either, because a forced reveal is not a correct answer.
|
|
246
246
|
// Refused here rather than released at runtime: releasing would deliver
|
|
@@ -250,7 +250,7 @@ export function normalizeTimedMediaSectionData(args) {
|
|
|
250
250
|
if (unpassable.length > 0) {
|
|
251
251
|
errors.push({
|
|
252
252
|
code: "gate-requires-unlimited-tries",
|
|
253
|
-
message: `Gate cue "${identifier}" releases on correctness, so every item it names needs
|
|
253
|
+
message: `Gate cue "${identifier}" releases on correctness, so every item it names needs item session control with maxTries: "unlimited" — otherwise a learner who runs out of Tries can never release playback. Offending item refs: ${unpassable.join(", ")}.`,
|
|
254
254
|
cueIdentifier: identifier,
|
|
255
255
|
});
|
|
256
256
|
continue;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistence for the timed-media slice.
|
|
3
3
|
*
|
|
4
|
-
* Same posture as the
|
|
4
|
+
* Same posture as the Try slice: an unreadable slice is rejected whole and
|
|
5
5
|
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
6
|
* are normalized separately and survive. A version bump here must never cost a
|
|
7
7
|
* learner their responses.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Persistence for the timed-media slice.
|
|
3
3
|
*
|
|
4
|
-
* Same posture as the
|
|
4
|
+
* Same posture as the Try slice: an unreadable slice is rejected whole and
|
|
5
5
|
* delivery starts from clean cue state, while item sessions in the same snapshot
|
|
6
6
|
* are normalized separately and survive. A version bump here must never cost a
|
|
7
7
|
* learner their responses.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* See `docs/prds/timed-media-section-contract.md` for the ratified contract and
|
|
10
10
|
* `docs/architecture/timed-media-section.md` for the layer-ownership record.
|
|
11
11
|
*/
|
|
12
|
-
import type {
|
|
12
|
+
import type { TryCorrectness } from "../item-session-control/types.js";
|
|
13
13
|
import type { MediaFragmentRange } from "../types/index.js";
|
|
14
14
|
/**
|
|
15
15
|
* What a cue does to the items it names.
|
|
@@ -23,15 +23,15 @@ export type TimedMediaCueActivation = "reveal" | "gate" | "metadata";
|
|
|
23
23
|
/**
|
|
24
24
|
* The condition over delivery state that releases a gate.
|
|
25
25
|
*
|
|
26
|
-
* Named from the shipped
|
|
27
|
-
* the whole reason
|
|
26
|
+
* Named from the shipped Try vocabulary rather than defined here, which is
|
|
27
|
+
* the whole reason item session control sequenced first — see
|
|
28
28
|
* `docs/adr/0001-formative-delivery-before-timed-media.md`. `"responded"` is the
|
|
29
29
|
* response-only case, and it reads item completion rather than Try state so a
|
|
30
|
-
* gate works in a section that does not
|
|
30
|
+
* gate works in a section that does not enable item session control at all.
|
|
31
31
|
*/
|
|
32
32
|
export type TimedMediaGateCondition = "responded" | "correct" | "partial-or-better";
|
|
33
33
|
/**
|
|
34
|
-
* What a gate does when the item cannot be auto-scored — `
|
|
34
|
+
* What a gate does when the item cannot be auto-scored — `TryCorrectness`
|
|
35
35
|
* of `"unknown"`, which is the honest state of an item holding a rubric element
|
|
36
36
|
* rather than a defect to design around.
|
|
37
37
|
*
|
|
@@ -131,13 +131,13 @@ export interface ResolvedTimedMediaSectionData {
|
|
|
131
131
|
scoringPolicy?: TimedMediaScoringPolicy;
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
|
-
* What a gated item's resolved
|
|
134
|
+
* What a gated item's resolved item session control policy leaves the learner.
|
|
135
135
|
*
|
|
136
136
|
* Read by validation, not by the reduction: a gate on correctness over an item
|
|
137
137
|
* with a finite Try budget is a dead end, and so is one over an item that does not
|
|
138
|
-
*
|
|
138
|
+
* enable item session control at all — no Try means no correctness, ever.
|
|
139
139
|
*/
|
|
140
|
-
export type TimedMediaItemTryBudget = "unlimited" | "finite" | "
|
|
140
|
+
export type TimedMediaItemTryBudget = "unlimited" | "finite" | "none";
|
|
141
141
|
export interface TimedMediaValidationError {
|
|
142
142
|
/** Machine-readable, so a host can branch without parsing prose. */
|
|
143
143
|
code: "missing-stimulus-ref" | "unresolved-stimulus-ref"
|
|
@@ -219,7 +219,7 @@ export interface MediaTimeSource {
|
|
|
219
219
|
/**
|
|
220
220
|
* The persisted slice, carried on `SectionControllerSessionState.timedMedia`.
|
|
221
221
|
*
|
|
222
|
-
* One shape live and persisted, unlike
|
|
222
|
+
* One shape live and persisted, unlike the Try state's split between a per-item
|
|
223
223
|
* reducer state and a slice: timed-media state is section-scoped, so there is
|
|
224
224
|
* nothing to key.
|
|
225
225
|
*/
|
|
@@ -241,14 +241,14 @@ export interface TimedMediaSectionSessionSlice {
|
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
243
|
* Delivery state a gate condition reads, supplied by the caller so this module
|
|
244
|
-
* stays pure and depends on neither the controller nor the
|
|
244
|
+
* stays pure and depends on neither the controller nor the item session control module's
|
|
245
245
|
* live state.
|
|
246
246
|
*/
|
|
247
247
|
export interface TimedMediaDeliveryState {
|
|
248
248
|
/** Item completion by canonical id: the `"responded"` condition's signal. */
|
|
249
249
|
respondedByItemId: Record<string, boolean>;
|
|
250
250
|
/** Last Try correctness by canonical id; absent means no Try yet. */
|
|
251
|
-
correctnessByItemId: Record<string,
|
|
251
|
+
correctnessByItemId: Record<string, TryCorrectness | undefined>;
|
|
252
252
|
/** Whether every item in the section reports completion. */
|
|
253
253
|
itemsComplete: boolean;
|
|
254
254
|
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* corpus behind one is licensed per programme, so a host supplies either an endpoint
|
|
14
14
|
* or a resolver of its own.
|
|
15
15
|
*/
|
|
16
|
+
import type { TextDirection } from "../i18n/types.js";
|
|
16
17
|
/** What a host's term service is asked for. */
|
|
17
18
|
export interface TermLookupRequest {
|
|
18
19
|
/** The term, already normalised by {@link normalizeTerm}. */
|
|
@@ -187,3 +188,17 @@ export declare function termPanelStatusMessage<TItem>(state: TermPanelState<TIte
|
|
|
187
188
|
countLabel: (count: number) => string;
|
|
188
189
|
emptyLabel: (term: string) => string;
|
|
189
190
|
}): string;
|
|
191
|
+
/**
|
|
192
|
+
* `lang` and `dir` for a lookup's results: the language the lookup asked in.
|
|
193
|
+
*
|
|
194
|
+
* Results are in the corpus's language, which is neither the panel's interface
|
|
195
|
+
* locale nor necessarily the content the term came from: a Spanish gloss for an
|
|
196
|
+
* English passage. Stamped on the results, an Arabic definition in an English panel
|
|
197
|
+
* reads and lays out as Arabic, and the panel's language never reaches the content.
|
|
198
|
+
* With no language named the corpus's is unknown, so the stamp is `lang=""` and
|
|
199
|
+
* `dir="auto"`, the one undeclared content gets.
|
|
200
|
+
*/
|
|
201
|
+
export declare function termResultLanguage(language: string | undefined): {
|
|
202
|
+
lang: string;
|
|
203
|
+
dir: TextDirection | "auto";
|
|
204
|
+
};
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* corpus behind one is licensed per programme, so a host supplies either an endpoint
|
|
14
14
|
* or a resolver of its own.
|
|
15
15
|
*/
|
|
16
|
+
import { toLanguageTag } from "../i18n/language-tags.js";
|
|
17
|
+
import { localeDirection } from "../i18n/provider.js";
|
|
16
18
|
/** Longest term worth sending. Past this it is prose, not a headword. */
|
|
17
19
|
export const MAX_LOOKUP_TERM_LENGTH = 80;
|
|
18
20
|
/** Words a term may span. Generous enough for `carbon dioxide` and `mother in law`. */
|
|
@@ -282,3 +284,17 @@ export function termPanelStatusMessage(state, labels) {
|
|
|
282
284
|
return labels.emptyLabel(state.term);
|
|
283
285
|
return "";
|
|
284
286
|
}
|
|
287
|
+
/**
|
|
288
|
+
* `lang` and `dir` for a lookup's results: the language the lookup asked in.
|
|
289
|
+
*
|
|
290
|
+
* Results are in the corpus's language, which is neither the panel's interface
|
|
291
|
+
* locale nor necessarily the content the term came from: a Spanish gloss for an
|
|
292
|
+
* English passage. Stamped on the results, an Arabic definition in an English panel
|
|
293
|
+
* reads and lays out as Arabic, and the panel's language never reaches the content.
|
|
294
|
+
* With no language named the corpus's is unknown, so the stamp is `lang=""` and
|
|
295
|
+
* `dir="auto"`, the one undeclared content gets.
|
|
296
|
+
*/
|
|
297
|
+
export function termResultLanguage(language) {
|
|
298
|
+
const tag = toLanguageTag(language);
|
|
299
|
+
return tag ? { lang: tag, dir: localeDirection(tag) } : { lang: "", dir: "auto" };
|
|
300
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* them. The module depends on nothing beyond this package's own type modules,
|
|
9
9
|
* so front-end and server code can both import it.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
11
|
+
import type { ItemSessionControlPolicy, TrySectionSlice } from "../item-session-control/types.js";
|
|
12
12
|
import type { TimedMediaSectionData, TimedMediaSectionSessionSlice } from "../timed-media/types.js";
|
|
13
|
-
import type { AdaptationLink, LanguagePolicy } from "./interchange.js";
|
|
14
|
-
export type {
|
|
13
|
+
import type { AdaptationLink, CurriculumStandard, LanguagePolicy, SkillLevel } from "./interchange.js";
|
|
14
|
+
export type { ItemSessionControlPolicy };
|
|
15
15
|
export type { TimedMediaSectionData };
|
|
16
|
-
export type { AdaptationLink, ContentSubstitution, LanguagePolicy, ServedContentIdentity, } from "./interchange.js";
|
|
16
|
+
export type { AdaptationLink, ContentSubstitution, CurriculumStandard, LanguagePolicy, ServedContentIdentity, SkillLevel, } from "./interchange.js";
|
|
17
17
|
export interface BaseEntity {
|
|
18
18
|
id?: string;
|
|
19
19
|
createdAt?: Date;
|
|
@@ -76,6 +76,13 @@ export interface PassageEntity extends VersionEntity, ConfigContainerEntity {
|
|
|
76
76
|
isAdaptationOf?: AdaptationLink;
|
|
77
77
|
/** QTI/APIP-style accessibility catalogs owned by this passage content. */
|
|
78
78
|
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
79
|
+
/**
|
|
80
|
+
* Standards and skills the content is aligned to. Absent means no data; `[]`
|
|
81
|
+
* means the writer states that none applies. No player reads it.
|
|
82
|
+
*/
|
|
83
|
+
curriculumStandards?: CurriculumStandard[];
|
|
84
|
+
/** DOK and Bloom's classifications, on the same terms. No player reads it. */
|
|
85
|
+
skillLevels?: SkillLevel[];
|
|
79
86
|
retired?: boolean;
|
|
80
87
|
published?: boolean;
|
|
81
88
|
}
|
|
@@ -96,6 +103,13 @@ export interface ItemEntity extends VersionEntity, ConfigContainerEntity {
|
|
|
96
103
|
passage?: string | PassageEntity | null;
|
|
97
104
|
/** QTI/APIP-style accessibility catalogs owned by the item root. */
|
|
98
105
|
accessibilityCatalogs?: AccessibilityCatalog[];
|
|
106
|
+
/**
|
|
107
|
+
* Standards and skills the content is aligned to. Absent means no data; `[]`
|
|
108
|
+
* means the writer states that none applies. No player reads it.
|
|
109
|
+
*/
|
|
110
|
+
curriculumStandards?: CurriculumStandard[];
|
|
111
|
+
/** DOK and Bloom's classifications, on the same terms. No player reads it. */
|
|
112
|
+
skillLevels?: SkillLevel[];
|
|
99
113
|
retired?: boolean;
|
|
100
114
|
published?: boolean;
|
|
101
115
|
configuration?: any;
|
|
@@ -161,13 +175,13 @@ export interface AssessmentItemRef extends BaseEntity {
|
|
|
161
175
|
/** Item-level settings for tool requirements and customization */
|
|
162
176
|
settings?: ItemSettings;
|
|
163
177
|
/**
|
|
164
|
-
*
|
|
165
|
-
* section's `
|
|
178
|
+
* Item session control override for this item. Every field overrides the
|
|
179
|
+
* section's `itemSessionControl` policy independently, which is the order QTI 3 uses
|
|
166
180
|
* for `qti-item-session-control` on an item ref versus its section.
|
|
167
181
|
*
|
|
168
|
-
* See `docs/prds/
|
|
182
|
+
* See `docs/prds/item-session-control-contract.md`.
|
|
169
183
|
*/
|
|
170
|
-
|
|
184
|
+
itemSessionControl?: ItemSessionControlPolicy;
|
|
171
185
|
}
|
|
172
186
|
export interface AssessmentSection extends BaseEntity {
|
|
173
187
|
id?: string;
|
|
@@ -194,15 +208,15 @@ export interface AssessmentSection extends BaseEntity {
|
|
|
194
208
|
assessmentItemRefs?: AssessmentItemRef[];
|
|
195
209
|
rubricBlocks?: RubricBlock[];
|
|
196
210
|
/**
|
|
197
|
-
*
|
|
211
|
+
* Item session control policy for this section: how many Tries a learner gets
|
|
198
212
|
* on each item, when feedback is revealed, and whether the behavior is on at
|
|
199
213
|
* all. Absent or `enabled: false` delivers exactly as a section without this
|
|
200
214
|
* field — no control, no state, no env override.
|
|
201
215
|
*
|
|
202
|
-
* Individual items override it through `AssessmentItemRef.
|
|
203
|
-
* `docs/prds/
|
|
216
|
+
* Individual items override it through `AssessmentItemRef.itemSessionControl`. See
|
|
217
|
+
* `docs/prds/item-session-control-contract.md`.
|
|
204
218
|
*/
|
|
205
|
-
|
|
219
|
+
itemSessionControl?: ItemSessionControlPolicy;
|
|
206
220
|
/**
|
|
207
221
|
* The section flavor. Absent for every section authored so far, and the only
|
|
208
222
|
* value is `"timed-media"`, which pairs with `timedMedia` below.
|
|
@@ -244,19 +258,19 @@ export interface SectionControllerSessionState {
|
|
|
244
258
|
visitedItemIdentifiers?: string[];
|
|
245
259
|
itemSessions: Record<string, unknown>;
|
|
246
260
|
/**
|
|
247
|
-
*
|
|
261
|
+
* Try state — Try counts and reveal state per item.
|
|
248
262
|
*
|
|
249
263
|
* Optional, and its absence is indistinguishable from a snapshot saved
|
|
250
|
-
* before
|
|
264
|
+
* before item session control existed, which is what keeps existing persisted
|
|
251
265
|
* sessions valid. A slice with an unrecognized `version` is rejected whole
|
|
252
|
-
* and
|
|
253
|
-
* unaffected, so a
|
|
266
|
+
* and Try state starts clean; `itemSessions` in the same snapshot is
|
|
267
|
+
* unaffected, so a Try slice version bump never costs a learner responses.
|
|
254
268
|
*/
|
|
255
|
-
|
|
269
|
+
tries?: TrySectionSlice;
|
|
256
270
|
/**
|
|
257
271
|
* Timed-media delivery state — media progress, cue visits and completions.
|
|
258
272
|
*
|
|
259
|
-
* Same posture as `
|
|
273
|
+
* Same posture as `tries`: optional, absence indistinguishable from a
|
|
260
274
|
* pre-timed-media snapshot, and an unrecognized `version` rejected whole so
|
|
261
275
|
* cue progress restarts while item sessions in the same snapshot are applied
|
|
262
276
|
* untouched.
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Interchange types: backends and authoring tools exchange them, and no player
|
|
3
|
+
* reads them. Their tier is listed on the content-model page.
|
|
4
4
|
*
|
|
5
|
+
* Language variants (PIE-1393): no player reads `AdaptationLink` or
|
|
6
|
+
* `LanguagePolicy`.
|
|
5
7
|
* A language variant is its own item or passage version that declares
|
|
6
8
|
* `language` and, when it is a translation, `isAdaptationOf`. Its family is
|
|
7
9
|
* `isAdaptationOf.baseId ?? baseId`.
|
|
@@ -40,8 +42,13 @@ export interface LanguagePolicy {
|
|
|
40
42
|
*/
|
|
41
43
|
staleness: "serve" | "require-current";
|
|
42
44
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Why the served content differs from what was requested: `"fallback"`, not
|
|
47
|
+
* in a requested language; `"variety"`, in another variety of a requested
|
|
48
|
+
* language, such as `es-ES` for `es-MX`; `"stale"`, a translation of an older
|
|
49
|
+
* source release.
|
|
50
|
+
*/
|
|
51
|
+
export type ContentSubstitution = "fallback" | "stale" | "variety";
|
|
45
52
|
/** The content version a backend served, and the language it served it in. */
|
|
46
53
|
export interface ServedContentIdentity {
|
|
47
54
|
baseId: string;
|
|
@@ -52,3 +59,58 @@ export interface ServedContentIdentity {
|
|
|
52
59
|
servedLanguage?: string;
|
|
53
60
|
substitution?: ContentSubstitution;
|
|
54
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* One curriculum standard, skill or competency the content is aligned to
|
|
64
|
+
* (PIE-1381). QTI 3 Curriculum Standards Metadata (CSM 1.1), one entry per
|
|
65
|
+
* `labelledGUID`. Content records direct links only; a framework provider
|
|
66
|
+
* resolves parents, crosswalks and successors on read.
|
|
67
|
+
*/
|
|
68
|
+
export interface CurriculumStandard {
|
|
69
|
+
/**
|
|
70
|
+
* CSM `curriculumStandardsMetadata@providerId`: who issued `guid`, from the
|
|
71
|
+
* provider-id registry on the content-model page, or a host value. Absent
|
|
72
|
+
* means `"AB"` (Academic Benchmarks).
|
|
73
|
+
*/
|
|
74
|
+
providerId?: string;
|
|
75
|
+
/**
|
|
76
|
+
* CSM `labelledGUID/GUID`, as the provider issued it. Stored with its case
|
|
77
|
+
* and compared case-insensitively.
|
|
78
|
+
*/
|
|
79
|
+
guid: string;
|
|
80
|
+
/** CSM `labelledGUID/caseItemURI`, for a CASE framework item. */
|
|
81
|
+
caseItemURI?: string;
|
|
82
|
+
/** CSM `labelledGUID/label`: a display code. Never a key. */
|
|
83
|
+
label?: string;
|
|
84
|
+
/** LOM classification purpose. Absent means `"educational objective"`. */
|
|
85
|
+
purpose?: "educational objective" | "prerequisite" | (string & {});
|
|
86
|
+
/** The record's main standard for this provider. At most one per `providerId`. */
|
|
87
|
+
primary?: boolean;
|
|
88
|
+
/** How the link was made. */
|
|
89
|
+
provenance?: "authored" | "derived" | "imported";
|
|
90
|
+
/**
|
|
91
|
+
* For a derived link: the link it was generated from. An absent
|
|
92
|
+
* `providerId` means `"AB"`.
|
|
93
|
+
*/
|
|
94
|
+
derivedFrom?: {
|
|
95
|
+
providerId?: string;
|
|
96
|
+
guid: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A cognitive-complexity classification of the content (PIE-1381). LOM
|
|
101
|
+
* classification purpose "skill level".
|
|
102
|
+
*/
|
|
103
|
+
export interface SkillLevel {
|
|
104
|
+
/**
|
|
105
|
+
* LOM `taxonPath/source`: `"webb-dok"`, `"blooms-revised"`,
|
|
106
|
+
* `"blooms-original"`, or a host taxonomy.
|
|
107
|
+
*/
|
|
108
|
+
source: string;
|
|
109
|
+
/**
|
|
110
|
+
* LOM `taxon/id`: `"1"` to `"4"` for `webb-dok`; `"remember"` to `"create"`
|
|
111
|
+
* for `blooms-revised`.
|
|
112
|
+
*/
|
|
113
|
+
id: string;
|
|
114
|
+
/** PIE extension: the author's justification. */
|
|
115
|
+
rationale?: string;
|
|
116
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FontAwesome for `<nds-icon-button>`, which renders its glyph as
|
|
3
|
+
* `<i class="fa-light fa-${name}">` and ships no font of its own.
|
|
4
|
+
*
|
|
5
|
+
* FontAwesome comes from the host page when it links any FA stylesheet.
|
|
6
|
+
* Otherwise `ensureFontAwesome` adds FA Free from the ESM CDN the item player's
|
|
7
|
+
* `loaderOptions` name, jsDelivr by default. Pro is never fetched from here:
|
|
8
|
+
* ui.renaissance.com serves its font files to Renaissance origins only, so a
|
|
9
|
+
* host that licenses it links it itself, as section-demos does through its dev
|
|
10
|
+
* proxy.
|
|
11
|
+
*
|
|
12
|
+
* `fa-light` is a weight only Pro carries. Free has no `.fa-light` rule and its
|
|
13
|
+
* Regular font lacks most toolbar glyphs, so without Pro `applyNdsGlyphVariant`
|
|
14
|
+
* renders every glyph in Solid, the one Free weight that has them all.
|
|
15
|
+
*
|
|
16
|
+
* Roboto comes from the vendored button: its connectedCallback links
|
|
17
|
+
* ui.renaissance.com's Roboto unless the page links a stylesheet whose URL
|
|
18
|
+
* contains `Roboto`.
|
|
19
|
+
*/
|
|
20
|
+
export type NdsGlyphVariant = "fa-light" | "fa-regular" | "fa-solid";
|
|
21
|
+
/**
|
|
22
|
+
* Adds FA Free to `<head>` unless the page already links a FontAwesome
|
|
23
|
+
* stylesheet. Appending Free after a host's Pro would override Pro's
|
|
24
|
+
* `.fa-regular` / `.fa-solid` font-family rules page-wide.
|
|
25
|
+
*
|
|
26
|
+
* A stylesheet that fails to load stays in `<head>` marked
|
|
27
|
+
* `data-pie-load-failed`: no later call requests it again, and no shadow root
|
|
28
|
+
* copies it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function ensureFontAwesome(loaderOptions: {
|
|
31
|
+
esmCdnUrl?: unknown;
|
|
32
|
+
esmCdnProvider?: unknown;
|
|
33
|
+
} | null | undefined): void;
|
|
34
|
+
/**
|
|
35
|
+
* Copies the page's FontAwesome `<link>`s into the shadow root `node` renders
|
|
36
|
+
* in, since `<head>` stylesheets do not cross a shadow boundary. Does nothing
|
|
37
|
+
* when `node` is in the document tree, and runs once per shadow root.
|
|
38
|
+
*
|
|
39
|
+
* A stylesheet still loading is copied once it loads, so the copy comes from
|
|
40
|
+
* the cache; one that fails is not copied, since every copy would request it
|
|
41
|
+
* again. Chromium gives a link a `sheet` even when its load fails, so a failed
|
|
42
|
+
* one is known by its mark.
|
|
43
|
+
*/
|
|
44
|
+
export declare function installFontAwesomeInShadow(node: Node): void;
|
|
45
|
+
/**
|
|
46
|
+
* Renders the `fa-light` glyphs inside `button` in `preferred` when the page
|
|
47
|
+
* links Pro, and in Solid otherwise. Re-applies whenever Lit renders or
|
|
48
|
+
* rewrites the icon's class; the class edit removes `fa-light`, so the
|
|
49
|
+
* callback it triggers is a no-op. Returns the observer's disconnect.
|
|
50
|
+
*/
|
|
51
|
+
export declare function applyNdsGlyphVariant(button: HTMLElement, preferred: NdsGlyphVariant): () => void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FontAwesome for `<nds-icon-button>`, which renders its glyph as
|
|
3
|
+
* `<i class="fa-light fa-${name}">` and ships no font of its own.
|
|
4
|
+
*
|
|
5
|
+
* FontAwesome comes from the host page when it links any FA stylesheet.
|
|
6
|
+
* Otherwise `ensureFontAwesome` adds FA Free from the ESM CDN the item player's
|
|
7
|
+
* `loaderOptions` name, jsDelivr by default. Pro is never fetched from here:
|
|
8
|
+
* ui.renaissance.com serves its font files to Renaissance origins only, so a
|
|
9
|
+
* host that licenses it links it itself, as section-demos does through its dev
|
|
10
|
+
* proxy.
|
|
11
|
+
*
|
|
12
|
+
* `fa-light` is a weight only Pro carries. Free has no `.fa-light` rule and its
|
|
13
|
+
* Regular font lacks most toolbar glyphs, so without Pro `applyNdsGlyphVariant`
|
|
14
|
+
* renders every glyph in Solid, the one Free weight that has them all.
|
|
15
|
+
*
|
|
16
|
+
* Roboto comes from the vendored button: its connectedCallback links
|
|
17
|
+
* ui.renaissance.com's Roboto unless the page links a stylesheet whose URL
|
|
18
|
+
* contains `Roboto`.
|
|
19
|
+
*/
|
|
20
|
+
import { resolveEsmPackageFileUrl } from "../loaders/esm-adapter.js";
|
|
21
|
+
/** The one FA Free build the players add; `<package>@<version>/<path>`. */
|
|
22
|
+
const FONTAWESOME_FREE_FILE = "@fortawesome/fontawesome-free@6.5.2/css/all.min.css";
|
|
23
|
+
// Matches any FA stylesheet the host page links: `fontawesome.min.css`,
|
|
24
|
+
// `font-awesome.css`, a `/_fa-pro/` proxy, `fontawesome-free@…`.
|
|
25
|
+
const FA_HREF_PATTERN = /font.?awesome|fa-?pro/i;
|
|
26
|
+
const FA_FREE_HREF_PATTERN = /fontawesome-free/i;
|
|
27
|
+
const documentStylesheetLinks = () => Array.from(document.querySelectorAll('link[rel="stylesheet"][href]'));
|
|
28
|
+
const documentFontAwesomeLinks = () => documentStylesheetLinks().filter((link) => FA_HREF_PATTERN.test(link.href));
|
|
29
|
+
/** Whether the page links a FontAwesome build other than Free. */
|
|
30
|
+
const pageLinksFontAwesomePro = () => documentFontAwesomeLinks().some((link) => !FA_FREE_HREF_PATTERN.test(link.href));
|
|
31
|
+
/**
|
|
32
|
+
* Adds FA Free to `<head>` unless the page already links a FontAwesome
|
|
33
|
+
* stylesheet. Appending Free after a host's Pro would override Pro's
|
|
34
|
+
* `.fa-regular` / `.fa-solid` font-family rules page-wide.
|
|
35
|
+
*
|
|
36
|
+
* A stylesheet that fails to load stays in `<head>` marked
|
|
37
|
+
* `data-pie-load-failed`: no later call requests it again, and no shadow root
|
|
38
|
+
* copies it.
|
|
39
|
+
*/
|
|
40
|
+
export function ensureFontAwesome(loaderOptions) {
|
|
41
|
+
if (typeof document === "undefined")
|
|
42
|
+
return;
|
|
43
|
+
if (documentFontAwesomeLinks().length > 0)
|
|
44
|
+
return;
|
|
45
|
+
const href = resolveEsmPackageFileUrl(loaderOptions, FONTAWESOME_FREE_FILE);
|
|
46
|
+
const link = document.createElement("link");
|
|
47
|
+
link.rel = "stylesheet";
|
|
48
|
+
link.href = href;
|
|
49
|
+
link.addEventListener("error", () => {
|
|
50
|
+
link.dataset.pieLoadFailed = "";
|
|
51
|
+
}, { once: true });
|
|
52
|
+
document.head.appendChild(link);
|
|
53
|
+
}
|
|
54
|
+
const mirroredShadowRoots = new WeakSet();
|
|
55
|
+
/**
|
|
56
|
+
* Copies the page's FontAwesome `<link>`s into the shadow root `node` renders
|
|
57
|
+
* in, since `<head>` stylesheets do not cross a shadow boundary. Does nothing
|
|
58
|
+
* when `node` is in the document tree, and runs once per shadow root.
|
|
59
|
+
*
|
|
60
|
+
* A stylesheet still loading is copied once it loads, so the copy comes from
|
|
61
|
+
* the cache; one that fails is not copied, since every copy would request it
|
|
62
|
+
* again. Chromium gives a link a `sheet` even when its load fails, so a failed
|
|
63
|
+
* one is known by its mark.
|
|
64
|
+
*/
|
|
65
|
+
export function installFontAwesomeInShadow(node) {
|
|
66
|
+
const shadow = node.getRootNode();
|
|
67
|
+
if (!(shadow instanceof ShadowRoot) || mirroredShadowRoots.has(shadow)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
mirroredShadowRoots.add(shadow);
|
|
71
|
+
const appendLink = (href) => {
|
|
72
|
+
const present = Array.from(shadow.querySelectorAll('link[rel="stylesheet"]')).some((link) => link.href === href);
|
|
73
|
+
if (!href || present)
|
|
74
|
+
return;
|
|
75
|
+
const link = document.createElement("link");
|
|
76
|
+
link.rel = "stylesheet";
|
|
77
|
+
link.href = href;
|
|
78
|
+
shadow.appendChild(link);
|
|
79
|
+
};
|
|
80
|
+
for (const link of documentFontAwesomeLinks()) {
|
|
81
|
+
if ("pieLoadFailed" in link.dataset)
|
|
82
|
+
continue;
|
|
83
|
+
if (link.sheet) {
|
|
84
|
+
appendLink(link.href);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const settle = (event) => {
|
|
88
|
+
link.removeEventListener("load", settle);
|
|
89
|
+
link.removeEventListener("error", settle);
|
|
90
|
+
if (event.type === "load")
|
|
91
|
+
appendLink(link.href);
|
|
92
|
+
};
|
|
93
|
+
link.addEventListener("load", settle);
|
|
94
|
+
link.addEventListener("error", settle);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Renders the `fa-light` glyphs inside `button` in `preferred` when the page
|
|
99
|
+
* links Pro, and in Solid otherwise. Re-applies whenever Lit renders or
|
|
100
|
+
* rewrites the icon's class; the class edit removes `fa-light`, so the
|
|
101
|
+
* callback it triggers is a no-op. Returns the observer's disconnect.
|
|
102
|
+
*/
|
|
103
|
+
export function applyNdsGlyphVariant(button, preferred) {
|
|
104
|
+
const variant = pageLinksFontAwesomePro() ? preferred : "fa-solid";
|
|
105
|
+
if (variant === "fa-light")
|
|
106
|
+
return () => { };
|
|
107
|
+
const apply = () => {
|
|
108
|
+
for (const icon of button.querySelectorAll("i.fa-light")) {
|
|
109
|
+
icon.classList.remove("fa-light");
|
|
110
|
+
icon.classList.add(variant);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
apply();
|
|
114
|
+
const observer = new MutationObserver(apply);
|
|
115
|
+
observer.observe(button, {
|
|
116
|
+
childList: true,
|
|
117
|
+
subtree: true,
|
|
118
|
+
attributes: true,
|
|
119
|
+
attributeFilter: ["class"],
|
|
120
|
+
});
|
|
121
|
+
return () => observer.disconnect();
|
|
122
|
+
}
|