@pie-players/pie-assessment-toolkit 0.3.73 → 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 (274) hide show
  1. package/README.md +469 -318
  2. package/dist/attempt/AssessmentSession.d.ts +1 -5
  3. package/dist/attempt/AssessmentSession.js +0 -29
  4. package/dist/attempt/TestSession.d.ts +1 -20
  5. package/dist/attempt/TestSession.js +8 -99
  6. package/dist/components/ItemScope.custom-element.d.ts +1 -0
  7. package/dist/components/ItemScope.custom-element.js +1 -0
  8. package/dist/components/ItemToolBar.custom-element.d.ts +1 -0
  9. package/dist/components/ItemToolBar.custom-element.js +1 -1
  10. package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
  11. package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
  12. package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
  13. package/dist/components/SectionToolBar.custom-element.js +1 -1
  14. package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
  15. package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
  16. package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
  17. package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
  18. package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
  19. package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
  20. package/dist/components/chunks/sre-engine-jd8cc08x.js +1 -0
  21. package/dist/components/item-scope-element.d.ts +2 -0
  22. package/dist/components/item-scope-element.js +2 -0
  23. package/dist/context/assessment-toolkit-context.d.ts +32 -6
  24. package/dist/index.d.ts +23 -48
  25. package/dist/index.js +12 -37
  26. package/dist/policy/core/PolicySource.d.ts +1 -1
  27. package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
  28. package/dist/policy/core/ToolPolicyEngine.js +148 -46
  29. package/dist/policy/core/compose-decision.d.ts +28 -7
  30. package/dist/policy/core/compose-decision.js +75 -54
  31. package/dist/policy/core/decision-types.d.ts +65 -34
  32. package/dist/policy/core/feature-decision.d.ts +16 -9
  33. package/dist/policy/core/feature-decision.js +14 -8
  34. package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
  35. package/dist/policy/core/pnp-policy-inputs.js +28 -56
  36. package/dist/policy/core/provenance.d.ts +9 -4
  37. package/dist/policy/core/provenance.js +1 -1
  38. package/dist/policy/engine.d.ts +7 -23
  39. package/dist/policy/engine.js +5 -20
  40. package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
  41. package/dist/policy/sources/PnpPolicySource.js +117 -118
  42. package/dist/runtime/SectionControllerBinding.d.ts +279 -0
  43. package/dist/runtime/SectionControllerBinding.js +450 -0
  44. package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
  45. package/dist/runtime/SectionRuntimeEngine.js +30 -372
  46. package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
  47. package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
  48. package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
  49. package/dist/runtime/adapter/dom-event-bridge.js +17 -44
  50. package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -2
  51. package/dist/runtime/adapter/subscriber-fanout.js +1 -2
  52. package/dist/runtime/catalog-context.d.ts +19 -0
  53. package/dist/runtime/catalog-context.js +28 -0
  54. package/dist/runtime/composition-emit-scheduler.d.ts +9 -15
  55. package/dist/runtime/composition-emit-scheduler.js +9 -15
  56. package/dist/runtime/content-language.d.ts +32 -0
  57. package/dist/runtime/content-language.js +46 -0
  58. package/dist/runtime/content-region.d.ts +17 -0
  59. package/dist/runtime/content-region.js +29 -0
  60. package/dist/runtime/content-registration.d.ts +13 -0
  61. package/dist/runtime/content-registration.js +25 -0
  62. package/dist/runtime/core/SectionEngineCore.d.ts +2 -2
  63. package/dist/runtime/core/SectionEngineCore.js +2 -2
  64. package/dist/runtime/core/cohort.d.ts +1 -1
  65. package/dist/runtime/core/cohort.js +1 -1
  66. package/dist/runtime/core/engine-input.d.ts +18 -30
  67. package/dist/runtime/core/engine-input.js +11 -12
  68. package/dist/runtime/core/engine-output.d.ts +16 -40
  69. package/dist/runtime/core/engine-output.js +9 -21
  70. package/dist/runtime/core/engine-readiness.d.ts +5 -37
  71. package/dist/runtime/core/engine-readiness.js +8 -41
  72. package/dist/runtime/core/engine-resolver.d.ts +46 -74
  73. package/dist/runtime/core/engine-resolver.js +18 -43
  74. package/dist/runtime/core/engine-state.d.ts +28 -36
  75. package/dist/runtime/core/engine-state.js +18 -21
  76. package/dist/runtime/core/engine-transition.d.ts +14 -19
  77. package/dist/runtime/core/engine-transition.js +71 -51
  78. package/dist/runtime/engine.d.ts +16 -19
  79. package/dist/runtime/engine.js +12 -19
  80. package/dist/runtime/page-warnings.d.ts +13 -0
  81. package/dist/runtime/page-warnings.js +22 -0
  82. package/dist/runtime/policy-input-forwarding.d.ts +25 -0
  83. package/dist/runtime/policy-input-forwarding.js +25 -0
  84. package/dist/runtime/registration-events.d.ts +14 -0
  85. package/dist/runtime/runtime-event-claim.d.ts +8 -0
  86. package/dist/runtime/runtime-event-claim.js +15 -0
  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 +188 -0
  93. package/dist/runtime/tool-host-contract.d.ts +28 -10
  94. package/dist/runtime/tool-host-contract.js +31 -10
  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/ElementToolStateStore.d.ts +14 -10
  98. package/dist/services/ElementToolStateStore.js +37 -13
  99. package/dist/services/HighlightCoordinator.d.ts +22 -28
  100. package/dist/services/HighlightCoordinator.js +431 -327
  101. package/dist/services/RangeSerializer.d.ts +10 -1
  102. package/dist/services/RangeSerializer.js +87 -14
  103. package/dist/services/TTSService.d.ts +130 -69
  104. package/dist/services/TTSService.js +713 -570
  105. package/dist/services/ToolCoordinator.d.ts +43 -66
  106. package/dist/services/ToolCoordinator.js +164 -208
  107. package/dist/services/ToolRegistry.d.ts +78 -54
  108. package/dist/services/ToolRegistry.js +107 -105
  109. package/dist/services/ToolkitCoordinator.d.ts +261 -175
  110. package/dist/services/ToolkitCoordinator.js +667 -433
  111. package/dist/services/audio-handoff.d.ts +6 -7
  112. package/dist/services/audio-handoff.js +20 -14
  113. package/dist/services/catalog-owner.d.ts +11 -1
  114. package/dist/services/catalog-owner.js +25 -0
  115. package/dist/services/create-tools-config.d.ts +2 -2
  116. package/dist/services/framework-error-bus.d.ts +3 -6
  117. package/dist/services/framework-error-bus.js +3 -6
  118. package/dist/services/framework-error.d.ts +30 -8
  119. package/dist/services/framework-error.js +9 -7
  120. package/dist/services/interfaces.d.ts +158 -129
  121. package/dist/services/section-controller-types.d.ts +15 -0
  122. package/dist/services/section-session-assignment.d.ts +21 -0
  123. package/dist/services/section-session-assignment.js +82 -0
  124. package/dist/services/spoken-audio-cards.d.ts +0 -2
  125. package/dist/services/spoken-audio-cards.js +1 -3
  126. package/dist/services/tool-config-defaults.d.ts +3 -3
  127. package/dist/services/tool-config-defaults.js +3 -3
  128. package/dist/services/tool-config-validation.d.ts +14 -4
  129. package/dist/services/tool-config-validation.js +63 -71
  130. package/dist/services/tool-context.d.ts +0 -8
  131. package/dist/services/tool-context.js +29 -25
  132. package/dist/services/tool-icons.d.ts +4 -3
  133. package/dist/services/tool-icons.js +4 -3
  134. package/dist/services/tool-instance-id.d.ts +0 -1
  135. package/dist/services/tool-instance-id.js +0 -6
  136. package/dist/services/tool-providers/TTSToolProvider.d.ts +41 -80
  137. package/dist/services/tool-providers/TTSToolProvider.js +81 -40
  138. package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
  139. package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
  140. package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
  141. package/dist/services/tool-providers/index.d.ts +0 -9
  142. package/dist/services/tool-providers/index.js +2 -4
  143. package/dist/services/tool-request.d.ts +27 -12
  144. package/dist/services/tool-request.js +24 -14
  145. package/dist/services/toolbar-decided-tools.d.ts +15 -0
  146. package/dist/services/toolbar-decided-tools.js +28 -0
  147. package/dist/services/tools-config-normalizer.d.ts +37 -23
  148. package/dist/services/tools-config-normalizer.js +2 -10
  149. package/dist/services/tts/browser-provider.d.ts +38 -2
  150. package/dist/services/tts/browser-provider.js +276 -140
  151. package/dist/services/tts/catalog-span-alignment.d.ts +2 -17
  152. package/dist/services/tts/catalog-span-alignment.js +17 -142
  153. package/dist/services/tts/flat-tree.d.ts +71 -0
  154. package/dist/services/tts/flat-tree.js +269 -0
  155. package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
  156. package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
  157. package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
  158. package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
  159. package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
  160. package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
  161. package/dist/services/tts/generated-speech/types.d.ts +0 -12
  162. package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
  163. package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
  164. package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
  165. package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
  166. package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
  167. package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
  168. package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
  169. package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
  170. package/dist/services/tts/math-alignment/index.d.ts +0 -5
  171. package/dist/services/tts/math-alignment/index.js +5 -25
  172. package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
  173. package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
  174. package/dist/services/tts/math-alignment/types.d.ts +4 -2
  175. package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
  176. package/dist/services/tts/math-aware-text-processing.js +74 -18
  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 +16 -1
  180. package/dist/services/tts/math-speech.js +55 -19
  181. package/dist/services/tts/provider-options.d.ts +41 -0
  182. package/dist/services/tts/provider-options.js +1 -0
  183. package/dist/services/tts/sre-engine.d.ts +7 -0
  184. package/dist/services/tts/sre-engine.js +11 -0
  185. package/dist/services/tts/sre-locales.d.ts +14 -0
  186. package/dist/services/tts/sre-locales.js +57 -0
  187. package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
  188. package/dist/services/tts/ssml/spoken-text.js +29 -0
  189. package/dist/services/tts/start-failure.d.ts +11 -0
  190. package/dist/services/tts/start-failure.js +19 -0
  191. package/dist/services/tts/text-processing.d.ts +9 -1
  192. package/dist/services/tts/text-processing.js +46 -34
  193. package/dist/services/tts/text-segmentation.d.ts +7 -0
  194. package/dist/services/tts/text-segmentation.js +47 -0
  195. package/dist/services/tts-runtime-config.d.ts +44 -18
  196. package/dist/services/tts-runtime-config.js +22 -93
  197. package/dist/tools/content-capability-resolution.d.ts +7 -9
  198. package/dist/tools/content-capability-resolution.js +8 -45
  199. package/dist/tools/registration.d.ts +58 -0
  200. package/dist/tools/registration.js +54 -0
  201. package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
  202. package/dist/tools/tool-surface-host.d.ts +6 -10
  203. package/dist/tools/tool-surface-host.js +44 -42
  204. package/dist/tools/tool-tag-map.d.ts +5 -15
  205. package/dist/tools/tool-tag-map.js +6 -20
  206. package/dist/utils/structural-equality.d.ts +11 -0
  207. package/dist/utils/structural-equality.js +47 -0
  208. package/package.json +24 -59
  209. package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
  210. package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
  211. package/dist/components/chunks/ItemToolBar-byq80z42.js +0 -40
  212. package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
  213. package/dist/context/runtime-context-consumer.d.ts +0 -13
  214. package/dist/context/runtime-context-consumer.js +0 -51
  215. package/dist/item-loader.d.ts +0 -44
  216. package/dist/item-loader.js +0 -61
  217. package/dist/policy/core/engine-context.d.ts +0 -29
  218. package/dist/policy/core/engine-context.js +0 -27
  219. package/dist/policy/core/policy-source-tag.d.ts +0 -31
  220. package/dist/policy/core/policy-source-tag.js +0 -28
  221. package/dist/policy/internal.d.ts +0 -25
  222. package/dist/policy/internal.js +0 -25
  223. package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
  224. package/dist/runtime/adapter/coordinator-bridge.js +0 -126
  225. package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
  226. package/dist/runtime/adapter/framework-error-bridge.js +0 -46
  227. package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
  228. package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
  229. package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
  230. package/dist/runtime/core/engine-stage-derivation.js +0 -29
  231. package/dist/runtime/engine-context.d.ts +0 -39
  232. package/dist/runtime/engine-context.js +0 -31
  233. package/dist/runtime/internal.d.ts +0 -43
  234. package/dist/runtime/internal.js +0 -45
  235. package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -80
  236. package/dist/runtime/section-runtime-engine-host-context.js +0 -95
  237. package/dist/runtime/stage-emit-gate.d.ts +0 -92
  238. package/dist/runtime/stage-emit-gate.js +0 -82
  239. package/dist/services/I18nService.d.ts +0 -47
  240. package/dist/services/I18nService.js +0 -84
  241. package/dist/services/ThemeProvider.d.ts +0 -95
  242. package/dist/services/ThemeProvider.js +0 -334
  243. package/dist/services/ToolConfigResolver.d.ts +0 -161
  244. package/dist/services/ToolConfigResolver.js +0 -229
  245. package/dist/services/catalog-media.d.ts +0 -56
  246. package/dist/services/catalog-media.js +0 -190
  247. package/dist/services/createDefaultToolRegistry.d.ts +0 -53
  248. package/dist/services/createDefaultToolRegistry.js +0 -45
  249. package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
  250. package/dist/services/defaultPersonalNeedsProfile.js +0 -26
  251. package/dist/services/pnp-standard-features.d.ts +0 -238
  252. package/dist/services/pnp-standard-features.js +0 -338
  253. package/dist/services/tool-providers/CortexToolProvider.d.ts +0 -13
  254. package/dist/services/tool-providers/CortexToolProvider.js +0 -32
  255. package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
  256. package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
  257. package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
  258. package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
  259. package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
  260. package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
  261. package/dist/services/tts/visual-line-ranges.d.ts +0 -18
  262. package/dist/services/tts/visual-line-ranges.js +0 -226
  263. package/dist/tools/client.d.ts +0 -12
  264. package/dist/tools/client.js +0 -14
  265. package/dist/tools/index.d.ts +0 -8
  266. package/dist/tools/index.js +0 -10
  267. package/dist/tools/internal.d.ts +0 -43
  268. package/dist/tools/internal.js +0 -41
  269. package/dist/tools/response-discovery.d.ts +0 -61
  270. package/dist/tools/response-discovery.js +0 -113
  271. package/dist/tools/types.d.ts +0 -337
  272. package/dist/tools/types.js +0 -12
  273. package/dist/tools/variant-resolver.d.ts +0 -41
  274. package/dist/tools/variant-resolver.js +0 -207
@@ -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) => {
@@ -44,7 +45,6 @@ const findCanonicalMathML = (element) => {
44
45
  }
45
46
  return null;
46
47
  };
47
- const isMathContainer = (element) => Boolean(findCanonicalMathML(element));
48
48
  const TEXT_CHUNK_SOURCE_TAGS = new Set([
49
49
  "P",
50
50
  "H1",
@@ -73,7 +73,7 @@ const TEXT_CHUNK_SOURCE_ROLES = new Set([
73
73
  "rowheader",
74
74
  ]);
75
75
  const resolveTextChunkSourceElement = (textNode, root) => {
76
- let current = textNode.parentElement;
76
+ let current = flatTreeParentElement(textNode);
77
77
  let best = null;
78
78
  while (current && current !== root) {
79
79
  const role = (current.getAttribute("role") || "").toLowerCase();
@@ -83,12 +83,12 @@ const resolveTextChunkSourceElement = (textNode, root) => {
83
83
  best = current;
84
84
  break;
85
85
  }
86
- current = current.parentElement;
86
+ current = flatTreeParentElement(current);
87
87
  }
88
88
  return best || root;
89
89
  };
90
90
  const hasMathCandidate = (element) => Boolean(typeof element.querySelector === "function" &&
91
- element.querySelector("math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
91
+ flatQuerySelector(element, "math, [data-mathml], .MathJax, mjx-container, mjx-assistive-mml")) ||
92
92
  element.tagName?.toLowerCase() === "math" ||
93
93
  element.hasAttribute?.("data-mathml") ||
94
94
  isMathJaxElement(element);
@@ -122,10 +122,11 @@ const shouldInsertBoundarySpace = (acc, nextCharacter) => {
122
122
  // visible-text collector, instead of an ASCII-only heuristic.
123
123
  return shouldInsertWordBoundarySpace(previous, nextCharacter, acc.options);
124
124
  };
125
- 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) => {
126
127
  const raw = textNode.textContent || "";
127
128
  let appendedNonWhitespaceInNode = false;
128
- for (let i = 0; i < raw.length; i++) {
129
+ for (let i = start; i < end; i++) {
129
130
  const character = raw[i];
130
131
  const isWhitespace = /\s/.test(character);
131
132
  if (acc.inLeadingWhitespace) {
@@ -223,6 +224,49 @@ const textFallbackFromMathML = (mathml) => {
223
224
  visit(parsed.documentElement);
224
225
  return normalizeTextForSpeech(parts.join(" "));
225
226
  };
227
+ // MathJax CHTML paints its glyphs from CSS, so a typeset equation's only text
228
+ // nodes are the source MathML it keeps, clipped, in mjx-assistive-mml. Mapping
229
+ // the fallback text onto them places a catalog word inside its equation, where
230
+ // the rendered-math resolver finds the glyph. Any divergence between the two
231
+ // texts leaves the fallback unmapped.
232
+ const mapFallbackToAssistiveMath = (element, fallbackText) => {
233
+ const map = new Map();
234
+ const sourceMath = isMathJaxElement(element)
235
+ ? flatQuerySelector(element, "mjx-assistive-mml math")
236
+ : null;
237
+ if (!sourceMath)
238
+ return map;
239
+ const characters = [];
240
+ const visit = (node) => {
241
+ if (node.nodeType === 3) {
242
+ const text = node.textContent || "";
243
+ for (let offset = 0; offset < text.length; offset++) {
244
+ if (!/\s/.test(text[offset])) {
245
+ characters.push({ node: node, offset });
246
+ }
247
+ }
248
+ return;
249
+ }
250
+ if (node.nodeType !== 1 || isAnnotationElement(node))
251
+ return;
252
+ for (const child of flatTreeChildNodes(node)) {
253
+ visit(child);
254
+ }
255
+ };
256
+ visit(sourceMath);
257
+ let next = 0;
258
+ for (let index = 0; index < fallbackText.length; index++) {
259
+ if (/\s/.test(fallbackText[index]))
260
+ continue;
261
+ const mapping = characters[next];
262
+ if (mapping?.node.textContent?.[mapping.offset] !== fallbackText[index]) {
263
+ return new Map();
264
+ }
265
+ map.set(index, mapping);
266
+ next++;
267
+ }
268
+ return next === characters.length ? map : new Map();
269
+ };
226
270
  const collectVisibleMathFallback = (element, canonicalMathML, options) => {
227
271
  const mathAcc = createAccumulator(options);
228
272
  const visit = (node) => {
@@ -239,7 +283,7 @@ const collectVisibleMathFallback = (element, canonicalMathML, options) => {
239
283
  isAnnotationElement(childElement)) {
240
284
  return;
241
285
  }
242
- for (const child of Array.from(childElement.childNodes)) {
286
+ for (const child of flatTreeChildNodes(childElement)) {
243
287
  visit(child);
244
288
  }
245
289
  };
@@ -255,7 +299,7 @@ const collectVisibleMathFallback = (element, canonicalMathML, options) => {
255
299
  }
256
300
  return {
257
301
  text: fallbackText,
258
- map: new Map(),
302
+ map: mapFallbackToAssistiveMath(element, fallbackText),
259
303
  };
260
304
  };
261
305
  const trimTrailingWhitespace = (acc) => {
@@ -265,7 +309,7 @@ const trimTrailingWhitespace = (acc) => {
265
309
  acc.map.delete(acc.position);
266
310
  }
267
311
  };
268
- const collectMathAware = (root, options) => {
312
+ const collectMathAware = (root, options, range) => {
269
313
  const acc = createAccumulator(options);
270
314
  const chunks = [];
271
315
  let textChunkStart = 0;
@@ -286,22 +330,28 @@ const collectMathAware = (root, options) => {
286
330
  const processNode = (node) => {
287
331
  if (isNodeExcludedFromSpeech(node, root))
288
332
  return;
289
- if (node.nodeType === Node.TEXT_NODE) {
333
+ if (range && !rangeIntersectsComposedNode(range, node))
334
+ return;
335
+ if (node.nodeType === 3) {
290
336
  const sourceElement = resolveTextChunkSourceElement(node, root);
291
337
  if (textChunkSourceElement && sourceElement !== textChunkSourceElement) {
292
338
  flushTextChunk();
293
339
  }
294
340
  textChunkSourceElement = sourceElement;
295
- appendTextNode(acc, node);
341
+ const textNode = node;
342
+ appendTextNode(acc, textNode, range?.startContainer === textNode ? range.startOffset : undefined, range?.endContainer === textNode ? range.endOffset : undefined);
296
343
  return;
297
344
  }
298
- if (node.nodeType !== Node.ELEMENT_NODE)
345
+ if (node.nodeType !== 1)
299
346
  return;
300
347
  const element = node;
301
348
  if (isAssistiveMathElement(element))
302
349
  return;
303
350
  const canonicalMathML = findCanonicalMathML(element);
304
- 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))) {
305
355
  flushTextChunk();
306
356
  const collected = collectVisibleMathFallback(element, canonicalMathML, options);
307
357
  appendCollectedText(acc, collected);
@@ -316,11 +366,11 @@ const collectMathAware = (root, options) => {
316
366
  containsMathMarkup = true;
317
367
  return;
318
368
  }
319
- for (const child of Array.from(element.childNodes)) {
369
+ for (const child of flatTreeChildNodes(element)) {
320
370
  processNode(child);
321
371
  }
322
372
  };
323
- for (const child of Array.from(root.childNodes)) {
373
+ for (const child of flatTreeChildNodes(root)) {
324
374
  processNode(child);
325
375
  }
326
376
  trimTrailingWhitespace(acc);
@@ -336,7 +386,13 @@ const collectMathAware = (root, options) => {
336
386
  containsMathMarkup,
337
387
  };
338
388
  };
339
- 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);
340
396
  if (!hasMathCandidate(element)) {
341
397
  const { text, map } = collectVisibleTextAndMap(element, options);
342
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
+ }
@@ -25,7 +25,11 @@ export interface SREMathSpeechOptions {
25
25
  domain?: string;
26
26
  /** Raw SRE style/preference string. ClearSpeak preferences are ":"-joined. */
27
27
  style?: string;
28
- /** Extra SRE setupEngine options for hosts that need a newly exposed SRE knob. */
28
+ /**
29
+ * Extra SRE setupEngine options for hosts that need a newly exposed SRE knob.
30
+ * In a browser, SRE's locale tables load from the host's bundle unless `json`
31
+ * (a base URL or path) or `custom` (a loader) names another source.
32
+ */
29
33
  engineOptions?: Record<string, unknown>;
30
34
  }
31
35
  export interface ResolvedMathSpeech {
@@ -39,6 +43,17 @@ export interface ResolvedMathSpeech {
39
43
  */
40
44
  ssml?: string;
41
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>);
42
57
  export declare const normalizeSREMathSpeechOptions: (options: unknown) => SREMathSpeechOptions | undefined;
43
58
  export declare const resolveMathSpeechFromChunks: (chunks: MathAwareSpeechChunk[], options?: ResolveMathSpeechOptions) => Promise<ResolvedMathSpeech>;
44
59
  export {};
@@ -1,19 +1,53 @@
1
+ import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
2
+ import { setSreStartupLocaleSource, sreLocaleSource } from "./sre-locales.js";
1
3
  import { normalizeTextForSpeech } from "./text-processing.js";
2
- 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);
3
6
  let sreOperationQueue = Promise.resolve();
4
7
  const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
5
- const defaultLoadSre = async () => {
6
- if (!sreLoadPromise) {
7
- sreLoadPromise = import("speech-rule-engine").then((module) => {
8
- const candidate = module.default ||
9
- module;
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
@@ -41,13 +75,15 @@ export const normalizeSREMathSpeechOptions = (options) => {
41
75
  const setupSre = async (sre, language, markup, mathSpeech) => {
42
76
  const locale = normalizeLocale(language);
43
77
  const normalizedMathSpeech = normalizeSREMathSpeechOptions(mathSpeech);
78
+ const engineOptions = normalizedMathSpeech?.engineOptions || {};
44
79
  await sre.setupEngine?.({
45
- ...(normalizedMathSpeech?.engineOptions || {}),
80
+ ...sreLocaleSource(engineOptions),
81
+ ...engineOptions,
46
82
  locale,
47
83
  domain: normalizedMathSpeech?.domain || domainForLocale(locale),
48
- ...(normalizedMathSpeech?.style
49
- ? { style: normalizedMathSpeech.style }
50
- : {}),
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",
51
87
  modality: "speech",
52
88
  markup,
53
89
  });
@@ -78,7 +114,7 @@ const generateMathSsml = async (sre, mathml, language, mathSpeech) => {
78
114
  return ssml.includes("<speak") ? ssml : null;
79
115
  }
80
116
  catch (error) {
81
- console.debug("[TTSService] Math SSML generation failed; using plain", {
117
+ logger.debug("math SSML generation failed; using plain", {
82
118
  message: error instanceof Error ? error.message : String(error),
83
119
  });
84
120
  return null;
@@ -97,7 +133,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
97
133
  let sre = null;
98
134
  let usedMathSpeech = false;
99
135
  let usedFallback = false;
100
- const loadSre = options.loadSre || defaultLoadSre;
136
+ const loadSre = options.loadSre || createReadSreLoad(options.mathSpeech);
101
137
  const speechParts = [];
102
138
  for (const chunk of chunks) {
103
139
  if (chunk.type === "text") {
@@ -119,7 +155,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
119
155
  }
120
156
  catch (error) {
121
157
  usedFallback = true;
122
- console.debug("[TTSService] Math speech generation failed; using visible fallback", {
158
+ logger.debug("math speech generation failed; using visible fallback", {
123
159
  message: error instanceof Error ? error.message : String(error),
124
160
  });
125
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 {};
@@ -0,0 +1,7 @@
1
+ import * as speechRuleEngine from "speech-rule-engine";
2
+ declare const sre: {
3
+ setupEngine: typeof speechRuleEngine.setupEngine;
4
+ engineReady: typeof speechRuleEngine.engineReady;
5
+ toSpeech: typeof speechRuleEngine.toSpeech;
6
+ };
7
+ export default sre;
@@ -0,0 +1,11 @@
1
+ import * as speechRuleEngine from "speech-rule-engine";
2
+ import { sreStartupLocaleSource } from "./sre-locales.js";
3
+ const sre = speechRuleEngine.default ?? speechRuleEngine;
4
+ // SRE starts itself up while this module's import of it evaluates, then loads
5
+ // `base` and `en` a few microtasks later from whatever locale source is set by
6
+ // then. In a bundle this call runs synchronously after that evaluation, so it
7
+ // lands first; configuring SRE after awaiting an `import()` of it lands after
8
+ // those loads. Bun drains microtasks between evaluating a CommonJS module and
9
+ // its importer, so there SRE reads `base` from its own package directory.
10
+ void sre.setupEngine(sreStartupLocaleSource());
11
+ export default sre;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The `setupEngine` options naming where SRE reads locale tables from: the
3
+ * host's `json` (a base URL or path) or `custom` (a loader) when its
4
+ * `engineOptions` set either, otherwise the packaged tables wherever SRE would
5
+ * fetch them. SRE prefers `custom` over `json`, so the packaged loader is left
6
+ * out whenever the host names a source.
7
+ */
8
+ export declare const sreLocaleSource: (engineOptions?: Record<string, unknown>) => Record<string, unknown>;
9
+ /**
10
+ * Sets the source SRE loads its start-up locales from; see `./sre-engine.ts`.
11
+ * Only a call made before that module first loads has any effect.
12
+ */
13
+ export declare const setSreStartupLocaleSource: (engineOptions?: Record<string, unknown>) => void;
14
+ export declare const sreStartupLocaleSource: () => Record<string, unknown>;
@@ -0,0 +1,57 @@
1
+ // SRE loads each locale's rule tables at runtime, from a public CDN unless it is
2
+ // handed a source. An import per table lets the host's bundler emit each as a
3
+ // lazy chunk served from the host's own origin, but a build that inlines every
4
+ // `import()` into one file carries every table listed here, at 0.2 to 0.4 MB
5
+ // each. So only `base`, which every locale builds on, and the `en` and `es`
6
+ // locales are packaged; any other locale needs a source from the host's
7
+ // `engineOptions`. No `with { type: "json" }`: bundlers infer JSON from the
8
+ // extension, while Vite's dev server and Bun's bundler turn a table into
9
+ // JavaScript and keep the attribute, which a browser then rejects.
10
+ const SRE_LOCALE_TABLES = {
11
+ base: () => import("speech-rule-engine/lib/mathmaps/base.json"),
12
+ en: () => import("speech-rule-engine/lib/mathmaps/en.json"),
13
+ es: () => import("speech-rule-engine/lib/mathmaps/es.json"),
14
+ };
15
+ // SRE's `custom` loader contract: called with a locale name, resolving to that
16
+ // locale's table as a JSON string or an already-parsed object. SRE falls back
17
+ // to English when a table fails to load.
18
+ const loadPackagedSreLocale = async (locale) => {
19
+ if (!Object.hasOwn(SRE_LOCALE_TABLES, locale)) {
20
+ throw new Error(`no speech-rule-engine locale table is packaged for "${locale}"; name a source with mathSpeech.engineOptions.json or .custom`);
21
+ }
22
+ return (await SRE_LOCALE_TABLES[locale]()).default;
23
+ };
24
+ // SRE's own test for fetching tables over HTTP (`common/system_external.js`).
25
+ // Elsewhere it reads them from its package directory, which Node's ESM loader
26
+ // could not replace with the imports above: it requires the attribute.
27
+ const sreFetchesTables = () => (typeof window !== "undefined" && typeof window.document !== "undefined") ||
28
+ typeof globalThis
29
+ .DedicatedWorkerGlobalScope !== "undefined";
30
+ /**
31
+ * The `setupEngine` options naming where SRE reads locale tables from: the
32
+ * host's `json` (a base URL or path) or `custom` (a loader) when its
33
+ * `engineOptions` set either, otherwise the packaged tables wherever SRE would
34
+ * fetch them. SRE prefers `custom` over `json`, so the packaged loader is left
35
+ * out whenever the host names a source.
36
+ */
37
+ export const sreLocaleSource = (engineOptions = {}) => {
38
+ const { json, custom } = engineOptions;
39
+ if (json || custom) {
40
+ return { ...(json ? { json } : {}), ...(custom ? { custom } : {}) };
41
+ }
42
+ return sreFetchesTables() ? { custom: loadPackagedSreLocale } : {};
43
+ };
44
+ // Resolved on first read: a call at module scope would keep the table imports
45
+ // in every bundle that includes this module, math speech used or not.
46
+ let startupLocaleSource;
47
+ /**
48
+ * Sets the source SRE loads its start-up locales from; see `./sre-engine.ts`.
49
+ * Only a call made before that module first loads has any effect.
50
+ */
51
+ export const setSreStartupLocaleSource = (engineOptions) => {
52
+ startupLocaleSource = sreLocaleSource(engineOptions);
53
+ };
54
+ export const sreStartupLocaleSource = () => {
55
+ startupLocaleSource ??= sreLocaleSource();
56
+ return startupLocaleSource;
57
+ };