@pie-players/pie-assessment-toolkit 0.3.63 → 0.3.64

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 (38) hide show
  1. package/README.md +63 -0
  2. package/dist/components/ItemToolBar.custom-element.js +1 -1
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
  4. package/dist/components/SectionToolBar.custom-element.js +1 -1
  5. package/dist/components/chunks/{ItemToolBar-843902tp.js → ItemToolBar-3cppre9r.js} +26 -26
  6. package/dist/components/chunks/{ItemToolBar-84nv78dy.js → ItemToolBar-7rq2gj8b.js} +1 -1
  7. package/dist/index.d.ts +8 -3
  8. package/dist/index.js +5 -2
  9. package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
  10. package/dist/policy/core/ToolPolicyEngine.js +27 -0
  11. package/dist/policy/core/feature-decision.d.ts +57 -0
  12. package/dist/policy/core/feature-decision.js +40 -0
  13. package/dist/policy/engine.d.ts +1 -0
  14. package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
  15. package/dist/policy/sources/PnpPolicySource.js +41 -11
  16. package/dist/runtime/catalog-registration.d.ts +56 -1
  17. package/dist/runtime/catalog-registration.js +64 -31
  18. package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
  19. package/dist/services/AccessibilityCatalogResolver.js +183 -58
  20. package/dist/services/SSMLExtractor.js +28 -18
  21. package/dist/services/TTSService.d.ts +25 -0
  22. package/dist/services/TTSService.js +241 -45
  23. package/dist/services/ToolkitCoordinator.d.ts +23 -2
  24. package/dist/services/ToolkitCoordinator.js +24 -0
  25. package/dist/services/catalog-media.d.ts +25 -0
  26. package/dist/services/catalog-media.js +101 -0
  27. package/dist/services/defaultPersonalNeedsProfile.d.ts +16 -0
  28. package/dist/services/defaultPersonalNeedsProfile.js +23 -0
  29. package/dist/services/interfaces.d.ts +29 -2
  30. package/dist/services/pnp-standard-features.d.ts +1 -1
  31. package/dist/services/sign-language-cards.d.ts +82 -0
  32. package/dist/services/sign-language-cards.js +133 -0
  33. package/dist/services/spoken-audio-cards.d.ts +54 -0
  34. package/dist/services/spoken-audio-cards.js +66 -0
  35. package/dist/services/tts/math-aware-text-processing.js +3 -3
  36. package/dist/services/tts/text-processing.d.ts +51 -0
  37. package/dist/services/tts/text-processing.js +117 -1
  38. package/package.json +9 -9
@@ -62,6 +62,122 @@ export const isNodeHiddenForTTS = (node, root) => {
62
62
  }
63
63
  return false;
64
64
  };
65
+ /**
66
+ * Marks content that must be shown but never spoken — items where reading *is*
67
+ * the construct, such as decoding and spelling, where speaking the node hands
68
+ * over the answer.
69
+ *
70
+ * Not a PNP field: `prohibitedSupports` is the learner declining a support, while
71
+ * this is the item saying "not here, for anyone", so it overrides an entitlement
72
+ * rather than yielding to it.
73
+ *
74
+ * Shape follows QTI 3's `data-qti-suppress-tts` — an attribute on the content
75
+ * element, single-valued, vocabulary below. Element placement is what makes it
76
+ * work on undocked nodes and enforceable in the selection read-aloud path, which
77
+ * consults no catalog. The name follows PIE's `data-tts-*` family, and PIE reads
78
+ * only this spelling; importers map QTI's.
79
+ */
80
+ export const TTS_SUPPRESS_ATTRIBUTE = "data-tts-suppress";
81
+ const SUPPRESSES_COMPUTER_READ_ALOUD = new Set(["computer-read-aloud", "all"]);
82
+ // `screen-reader` is in the vocabulary but is not ours: it asks the delivery
83
+ // engine to hide the node from assistive technology, which is the host's job
84
+ // (and is what `aria-hidden` above already covers on the way in). A node marked
85
+ // only `screen-reader` is still legitimately machine-read aloud.
86
+ const SUPPRESS_VALUES = new Set([
87
+ ...SUPPRESSES_COMPUTER_READ_ALOUD,
88
+ "screen-reader",
89
+ ]);
90
+ const warnedSuppressValues = new Set();
91
+ /**
92
+ * Whether this element forbids machine read-aloud of itself and its subtree.
93
+ *
94
+ * Unrecognized and empty values suppress rather than pass through, and say so once
95
+ * per distinct value: a typo that fell through would speak a word the item was
96
+ * measuring, invalidating the score with no visible symptom, whereas
97
+ * over-suppressing only withholds speech an author had already marked as withheld.
98
+ */
99
+ export const isElementSuppressedForTTS = (element) => {
100
+ const raw = element.getAttribute?.(TTS_SUPPRESS_ATTRIBUTE);
101
+ if (raw === null || raw === undefined)
102
+ return false;
103
+ const value = raw.trim().toLowerCase();
104
+ if (SUPPRESSES_COMPUTER_READ_ALOUD.has(value))
105
+ return true;
106
+ if (SUPPRESS_VALUES.has(value))
107
+ return false;
108
+ if (!warnedSuppressValues.has(value)) {
109
+ warnedSuppressValues.add(value);
110
+ console.warn(`[tts] ${TTS_SUPPRESS_ATTRIBUTE}="${raw}" is not one of ${Array.from(SUPPRESS_VALUES).join(", ")}; suppressing read-aloud for this content anyway, because a suppression attribute that fails open would leak the answer to items where reading is the construct. Correct the value to silence this.`);
111
+ }
112
+ return true;
113
+ };
114
+ export const isNodeSuppressedForTTS = (node, root) => {
115
+ let current = node.nodeType === 1
116
+ ? node
117
+ : node.parentElement;
118
+ while (current) {
119
+ if (isElementSuppressedForTTS(current))
120
+ return true;
121
+ if (root && current === root)
122
+ break;
123
+ current = current.parentElement;
124
+ }
125
+ return false;
126
+ };
127
+ /**
128
+ * The predicate every speech-producing path filters on: hidden *or* suppressed.
129
+ *
130
+ * Kept distinct from `isNodeHiddenForTTS`, which stays a question about
131
+ * visibility — suppressed content is visible on purpose, and the highlight
132
+ * geometry resolvers that ask "can the candidate see this" must keep getting
133
+ * the visibility answer rather than this one.
134
+ */
135
+ export const isNodeExcludedFromSpeech = (node, root) => isNodeHiddenForTTS(node, root) || isNodeSuppressedForTTS(node);
136
+ /**
137
+ * Text of a range with the parts that must not be spoken removed.
138
+ *
139
+ * `Range.toString()` is not usable for speech: it is pure character extraction
140
+ * and honours no DOM filter at all, so it happily returns suppressed — and
141
+ * hidden — text. The selection read-aloud path is a text-in path rather than a
142
+ * DOM walk, which makes this the only place its content can be filtered.
143
+ *
144
+ * `filtered` reports whether anything was dropped, so a caller can tell "the
145
+ * candidate selected nothing speakable" apart from "the candidate selected
146
+ * nothing".
147
+ */
148
+ export const collectRangeTextForSpeech = (range, root) => {
149
+ if (typeof document === "undefined" ||
150
+ typeof document.createTreeWalker !==
151
+ "function" ||
152
+ typeof NodeFilter === "undefined" ||
153
+ typeof range.intersectsNode !== "function") {
154
+ // Degraded, and deliberately not silent about the difference: callers still
155
+ // enforce whole-selection suppression from the range's common ancestor, so
156
+ // the construct guard holds even here. What is lost is per-node filtering
157
+ // of a selection that only partly overlaps suppressed content.
158
+ return { text: range.toString(), filtered: false };
159
+ }
160
+ const parts = [];
161
+ let filtered = false;
162
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
163
+ let current = walker.nextNode();
164
+ while (current) {
165
+ const textNode = current;
166
+ if (range.intersectsNode(textNode)) {
167
+ if (isNodeExcludedFromSpeech(textNode, root)) {
168
+ filtered = true;
169
+ }
170
+ else {
171
+ const raw = textNode.textContent || "";
172
+ const start = textNode === range.startContainer ? range.startOffset : 0;
173
+ const end = textNode === range.endContainer ? range.endOffset : raw.length;
174
+ parts.push(raw.slice(start, end));
175
+ }
176
+ }
177
+ current = walker.nextNode();
178
+ }
179
+ return { text: parts.join(""), filtered };
180
+ };
65
181
  export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) => {
66
182
  if (!previousChar || !nextChar)
67
183
  return false;
@@ -111,7 +227,7 @@ export const collectVisibleTextAndMap = (element, options) => {
111
227
  while (current) {
112
228
  const textNode = current;
113
229
  const parent = textNode.parentElement;
114
- if (parent && !isNodeHiddenForTTS(textNode, element)) {
230
+ if (parent && !isNodeExcludedFromSpeech(textNode, element)) {
115
231
  const raw = textNode.textContent || "";
116
232
  const firstVisibleMatch = raw.match(/\S/);
117
233
  const firstVisibleChar = firstVisibleMatch ? firstVisibleMatch[0] : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pie-players/pie-assessment-toolkit",
3
- "version": "0.3.63",
3
+ "version": "0.3.64",
4
4
  "type": "module",
5
5
  "description": "PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination",
6
6
  "license": "MIT",
@@ -75,15 +75,15 @@
75
75
  "test": "bun test"
76
76
  },
77
77
  "dependencies": {
78
- "@pie-players/pie-calculator": "0.3.63",
79
- "@pie-players/pie-context": "0.3.63",
80
- "@pie-players/pie-players-shared": "0.3.63",
81
- "@pie-players/pie-tts": "0.3.63",
78
+ "@pie-players/pie-calculator": "0.3.64",
79
+ "@pie-players/pie-context": "0.3.64",
80
+ "@pie-players/pie-players-shared": "0.3.64",
81
+ "@pie-players/pie-tts": "0.3.64",
82
82
  "speech-rule-engine": "^5.0.0-rc.4"
83
83
  },
84
84
  "peerDependencies": {
85
- "@pie-players/pie-calculator-desmos": "0.3.63",
86
- "@pie-players/tts-client-server": "0.3.63"
85
+ "@pie-players/pie-calculator-desmos": "0.3.64",
86
+ "@pie-players/tts-client-server": "0.3.64"
87
87
  },
88
88
  "peerDependenciesMeta": {
89
89
  "@pie-players/pie-calculator-desmos": {
@@ -96,8 +96,8 @@
96
96
  "devDependencies": {
97
97
  "@biomejs/biome": "^2.5.6",
98
98
  "@happy-dom/global-registrator": "^20.11.1",
99
- "@pie-players/pie-calculator-desmos": "0.3.63",
100
- "@pie-players/tts-client-server": "0.3.63",
99
+ "@pie-players/pie-calculator-desmos": "0.3.64",
100
+ "@pie-players/tts-client-server": "0.3.64",
101
101
  "svelte": "^5.56.8",
102
102
  "typescript": "^5.9.3"
103
103
  },