@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.
Files changed (44) hide show
  1. package/README.md +16 -14
  2. package/dist/i18n/catalogs.d.ts +20 -13
  3. package/dist/i18n/catalogs.js +22 -23
  4. package/dist/i18n/language-tags.d.ts +23 -6
  5. package/dist/i18n/language-tags.js +74 -9
  6. package/dist/i18n/messages/en-US.d.ts +21 -4
  7. package/dist/i18n/messages/en-US.js +23 -4
  8. package/dist/i18n/types.d.ts +7 -2
  9. package/dist/index.d.ts +1 -1
  10. package/dist/item-session-control/index.d.ts +16 -0
  11. package/dist/item-session-control/index.js +5 -0
  12. package/dist/{formative → item-session-control}/mastery.d.ts +5 -5
  13. package/dist/{formative → item-session-control}/mastery.js +3 -3
  14. package/dist/{formative → item-session-control}/outcome.d.ts +3 -3
  15. package/dist/{formative → item-session-control}/outcome.js +2 -2
  16. package/dist/{formative → item-session-control}/policy.d.ts +11 -11
  17. package/dist/{formative → item-session-control}/policy.js +12 -12
  18. package/dist/item-session-control/session.d.ts +19 -0
  19. package/dist/{formative → item-session-control}/session.js +12 -12
  20. package/dist/{formative → item-session-control}/state.d.ts +26 -26
  21. package/dist/{formative → item-session-control}/state.js +13 -13
  22. package/dist/{formative → item-session-control}/types.d.ts +38 -40
  23. package/dist/{formative → item-session-control}/types.js +2 -2
  24. package/dist/security/private-hosts.d.ts +27 -0
  25. package/dist/security/private-hosts.js +85 -0
  26. package/dist/timed-media/index.d.ts +1 -1
  27. package/dist/timed-media/index.js +1 -1
  28. package/dist/timed-media/policy.d.ts +2 -2
  29. package/dist/timed-media/policy.js +2 -2
  30. package/dist/timed-media/session.d.ts +1 -1
  31. package/dist/timed-media/session.js +1 -1
  32. package/dist/timed-media/types.d.ts +11 -11
  33. package/dist/tools/term-lookup.d.ts +15 -0
  34. package/dist/tools/term-lookup.js +16 -0
  35. package/dist/types/index.d.ts +32 -18
  36. package/dist/types/interchange.d.ts +66 -4
  37. package/dist/ui/fontawesome.d.ts +51 -0
  38. package/dist/ui/fontawesome.js +122 -0
  39. package/package.json +15 -6
  40. package/dist/formative/index.d.ts +0 -16
  41. package/dist/formative/index.js +0 -5
  42. package/dist/formative/session.d.ts +0 -19
  43. package/dist/i18n/messages/nl-NL.d.ts +0 -639
  44. 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 formative state
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 formative state
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 formative policy leaves, so a gate on
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 formative policy resolution is that contract's, not this one's.
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 deliver formatively there is never a Try at all — in both cases
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 formative delivery with maxTries: "unlimited" — otherwise a learner who runs out of Tries can never release playback. Offending item refs: ${unpassable.join(", ")}.`,
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 formative slice: an unreadable slice is rejected whole and
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 formative slice: an unreadable slice is rejected whole and
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 { FormativeCorrectness } from "../formative/types.js";
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 formative vocabulary rather than defined here, which is
27
- * the whole reason formative delivery sequenced first — see
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 deliver formatively at all.
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 — `FormativeCorrectness`
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 formative policy leaves the learner.
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
- * deliver formatively at all — no Try means no correctness, ever.
138
+ * enable item session control at all — no Try means no correctness, ever.
139
139
  */
140
- export type TimedMediaItemTryBudget = "unlimited" | "finite" | "not-formative";
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 formative's split between a per-item
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 formative module's
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, FormativeCorrectness | undefined>;
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
+ }
@@ -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 { FormativeDeliveryPolicy, FormativeItemPolicy, FormativeSectionSlice } from "../formative/types.js";
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 { FormativeDeliveryPolicy, FormativeItemPolicy };
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
- * Formative delivery override for this item. Every field overrides the
165
- * section's `formative` policy independently, which is the order QTI 3 uses
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/formative-delivery-contract.md`.
182
+ * See `docs/prds/item-session-control-contract.md`.
169
183
  */
170
- formative?: FormativeItemPolicy;
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
- * Formative delivery policy for this section: how many Tries a learner gets
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.formative`. See
203
- * `docs/prds/formative-delivery-contract.md`.
216
+ * Individual items override it through `AssessmentItemRef.itemSessionControl`. See
217
+ * `docs/prds/item-session-control-contract.md`.
204
218
  */
205
- formative?: FormativeDeliveryPolicy;
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
- * Formative delivery state — Try counts and reveal state per item.
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 formative delivery existed, which is what keeps existing persisted
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 formative state starts clean; `itemSessions` in the same snapshot is
253
- * unaffected, so a formative version bump never costs a learner responses.
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
- formative?: FormativeSectionSlice;
269
+ tries?: TrySectionSlice;
256
270
  /**
257
271
  * Timed-media delivery state — media progress, cue visits and completions.
258
272
  *
259
- * Same posture as `formative`: optional, absence indistinguishable from a
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
- * Language-variant interchange types (PIE-1393). Backends and authoring tools
3
- * exchange them; no player reads `AdaptationLink` or `LanguagePolicy`.
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
- /** Why the served content differs from what was requested. */
44
- export type ContentSubstitution = "fallback" | "stale";
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
+ }