@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.
Files changed (257) hide show
  1. package/README.md +658 -714
  2. package/custom-elements.json +1315 -0
  3. package/dist/attempt/TestSession.d.ts +1 -21
  4. package/dist/attempt/TestSession.js +16 -102
  5. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  6. package/dist/components/ItemScope.custom-element.js +1 -0
  7. package/dist/components/ItemToolBar.custom-element.js +1 -1
  8. package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
  9. package/dist/components/SectionToolBar.custom-element.js +1 -1
  10. package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
  11. package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
  12. package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
  13. package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
  14. package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
  15. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  16. package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
  17. package/dist/components/item-scope-element.d.ts +2 -0
  18. package/dist/components/item-scope-element.js +2 -0
  19. package/dist/context/assessment-toolkit-context.d.ts +33 -6
  20. package/dist/index.d.ts +25 -50
  21. package/dist/index.js +15 -45
  22. package/dist/policy/core/PolicySource.d.ts +2 -2
  23. package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
  24. package/dist/policy/core/ToolPolicyEngine.js +203 -56
  25. package/dist/policy/core/compose-decision.d.ts +28 -7
  26. package/dist/policy/core/compose-decision.js +95 -74
  27. package/dist/policy/core/decision-types.d.ts +98 -55
  28. package/dist/policy/core/feature-decision.d.ts +28 -19
  29. package/dist/policy/core/feature-decision.js +15 -11
  30. package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
  31. package/dist/policy/core/pnp-policy-inputs.js +40 -88
  32. package/dist/policy/core/provenance.d.ts +10 -5
  33. package/dist/policy/core/provenance.js +1 -1
  34. package/dist/policy/core/tool-parameters.d.ts +11 -0
  35. package/dist/policy/core/tool-parameters.js +15 -0
  36. package/dist/policy/engine.d.ts +7 -23
  37. package/dist/policy/engine.js +5 -20
  38. package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
  39. package/dist/policy/sources/PnpPolicySource.js +133 -132
  40. package/dist/runtime/SectionControllerBinding.d.ts +283 -0
  41. package/dist/runtime/SectionControllerBinding.js +456 -0
  42. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  43. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  44. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  45. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  46. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  47. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  48. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
  49. package/dist/runtime/adapter/subscriber-fanout.js +2 -6
  50. package/dist/runtime/catalog-context.d.ts +19 -0
  51. package/dist/runtime/catalog-context.js +28 -0
  52. package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
  53. package/dist/runtime/composition-emit-scheduler.js +10 -16
  54. package/dist/runtime/content-language.d.ts +32 -0
  55. package/dist/runtime/content-language.js +46 -0
  56. package/dist/runtime/content-region.d.ts +13 -0
  57. package/dist/runtime/content-region.js +29 -0
  58. package/dist/runtime/content-registration.d.ts +13 -0
  59. package/dist/runtime/content-registration.js +25 -0
  60. package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
  61. package/dist/runtime/core/SectionEngineCore.js +2 -6
  62. package/dist/runtime/core/cohort.d.ts +1 -6
  63. package/dist/runtime/core/cohort.js +1 -10
  64. package/dist/runtime/core/engine-input.d.ts +18 -30
  65. package/dist/runtime/core/engine-input.js +11 -12
  66. package/dist/runtime/core/engine-output.d.ts +17 -40
  67. package/dist/runtime/core/engine-output.js +9 -21
  68. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  69. package/dist/runtime/core/engine-readiness.js +8 -41
  70. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  71. package/dist/runtime/core/engine-resolver.js +18 -43
  72. package/dist/runtime/core/engine-state.d.ts +28 -36
  73. package/dist/runtime/core/engine-state.js +18 -21
  74. package/dist/runtime/core/engine-transition.d.ts +14 -19
  75. package/dist/runtime/core/engine-transition.js +74 -53
  76. package/dist/runtime/engine.d.ts +16 -19
  77. package/dist/runtime/engine.js +12 -19
  78. package/dist/runtime/page-warnings.d.ts +13 -0
  79. package/dist/runtime/page-warnings.js +22 -0
  80. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  81. package/dist/runtime/policy-input-forwarding.js +25 -0
  82. package/dist/runtime/registration-events.d.ts +22 -14
  83. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  84. package/dist/runtime/runtime-event-claim.js +15 -0
  85. package/dist/runtime/runtime-id.d.ts +1 -1
  86. package/dist/runtime/runtime-id.js +1 -1
  87. package/dist/runtime/shell-event-bridge.d.ts +45 -0
  88. package/dist/runtime/shell-event-bridge.js +127 -0
  89. package/dist/runtime/shell-registration.d.ts +73 -0
  90. package/dist/runtime/shell-registration.js +95 -0
  91. package/dist/runtime/shell-scope.d.ts +68 -0
  92. package/dist/runtime/shell-scope.js +194 -0
  93. package/dist/runtime/tool-host-contract.d.ts +8 -14
  94. package/dist/runtime/tool-host-contract.js +12 -12
  95. package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
  96. package/dist/runtime/unclaimed-registration-watch.js +23 -0
  97. package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
  98. package/dist/services/AccessibilityCatalogResolver.js +29 -42
  99. package/dist/services/ElementToolStateStore.d.ts +42 -67
  100. package/dist/services/ElementToolStateStore.js +65 -71
  101. package/dist/services/HighlightCoordinator.d.ts +28 -40
  102. package/dist/services/HighlightCoordinator.js +459 -381
  103. package/dist/services/RangeSerializer.d.ts +11 -33
  104. package/dist/services/RangeSerializer.js +88 -59
  105. package/dist/services/SSMLExtractor.d.ts +8 -31
  106. package/dist/services/SSMLExtractor.js +18 -40
  107. package/dist/services/TTSService.d.ts +159 -119
  108. package/dist/services/TTSService.js +937 -822
  109. package/dist/services/ToolCoordinator.d.ts +42 -102
  110. package/dist/services/ToolCoordinator.js +178 -256
  111. package/dist/services/ToolRegistry.d.ts +152 -133
  112. package/dist/services/ToolRegistry.js +192 -178
  113. package/dist/services/ToolkitCoordinator.d.ts +233 -237
  114. package/dist/services/ToolkitCoordinator.js +615 -557
  115. package/dist/services/audio-handoff.d.ts +6 -7
  116. package/dist/services/audio-handoff.js +20 -14
  117. package/dist/services/catalog-owner.d.ts +0 -1
  118. package/dist/services/framework-error-bus.d.ts +3 -6
  119. package/dist/services/framework-error-bus.js +3 -6
  120. package/dist/services/framework-error.d.ts +51 -13
  121. package/dist/services/framework-error.js +19 -20
  122. package/dist/services/interfaces.d.ts +189 -184
  123. package/dist/services/section-controller-types.d.ts +7 -7
  124. package/dist/services/section-session-assignment.d.ts +21 -0
  125. package/dist/services/section-session-assignment.js +80 -0
  126. package/dist/services/spoken-audio-cards.d.ts +2 -6
  127. package/dist/services/spoken-audio-cards.js +2 -6
  128. package/dist/services/tool-config-defaults.d.ts +3 -3
  129. package/dist/services/tool-config-defaults.js +3 -3
  130. package/dist/services/tool-config-validation.d.ts +9 -12
  131. package/dist/services/tool-config-validation.js +47 -87
  132. package/dist/services/tool-context.d.ts +7 -31
  133. package/dist/services/tool-context.js +99 -65
  134. package/dist/services/tool-failure.d.ts +30 -0
  135. package/dist/services/tool-failure.js +11 -0
  136. package/dist/services/tool-icons.d.ts +5 -5
  137. package/dist/services/tool-icons.js +6 -5
  138. package/dist/services/tool-instance-id.d.ts +8 -7
  139. package/dist/services/tool-instance-id.js +12 -24
  140. package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
  141. package/dist/services/tool-providers/TTSToolProvider.js +32 -85
  142. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
  143. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
  144. package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
  145. package/dist/services/tool-request.d.ts +31 -12
  146. package/dist/services/tool-request.js +31 -16
  147. package/dist/services/toolbar-decided-tools.d.ts +16 -0
  148. package/dist/services/toolbar-decided-tools.js +29 -0
  149. package/dist/services/toolbar-items.js +2 -5
  150. package/dist/services/tools-config-normalizer.d.ts +33 -29
  151. package/dist/services/tools-config-normalizer.js +26 -28
  152. package/dist/services/tts/browser-provider.d.ts +39 -6
  153. package/dist/services/tts/browser-provider.js +280 -160
  154. package/dist/services/tts/catalog-span-alignment.js +14 -7
  155. package/dist/services/tts/flat-tree.d.ts +64 -0
  156. package/dist/services/tts/flat-tree.js +269 -0
  157. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  158. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  159. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  160. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  161. package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
  162. package/dist/services/tts/generated-speech/index.d.ts +1 -1
  163. package/dist/services/tts/generated-speech/index.js +1 -1
  164. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  165. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  166. package/dist/services/tts/generated-speech/types.d.ts +1 -1
  167. package/dist/services/tts/generated-speech/types.js +1 -1
  168. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
  169. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  170. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  171. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  172. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  173. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  174. package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +42 -47
  177. package/dist/services/tts/math-control-names.d.ts +21 -0
  178. package/dist/services/tts/math-control-names.js +142 -0
  179. package/dist/services/tts/math-speech.d.ts +12 -1
  180. package/dist/services/tts/math-speech.js +54 -18
  181. package/dist/services/tts/mathml-sanitization.d.ts +7 -0
  182. package/dist/services/tts/mathml-sanitization.js +9 -1
  183. package/dist/services/tts/provider-options.d.ts +52 -0
  184. package/dist/services/tts/provider-options.js +1 -0
  185. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  186. package/dist/services/tts/ssml/spoken-text.js +29 -0
  187. package/dist/services/tts/start-failure.d.ts +4 -0
  188. package/dist/services/tts/start-failure.js +19 -0
  189. package/dist/services/tts/text-processing.d.ts +9 -0
  190. package/dist/services/tts/text-processing.js +46 -33
  191. package/dist/services/tts/text-segmentation.d.ts +12 -0
  192. package/dist/services/tts/text-segmentation.js +60 -10
  193. package/dist/services/tts-runtime-config.d.ts +54 -26
  194. package/dist/services/tts-runtime-config.js +27 -112
  195. package/dist/tools/content-capability-resolution.d.ts +9 -11
  196. package/dist/tools/content-capability-resolution.js +8 -45
  197. package/dist/tools/registration.d.ts +59 -0
  198. package/dist/tools/registration.js +55 -0
  199. package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
  200. package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
  201. package/dist/tools/tool-surface-host.d.ts +6 -10
  202. package/dist/tools/tool-surface-host.js +45 -50
  203. package/dist/tools/tool-tag-map.d.ts +5 -15
  204. package/dist/tools/tool-tag-map.js +6 -20
  205. package/dist/utils/structural-equality.d.ts +11 -0
  206. package/dist/utils/structural-equality.js +47 -0
  207. package/package.json +15 -29
  208. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  210. package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
  211. package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
  212. package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
  213. package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
  214. package/dist/context/runtime-context-consumer.d.ts +0 -13
  215. package/dist/context/runtime-context-consumer.js +0 -18
  216. package/dist/item-loader.d.ts +0 -44
  217. package/dist/item-loader.js +0 -61
  218. package/dist/policy/core/engine-context.d.ts +0 -29
  219. package/dist/policy/core/engine-context.js +0 -27
  220. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  221. package/dist/policy/core/policy-source-tag.js +0 -28
  222. package/dist/policy/internal.d.ts +0 -25
  223. package/dist/policy/internal.js +0 -25
  224. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  225. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  226. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  227. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  228. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  229. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  230. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  231. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  232. package/dist/runtime/engine-context.d.ts +0 -39
  233. package/dist/runtime/engine-context.js +0 -31
  234. package/dist/runtime/internal.d.ts +0 -43
  235. package/dist/runtime/internal.js +0 -45
  236. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
  237. package/dist/runtime/section-runtime-engine-host-context.js +0 -57
  238. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  239. package/dist/runtime/stage-emit-gate.js +0 -82
  240. package/dist/services/ContextVariableStore.d.ts +0 -113
  241. package/dist/services/ContextVariableStore.js +0 -206
  242. package/dist/services/I18nService.d.ts +0 -47
  243. package/dist/services/I18nService.js +0 -84
  244. package/dist/services/ThemeProvider.d.ts +0 -95
  245. package/dist/services/ThemeProvider.js +0 -334
  246. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  247. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  248. package/dist/services/pnp-standard-features.d.ts +0 -238
  249. package/dist/services/pnp-standard-features.js +0 -338
  250. package/dist/services/tts-control-events.d.ts +0 -13
  251. package/dist/services/tts-control-events.js +0 -7
  252. package/dist/tools/client.d.ts +0 -10
  253. package/dist/tools/client.js +0 -10
  254. package/dist/tools/internal.d.ts +0 -44
  255. package/dist/tools/internal.js +0 -39
  256. package/dist/utils/logger.d.ts +0 -6
  257. package/dist/utils/logger.js +0 -10
@@ -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
  };
@@ -1,3 +1,7 @@
1
+ /**
2
+ * What the host knows about the content being highlighted. `scopeElement`
3
+ * falls back to the content element being read.
4
+ */
1
5
  export interface TTSHighlightContext {
2
6
  scopeElement?: HTMLElement | null;
3
7
  itemId?: string;
@@ -6,12 +10,20 @@ export interface TTSHighlightContext {
6
10
  contentKind?: string;
7
11
  regionPolicy?: string;
8
12
  }
13
+ /**
14
+ * Host remap of TTS highlight targets. A null or undefined result keeps the
15
+ * native targets, and so does a result outside `scopeElement` or a throw. For
16
+ * sentences, one out-of-scope or non-Range/Element entry keeps all native
17
+ * ranges.
18
+ */
9
19
  export interface TTSHighlightTargetResolver {
10
20
  resolveWordRange?(range: Range, context: TTSHighlightContext): Range | null | undefined;
11
21
  resolveSentenceRanges?(ranges: Range[], context: TTSHighlightContext): Array<Range | HTMLElement> | null | undefined;
12
22
  }
23
+ /** A resolver with the context it is called with. */
13
24
  export interface TTSHighlightTargetResolverRuntime {
14
25
  context: TTSHighlightContext;
15
26
  resolver?: TTSHighlightTargetResolver | null;
16
27
  }
28
+ /** Read at every highlight; a throw counts as no resolver. */
17
29
  export type TTSHighlightTargetResolverProvider = () => TTSHighlightTargetResolverRuntime | null | undefined;
@@ -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;
@@ -1,5 +1,6 @@
1
+ import { flatQuerySelector, flatTreeChildNodes, flatTreeParentElement, rangeIntersectsComposedNode, } from "./flat-tree.js";
1
2
  import { canonicalizeMathML } from "./mathml-sanitization.js";
2
- import { collectVisibleTextAndMap, isNodeExcludedFromSpeech, normalizeTextForSpeech, shouldInsertWordBoundarySpace, } from "./text-processing.js";
3
+ import { collectVisibleTextAndMap, isNodeExcludedFromSpeech, normalizeTextForSpeech, rangeHoldsSpeakableElement, shouldInsertWordBoundarySpace, } from "./text-processing.js";
3
4
  const createAccumulator = (options) => ({
4
5
  chars: [],
5
6
  map: new Map(),
@@ -21,7 +22,7 @@ const getDataMathML = (element) => {
21
22
  return value && value.trim() ? value : null;
22
23
  };
23
24
  const findAssistiveMathML = (element) => {
24
- const assistive = element.querySelector("mjx-assistive-mml math");
25
+ const assistive = flatQuerySelector(element, "mjx-assistive-mml math");
25
26
  return assistive?.outerHTML || null;
26
27
  };
27
28
  const findCanonicalMathML = (element) => {
@@ -72,22 +73,20 @@ const TEXT_CHUNK_SOURCE_ROLES = new Set([
72
73
  "rowheader",
73
74
  ]);
74
75
  const resolveTextChunkSourceElement = (textNode, root) => {
75
- let current = textNode.parentElement;
76
- let best = null;
76
+ let current = flatTreeParentElement(textNode);
77
77
  while (current && current !== root) {
78
78
  const role = (current.getAttribute("role") || "").toLowerCase();
79
79
  const tagName = current.tagName.toUpperCase();
80
80
  if (TEXT_CHUNK_SOURCE_TAGS.has(tagName) ||
81
81
  TEXT_CHUNK_SOURCE_ROLES.has(role)) {
82
- best = current;
83
- break;
82
+ return current;
84
83
  }
85
- current = current.parentElement;
84
+ current = flatTreeParentElement(current);
86
85
  }
87
- return best || root;
86
+ return root;
88
87
  };
89
88
  const hasMathCandidate = (element) => Boolean(typeof element.querySelector === "function" &&
90
- element.querySelector("math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
89
+ flatQuerySelector(element, "math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
91
90
  element.tagName?.toLowerCase() === "math" ||
92
91
  element.hasAttribute?.("data-mathml") ||
93
92
  isMathJaxElement(element);
@@ -121,10 +120,11 @@ const shouldInsertBoundarySpace = (acc, nextCharacter) => {
121
120
  // visible-text collector, instead of an ASCII-only heuristic.
122
121
  return shouldInsertWordBoundarySpace(previous, nextCharacter, acc.options);
123
122
  };
124
- const appendTextNode = (acc, textNode) => {
123
+ /** `start` and `end` bound the part of the node appended, for a selection. */
124
+ const appendTextNode = (acc, textNode, start = 0, end = (textNode.textContent || "").length) => {
125
125
  const raw = textNode.textContent || "";
126
126
  let appendedNonWhitespaceInNode = false;
127
- for (let i = 0; i < raw.length; i++) {
127
+ for (let i = start; i < end; i++) {
128
128
  const character = raw[i];
129
129
  const isWhitespace = /\s/.test(character);
130
130
  if (acc.inLeadingWhitespace) {
@@ -156,13 +156,12 @@ const appendCollectedText = (acc, collected) => {
156
156
  for (let i = 0; i < collected.text.length; i++) {
157
157
  const character = collected.text[i];
158
158
  const isWhitespace = /\s/.test(character);
159
- const mapping = collected.map.get(i) || null;
160
- const syntheticNode = mapping?.node || acc.lastMapped?.node || null;
161
- const syntheticOffset = mapping?.offset ?? acc.lastMapped?.offset ?? 0;
162
- const normalizedMapping = mapping
159
+ const mapping = collected.map.get(i);
160
+ // An unmapped character borrows the last mapped position.
161
+ const characterMapping = mapping
163
162
  ? { node: mapping.node, offset: mapping.offset }
164
- : syntheticNode
165
- ? { node: syntheticNode, offset: syntheticOffset }
163
+ : acc.lastMapped
164
+ ? { node: acc.lastMapped.node, offset: acc.lastMapped.offset }
166
165
  : null;
167
166
  if (acc.inLeadingWhitespace) {
168
167
  if (isWhitespace)
@@ -171,29 +170,16 @@ const appendCollectedText = (acc, collected) => {
171
170
  }
172
171
  if (isWhitespace) {
173
172
  if (!acc.lastCharWasWhitespace) {
174
- appendCharacter(acc, " ", normalizedMapping);
173
+ appendCharacter(acc, " ", characterMapping);
175
174
  }
176
175
  acc.lastCharWasWhitespace = true;
177
176
  continue;
178
177
  }
179
178
  if (i === 0 && shouldInsertBoundarySpace(acc, character)) {
180
- appendCharacter(acc, " ", normalizedMapping);
179
+ appendCharacter(acc, " ", characterMapping);
181
180
  }
182
- appendCharacter(acc, character, normalizedMapping);
181
+ appendCharacter(acc, character, characterMapping);
183
182
  acc.lastCharWasWhitespace = false;
184
- if (mapping) {
185
- acc.map.set(acc.position - 1, {
186
- node: mapping.node,
187
- offset: mapping.offset,
188
- });
189
- acc.lastMapped = { node: mapping.node, offset: mapping.offset };
190
- }
191
- else if (syntheticNode) {
192
- acc.map.set(acc.position - 1, {
193
- node: syntheticNode,
194
- offset: syntheticOffset,
195
- });
196
- }
197
183
  }
198
184
  };
199
185
  const textFallbackFromMathML = (mathml) => {
@@ -230,7 +216,7 @@ const textFallbackFromMathML = (mathml) => {
230
216
  const mapFallbackToAssistiveMath = (element, fallbackText) => {
231
217
  const map = new Map();
232
218
  const sourceMath = isMathJaxElement(element)
233
- ? element.querySelector("mjx-assistive-mml math")
219
+ ? flatQuerySelector(element, "mjx-assistive-mml math")
234
220
  : null;
235
221
  if (!sourceMath)
236
222
  return map;
@@ -247,7 +233,7 @@ const mapFallbackToAssistiveMath = (element, fallbackText) => {
247
233
  }
248
234
  if (node.nodeType !== 1 || isAnnotationElement(node))
249
235
  return;
250
- for (const child of Array.from(node.childNodes)) {
236
+ for (const child of flatTreeChildNodes(node)) {
251
237
  visit(child);
252
238
  }
253
239
  };
@@ -281,7 +267,7 @@ const collectVisibleMathFallback = (element, canonicalMathML, options) => {
281
267
  isAnnotationElement(childElement)) {
282
268
  return;
283
269
  }
284
- for (const child of Array.from(childElement.childNodes)) {
270
+ for (const child of flatTreeChildNodes(childElement)) {
285
271
  visit(child);
286
272
  }
287
273
  };
@@ -307,7 +293,7 @@ const trimTrailingWhitespace = (acc) => {
307
293
  acc.map.delete(acc.position);
308
294
  }
309
295
  };
310
- const collectMathAware = (root, options) => {
296
+ const collectMathAware = (root, options, range) => {
311
297
  const acc = createAccumulator(options);
312
298
  const chunks = [];
313
299
  let textChunkStart = 0;
@@ -328,22 +314,28 @@ const collectMathAware = (root, options) => {
328
314
  const processNode = (node) => {
329
315
  if (isNodeExcludedFromSpeech(node, root))
330
316
  return;
331
- if (node.nodeType === Node.TEXT_NODE) {
317
+ if (range && !rangeIntersectsComposedNode(range, node))
318
+ return;
319
+ if (node.nodeType === 3) {
332
320
  const sourceElement = resolveTextChunkSourceElement(node, root);
333
321
  if (textChunkSourceElement && sourceElement !== textChunkSourceElement) {
334
322
  flushTextChunk();
335
323
  }
336
324
  textChunkSourceElement = sourceElement;
337
- appendTextNode(acc, node);
325
+ const textNode = node;
326
+ appendTextNode(acc, textNode, range?.startContainer === textNode ? range.startOffset : undefined, range?.endContainer === textNode ? range.endOffset : undefined);
338
327
  return;
339
328
  }
340
- if (node.nodeType !== Node.ELEMENT_NODE)
329
+ if (node.nodeType !== 1)
341
330
  return;
342
331
  const element = node;
343
332
  if (isAssistiveMathElement(element))
344
333
  return;
345
334
  const canonicalMathML = findCanonicalMathML(element);
346
- if (canonicalMathML) {
335
+ // An equation is spoken as math only when the selection holds all of it;
336
+ // a part of one reads as the text selected.
337
+ if (canonicalMathML &&
338
+ (!range || rangeHoldsSpeakableElement(range, element, root))) {
347
339
  flushTextChunk();
348
340
  const collected = collectVisibleMathFallback(element, canonicalMathML, options);
349
341
  appendCollectedText(acc, collected);
@@ -358,19 +350,16 @@ const collectMathAware = (root, options) => {
358
350
  containsMathMarkup = true;
359
351
  return;
360
352
  }
361
- for (const child of Array.from(element.childNodes)) {
353
+ for (const child of flatTreeChildNodes(element)) {
362
354
  processNode(child);
363
355
  }
364
356
  };
365
- for (const child of Array.from(root.childNodes)) {
357
+ for (const child of flatTreeChildNodes(root)) {
366
358
  processNode(child);
367
359
  }
368
360
  trimTrailingWhitespace(acc);
369
361
  flushTextChunk();
370
362
  const visibleText = acc.chars.join("");
371
- while (acc.map.has(visibleText.length)) {
372
- acc.map.delete(visibleText.length);
373
- }
374
363
  return {
375
364
  visibleText,
376
365
  map: acc.map,
@@ -378,7 +367,13 @@ const collectMathAware = (root, options) => {
378
367
  containsMathMarkup,
379
368
  };
380
369
  };
381
- export const collectMathAwareTextAndMap = (element, options) => {
370
+ /**
371
+ * With `range`, only the part of `element` the range selects: text it selects
372
+ * and equations it holds whole.
373
+ */
374
+ export const collectMathAwareTextAndMap = (element, options, range) => {
375
+ if (range)
376
+ return collectMathAware(element, options, range);
382
377
  if (!hasMathCandidate(element)) {
383
378
  const { text, map } = collectVisibleTextAndMap(element, options);
384
379
  return {
@@ -0,0 +1,21 @@
1
+ import { type ResolveMathSpeechOptions, type SREMathSpeechOptions } from "./math-speech.js";
2
+ export interface MathControlNamesOptions {
3
+ /**
4
+ * The host's math speech options. Names take only `engineOptions`, where
5
+ * SRE's locale tables load from: their domain and style are fixed.
6
+ */
7
+ getMathSpeech?: () => SREMathSpeechOptions | undefined;
8
+ /** The host's `content-language`, read when an expression is named. */
9
+ getContentLanguage?: () => string | null | undefined;
10
+ loadSre?: ResolveMathSpeechOptions["loadSre"];
11
+ }
12
+ /**
13
+ * Labels each typeset expression inside a control under `root`, open shadow
14
+ * roots included, with SRE's speech, as MathJax adds it and again whenever
15
+ * MathJax replaces it. An `aria-label` on the role-less `mjx-container` puts
16
+ * that speech in the name of the control around it, among the control's other
17
+ * text. English reads ClearSpeak, other languages MathSpeak, in the language of
18
+ * the content. The label replaces the elements' own; it stays when SRE cannot
19
+ * speak the math. Returns the function that stops observing.
20
+ */
21
+ export declare function observeMathControlNames(root: Element, options?: MathControlNamesOptions): () => void;
@@ -0,0 +1,142 @@
1
+ import { resolveContentLanguage } from "../../runtime/content-language.js";
2
+ import { flatQuerySelector, flatTreeClosest, walkFlatTree, } from "./flat-tree.js";
3
+ import { resolveMathSpeechFromChunks, } from "./math-speech.js";
4
+ import { canonicalizeMathML } from "./mathml-sanitization.js";
5
+ /**
6
+ * Elements that take their accessible name from their content. Browsers leave
7
+ * MathML out of that name, so a choice labelled with math is named from its
8
+ * bare symbols ("4 12" for 4 over 12). The elements' renderer labels the math
9
+ * in the same set of controls with its own English speech.
10
+ */
11
+ const CONTROLS = [
12
+ "a[href]",
13
+ "button",
14
+ "label",
15
+ "summary",
16
+ ...[
17
+ "button",
18
+ "checkbox",
19
+ "combobox",
20
+ "link",
21
+ "menuitem",
22
+ "menuitemcheckbox",
23
+ "menuitemradio",
24
+ "option",
25
+ "radio",
26
+ "switch",
27
+ "tab",
28
+ "treeitem",
29
+ ].map((role) => `[role="${role}"]`),
30
+ ].join(", ");
31
+ const TOKENS = new Set(["mi", "mn", "mtext"]);
32
+ const isToken = (element) => TOKENS.has(element.localName) ||
33
+ (element.localName === "mrow" &&
34
+ element.children.length === 1 &&
35
+ TOKENS.has(element.children[0].localName));
36
+ /**
37
+ * ClearSpeak's fraction preference for a name. "4 over 12" keeps the numerals
38
+ * as written, where the default says "four fourths" for 4 over 4 (WCAG 2.5.3:
39
+ * a name does not put words in place of the formula). "Over" between longer
40
+ * parts is ambiguous, so an expression holding such a fraction reads every
41
+ * fraction by numerator and denominator.
42
+ */
43
+ const fractionStyle = (math) => Array.from(math.getElementsByTagName("mfrac")).every((fraction) => Array.from(fraction.children).every(isToken))
44
+ ? "Fraction_Over"
45
+ : "Fraction_General";
46
+ const isEnglish = (language) => !language || language.toLowerCase().split("-")[0] === "en";
47
+ /**
48
+ * Labels each typeset expression inside a control under `root`, open shadow
49
+ * roots included, with SRE's speech, as MathJax adds it and again whenever
50
+ * MathJax replaces it. An `aria-label` on the role-less `mjx-container` puts
51
+ * that speech in the name of the control around it, among the control's other
52
+ * text. English reads ClearSpeak, other languages MathSpeak, in the language of
53
+ * the content. The label replaces the elements' own; it stays when SRE cannot
54
+ * speak the math. Returns the function that stops observing.
55
+ */
56
+ export function observeMathControlNames(root, options = {}) {
57
+ if (typeof MutationObserver === "undefined")
58
+ return () => { };
59
+ const speech = new Map();
60
+ let stopped = false;
61
+ const speak = async (mathml, math, language) => {
62
+ const result = await resolveMathSpeechFromChunks([{ type: "math", mathml, fallbackText: "" }], {
63
+ language,
64
+ loadSre: options.loadSre,
65
+ mathSpeech: {
66
+ ...(isEnglish(language) ? { style: fractionStyle(math) } : {}),
67
+ engineOptions: options.getMathSpeech?.()?.engineOptions,
68
+ },
69
+ });
70
+ return result.usedMathSpeech ? result.speechText : null;
71
+ };
72
+ const name = (container) => {
73
+ if (!flatTreeClosest(container, CONTROLS))
74
+ return;
75
+ const math = flatQuerySelector(container, "math");
76
+ const mathml = math && canonicalizeMathML(math.outerHTML);
77
+ if (!math || !mathml)
78
+ return;
79
+ const language = resolveContentLanguage(container, {
80
+ contentLanguage: options.getContentLanguage?.(),
81
+ });
82
+ const key = `${language ?? ""}\u0000${mathml}`;
83
+ let pending = speech.get(key);
84
+ if (!pending) {
85
+ pending = speak(mathml, math, language);
86
+ speech.set(key, pending);
87
+ }
88
+ void pending.then((label) => {
89
+ if (stopped || !label || !container.isConnected)
90
+ return;
91
+ if (container.getAttribute("aria-label") !== label) {
92
+ container.setAttribute("aria-label", label);
93
+ }
94
+ });
95
+ };
96
+ // A shadow root is a tree of its own, which an observer on `root` does not
97
+ // see into, so each one found is observed as well. One attached after its
98
+ // host was visited is not found until something is added around the host.
99
+ const observed = new WeakSet();
100
+ const observeTree = (tree) => {
101
+ if (observed.has(tree))
102
+ return;
103
+ observed.add(tree);
104
+ observer.observe(tree, { childList: true, subtree: true });
105
+ };
106
+ const visit = (node) => {
107
+ if (node.nodeType !== 1)
108
+ return;
109
+ const element = node;
110
+ const container = flatTreeClosest(element, "mjx-container");
111
+ if (container) {
112
+ name(container);
113
+ return;
114
+ }
115
+ if (element.shadowRoot)
116
+ observeTree(element.shadowRoot);
117
+ walkFlatTree(element, (descendant) => {
118
+ if (descendant.nodeType !== 1)
119
+ return "skip";
120
+ const found = descendant;
121
+ if (found.localName === "mjx-container") {
122
+ name(found);
123
+ return "skip";
124
+ }
125
+ if (found.shadowRoot)
126
+ observeTree(found.shadowRoot);
127
+ return undefined;
128
+ });
129
+ };
130
+ const observer = new MutationObserver((records) => {
131
+ for (const record of records) {
132
+ for (const node of record.addedNodes)
133
+ visit(node);
134
+ }
135
+ });
136
+ observeTree(root);
137
+ visit(root);
138
+ return () => {
139
+ stopped = true;
140
+ observer.disconnect();
141
+ };
142
+ }
@@ -14,7 +14,7 @@ export interface ResolveMathSpeechOptions {
14
14
  mathSpeech?: SREMathSpeechOptions;
15
15
  /**
16
16
  * Also produce SRE's SSML rendering (markup: "ssml") for the equation, for
17
- * the runtime generated-SSML path (PIE-623). Only populated when the input
17
+ * the runtime generated-SSML path. Only populated when the input
18
18
  * is a single math chunk and SRE succeeds. The plain `speechText` is always
19
19
  * computed with markup: "none" so the plain path is unaffected.
20
20
  */
@@ -43,6 +43,17 @@ export interface ResolvedMathSpeech {
43
43
  */
44
44
  ssml?: string;
45
45
  }
46
+ /**
47
+ * Loads the speech-rule-engine once per page. A failed load is dropped, so the
48
+ * next call tries again; meanwhile math reads as its visible text. The first
49
+ * failure warns, later ones log at debug.
50
+ */
51
+ export declare const createSreLoader: (importEngine: () => Promise<SpeechRuleEngineApi>) => ((engineOptions?: Record<string, unknown>) => Promise<SpeechRuleEngineApi>);
52
+ /**
53
+ * One speech-rule-engine load for a whole read: its equations share the
54
+ * attempt, so a failed load costs a read one attempt.
55
+ */
56
+ export declare const createReadSreLoad: (mathSpeech?: SREMathSpeechOptions) => (() => Promise<SpeechRuleEngineApi>);
46
57
  export declare const normalizeSREMathSpeechOptions: (options: unknown) => SREMathSpeechOptions | undefined;
47
58
  export declare const resolveMathSpeechFromChunks: (chunks: MathAwareSpeechChunk[], options?: ResolveMathSpeechOptions) => Promise<ResolvedMathSpeech>;
48
59
  export {};