@pie-players/pie-assessment-toolkit 0.3.74 → 0.3.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/README.md +349 -313
  2. package/dist/attempt/TestSession.d.ts +1 -20
  3. package/dist/attempt/TestSession.js +8 -99
  4. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  5. package/dist/components/ItemScope.custom-element.js +1 -0
  6. package/dist/components/ItemToolBar.custom-element.js +1 -1
  7. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  8. package/dist/components/SectionToolBar.custom-element.js +1 -1
  9. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  10. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
  11. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  13. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  14. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  15. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  16. package/dist/components/item-scope-element.d.ts +2 -0
  17. package/dist/components/item-scope-element.js +2 -0
  18. package/dist/context/assessment-toolkit-context.d.ts +27 -6
  19. package/dist/index.d.ts +22 -47
  20. package/dist/index.js +12 -36
  21. package/dist/policy/core/PolicySource.d.ts +1 -1
  22. package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
  23. package/dist/policy/core/ToolPolicyEngine.js +136 -46
  24. package/dist/policy/core/compose-decision.d.ts +28 -7
  25. package/dist/policy/core/compose-decision.js +75 -54
  26. package/dist/policy/core/decision-types.d.ts +65 -34
  27. package/dist/policy/core/feature-decision.d.ts +16 -9
  28. package/dist/policy/core/feature-decision.js +14 -8
  29. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  30. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  31. package/dist/policy/core/provenance.d.ts +9 -4
  32. package/dist/policy/core/provenance.js +1 -1
  33. package/dist/policy/engine.d.ts +7 -23
  34. package/dist/policy/engine.js +5 -20
  35. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  36. package/dist/policy/sources/PnpPolicySource.js +117 -118
  37. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  38. package/dist/runtime/SectionControllerBinding.js +450 -0
  39. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  40. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  41. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  42. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  43. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  44. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  45. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  46. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  47. package/dist/runtime/catalog-context.d.ts +19 -0
  48. package/dist/runtime/catalog-context.js +28 -0
  49. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  50. package/dist/runtime/composition-emit-scheduler.js +9 -15
  51. package/dist/runtime/content-language.d.ts +32 -0
  52. package/dist/runtime/content-language.js +46 -0
  53. package/dist/runtime/content-region.d.ts +17 -0
  54. package/dist/runtime/content-region.js +29 -0
  55. package/dist/runtime/content-registration.d.ts +13 -0
  56. package/dist/runtime/content-registration.js +25 -0
  57. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  58. package/dist/runtime/core/SectionEngineCore.js +2 -2
  59. package/dist/runtime/core/cohort.d.ts +1 -1
  60. package/dist/runtime/core/cohort.js +1 -1
  61. package/dist/runtime/core/engine-input.d.ts +18 -30
  62. package/dist/runtime/core/engine-input.js +11 -12
  63. package/dist/runtime/core/engine-output.d.ts +16 -40
  64. package/dist/runtime/core/engine-output.js +9 -21
  65. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  66. package/dist/runtime/core/engine-readiness.js +8 -41
  67. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  68. package/dist/runtime/core/engine-resolver.js +18 -43
  69. package/dist/runtime/core/engine-state.d.ts +28 -36
  70. package/dist/runtime/core/engine-state.js +18 -21
  71. package/dist/runtime/core/engine-transition.d.ts +14 -19
  72. package/dist/runtime/core/engine-transition.js +71 -51
  73. package/dist/runtime/engine.d.ts +16 -19
  74. package/dist/runtime/engine.js +12 -19
  75. package/dist/runtime/page-warnings.d.ts +13 -0
  76. package/dist/runtime/page-warnings.js +22 -0
  77. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  78. package/dist/runtime/policy-input-forwarding.js +25 -0
  79. package/dist/runtime/registration-events.d.ts +11 -0
  80. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  81. package/dist/runtime/runtime-event-claim.js +15 -0
  82. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  83. package/dist/runtime/shell-event-bridge.js +127 -0
  84. package/dist/runtime/shell-registration.d.ts +73 -0
  85. package/dist/runtime/shell-registration.js +95 -0
  86. package/dist/runtime/shell-scope.d.ts +68 -0
  87. package/dist/runtime/shell-scope.js +188 -0
  88. package/dist/runtime/tool-host-contract.d.ts +8 -10
  89. package/dist/runtime/tool-host-contract.js +11 -11
  90. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  91. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  92. package/dist/services/ElementToolStateStore.d.ts +14 -10
  93. package/dist/services/ElementToolStateStore.js +37 -13
  94. package/dist/services/HighlightCoordinator.d.ts +21 -21
  95. package/dist/services/HighlightCoordinator.js +431 -325
  96. package/dist/services/RangeSerializer.d.ts +10 -1
  97. package/dist/services/RangeSerializer.js +87 -14
  98. package/dist/services/TTSService.d.ts +117 -70
  99. package/dist/services/TTSService.js +668 -578
  100. package/dist/services/ToolCoordinator.d.ts +43 -60
  101. package/dist/services/ToolCoordinator.js +164 -206
  102. package/dist/services/ToolRegistry.d.ts +74 -57
  103. package/dist/services/ToolRegistry.js +104 -114
  104. package/dist/services/ToolkitCoordinator.d.ts +183 -189
  105. package/dist/services/ToolkitCoordinator.js +477 -431
  106. package/dist/services/audio-handoff.d.ts +6 -7
  107. package/dist/services/audio-handoff.js +20 -14
  108. package/dist/services/catalog-owner.d.ts +0 -1
  109. package/dist/services/framework-error-bus.d.ts +3 -6
  110. package/dist/services/framework-error-bus.js +3 -6
  111. package/dist/services/framework-error.d.ts +22 -7
  112. package/dist/services/framework-error.js +9 -7
  113. package/dist/services/interfaces.d.ts +158 -129
  114. package/dist/services/section-session-assignment.d.ts +21 -0
  115. package/dist/services/section-session-assignment.js +82 -0
  116. package/dist/services/spoken-audio-cards.d.ts +0 -2
  117. package/dist/services/spoken-audio-cards.js +0 -2
  118. package/dist/services/tool-config-defaults.d.ts +3 -3
  119. package/dist/services/tool-config-defaults.js +3 -3
  120. package/dist/services/tool-config-validation.d.ts +4 -5
  121. package/dist/services/tool-config-validation.js +36 -61
  122. package/dist/services/tool-context.d.ts +0 -8
  123. package/dist/services/tool-context.js +0 -12
  124. package/dist/services/tool-icons.d.ts +4 -3
  125. package/dist/services/tool-icons.js +4 -3
  126. package/dist/services/tool-instance-id.d.ts +0 -1
  127. package/dist/services/tool-instance-id.js +0 -6
  128. package/dist/services/tool-providers/TTSToolProvider.d.ts +18 -19
  129. package/dist/services/tool-providers/TTSToolProvider.js +37 -34
  130. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  131. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
  132. package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
  133. package/dist/services/tool-request.d.ts +27 -12
  134. package/dist/services/tool-request.js +24 -14
  135. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  136. package/dist/services/toolbar-decided-tools.js +28 -0
  137. package/dist/services/tools-config-normalizer.d.ts +17 -22
  138. package/dist/services/tools-config-normalizer.js +2 -10
  139. package/dist/services/tts/browser-provider.d.ts +38 -2
  140. package/dist/services/tts/browser-provider.js +276 -140
  141. package/dist/services/tts/flat-tree.d.ts +71 -0
  142. package/dist/services/tts/flat-tree.js +269 -0
  143. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  144. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  145. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  146. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  147. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  148. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  149. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +12 -4
  150. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  151. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  152. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  153. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  154. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  155. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  156. package/dist/services/tts/math-aware-text-processing.js +32 -18
  157. package/dist/services/tts/math-control-names.d.ts +21 -0
  158. package/dist/services/tts/math-control-names.js +142 -0
  159. package/dist/services/tts/math-speech.d.ts +11 -0
  160. package/dist/services/tts/math-speech.js +51 -18
  161. package/dist/services/tts/provider-options.d.ts +41 -0
  162. package/dist/services/tts/provider-options.js +1 -0
  163. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  164. package/dist/services/tts/ssml/spoken-text.js +29 -0
  165. package/dist/services/tts/start-failure.d.ts +11 -0
  166. package/dist/services/tts/start-failure.js +19 -0
  167. package/dist/services/tts/text-processing.d.ts +9 -0
  168. package/dist/services/tts/text-processing.js +46 -33
  169. package/dist/services/tts/text-segmentation.d.ts +7 -0
  170. package/dist/services/tts/text-segmentation.js +47 -0
  171. package/dist/services/tts-runtime-config.d.ts +25 -17
  172. package/dist/services/tts-runtime-config.js +16 -95
  173. package/dist/tools/content-capability-resolution.d.ts +7 -9
  174. package/dist/tools/content-capability-resolution.js +8 -45
  175. package/dist/tools/registration.d.ts +58 -0
  176. package/dist/tools/registration.js +54 -0
  177. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  178. package/dist/tools/tool-surface-host.d.ts +6 -10
  179. package/dist/tools/tool-surface-host.js +44 -42
  180. package/dist/tools/tool-tag-map.d.ts +5 -15
  181. package/dist/tools/tool-tag-map.js +6 -20
  182. package/dist/utils/structural-equality.d.ts +11 -0
  183. package/dist/utils/structural-equality.js +47 -0
  184. package/package.json +13 -29
  185. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  186. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  187. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  188. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  189. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  190. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  191. package/dist/context/runtime-context-consumer.d.ts +0 -13
  192. package/dist/context/runtime-context-consumer.js +0 -18
  193. package/dist/item-loader.d.ts +0 -44
  194. package/dist/item-loader.js +0 -61
  195. package/dist/policy/core/engine-context.d.ts +0 -29
  196. package/dist/policy/core/engine-context.js +0 -27
  197. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  198. package/dist/policy/core/policy-source-tag.js +0 -28
  199. package/dist/policy/internal.d.ts +0 -25
  200. package/dist/policy/internal.js +0 -25
  201. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  202. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  203. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  204. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  205. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  206. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  207. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  208. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  209. package/dist/runtime/engine-context.d.ts +0 -39
  210. package/dist/runtime/engine-context.js +0 -31
  211. package/dist/runtime/internal.d.ts +0 -43
  212. package/dist/runtime/internal.js +0 -45
  213. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  214. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  215. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  216. package/dist/runtime/stage-emit-gate.js +0 -82
  217. package/dist/services/I18nService.d.ts +0 -47
  218. package/dist/services/I18nService.js +0 -84
  219. package/dist/services/ThemeProvider.d.ts +0 -95
  220. package/dist/services/ThemeProvider.js +0 -334
  221. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  222. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  223. package/dist/services/pnp-standard-features.d.ts +0 -238
  224. package/dist/services/pnp-standard-features.js +0 -338
  225. package/dist/tools/client.d.ts +0 -10
  226. package/dist/tools/client.js +0 -10
  227. package/dist/tools/internal.d.ts +0 -44
  228. package/dist/tools/internal.js +0 -39
@@ -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
+ export 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
+ export 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,
@@ -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 keyed by
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 keyed by
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,
@@ -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.intersectsNode(element);
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 === element || target.element.contains(element);
43
+ return composedContains(target.element, element);
36
44
  }
37
- return target.node.parentElement?.contains(element) || false;
45
+ return composedContains(composedParentElement(target.node), element);
38
46
  }
39
47
  catch {
40
48
  return false;
@@ -1,19 +1,21 @@
1
1
  import { resolveVisibleSpanForBoundary } from "../catalog-span-alignment.js";
2
- import { createRangeFromVisibleMap } from "./visible-map-range.js";
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
- const range = createRangeFromVisibleMap(chunk.visibleMap, visibleSpan.start, visibleSpan.end);
10
- if (!range)
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
  };
@@ -1,3 +1,4 @@
1
+ import { composedClosest, composedContains } from "../flat-tree.js";
1
2
  const READABLE_REGION_SELECTOR = [
2
3
  "[data-pie-tts-region]",
3
4
  "[data-catalog-region]",
@@ -10,8 +11,8 @@ const READABLE_REGION_SELECTOR = [
10
11
  "div",
11
12
  ].join(",");
12
13
  export const resolveReadableRegion = (sourceElement, contentRoot) => {
13
- const region = sourceElement.closest(READABLE_REGION_SELECTOR);
14
- if (region && contentRoot.contains(region)) {
14
+ const region = composedClosest(sourceElement, READABLE_REGION_SELECTOR);
15
+ if (region && composedContains(contentRoot, region)) {
15
16
  return region;
16
17
  }
17
18
  return sourceElement;
@@ -44,6 +44,11 @@ export type RenderableHighlightTarget = {
44
44
  type: "range";
45
45
  quality: "exact-word" | "semantic-token" | "expression" | "region";
46
46
  range: Range;
47
+ /**
48
+ * Every range to paint when the target crosses shadow boundaries, one per
49
+ * tree, `range` among them; absent when `range` covers the target.
50
+ */
51
+ ranges?: Range[];
47
52
  } | {
48
53
  type: "element";
49
54
  quality: "semantic-token" | "expression" | "region";
@@ -1,2 +1,14 @@
1
1
  import type { NormalizedTextMap } from "../text-processing.js";
2
+ /**
3
+ * The range over visible characters `start` to `end` (exclusive). A live range
4
+ * holds one tree, so when the span runs into a shadow tree, or out of one, the
5
+ * range ends at the last character in the tree it starts in.
6
+ */
2
7
  export declare const createRangeFromVisibleMap: (visibleMap: NormalizedTextMap | undefined, start: number, end: number) => Range | null;
8
+ /**
9
+ * The ranges over visible characters `start` to `end` (exclusive), one per run of
10
+ * characters in the same tree, so a span crossing shadow boundaries paints in
11
+ * each tree it covers. A span within one tree gives exactly
12
+ * {@link createRangeFromVisibleMap}'s range.
13
+ */
14
+ export declare const createRangesFromVisibleMap: (visibleMap: NormalizedTextMap | undefined, start: number, end: number) => Range[];
@@ -1,3 +1,20 @@
1
+ const treeOf = (mapping) => mapping.node.getRootNode?.() ?? mapping.node;
2
+ const rangeBetween = (start, end) => {
3
+ try {
4
+ const range = document.createRange();
5
+ range.setStart(start.node, start.offset);
6
+ range.setEnd(end.node, end.offset + 1);
7
+ return range;
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ };
13
+ /**
14
+ * The range over visible characters `start` to `end` (exclusive). A live range
15
+ * holds one tree, so when the span runs into a shadow tree, or out of one, the
16
+ * range ends at the last character in the tree it starts in.
17
+ */
1
18
  export const createRangeFromVisibleMap = (visibleMap, start, end) => {
2
19
  if (!visibleMap || typeof document === "undefined")
3
20
  return null;
@@ -9,13 +26,62 @@ export const createRangeFromVisibleMap = (visibleMap, start, end) => {
9
26
  return null;
10
27
  if (!endMapping)
11
28
  endMapping = startMapping;
12
- try {
13
- const range = document.createRange();
14
- range.setStart(startMapping.node, startMapping.offset);
15
- range.setEnd(endMapping.node, endMapping.offset + 1);
16
- return range;
29
+ if (treeOf(endMapping) !== treeOf(startMapping)) {
30
+ const tree = treeOf(startMapping);
31
+ endMapping = startMapping;
32
+ for (let index = start + 1; index < end; index++) {
33
+ const mapping = visibleMap.get(index);
34
+ if (mapping && treeOf(mapping) === tree)
35
+ endMapping = mapping;
36
+ }
17
37
  }
18
- catch {
19
- return null;
38
+ return rangeBetween(startMapping, endMapping);
39
+ };
40
+ /**
41
+ * The ranges over visible characters `start` to `end` (exclusive), one per run of
42
+ * characters in the same tree, so a span crossing shadow boundaries paints in
43
+ * each tree it covers. A span within one tree gives exactly
44
+ * {@link createRangeFromVisibleMap}'s range.
45
+ */
46
+ export const createRangesFromVisibleMap = (visibleMap, start, end) => {
47
+ if (!visibleMap || typeof document === "undefined" || end <= start)
48
+ return [];
49
+ const startMapping = visibleMap.get(start);
50
+ if (!startMapping)
51
+ return [];
52
+ const tree = treeOf(startMapping);
53
+ let crossesTrees = false;
54
+ for (let index = start + 1; index < end && !crossesTrees; index++) {
55
+ const mapping = visibleMap.get(index);
56
+ if (mapping && treeOf(mapping) !== tree)
57
+ crossesTrees = true;
58
+ }
59
+ if (!crossesTrees) {
60
+ const range = createRangeFromVisibleMap(visibleMap, start, end);
61
+ return range ? [range] : [];
62
+ }
63
+ const ranges = [];
64
+ let runStart = null;
65
+ let runEnd = null;
66
+ const closeRun = () => {
67
+ if (runStart && runEnd) {
68
+ const range = rangeBetween(runStart, runEnd);
69
+ if (range)
70
+ ranges.push(range);
71
+ }
72
+ runStart = null;
73
+ runEnd = null;
74
+ };
75
+ for (let index = start; index < end; index++) {
76
+ const mapping = visibleMap.get(index);
77
+ if (!mapping)
78
+ continue;
79
+ if (runStart && treeOf(runStart) !== treeOf(mapping))
80
+ closeRun();
81
+ if (!runStart)
82
+ runStart = mapping;
83
+ runEnd = mapping;
20
84
  }
85
+ closeRun();
86
+ return ranges;
21
87
  };
@@ -15,4 +15,8 @@ export interface MathAwareTextResult {
15
15
  chunks: MathAwareSpeechChunk[];
16
16
  containsMathMarkup: boolean;
17
17
  }
18
- export declare const collectMathAwareTextAndMap: (element: Element, options?: TextProcessingOptions) => MathAwareTextResult;
18
+ /**
19
+ * With `range`, only the part of `element` the range selects: text it selects
20
+ * and equations it holds whole.
21
+ */
22
+ export declare const collectMathAwareTextAndMap: (element: Element, options?: TextProcessingOptions, range?: Range) => MathAwareTextResult;