@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0
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 +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { tokenizeAlignmentText, tokenizeSpeechSource, } from "./math-alignment/speech-tokenizer.js";
|
|
2
2
|
import { normalizeTextForSpeech } from "./text-processing.js";
|
|
3
|
+
// A spoken token equal to a visible token; phrase targets score below it.
|
|
4
|
+
const EXACT_TOKEN_SCORE = 10;
|
|
5
|
+
// Below this, an anchored chunk highlights its region instead of spans.
|
|
6
|
+
const MIN_ANCHOR_SPAN_CONFIDENCE = 0.12;
|
|
3
7
|
// LIMITATION (i18n): the phrase table is English, so multi-word operator
|
|
4
8
|
// phrases ("plus or minus", "divided by") only align for English speech; other
|
|
5
9
|
// locales degrade to coarse region highlighting.
|
|
@@ -29,7 +33,7 @@ const createCandidates = (spokenTokens, visibleTokens) => {
|
|
|
29
33
|
spokenStartToken: spokenIndex,
|
|
30
34
|
spokenEndToken: spokenIndex + 1,
|
|
31
35
|
visibleToken: visibleIndex,
|
|
32
|
-
score:
|
|
36
|
+
score: EXACT_TOKEN_SCORE,
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
39
|
}
|
|
@@ -57,6 +61,9 @@ const createCandidates = (spokenTokens, visibleTokens) => {
|
|
|
57
61
|
}
|
|
58
62
|
return candidates;
|
|
59
63
|
};
|
|
64
|
+
// The highest-scoring chain of candidates monotonic in both spoken and visible
|
|
65
|
+
// order (O(n²) DP). A gap whose spoken and visible token counts differ by more
|
|
66
|
+
// than 2 pays the excess.
|
|
60
67
|
const computeAnchors = (spokenTokens, visibleTokens) => {
|
|
61
68
|
const candidates = createCandidates(spokenTokens, visibleTokens).sort((left, right) => spokenTokens[left.spokenStartToken].start -
|
|
62
69
|
spokenTokens[right.spokenStartToken].start ||
|
|
@@ -116,7 +123,7 @@ const scoreConfidence = (anchors, spokenTokens, visibleTokens) => {
|
|
|
116
123
|
Math.max(1, Math.min(spokenTokens.length, visibleTokens.length));
|
|
117
124
|
const averageScore = anchors.reduce((total, anchor) => total + anchor.score, 0) /
|
|
118
125
|
anchors.length /
|
|
119
|
-
|
|
126
|
+
EXACT_TOKEN_SCORE;
|
|
120
127
|
return Math.min(1, tokenCoverage * averageScore);
|
|
121
128
|
};
|
|
122
129
|
export const createCatalogSpanAlignment = (args) => {
|
|
@@ -130,17 +137,17 @@ export const createCatalogSpanAlignment = (args) => {
|
|
|
130
137
|
const confidence = scoreConfidence(anchors, spokenTokens, visibleTokens);
|
|
131
138
|
const isExact = speech.spokenText === visibleText && speech.spokenText.length > 0;
|
|
132
139
|
const hasShortSingleExactAnchor = anchors.length === 1 &&
|
|
133
|
-
anchors[0].score ===
|
|
140
|
+
anchors[0].score === EXACT_TOKEN_SCORE &&
|
|
134
141
|
spokenTokens.length <= 2 &&
|
|
135
142
|
visibleTokens.length <= 2;
|
|
143
|
+
const anchorsAreTrusted = (anchors.length >= 2 || hasShortSingleExactAnchor) &&
|
|
144
|
+
confidence >= MIN_ANCHOR_SPAN_CONFIDENCE;
|
|
136
145
|
const playbackMode = speech.unsupportedSemantic
|
|
137
146
|
? "region-fallback"
|
|
138
147
|
: isExact
|
|
139
148
|
? "exact-word"
|
|
140
|
-
:
|
|
141
|
-
?
|
|
142
|
-
? "anchor-span"
|
|
143
|
-
: "region-fallback"
|
|
149
|
+
: anchorsAreTrusted
|
|
150
|
+
? "anchor-span"
|
|
144
151
|
: "region-fallback";
|
|
145
152
|
return {
|
|
146
153
|
speech,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tree read-aloud walks: the flat tree, which is the tree the page renders.
|
|
3
|
+
*
|
|
4
|
+
* An element with an open shadow root contributes that root's children in place
|
|
5
|
+
* of its own light children, and a `<slot>` inside a shadow root contributes the
|
|
6
|
+
* nodes assigned to it, flattened through nested slots, so a host's light
|
|
7
|
+
* children are reached once, at the slot that renders them. Closed shadow roots
|
|
8
|
+
* are invisible to script, so their hosts are walked as light DOM.
|
|
9
|
+
*
|
|
10
|
+
* Toolkit chrome is skipped wherever it appears: toolbars and tool elements
|
|
11
|
+
* render their controls into open shadow roots, and descending into them would
|
|
12
|
+
* read button labels as content. So are the empty `aria-hidden` anchors the
|
|
13
|
+
* shells and the toolkit keep beside their slots, and elements whose text never
|
|
14
|
+
* renders (`style`, `script`, `noscript`, `template`).
|
|
15
|
+
*
|
|
16
|
+
* Node types are compared as the spec's numbers, so this module reads no DOM
|
|
17
|
+
* global, at import or at call time.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isShadowRootNode: (node: Node | null | undefined) => node is ShadowRoot;
|
|
20
|
+
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
21
|
+
export declare const flatTreeChildNodes: (node: Node) => Node[];
|
|
22
|
+
/**
|
|
23
|
+
* What a visitor returns: `"skip"` leaves the node's subtree out, `"stop"` ends
|
|
24
|
+
* the walk, anything else descends.
|
|
25
|
+
*/
|
|
26
|
+
export type FlatTreeVisitResult = "skip" | "stop" | undefined;
|
|
27
|
+
/** Visits `root`'s flat-tree descendants in rendering order; `root` itself is not visited. */
|
|
28
|
+
export declare const walkFlatTree: (root: Node, visit: (node: Node) => FlatTreeVisitResult) => void;
|
|
29
|
+
/** `root`'s text nodes in rendering order. */
|
|
30
|
+
export declare const flatTreeTextNodes: (root: Node) => Text[];
|
|
31
|
+
/** `textContent` over the flat tree: shadow content in, skipped elements out. */
|
|
32
|
+
export declare const flatTextContent: (root: Node) => string;
|
|
33
|
+
/** Elements under `root` matching `selector`, in rendering order. */
|
|
34
|
+
export declare const flatQuerySelectorAll: (root: Node, selector: string) => Element[];
|
|
35
|
+
export declare const flatQuerySelector: (root: Node, selector: string) => Element | null;
|
|
36
|
+
export declare const flatTreeParentElement: (node: Node) => Element | null;
|
|
37
|
+
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
38
|
+
export declare const flatTreeClosest: (node: Node, selector: string) => Element | null;
|
|
39
|
+
/**
|
|
40
|
+
* `node`'s parent element, continuing from a shadow root to its host. This is the
|
|
41
|
+
* chain inheritance follows (`lang`, regions an author marks up), and for slotted
|
|
42
|
+
* content it stays in the light tree the author wrote.
|
|
43
|
+
*/
|
|
44
|
+
export declare const composedParentElement: (node: Node) => Element | null;
|
|
45
|
+
/**
|
|
46
|
+
* The nearest element at or above `node` on the composed chain that matches
|
|
47
|
+
* `selector`, or null. With `boundary`, the climb ends after testing it.
|
|
48
|
+
*/
|
|
49
|
+
export declare const composedClosest: (node: Node, selector: string, boundary?: Element | null) => Element | null;
|
|
50
|
+
/** `contains` across shadow boundaries: whether `node` is `container` or below it. */
|
|
51
|
+
export declare const composedContains: (container: Node | null | undefined, node: Node | null | undefined) => boolean;
|
|
52
|
+
/**
|
|
53
|
+
* `node`, or the shadow host above it, that lies in the tree rooted at
|
|
54
|
+
* `treeRoot`; null when `node` is not inside that tree at any depth.
|
|
55
|
+
*/
|
|
56
|
+
export declare const retargetToTree: (node: Node, treeRoot: Node) => Node | null;
|
|
57
|
+
/**
|
|
58
|
+
* Whether the character at `offset` in `node` lies inside `range`. A live range
|
|
59
|
+
* holds nodes of one tree only, so a node in a shadow tree below it counts as
|
|
60
|
+
* inside when the range holds that tree's host whole.
|
|
61
|
+
*/
|
|
62
|
+
export declare const rangeHoldsTextPosition: (range: Range, node: Text, offset: number) => boolean;
|
|
63
|
+
/** Whether `range` intersects `node`, counting a shadow tree below the range as {@link rangeHoldsTextPosition} does. */
|
|
64
|
+
export declare const rangeIntersectsComposedNode: (range: Range, node: Node) => boolean;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tree read-aloud walks: the flat tree, which is the tree the page renders.
|
|
3
|
+
*
|
|
4
|
+
* An element with an open shadow root contributes that root's children in place
|
|
5
|
+
* of its own light children, and a `<slot>` inside a shadow root contributes the
|
|
6
|
+
* nodes assigned to it, flattened through nested slots, so a host's light
|
|
7
|
+
* children are reached once, at the slot that renders them. Closed shadow roots
|
|
8
|
+
* are invisible to script, so their hosts are walked as light DOM.
|
|
9
|
+
*
|
|
10
|
+
* Toolkit chrome is skipped wherever it appears: toolbars and tool elements
|
|
11
|
+
* render their controls into open shadow roots, and descending into them would
|
|
12
|
+
* read button labels as content. So are the empty `aria-hidden` anchors the
|
|
13
|
+
* shells and the toolkit keep beside their slots, and elements whose text never
|
|
14
|
+
* renders (`style`, `script`, `noscript`, `template`).
|
|
15
|
+
*
|
|
16
|
+
* Node types are compared as the spec's numbers, so this module reads no DOM
|
|
17
|
+
* global, at import or at call time.
|
|
18
|
+
*/
|
|
19
|
+
const ELEMENT_NODE = 1;
|
|
20
|
+
const TEXT_NODE = 3;
|
|
21
|
+
const DOCUMENT_FRAGMENT_NODE = 11;
|
|
22
|
+
const CHROME_TAGS = new Set([
|
|
23
|
+
"pie-item-toolbar",
|
|
24
|
+
"pie-section-toolbar",
|
|
25
|
+
"pie-item-player-session-debugger",
|
|
26
|
+
]);
|
|
27
|
+
const CHROME_TAG_PREFIXES = ["pie-tool-", "pie-section-player-tools-"];
|
|
28
|
+
const UNRENDERED_TEXT_TAGS = new Set([
|
|
29
|
+
"style",
|
|
30
|
+
"script",
|
|
31
|
+
"noscript",
|
|
32
|
+
"template",
|
|
33
|
+
]);
|
|
34
|
+
export const isShadowRootNode = (node) => !!node &&
|
|
35
|
+
node.nodeType === DOCUMENT_FRAGMENT_NODE &&
|
|
36
|
+
node.host?.nodeType === ELEMENT_NODE;
|
|
37
|
+
/** Toolbars, tool elements and debug panels: chrome around content, never content. */
|
|
38
|
+
const isToolkitChromeElement = (element) => {
|
|
39
|
+
const tag = element.localName?.toLowerCase() || "";
|
|
40
|
+
if (CHROME_TAGS.has(tag))
|
|
41
|
+
return true;
|
|
42
|
+
return CHROME_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix));
|
|
43
|
+
};
|
|
44
|
+
const isSkippedElement = (element) => {
|
|
45
|
+
const tag = element.localName?.toLowerCase() || "";
|
|
46
|
+
if (UNRENDERED_TEXT_TAGS.has(tag))
|
|
47
|
+
return true;
|
|
48
|
+
if (isToolkitChromeElement(element))
|
|
49
|
+
return true;
|
|
50
|
+
// A shell's anchor: an `aria-hidden` element at the top of a shadow root,
|
|
51
|
+
// there to reach the host's context and holding nothing to read.
|
|
52
|
+
return (isShadowRootNode(element.parentNode) &&
|
|
53
|
+
element.getAttribute("aria-hidden") === "true");
|
|
54
|
+
};
|
|
55
|
+
const slotFlattenedNodes = (slot) => {
|
|
56
|
+
if (!isShadowRootNode(slot.getRootNode?.()))
|
|
57
|
+
return null;
|
|
58
|
+
const assignedNodes = slot.assignedNodes;
|
|
59
|
+
if (typeof assignedNodes !== "function")
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
return assignedNodes.call(slot, { flatten: true });
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
69
|
+
export const flatTreeChildNodes = (node) => {
|
|
70
|
+
let children = node.childNodes;
|
|
71
|
+
if (node.nodeType === ELEMENT_NODE) {
|
|
72
|
+
const element = node;
|
|
73
|
+
const shadowRoot = element.shadowRoot;
|
|
74
|
+
if (shadowRoot) {
|
|
75
|
+
children = shadowRoot.childNodes;
|
|
76
|
+
}
|
|
77
|
+
else if (element.localName?.toLowerCase() === "slot") {
|
|
78
|
+
children = slotFlattenedNodes(element) ?? element.childNodes;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const result = [];
|
|
82
|
+
for (const child of Array.from(children || [])) {
|
|
83
|
+
if (child.nodeType === ELEMENT_NODE && isSkippedElement(child)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
result.push(child);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
/** Visits `root`'s flat-tree descendants in rendering order; `root` itself is not visited. */
|
|
91
|
+
export const walkFlatTree = (root, visit) => {
|
|
92
|
+
const descend = (node) => {
|
|
93
|
+
for (const child of flatTreeChildNodes(node)) {
|
|
94
|
+
const result = visit(child);
|
|
95
|
+
if (result === "stop")
|
|
96
|
+
return false;
|
|
97
|
+
if (result === "skip")
|
|
98
|
+
continue;
|
|
99
|
+
if (!descend(child))
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
};
|
|
104
|
+
descend(root);
|
|
105
|
+
};
|
|
106
|
+
/** `root`'s text nodes in rendering order. */
|
|
107
|
+
export const flatTreeTextNodes = (root) => {
|
|
108
|
+
const nodes = [];
|
|
109
|
+
walkFlatTree(root, (node) => {
|
|
110
|
+
if (node.nodeType === TEXT_NODE)
|
|
111
|
+
nodes.push(node);
|
|
112
|
+
return undefined;
|
|
113
|
+
});
|
|
114
|
+
return nodes;
|
|
115
|
+
};
|
|
116
|
+
/** `textContent` over the flat tree: shadow content in, skipped elements out. */
|
|
117
|
+
export const flatTextContent = (root) => flatTreeTextNodes(root)
|
|
118
|
+
.map((node) => node.data)
|
|
119
|
+
.join("");
|
|
120
|
+
/** Elements under `root` matching `selector`, in rendering order. */
|
|
121
|
+
export const flatQuerySelectorAll = (root, selector) => {
|
|
122
|
+
const matches = [];
|
|
123
|
+
walkFlatTree(root, (node) => {
|
|
124
|
+
if (node.nodeType === ELEMENT_NODE && node.matches(selector)) {
|
|
125
|
+
matches.push(node);
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
});
|
|
129
|
+
return matches;
|
|
130
|
+
};
|
|
131
|
+
export const flatQuerySelector = (root, selector) => {
|
|
132
|
+
let match = null;
|
|
133
|
+
walkFlatTree(root, (node) => {
|
|
134
|
+
if (node.nodeType === ELEMENT_NODE && node.matches(selector)) {
|
|
135
|
+
match = node;
|
|
136
|
+
return "stop";
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
});
|
|
140
|
+
return match;
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* `node`'s parent in the flat tree: the slot rendering it, else its parent, with
|
|
144
|
+
* a shadow root standing for its host.
|
|
145
|
+
*/
|
|
146
|
+
const flatTreeParentNode = (node) => {
|
|
147
|
+
const slot = node.assignedSlot;
|
|
148
|
+
if (slot)
|
|
149
|
+
return slot;
|
|
150
|
+
const parent = node.parentNode;
|
|
151
|
+
if (isShadowRootNode(parent))
|
|
152
|
+
return parent.host;
|
|
153
|
+
return parent;
|
|
154
|
+
};
|
|
155
|
+
export const flatTreeParentElement = (node) => {
|
|
156
|
+
let parent = flatTreeParentNode(node);
|
|
157
|
+
while (parent && parent.nodeType !== ELEMENT_NODE) {
|
|
158
|
+
parent = flatTreeParentNode(parent);
|
|
159
|
+
}
|
|
160
|
+
return parent;
|
|
161
|
+
};
|
|
162
|
+
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
163
|
+
export const flatTreeClosest = (node, selector) => {
|
|
164
|
+
let current = node.nodeType === ELEMENT_NODE
|
|
165
|
+
? node
|
|
166
|
+
: flatTreeParentElement(node);
|
|
167
|
+
while (current) {
|
|
168
|
+
if (current.matches(selector))
|
|
169
|
+
return current;
|
|
170
|
+
current = flatTreeParentElement(current);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* `node`'s parent element, continuing from a shadow root to its host. This is the
|
|
176
|
+
* chain inheritance follows (`lang`, regions an author marks up), and for slotted
|
|
177
|
+
* content it stays in the light tree the author wrote.
|
|
178
|
+
*/
|
|
179
|
+
export const composedParentElement = (node) => {
|
|
180
|
+
if (node.parentElement)
|
|
181
|
+
return node.parentElement;
|
|
182
|
+
const parent = node.parentNode;
|
|
183
|
+
return isShadowRootNode(parent) ? parent.host : null;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* The nearest element at or above `node` on the composed chain that matches
|
|
187
|
+
* `selector`, or null. With `boundary`, the climb ends after testing it.
|
|
188
|
+
*/
|
|
189
|
+
export const composedClosest = (node, selector, boundary) => {
|
|
190
|
+
let current = node.nodeType === ELEMENT_NODE
|
|
191
|
+
? node
|
|
192
|
+
: composedParentElement(node);
|
|
193
|
+
while (current) {
|
|
194
|
+
if (current.matches(selector))
|
|
195
|
+
return current;
|
|
196
|
+
if (boundary && current === boundary)
|
|
197
|
+
return null;
|
|
198
|
+
current = composedParentElement(current);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
/** `contains` across shadow boundaries: whether `node` is `container` or below it. */
|
|
203
|
+
export const composedContains = (container, node) => {
|
|
204
|
+
if (!container || !node)
|
|
205
|
+
return false;
|
|
206
|
+
let current = node;
|
|
207
|
+
while (current) {
|
|
208
|
+
if (current === container || container.contains(current))
|
|
209
|
+
return true;
|
|
210
|
+
const root = current.getRootNode?.();
|
|
211
|
+
current = isShadowRootNode(root) ? root.host : null;
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* `node`, or the shadow host above it, that lies in the tree rooted at
|
|
217
|
+
* `treeRoot`; null when `node` is not inside that tree at any depth.
|
|
218
|
+
*/
|
|
219
|
+
export const retargetToTree = (node, treeRoot) => {
|
|
220
|
+
let current = node;
|
|
221
|
+
while (current) {
|
|
222
|
+
const root = current.getRootNode?.();
|
|
223
|
+
if (root === treeRoot)
|
|
224
|
+
return current;
|
|
225
|
+
current = isShadowRootNode(root) ? root.host : null;
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Whether the character at `offset` in `node` lies inside `range`. A live range
|
|
231
|
+
* holds nodes of one tree only, so a node in a shadow tree below it counts as
|
|
232
|
+
* inside when the range holds that tree's host whole.
|
|
233
|
+
*/
|
|
234
|
+
export const rangeHoldsTextPosition = (range, node, offset) => {
|
|
235
|
+
try {
|
|
236
|
+
const treeRoot = range.startContainer.getRootNode?.();
|
|
237
|
+
if (!treeRoot || node.getRootNode?.() === treeRoot) {
|
|
238
|
+
return (range.comparePoint(node, offset) === 0 &&
|
|
239
|
+
range.comparePoint(node, Math.min(offset + 1, node.length)) === 0);
|
|
240
|
+
}
|
|
241
|
+
return rangeHoldsNodeWhole(range, retargetToTree(node, treeRoot));
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
/** Whether `range` intersects `node`, counting a shadow tree below the range as {@link rangeHoldsTextPosition} does. */
|
|
248
|
+
export const rangeIntersectsComposedNode = (range, node) => {
|
|
249
|
+
try {
|
|
250
|
+
const treeRoot = range.startContainer.getRootNode?.();
|
|
251
|
+
if (!treeRoot || node.getRootNode?.() === treeRoot) {
|
|
252
|
+
return range.intersectsNode(node);
|
|
253
|
+
}
|
|
254
|
+
return rangeHoldsNodeWhole(range, retargetToTree(node, treeRoot));
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const rangeHoldsNodeWhole = (range, node) => {
|
|
261
|
+
const parent = node?.parentNode;
|
|
262
|
+
if (!node || !parent)
|
|
263
|
+
return false;
|
|
264
|
+
const index = Array.prototype.indexOf.call(parent.childNodes, node);
|
|
265
|
+
if (index < 0)
|
|
266
|
+
return false;
|
|
267
|
+
return (range.comparePoint(parent, index) === 0 &&
|
|
268
|
+
range.comparePoint(parent, index + 1) === 0);
|
|
269
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MathAwareSpeechChunk } from "../math-aware-text-processing.js";
|
|
2
|
-
import { type ResolvedMathSpeech, type SREMathSpeechOptions } from "../math-speech.js";
|
|
2
|
+
import { type ResolveMathSpeechOptions, type ResolvedMathSpeech, type SREMathSpeechOptions } from "../math-speech.js";
|
|
3
3
|
import type { AssembledSpeech } from "./types.js";
|
|
4
4
|
type MathChunk = Extract<MathAwareSpeechChunk, {
|
|
5
5
|
type: "math";
|
|
@@ -13,6 +13,8 @@ export type MathSpeechResolver = (chunk: MathChunk, options: {
|
|
|
13
13
|
language?: string;
|
|
14
14
|
produceSsml?: boolean;
|
|
15
15
|
mathSpeech?: SREMathSpeechOptions;
|
|
16
|
+
/** The read's one engine load, shared by its equations. */
|
|
17
|
+
loadSre?: ResolveMathSpeechOptions["loadSre"];
|
|
16
18
|
}) => Promise<ResolvedMathSpeech>;
|
|
17
19
|
/**
|
|
18
20
|
* Assemble an anchor-free speech plan from already-collected math-aware chunks.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveMathSpeechFromChunks, } from "../math-speech.js";
|
|
1
|
+
import { createReadSreLoad, resolveMathSpeechFromChunks, } from "../math-speech.js";
|
|
2
2
|
import { normalizeTextForSpeech } from "../text-processing.js";
|
|
3
3
|
import { segmentSentences as segmentTextToSentences } from "../text-segmentation.js";
|
|
4
4
|
const defaultMathSpeechResolver = (chunk, options) => resolveMathSpeechFromChunks([chunk], options);
|
|
@@ -37,6 +37,7 @@ const shouldPreserveProseChunk = (chunk) => {
|
|
|
37
37
|
*/
|
|
38
38
|
export const assembleGeneratedSpeech = async (args) => {
|
|
39
39
|
const resolveMathSpeech = args.resolveMathSpeech ?? defaultMathSpeechResolver;
|
|
40
|
+
const loadSre = createReadSreLoad(args.mathSpeech);
|
|
40
41
|
const { visibleText } = args;
|
|
41
42
|
const segments = [];
|
|
42
43
|
// Cursor-based span tracking:
|
|
@@ -84,6 +85,7 @@ export const assembleGeneratedSpeech = async (args) => {
|
|
|
84
85
|
language: args.language,
|
|
85
86
|
produceSsml: args.produceSsml,
|
|
86
87
|
mathSpeech: args.mathSpeech,
|
|
88
|
+
loadSre,
|
|
87
89
|
});
|
|
88
90
|
const usedFallback = !generated.speechText;
|
|
89
91
|
const spokenText = generated.speechText || chunk.fallbackText;
|
|
@@ -16,6 +16,8 @@ export interface BuildGeneratedSpeechResult {
|
|
|
16
16
|
*/
|
|
17
17
|
export declare const buildGeneratedSpeechFromRoot: (args: {
|
|
18
18
|
contentRoot: Element;
|
|
19
|
+
/** Limit the plan to the part of `contentRoot` this range selects. */
|
|
20
|
+
range?: Range;
|
|
19
21
|
language?: string;
|
|
20
22
|
mathSpeech?: SREMathSpeechOptions;
|
|
21
23
|
textProcessingOptions?: TextProcessingOptions;
|
|
@@ -17,7 +17,7 @@ const sliceVisibleMap = (map, start, end) => {
|
|
|
17
17
|
* entry point that touches the DOM; assembly and serialization stay pure.
|
|
18
18
|
*/
|
|
19
19
|
export const buildGeneratedSpeechFromRoot = async (args) => {
|
|
20
|
-
const extracted = collectMathAwareTextAndMap(args.contentRoot, args.textProcessingOptions);
|
|
20
|
+
const extracted = collectMathAwareTextAndMap(args.contentRoot, args.textProcessingOptions, args.range);
|
|
21
21
|
const assembled = await assembleGeneratedSpeech({
|
|
22
22
|
chunks: extracted.chunks,
|
|
23
23
|
visibleText: extracted.visibleText,
|
|
@@ -3,8 +3,8 @@ import type { DomAnchor, GeneratedSpeechChunk } from "./types.js";
|
|
|
3
3
|
export interface PlanToChunksOptions {
|
|
4
4
|
/**
|
|
5
5
|
* Playback format:
|
|
6
|
-
* - "plain" (default): plain spoken text for every chunk (
|
|
7
|
-
*
|
|
6
|
+
* - "plain" (default): plain spoken text for every chunk (the stable
|
|
7
|
+
* generated-speech behavior).
|
|
8
8
|
* - "ssml": math chunks with SRE SSML send that SSML to the provider (with
|
|
9
9
|
* a plain fallback attached); prose chunks always stay plain.
|
|
10
10
|
*/
|
|
@@ -7,7 +7,9 @@ export interface MathSpeechCacheOptions {
|
|
|
7
7
|
/**
|
|
8
8
|
* Build a memoized {@link MathSpeechResolver}.
|
|
9
9
|
*
|
|
10
|
-
* Caches only DOM-free spoken-text results
|
|
10
|
+
* Caches only DOM-free spoken-text results that SRE produced, so an equation
|
|
11
|
+
* read as its visible text after a failed engine load is resolved again on
|
|
12
|
+
* the next read. Keyed by
|
|
11
13
|
* `(SRE version, locale, output mode, SRE mathSpeech options, MathML source)`.
|
|
12
14
|
* Never caches DOM nodes or maps. Bounded by a simple LRU so long sessions do
|
|
13
15
|
* not grow unbounded.
|
|
@@ -35,7 +35,9 @@ const cacheKeyFor = (language, produceSsml, mathSpeech, mathml) => `${SRE_CACHE_
|
|
|
35
35
|
/**
|
|
36
36
|
* Build a memoized {@link MathSpeechResolver}.
|
|
37
37
|
*
|
|
38
|
-
* Caches only DOM-free spoken-text results
|
|
38
|
+
* Caches only DOM-free spoken-text results that SRE produced, so an equation
|
|
39
|
+
* read as its visible text after a failed engine load is resolved again on
|
|
40
|
+
* the next read. Keyed by
|
|
39
41
|
* `(SRE version, locale, output mode, SRE mathSpeech options, MathML source)`.
|
|
40
42
|
* Never caches DOM nodes or maps. Bounded by a simple LRU so long sessions do
|
|
41
43
|
* not grow unbounded.
|
|
@@ -66,6 +68,8 @@ export const createMemoizedMathSpeechResolver = (options = {}) => {
|
|
|
66
68
|
return cached;
|
|
67
69
|
}
|
|
68
70
|
const resolved = await resolve(chunk, { ...opts, mathSpeech });
|
|
71
|
+
if (resolved.usedFallback)
|
|
72
|
+
return resolved;
|
|
69
73
|
if (produceSsml && resolved.ssml && maxEntries > 1) {
|
|
70
74
|
setCached(cacheKeyFor(opts.language, false, mathSpeech, chunk.mathml), {
|
|
71
75
|
speechText: resolved.speechText,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated-speech plan model (PURE core).
|
|
3
3
|
*
|
|
4
4
|
* This module is the speech-composition core for items that contain math but
|
|
5
|
-
* ship without authored SSML / `accessibilityCatalogs
|
|
5
|
+
* ship without authored SSML / `accessibilityCatalogs`. It is
|
|
6
6
|
* deliberately free of any imports from `TTSService`, the highlight renderer,
|
|
7
7
|
* `HighlightCoordinator`, Svelte, or custom-element entrypoints
|
|
8
8
|
* (`scripts/check-speech-composition-purity.mjs` enforces this). DOM primitive
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Generated-speech plan model (PURE core).
|
|
3
3
|
*
|
|
4
4
|
* This module is the speech-composition core for items that contain math but
|
|
5
|
-
* ship without authored SSML / `accessibilityCatalogs
|
|
5
|
+
* ship without authored SSML / `accessibilityCatalogs`. It is
|
|
6
6
|
* deliberately free of any imports from `TTSService`, the highlight renderer,
|
|
7
7
|
* `HighlightCoordinator`, Svelte, or custom-element entrypoints
|
|
8
8
|
* (`scripts/check-speech-composition-purity.mjs` enforces this). DOM primitive
|
|
@@ -3,12 +3,19 @@ import { createHighlightDecision } from "./highlight-policy.js";
|
|
|
3
3
|
import { resolveMathBoundaryTarget } from "./math-semantic-resolver.js";
|
|
4
4
|
import { resolveProseBoundaryTarget } from "./prose-boundary-resolver.js";
|
|
5
5
|
import { classifyMathHighlightCapability, createRenderedMathTargetResolver, } from "./rendered-math-target-resolver.js";
|
|
6
|
+
import { composedContains, composedParentElement, rangeIntersectsComposedNode, } from "../flat-tree.js";
|
|
6
7
|
import { isNodeHiddenForTTS } from "../text-processing.js";
|
|
8
|
+
import { createRangesFromVisibleMap } from "./visible-map-range.js";
|
|
9
|
+
const regionRangesFor = (chunk) => {
|
|
10
|
+
const ranges = createRangesFromVisibleMap(chunk.visibleMap, 0, chunk.visibleText.length);
|
|
11
|
+
return ranges.length > 1 ? ranges : undefined;
|
|
12
|
+
};
|
|
7
13
|
const regionTargetFor = (chunk) => chunk.regionRange
|
|
8
14
|
? {
|
|
9
15
|
type: "range",
|
|
10
16
|
quality: "region",
|
|
11
17
|
range: chunk.regionRange,
|
|
18
|
+
ranges: regionRangesFor(chunk),
|
|
12
19
|
}
|
|
13
20
|
: chunk.regionElement
|
|
14
21
|
? {
|
|
@@ -29,12 +36,13 @@ const mathCandidatesForTarget = (target, chunk) => {
|
|
|
29
36
|
return chunk.mathAlignments;
|
|
30
37
|
return chunk.mathAlignments.filter(({ element }) => {
|
|
31
38
|
try {
|
|
32
|
-
if (target.type === "range")
|
|
33
|
-
return target.range.
|
|
39
|
+
if (target.type === "range") {
|
|
40
|
+
return (target.ranges ?? [target.range]).some((range) => rangeIntersectsComposedNode(range, element));
|
|
41
|
+
}
|
|
34
42
|
if (target.type === "element") {
|
|
35
|
-
return target.element
|
|
43
|
+
return composedContains(target.element, element);
|
|
36
44
|
}
|
|
37
|
-
return target.node
|
|
45
|
+
return composedContains(composedParentElement(target.node), element);
|
|
38
46
|
}
|
|
39
47
|
catch {
|
|
40
48
|
return false;
|
|
@@ -59,11 +67,10 @@ const TEXT_NODE = 3;
|
|
|
59
67
|
// the last token — painting it would be a likely false positive, and "false
|
|
60
68
|
// positives are worse than coarse highlighting." A single-text-node prose range
|
|
61
69
|
// is kept: it underlines one token cleanly and usefully fills in any token the
|
|
62
|
-
// math resolver missed.
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
const wouldEscalateInsideTokenMath = (planned, target, candidates) => {
|
|
70
|
+
// math resolver missed. The word layer never escalates a range to the enclosing
|
|
71
|
+
// <math>/<mjx-container> (HighlightCoordinator WORD_FALLBACK_SELECTOR), so this
|
|
72
|
+
// is a precision policy.
|
|
73
|
+
const isMultiNodeRangeInsideTokenMath = (planned, target, candidates) => {
|
|
67
74
|
if (!target || target.type !== "range")
|
|
68
75
|
return false;
|
|
69
76
|
const { startContainer, endContainer } = target.range;
|
|
@@ -120,8 +127,7 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
120
127
|
const planned = plannedChunksById.get(chunkId);
|
|
121
128
|
if (!planned)
|
|
122
129
|
return missingChunkDecision();
|
|
123
|
-
const
|
|
124
|
-
const soleElement = alignments.length === 1 ? alignments[0].element : null;
|
|
130
|
+
const soleElement = soleEquationElement(planned.chunk.mathAlignments);
|
|
125
131
|
const capability = soleElement
|
|
126
132
|
? planned.mathCapabilityByElement.get(soleElement)
|
|
127
133
|
: null;
|
|
@@ -156,9 +162,9 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
156
162
|
const normalized = normalizeBoundaryEvent(chunk, event);
|
|
157
163
|
const proseTarget = resolveProseBoundaryTarget(chunk, normalized);
|
|
158
164
|
const mathCandidates = mathCandidatesForTarget(proseTarget, chunk);
|
|
159
|
-
const resolvedMathTarget =
|
|
165
|
+
const resolvedMathTarget = !proseTarget || mathCandidates.length > 0
|
|
160
166
|
? resolveMathBoundaryTarget(chunk, normalized, event, mathCandidates, planned.renderedMathTargetResolver)
|
|
161
|
-
: null
|
|
167
|
+
: null;
|
|
162
168
|
const mathTarget = resolvedMathTarget?.quality === "semantic-token"
|
|
163
169
|
? resolvedMathTarget
|
|
164
170
|
: (renderHiddenProseTarget(planned, proseTarget) ??
|
|
@@ -187,15 +193,15 @@ export const createTTSHighlightPlan = (args) => {
|
|
|
187
193
|
}
|
|
188
194
|
// (2) A spoken word that maps to visible prose. The prose anchor is
|
|
189
195
|
// precise; a coarse math expression fallback must never override it.
|
|
190
|
-
// Exception: a multi-node prose range inside a token-mode equation
|
|
191
|
-
//
|
|
196
|
+
// Exception: a multi-node prose range inside a token-mode equation is
|
|
197
|
+
// an untrustworthy glyph-level match, so it is rejected here and
|
|
192
198
|
// falls through to the token hold below. When per-token math
|
|
193
199
|
// highlighting is disabled, any prose range that lands inside a
|
|
194
200
|
// formula is likewise rejected so the formula stays a single block.
|
|
195
201
|
if (proseTarget &&
|
|
196
202
|
!isHiddenRange(proseTarget) &&
|
|
197
203
|
(mathTokenHighlighting
|
|
198
|
-
? !
|
|
204
|
+
? !isMultiNodeRangeInsideTokenMath(planned, proseTarget, mathCandidates)
|
|
199
205
|
: mathCandidates.length === 0)) {
|
|
200
206
|
return createHighlightDecision({
|
|
201
207
|
semanticTarget: proseTarget,
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { resolveVisibleSpanForBoundary } from "../catalog-span-alignment.js";
|
|
2
|
-
import {
|
|
2
|
+
import { createRangesFromVisibleMap } from "./visible-map-range.js";
|
|
3
3
|
export const resolveProseBoundaryTarget = (chunk, boundary) => {
|
|
4
4
|
if (!chunk.catalogAlignment || boundary.chunkSpokenStart === null)
|
|
5
5
|
return null;
|
|
6
6
|
const visibleSpan = resolveVisibleSpanForBoundary(chunk.catalogAlignment, boundary.chunkSpokenStart);
|
|
7
7
|
if (!visibleSpan)
|
|
8
8
|
return null;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// One range per tree, as the uncataloged word path builds them.
|
|
10
|
+
const ranges = createRangesFromVisibleMap(chunk.visibleMap, visibleSpan.start, visibleSpan.end);
|
|
11
|
+
if (ranges.length === 0)
|
|
11
12
|
return null;
|
|
12
13
|
return {
|
|
13
14
|
type: "range",
|
|
14
15
|
quality: chunk.catalogAlignment.playbackMode === "exact-word"
|
|
15
16
|
? "exact-word"
|
|
16
17
|
: "semantic-token",
|
|
17
|
-
range,
|
|
18
|
+
range: ranges[0],
|
|
19
|
+
ranges: ranges.length > 1 ? ranges : undefined,
|
|
18
20
|
};
|
|
19
21
|
};
|