@pie-players/pie-default-tool-loaders 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/dist/index.d.ts CHANGED
@@ -3,10 +3,11 @@ export { createEmptyPersonalNeedsProfile, createPackagedToolRegistry, createUniv
3
3
  export { type AfaFeatureSet, type AfaPnpRecord, type AfaSpoken, type AfaZoomAmount, createPersonalNeedsProfileFromAfa, } from "./afa-profile.js";
4
4
  export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
5
5
  export { annotationToolbarRegistration, lineReaderToolRegistration, magnificationToolRegistration, themeToolRegistration, } from "./registrations/accessibility-tools.js";
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";
6
+ export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, resolveAudioTranscript, type ResolvedAudioTranscript, } from "./registrations/audio-transcript.js";
7
+ export { CONTENT_LEAD_SURFACE } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
8
  export { type CalculatorProviderDefinition, type CalculatorRegistrationOptions, calculatorToolRegistration, createCalculatorToolRegistration, } from "./registrations/calculator.js";
8
9
  export { createDictionaryToolRegistration, createPictureDictionaryToolRegistration, dictionaryToolRegistration, type DictionaryVariantOptions, pictureDictionaryToolRegistration, spanishDictionaryToolRegistration, spanishPictureDictionaryToolRegistration, } from "./registrations/dictionary-tools.js";
9
- export { answerEliminatorToolRegistration } from "./registrations/interaction-tools.js";
10
+ export { type AnswerEliminatorToolConfig, answerEliminatorToolRegistration, } from "./registrations/interaction-tools.js";
10
11
  export { protractorToolRegistration, rulerToolRegistration, } from "./registrations/measurement-tools.js";
11
12
  export { graphToolRegistration, periodicTableToolRegistration, } from "./registrations/subject-specific-tools.js";
12
13
  export { ttsToolRegistration } from "./registrations/tts.js";
package/dist/index.js CHANGED
@@ -7,10 +7,13 @@ export { CONTENT_ALTERNATE_REGISTRATIONS } from "./content-alternates.js";
7
7
  // Individual registrations, for a host composing a subset rather than the whole
8
8
  // packaged set.
9
9
  export { annotationToolbarRegistration, lineReaderToolRegistration, magnificationToolRegistration, themeToolRegistration, } from "./registrations/accessibility-tools.js";
10
- export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, CONTENT_LEAD_SURFACE, resolveAudioTranscript, } from "./registrations/audio-transcript.js";
10
+ export { AUDIO_TRANSCRIPT_FEATURE_ID, AUDIO_TRANSCRIPT_REGION_CLASS, AUDIO_TRANSCRIPT_REGION_LABEL, audioTranscriptRegistration, resolveAudioTranscript, } from "./registrations/audio-transcript.js";
11
+ // The canonical surface ids live in the toolkit; re-exported so the packaged
12
+ // set's surface stays unchanged.
13
+ export { CONTENT_LEAD_SURFACE } from "@pie-players/pie-assessment-toolkit/tools/registration";
11
14
  export { calculatorToolRegistration, createCalculatorToolRegistration, } from "./registrations/calculator.js";
12
15
  export { createDictionaryToolRegistration, createPictureDictionaryToolRegistration, dictionaryToolRegistration, pictureDictionaryToolRegistration, spanishDictionaryToolRegistration, spanishPictureDictionaryToolRegistration, } from "./registrations/dictionary-tools.js";
13
- export { answerEliminatorToolRegistration } from "./registrations/interaction-tools.js";
16
+ export { answerEliminatorToolRegistration, } from "./registrations/interaction-tools.js";
14
17
  export { protractorToolRegistration, rulerToolRegistration, } from "./registrations/measurement-tools.js";
15
18
  export { graphToolRegistration, periodicTableToolRegistration, } from "./registrations/subject-specific-tools.js";
16
19
  export { ttsToolRegistration } from "./registrations/tts.js";
@@ -66,11 +66,4 @@ export interface ResolvedAudioTranscript {
66
66
  * transcript is in the language of the audio it transcribes, one per prompt.
67
67
  */
68
68
  export declare function resolveAudioTranscript(context: ToolContentDependencyContext): ResolvedAudioTranscript | null;
69
- /**
70
- * Host surface this capability fills: the full-width slot above a card's content.
71
- *
72
- * Item cards and passage cards open the same surface, so declaring it once
73
- * reaches both — a passage can carry an audio prompt as an item can.
74
- */
75
- export declare const CONTENT_LEAD_SURFACE = "content-lead";
76
69
  export declare const audioTranscriptRegistration: ToolRegistration;
@@ -35,6 +35,7 @@
35
35
  * absent from the universal preset — a content-dependent support never belongs in
36
36
  * a wholesale grant.
37
37
  */
38
+ import { CONTENT_LEAD_SURFACE } from "@pie-players/pie-assessment-toolkit/tools/registration";
38
39
  import { toLanguageTag } from "@pie-players/pie-players-shared/i18n/language-tags";
39
40
  /** QTI 3.0 / AfA support id gating an audio transcript. */
40
41
  export const AUDIO_TRANSCRIPT_FEATURE_ID = "transcript";
@@ -103,13 +104,6 @@ export function resolveAudioTranscript(context) {
103
104
  // and being asked anyway is the price of also serving the authored case.
104
105
  return context.granted ? firstOnGrant : null;
105
106
  }
106
- /**
107
- * Host surface this capability fills: the full-width slot above a card's content.
108
- *
109
- * Item cards and passage cards open the same surface, so declaring it once
110
- * reaches both — a passage can carry an audio prompt as an item can.
111
- */
112
- export const CONTENT_LEAD_SURFACE = "content-lead";
113
107
  /**
114
108
  * A labelled region holding the transcript text.
115
109
  *
@@ -4,7 +4,20 @@
4
4
  * Registers tools for interacting with question content:
5
5
  * - Answer Eliminator (strike through answer choices)
6
6
  */
7
- import type { ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
7
+ import type { ToolProviderConfig, ToolRegistration } from "@pie-players/pie-assessment-toolkit/tools/registration";
8
+ /**
9
+ * The answer eliminator's `tools.providers.answerEliminator` entry.
10
+ */
11
+ export interface AnswerEliminatorToolConfig extends ToolProviderConfig {
12
+ /**
13
+ * The elements the eliminator acts on, by the name `<name>` of their
14
+ * `@pie-element/<name>` package, such as `["multiple-choice"]`. An element
15
+ * must also support answer masking. Omitted, every element that supports it
16
+ * takes the eliminator; empty, none does. A host lists the elements whose
17
+ * support it has vetted.
18
+ */
19
+ elements?: readonly string[];
20
+ }
8
21
  /**
9
22
  * Answer Eliminator tool registration
10
23
  *
@@ -19,24 +19,84 @@ const ANSWER_MASKING = "answer-masking";
19
19
  * its supports is decided by its declaration, these two included.
20
20
  */
21
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
22
+ function elementName(tag) {
23
+ return tag
27
24
  .toLowerCase()
28
25
  .replace(/--version-[0-9a-z-]+$/, "")
29
26
  .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
27
  }
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));
28
+ // Content names one element several ways: `multiple-choice`,
29
+ // `pie-multiple-choice`, `pie-element-multiple-choice`, a runtime `--version-*`
30
+ // tag, or any tag the item's `elements` map binds to `@pie-element/multiple-choice`.
31
+ function elementNames(tag, packageSpec) {
32
+ const names = [elementName(tag)];
33
+ const packageName = pieElementPackageName(packageSpec);
34
+ if (packageName)
35
+ names.push(packageName);
36
+ return names;
37
+ }
38
+ function pieElementPackageName(packageSpec) {
39
+ return packageSpec?.match(/^@pie-element\/([^@]+)/)?.[1]?.toLowerCase() ?? null;
40
+ }
41
+ /**
42
+ * The name an `elements` list matches: the `@pie-element/<name>` package the
43
+ * item binds the tag to, else the tag's own name. A tag bound to another
44
+ * package matches no name, whatever the tag says.
45
+ */
46
+ function listedName(tag, packageSpec) {
47
+ return packageSpec ? pieElementPackageName(packageSpec) : elementName(tag);
48
+ }
49
+ /**
50
+ * The element names `config.elements` allows, or `null` for every element. An
51
+ * `elements` that is not a list allows none: validation reports it, and an
52
+ * element a host has not vetted stays without the eliminator.
53
+ */
54
+ function allowedElementNames(config) {
55
+ const elements = config
56
+ ?.elements;
57
+ if (elements === undefined)
58
+ return null;
59
+ if (!Array.isArray(elements))
60
+ return new Set();
61
+ return new Set(elements
62
+ .filter((name) => typeof name === "string")
63
+ .map(elementName));
64
+ }
65
+ /**
66
+ * The model ids of the context's elements that support answer masking and the
67
+ * host config allows.
68
+ */
69
+ function answerMaskingElementIds(context, config) {
70
+ const allowed = allowedElementNames(config);
71
+ return elementSupports(context)
72
+ .filter(({ supports, tag, packageSpec }) => {
73
+ if (allowed) {
74
+ const name = listedName(tag, packageSpec);
75
+ if (name === null || !allowed.has(name))
76
+ return false;
77
+ }
78
+ return supports
79
+ ? supports.includes(ANSWER_MASKING)
80
+ : elementNames(tag, packageSpec).some((name) => UNDECLARED_ANSWER_MASKING_ELEMENTS.has(name));
81
+ })
82
+ .flatMap(({ elementId }) => (elementId ? [elementId] : []));
83
+ }
84
+ function validateAnswerEliminatorConfig(config) {
85
+ const { elements } = config;
86
+ if (elements === undefined)
87
+ return [];
88
+ if (Array.isArray(elements) &&
89
+ elements.every((name) => typeof name === "string" && name.trim() !== "")) {
90
+ return [];
91
+ }
92
+ return [
93
+ {
94
+ code: "tools.invalidProviderConfig",
95
+ severity: "error",
96
+ path: "providers.answerEliminator.elements",
97
+ message: '`elements` must be a list of element names, such as ["multiple-choice"].',
98
+ },
99
+ ];
40
100
  }
41
101
  /** The state-store key of each PIE element in the item, by model id. */
42
102
  function resolveElementStateKeys(context, toolbarContext) {
@@ -67,23 +127,25 @@ export const answerEliminatorToolRegistration = {
67
127
  descriptionKey: "tools.answerEliminator.description",
68
128
  icon: "strikethrough",
69
129
  supportedLevels: ["item"],
130
+ validateConfig: validateAnswerEliminatorConfig,
70
131
  /**
71
132
  * Pass 2: relevant where an element supports answer masking. An element
72
133
  * declares it on its class, and the answer eliminator's README says what the
73
134
  * element then supplies.
74
135
  */
75
136
  isVisibleInContext(context) {
76
- return supportsAnswerMasking(context);
137
+ return answerMaskingElementIds(context, null).length > 0;
77
138
  },
78
139
  /**
79
- * Pass 3: the same question, asked as a capability. Elimination controls are
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.
140
+ * Pass 3: the same question, asked as a capability and narrowed to the
141
+ * elements the host config allows. Elimination controls are rendered per
142
+ * choice, so an item none of whose allowed elements supports answer masking
143
+ * gives this tool nothing to act on. Answering the relevance gate alone left
144
+ * the button on such items for any learner whose profile grants answer
145
+ * masking.
84
146
  */
85
- isApplicableToContent(context) {
86
- return supportsAnswerMasking(context);
147
+ isApplicableToContent(context, config) {
148
+ return answerMaskingElementIds(context, config).length > 0;
87
149
  },
88
150
  renderToolbar(context, toolbarContext) {
89
151
  const visibility = createScopedVisibilityBinding(this.toolId, toolbarContext);
@@ -94,7 +156,7 @@ export const answerEliminatorToolRegistration = {
94
156
  const button = {
95
157
  toolId: this.toolId,
96
158
  label: this.name,
97
- icon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20" fill="currentColor" aria-hidden="true"><path d="M19,3H16.3H7.7H5A2,2 0 0,0 3,5V7.7V16.4V19A2,2 0 0,0 5,21H7.7H16.4H19A2,2 0 0,0 21,19V16.3V7.7V5A2,2 0 0,0 19,3M15.6,17L12,13.4L8.4,17L7,15.6L10.6,12L7,8.4L8.4,7L12,10.6L15.6,7L17,8.4L13.4,12L17,15.6L15.6,17Z"/></svg>',
159
+ icon: typeof this.icon === "function" ? this.icon(context) : this.icon,
98
160
  disabled: false,
99
161
  ariaLabel: toolbarContext.i18n.t("tools.answerEliminator.buttonA11y"),
100
162
  tooltip: toolbarContext.i18n.t("tools.answerEliminator.tooltip"),
@@ -112,6 +174,12 @@ export const answerEliminatorToolRegistration = {
112
174
  isActive: visibility.isActive,
113
175
  });
114
176
  overlay.elementStateKeys = resolveElementStateKeys(context, toolbarContext);
177
+ const config = toolbarContext.toolkitCoordinator?.getToolConfig(this.toolId) ??
178
+ null;
179
+ overlay.elementIds =
180
+ allowedElementNames(config) === null
181
+ ? undefined
182
+ : answerMaskingElementIds(context, config);
115
183
  },
116
184
  subscribeActive: visibility.subscribeActive,
117
185
  };
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.7",
3
+ "version": "1.0.0-beta.9",
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,28 +33,28 @@
33
33
  "test": "bun test"
34
34
  },
35
35
  "dependencies": {
36
- "@pie-players/pie-assessment-toolkit": "1.0.0-beta.7",
37
- "@pie-players/pie-calculator": "1.0.0-beta.7",
38
- "@pie-players/pie-calculator-desmos": "1.0.0-beta.7",
39
- "@pie-players/pie-calculator-geogebra": "1.0.0-beta.7",
40
- "@pie-players/pie-players-shared": "1.0.0-beta.7",
41
- "@pie-players/pie-tool-annotation-toolbar": "1.0.0-beta.7",
42
- "@pie-players/pie-tool-answer-eliminator": "1.0.0-beta.7",
43
- "@pie-players/pie-tool-calculator-shared": "1.0.0-beta.7",
44
- "@pie-players/pie-tool-dictionary": "1.0.0-beta.7",
45
- "@pie-players/pie-tool-graph": "1.0.0-beta.7",
46
- "@pie-players/pie-tool-line-reader": "1.0.0-beta.7",
47
- "@pie-players/pie-tool-magnifier": "1.0.0-beta.7",
48
- "@pie-players/pie-tool-periodic-table": "1.0.0-beta.7",
49
- "@pie-players/pie-tool-picture-dictionary": "1.0.0-beta.7",
50
- "@pie-players/pie-tool-protractor": "1.0.0-beta.7",
51
- "@pie-players/pie-tool-ruler": "1.0.0-beta.7",
52
- "@pie-players/pie-tool-theme": "1.0.0-beta.7",
53
- "@pie-players/pie-tool-tts-inline": "1.0.0-beta.7",
54
- "@pie-players/tts-client-server": "1.0.0-beta.7"
36
+ "@pie-players/pie-assessment-toolkit": "1.0.0-beta.9",
37
+ "@pie-players/pie-calculator": "1.0.0-beta.9",
38
+ "@pie-players/pie-calculator-desmos": "1.0.0-beta.9",
39
+ "@pie-players/pie-calculator-geogebra": "1.0.0-beta.9",
40
+ "@pie-players/pie-players-shared": "1.0.0-beta.9",
41
+ "@pie-players/pie-tool-annotation-toolbar": "1.0.0-beta.9",
42
+ "@pie-players/pie-tool-answer-eliminator": "1.0.0-beta.9",
43
+ "@pie-players/pie-tool-calculator-shared": "1.0.0-beta.9",
44
+ "@pie-players/pie-tool-dictionary": "1.0.0-beta.9",
45
+ "@pie-players/pie-tool-graph": "1.0.0-beta.9",
46
+ "@pie-players/pie-tool-line-reader": "1.0.0-beta.9",
47
+ "@pie-players/pie-tool-magnifier": "1.0.0-beta.9",
48
+ "@pie-players/pie-tool-periodic-table": "1.0.0-beta.9",
49
+ "@pie-players/pie-tool-picture-dictionary": "1.0.0-beta.9",
50
+ "@pie-players/pie-tool-protractor": "1.0.0-beta.9",
51
+ "@pie-players/pie-tool-ruler": "1.0.0-beta.9",
52
+ "@pie-players/pie-tool-theme": "1.0.0-beta.9",
53
+ "@pie-players/pie-tool-tts-inline": "1.0.0-beta.9",
54
+ "@pie-players/tts-client-server": "1.0.0-beta.9"
55
55
  },
56
56
  "devDependencies": {
57
- "@biomejs/biome": "^2.6.2",
57
+ "@biomejs/biome": "^2.6.4",
58
58
  "@happy-dom/global-registrator": "^21.0.4",
59
59
  "typescript": "^5.9.3"
60
60
  },