@pie-players/pie-default-tool-loaders 1.0.0-beta.0 → 1.0.0-beta.2

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 CHANGED
@@ -17,6 +17,7 @@ capability id. A support id is the tool id a profile grants.
17
17
  | `PACKAGED_TOOL_TAG_MAP` | Which custom element each one renders as |
18
18
  | `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` | Where they appear and in what order |
19
19
  | `createUniversalPersonalNeedsProfile`, `createEmptyPersonalNeedsProfile` | Which of them the program grants to everyone, or to no one |
20
+ | `createPersonalNeedsProfileFromAfa` | Which of them a student's AfA PNP 3.0 record grants, prohibits and turns on at start, and with which settings |
20
21
  | `DEFAULT_TOOL_MODULE_LOADERS` | When each one's bundle loads |
21
22
  | `CONTENT_ALTERNATE_REGISTRATIONS` | Which of them carry an authored alternate that renders as a region; the print player mounts these |
22
23
 
@@ -159,6 +160,25 @@ An assessment carrying no profile is left alone. `pnpEnforcement`
159
160
  auto-detection engages only on host policy material, so with no profile,
160
161
  placement alone decides which tools appear.
161
162
 
163
+ ## AfA profiles
164
+
165
+ `createPersonalNeedsProfileFromAfa(record)` builds a fresh profile from a
166
+ student's AfA PNP 3.0 record, held as JSON keyed by the spec's terms. Features
167
+ become support ids in the packaged set's names, the activation sets fill
168
+ `activateAtInit` and `prohibitedSupports`, and the settings the packaged tools
169
+ read become `toolParameters`: read-aloud speed, pitch and volume, the calculator
170
+ type and the sign language. The toolkit's PNP configuration guide gives each AfA
171
+ feature's disposition.
172
+
173
+ ```ts
174
+ import { createPersonalNeedsProfileFromAfa } from "@pie-players/pie-default-tool-loaders";
175
+
176
+ sectionPlayer.assessment = {
177
+ ...authoredAssessment,
178
+ personalNeedsProfile: createPersonalNeedsProfileFromAfa(afaRecord),
179
+ };
180
+ ```
181
+
162
182
  ## Packaged capability composition
163
183
 
164
184
  The exports are projections of one internal composition. Each packaged
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The AfA PNP 3.0 adapter: an AfA record, as JSON keyed by the spec's
3
+ * kebab-case terms, becomes the toolkit's personal-needs profile.
4
+ *
5
+ * Each feature the record names is a support id, the term camelCased, except
6
+ * where a packaged tool carries the capability under an id of its own
7
+ * (`TOOL_REGISTRY.md`, Support ids). `spoken` resolves by its `reading-type`:
8
+ * `computer-read-aloud` is `textToSpeech`, and `screen-reader`, AfA's default,
9
+ * is `screenReader` (ADR 0004). `extensions` and the resource-matching
10
+ * preferences name no support and are skipped.
11
+ *
12
+ * A feature at the record's root or in `activate-as-option-set` is granted, one
13
+ * in `activate-at-initialization-set` is granted and on at start, and one in
14
+ * `prohibit-set` is prohibited, which withdraws it from both. The settings of
15
+ * a feature a packaged tool reads become that tool's `toolParameters`.
16
+ */
17
+ import type { PersonalNeedsProfile } from "@pie-players/pie-players-shared/types";
18
+ /** `spoken`'s settings, in AfA's units. */
19
+ export interface AfaSpoken {
20
+ "reading-type"?: "screen-reader" | "computer-read-aloud";
21
+ /** Words per minute; AfA's default is 180. */
22
+ "speech-rate"?: number;
23
+ /** 0–1; AfA's default is 0.5. */
24
+ pitch?: number;
25
+ /** 0–1; AfA's default is 0.5. */
26
+ volume?: number;
27
+ [setting: string]: unknown;
28
+ }
29
+ /** A set's features: empty elements keyed by feature, or the feature names. */
30
+ export type AfaFeatureSet = Readonly<Record<string, unknown>> | readonly string[];
31
+ /** An AfA PNP 3.0 record in JSON. A feature is present unless absent, `null` or `false`. */
32
+ export interface AfaPnpRecord {
33
+ spoken?: AfaSpoken;
34
+ "calculator-on-screen"?: {
35
+ "calculator-type"?: string;
36
+ [setting: string]: unknown;
37
+ };
38
+ "sign-language"?: {
39
+ language?: string;
40
+ [setting: string]: unknown;
41
+ };
42
+ "activate-at-initialization-set"?: AfaFeatureSet;
43
+ "activate-as-option-set"?: AfaFeatureSet;
44
+ "prohibit-set"?: AfaFeatureSet;
45
+ [feature: string]: unknown;
46
+ }
47
+ /** A fresh profile carrying `record`'s grants, prohibitions and settings, ids sorted. */
48
+ export declare function createPersonalNeedsProfileFromAfa(record: AfaPnpRecord): PersonalNeedsProfile;
@@ -0,0 +1,117 @@
1
+ /**
2
+ * The AfA PNP 3.0 adapter: an AfA record, as JSON keyed by the spec's
3
+ * kebab-case terms, becomes the toolkit's personal-needs profile.
4
+ *
5
+ * Each feature the record names is a support id, the term camelCased, except
6
+ * where a packaged tool carries the capability under an id of its own
7
+ * (`TOOL_REGISTRY.md`, Support ids). `spoken` resolves by its `reading-type`:
8
+ * `computer-read-aloud` is `textToSpeech`, and `screen-reader`, AfA's default,
9
+ * is `screenReader` (ADR 0004). `extensions` and the resource-matching
10
+ * preferences name no support and are skipped.
11
+ *
12
+ * A feature at the record's root or in `activate-as-option-set` is granted, one
13
+ * in `activate-at-initialization-set` is granted and on at start, and one in
14
+ * `prohibit-set` is prohibited, which withdraws it from both. The settings of
15
+ * a feature a packaged tool reads become that tool's `toolParameters`.
16
+ */
17
+ const RENAMED_FEATURES = {
18
+ "calculator-on-screen": "calculator",
19
+ "answer-masking": "answerEliminator",
20
+ "dictionary-on-screen": "dictionary",
21
+ };
22
+ /**
23
+ * Keys that name no support: the sets, `extensions`, and the preferences that
24
+ * select among adapted resources by metadata PIE content does not carry. The
25
+ * interface language is the host's `locale`.
26
+ */
27
+ const NON_SUPPORT_KEYS = new Set([
28
+ "activate-at-initialization-set",
29
+ "activate-as-option-set",
30
+ "prohibit-set",
31
+ "extensions",
32
+ "access-mode-required",
33
+ "adaptation-type-required",
34
+ "at-interoperable",
35
+ "educational-complexity-of-adaptation",
36
+ "hazard-avoidance",
37
+ "input-requirements",
38
+ "language-of-adaptation",
39
+ "language-of-interface",
40
+ "adaptation-detail-required",
41
+ "adaptation-media-required",
42
+ "educational-level-of-adaptation",
43
+ ]);
44
+ /** AfA's `standard` calculator has no PIE type, so it keeps the tool's default. */
45
+ const CALCULATOR_TYPES = new Set(["basic", "scientific", "graphing"]);
46
+ /** AfA's default `speech-rate`, which PIE's normal speed 1 stands for. */
47
+ const AFA_NORMAL_SPEECH_RATE = 180;
48
+ const isPresent = (value) => value !== undefined && value !== null && value !== false;
49
+ const finiteNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : undefined;
50
+ const camelCase = (term) => term.replace(/-([a-z0-9])/g, (_, next) => next.toUpperCase());
51
+ function featureNames(set) {
52
+ if (!set)
53
+ return [];
54
+ const names = Array.isArray(set)
55
+ ? set.filter((name) => typeof name === "string")
56
+ : Object.entries(set)
57
+ .filter(([, value]) => isPresent(value))
58
+ .map(([name]) => name);
59
+ return names.filter((name) => !NON_SUPPORT_KEYS.has(name));
60
+ }
61
+ function spokenParameters(spoken) {
62
+ const speechRate = finiteNumber(spoken["speech-rate"]);
63
+ const pitch = finiteNumber(spoken.pitch);
64
+ const volume = finiteNumber(spoken.volume);
65
+ return {
66
+ ...(speechRate !== undefined && speechRate > 0
67
+ ? { rate: speechRate / AFA_NORMAL_SPEECH_RATE }
68
+ : {}),
69
+ ...(pitch !== undefined ? { pitch: pitch * 2 } : {}),
70
+ ...(volume !== undefined ? { volume } : {}),
71
+ };
72
+ }
73
+ /** A fresh profile carrying `record`'s grants, prohibitions and settings, ids sorted. */
74
+ export function createPersonalNeedsProfileFromAfa(record) {
75
+ const spoken = typeof record.spoken === "object" && record.spoken !== null
76
+ ? record.spoken
77
+ : {};
78
+ const readAloud = spoken["reading-type"] === "computer-read-aloud";
79
+ const supportId = (feature) => feature === "spoken"
80
+ ? readAloud
81
+ ? "textToSpeech"
82
+ : "screenReader"
83
+ : (RENAMED_FEATURES[feature] ?? camelCase(feature));
84
+ // PIE cannot withhold a screen reader, so a prohibited `spoken` withholds
85
+ // the spoken presentation PIE makes, read-aloud.
86
+ const prohibited = new Set(featureNames(record["prohibit-set"]).map((feature) => feature === "spoken" ? "textToSpeech" : supportId(feature)));
87
+ const admitted = (ids) => [...new Set(ids)].filter((id) => !prohibited.has(id)).sort();
88
+ const activateAtInit = admitted(featureNames(record["activate-at-initialization-set"]).map(supportId));
89
+ const supports = admitted([
90
+ ...featureNames(record).map(supportId),
91
+ ...featureNames(record["activate-as-option-set"]).map(supportId),
92
+ ...activateAtInit,
93
+ ]);
94
+ const toolParameters = {};
95
+ if (readAloud) {
96
+ const speech = spokenParameters(spoken);
97
+ if (Object.keys(speech).length > 0) {
98
+ toolParameters.textToSpeech = speech;
99
+ }
100
+ }
101
+ const calculatorType = record["calculator-on-screen"]?.["calculator-type"];
102
+ if (calculatorType && CALCULATOR_TYPES.has(calculatorType)) {
103
+ toolParameters.calculator = {
104
+ type: calculatorType,
105
+ };
106
+ }
107
+ const signLang = record["sign-language"]?.language;
108
+ if (typeof signLang === "string" && signLang.length > 0) {
109
+ toolParameters.signLanguage = { signLang };
110
+ }
111
+ return {
112
+ supports,
113
+ prohibitedSupports: [...prohibited].sort(),
114
+ ...(activateAtInit.length > 0 ? { activateAtInit } : {}),
115
+ ...(Object.keys(toolParameters).length > 0 ? { toolParameters } : {}),
116
+ };
117
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { PackagedToolRegistryOptions } from "./packaged-capability-composition.js";
2
2
  export { createEmptyPersonalNeedsProfile, createPackagedToolRegistry, createUniversalPersonalNeedsProfile, DEFAULT_TOOL_MODULE_LOADERS, PACKAGED_TOOL_REGISTRATIONS, PACKAGED_TOOL_TAG_MAP, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
3
+ export { type AfaFeatureSet, type AfaPnpRecord, type AfaSpoken, createPersonalNeedsProfileFromAfa, } from "./afa-profile.js";
3
4
  export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
4
5
  export { annotationToolbarRegistration, lineReaderToolRegistration, themeToolRegistration, } from "./registrations/accessibility-tools.js";
5
6
  export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, CONTENT_LEAD_SURFACE, resolveAudioTranscript, type ResolvedAudioTranscript, } from "./registrations/audio-transcript.js";
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export { createEmptyPersonalNeedsProfile, createPackagedToolRegistry, createUniversalPersonalNeedsProfile, DEFAULT_TOOL_MODULE_LOADERS, PACKAGED_TOOL_REGISTRATIONS, PACKAGED_TOOL_TAG_MAP, SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT, } from "./packaged-capability-composition.js";
2
+ // A profile from an AfA PNP 3.0 record, in the packaged set's support ids.
3
+ export { createPersonalNeedsProfileFromAfa, } from "./afa-profile.js";
2
4
  // The authored-alternate subset, for a renderer that shows alternates and no
3
5
  // toolbar — importable without the packaged composition's tool loaders.
4
6
  export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
@@ -9,6 +9,6 @@ import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools
9
9
  * Answer Eliminator tool registration
10
10
  *
11
11
  * Allows students to strike through incorrect answer choices.
12
- * Only appears on multiple-choice style questions.
12
+ * Only appears on items with an element that supports answer masking.
13
13
  */
14
14
  export declare const answerEliminatorToolRegistration: ToolRegistration;
@@ -4,12 +4,40 @@
4
4
  * Registers tools for interacting with question content:
5
5
  * - Answer Eliminator (strike through answer choices)
6
6
  */
7
- import { hasChoiceInteraction } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
+ import { elementSupports } from "@pie-players/pie-assessment-toolkit/tools/registration";
8
8
  import { createToolElement, } from "@pie-players/pie-assessment-toolkit/tools/registration";
9
9
  import { createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "@pie-players/pie-assessment-toolkit/tools/registration";
10
10
  import { resolveOverlayElement } from "./overlay-element-cache.js";
11
11
  /** The policy parameters' `strategy`, defaulting to strikethrough. */
12
12
  const normalizeEliminationStrategy = (value) => value === "mask" ? "mask" : "strikethrough";
13
+ /** The AfA PNP 3.0 support term an element declares to take the eliminator. */
14
+ const ANSWER_MASKING = "answer-masking";
15
+ /**
16
+ * pie-item contract compatibility: legacy pie-elements, and pie-elements-ng
17
+ * releases from before elements declared their supports, render the markup the
18
+ * eliminator's built-in adapters read for these two. An element that declares
19
+ * its supports is decided by its declaration, these two included.
20
+ */
21
+ const UNDECLARED_ANSWER_MASKING_ELEMENTS = new Set(["multiple-choice", "ebsr"]);
22
+ // Content names one element several ways: `multiple-choice`,
23
+ // `pie-multiple-choice`, `pie-element-multiple-choice`, a runtime `--version-*`
24
+ // tag, or any tag the item's `elements` map binds to `@pie-element/multiple-choice`.
25
+ function isUndeclaredAnswerMaskingElement(tag, packageSpec) {
26
+ const name = tag
27
+ .toLowerCase()
28
+ .replace(/--version-[0-9a-z-]+$/, "")
29
+ .replace(/^pie-(element-)?/, "");
30
+ if (UNDECLARED_ANSWER_MASKING_ELEMENTS.has(name))
31
+ return true;
32
+ const packageName = packageSpec?.match(/^@pie-element\/([^@]+)/)?.[1];
33
+ return !!packageName && UNDECLARED_ANSWER_MASKING_ELEMENTS.has(packageName);
34
+ }
35
+ /** Whether an element of the context supports answer masking. */
36
+ function supportsAnswerMasking(context) {
37
+ return elementSupports(context).some(({ supports, tag, packageSpec }) => supports
38
+ ? supports.includes(ANSWER_MASKING)
39
+ : isUndeclaredAnswerMaskingElement(tag, packageSpec));
40
+ }
13
41
  /** The state-store key of each PIE element in the item, by model id. */
14
42
  function resolveElementStateKeys(context, toolbarContext) {
15
43
  const keys = {};
@@ -29,7 +57,7 @@ function resolveElementStateKeys(context, toolbarContext) {
29
57
  * Answer Eliminator tool registration
30
58
  *
31
59
  * Allows students to strike through incorrect answer choices.
32
- * Only appears on multiple-choice style questions.
60
+ * Only appears on items with an element that supports answer masking.
33
61
  */
34
62
  export const answerEliminatorToolRegistration = {
35
63
  toolId: "answerEliminator",
@@ -40,21 +68,22 @@ export const answerEliminatorToolRegistration = {
40
68
  icon: "strikethrough",
41
69
  supportedLevels: ["item"],
42
70
  /**
43
- * Pass 2: Answer eliminator is relevant only for choice-based questions
71
+ * Pass 2: relevant where an element supports answer masking. An element
72
+ * declares it on its class, and the answer eliminator's README says what the
73
+ * element then supplies.
44
74
  */
45
75
  isVisibleInContext(context) {
46
- return hasChoiceInteraction(context);
76
+ return supportsAnswerMasking(context);
47
77
  },
48
78
  /**
49
79
  * Pass 3: the same question, asked as a capability. Elimination controls are
50
- * rendered per choice, so an item with no choice interaction gives this tool
51
- * nothing to act on — `placement-ordering`, `categorize` and
52
- * `drag-in-the-blank` included, whose `choices` hold draggables the
53
- * eliminator cannot reach. Answering the relevance gate alone left the button
54
- * on those items for any learner whose profile grants answer masking.
80
+ * rendered per choice, so an item none of whose elements supports answer
81
+ * masking gives this tool nothing to act on. Answering the relevance gate
82
+ * alone left the button on such items for any learner whose profile grants
83
+ * answer masking.
55
84
  */
56
85
  isApplicableToContent(context) {
57
- return hasChoiceInteraction(context);
86
+ return supportsAnswerMasking(context);
58
87
  },
59
88
  renderToolbar(context, toolbarContext) {
60
89
  const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
@@ -11,6 +11,7 @@ import { resolveOverlayElement } from "./overlay-element-cache.js";
11
11
  // This package owns the server adapter's import, so the toolkit never names it
12
12
  // and a bundler building the toolkit alone has nothing to resolve.
13
13
  const loadServerTTSProvider = async () => (await import("@pie-players/tts-client-server")).ServerTTSProvider;
14
+ const numberOrUndefined = (value) => typeof value === "number" ? value : undefined;
14
15
  /**
15
16
  * Text-to-Speech tool registration
16
17
  *
@@ -80,6 +81,11 @@ export const ttsToolRegistration = {
80
81
  element.speedOptions = resolveElementSpeedOptions();
81
82
  element.showSingleSpeedOption =
82
83
  resolveRuntimeSettings().showSingleSpeedOption === true;
84
+ // The element range-checks these; a non-number means the default.
85
+ const speech = toolbarContext.getToolParameters?.("textToSpeech");
86
+ element.baseRate = numberOrUndefined(speech?.rate);
87
+ element.pitch = numberOrUndefined(speech?.pitch);
88
+ element.volume = numberOrUndefined(speech?.volume);
83
89
  };
84
90
  applyAttributes();
85
91
  const hostLayout = resolveTTSHostToolbarLayout(resolveRuntimeSettings());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-default-tool-loaders",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.2",
4
4
  "type": "module",
5
5
  "description": "Composition layer for the packaged PIE assessment tools: registrations, tag map, placement preset, universal and empty profiles, and lazy module loaders",
6
6
  "repository": {
@@ -33,25 +33,25 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "dependencies": {
36
- "@pie-players/pie-assessment-toolkit": "1.0.0-beta.0",
37
- "@pie-players/pie-calculator": "1.0.0-beta.0",
38
- "@pie-players/pie-calculator-cortex": "1.0.0-beta.0",
39
- "@pie-players/pie-calculator-desmos": "1.0.0-beta.0",
40
- "@pie-players/pie-calculator-geogebra": "1.0.0-beta.0",
41
- "@pie-players/pie-players-shared": "1.0.0-beta.0",
42
- "@pie-players/pie-tool-annotation-toolbar": "1.0.0-beta.0",
43
- "@pie-players/pie-tool-answer-eliminator": "1.0.0-beta.0",
44
- "@pie-players/pie-tool-calculator-shared": "1.0.0-beta.0",
45
- "@pie-players/pie-tool-dictionary": "1.0.0-beta.0",
46
- "@pie-players/pie-tool-graph": "1.0.0-beta.0",
47
- "@pie-players/pie-tool-line-reader": "1.0.0-beta.0",
48
- "@pie-players/pie-tool-periodic-table": "1.0.0-beta.0",
49
- "@pie-players/pie-tool-picture-dictionary": "1.0.0-beta.0",
50
- "@pie-players/pie-tool-protractor": "1.0.0-beta.0",
51
- "@pie-players/pie-tool-ruler": "1.0.0-beta.0",
52
- "@pie-players/pie-tool-theme": "1.0.0-beta.0",
53
- "@pie-players/pie-tool-tts-inline": "1.0.0-beta.0",
54
- "@pie-players/tts-client-server": "1.0.0-beta.0"
36
+ "@pie-players/pie-assessment-toolkit": "1.0.0-beta.2",
37
+ "@pie-players/pie-calculator": "1.0.0-beta.2",
38
+ "@pie-players/pie-calculator-cortex": "1.0.0-beta.2",
39
+ "@pie-players/pie-calculator-desmos": "1.0.0-beta.2",
40
+ "@pie-players/pie-calculator-geogebra": "1.0.0-beta.2",
41
+ "@pie-players/pie-players-shared": "1.0.0-beta.2",
42
+ "@pie-players/pie-tool-annotation-toolbar": "1.0.0-beta.2",
43
+ "@pie-players/pie-tool-answer-eliminator": "1.0.0-beta.2",
44
+ "@pie-players/pie-tool-calculator-shared": "1.0.0-beta.2",
45
+ "@pie-players/pie-tool-dictionary": "1.0.0-beta.2",
46
+ "@pie-players/pie-tool-graph": "1.0.0-beta.2",
47
+ "@pie-players/pie-tool-line-reader": "1.0.0-beta.2",
48
+ "@pie-players/pie-tool-periodic-table": "1.0.0-beta.2",
49
+ "@pie-players/pie-tool-picture-dictionary": "1.0.0-beta.2",
50
+ "@pie-players/pie-tool-protractor": "1.0.0-beta.2",
51
+ "@pie-players/pie-tool-ruler": "1.0.0-beta.2",
52
+ "@pie-players/pie-tool-theme": "1.0.0-beta.2",
53
+ "@pie-players/pie-tool-tts-inline": "1.0.0-beta.2",
54
+ "@pie-players/tts-client-server": "1.0.0-beta.2"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@biomejs/biome": "^2.5.15",