@pie-players/pie-assessment-toolkit 0.3.44 → 0.3.46

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 (191) hide show
  1. package/README.md +59 -70
  2. package/dist/components/ItemToolBar.custom-element.js +776 -357
  3. package/dist/components/PieAssessmentToolkit.custom-element.js +19501 -3591
  4. package/dist/components/SectionToolBar.custom-element.js +1058 -540
  5. package/dist/index.d.ts +3 -6
  6. package/dist/index.js +1 -3
  7. package/dist/index.js.map +1 -1
  8. package/dist/policy/core/ToolPolicyEngine.d.ts +1 -2
  9. package/dist/policy/core/ToolPolicyEngine.js +1 -2
  10. package/dist/policy/core/ToolPolicyEngine.js.map +1 -1
  11. package/dist/policy/core/pnp-policy-inputs.d.ts +2 -3
  12. package/dist/policy/core/pnp-policy-inputs.js +2 -3
  13. package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
  14. package/dist/policy/core/provenance.d.ts +7 -34
  15. package/dist/policy/core/provenance.js +4 -21
  16. package/dist/policy/core/provenance.js.map +1 -1
  17. package/dist/policy/sources/PnpPolicySource.d.ts +6 -19
  18. package/dist/policy/sources/PnpPolicySource.js +6 -19
  19. package/dist/policy/sources/PnpPolicySource.js.map +1 -1
  20. package/dist/runtime/SectionRuntimeEngine.d.ts +13 -28
  21. package/dist/runtime/SectionRuntimeEngine.js +13 -27
  22. package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
  23. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +2 -2
  24. package/dist/runtime/adapter/SectionEngineAdapter.js +2 -2
  25. package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -1
  26. package/dist/runtime/adapter/coordinator-bridge.d.ts +1 -1
  27. package/dist/runtime/adapter/coordinator-bridge.js +1 -1
  28. package/dist/runtime/adapter/coordinator-bridge.js.map +1 -1
  29. package/dist/runtime/adapter/dom-event-bridge.d.ts +1 -1
  30. package/dist/runtime/adapter/dom-event-bridge.js +1 -1
  31. package/dist/runtime/adapter/dom-event-bridge.js.map +1 -1
  32. package/dist/runtime/catalog-registration.d.ts +12 -0
  33. package/dist/runtime/catalog-registration.js +54 -0
  34. package/dist/runtime/catalog-registration.js.map +1 -0
  35. package/dist/runtime/core/engine-output.d.ts +2 -2
  36. package/dist/runtime/core/engine-output.js +2 -2
  37. package/dist/runtime/core/engine-output.js.map +1 -1
  38. package/dist/runtime/core/engine-readiness.d.ts +2 -2
  39. package/dist/runtime/core/engine-readiness.js.map +1 -1
  40. package/dist/runtime/core/engine-resolver.d.ts +13 -41
  41. package/dist/runtime/core/engine-resolver.js +19 -76
  42. package/dist/runtime/core/engine-resolver.js.map +1 -1
  43. package/dist/runtime/core/engine-transition.d.ts +1 -1
  44. package/dist/runtime/core/engine-transition.js +1 -1
  45. package/dist/runtime/core/engine-transition.js.map +1 -1
  46. package/dist/runtime/engine.d.ts +1 -1
  47. package/dist/runtime/engine.js.map +1 -1
  48. package/dist/runtime/section-runtime-engine-host-context.d.ts +22 -15
  49. package/dist/runtime/section-runtime-engine-host-context.js +10 -10
  50. package/dist/runtime/section-runtime-engine-host-context.js.map +1 -1
  51. package/dist/runtime/stage-emit-gate.d.ts +4 -4
  52. package/dist/runtime/stage-emit-gate.js +3 -3
  53. package/dist/runtime/stage-emit-gate.js.map +1 -1
  54. package/dist/services/AccessibilityCatalogResolver.d.ts +23 -0
  55. package/dist/services/AccessibilityCatalogResolver.js +200 -28
  56. package/dist/services/AccessibilityCatalogResolver.js.map +1 -1
  57. package/dist/services/HighlightCoordinator.d.ts +25 -0
  58. package/dist/services/HighlightCoordinator.js +125 -0
  59. package/dist/services/HighlightCoordinator.js.map +1 -1
  60. package/dist/services/SSMLExtractor.d.ts +16 -1
  61. package/dist/services/SSMLExtractor.js +86 -7
  62. package/dist/services/SSMLExtractor.js.map +1 -1
  63. package/dist/services/TTSService.d.ts +30 -4
  64. package/dist/services/TTSService.js +652 -159
  65. package/dist/services/TTSService.js.map +1 -1
  66. package/dist/services/ToolRegistry.d.ts +11 -2
  67. package/dist/services/ToolRegistry.js +1 -1
  68. package/dist/services/ToolRegistry.js.map +1 -1
  69. package/dist/services/ToolkitCoordinator.d.ts +16 -73
  70. package/dist/services/ToolkitCoordinator.js +34 -113
  71. package/dist/services/ToolkitCoordinator.js.map +1 -1
  72. package/dist/services/interfaces.d.ts +20 -2
  73. package/dist/services/interfaces.js.map +1 -1
  74. package/dist/services/tool-config-validation.d.ts +1 -1
  75. package/dist/services/tool-config-validation.js +7 -7
  76. package/dist/services/tool-config-validation.js.map +1 -1
  77. package/dist/services/tool-instance-id.js +1 -1
  78. package/dist/services/tool-instance-id.js.map +1 -1
  79. package/dist/services/tool-providers/TTSToolProvider.js +3 -3
  80. package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
  81. package/dist/services/tools-config-normalizer.d.ts +13 -2
  82. package/dist/services/tools-config-normalizer.js +3 -8
  83. package/dist/services/tools-config-normalizer.js.map +1 -1
  84. package/dist/services/tts/browser-provider.js +2 -0
  85. package/dist/services/tts/browser-provider.js.map +1 -1
  86. package/dist/services/tts/catalog-span-alignment.d.ts +39 -0
  87. package/dist/services/tts/catalog-span-alignment.js +300 -0
  88. package/dist/services/tts/catalog-span-alignment.js.map +1 -0
  89. package/dist/services/tts/generated-speech/assemble-plan.d.ts +33 -0
  90. package/dist/services/tts/generated-speech/assemble-plan.js +111 -0
  91. package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -0
  92. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +25 -0
  93. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +66 -0
  94. package/dist/services/tts/generated-speech/dom/build-dom-plan.js.map +1 -0
  95. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +19 -0
  96. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js +71 -0
  97. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js.map +1 -0
  98. package/dist/services/tts/generated-speech/dom/types.d.ts +37 -0
  99. package/dist/services/tts/generated-speech/dom/types.js +2 -0
  100. package/dist/services/tts/generated-speech/dom/types.js.map +1 -0
  101. package/dist/services/tts/generated-speech/index.d.ts +17 -0
  102. package/dist/services/tts/generated-speech/index.js +17 -0
  103. package/dist/services/tts/generated-speech/index.js.map +1 -0
  104. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +15 -0
  105. package/dist/services/tts/generated-speech/math-speech-cache.js +78 -0
  106. package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -0
  107. package/dist/services/tts/generated-speech/types.d.ts +104 -0
  108. package/dist/services/tts/generated-speech/types.js +18 -0
  109. package/dist/services/tts/generated-speech/types.js.map +1 -0
  110. package/dist/services/tts/highlight-pipeline/boundary-normalizer.d.ts +2 -0
  111. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +106 -0
  112. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -0
  113. package/dist/services/tts/highlight-pipeline/create-highlight-plan.d.ts +6 -0
  114. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +217 -0
  115. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -0
  116. package/dist/services/tts/highlight-pipeline/highlight-policy.d.ts +2 -0
  117. package/dist/services/tts/highlight-pipeline/highlight-policy.js +32 -0
  118. package/dist/services/tts/highlight-pipeline/highlight-policy.js.map +1 -0
  119. package/dist/services/tts/highlight-pipeline/index.d.ts +9 -0
  120. package/dist/services/tts/highlight-pipeline/index.js +9 -0
  121. package/dist/services/tts/highlight-pipeline/index.js.map +1 -0
  122. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.d.ts +3 -0
  123. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js +32 -0
  124. package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js.map +1 -0
  125. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.d.ts +29 -0
  126. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +44 -0
  127. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js.map +1 -0
  128. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.d.ts +2 -0
  129. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +20 -0
  130. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js.map +1 -0
  131. package/dist/services/tts/highlight-pipeline/readable-region.d.ts +1 -0
  132. package/dist/services/tts/highlight-pipeline/readable-region.js +19 -0
  133. package/dist/services/tts/highlight-pipeline/readable-region.js.map +1 -0
  134. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.d.ts +6 -0
  135. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js +198 -0
  136. package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js.map +1 -0
  137. package/dist/services/tts/highlight-pipeline/types.d.ts +72 -0
  138. package/dist/services/tts/highlight-pipeline/types.js +2 -0
  139. package/dist/services/tts/highlight-pipeline/types.js.map +1 -0
  140. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +2 -0
  141. package/dist/services/tts/highlight-pipeline/visible-map-range.js +22 -0
  142. package/dist/services/tts/highlight-pipeline/visible-map-range.js.map +1 -0
  143. package/dist/services/tts/math-alignment/index.d.ts +30 -0
  144. package/dist/services/tts/math-alignment/index.js +66 -0
  145. package/dist/services/tts/math-alignment/index.js.map +1 -0
  146. package/dist/services/tts/math-alignment/mathml-tokenizer.d.ts +2 -0
  147. package/dist/services/tts/math-alignment/mathml-tokenizer.js +317 -0
  148. package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -0
  149. package/dist/services/tts/math-alignment/range-resolver.d.ts +10 -0
  150. package/dist/services/tts/math-alignment/range-resolver.js +13 -0
  151. package/dist/services/tts/math-alignment/range-resolver.js.map +1 -0
  152. package/dist/services/tts/math-alignment/sequence-aligner.d.ts +7 -0
  153. package/dist/services/tts/math-alignment/sequence-aligner.js +119 -0
  154. package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -0
  155. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +31 -0
  156. package/dist/services/tts/math-alignment/speech-tokenizer.js +143 -0
  157. package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -0
  158. package/dist/services/tts/math-alignment/types.d.ts +58 -0
  159. package/dist/services/tts/math-alignment/types.js +2 -0
  160. package/dist/services/tts/math-alignment/types.js.map +1 -0
  161. package/dist/services/tts/math-aware-text-processing.d.ts +18 -0
  162. package/dist/services/tts/math-aware-text-processing.js +348 -0
  163. package/dist/services/tts/math-aware-text-processing.js.map +1 -0
  164. package/dist/services/tts/math-speech.d.ts +44 -0
  165. package/dist/services/tts/math-speech.js +147 -0
  166. package/dist/services/tts/math-speech.js.map +1 -0
  167. package/dist/services/tts/mathml-sanitization.d.ts +1 -0
  168. package/dist/services/tts/mathml-sanitization.js +170 -0
  169. package/dist/services/tts/mathml-sanitization.js.map +1 -0
  170. package/dist/services/tts/ssml/spoken-text.d.ts +33 -0
  171. package/dist/services/tts/ssml/spoken-text.js +159 -0
  172. package/dist/services/tts/ssml/spoken-text.js.map +1 -0
  173. package/dist/services/tts/text-processing.d.ts +3 -0
  174. package/dist/services/tts/text-processing.js +30 -4
  175. package/dist/services/tts/text-processing.js.map +1 -1
  176. package/dist/services/tts-runtime-config.d.ts +17 -1
  177. package/dist/services/tts-runtime-config.js +11 -1
  178. package/dist/services/tts-runtime-config.js.map +1 -1
  179. package/dist/tools/registrations/accessibility-tools.js +0 -1
  180. package/dist/tools/registrations/accessibility-tools.js.map +1 -1
  181. package/dist/tools/registrations/calculator.js +46 -5
  182. package/dist/tools/registrations/calculator.js.map +1 -1
  183. package/dist/tools/tool-tag-map.js +0 -1
  184. package/dist/tools/tool-tag-map.js.map +1 -1
  185. package/package.json +8 -11
  186. package/dist/services/PNPToolResolver.d.ts +0 -280
  187. package/dist/services/PNPToolResolver.js +0 -514
  188. package/dist/services/PNPToolResolver.js.map +0 -1
  189. package/dist/services/pnp-provenance.d.ts +0 -166
  190. package/dist/services/pnp-provenance.js +0 -231
  191. package/dist/services/pnp-provenance.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { sanitizeSsmlString } from "./SSMLExtractor.js";
1
2
  /**
2
3
  * Accessibility Catalog Resolver Service
3
4
  *
@@ -27,8 +28,18 @@
27
28
  export class AccessibilityCatalogResolver {
28
29
  assessmentCatalogs = new Map();
29
30
  itemCatalogs = new Map();
30
- defaultLanguage = "en";
31
- constructor(assessmentCatalogs, defaultLanguage = "en") {
31
+ scopedCatalogs = new Map();
32
+ // Matches the language the extractor tags cards with and the TTSService
33
+ // passes on lookup ("en-US"), so the default-language fallback rung in
34
+ // findMatchingCard is actually reachable for the common case.
35
+ defaultLanguage = "en-US";
36
+ // Egress sanitization cache. Catalogs are sanitized at registration, but that
37
+ // is a no-op when indexing runs without a DOM (SSR). Sanitizing again as
38
+ // spoken content leaves the resolver guarantees no raw author SSML reaches a
39
+ // provider regardless of where indexing happened; the cache keeps it to one
40
+ // pass per unique string (sanitizeSsmlString is idempotent).
41
+ sanitizedSpokenCache = new Map();
42
+ constructor(assessmentCatalogs, defaultLanguage = "en-US") {
32
43
  this.defaultLanguage = defaultLanguage;
33
44
  this.indexCatalogs(assessmentCatalogs ?? [], "assessment");
34
45
  }
@@ -49,10 +60,47 @@ export class AccessibilityCatalogResolver {
49
60
  */
50
61
  indexCatalogs(catalogs, source) {
51
62
  const targetMap = source === "assessment" ? this.assessmentCatalogs : this.itemCatalogs;
52
- for (const catalog of catalogs) {
63
+ for (const catalog of this.sanitizeCatalogs(catalogs)) {
64
+ if (targetMap.has(catalog.identifier)) {
65
+ console.warn(`[AccessibilityCatalogResolver] Duplicate ${source} catalog "${catalog.identifier}" ignored`);
66
+ continue;
67
+ }
53
68
  targetMap.set(catalog.identifier, catalog);
54
69
  }
55
70
  }
71
+ registerCatalogs(context, catalogs) {
72
+ if (!catalogs || catalogs.length === 0)
73
+ return () => { };
74
+ const key = this.getOwnerKey(context);
75
+ const existing = this.scopedCatalogs.get(key);
76
+ const scoped = existing?.catalogs ?? new Map();
77
+ const insertedIds = [];
78
+ for (const catalog of this.sanitizeCatalogs(catalogs)) {
79
+ if (scoped.has(catalog.identifier)) {
80
+ console.warn(`[AccessibilityCatalogResolver] Duplicate scoped catalog "${catalog.identifier}" ignored for ${key}`);
81
+ continue;
82
+ }
83
+ scoped.set(catalog.identifier, catalog);
84
+ insertedIds.push(catalog.identifier);
85
+ }
86
+ if (!existing) {
87
+ this.scopedCatalogs.set(key, {
88
+ context: { ...context },
89
+ catalogs: scoped,
90
+ });
91
+ }
92
+ return () => {
93
+ const current = this.scopedCatalogs.get(key);
94
+ if (current?.catalogs !== scoped)
95
+ return;
96
+ for (const insertedId of insertedIds) {
97
+ current.catalogs.delete(insertedId);
98
+ }
99
+ if (current.catalogs.size === 0) {
100
+ this.scopedCatalogs.delete(key);
101
+ }
102
+ };
103
+ }
56
104
  /**
57
105
  * Add item-level catalogs (called when rendering a new item)
58
106
  */
@@ -71,7 +119,9 @@ export class AccessibilityCatalogResolver {
71
119
  * Check if a catalog exists (checks both assessment and item levels)
72
120
  */
73
121
  hasCatalog(catalogId) {
74
- return (this.itemCatalogs.has(catalogId) || this.assessmentCatalogs.has(catalogId));
122
+ return (this.itemCatalogs.has(catalogId) ||
123
+ this.assessmentCatalogs.has(catalogId) ||
124
+ Array.from(this.scopedCatalogs.values()).some((owner) => owner.catalogs.has(catalogId)));
75
125
  }
76
126
  /**
77
127
  * Get alternative content for a catalog identifier
@@ -79,36 +129,128 @@ export class AccessibilityCatalogResolver {
79
129
  * Priority: Item-level catalogs take precedence over assessment-level
80
130
  */
81
131
  getAlternative(catalogId, options) {
132
+ const scopedCatalog = options.context
133
+ ? this.scopedCatalogs
134
+ .get(this.getOwnerKey(options.context))
135
+ ?.catalogs.get(catalogId)
136
+ : null;
137
+ if (scopedCatalog) {
138
+ const card = this.findMatchingCard(scopedCatalog, options);
139
+ if (card)
140
+ return this.resolveCard(catalogId, card, "item");
141
+ }
142
+ if (options.context) {
143
+ const candidates = this.findScopedCandidates(catalogId, options.context);
144
+ if (candidates.length === 1) {
145
+ const card = this.findMatchingCard(candidates[0], options);
146
+ if (card)
147
+ return this.resolveCard(catalogId, card, "item");
148
+ }
149
+ else if (candidates.length > 1) {
150
+ console.warn(`[AccessibilityCatalogResolver] Ambiguous scoped catalog "${catalogId}" for owner context`, options.context);
151
+ return null;
152
+ }
153
+ }
82
154
  // Check item-level first (higher precedence)
83
155
  const itemCatalog = this.itemCatalogs.get(catalogId);
84
156
  if (itemCatalog) {
85
157
  const card = this.findMatchingCard(itemCatalog, options);
86
- if (card) {
87
- return {
88
- catalogId,
89
- type: card.catalog,
90
- language: card.language,
91
- content: card.content,
92
- source: "item",
93
- };
94
- }
158
+ if (card)
159
+ return this.resolveCard(catalogId, card, "item");
95
160
  }
96
161
  // Fallback to assessment-level
97
162
  const assessmentCatalog = this.assessmentCatalogs.get(catalogId);
98
163
  if (assessmentCatalog) {
99
164
  const card = this.findMatchingCard(assessmentCatalog, options);
100
- if (card) {
101
- return {
102
- catalogId,
103
- type: card.catalog,
104
- language: card.language,
105
- content: card.content,
106
- source: "assessment",
107
- };
108
- }
165
+ if (card)
166
+ return this.resolveCard(catalogId, card, "assessment");
109
167
  }
110
168
  return null;
111
169
  }
170
+ resolveCard(catalogId, card, source) {
171
+ return {
172
+ catalogId,
173
+ type: card.catalog,
174
+ language: card.language,
175
+ content: card.catalog === "spoken"
176
+ ? this.ensureSpokenSanitized(card.content)
177
+ : card.content,
178
+ source,
179
+ };
180
+ }
181
+ ensureSpokenSanitized(content) {
182
+ const cached = this.sanitizedSpokenCache.get(content);
183
+ if (cached !== undefined)
184
+ return cached;
185
+ const sanitized = sanitizeSsmlString(content);
186
+ this.sanitizedSpokenCache.set(content, sanitized);
187
+ return sanitized;
188
+ }
189
+ getOwnerKey(context) {
190
+ return [
191
+ context.ownerKind,
192
+ context.assessmentId || "",
193
+ context.sectionId || "",
194
+ context.canonicalItemId || "",
195
+ context.itemId || "",
196
+ context.passageId || "",
197
+ context.modelId || "",
198
+ ].join("|");
199
+ }
200
+ // Flattened [id, catalog] pairs for every context-scoped owner. The primary
201
+ // runtime path registers catalogs as scoped, so enumeration APIs must include
202
+ // these or they silently under-report what TTS content exists.
203
+ scopedCatalogEntries() {
204
+ const entries = [];
205
+ for (const owner of this.scopedCatalogs.values()) {
206
+ for (const entry of owner.catalogs.entries()) {
207
+ entries.push(entry);
208
+ }
209
+ }
210
+ return entries;
211
+ }
212
+ findScopedCandidates(catalogId, context) {
213
+ return Array.from(this.scopedCatalogs.values())
214
+ .filter((owner) => this.isCompatibleOwnerContext(owner.context, context))
215
+ .map((owner) => owner.catalogs.get(catalogId))
216
+ .filter((catalog) => Boolean(catalog));
217
+ }
218
+ isCompatibleOwnerContext(registered, lookup) {
219
+ if (registered.ownerKind !== lookup.ownerKind)
220
+ return false;
221
+ if (lookup.assessmentId &&
222
+ registered.assessmentId !== lookup.assessmentId) {
223
+ return false;
224
+ }
225
+ if (lookup.sectionId && registered.sectionId !== lookup.sectionId) {
226
+ return false;
227
+ }
228
+ if (lookup.canonicalItemId &&
229
+ registered.canonicalItemId !== lookup.canonicalItemId) {
230
+ return false;
231
+ }
232
+ if (lookup.itemId && registered.itemId !== lookup.itemId) {
233
+ return false;
234
+ }
235
+ if (lookup.passageId && registered.passageId !== lookup.passageId) {
236
+ return false;
237
+ }
238
+ if (lookup.modelId && registered.modelId !== lookup.modelId) {
239
+ return false;
240
+ }
241
+ return true;
242
+ }
243
+ sanitizeCatalogs(catalogs) {
244
+ return catalogs.map((catalog) => ({
245
+ ...catalog,
246
+ cards: catalog.cards.map((card) => ({
247
+ ...card,
248
+ content: card.catalog === "spoken"
249
+ ? sanitizeSsmlString(card.content)
250
+ : card.content,
251
+ })),
252
+ }));
253
+ }
112
254
  /**
113
255
  * Find a matching catalog card based on lookup options
114
256
  */
@@ -167,16 +309,36 @@ export class AccessibilityCatalogResolver {
167
309
  }
168
310
  }
169
311
  }
312
+ // Add scoped (context-registered) alternatives. These resolve as "item"
313
+ // in getAlternative, so report them the same way here.
314
+ for (const [id, catalog] of this.scopedCatalogEntries()) {
315
+ if (id !== catalogId)
316
+ continue;
317
+ for (const card of catalog.cards) {
318
+ const exists = results.some((r) => r.type === card.catalog && r.language === card.language);
319
+ if (!exists) {
320
+ results.push({
321
+ catalogId,
322
+ type: card.catalog,
323
+ language: card.language,
324
+ content: card.content,
325
+ source: "item",
326
+ });
327
+ }
328
+ }
329
+ }
170
330
  return results;
171
331
  }
172
332
  /**
173
333
  * Get all catalog identifiers available (both assessment and item)
174
334
  */
175
335
  getAllCatalogIds() {
176
- const itemIds = Array.from(this.itemCatalogs.keys());
177
- const assessmentIds = Array.from(this.assessmentCatalogs.keys());
178
- // Combine and deduplicate
179
- return Array.from(new Set([...itemIds, ...assessmentIds]));
336
+ const scopedIds = this.scopedCatalogEntries().map(([id]) => id);
337
+ return Array.from(new Set([
338
+ ...this.itemCatalogs.keys(),
339
+ ...this.assessmentCatalogs.keys(),
340
+ ...scopedIds,
341
+ ]));
180
342
  }
181
343
  /**
182
344
  * Get statistics about available catalogs
@@ -201,9 +363,12 @@ export class AccessibilityCatalogResolver {
201
363
  }
202
364
  }
203
365
  return {
204
- totalCatalogs: this.assessmentCatalogs.size + this.itemCatalogs.size,
366
+ totalCatalogs: this.assessmentCatalogs.size +
367
+ this.itemCatalogs.size +
368
+ Array.from(this.scopedCatalogs.values()).reduce((total, owner) => total + owner.catalogs.size, 0),
205
369
  assessmentCatalogs: this.assessmentCatalogs.size,
206
- itemCatalogs: this.itemCatalogs.size,
370
+ itemCatalogs: this.itemCatalogs.size +
371
+ Array.from(this.scopedCatalogs.values()).reduce((total, owner) => total + owner.catalogs.size, 0),
207
372
  availableTypes: allTypes,
208
373
  availableLanguages: allLanguages,
209
374
  };
@@ -230,6 +395,11 @@ export class AccessibilityCatalogResolver {
230
395
  catalogIds.add(id);
231
396
  }
232
397
  }
398
+ for (const [id, catalog] of this.scopedCatalogEntries()) {
399
+ if (catalog.cards.some((card) => card.catalog === type)) {
400
+ catalogIds.add(id);
401
+ }
402
+ }
233
403
  return Array.from(catalogIds);
234
404
  }
235
405
  /**
@@ -238,6 +408,8 @@ export class AccessibilityCatalogResolver {
238
408
  reset() {
239
409
  this.assessmentCatalogs.clear();
240
410
  this.itemCatalogs.clear();
411
+ this.scopedCatalogs.clear();
412
+ this.sanitizedSpokenCache.clear();
241
413
  }
242
414
  /**
243
415
  * Destroy and cleanup
@@ -1 +1 @@
1
- {"version":3,"file":"AccessibilityCatalogResolver.js","sourceRoot":"","sources":["../../src/services/AccessibilityCatalogResolver.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAccessibilityCatalog,\n\tCatalogCard,\n} from \"@pie-players/pie-players-shared/types\";\n\n/**\n * Supported accessibility catalog types from QTI 3.0 / APIP\n */\nexport type CatalogType =\n\t| \"spoken\" // Text-to-speech scripts\n\t| \"sign-language\" // Video URLs for signed content\n\t| \"braille\" // Braille transcriptions\n\t| \"tactile\" // Tactile graphics descriptions\n\t| \"simplified-language\" // Plain language alternatives\n\t| \"audio-description\" // Extended audio descriptions\n\t| \"extended-description\" // Extended text descriptions\n\t| string; // Support custom types\n\n/**\n * Lookup options for catalog resolution\n */\nexport interface CatalogLookupOptions {\n\t/** Catalog type (e.g., 'spoken', 'sign-language') */\n\ttype: CatalogType;\n\t/** Language code (e.g., 'en-US', 'es-ES') */\n\tlanguage?: string;\n\t/** Fallback to default language if requested language not found */\n\tuseFallback?: boolean;\n}\n\n/**\n * Resolved catalog result\n */\nexport interface ResolvedCatalog {\n\t/** The catalog identifier */\n\tcatalogId: string;\n\t/** The catalog type */\n\ttype: CatalogType;\n\t/** The language code */\n\tlanguage?: string;\n\t/** The content (HTML, URL, or plain text) */\n\tcontent: string;\n\t/** Source of the catalog (assessment or item) */\n\tsource: \"assessment\" | \"item\";\n}\n\n/**\n * Statistics about available catalogs\n */\nexport interface CatalogStatistics {\n\t/** Total number of catalogs */\n\ttotalCatalogs: number;\n\t/** Number of assessment-level catalogs */\n\tassessmentCatalogs: number;\n\t/** Number of item-level catalogs */\n\titemCatalogs: number;\n\t/** Catalog types available */\n\tavailableTypes: Set<CatalogType>;\n\t/** Languages available */\n\tavailableLanguages: Set<string>;\n}\n\n/**\n * Accessibility Catalog Resolver Service\n *\n * Manages QTI 3.0 accessibility catalogs at both assessment and item levels.\n * Provides lookup and resolution services for alternative content representations.\n *\n * @example\n * ```typescript\n * // Initialize with assessment-level catalogs\n * const resolver = new AccessibilityCatalogResolver(assessment.accessibilityCatalogs);\n *\n * // Add item-level catalogs when rendering item\n * resolver.addItemCatalogs(item.accessibilityCatalogs);\n *\n * // Resolve catalog by identifier\n * const spokenContent = resolver.getAlternative('intro-passage', {\n * type: 'spoken',\n * language: 'en-US'\n * });\n *\n * // Check if catalog exists\n * if (resolver.hasCatalog('math-problem-1')) {\n * const braille = resolver.getAlternative('math-problem-1', { type: 'braille' });\n * }\n * ```\n */\nexport class AccessibilityCatalogResolver {\n\tprivate assessmentCatalogs: Map<string, AccessibilityCatalog> = new Map();\n\tprivate itemCatalogs: Map<string, AccessibilityCatalog> = new Map();\n\tprivate defaultLanguage: string = \"en\";\n\n\tconstructor(\n\t\tassessmentCatalogs?: AccessibilityCatalog[],\n\t\tdefaultLanguage: string = \"en\",\n\t) {\n\t\tthis.defaultLanguage = defaultLanguage;\n\t\tthis.indexCatalogs(assessmentCatalogs ?? [], \"assessment\");\n\t}\n\n\t/**\n\t * Set the default language for fallback resolution\n\t */\n\tsetDefaultLanguage(language: string): void {\n\t\tthis.defaultLanguage = language;\n\t}\n\n\t/**\n\t * Get the default language\n\t */\n\tgetDefaultLanguage(): string {\n\t\treturn this.defaultLanguage;\n\t}\n\n\t/**\n\t * Index catalogs into the appropriate map\n\t */\n\tprivate indexCatalogs(\n\t\tcatalogs: AccessibilityCatalog[],\n\t\tsource: \"assessment\" | \"item\",\n\t): void {\n\t\tconst targetMap =\n\t\t\tsource === \"assessment\" ? this.assessmentCatalogs : this.itemCatalogs;\n\n\t\tfor (const catalog of catalogs) {\n\t\t\ttargetMap.set(catalog.identifier, catalog);\n\t\t}\n\t}\n\n\t/**\n\t * Add item-level catalogs (called when rendering a new item)\n\t */\n\taddItemCatalogs(catalogs?: AccessibilityCatalog[]): void {\n\t\tif (!catalogs || catalogs.length === 0) return;\n\t\tthis.indexCatalogs(catalogs, \"item\");\n\t}\n\n\t/**\n\t * Clear item-level catalogs (called when leaving an item)\n\t */\n\tclearItemCatalogs(): void {\n\t\tthis.itemCatalogs.clear();\n\t}\n\n\t/**\n\t * Check if a catalog exists (checks both assessment and item levels)\n\t */\n\thasCatalog(catalogId: string): boolean {\n\t\treturn (\n\t\t\tthis.itemCatalogs.has(catalogId) || this.assessmentCatalogs.has(catalogId)\n\t\t);\n\t}\n\n\t/**\n\t * Get alternative content for a catalog identifier\n\t *\n\t * Priority: Item-level catalogs take precedence over assessment-level\n\t */\n\tgetAlternative(\n\t\tcatalogId: string,\n\t\toptions: CatalogLookupOptions,\n\t): ResolvedCatalog | null {\n\t\t// Check item-level first (higher precedence)\n\t\tconst itemCatalog = this.itemCatalogs.get(catalogId);\n\t\tif (itemCatalog) {\n\t\t\tconst card = this.findMatchingCard(itemCatalog, options);\n\t\t\tif (card) {\n\t\t\t\treturn {\n\t\t\t\t\tcatalogId,\n\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\tcontent: card.content,\n\t\t\t\t\tsource: \"item\",\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Fallback to assessment-level\n\t\tconst assessmentCatalog = this.assessmentCatalogs.get(catalogId);\n\t\tif (assessmentCatalog) {\n\t\t\tconst card = this.findMatchingCard(assessmentCatalog, options);\n\t\t\tif (card) {\n\t\t\t\treturn {\n\t\t\t\t\tcatalogId,\n\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\tcontent: card.content,\n\t\t\t\t\tsource: \"assessment\",\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Find a matching catalog card based on lookup options\n\t */\n\tprivate findMatchingCard(\n\t\tcatalog: AccessibilityCatalog,\n\t\toptions: CatalogLookupOptions,\n\t): CatalogCard | null {\n\t\tconst { type, language, useFallback = true } = options;\n\n\t\t// Try exact match (type + language)\n\t\tif (language) {\n\t\t\tconst exactMatch = catalog.cards.find(\n\t\t\t\t(card) => card.catalog === type && card.language === language,\n\t\t\t);\n\t\t\tif (exactMatch) return exactMatch;\n\t\t}\n\n\t\t// Try type match with default language (if fallback enabled)\n\t\tif (useFallback) {\n\t\t\tconst defaultMatch = catalog.cards.find(\n\t\t\t\t(card) =>\n\t\t\t\t\tcard.catalog === type && card.language === this.defaultLanguage,\n\t\t\t);\n\t\t\tif (defaultMatch) return defaultMatch;\n\n\t\t\t// Try type match without language constraint\n\t\t\tconst typeMatch = catalog.cards.find((card) => card.catalog === type);\n\t\t\tif (typeMatch) return typeMatch;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Get all available alternatives for a catalog identifier\n\t */\n\tgetAllAlternatives(catalogId: string): ResolvedCatalog[] {\n\t\tconst results: ResolvedCatalog[] = [];\n\n\t\t// Add item-level alternatives\n\t\tconst itemCatalog = this.itemCatalogs.get(catalogId);\n\t\tif (itemCatalog) {\n\t\t\tfor (const card of itemCatalog.cards) {\n\t\t\t\tresults.push({\n\t\t\t\t\tcatalogId,\n\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\tcontent: card.content,\n\t\t\t\t\tsource: \"item\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Add assessment-level alternatives (if not already provided by item)\n\t\tconst assessmentCatalog = this.assessmentCatalogs.get(catalogId);\n\t\tif (assessmentCatalog) {\n\t\t\tfor (const card of assessmentCatalog.cards) {\n\t\t\t\t// Only add if not already provided by item catalog\n\t\t\t\tconst exists = results.some(\n\t\t\t\t\t(r) => r.type === card.catalog && r.language === card.language,\n\t\t\t\t);\n\t\t\t\tif (!exists) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tcatalogId,\n\t\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\t\tcontent: card.content,\n\t\t\t\t\t\tsource: \"assessment\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Get all catalog identifiers available (both assessment and item)\n\t */\n\tgetAllCatalogIds(): string[] {\n\t\tconst itemIds = Array.from(this.itemCatalogs.keys());\n\t\tconst assessmentIds = Array.from(this.assessmentCatalogs.keys());\n\n\t\t// Combine and deduplicate\n\t\treturn Array.from(new Set([...itemIds, ...assessmentIds]));\n\t}\n\n\t/**\n\t * Get statistics about available catalogs\n\t */\n\tgetStatistics(): CatalogStatistics {\n\t\tconst allTypes = new Set<CatalogType>();\n\t\tconst allLanguages = new Set<string>();\n\n\t\t// Collect from assessment catalogs\n\t\tfor (const catalog of this.assessmentCatalogs.values()) {\n\t\t\tfor (const card of catalog.cards) {\n\t\t\t\tallTypes.add(card.catalog);\n\t\t\t\tif (card.language) allLanguages.add(card.language);\n\t\t\t}\n\t\t}\n\n\t\t// Collect from item catalogs\n\t\tfor (const catalog of this.itemCatalogs.values()) {\n\t\t\tfor (const card of catalog.cards) {\n\t\t\t\tallTypes.add(card.catalog);\n\t\t\t\tif (card.language) allLanguages.add(card.language);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\ttotalCatalogs: this.assessmentCatalogs.size + this.itemCatalogs.size,\n\t\t\tassessmentCatalogs: this.assessmentCatalogs.size,\n\t\t\titemCatalogs: this.itemCatalogs.size,\n\t\t\tavailableTypes: allTypes,\n\t\t\tavailableLanguages: allLanguages,\n\t\t};\n\t}\n\n\t/**\n\t * Check if a specific catalog type is available for a given catalog ID\n\t */\n\thasAlternativeType(catalogId: string, type: CatalogType): boolean {\n\t\tconst alternatives = this.getAllAlternatives(catalogId);\n\t\treturn alternatives.some((alt) => alt.type === type);\n\t}\n\n\t/**\n\t * Get all catalog IDs that have a specific type of alternative\n\t */\n\tgetCatalogsByType(type: CatalogType): string[] {\n\t\tconst catalogIds = new Set<string>();\n\n\t\tfor (const [id, catalog] of this.assessmentCatalogs.entries()) {\n\t\t\tif (catalog.cards.some((card) => card.catalog === type)) {\n\t\t\t\tcatalogIds.add(id);\n\t\t\t}\n\t\t}\n\n\t\tfor (const [id, catalog] of this.itemCatalogs.entries()) {\n\t\t\tif (catalog.cards.some((card) => card.catalog === type)) {\n\t\t\t\tcatalogIds.add(id);\n\t\t\t}\n\t\t}\n\n\t\treturn Array.from(catalogIds);\n\t}\n\n\t/**\n\t * Reset all catalogs (both assessment and item)\n\t */\n\treset(): void {\n\t\tthis.assessmentCatalogs.clear();\n\t\tthis.itemCatalogs.clear();\n\t}\n\n\t/**\n\t * Destroy and cleanup\n\t */\n\tdestroy(): void {\n\t\tthis.reset();\n\t}\n}\n"]}
1
+ {"version":3,"file":"AccessibilityCatalogResolver.js","sourceRoot":"","sources":["../../src/services/AccessibilityCatalogResolver.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n\tAccessibilityCatalog,\n\tCatalogCard,\n} from \"@pie-players/pie-players-shared/types\";\nimport { sanitizeSsmlString } from \"./SSMLExtractor.js\";\n\nexport type CatalogOwnerKind = \"global\" | \"passage\" | \"itemModel\";\n\nexport interface CatalogOwnerContext {\n\townerKind: CatalogOwnerKind;\n\tassessmentId?: string;\n\tsectionId?: string;\n\tcanonicalItemId?: string;\n\titemId?: string;\n\tpassageId?: string;\n\tmodelId?: string;\n}\n\nexport type CatalogLookupContext = CatalogOwnerContext;\n\n/**\n * Supported accessibility catalog types from QTI 3.0 / APIP\n */\nexport type CatalogType =\n\t| \"spoken\" // Text-to-speech scripts\n\t| \"sign-language\" // Video URLs for signed content\n\t| \"braille\" // Braille transcriptions\n\t| \"tactile\" // Tactile graphics descriptions\n\t| \"simplified-language\" // Plain language alternatives\n\t| \"audio-description\" // Extended audio descriptions\n\t| \"extended-description\" // Extended text descriptions\n\t| string; // Support custom types\n\n/**\n * Lookup options for catalog resolution\n */\nexport interface CatalogLookupOptions {\n\t/** Catalog type (e.g., 'spoken', 'sign-language') */\n\ttype: CatalogType;\n\t/** Language code (e.g., 'en-US', 'es-ES') */\n\tlanguage?: string;\n\t/** Fallback to default language if requested language not found */\n\tuseFallback?: boolean;\n\t/** Scope used to resolve local catalog idrefs for rendered content */\n\tcontext?: CatalogLookupContext;\n}\n\n/**\n * Resolved catalog result\n */\nexport interface ResolvedCatalog {\n\t/** The catalog identifier */\n\tcatalogId: string;\n\t/** The catalog type */\n\ttype: CatalogType;\n\t/** The language code */\n\tlanguage?: string;\n\t/** The content (HTML, URL, or plain text) */\n\tcontent: string;\n\t/** Source of the catalog (assessment or item) */\n\tsource: \"assessment\" | \"item\";\n}\n\n/**\n * Statistics about available catalogs\n */\nexport interface CatalogStatistics {\n\t/** Total number of catalogs */\n\ttotalCatalogs: number;\n\t/** Number of assessment-level catalogs */\n\tassessmentCatalogs: number;\n\t/** Number of item-level catalogs */\n\titemCatalogs: number;\n\t/** Catalog types available */\n\tavailableTypes: Set<CatalogType>;\n\t/** Languages available */\n\tavailableLanguages: Set<string>;\n}\n\n/**\n * Accessibility Catalog Resolver Service\n *\n * Manages QTI 3.0 accessibility catalogs at both assessment and item levels.\n * Provides lookup and resolution services for alternative content representations.\n *\n * @example\n * ```typescript\n * // Initialize with assessment-level catalogs\n * const resolver = new AccessibilityCatalogResolver(assessment.accessibilityCatalogs);\n *\n * // Add item-level catalogs when rendering item\n * resolver.addItemCatalogs(item.accessibilityCatalogs);\n *\n * // Resolve catalog by identifier\n * const spokenContent = resolver.getAlternative('intro-passage', {\n * type: 'spoken',\n * language: 'en-US'\n * });\n *\n * // Check if catalog exists\n * if (resolver.hasCatalog('math-problem-1')) {\n * const braille = resolver.getAlternative('math-problem-1', { type: 'braille' });\n * }\n * ```\n */\nexport class AccessibilityCatalogResolver {\n\tprivate assessmentCatalogs: Map<string, AccessibilityCatalog> = new Map();\n\tprivate itemCatalogs: Map<string, AccessibilityCatalog> = new Map();\n\tprivate scopedCatalogs = new Map<\n\t\tstring,\n\t\t{\n\t\t\tcontext: CatalogOwnerContext;\n\t\t\tcatalogs: Map<string, AccessibilityCatalog>;\n\t\t}\n\t>();\n\t// Matches the language the extractor tags cards with and the TTSService\n\t// passes on lookup (\"en-US\"), so the default-language fallback rung in\n\t// findMatchingCard is actually reachable for the common case.\n\tprivate defaultLanguage: string = \"en-US\";\n\t// Egress sanitization cache. Catalogs are sanitized at registration, but that\n\t// is a no-op when indexing runs without a DOM (SSR). Sanitizing again as\n\t// spoken content leaves the resolver guarantees no raw author SSML reaches a\n\t// provider regardless of where indexing happened; the cache keeps it to one\n\t// pass per unique string (sanitizeSsmlString is idempotent).\n\tprivate sanitizedSpokenCache = new Map<string, string>();\n\n\tconstructor(\n\t\tassessmentCatalogs?: AccessibilityCatalog[],\n\t\tdefaultLanguage: string = \"en-US\",\n\t) {\n\t\tthis.defaultLanguage = defaultLanguage;\n\t\tthis.indexCatalogs(assessmentCatalogs ?? [], \"assessment\");\n\t}\n\n\t/**\n\t * Set the default language for fallback resolution\n\t */\n\tsetDefaultLanguage(language: string): void {\n\t\tthis.defaultLanguage = language;\n\t}\n\n\t/**\n\t * Get the default language\n\t */\n\tgetDefaultLanguage(): string {\n\t\treturn this.defaultLanguage;\n\t}\n\n\t/**\n\t * Index catalogs into the appropriate map\n\t */\n\tprivate indexCatalogs(\n\t\tcatalogs: AccessibilityCatalog[],\n\t\tsource: \"assessment\" | \"item\",\n\t): void {\n\t\tconst targetMap =\n\t\t\tsource === \"assessment\" ? this.assessmentCatalogs : this.itemCatalogs;\n\n\t\tfor (const catalog of this.sanitizeCatalogs(catalogs)) {\n\t\t\tif (targetMap.has(catalog.identifier)) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[AccessibilityCatalogResolver] Duplicate ${source} catalog \"${catalog.identifier}\" ignored`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\ttargetMap.set(catalog.identifier, catalog);\n\t\t}\n\t}\n\n\tregisterCatalogs(\n\t\tcontext: CatalogOwnerContext,\n\t\tcatalogs?: AccessibilityCatalog[],\n\t): () => void {\n\t\tif (!catalogs || catalogs.length === 0) return () => {};\n\t\tconst key = this.getOwnerKey(context);\n\t\tconst existing = this.scopedCatalogs.get(key);\n\t\tconst scoped =\n\t\t\texisting?.catalogs ?? new Map<string, AccessibilityCatalog>();\n\t\tconst insertedIds: string[] = [];\n\t\tfor (const catalog of this.sanitizeCatalogs(catalogs)) {\n\t\t\tif (scoped.has(catalog.identifier)) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[AccessibilityCatalogResolver] Duplicate scoped catalog \"${catalog.identifier}\" ignored for ${key}`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tscoped.set(catalog.identifier, catalog);\n\t\t\tinsertedIds.push(catalog.identifier);\n\t\t}\n\t\tif (!existing) {\n\t\t\tthis.scopedCatalogs.set(key, {\n\t\t\t\tcontext: { ...context },\n\t\t\t\tcatalogs: scoped,\n\t\t\t});\n\t\t}\n\t\treturn () => {\n\t\t\tconst current = this.scopedCatalogs.get(key);\n\t\t\tif (current?.catalogs !== scoped) return;\n\t\t\tfor (const insertedId of insertedIds) {\n\t\t\t\tcurrent.catalogs.delete(insertedId);\n\t\t\t}\n\t\t\tif (current.catalogs.size === 0) {\n\t\t\t\tthis.scopedCatalogs.delete(key);\n\t\t\t}\n\t\t};\n\t}\n\n\t/**\n\t * Add item-level catalogs (called when rendering a new item)\n\t */\n\taddItemCatalogs(catalogs?: AccessibilityCatalog[]): void {\n\t\tif (!catalogs || catalogs.length === 0) return;\n\t\tthis.indexCatalogs(catalogs, \"item\");\n\t}\n\n\t/**\n\t * Clear item-level catalogs (called when leaving an item)\n\t */\n\tclearItemCatalogs(): void {\n\t\tthis.itemCatalogs.clear();\n\t}\n\n\t/**\n\t * Check if a catalog exists (checks both assessment and item levels)\n\t */\n\thasCatalog(catalogId: string): boolean {\n\t\treturn (\n\t\t\tthis.itemCatalogs.has(catalogId) ||\n\t\t\tthis.assessmentCatalogs.has(catalogId) ||\n\t\t\tArray.from(this.scopedCatalogs.values()).some((owner) =>\n\t\t\t\towner.catalogs.has(catalogId),\n\t\t\t)\n\t\t);\n\t}\n\n\t/**\n\t * Get alternative content for a catalog identifier\n\t *\n\t * Priority: Item-level catalogs take precedence over assessment-level\n\t */\n\tgetAlternative(\n\t\tcatalogId: string,\n\t\toptions: CatalogLookupOptions,\n\t): ResolvedCatalog | null {\n\t\tconst scopedCatalog = options.context\n\t\t\t? this.scopedCatalogs\n\t\t\t\t\t.get(this.getOwnerKey(options.context))\n\t\t\t\t\t?.catalogs.get(catalogId)\n\t\t\t: null;\n\t\tif (scopedCatalog) {\n\t\t\tconst card = this.findMatchingCard(scopedCatalog, options);\n\t\t\tif (card) return this.resolveCard(catalogId, card, \"item\");\n\t\t}\n\t\tif (options.context) {\n\t\t\tconst candidates = this.findScopedCandidates(catalogId, options.context);\n\t\t\tif (candidates.length === 1) {\n\t\t\t\tconst card = this.findMatchingCard(candidates[0], options);\n\t\t\t\tif (card) return this.resolveCard(catalogId, card, \"item\");\n\t\t\t} else if (candidates.length > 1) {\n\t\t\t\tconsole.warn(\n\t\t\t\t\t`[AccessibilityCatalogResolver] Ambiguous scoped catalog \"${catalogId}\" for owner context`,\n\t\t\t\t\toptions.context,\n\t\t\t\t);\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\t// Check item-level first (higher precedence)\n\t\tconst itemCatalog = this.itemCatalogs.get(catalogId);\n\t\tif (itemCatalog) {\n\t\t\tconst card = this.findMatchingCard(itemCatalog, options);\n\t\t\tif (card) return this.resolveCard(catalogId, card, \"item\");\n\t\t}\n\n\t\t// Fallback to assessment-level\n\t\tconst assessmentCatalog = this.assessmentCatalogs.get(catalogId);\n\t\tif (assessmentCatalog) {\n\t\t\tconst card = this.findMatchingCard(assessmentCatalog, options);\n\t\t\tif (card) return this.resolveCard(catalogId, card, \"assessment\");\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tprivate resolveCard(\n\t\tcatalogId: string,\n\t\tcard: CatalogCard,\n\t\tsource: ResolvedCatalog[\"source\"],\n\t): ResolvedCatalog {\n\t\treturn {\n\t\t\tcatalogId,\n\t\t\ttype: card.catalog,\n\t\t\tlanguage: card.language,\n\t\t\tcontent:\n\t\t\t\tcard.catalog === \"spoken\"\n\t\t\t\t\t? this.ensureSpokenSanitized(card.content)\n\t\t\t\t\t: card.content,\n\t\t\tsource,\n\t\t};\n\t}\n\n\tprivate ensureSpokenSanitized(content: string): string {\n\t\tconst cached = this.sanitizedSpokenCache.get(content);\n\t\tif (cached !== undefined) return cached;\n\t\tconst sanitized = sanitizeSsmlString(content);\n\t\tthis.sanitizedSpokenCache.set(content, sanitized);\n\t\treturn sanitized;\n\t}\n\n\tprivate getOwnerKey(context: CatalogOwnerContext): string {\n\t\treturn [\n\t\t\tcontext.ownerKind,\n\t\t\tcontext.assessmentId || \"\",\n\t\t\tcontext.sectionId || \"\",\n\t\t\tcontext.canonicalItemId || \"\",\n\t\t\tcontext.itemId || \"\",\n\t\t\tcontext.passageId || \"\",\n\t\t\tcontext.modelId || \"\",\n\t\t].join(\"|\");\n\t}\n\n\t// Flattened [id, catalog] pairs for every context-scoped owner. The primary\n\t// runtime path registers catalogs as scoped, so enumeration APIs must include\n\t// these or they silently under-report what TTS content exists.\n\tprivate scopedCatalogEntries(): Array<[string, AccessibilityCatalog]> {\n\t\tconst entries: Array<[string, AccessibilityCatalog]> = [];\n\t\tfor (const owner of this.scopedCatalogs.values()) {\n\t\t\tfor (const entry of owner.catalogs.entries()) {\n\t\t\t\tentries.push(entry);\n\t\t\t}\n\t\t}\n\t\treturn entries;\n\t}\n\n\tprivate findScopedCandidates(\n\t\tcatalogId: string,\n\t\tcontext: CatalogOwnerContext,\n\t): AccessibilityCatalog[] {\n\t\treturn Array.from(this.scopedCatalogs.values())\n\t\t\t.filter((owner) => this.isCompatibleOwnerContext(owner.context, context))\n\t\t\t.map((owner) => owner.catalogs.get(catalogId))\n\t\t\t.filter((catalog): catalog is AccessibilityCatalog => Boolean(catalog));\n\t}\n\n\tprivate isCompatibleOwnerContext(\n\t\tregistered: CatalogOwnerContext,\n\t\tlookup: CatalogOwnerContext,\n\t): boolean {\n\t\tif (registered.ownerKind !== lookup.ownerKind) return false;\n\t\tif (\n\t\t\tlookup.assessmentId &&\n\t\t\tregistered.assessmentId !== lookup.assessmentId\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t\tif (lookup.sectionId && registered.sectionId !== lookup.sectionId) {\n\t\t\treturn false;\n\t\t}\n\t\tif (\n\t\t\tlookup.canonicalItemId &&\n\t\t\tregistered.canonicalItemId !== lookup.canonicalItemId\n\t\t) {\n\t\t\treturn false;\n\t\t}\n\t\tif (lookup.itemId && registered.itemId !== lookup.itemId) {\n\t\t\treturn false;\n\t\t}\n\t\tif (lookup.passageId && registered.passageId !== lookup.passageId) {\n\t\t\treturn false;\n\t\t}\n\t\tif (lookup.modelId && registered.modelId !== lookup.modelId) {\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t}\n\n\tprivate sanitizeCatalogs(\n\t\tcatalogs: AccessibilityCatalog[],\n\t): AccessibilityCatalog[] {\n\t\treturn catalogs.map((catalog) => ({\n\t\t\t...catalog,\n\t\t\tcards: catalog.cards.map((card) => ({\n\t\t\t\t...card,\n\t\t\t\tcontent:\n\t\t\t\t\tcard.catalog === \"spoken\"\n\t\t\t\t\t\t? sanitizeSsmlString(card.content)\n\t\t\t\t\t\t: card.content,\n\t\t\t})),\n\t\t}));\n\t}\n\n\t/**\n\t * Find a matching catalog card based on lookup options\n\t */\n\tprivate findMatchingCard(\n\t\tcatalog: AccessibilityCatalog,\n\t\toptions: CatalogLookupOptions,\n\t): CatalogCard | null {\n\t\tconst { type, language, useFallback = true } = options;\n\n\t\t// Try exact match (type + language)\n\t\tif (language) {\n\t\t\tconst exactMatch = catalog.cards.find(\n\t\t\t\t(card) => card.catalog === type && card.language === language,\n\t\t\t);\n\t\t\tif (exactMatch) return exactMatch;\n\t\t}\n\n\t\t// Try type match with default language (if fallback enabled)\n\t\tif (useFallback) {\n\t\t\tconst defaultMatch = catalog.cards.find(\n\t\t\t\t(card) =>\n\t\t\t\t\tcard.catalog === type && card.language === this.defaultLanguage,\n\t\t\t);\n\t\t\tif (defaultMatch) return defaultMatch;\n\n\t\t\t// Try type match without language constraint\n\t\t\tconst typeMatch = catalog.cards.find((card) => card.catalog === type);\n\t\t\tif (typeMatch) return typeMatch;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/**\n\t * Get all available alternatives for a catalog identifier\n\t */\n\tgetAllAlternatives(catalogId: string): ResolvedCatalog[] {\n\t\tconst results: ResolvedCatalog[] = [];\n\n\t\t// Add item-level alternatives\n\t\tconst itemCatalog = this.itemCatalogs.get(catalogId);\n\t\tif (itemCatalog) {\n\t\t\tfor (const card of itemCatalog.cards) {\n\t\t\t\tresults.push({\n\t\t\t\t\tcatalogId,\n\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\tcontent: card.content,\n\t\t\t\t\tsource: \"item\",\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\t// Add assessment-level alternatives (if not already provided by item)\n\t\tconst assessmentCatalog = this.assessmentCatalogs.get(catalogId);\n\t\tif (assessmentCatalog) {\n\t\t\tfor (const card of assessmentCatalog.cards) {\n\t\t\t\t// Only add if not already provided by item catalog\n\t\t\t\tconst exists = results.some(\n\t\t\t\t\t(r) => r.type === card.catalog && r.language === card.language,\n\t\t\t\t);\n\t\t\t\tif (!exists) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tcatalogId,\n\t\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\t\tcontent: card.content,\n\t\t\t\t\t\tsource: \"assessment\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Add scoped (context-registered) alternatives. These resolve as \"item\"\n\t\t// in getAlternative, so report them the same way here.\n\t\tfor (const [id, catalog] of this.scopedCatalogEntries()) {\n\t\t\tif (id !== catalogId) continue;\n\t\t\tfor (const card of catalog.cards) {\n\t\t\t\tconst exists = results.some(\n\t\t\t\t\t(r) => r.type === card.catalog && r.language === card.language,\n\t\t\t\t);\n\t\t\t\tif (!exists) {\n\t\t\t\t\tresults.push({\n\t\t\t\t\t\tcatalogId,\n\t\t\t\t\t\ttype: card.catalog,\n\t\t\t\t\t\tlanguage: card.language,\n\t\t\t\t\t\tcontent: card.content,\n\t\t\t\t\t\tsource: \"item\",\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/**\n\t * Get all catalog identifiers available (both assessment and item)\n\t */\n\tgetAllCatalogIds(): string[] {\n\t\tconst scopedIds = this.scopedCatalogEntries().map(([id]) => id);\n\t\treturn Array.from(\n\t\t\tnew Set([\n\t\t\t\t...this.itemCatalogs.keys(),\n\t\t\t\t...this.assessmentCatalogs.keys(),\n\t\t\t\t...scopedIds,\n\t\t\t]),\n\t\t);\n\t}\n\n\t/**\n\t * Get statistics about available catalogs\n\t */\n\tgetStatistics(): CatalogStatistics {\n\t\tconst allTypes = new Set<CatalogType>();\n\t\tconst allLanguages = new Set<string>();\n\n\t\t// Collect from assessment catalogs\n\t\tfor (const catalog of this.assessmentCatalogs.values()) {\n\t\t\tfor (const card of catalog.cards) {\n\t\t\t\tallTypes.add(card.catalog);\n\t\t\t\tif (card.language) allLanguages.add(card.language);\n\t\t\t}\n\t\t}\n\n\t\t// Collect from item catalogs\n\t\tfor (const catalog of this.itemCatalogs.values()) {\n\t\t\tfor (const card of catalog.cards) {\n\t\t\t\tallTypes.add(card.catalog);\n\t\t\t\tif (card.language) allLanguages.add(card.language);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\ttotalCatalogs:\n\t\t\t\tthis.assessmentCatalogs.size +\n\t\t\t\tthis.itemCatalogs.size +\n\t\t\t\tArray.from(this.scopedCatalogs.values()).reduce(\n\t\t\t\t\t(total, owner) => total + owner.catalogs.size,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\tassessmentCatalogs: this.assessmentCatalogs.size,\n\t\t\titemCatalogs:\n\t\t\t\tthis.itemCatalogs.size +\n\t\t\t\tArray.from(this.scopedCatalogs.values()).reduce(\n\t\t\t\t\t(total, owner) => total + owner.catalogs.size,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\tavailableTypes: allTypes,\n\t\t\tavailableLanguages: allLanguages,\n\t\t};\n\t}\n\n\t/**\n\t * Check if a specific catalog type is available for a given catalog ID\n\t */\n\thasAlternativeType(catalogId: string, type: CatalogType): boolean {\n\t\tconst alternatives = this.getAllAlternatives(catalogId);\n\t\treturn alternatives.some((alt) => alt.type === type);\n\t}\n\n\t/**\n\t * Get all catalog IDs that have a specific type of alternative\n\t */\n\tgetCatalogsByType(type: CatalogType): string[] {\n\t\tconst catalogIds = new Set<string>();\n\n\t\tfor (const [id, catalog] of this.assessmentCatalogs.entries()) {\n\t\t\tif (catalog.cards.some((card) => card.catalog === type)) {\n\t\t\t\tcatalogIds.add(id);\n\t\t\t}\n\t\t}\n\n\t\tfor (const [id, catalog] of this.itemCatalogs.entries()) {\n\t\t\tif (catalog.cards.some((card) => card.catalog === type)) {\n\t\t\t\tcatalogIds.add(id);\n\t\t\t}\n\t\t}\n\n\t\tfor (const [id, catalog] of this.scopedCatalogEntries()) {\n\t\t\tif (catalog.cards.some((card) => card.catalog === type)) {\n\t\t\t\tcatalogIds.add(id);\n\t\t\t}\n\t\t}\n\n\t\treturn Array.from(catalogIds);\n\t}\n\n\t/**\n\t * Reset all catalogs (both assessment and item)\n\t */\n\treset(): void {\n\t\tthis.assessmentCatalogs.clear();\n\t\tthis.itemCatalogs.clear();\n\t\tthis.scopedCatalogs.clear();\n\t\tthis.sanitizedSpokenCache.clear();\n\t}\n\n\t/**\n\t * Destroy and cleanup\n\t */\n\tdestroy(): void {\n\t\tthis.reset();\n\t}\n}\n"]}
@@ -60,6 +60,8 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
60
60
  private config;
61
61
  private ttsWordHighlight;
62
62
  private ttsSentenceHighlight;
63
+ private ttsWordElementHighlights;
64
+ private ttsSentenceElementHighlights;
63
65
  private annotations;
64
66
  private colorHighlights;
65
67
  private nextAnnotationId;
@@ -91,6 +93,24 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
91
93
  * @param endOffset End position in text node
92
94
  */
93
95
  highlightTTSWord(textNode: Text, startOffset: number, endOffset: number): void;
96
+ /**
97
+ * Highlight a single element as the active TTS word (temporary).
98
+ *
99
+ * Unlike {@link highlightTTSWord} (which paints a CSS range over a text node)
100
+ * this marks the element itself via `data-pie-tts-word-element`. It is used
101
+ * for atomic targets that have no direct text node to range over — most
102
+ * notably MathJax CHTML tokens (e.g. `<mjx-mi><mjx-c/></mjx-mi>`, whose glyph
103
+ * lives in a font-driven pseudo-element a CSS range cannot paint) and
104
+ * whole-expression fallbacks.
105
+ *
106
+ * The supplied element is painted exactly; we deliberately do NOT walk up to
107
+ * a containing `<math>` / `<mjx-container>`. That escalation is what made
108
+ * MathJax-rendered math highlight as a full block while native MathML tracked
109
+ * per-token: a resolved single token must paint as a token, and only a
110
+ * genuine expression fallback (where the element already is the container)
111
+ * should paint the whole expression.
112
+ */
113
+ highlightTTSWordElement(element: Element): void;
94
114
  /**
95
115
  * Highlight the coarse TTS read-along band (temporary).
96
116
  * Usually one Range per visual line; CSS layer `tts-sentence` for compatibility.
@@ -98,6 +118,11 @@ export declare class HighlightCoordinator implements HighlightCoordinatorApi {
98
118
  * @param ranges Line (or sentence) ranges to paint with `--pie-tts-line-highlight`
99
119
  */
100
120
  highlightTTSSentence(ranges: Range[]): void;
121
+ private static readonly SENTENCE_FALLBACK_SELECTOR;
122
+ private static readonly WORD_FALLBACK_SELECTOR;
123
+ private highlightTTSElementFallbacks;
124
+ private highlightTTSWordElementFallbacks;
125
+ private highlightElementFallbacks;
101
126
  /**
102
127
  * Clear TTS word highlight
103
128
  */
@@ -46,6 +46,8 @@ export class HighlightCoordinator {
46
46
  config;
47
47
  ttsWordHighlight = null;
48
48
  ttsSentenceHighlight = null;
49
+ ttsWordElementHighlights = new Set();
50
+ ttsSentenceElementHighlights = new Set();
49
51
  annotations = new Map();
50
52
  // Shared highlights per color (one Highlight object per color, contains all ranges)
51
53
  colorHighlights = new Map();
@@ -285,6 +287,25 @@ export class HighlightCoordinator {
285
287
  color: inherit;
286
288
  }
287
289
 
290
+ [data-pie-tts-sentence-element="true"] {
291
+ background-color: var(--pie-tts-line-highlight, var(--pie-tts-sentence-highlight, color-mix(in srgb, var(--pie-missing, #ffeb3b) 38%, transparent)));
292
+ border-radius: 0.12em;
293
+ box-decoration-break: clone;
294
+ -webkit-box-decoration-break: clone;
295
+ }
296
+
297
+ [data-pie-tts-word-element="true"] {
298
+ background-color: var(--pie-tts-word-highlight, color-mix(in srgb, var(--pie-missing, #ffeb3b) 68%, transparent));
299
+ text-decoration: underline 2px solid var(--pie-tts-word-underline, color-mix(in srgb, var(--pie-text, #111827) 70%, transparent));
300
+ text-underline-offset: 2px;
301
+ text-shadow: 0 0 1px var(--pie-tts-word-shadow, color-mix(in srgb, var(--pie-text, #111827) 35%, transparent));
302
+ border-bottom: 2px solid var(--pie-tts-word-underline, color-mix(in srgb, var(--pie-text, #111827) 70%, transparent));
303
+ padding-bottom: 1px;
304
+ border-radius: 0.12em;
305
+ box-decoration-break: clone;
306
+ -webkit-box-decoration-break: clone;
307
+ }
308
+
288
309
  /* Annotation highlights - persistent */
289
310
  ::highlight(annotation-yellow) {
290
311
  background-color: color-mix(in srgb, var(--pie-missing, #ffff00) 35%, transparent);
@@ -340,6 +361,32 @@ export class HighlightCoordinator {
340
361
  // Add to highlight
341
362
  this.ttsWordHighlight.add(range);
342
363
  }
364
+ /**
365
+ * Highlight a single element as the active TTS word (temporary).
366
+ *
367
+ * Unlike {@link highlightTTSWord} (which paints a CSS range over a text node)
368
+ * this marks the element itself via `data-pie-tts-word-element`. It is used
369
+ * for atomic targets that have no direct text node to range over — most
370
+ * notably MathJax CHTML tokens (e.g. `<mjx-mi><mjx-c/></mjx-mi>`, whose glyph
371
+ * lives in a font-driven pseudo-element a CSS range cannot paint) and
372
+ * whole-expression fallbacks.
373
+ *
374
+ * The supplied element is painted exactly; we deliberately do NOT walk up to
375
+ * a containing `<math>` / `<mjx-container>`. That escalation is what made
376
+ * MathJax-rendered math highlight as a full block while native MathML tracked
377
+ * per-token: a resolved single token must paint as a token, and only a
378
+ * genuine expression fallback (where the element already is the container)
379
+ * should paint the whole expression.
380
+ */
381
+ highlightTTSWordElement(element) {
382
+ if (!this.ttsWordHighlight)
383
+ return;
384
+ this.applyAdaptiveTTSStyle(element);
385
+ // Clear previous word highlight (CSS range and any element attributes)
386
+ this.clearTTSWord();
387
+ element.setAttribute("data-pie-tts-word-element", "true");
388
+ this.ttsWordElementHighlights.add(element);
389
+ }
343
390
  /**
344
391
  * Highlight the coarse TTS read-along band (temporary).
345
392
  * Usually one Range per visual line; CSS layer `tts-sentence` for compatibility.
@@ -360,6 +407,69 @@ export class HighlightCoordinator {
360
407
  for (const range of ranges) {
361
408
  this.ttsSentenceHighlight.add(range);
362
409
  }
410
+ this.highlightTTSElementFallbacks(ranges);
411
+ }
412
+ // The read-along (sentence) band legitimately covers whole equations and
413
+ // replaced elements, so it escalates to them.
414
+ static SENTENCE_FALLBACK_SELECTOR = "math, mjx-container, svg, img, canvas, [role='img']";
415
+ // The word layer must NOT escalate a multi-node range to the enclosing
416
+ // `<math>` / `<mjx-container>`: that is what made an equation flash as a
417
+ // whole block whenever a spoken word's visible range crossed several math
418
+ // glyph nodes. Math is tracked per token (or held / region) by the highlight
419
+ // pipeline instead. Replaced elements (svg/img/canvas) still need the element
420
+ // fallback because a CSS range cannot paint them.
421
+ static WORD_FALLBACK_SELECTOR = "svg, img, canvas, [role='img']";
422
+ highlightTTSElementFallbacks(ranges) {
423
+ if (typeof Element === "undefined")
424
+ return;
425
+ this.highlightElementFallbacks(ranges, "data-pie-tts-sentence-element", this.ttsSentenceElementHighlights, HighlightCoordinator.SENTENCE_FALLBACK_SELECTOR);
426
+ }
427
+ highlightTTSWordElementFallbacks(ranges) {
428
+ if (typeof Element === "undefined")
429
+ return;
430
+ this.highlightElementFallbacks(ranges, "data-pie-tts-word-element", this.ttsWordElementHighlights, HighlightCoordinator.WORD_FALLBACK_SELECTOR);
431
+ }
432
+ highlightElementFallbacks(ranges, attributeName, trackedElements, selector) {
433
+ for (const range of ranges) {
434
+ const root = range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
435
+ ? range.commonAncestorContainer
436
+ : range.commonAncestorContainer.parentElement;
437
+ if (!root)
438
+ continue;
439
+ const candidates = [];
440
+ const rangeStartElement = range.startContainer.nodeType === Node.ELEMENT_NODE
441
+ ? range.startContainer
442
+ : range.startContainer.parentElement;
443
+ const rangeEndElement = range.endContainer.nodeType === Node.ELEMENT_NODE
444
+ ? range.endContainer
445
+ : range.endContainer.parentElement;
446
+ for (const element of [rangeStartElement, rangeEndElement]) {
447
+ let current = element;
448
+ while (current) {
449
+ if (current.matches?.(selector))
450
+ candidates.push(current);
451
+ current = current.parentElement;
452
+ }
453
+ }
454
+ if (root.matches?.(selector)) {
455
+ candidates.push(root);
456
+ }
457
+ candidates.push(...Array.from(root.querySelectorAll(selector)));
458
+ for (const element of new Set(candidates)) {
459
+ try {
460
+ if (!range.intersectsNode(element) &&
461
+ !element.contains(rangeStartElement) &&
462
+ !element.contains(rangeEndElement)) {
463
+ continue;
464
+ }
465
+ }
466
+ catch {
467
+ continue;
468
+ }
469
+ element.setAttribute(attributeName, "true");
470
+ trackedElements.add(element);
471
+ }
472
+ }
363
473
  }
364
474
  /**
365
475
  * Clear TTS word highlight
@@ -368,6 +478,10 @@ export class HighlightCoordinator {
368
478
  if (!this.ttsWordHighlight)
369
479
  return;
370
480
  this.ttsWordHighlight.clear();
481
+ for (const element of this.ttsWordElementHighlights) {
482
+ element.removeAttribute("data-pie-tts-word-element");
483
+ }
484
+ this.ttsWordElementHighlights.clear();
371
485
  }
372
486
  /**
373
487
  * Clear TTS sentence highlight
@@ -376,6 +490,10 @@ export class HighlightCoordinator {
376
490
  if (!this.ttsSentenceHighlight)
377
491
  return;
378
492
  this.ttsSentenceHighlight.clear();
493
+ for (const element of this.ttsSentenceElementHighlights) {
494
+ element.removeAttribute("data-pie-tts-sentence-element");
495
+ }
496
+ this.ttsSentenceElementHighlights.clear();
379
497
  }
380
498
  /**
381
499
  * Clear all TTS highlights
@@ -539,6 +657,13 @@ export class HighlightCoordinator {
539
657
  if (highlight) {
540
658
  highlight.clear();
541
659
  highlight.add(range);
660
+ if (type === HighlightType.TTS_WORD) {
661
+ for (const element of this.ttsWordElementHighlights) {
662
+ element.removeAttribute("data-pie-tts-word-element");
663
+ }
664
+ this.ttsWordElementHighlights.clear();
665
+ this.highlightTTSWordElementFallbacks([range]);
666
+ }
542
667
  }
543
668
  break;
544
669
  }