@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.
- package/README.md +59 -70
- package/dist/components/ItemToolBar.custom-element.js +776 -357
- package/dist/components/PieAssessmentToolkit.custom-element.js +19501 -3591
- package/dist/components/SectionToolBar.custom-element.js +1058 -540
- package/dist/index.d.ts +3 -6
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +1 -2
- package/dist/policy/core/ToolPolicyEngine.js +1 -2
- package/dist/policy/core/ToolPolicyEngine.js.map +1 -1
- package/dist/policy/core/pnp-policy-inputs.d.ts +2 -3
- package/dist/policy/core/pnp-policy-inputs.js +2 -3
- package/dist/policy/core/pnp-policy-inputs.js.map +1 -1
- package/dist/policy/core/provenance.d.ts +7 -34
- package/dist/policy/core/provenance.js +4 -21
- package/dist/policy/core/provenance.js.map +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +6 -19
- package/dist/policy/sources/PnpPolicySource.js +6 -19
- package/dist/policy/sources/PnpPolicySource.js.map +1 -1
- package/dist/runtime/SectionRuntimeEngine.d.ts +13 -28
- package/dist/runtime/SectionRuntimeEngine.js +13 -27
- package/dist/runtime/SectionRuntimeEngine.js.map +1 -1
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +2 -2
- package/dist/runtime/adapter/SectionEngineAdapter.js +2 -2
- package/dist/runtime/adapter/SectionEngineAdapter.js.map +1 -1
- package/dist/runtime/adapter/coordinator-bridge.d.ts +1 -1
- package/dist/runtime/adapter/coordinator-bridge.js +1 -1
- package/dist/runtime/adapter/coordinator-bridge.js.map +1 -1
- package/dist/runtime/adapter/dom-event-bridge.d.ts +1 -1
- package/dist/runtime/adapter/dom-event-bridge.js +1 -1
- package/dist/runtime/adapter/dom-event-bridge.js.map +1 -1
- package/dist/runtime/catalog-registration.d.ts +12 -0
- package/dist/runtime/catalog-registration.js +54 -0
- package/dist/runtime/catalog-registration.js.map +1 -0
- package/dist/runtime/core/engine-output.d.ts +2 -2
- package/dist/runtime/core/engine-output.js +2 -2
- package/dist/runtime/core/engine-output.js.map +1 -1
- package/dist/runtime/core/engine-readiness.d.ts +2 -2
- package/dist/runtime/core/engine-readiness.js.map +1 -1
- package/dist/runtime/core/engine-resolver.d.ts +13 -41
- package/dist/runtime/core/engine-resolver.js +19 -76
- package/dist/runtime/core/engine-resolver.js.map +1 -1
- package/dist/runtime/core/engine-transition.d.ts +1 -1
- package/dist/runtime/core/engine-transition.js +1 -1
- package/dist/runtime/core/engine-transition.js.map +1 -1
- package/dist/runtime/engine.d.ts +1 -1
- package/dist/runtime/engine.js.map +1 -1
- package/dist/runtime/section-runtime-engine-host-context.d.ts +22 -15
- package/dist/runtime/section-runtime-engine-host-context.js +10 -10
- package/dist/runtime/section-runtime-engine-host-context.js.map +1 -1
- package/dist/runtime/stage-emit-gate.d.ts +4 -4
- package/dist/runtime/stage-emit-gate.js +3 -3
- package/dist/runtime/stage-emit-gate.js.map +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +23 -0
- package/dist/services/AccessibilityCatalogResolver.js +200 -28
- package/dist/services/AccessibilityCatalogResolver.js.map +1 -1
- package/dist/services/HighlightCoordinator.d.ts +25 -0
- package/dist/services/HighlightCoordinator.js +125 -0
- package/dist/services/HighlightCoordinator.js.map +1 -1
- package/dist/services/SSMLExtractor.d.ts +16 -1
- package/dist/services/SSMLExtractor.js +86 -7
- package/dist/services/SSMLExtractor.js.map +1 -1
- package/dist/services/TTSService.d.ts +30 -4
- package/dist/services/TTSService.js +652 -159
- package/dist/services/TTSService.js.map +1 -1
- package/dist/services/ToolRegistry.d.ts +11 -2
- package/dist/services/ToolRegistry.js +1 -1
- package/dist/services/ToolRegistry.js.map +1 -1
- package/dist/services/ToolkitCoordinator.d.ts +16 -73
- package/dist/services/ToolkitCoordinator.js +34 -113
- package/dist/services/ToolkitCoordinator.js.map +1 -1
- package/dist/services/interfaces.d.ts +20 -2
- package/dist/services/interfaces.js.map +1 -1
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +7 -7
- package/dist/services/tool-config-validation.js.map +1 -1
- package/dist/services/tool-instance-id.js +1 -1
- package/dist/services/tool-instance-id.js.map +1 -1
- package/dist/services/tool-providers/TTSToolProvider.js +3 -3
- package/dist/services/tool-providers/TTSToolProvider.js.map +1 -1
- package/dist/services/tools-config-normalizer.d.ts +13 -2
- package/dist/services/tools-config-normalizer.js +3 -8
- package/dist/services/tools-config-normalizer.js.map +1 -1
- package/dist/services/tts/browser-provider.js +2 -0
- package/dist/services/tts/browser-provider.js.map +1 -1
- package/dist/services/tts/catalog-span-alignment.d.ts +39 -0
- package/dist/services/tts/catalog-span-alignment.js +300 -0
- package/dist/services/tts/catalog-span-alignment.js.map +1 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +33 -0
- package/dist/services/tts/generated-speech/assemble-plan.js +111 -0
- package/dist/services/tts/generated-speech/assemble-plan.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +25 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +66 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +19 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js +71 -0
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.js.map +1 -0
- package/dist/services/tts/generated-speech/dom/types.d.ts +37 -0
- package/dist/services/tts/generated-speech/dom/types.js +2 -0
- package/dist/services/tts/generated-speech/dom/types.js.map +1 -0
- package/dist/services/tts/generated-speech/index.d.ts +17 -0
- package/dist/services/tts/generated-speech/index.js +17 -0
- package/dist/services/tts/generated-speech/index.js.map +1 -0
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +15 -0
- package/dist/services/tts/generated-speech/math-speech-cache.js +78 -0
- package/dist/services/tts/generated-speech/math-speech-cache.js.map +1 -0
- package/dist/services/tts/generated-speech/types.d.ts +104 -0
- package/dist/services/tts/generated-speech/types.js +18 -0
- package/dist/services/tts/generated-speech/types.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +106 -0
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.d.ts +6 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +217 -0
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.js +32 -0
- package/dist/services/tts/highlight-pipeline/highlight-policy.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/index.d.ts +9 -0
- package/dist/services/tts/highlight-pipeline/index.js +9 -0
- package/dist/services/tts/highlight-pipeline/index.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.d.ts +3 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js +32 -0
- package/dist/services/tts/highlight-pipeline/math-semantic-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.d.ts +29 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +44 -0
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +20 -0
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/readable-region.d.ts +1 -0
- package/dist/services/tts/highlight-pipeline/readable-region.js +19 -0
- package/dist/services/tts/highlight-pipeline/readable-region.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.d.ts +6 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js +198 -0
- package/dist/services/tts/highlight-pipeline/rendered-math-target-resolver.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/types.d.ts +72 -0
- package/dist/services/tts/highlight-pipeline/types.js +2 -0
- package/dist/services/tts/highlight-pipeline/types.js.map +1 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +2 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +22 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js.map +1 -0
- package/dist/services/tts/math-alignment/index.d.ts +30 -0
- package/dist/services/tts/math-alignment/index.js +66 -0
- package/dist/services/tts/math-alignment/index.js.map +1 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.d.ts +2 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.js +317 -0
- package/dist/services/tts/math-alignment/mathml-tokenizer.js.map +1 -0
- package/dist/services/tts/math-alignment/range-resolver.d.ts +10 -0
- package/dist/services/tts/math-alignment/range-resolver.js +13 -0
- package/dist/services/tts/math-alignment/range-resolver.js.map +1 -0
- package/dist/services/tts/math-alignment/sequence-aligner.d.ts +7 -0
- package/dist/services/tts/math-alignment/sequence-aligner.js +119 -0
- package/dist/services/tts/math-alignment/sequence-aligner.js.map +1 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +31 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.js +143 -0
- package/dist/services/tts/math-alignment/speech-tokenizer.js.map +1 -0
- package/dist/services/tts/math-alignment/types.d.ts +58 -0
- package/dist/services/tts/math-alignment/types.js +2 -0
- package/dist/services/tts/math-alignment/types.js.map +1 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +18 -0
- package/dist/services/tts/math-aware-text-processing.js +348 -0
- package/dist/services/tts/math-aware-text-processing.js.map +1 -0
- package/dist/services/tts/math-speech.d.ts +44 -0
- package/dist/services/tts/math-speech.js +147 -0
- package/dist/services/tts/math-speech.js.map +1 -0
- package/dist/services/tts/mathml-sanitization.d.ts +1 -0
- package/dist/services/tts/mathml-sanitization.js +170 -0
- package/dist/services/tts/mathml-sanitization.js.map +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +33 -0
- package/dist/services/tts/ssml/spoken-text.js +159 -0
- package/dist/services/tts/ssml/spoken-text.js.map +1 -0
- package/dist/services/tts/text-processing.d.ts +3 -0
- package/dist/services/tts/text-processing.js +30 -4
- package/dist/services/tts/text-processing.js.map +1 -1
- package/dist/services/tts-runtime-config.d.ts +17 -1
- package/dist/services/tts-runtime-config.js +11 -1
- package/dist/services/tts-runtime-config.js.map +1 -1
- package/dist/tools/registrations/accessibility-tools.js +0 -1
- package/dist/tools/registrations/accessibility-tools.js.map +1 -1
- package/dist/tools/registrations/calculator.js +46 -5
- package/dist/tools/registrations/calculator.js.map +1 -1
- package/dist/tools/tool-tag-map.js +0 -1
- package/dist/tools/tool-tag-map.js.map +1 -1
- package/package.json +8 -11
- package/dist/services/PNPToolResolver.d.ts +0 -280
- package/dist/services/PNPToolResolver.js +0 -514
- package/dist/services/PNPToolResolver.js.map +0 -1
- package/dist/services/pnp-provenance.d.ts +0 -166
- package/dist/services/pnp-provenance.js +0 -231
- 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
|
-
|
|
31
|
-
|
|
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) ||
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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 +
|
|
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
|
}
|