@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
@@ -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,7 +73,7 @@ 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 current = flatTreeParentElement(textNode);
76
77
  let best = null;
77
78
  while (current && current !== root) {
78
79
  const role = (current.getAttribute("role") || "").toLowerCase();
@@ -82,12 +83,12 @@ const resolveTextChunkSourceElement = (textNode, root) => {
82
83
  best = current;
83
84
  break;
84
85
  }
85
- current = current.parentElement;
86
+ current = flatTreeParentElement(current);
86
87
  }
87
88
  return best || root;
88
89
  };
89
90
  const hasMathCandidate = (element) => Boolean(typeof element.querySelector === "function" &&
90
- element.querySelector("math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
91
+ flatQuerySelector(element, "math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
91
92
  element.tagName?.toLowerCase() === "math" ||
92
93
  element.hasAttribute?.("data-mathml") ||
93
94
  isMathJaxElement(element);
@@ -121,10 +122,11 @@ const shouldInsertBoundarySpace = (acc, nextCharacter) => {
121
122
  // visible-text collector, instead of an ASCII-only heuristic.
122
123
  return shouldInsertWordBoundarySpace(previous, nextCharacter, acc.options);
123
124
  };
124
- const appendTextNode = (acc, textNode) => {
125
+ /** `start` and `end` bound the part of the node appended, for a selection. */
126
+ const appendTextNode = (acc, textNode, start = 0, end = (textNode.textContent || "").length) => {
125
127
  const raw = textNode.textContent || "";
126
128
  let appendedNonWhitespaceInNode = false;
127
- for (let i = 0; i < raw.length; i++) {
129
+ for (let i = start; i < end; i++) {
128
130
  const character = raw[i];
129
131
  const isWhitespace = /\s/.test(character);
130
132
  if (acc.inLeadingWhitespace) {
@@ -230,7 +232,7 @@ const textFallbackFromMathML = (mathml) => {
230
232
  const mapFallbackToAssistiveMath = (element, fallbackText) => {
231
233
  const map = new Map();
232
234
  const sourceMath = isMathJaxElement(element)
233
- ? element.querySelector("mjx-assistive-mml math")
235
+ ? flatQuerySelector(element, "mjx-assistive-mml math")
234
236
  : null;
235
237
  if (!sourceMath)
236
238
  return map;
@@ -247,7 +249,7 @@ const mapFallbackToAssistiveMath = (element, fallbackText) => {
247
249
  }
248
250
  if (node.nodeType !== 1 || isAnnotationElement(node))
249
251
  return;
250
- for (const child of Array.from(node.childNodes)) {
252
+ for (const child of flatTreeChildNodes(node)) {
251
253
  visit(child);
252
254
  }
253
255
  };
@@ -281,7 +283,7 @@ const collectVisibleMathFallback = (element, canonicalMathML, options) => {
281
283
  isAnnotationElement(childElement)) {
282
284
  return;
283
285
  }
284
- for (const child of Array.from(childElement.childNodes)) {
286
+ for (const child of flatTreeChildNodes(childElement)) {
285
287
  visit(child);
286
288
  }
287
289
  };
@@ -307,7 +309,7 @@ const trimTrailingWhitespace = (acc) => {
307
309
  acc.map.delete(acc.position);
308
310
  }
309
311
  };
310
- const collectMathAware = (root, options) => {
312
+ const collectMathAware = (root, options, range) => {
311
313
  const acc = createAccumulator(options);
312
314
  const chunks = [];
313
315
  let textChunkStart = 0;
@@ -328,22 +330,28 @@ const collectMathAware = (root, options) => {
328
330
  const processNode = (node) => {
329
331
  if (isNodeExcludedFromSpeech(node, root))
330
332
  return;
331
- if (node.nodeType === Node.TEXT_NODE) {
333
+ if (range && !rangeIntersectsComposedNode(range, node))
334
+ return;
335
+ if (node.nodeType === 3) {
332
336
  const sourceElement = resolveTextChunkSourceElement(node, root);
333
337
  if (textChunkSourceElement && sourceElement !== textChunkSourceElement) {
334
338
  flushTextChunk();
335
339
  }
336
340
  textChunkSourceElement = sourceElement;
337
- appendTextNode(acc, node);
341
+ const textNode = node;
342
+ appendTextNode(acc, textNode, range?.startContainer === textNode ? range.startOffset : undefined, range?.endContainer === textNode ? range.endOffset : undefined);
338
343
  return;
339
344
  }
340
- if (node.nodeType !== Node.ELEMENT_NODE)
345
+ if (node.nodeType !== 1)
341
346
  return;
342
347
  const element = node;
343
348
  if (isAssistiveMathElement(element))
344
349
  return;
345
350
  const canonicalMathML = findCanonicalMathML(element);
346
- if (canonicalMathML) {
351
+ // An equation is spoken as math only when the selection holds all of it;
352
+ // a part of one reads as the text selected.
353
+ if (canonicalMathML &&
354
+ (!range || rangeHoldsSpeakableElement(range, element, root))) {
347
355
  flushTextChunk();
348
356
  const collected = collectVisibleMathFallback(element, canonicalMathML, options);
349
357
  appendCollectedText(acc, collected);
@@ -358,11 +366,11 @@ const collectMathAware = (root, options) => {
358
366
  containsMathMarkup = true;
359
367
  return;
360
368
  }
361
- for (const child of Array.from(element.childNodes)) {
369
+ for (const child of flatTreeChildNodes(element)) {
362
370
  processNode(child);
363
371
  }
364
372
  };
365
- for (const child of Array.from(root.childNodes)) {
373
+ for (const child of flatTreeChildNodes(root)) {
366
374
  processNode(child);
367
375
  }
368
376
  trimTrailingWhitespace(acc);
@@ -378,7 +386,13 @@ const collectMathAware = (root, options) => {
378
386
  containsMathMarkup,
379
387
  };
380
388
  };
381
- export const collectMathAwareTextAndMap = (element, options) => {
389
+ /**
390
+ * With `range`, only the part of `element` the range selects: text it selects
391
+ * and equations it holds whole.
392
+ */
393
+ export const collectMathAwareTextAndMap = (element, options, range) => {
394
+ if (range)
395
+ return collectMathAware(element, options, range);
382
396
  if (!hasMathCandidate(element)) {
383
397
  const { text, map } = collectVisibleTextAndMap(element, options);
384
398
  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
+ }
@@ -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 {};
@@ -1,19 +1,53 @@
1
+ import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
1
2
  import { setSreStartupLocaleSource, sreLocaleSource } from "./sre-locales.js";
2
3
  import { normalizeTextForSpeech } from "./text-processing.js";
3
- let sreLoadPromise = null;
4
+ // Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
5
+ const logger = createPieLogger("tts-math-speech", isTtsDebugEnabled);
4
6
  let sreOperationQueue = Promise.resolve();
5
7
  const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
6
- const defaultLoadSre = async (engineOptions) => {
7
- if (!sreLoadPromise) {
8
- setSreStartupLocaleSource(engineOptions);
9
- sreLoadPromise = import("./sre-engine.js").then(({ default: candidate }) => {
10
- if (!candidate || typeof candidate.toSpeech !== "function") {
11
- throw new Error("speech-rule-engine did not expose toSpeech");
12
- }
13
- return candidate;
14
- });
8
+ /**
9
+ * Loads the speech-rule-engine once per page. A failed load is dropped, so the
10
+ * next call tries again; meanwhile math reads as its visible text. The first
11
+ * failure warns, later ones log at debug.
12
+ */
13
+ export const createSreLoader = (importEngine) => {
14
+ let load = null;
15
+ let warned = false;
16
+ return (engineOptions) => {
17
+ if (!load) {
18
+ setSreStartupLocaleSource(engineOptions);
19
+ const attempt = importEngine();
20
+ load = attempt;
21
+ attempt.catch((error) => {
22
+ if (load === attempt)
23
+ load = null;
24
+ if (warned) {
25
+ logger.debug("speech-rule-engine failed to load again", error);
26
+ return;
27
+ }
28
+ warned = true;
29
+ logger.warn("speech-rule-engine failed to load; math reads as its visible text until a later read loads it.", error);
30
+ });
31
+ }
32
+ return load;
33
+ };
34
+ };
35
+ const defaultLoadSre = createSreLoader(() => import("./sre-engine.js").then(({ default: candidate }) => {
36
+ if (!candidate || typeof candidate.toSpeech !== "function") {
37
+ throw new Error("speech-rule-engine did not expose toSpeech");
15
38
  }
16
- return sreLoadPromise;
39
+ return candidate;
40
+ }));
41
+ /**
42
+ * One speech-rule-engine load for a whole read: its equations share the
43
+ * attempt, so a failed load costs a read one attempt.
44
+ */
45
+ export const createReadSreLoad = (mathSpeech) => {
46
+ let attempt = null;
47
+ return () => {
48
+ attempt ??= defaultLoadSre(normalizeSREMathSpeechOptions(mathSpeech)?.engineOptions);
49
+ return attempt;
50
+ };
17
51
  };
18
52
  // SRE's ClearSpeak rule set is English-only; MathSpeak is localized to many more
19
53
  // locales. Use ClearSpeak for English (highest-quality math prose) and MathSpeak
@@ -47,9 +81,9 @@ const setupSre = async (sre, language, markup, mathSpeech) => {
47
81
  ...engineOptions,
48
82
  locale,
49
83
  domain: normalizedMathSpeech?.domain || domainForLocale(locale),
50
- ...(normalizedMathSpeech?.style
51
- ? { style: normalizedMathSpeech.style }
52
- : {}),
84
+ // SRE keeps a style it was given until it is given another, so a call
85
+ // without one resets it: math control names set ClearSpeak preferences.
86
+ style: normalizedMathSpeech?.style || "default",
53
87
  modality: "speech",
54
88
  markup,
55
89
  });
@@ -80,7 +114,7 @@ const generateMathSsml = async (sre, mathml, language, mathSpeech) => {
80
114
  return ssml.includes("<speak") ? ssml : null;
81
115
  }
82
116
  catch (error) {
83
- console.debug("[TTSService] Math SSML generation failed; using plain", {
117
+ logger.debug("math SSML generation failed; using plain", {
84
118
  message: error instanceof Error ? error.message : String(error),
85
119
  });
86
120
  return null;
@@ -99,8 +133,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
99
133
  let sre = null;
100
134
  let usedMathSpeech = false;
101
135
  let usedFallback = false;
102
- const loadSre = options.loadSre ||
103
- (() => defaultLoadSre(normalizeSREMathSpeechOptions(options.mathSpeech)?.engineOptions));
136
+ const loadSre = options.loadSre || createReadSreLoad(options.mathSpeech);
104
137
  const speechParts = [];
105
138
  for (const chunk of chunks) {
106
139
  if (chunk.type === "text") {
@@ -122,7 +155,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
122
155
  }
123
156
  catch (error) {
124
157
  usedFallback = true;
125
- console.debug("[TTSService] Math speech generation failed; using visible fallback", {
158
+ logger.debug("math speech generation failed; using visible fallback", {
126
159
  message: error instanceof Error ? error.message : String(error),
127
160
  });
128
161
  speechParts.push(chunk.fallbackText);
@@ -0,0 +1,41 @@
1
+ import type { TTSConfig, TTSProviderOptions } from "@pie-players/pie-tts";
2
+ import type { SREMathSpeechOptions } from "./math-speech.js";
3
+ import type { BoundarySpacingMode } from "./text-processing.js";
4
+ /**
5
+ * The provider options the toolkit reads, on top of the ones every provider
6
+ * receives. Hosts configure them as free-form options, so readers still check
7
+ * each value at runtime.
8
+ */
9
+ export interface ToolkitTTSProviderOptions extends TTSProviderOptions {
10
+ /** Highlight words or sentences while reading. Defaults by provider capability. */
11
+ highlightMode?: "word" | "sentence";
12
+ /** Locale for text processing; the toolkit sets it per speak. */
13
+ locale?: string;
14
+ /** Speech-rule-engine options for math speech. */
15
+ mathSpeech?: SREMathSpeechOptions;
16
+ /** Sentence and word segmentation of the browser provider. */
17
+ segmenter?: {
18
+ /** `regexOnly` segments without `Intl.Segmenter`. */
19
+ mode?: "regexOnly";
20
+ locale?: string;
21
+ };
22
+ /** Pauses at structural boundaries: `baseMs` scaled by a strength unit, clamped. */
23
+ structuralPauses?: {
24
+ baseMs?: number | string;
25
+ units?: {
26
+ minor?: number | string;
27
+ section?: number | string;
28
+ major?: number | string;
29
+ };
30
+ minMs?: number | string;
31
+ maxMs?: number | string;
32
+ };
33
+ textNormalization?: {
34
+ boundarySpacingMode?: BoundarySpacingMode;
35
+ locale?: string;
36
+ };
37
+ }
38
+ /** A provider configuration as the toolkit reads it. */
39
+ export interface ToolkitTTSConfig extends TTSConfig {
40
+ providerOptions?: ToolkitTTSProviderOptions;
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,29 +1,5 @@
1
- /**
2
- * Shared SSML → spoken-text extractor.
3
- *
4
- * Strips SSML markup to the words a TTS engine would actually voice, while
5
- * recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
6
- * normalized spoken-text offsets. This is the single source of truth for SSML
7
- * stripping used by:
8
- * - the catalog span aligner (`catalog-span-alignment.ts`),
9
- * - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
10
- * - the runtime generated-speech path (PIE-623).
11
- *
12
- * Keeping one implementation guarantees provider word-boundary offsets (which
13
- * are into the raw SSML string) map to the same spoken text on every path.
14
- *
15
- * Tag handling:
16
- * - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
17
- * `voice`, `lang`) are transparent; their text content flows through.
18
- * `p`/`s` open tags inject a word boundary space.
19
- * - `break` injects a space (a pause is a word boundary).
20
- * - `sub alias="…"` substitutes the alias text (and skips the original).
21
- * `sub` with no alias marks the content unsupported for word tracking.
22
- * - Semantic tags that change *what* is spoken without leaving trackable
23
- * text (`audio`, `phoneme`) mark the content unsupported.
24
- * - Any other unknown element marks the content unsupported (degrade to
25
- * coarse region highlighting rather than mis-track).
26
- */
1
+ /** Whether `text` is an SSML document: it opens with a `<speak>` element. */
2
+ export declare const isSsmlDocument: (text: string) => boolean;
27
3
  export interface ExtractedSpokenText {
28
4
  spokenText: string;
29
5
  rawToSpokenOffsetMap: Map<number, number>;
@@ -1,4 +1,33 @@
1
1
  import { normalizeTextForSpeech } from "../text-processing.js";
2
+ /**
3
+ * Shared SSML → spoken-text extractor.
4
+ *
5
+ * Strips SSML markup to the words a TTS engine would actually voice, while
6
+ * recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
7
+ * normalized spoken-text offsets. This is the single source of truth for SSML
8
+ * stripping used by:
9
+ * - the catalog span aligner (`catalog-span-alignment.ts`),
10
+ * - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
11
+ * - the runtime generated-speech path (PIE-623).
12
+ *
13
+ * Keeping one implementation guarantees provider word-boundary offsets (which
14
+ * are into the raw SSML string) map to the same spoken text on every path.
15
+ *
16
+ * Tag handling:
17
+ * - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
18
+ * `voice`, `lang`) are transparent; their text content flows through.
19
+ * `p`/`s` open tags inject a word boundary space.
20
+ * - `break` injects a space (a pause is a word boundary).
21
+ * - `sub alias="…"` substitutes the alias text (and skips the original).
22
+ * `sub` with no alias marks the content unsupported for word tracking.
23
+ * - Semantic tags that change *what* is spoken without leaving trackable
24
+ * text (`audio`, `phoneme`) mark the content unsupported.
25
+ * - Any other unknown element marks the content unsupported (degrade to
26
+ * coarse region highlighting rather than mis-track).
27
+ */
28
+ const SSML_DOCUMENT = /^\s*<speak[\s/>]/i;
29
+ /** Whether `text` is an SSML document: it opens with a `<speak>` element. */
30
+ export const isSsmlDocument = (text) => SSML_DOCUMENT.test(text);
2
31
  const STRUCTURAL_SSML_TAGS = new Set([
3
32
  "speak",
4
33
  "p",
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The `code` a speak's rejection carries when speech could not start, as
3
+ * distinct from a failure while speaking. Read by shape: several copies of the
4
+ * toolkit can share a page, so an error class from one fails `instanceof` in
5
+ * another.
6
+ */
7
+ export declare const TTS_START_FAILED_CODE = "tts-start-failed";
8
+ /** Whether a speak rejected because speech could not start. */
9
+ export declare function isTTSStartFailure(error: unknown): boolean;
10
+ /** Marks `cause` as a start failure, keeping its message and stack. */
11
+ export declare function toTTSStartFailure(cause: unknown): Error;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The `code` a speak's rejection carries when speech could not start, as
3
+ * distinct from a failure while speaking. Read by shape: several copies of the
4
+ * toolkit can share a page, so an error class from one fails `instanceof` in
5
+ * another.
6
+ */
7
+ export const TTS_START_FAILED_CODE = "tts-start-failed";
8
+ /** Whether a speak rejected because speech could not start. */
9
+ export function isTTSStartFailure(error) {
10
+ return (typeof error === "object" &&
11
+ error !== null &&
12
+ error.code === TTS_START_FAILED_CODE);
13
+ }
14
+ /** Marks `cause` as a start failure, keeping its message and stack. */
15
+ export function toTTSStartFailure(cause) {
16
+ const error = cause instanceof Error ? cause : new Error(String(cause));
17
+ error.code = TTS_START_FAILED_CODE;
18
+ return error;
19
+ }
@@ -62,6 +62,15 @@ export declare const collectRangeTextForSpeech: (range: Range, root: Element) =>
62
62
  text: string;
63
63
  filtered: boolean;
64
64
  };
65
+ /** The part of `textNode`'s text that `range` holds, for a node it intersects. */
66
+ export declare const textInRange: (textNode: Text, range: Range) => string;
67
+ /**
68
+ * Whether `range` holds all of `element`'s speakable text: its first and last
69
+ * speakable characters, which a contiguous range holding both holds everything
70
+ * between. An element with no speakable text, an image for one, is held when the
71
+ * range touches it, since no part of it can be selected alone.
72
+ */
73
+ export declare const rangeHoldsSpeakableElement: (range: Range, element: Element, root?: Element | null) => boolean;
65
74
  export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
66
75
  export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
67
76
  text: string;