@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
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Shared runtime utilities for the `@pie-players/*` player suite: the element
|
|
4
4
|
loader, PIE config and session helpers, markup security, i18n, and the
|
|
5
|
-
|
|
5
|
+
item session control, timed-media and media contracts. For framework code and host
|
|
6
6
|
integrations that need the helpers the player packages use.
|
|
7
7
|
|
|
8
8
|
## Public Exports
|
|
@@ -20,9 +20,10 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
20
20
|
| `@pie-players/pie-players-shared` | The root: the loader, PIE, security and object helpers, loader config, instrumentation providers, the item-player strategy helpers, the default bundle host URL (`BUILDER_BUNDLE_URL`), a subset of the shared types, and the UI helpers (attribute coercion, content styles, pointer gestures, overlay placement, focus trap, safe storage, scoped CSS) |
|
|
21
21
|
| `/loader-config` | `LoaderConfig` and its defaults: instrumentation (`trackPageActions`, `instrumentationProvider`), resource retry, and IIFE bundle retry |
|
|
22
22
|
| `/security` | The item-markup sanitizer (`sanitizeItemMarkup`, `createDefaultItemMarkupSanitizer`, `buildAuthoringAllowList`), external-stylesheet URL validation, the SVG icon and `style` attribute sanitizers, and the wrappers that put overwide images and tables in a scrollable region |
|
|
23
|
+
| `/security/private-hosts` | `isPrivateHostname` and `isCloudMetadataHostname`, the host classification for a URL a server fetches on someone else's behalf. Node-safe: it loads none of the markup sanitizers |
|
|
23
24
|
| `/object` | `mergeObjectsIgnoringNullUndefined`, `cloneDeep`, `isPlainRecord` |
|
|
24
25
|
| `/types` | The shared types: item, passage, section and session entities, PIE models and controllers, accessibility catalogs, and the [media vocabulary](#media-validation) |
|
|
25
|
-
| `/
|
|
26
|
+
| `/item-session-control` | [Item session control](#item-session-control) |
|
|
26
27
|
| `/timed-media` | [Timed media](#timed-media) |
|
|
27
28
|
| `/media` | [Media validation](#media-validation) |
|
|
28
29
|
| `/pie` | PIE runtime utilities: config helpers, bundle initialization, `ItemController` and the item session contract, session commit and snapshots, authoring helpers, math renderer setup, instrumentation event maps ([PIE utilities](src/pie/README.md)) |
|
|
@@ -33,14 +34,15 @@ The subpaths below are the package's `exports`; nothing else is importable.
|
|
|
33
34
|
| `/i18n/provider` | `SimpleI18n`, `getDefaultI18n` and `resolveInterfaceI18n`, with the English catalog only |
|
|
34
35
|
| `/i18n/catalogs` | `BUNDLED_LOCALES` and lazy loaders for the bundled locales other than English |
|
|
35
36
|
| `/i18n/language-tags` | BCP 47 language tag normalization and matching |
|
|
36
|
-
| `/nds-icon-button` | `<nds-icon-button>`, an icon button vendored as a prebuilt bundle with Lit inlined; importing it registers the tag. The assessment toolkit and `@pie-players/pie-tool-tts-inline` render it |
|
|
37
|
+
| `/nds-icon-button` | `<nds-icon-button>`, an icon button vendored as a prebuilt bundle with Lit inlined; importing it registers the tag. The assessment toolkit, the section player and `@pie-players/pie-tool-tts-inline` render it |
|
|
37
38
|
| `/ui/attribute-coercion` | Boolean attribute coercion for custom elements |
|
|
38
39
|
| `/ui/content-styles` | `installContentStyles`, `auditContentStyles` and the opt-out checks behind the item player's content styles |
|
|
40
|
+
| `/ui/fontawesome` | The FontAwesome `<nds-icon-button>` glyphs need: the one FA Free pin, Pro detection, the `<head>` and shadow-root installs, and the glyph weight. The assessment toolkit, the section player and `@pie-players/pie-tool-tts-inline` use it |
|
|
39
41
|
| `/tools/term-lookup` | Term normalization and the lookup session the dictionary tools share; the host supplies the endpoint or a resolver |
|
|
40
42
|
|
|
41
|
-
##
|
|
43
|
+
## Item Session Control
|
|
42
44
|
|
|
43
|
-
`@pie-players/pie-players-shared/
|
|
45
|
+
`@pie-players/pie-players-shared/item-session-control` holds the policy resolution, outcome
|
|
44
46
|
aggregation, Try-state reducer, item view, and mastery rollup that
|
|
45
47
|
`pie-section-player` drives. It is pure (no DOM, no timers, no element
|
|
46
48
|
registry), so it is testable without a browser and importable by an adapter that
|
|
@@ -48,18 +50,18 @@ has no player.
|
|
|
48
50
|
|
|
49
51
|
```ts
|
|
50
52
|
import {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} from "@pie-players/pie-players-shared/
|
|
53
|
+
aggregateTryOutcome,
|
|
54
|
+
resolveItemSessionControl,
|
|
55
|
+
resolveItemTryView,
|
|
56
|
+
rollupMastery,
|
|
57
|
+
} from "@pie-players/pie-players-shared/item-session-control";
|
|
56
58
|
```
|
|
57
59
|
|
|
58
|
-
The authored half, `
|
|
59
|
-
`
|
|
60
|
+
The authored half, `ItemSessionControlPolicy` on `AssessmentSection` and
|
|
61
|
+
`AssessmentItemRef`, is re-exported from the package
|
|
60
62
|
root and from `/types`, beside the section types it annotates.
|
|
61
63
|
|
|
62
|
-
The contract is [`docs/prds/
|
|
64
|
+
The contract is [`docs/prds/item-session-control-contract.md`](../../docs/prds/item-session-control-contract.md),
|
|
63
65
|
including the QTI 3 mapping and why PIE says **Try** rather than "attempt".
|
|
64
66
|
|
|
65
67
|
## Timed Media
|
|
@@ -92,7 +94,7 @@ re-exported from the package root and from `/types`, beside the section types it
|
|
|
92
94
|
annotates.
|
|
93
95
|
|
|
94
96
|
The contract is [`docs/prds/timed-media-section-contract.md`](../../docs/prds/timed-media-section-contract.md),
|
|
95
|
-
including why cue gate conditions name the
|
|
97
|
+
including why cue gate conditions name the Try vocabulary and why an
|
|
96
98
|
unenforceable policy degrades to advisory rather than failing closed.
|
|
97
99
|
|
|
98
100
|
## Media Validation
|
package/dist/i18n/catalogs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The bundled catalog loader: `en-US`, the one statically shipped catalog.
|
|
3
3
|
*
|
|
4
4
|
* Imported by players, never by tools. A tool resolves its provider from the
|
|
5
5
|
* toolkit runtime context and needs only the interface plus the English
|
|
@@ -7,25 +7,32 @@
|
|
|
7
7
|
* into a bundle that never calls `setLocale`, because every tool
|
|
8
8
|
* `vite.config.ts` sets `external: []`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* alongside the file.
|
|
10
|
+
* `en-US` is the source of truth — the fallback for every missing key in every
|
|
11
|
+
* other locale — so the provider imports it statically and it is always
|
|
12
|
+
* resident. It is the only catalog this package ships: every other locale this
|
|
13
|
+
* repository ships lives in the optional generated tier
|
|
14
|
+
* (`@pie-players/pie-i18n-generated`), whose loaders a host composes after
|
|
15
|
+
* this one. The composed shape is that package's README's wiring example:
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
* locale
|
|
17
|
+
* ```ts
|
|
18
|
+
* loadCatalog: (locale) =>
|
|
19
|
+
* loadBundledCatalog(locale).catch(() => loadGeneratedCatalog(locale));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* A request this function cannot serve rejects, and the provider treats that
|
|
23
|
+
* as "resolve through the fallback chain" rather than an error, so a host
|
|
24
|
+
* locale nothing ships still renders English.
|
|
20
25
|
*
|
|
21
26
|
* @module @pie-players/pie-players-shared/i18n/catalogs
|
|
22
27
|
*/
|
|
23
28
|
import type { BundledLocaleCode, MessageCatalog } from "./types.js";
|
|
24
29
|
/**
|
|
25
|
-
* Every locale tag this repository
|
|
30
|
+
* Every locale tag this repository bundles a catalog for. `en-US` alone: the
|
|
31
|
+
* statically resident source of truth.
|
|
26
32
|
*
|
|
27
33
|
* The provider resolves a request against this list through RFC 4647 lookup and
|
|
28
|
-
* primary-subtag widening, so `
|
|
34
|
+
* primary-subtag widening, so `en`, `en_US` and `en-GB`-without-a-catalog all
|
|
35
|
+
* land on `en-US` unless a composed loader serves the region.
|
|
29
36
|
*/
|
|
30
37
|
export declare const BUNDLED_LOCALES: readonly BundledLocaleCode[];
|
|
31
38
|
/**
|
|
@@ -36,7 +43,7 @@ export declare const BUNDLED_LOCALES: readonly BundledLocaleCode[];
|
|
|
36
43
|
* or a pseudo-localization of the English catalog — asks for it by name like
|
|
37
44
|
* any other locale.
|
|
38
45
|
*
|
|
39
|
-
*
|
|
46
|
+
* Rejects for a locale with no bundled catalog; the provider treats that as
|
|
40
47
|
* "resolve through the fallback chain" rather than an error, so a host locale we
|
|
41
48
|
* do not ship still renders.
|
|
42
49
|
*/
|
package/dist/i18n/catalogs.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The bundled catalog loader: `en-US`, the one statically shipped catalog.
|
|
3
3
|
*
|
|
4
4
|
* Imported by players, never by tools. A tool resolves its provider from the
|
|
5
5
|
* toolkit runtime context and needs only the interface plus the English
|
|
@@ -7,30 +7,34 @@
|
|
|
7
7
|
* into a bundle that never calls `setLocale`, because every tool
|
|
8
8
|
* `vite.config.ts` sets `external: []`.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* alongside the file.
|
|
10
|
+
* `en-US` is the source of truth — the fallback for every missing key in every
|
|
11
|
+
* other locale — so the provider imports it statically and it is always
|
|
12
|
+
* resident. It is the only catalog this package ships: every other locale this
|
|
13
|
+
* repository ships lives in the optional generated tier
|
|
14
|
+
* (`@pie-players/pie-i18n-generated`), whose loaders a host composes after
|
|
15
|
+
* this one. The composed shape is that package's README's wiring example:
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
* locale
|
|
17
|
+
* ```ts
|
|
18
|
+
* loadCatalog: (locale) =>
|
|
19
|
+
* loadBundledCatalog(locale).catch(() => loadGeneratedCatalog(locale));
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* A request this function cannot serve rejects, and the provider treats that
|
|
23
|
+
* as "resolve through the fallback chain" rather than an error, so a host
|
|
24
|
+
* locale nothing ships still renders English.
|
|
20
25
|
*
|
|
21
26
|
* @module @pie-players/pie-players-shared/i18n/catalogs
|
|
22
27
|
*/
|
|
23
28
|
import enUS from "./messages/en-US.js";
|
|
24
|
-
const CATALOG_LOADERS = {
|
|
25
|
-
"nl-NL": () => import("./messages/nl-NL.js"),
|
|
26
|
-
};
|
|
27
29
|
/**
|
|
28
|
-
* Every locale tag this repository
|
|
30
|
+
* Every locale tag this repository bundles a catalog for. `en-US` alone: the
|
|
31
|
+
* statically resident source of truth.
|
|
29
32
|
*
|
|
30
33
|
* The provider resolves a request against this list through RFC 4647 lookup and
|
|
31
|
-
* primary-subtag widening, so `
|
|
34
|
+
* primary-subtag widening, so `en`, `en_US` and `en-GB`-without-a-catalog all
|
|
35
|
+
* land on `en-US` unless a composed loader serves the region.
|
|
32
36
|
*/
|
|
33
|
-
export const BUNDLED_LOCALES = ["en-US"
|
|
37
|
+
export const BUNDLED_LOCALES = ["en-US"];
|
|
34
38
|
/**
|
|
35
39
|
* Load one shipped catalog.
|
|
36
40
|
*
|
|
@@ -39,7 +43,7 @@ export const BUNDLED_LOCALES = ["en-US", "nl-NL"];
|
|
|
39
43
|
* or a pseudo-localization of the English catalog — asks for it by name like
|
|
40
44
|
* any other locale.
|
|
41
45
|
*
|
|
42
|
-
*
|
|
46
|
+
* Rejects for a locale with no bundled catalog; the provider treats that as
|
|
43
47
|
* "resolve through the fallback chain" rather than an error, so a host locale we
|
|
44
48
|
* do not ship still renders.
|
|
45
49
|
*/
|
|
@@ -47,10 +51,5 @@ export async function loadBundledCatalog(locale) {
|
|
|
47
51
|
if (locale === "en-US") {
|
|
48
52
|
return enUS;
|
|
49
53
|
}
|
|
50
|
-
|
|
51
|
-
if (!loader) {
|
|
52
|
-
throw new Error(`No bundled i18n catalog for locale: ${locale}`);
|
|
53
|
-
}
|
|
54
|
-
const module = await loader();
|
|
55
|
-
return module.default;
|
|
54
|
+
throw new Error(`No bundled i18n catalog for locale: ${locale}`);
|
|
56
55
|
}
|
|
@@ -78,19 +78,31 @@ export declare function languageTagScriptLookupSequence(tag: string | null | und
|
|
|
78
78
|
* `zh-TW` passes it by.
|
|
79
79
|
*/
|
|
80
80
|
export declare function findBestLanguageMatch(requested: string | null | undefined, available: readonly (string | null | undefined)[]): string | undefined;
|
|
81
|
+
/**
|
|
82
|
+
* A tag without its variant, extension and private-use subtags, casing kept:
|
|
83
|
+
* `en-GB-x-gulf` yields `en-GB`. The form for a speech engine, whose voices
|
|
84
|
+
* are keyed by language and region.
|
|
85
|
+
*/
|
|
86
|
+
export declare function languageTagWithoutVariants(tag: string | null | undefined): string;
|
|
81
87
|
/**
|
|
82
88
|
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
83
|
-
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`.
|
|
84
|
-
* the request's script
|
|
85
|
-
* `sr-
|
|
89
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Widening stays
|
|
90
|
+
* within the request's script and keeps its variant subtags: `zh` never
|
|
91
|
+
* reaches `zh-TW`, `sr-Latn` may reach `sr-ME`, `en-GB` never reaches
|
|
92
|
+
* `en-GB-x-gulf`, and `en-US-x-gulf` may reach `en-GB-x-gulf`. Among several
|
|
93
|
+
* siblings the first in `available` wins. The rule that picks an interface
|
|
94
|
+
* catalog and a content variant alike.
|
|
86
95
|
*/
|
|
87
96
|
export declare function findClosestLanguageMatch(requested: string | null | undefined, available: readonly (string | null | undefined)[]): string | undefined;
|
|
88
97
|
/** The content language a backend serves, and whether it is a substitute. */
|
|
89
98
|
export interface ContentLanguageMatch {
|
|
90
99
|
/** The `available` entry served, as authored; `null` for content that declares no language. */
|
|
91
100
|
language: string | null;
|
|
92
|
-
/**
|
|
93
|
-
|
|
101
|
+
/**
|
|
102
|
+
* `"fallback"` when the served content stands in for the request;
|
|
103
|
+
* `"variety"` when it is another variety of the requested language.
|
|
104
|
+
*/
|
|
105
|
+
substitution?: "fallback" | "variety";
|
|
94
106
|
}
|
|
95
107
|
/**
|
|
96
108
|
* The language to serve content in: the closest `available` match for the
|
|
@@ -100,7 +112,12 @@ export interface ContentLanguageMatch {
|
|
|
100
112
|
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
101
113
|
* then content that declares no language (a `null` entry), then `source`;
|
|
102
114
|
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
103
|
-
* requested languages run the chain without a substitution.
|
|
115
|
+
* requested languages run the chain without a substitution. A closest match
|
|
116
|
+
* in another variety, `es-MX` served `es-ES`, records `"variety"`; a host
|
|
117
|
+
* that must not serve one sets `matching: "exact"` and lists the varieties it
|
|
118
|
+
* accepts. Candidates rank
|
|
119
|
+
* `source` first, then by tag, so the order of `available` never decides
|
|
120
|
+
* between two siblings.
|
|
104
121
|
*/
|
|
105
122
|
export declare function negotiateContentLanguage(requested: readonly string[], available: readonly (string | null)[], options?: {
|
|
106
123
|
defaultLanguage?: string;
|
|
@@ -149,11 +149,59 @@ export function findBestLanguageMatch(requested, available) {
|
|
|
149
149
|
}
|
|
150
150
|
return undefined;
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* A tag's subtags split at the end of its language, script and region:
|
|
154
|
+
* `en-GB-x-gulf` yields `[["en", "GB"], ["x", "gulf"]]`. The second part holds
|
|
155
|
+
* variants, extensions and private use, as authored.
|
|
156
|
+
*/
|
|
157
|
+
function splitVariantSubtags(tag) {
|
|
158
|
+
const subtags = toLanguageTag(tag).split(/[.@]/)[0].split("-");
|
|
159
|
+
let length = 1;
|
|
160
|
+
if (/^[a-z]{4}$/i.test(subtags[length] ?? ""))
|
|
161
|
+
length++;
|
|
162
|
+
if (/^([a-z]{2}|\d{3})$/i.test(subtags[length] ?? ""))
|
|
163
|
+
length++;
|
|
164
|
+
return [subtags.slice(0, length), subtags.slice(length)];
|
|
165
|
+
}
|
|
166
|
+
/** The variant, extension and private-use subtags as a comparison key. */
|
|
167
|
+
function variantSubtagsKey(tag) {
|
|
168
|
+
return splitVariantSubtags(tag)[1].join("-").toLowerCase();
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Whether `served` is another variety of `requested`: it names a different
|
|
172
|
+
* region, or different variant or private-use subtags. `es-MX` served `es-ES`
|
|
173
|
+
* or `en-GB-x-gulf` served `en-GB` is; `es-MX` served `es`, or `zh-TW` served
|
|
174
|
+
* `zh-Hant`, is not.
|
|
175
|
+
*/
|
|
176
|
+
function isOtherLanguageVariety(requested, served) {
|
|
177
|
+
const region = (tag) => {
|
|
178
|
+
const [base] = splitVariantSubtags(tag);
|
|
179
|
+
const last = base.length > 1 ? base[base.length - 1] : "";
|
|
180
|
+
return /^([a-z]{2}|\d{3})$/i.test(last) ? last.toLowerCase() : undefined;
|
|
181
|
+
};
|
|
182
|
+
const requestedRegion = region(requested);
|
|
183
|
+
const servedRegion = region(served);
|
|
184
|
+
return ((requestedRegion !== undefined &&
|
|
185
|
+
servedRegion !== undefined &&
|
|
186
|
+
requestedRegion !== servedRegion) ||
|
|
187
|
+
variantSubtagsKey(requested) !== variantSubtagsKey(served));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* A tag without its variant, extension and private-use subtags, casing kept:
|
|
191
|
+
* `en-GB-x-gulf` yields `en-GB`. The form for a speech engine, whose voices
|
|
192
|
+
* are keyed by language and region.
|
|
193
|
+
*/
|
|
194
|
+
export function languageTagWithoutVariants(tag) {
|
|
195
|
+
return splitVariantSubtags(tag)[0].join("-");
|
|
196
|
+
}
|
|
152
197
|
/**
|
|
153
198
|
* `findBestLanguageMatch`, then primary-subtag widening, so `nl-BE` and a bare
|
|
154
|
-
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`.
|
|
155
|
-
* the request's script
|
|
156
|
-
* `sr-
|
|
199
|
+
* `nl` reach a regional `nl-NL` and `es-MX` reaches `es-ES`. Widening stays
|
|
200
|
+
* within the request's script and keeps its variant subtags: `zh` never
|
|
201
|
+
* reaches `zh-TW`, `sr-Latn` may reach `sr-ME`, `en-GB` never reaches
|
|
202
|
+
* `en-GB-x-gulf`, and `en-US-x-gulf` may reach `en-GB-x-gulf`. Among several
|
|
203
|
+
* siblings the first in `available` wins. The rule that picks an interface
|
|
204
|
+
* catalog and a content variant alike.
|
|
157
205
|
*/
|
|
158
206
|
export function findClosestLanguageMatch(requested, available) {
|
|
159
207
|
const best = findBestLanguageMatch(requested, available);
|
|
@@ -162,7 +210,9 @@ export function findClosestLanguageMatch(requested, available) {
|
|
|
162
210
|
const language = normalizeLanguageTag(requested).split("-")[0];
|
|
163
211
|
if (!language)
|
|
164
212
|
return undefined;
|
|
213
|
+
const variants = variantSubtagsKey(requested);
|
|
165
214
|
const sibling = available.find((candidate) => normalizeLanguageTag(candidate).split("-")[0] === language &&
|
|
215
|
+
variantSubtagsKey(candidate) === variants &&
|
|
166
216
|
languageTagScriptsMatch(requested, candidate));
|
|
167
217
|
return sibling ?? undefined;
|
|
168
218
|
}
|
|
@@ -174,20 +224,35 @@ export function findClosestLanguageMatch(requested, available) {
|
|
|
174
224
|
* chain serves the closest match for `defaultLanguage` (`en-US` reaches `en`),
|
|
175
225
|
* then content that declares no language (a `null` entry), then `source`;
|
|
176
226
|
* `matching` does not narrow it. `undefined` means nothing can be served. No
|
|
177
|
-
* requested languages run the chain without a substitution.
|
|
227
|
+
* requested languages run the chain without a substitution. A closest match
|
|
228
|
+
* in another variety, `es-MX` served `es-ES`, records `"variety"`; a host
|
|
229
|
+
* that must not serve one sets `matching: "exact"` and lists the varieties it
|
|
230
|
+
* accepts. Candidates rank
|
|
231
|
+
* `source` first, then by tag, so the order of `available` never decides
|
|
232
|
+
* between two siblings.
|
|
178
233
|
*/
|
|
179
234
|
export function negotiateContentLanguage(requested, available, options = {}) {
|
|
180
235
|
const { defaultLanguage, fallback = "default", matching = "closest", source } = options;
|
|
236
|
+
const candidates = [...available].sort((a, b) => {
|
|
237
|
+
if ((a === source) !== (b === source))
|
|
238
|
+
return a === source ? -1 : 1;
|
|
239
|
+
const left = normalizeLanguageTag(a);
|
|
240
|
+
const right = normalizeLanguageTag(b);
|
|
241
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
242
|
+
});
|
|
181
243
|
for (const tag of requested) {
|
|
182
244
|
const language = matching === "exact"
|
|
183
|
-
?
|
|
184
|
-
: findClosestLanguageMatch(tag,
|
|
185
|
-
if (language != null)
|
|
186
|
-
return
|
|
245
|
+
? candidates.find((candidate) => languageTagsEqual(candidate, tag))
|
|
246
|
+
: findClosestLanguageMatch(tag, candidates);
|
|
247
|
+
if (language != null) {
|
|
248
|
+
return isOtherLanguageVariety(tag, language)
|
|
249
|
+
? { language, substitution: "variety" }
|
|
250
|
+
: { language };
|
|
251
|
+
}
|
|
187
252
|
}
|
|
188
253
|
if (requested.length > 0 && fallback === "none")
|
|
189
254
|
return undefined;
|
|
190
|
-
let language = findClosestLanguageMatch(defaultLanguage,
|
|
255
|
+
let language = findClosestLanguageMatch(defaultLanguage, candidates);
|
|
191
256
|
if (language === undefined && available.includes(null))
|
|
192
257
|
language = null;
|
|
193
258
|
if (language === undefined && source !== undefined && available.includes(source)) {
|
|
@@ -109,16 +109,16 @@ declare const enUS: {
|
|
|
109
109
|
loadFailed: string;
|
|
110
110
|
restoreFailed: string;
|
|
111
111
|
};
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
/** Item session control: check-answer control and its outcome announcements. */
|
|
113
|
+
check: {
|
|
114
|
+
answer: string;
|
|
115
115
|
tryAgain: string;
|
|
116
116
|
answerRecorded: string;
|
|
117
117
|
correct: string;
|
|
118
118
|
partlyCorrect: string;
|
|
119
119
|
notCorrect: string;
|
|
120
120
|
notAutoScored: string;
|
|
121
|
-
|
|
121
|
+
failed: string;
|
|
122
122
|
/**
|
|
123
123
|
* Appended after the outcome, so the announcement reads
|
|
124
124
|
* "Not correct. 2 tries left." Kept a separate plural group rather than
|
|
@@ -301,6 +301,12 @@ declare const enUS: {
|
|
|
301
301
|
lanthanide: string;
|
|
302
302
|
actinide: string;
|
|
303
303
|
};
|
|
304
|
+
/** Standard-state phase, keyed by the data file's lowercased `phase` value. */
|
|
305
|
+
phaseValue: {
|
|
306
|
+
solid: string;
|
|
307
|
+
liquid: string;
|
|
308
|
+
gas: string;
|
|
309
|
+
};
|
|
304
310
|
};
|
|
305
311
|
ruler: {
|
|
306
312
|
name: string;
|
|
@@ -462,6 +468,17 @@ declare const enUS: {
|
|
|
462
468
|
highlightPinkA11y: string;
|
|
463
469
|
highlightBlueA11y: string;
|
|
464
470
|
highlightGreenA11y: string;
|
|
471
|
+
selectionAvailableA11y: string;
|
|
472
|
+
highlightedYellowA11y: string;
|
|
473
|
+
highlightedPinkA11y: string;
|
|
474
|
+
highlightedBlueA11y: string;
|
|
475
|
+
highlightedGreenA11y: string;
|
|
476
|
+
underlinedA11y: string;
|
|
477
|
+
removedA11y: string;
|
|
478
|
+
clearedA11y: {
|
|
479
|
+
one: string;
|
|
480
|
+
other: string;
|
|
481
|
+
};
|
|
465
482
|
};
|
|
466
483
|
theme: {
|
|
467
484
|
name: string;
|
|
@@ -109,16 +109,16 @@ const enUS = {
|
|
|
109
109
|
loadFailed: "The assessment could not be loaded. Try again.",
|
|
110
110
|
restoreFailed: "Your saved answers for this section could not be restored. Try again.",
|
|
111
111
|
},
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
/** Item session control: check-answer control and its outcome announcements. */
|
|
113
|
+
check: {
|
|
114
|
+
answer: "Check answer",
|
|
115
115
|
tryAgain: "Try again",
|
|
116
116
|
answerRecorded: "Answer recorded.",
|
|
117
117
|
correct: "Correct.",
|
|
118
118
|
partlyCorrect: "Partly correct.",
|
|
119
119
|
notCorrect: "Not correct.",
|
|
120
120
|
notAutoScored: "Answer recorded. This question is not scored automatically.",
|
|
121
|
-
|
|
121
|
+
failed: "This question could not be checked. Try again.",
|
|
122
122
|
/**
|
|
123
123
|
* Appended after the outcome, so the announcement reads
|
|
124
124
|
* "Not correct. 2 tries left." Kept a separate plural group rather than
|
|
@@ -306,6 +306,12 @@ const enUS = {
|
|
|
306
306
|
lanthanide: "Lanthanide",
|
|
307
307
|
actinide: "Actinide",
|
|
308
308
|
},
|
|
309
|
+
/** Standard-state phase, keyed by the data file's lowercased `phase` value. */
|
|
310
|
+
phaseValue: {
|
|
311
|
+
solid: "Solid",
|
|
312
|
+
liquid: "Liquid",
|
|
313
|
+
gas: "Gas",
|
|
314
|
+
},
|
|
309
315
|
},
|
|
310
316
|
ruler: {
|
|
311
317
|
name: "Ruler",
|
|
@@ -482,6 +488,19 @@ const enUS = {
|
|
|
482
488
|
highlightPinkA11y: "Pink highlight",
|
|
483
489
|
highlightBlueA11y: "Blue highlight",
|
|
484
490
|
highlightGreenA11y: "Green highlight",
|
|
491
|
+
// Announced in the strip's live region. A colour is authored per phrase:
|
|
492
|
+
// a colour adjective agrees with its noun in some languages.
|
|
493
|
+
selectionAvailableA11y: "Annotation toolbar available for “{text}”. Press Shift+F10 for annotation tools.",
|
|
494
|
+
highlightedYellowA11y: "“{text}” highlighted in yellow",
|
|
495
|
+
highlightedPinkA11y: "“{text}” highlighted in pink",
|
|
496
|
+
highlightedBlueA11y: "“{text}” highlighted in blue",
|
|
497
|
+
highlightedGreenA11y: "“{text}” highlighted in green",
|
|
498
|
+
underlinedA11y: "“{text}” underlined",
|
|
499
|
+
removedA11y: "Removed annotation from “{text}”",
|
|
500
|
+
clearedA11y: {
|
|
501
|
+
one: "{count} annotation cleared",
|
|
502
|
+
other: "{count} annotations cleared",
|
|
503
|
+
},
|
|
485
504
|
},
|
|
486
505
|
theme: {
|
|
487
506
|
name: "Theme",
|
package/dist/i18n/types.d.ts
CHANGED
|
@@ -16,8 +16,13 @@ import type enUS from "./messages/en-US.js";
|
|
|
16
16
|
* syntax — the Learnosity transform emits POSIX `nl_NL`, QTI catalog cards carry
|
|
17
17
|
* `xml:lang` — and the resolver normalizes both onto these, so this list is the
|
|
18
18
|
* set of catalogs, not the set of accepted inputs.
|
|
19
|
+
*
|
|
20
|
+
* `en-US` is the only bundled catalog: the statically resident source of truth
|
|
21
|
+
* every other locale falls back through. Every other locale this repository
|
|
22
|
+
* ships lives in the optional generated tier
|
|
23
|
+
* (`@pie-players/pie-i18n-generated`), which a host composes into its loader.
|
|
19
24
|
*/
|
|
20
|
-
export type BundledLocaleCode = "en-US"
|
|
25
|
+
export type BundledLocaleCode = "en-US";
|
|
21
26
|
/**
|
|
22
27
|
* Any locale tag. A host may supply a locale we ship no catalog for and provide
|
|
23
28
|
* its own messages through {@link I18nConfig.customMessages}, so this is
|
|
@@ -52,7 +57,7 @@ type PluralCategoryKey = keyof PluralTranslation;
|
|
|
52
57
|
* Dot-notation keys of a catalog shape.
|
|
53
58
|
*
|
|
54
59
|
* A plural group terminates the path at the group itself: `plural()` is called
|
|
55
|
-
* with `player.
|
|
60
|
+
* with `player.check.triesLeft`, never with `…triesLeft.other`. A group is
|
|
56
61
|
* recognized by every one of its keys being a CLDR plural category — the
|
|
57
62
|
* containment has to run this way round, because a group carrying only
|
|
58
63
|
* `one`/`other` does not contain the whole category union.
|
package/dist/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export * from "./security/index.js";
|
|
|
6
6
|
export * from "./loaders/index.js";
|
|
7
7
|
export * from "./object/index.js";
|
|
8
8
|
export * from "./pie/index.js";
|
|
9
|
-
export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env,
|
|
9
|
+
export type { AssessmentEntity, AssessmentItemRef, AssessmentSection, AdvancedItemConfig, ConfigEntity, ConfigResource, Env, ItemSessionControlPolicy, TimedMediaSectionData, ItemConfig, ItemEntity, ItemSession, OutcomeResponse, PassageEntity, PieController, PieContent, PieDefaultModel, PieItemElement, PieModel, PieUpdateSession, ReferencedItemEntity, ReferencedPassageEntity, RubricBlock, TestPart, } from "./types/index.js";
|
|
10
10
|
export { editorPostFix } from "./types/index.js";
|
|
11
11
|
export * from "./ui/attribute-coercion.js";
|
|
12
12
|
export * from "./ui/content-styles.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Item session control: Try state, feedback reveal, and section mastery.
|
|
3
|
+
*
|
|
4
|
+
* Pure and Node-safe by design — no DOM, no timers, no element registry — so
|
|
5
|
+
* the contract is testable without a browser and an adapter can import it
|
|
6
|
+
* without pulling in a player. Everything that touches the DOM stays in
|
|
7
|
+
* `@pie-players/pie-section-player`.
|
|
8
|
+
*
|
|
9
|
+
* Contract: `docs/prds/item-session-control-contract.md`.
|
|
10
|
+
*/
|
|
11
|
+
export type { TryAction, TryCorrectness, ItemSessionControlPolicy, FeedbackEnvOverride, FeedbackReveal, ItemTryState, ItemTryView, MasteryRollup, FeedbackRevealTiming, ScoredElementOutcome, TrySectionProjection, TrySectionSlice, TryLimit, TryOutcome, ResolvedItemSessionControlPolicy, } from "./types.js";
|
|
12
|
+
export { ITEM_SESSION_CONTROL_DEFAULTS, isItemSessionControlEnabledInSection, resolveItemSessionControls, resolveItemSessionControl, } from "./policy.js";
|
|
13
|
+
export { aggregateTryOutcome } from "./outcome.js";
|
|
14
|
+
export { ITEM_TRY_STATE_VERSION, createItemTryState, hideItemFeedback, recordTry, resolveItemTryView, retryItem, revealItemFeedback, } from "./state.js";
|
|
15
|
+
export { MASTERY_ROLLUP_VERSION, rollupMastery } from "./mastery.js";
|
|
16
|
+
export { TRY_SLICE_VERSION, normalizeTrySectionSlice, toTrySectionSlice, } from "./session.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ITEM_SESSION_CONTROL_DEFAULTS, isItemSessionControlEnabledInSection, resolveItemSessionControls, resolveItemSessionControl, } from "./policy.js";
|
|
2
|
+
export { aggregateTryOutcome } from "./outcome.js";
|
|
3
|
+
export { ITEM_TRY_STATE_VERSION, createItemTryState, hideItemFeedback, recordTry, resolveItemTryView, retryItem, revealItemFeedback, } from "./state.js";
|
|
4
|
+
export { MASTERY_ROLLUP_VERSION, rollupMastery } from "./mastery.js";
|
|
5
|
+
export { TRY_SLICE_VERSION, normalizeTrySectionSlice, toTrySectionSlice, } from "./session.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare const
|
|
1
|
+
import type { ItemTryState, MasteryRollup } from "./types.js";
|
|
2
|
+
export declare const MASTERY_ROLLUP_VERSION: 1;
|
|
3
3
|
/**
|
|
4
4
|
* Roll Try outcomes up to the section.
|
|
5
5
|
*
|
|
@@ -10,7 +10,7 @@ export declare const FORMATIVE_MASTERY_VERSION: 1;
|
|
|
10
10
|
* excluded: nothing yet says it cannot be scored, so it keeps the section from
|
|
11
11
|
* reading as complete after one correct answer.
|
|
12
12
|
*/
|
|
13
|
-
export declare function
|
|
13
|
+
export declare function rollupMastery(args: {
|
|
14
14
|
itemIdentifiers: readonly string[];
|
|
15
|
-
states: Record<string,
|
|
16
|
-
}):
|
|
15
|
+
states: Record<string, ItemTryState>;
|
|
16
|
+
}): MasteryRollup;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const
|
|
1
|
+
export const MASTERY_ROLLUP_VERSION = 1;
|
|
2
2
|
/**
|
|
3
3
|
* Roll Try outcomes up to the section.
|
|
4
4
|
*
|
|
@@ -9,7 +9,7 @@ export const FORMATIVE_MASTERY_VERSION = 1;
|
|
|
9
9
|
* excluded: nothing yet says it cannot be scored, so it keeps the section from
|
|
10
10
|
* reading as complete after one correct answer.
|
|
11
11
|
*/
|
|
12
|
-
export function
|
|
12
|
+
export function rollupMastery(args) {
|
|
13
13
|
const identifiers = args.itemIdentifiers ?? [];
|
|
14
14
|
const totalItems = identifiers.length;
|
|
15
15
|
let notScorableItems = 0;
|
|
@@ -31,7 +31,7 @@ export function rollupFormativeMastery(args) {
|
|
|
31
31
|
}
|
|
32
32
|
const scorableItems = Math.max(0, totalItems - notScorableItems);
|
|
33
33
|
return {
|
|
34
|
-
version:
|
|
34
|
+
version: MASTERY_ROLLUP_VERSION,
|
|
35
35
|
totalItems,
|
|
36
36
|
scorableItems,
|
|
37
37
|
masteredItems,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScoredElementOutcome, TryOutcome } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Reduce the array `pie-item-player.provideScore()` returns to one Try outcome.
|
|
4
4
|
*
|
|
5
5
|
* The aggregation mirrors the policy the persisted API scoring path already
|
|
6
6
|
* documents (`docs/item-player/scoring-and-rubrics.md`), so a browser-derived
|
|
7
|
-
*
|
|
7
|
+
* Try result and a server-derived score do not disagree about what a
|
|
8
8
|
* multi-element item is worth:
|
|
9
9
|
*
|
|
10
10
|
* - one scored outcome: `points = score`, `max = max ?? 1`;
|
|
@@ -16,4 +16,4 @@ import type { FormativeScoredOutcome, FormativeTryOutcome } from "./types.js";
|
|
|
16
16
|
* those slots count toward `totalElementCount` and not toward
|
|
17
17
|
* `scoredElementCount`.
|
|
18
18
|
*/
|
|
19
|
-
export declare function
|
|
19
|
+
export declare function aggregateTryOutcome(outcomes: ReadonlyArray<ScoredElementOutcome | null | undefined> | null): TryOutcome;
|
|
@@ -39,7 +39,7 @@ function classify(points, max) {
|
|
|
39
39
|
*
|
|
40
40
|
* The aggregation mirrors the policy the persisted API scoring path already
|
|
41
41
|
* documents (`docs/item-player/scoring-and-rubrics.md`), so a browser-derived
|
|
42
|
-
*
|
|
42
|
+
* Try result and a server-derived score do not disagree about what a
|
|
43
43
|
* multi-element item is worth:
|
|
44
44
|
*
|
|
45
45
|
* - one scored outcome: `points = score`, `max = max ?? 1`;
|
|
@@ -51,7 +51,7 @@ function classify(points, max) {
|
|
|
51
51
|
* those slots count toward `totalElementCount` and not toward
|
|
52
52
|
* `scoredElementCount`.
|
|
53
53
|
*/
|
|
54
|
-
export function
|
|
54
|
+
export function aggregateTryOutcome(outcomes) {
|
|
55
55
|
const all = Array.isArray(outcomes) ? outcomes : [];
|
|
56
56
|
const scored = all.filter(isScored);
|
|
57
57
|
const totalElementCount = all.length;
|