@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
@@ -5,7 +5,10 @@
5
5
  * Used by both TTS (for timing data) and annotations (for persistence).
6
6
  *
7
7
  * Uses CSS selector paths and node indices for robust serialization that
8
- * survives content changes when possible.
8
+ * survives content changes when possible. A path into an open shadow root names
9
+ * its host, then ` >>> `, then the path inside the root, so ranges in content
10
+ * that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
11
+ * selector path.
9
12
  */
10
13
  /**
11
14
  * Serialized form of a Range suitable for storage.
@@ -77,42 +80,17 @@ export declare class RangeSerializer {
77
80
  * Uses a hybrid approach:
78
81
  * - For element nodes: CSS selector path
79
82
  * - For text nodes: parent selector + text node index
80
- *
81
- * @param node - Node to get path for
82
- * @param root - Root element
83
- * @returns Path string
84
83
  */
85
84
  private getNodePath;
86
- /**
87
- * Get CSS selector path to an element.
88
- *
89
- * @param element - Element to get path for
90
- * @param root - Root element
91
- * @returns CSS selector path
92
- */
85
+ /** Get CSS selector path to an element. */
93
86
  private getElementPath;
87
+ /** The path to `shadowRoot` itself: its host's path and an empty step into it. */
88
+ private getShadowRootPath;
94
89
  /**
95
- * Find a node by its path from root.
96
- *
97
- * @param path - Path string from getNodePath
98
- * @param root - Root element
99
- * @returns Node, or null if not found
90
+ * The element, or shadow root, an element path names: each step after a
91
+ * {@link SHADOW_STEP} continues in the shadow root of what the step before named.
100
92
  */
93
+ private resolveElementPath;
94
+ /** The node a getNodePath path names, or null if not found. */
101
95
  private findNodeByPath;
102
- /**
103
- * Batch serialize multiple ranges.
104
- *
105
- * @param ranges - Ranges to serialize
106
- * @param root - Root element
107
- * @returns Array of serialized ranges
108
- */
109
- serializeMany(ranges: Range[], root: Element): SerializedRange[];
110
- /**
111
- * Batch deserialize multiple ranges.
112
- *
113
- * @param data - Array of serialized ranges
114
- * @param root - Root element
115
- * @returns Array of ranges (nulls for invalid ranges)
116
- */
117
- deserializeMany(data: SerializedRange[], root: Element): (Range | null)[];
118
96
  }
@@ -5,8 +5,14 @@
5
5
  * Used by both TTS (for timing data) and annotations (for persistence).
6
6
  *
7
7
  * Uses CSS selector paths and node indices for robust serialization that
8
- * survives content changes when possible.
8
+ * survives content changes when possible. A path into an open shadow root names
9
+ * its host, then ` >>> `, then the path inside the root, so ranges in content
10
+ * that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
11
+ * selector path.
9
12
  */
13
+ import { isShadowRootNode } from "./tts/flat-tree.js";
14
+ /** Separates a shadow host's path from the path inside its shadow root. */
15
+ const SHADOW_STEP = " >>> ";
10
16
  /**
11
17
  * RangeSerializer - Serialize and deserialize Range objects.
12
18
  */
@@ -64,11 +70,9 @@ export class RangeSerializer {
64
70
  const range = new Range();
65
71
  range.setStart(startNode, data.startOffset);
66
72
  range.setEnd(endNode, data.endOffset);
67
- // Validate text hasn't changed
68
73
  if (range.toString() === data.text) {
69
74
  return range;
70
75
  }
71
- // Text changed, range is invalid
72
76
  return null;
73
77
  }
74
78
  catch (error) {
@@ -83,116 +87,141 @@ export class RangeSerializer {
83
87
  * Uses a hybrid approach:
84
88
  * - For element nodes: CSS selector path
85
89
  * - For text nodes: parent selector + text node index
86
- *
87
- * @param node - Node to get path for
88
- * @param root - Root element
89
- * @returns Path string
90
90
  */
91
91
  getNodePath(node, root) {
92
92
  if (node === root) {
93
93
  return "";
94
94
  }
95
- // Handle text nodes
96
95
  if (node.nodeType === Node.TEXT_NODE) {
97
- const parent = node.parentElement;
96
+ const parent = node.parentNode;
98
97
  if (!parent) {
99
98
  throw new Error("Text node has no parent");
100
99
  }
101
- // Get index of this text node among its siblings
102
100
  const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
103
101
  const index = textNodes.indexOf(node);
104
- const parentPath = this.getElementPath(parent, root);
102
+ const parentPath = isShadowRootNode(parent)
103
+ ? this.getShadowRootPath(parent, root)
104
+ : this.getElementPath(parent, root);
105
105
  return `${parentPath}::text[${index}]`;
106
106
  }
107
- // Handle element nodes
108
107
  if (node.nodeType === Node.ELEMENT_NODE) {
109
108
  return this.getElementPath(node, root);
110
109
  }
110
+ if (isShadowRootNode(node)) {
111
+ return this.getShadowRootPath(node, root);
112
+ }
111
113
  throw new Error(`Unsupported node type: ${node.nodeType}`);
112
114
  }
113
- /**
114
- * Get CSS selector path to an element.
115
- *
116
- * @param element - Element to get path for
117
- * @param root - Root element
118
- * @returns CSS selector path
119
- */
115
+ /** Get CSS selector path to an element. */
120
116
  getElementPath(element, root) {
121
117
  if (element === root) {
122
118
  return "";
123
119
  }
124
- const path = [];
120
+ const rootTree = root.getRootNode?.();
121
+ // One path per tree, outermost first.
122
+ const trees = [];
123
+ let path = [];
125
124
  let current = element;
125
+ // Ends `current`'s tree at the shadow root holding it, continuing at the host.
126
+ const leaveShadowRoot = (shadowRoot) => {
127
+ trees.unshift(path.join(" > "));
128
+ path = [];
129
+ current = shadowRoot.host;
130
+ };
126
131
  while (current && current !== root) {
127
- // Use ID if available (more stable)
132
+ // Use ID if available (more stable). An id is unique within its tree, so
133
+ // the path still names the shadow host above it.
128
134
  if (current.id) {
129
135
  path.unshift(`#${current.id}`);
136
+ const tree = current.getRootNode?.();
137
+ if (isShadowRootNode(tree) && tree !== rootTree) {
138
+ leaveShadowRoot(tree);
139
+ continue;
140
+ }
130
141
  break;
131
142
  }
132
143
  // Otherwise use tag + nth-of-type
133
144
  const parent = current.parentElement;
134
- if (!parent)
145
+ const parentNode = current.parentNode;
146
+ const siblingsParent = parent ?? (isShadowRootNode(parentNode) ? parentNode : null);
147
+ if (!siblingsParent)
135
148
  break;
136
- const siblings = Array.from(parent.children).filter((el) => el.tagName === current.tagName);
149
+ const tagName = current.tagName;
150
+ const siblings = Array.from(siblingsParent.children).filter((el) => el.tagName === tagName);
137
151
  const index = siblings.indexOf(current);
138
152
  const selector = siblings.length > 1
139
- ? `${current.tagName.toLowerCase()}:nth-of-type(${index + 1})`
140
- : current.tagName.toLowerCase();
153
+ ? `${tagName.toLowerCase()}:nth-of-type(${index + 1})`
154
+ : tagName.toLowerCase();
141
155
  path.unshift(selector);
142
- current = parent;
156
+ if (parent) {
157
+ current = parent;
158
+ }
159
+ else {
160
+ leaveShadowRoot(siblingsParent);
161
+ }
143
162
  }
144
- return path.join(" > ");
163
+ trees.unshift(path.join(" > "));
164
+ return trees.join(SHADOW_STEP);
165
+ }
166
+ /** The path to `shadowRoot` itself: its host's path and an empty step into it. */
167
+ getShadowRootPath(shadowRoot, root) {
168
+ return `${this.getElementPath(shadowRoot.host, root)}${SHADOW_STEP}`;
145
169
  }
146
170
  /**
147
- * Find a node by its path from root.
148
- *
149
- * @param path - Path string from getNodePath
150
- * @param root - Root element
151
- * @returns Node, or null if not found
171
+ * The element, or shadow root, an element path names: each step after a
172
+ * {@link SHADOW_STEP} continues in the shadow root of what the step before named.
152
173
  */
174
+ resolveElementPath(path, root) {
175
+ let scope = root;
176
+ const steps = path.split(SHADOW_STEP);
177
+ for (let index = 0; index < steps.length; index++) {
178
+ if (index > 0) {
179
+ const shadowRoot = isShadowRootNode(scope)
180
+ ? null
181
+ : scope.shadowRoot;
182
+ if (!shadowRoot)
183
+ return null;
184
+ scope = shadowRoot;
185
+ }
186
+ const selector = steps[index].trim();
187
+ if (!selector)
188
+ continue;
189
+ const found = scope.querySelector(selector);
190
+ if (!found)
191
+ return null;
192
+ scope = found;
193
+ }
194
+ return scope;
195
+ }
196
+ /** The node a getNodePath path names, or null if not found. */
153
197
  findNodeByPath(path, root) {
154
198
  if (path === "") {
155
199
  return root;
156
200
  }
157
- // Handle text node paths
158
201
  if (path.includes("::text[")) {
159
- const [elementPath, textPart] = path.split("::text[");
202
+ const marker = path.lastIndexOf("::text[");
203
+ const elementPath = path.slice(0, marker);
204
+ const textPart = path.slice(marker + "::text[".length);
160
205
  const textIndex = Number.parseInt(textPart.replace("]", ""), 10);
161
- // Find parent element
162
- const parent = elementPath ? root.querySelector(elementPath) : root;
206
+ let parent;
207
+ try {
208
+ parent = this.resolveElementPath(elementPath, root);
209
+ }
210
+ catch (error) {
211
+ console.warn("Invalid selector path:", path, error);
212
+ return null;
213
+ }
163
214
  if (!parent)
164
215
  return null;
165
- // Find text node by index
166
216
  const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
167
217
  return textNodes[textIndex] || null;
168
218
  }
169
- // Handle element paths
170
219
  try {
171
- return root.querySelector(path);
220
+ return this.resolveElementPath(path, root);
172
221
  }
173
222
  catch (error) {
174
223
  console.warn("Invalid selector path:", path, error);
175
224
  return null;
176
225
  }
177
226
  }
178
- /**
179
- * Batch serialize multiple ranges.
180
- *
181
- * @param ranges - Ranges to serialize
182
- * @param root - Root element
183
- * @returns Array of serialized ranges
184
- */
185
- serializeMany(ranges, root) {
186
- return ranges.map((range) => this.serialize(range, root));
187
- }
188
- /**
189
- * Batch deserialize multiple ranges.
190
- *
191
- * @param data - Array of serialized ranges
192
- * @param root - Root element
193
- * @returns Array of ranges (nulls for invalid ranges)
194
- */
195
- deserializeMany(data, root) {
196
- return data.map((item) => this.deserialize(item, root));
197
- }
198
227
  }
@@ -13,8 +13,6 @@
13
13
  *
14
14
  * This enables proper pronunciation, emphasis, and pacing for TTS without
15
15
  * requiring authors to maintain separate catalog files.
16
- *
17
- * Part of PIE Assessment Toolkit.
18
16
  */
19
17
  import type { AccessibilityCatalog, ConfigEntity } from "@pie-players/pie-players-shared/types";
20
18
  /**
@@ -25,19 +23,11 @@ export interface ExtractionResult {
25
23
  cleanedConfig: ConfigEntity;
26
24
  }
27
25
  /**
28
- * Sanitize an SSML subtree in place: remove disallowed attributes and
29
- * replace disallowed elements with their text content (so phrasing order
30
- * is preserved but no scripts / iframes / unknown tags survive).
31
- */
32
- export declare function sanitizeSsmlElement(element: Element): void;
33
- /**
34
- * Serialize a sanitized `<speak>` element back to a minimal SSML string.
35
- * Uses a manual walker instead of `outerHTML` so that any namespace
36
- * quirks (e.g. `xmlns` attributes added by the HTML parser) are
37
- * normalised and disallowed descendants cannot reappear through quirks
38
- * in the serializer.
26
+ * SSML cut down to the allow-listed tags and attributes. Input with no markup,
27
+ * or with no DOMParser available, comes back unchanged. Otherwise the first
28
+ * <speak> element comes back sanitized, or, when there is none, the sanitized
29
+ * fragment with no <speak> wrapper.
39
30
  */
40
- export declare function serializeSsmlElement(element: Element): string;
41
31
  export declare function sanitizeSsmlString(input: string): string;
42
32
  /**
43
33
  * SSMLExtractor Service
@@ -46,26 +36,13 @@ export declare function sanitizeSsmlString(input: string): string;
46
36
  */
47
37
  export declare class SSMLExtractor {
48
38
  private catalogCounter;
49
- /**
50
- * Extract SSML from entire item config (markup + models)
51
- *
52
- * @param config Item configuration with potential embedded SSML
53
- * @returns Extracted catalogs and cleaned config
54
- */
39
+ /** Extract SSML from the item's markup, prompts and choice labels. */
55
40
  extractFromItemConfig(config: ConfigEntity): ExtractionResult;
56
- /**
57
- * Extract SSML from markup string
58
- *
59
- * @param markup HTML markup potentially containing <speak> elements
60
- * @param idPrefix Prefix for generating catalog IDs
61
- * @returns Extracted catalogs and cleaned markup
62
- */
41
+ /** Extract SSML from one markup string. */
63
42
  private extractFromMarkup;
64
43
  /**
65
- * Generate unique catalog ID
66
- *
67
- * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')
68
- * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')
44
+ * A catalog id unique to this extractor until reset(), e.g.
45
+ * `auto-prompt-q1-0`.
69
46
  */
70
47
  private generateCatalogId;
71
48
  /**
@@ -13,8 +13,6 @@
13
13
  *
14
14
  * This enables proper pronunciation, emphasis, and pacing for TTS without
15
15
  * requiring authors to maintain separate catalog files.
16
- *
17
- * Part of PIE Assessment Toolkit.
18
16
  */
19
17
  /**
20
18
  * Allow-listed SSML elements (from the SSML 1.1 spec subset we forward to
@@ -38,11 +36,6 @@ const SSML_ALLOWED_TAGS = new Set([
38
36
  "audio",
39
37
  "lexicon",
40
38
  ]);
41
- /**
42
- * Allow-listed attributes across SSML elements. Attribute allow-listing is
43
- * global (not per-tag) because the union is small and consistent across
44
- * providers.
45
- */
46
39
  /**
47
40
  * Tags whose content must be dropped entirely (rather than unwrapped to
48
41
  * preserve phrasing text). These are dangerous containers whose text
@@ -58,6 +51,10 @@ const SSML_DROP_FULLY_TAGS = new Set([
58
51
  "noscript",
59
52
  "template",
60
53
  ]);
54
+ /**
55
+ * Allow-listed attributes across SSML elements. One list covers every tag
56
+ * because the union is small and consistent across providers.
57
+ */
61
58
  const SSML_ALLOWED_ATTRS = new Set([
62
59
  "alphabet",
63
60
  "ph",
@@ -153,7 +150,7 @@ function isSafeSsmlAudioSrc(raw) {
153
150
  * replace disallowed elements with their text content (so phrasing order
154
151
  * is preserved but no scripts / iframes / unknown tags survive).
155
152
  */
156
- export function sanitizeSsmlElement(element) {
153
+ function sanitizeSsmlElement(element) {
157
154
  // Walk children snapshot first because we may replace nodes as we go.
158
155
  const children = Array.from(element.children);
159
156
  for (const child of children) {
@@ -175,10 +172,8 @@ export function sanitizeSsmlElement(element) {
175
172
  element.removeAttribute(attrName);
176
173
  }
177
174
  }
178
- // Belt-and-braces: even though `src` is allow-listed, SSML `<audio>`
179
- // sends its URL to a server-side TTS provider that will dereference
180
- // it. Refuse non-http(s) schemes and private / cloud-metadata hosts
181
- // at the SSML boundary so we never forward an SSRF primitive.
175
+ // `src` survives the attribute allow-list, and a server-side provider
176
+ // fetches it: keep it only for an http(s) URL off SSML_PRIVATE_AUDIO_HOSTS.
182
177
  if (element.tagName.toLowerCase() === "audio") {
183
178
  const src = element.getAttribute("src");
184
179
  if (src !== null && !isSafeSsmlAudioSrc(src)) {
@@ -193,7 +188,7 @@ export function sanitizeSsmlElement(element) {
193
188
  * normalised and disallowed descendants cannot reappear through quirks
194
189
  * in the serializer.
195
190
  */
196
- export function serializeSsmlElement(element) {
191
+ function serializeSsmlElement(element) {
197
192
  const tagName = element.tagName.toLowerCase();
198
193
  const attrs = Array.from(element.attributes)
199
194
  .filter((attr) => SSML_ALLOWED_ATTRS.has(attr.name.toLowerCase()))
@@ -227,6 +222,12 @@ export function serializeSsmlElement(element) {
227
222
  }
228
223
  return `<${tagName}${attrs}>${childPieces.join("")}</${tagName}>`;
229
224
  }
225
+ /**
226
+ * SSML cut down to the allow-listed tags and attributes. Input with no markup,
227
+ * or with no DOMParser available, comes back unchanged. Otherwise the first
228
+ * <speak> element comes back sanitized, or, when there is none, the sanitized
229
+ * fragment with no <speak> wrapper.
230
+ */
230
231
  export function sanitizeSsmlString(input) {
231
232
  const trimmed = String(input || "").trim();
232
233
  if (!trimmed || !trimmed.includes("<"))
@@ -281,12 +282,7 @@ function escapeXmlAttr(value) {
281
282
  */
282
283
  export class SSMLExtractor {
283
284
  catalogCounter = 0;
284
- /**
285
- * Extract SSML from entire item config (markup + models)
286
- *
287
- * @param config Item configuration with potential embedded SSML
288
- * @returns Extracted catalogs and cleaned config
289
- */
285
+ /** Extract SSML from the item's markup, prompts and choice labels. */
290
286
  extractFromItemConfig(config) {
291
287
  const allCatalogs = [];
292
288
  // Shallow clone of config + a fresh models array. The per-model/choice
@@ -336,18 +332,11 @@ export class SSMLExtractor {
336
332
  cleanedConfig,
337
333
  };
338
334
  }
339
- /**
340
- * Extract SSML from markup string
341
- *
342
- * @param markup HTML markup potentially containing <speak> elements
343
- * @param idPrefix Prefix for generating catalog IDs
344
- * @returns Extracted catalogs and cleaned markup
345
- */
335
+ /** Extract SSML from one markup string. */
346
336
  extractFromMarkup(markup, idPrefix) {
347
337
  if (!markup || markup.trim() === "") {
348
338
  return { catalogs: [], cleanedMarkup: markup };
349
339
  }
350
- // Check if running in browser environment
351
340
  if (typeof window === "undefined" || typeof DOMParser === "undefined") {
352
341
  console.warn("[SSMLExtractor] DOMParser not available (SSR?), skipping extraction");
353
342
  return { catalogs: [], cleanedMarkup: markup };
@@ -356,16 +345,12 @@ export class SSMLExtractor {
356
345
  try {
357
346
  const parser = new DOMParser();
358
347
  const doc = parser.parseFromString(expandSelfClosingVoidTags(markup), "text/html");
359
- // Find all <speak> elements
360
348
  const speakElements = Array.from(doc.querySelectorAll("speak"));
361
349
  if (speakElements.length === 0) {
362
- // No SSML found, return original markup
363
350
  return { catalogs: [], cleanedMarkup: markup };
364
351
  }
365
- // Process each <speak> element
366
352
  for (const speakEl of speakElements) {
367
353
  try {
368
- // Generate unique catalog ID
369
354
  const catalogId = this.generateCatalogId(idPrefix);
370
355
  // Sanitize before serializing so the SSML forwarded to the TTS
371
356
  // provider only contains spec-allowed tags/attributes. This
@@ -373,7 +358,6 @@ export class SSMLExtractor {
373
358
  // otherwise accept arbitrary HTML.
374
359
  sanitizeSsmlElement(speakEl);
375
360
  const ssmlContent = serializeSsmlElement(speakEl);
376
- // Extract language (check both xml:lang and lang attributes)
377
361
  const language = speakEl.getAttribute("xml:lang") ||
378
362
  speakEl.getAttribute("lang") ||
379
363
  "en-US";
@@ -409,7 +393,6 @@ export class SSMLExtractor {
409
393
  else {
410
394
  wrapper.setAttribute("data-catalog-idref", catalogId);
411
395
  }
412
- // Create catalog entry
413
396
  catalogs.push({
414
397
  identifier: catalogId,
415
398
  cards: [
@@ -423,24 +406,19 @@ export class SSMLExtractor {
423
406
  }
424
407
  catch (error) {
425
408
  console.error("[SSMLExtractor] Error processing <speak> element:", error);
426
- // Continue with other elements
427
409
  }
428
410
  }
429
- // Return cleaned markup
430
411
  const cleanedMarkup = doc.body.innerHTML;
431
412
  return { catalogs, cleanedMarkup };
432
413
  }
433
414
  catch (error) {
434
415
  console.error("[SSMLExtractor] Error parsing markup:", error);
435
- // Return original markup if parsing fails
436
416
  return { catalogs: [], cleanedMarkup: markup };
437
417
  }
438
418
  }
439
419
  /**
440
- * Generate unique catalog ID
441
- *
442
- * @param prefix Context prefix (e.g., 'prompt-q1', 'choice-q1-a')
443
- * @returns Unique catalog ID (e.g., 'auto-prompt-q1-0')
420
+ * A catalog id unique to this extractor until reset(), e.g.
421
+ * `auto-prompt-q1-0`.
444
422
  */
445
423
  generateCatalogId(prefix) {
446
424
  const id = `auto-${prefix}-${this.catalogCounter}`;