@pie-players/pie-assessment-toolkit 0.3.75 → 1.0.0-beta.1
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.
- package/README.md +451 -541
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +0 -1
- package/dist/attempt/TestSession.js +10 -5
- package/dist/components/ItemScope.custom-element.js +1 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +8 -8
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/{ItemToolBar-7tt7v2xp.js → ItemToolBar-3zrc4p3d.js} +4 -4
- package/dist/components/chunks/{ItemToolBar-jx38w99s.js → ItemToolBar-66hcwmfw.js} +2 -2
- package/dist/components/chunks/ItemToolBar-asenkvx6.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-97hx60z4.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/context/assessment-toolkit-context.d.ts +6 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +4 -10
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +46 -7
- package/dist/policy/core/ToolPolicyEngine.js +73 -16
- package/dist/policy/core/compose-decision.d.ts +3 -3
- package/dist/policy/core/compose-decision.js +32 -30
- package/dist/policy/core/decision-types.d.ts +51 -34
- package/dist/policy/core/feature-decision.d.ts +17 -15
- package/dist/policy/core/feature-decision.js +11 -13
- package/dist/policy/core/pnp-policy-inputs.d.ts +7 -3
- package/dist/policy/core/pnp-policy-inputs.js +17 -33
- package/dist/policy/core/provenance.d.ts +2 -2
- package/dist/policy/core/tool-parameters.d.ts +12 -0
- package/dist/policy/core/tool-parameters.js +27 -0
- package/dist/policy/engine.d.ts +1 -1
- package/dist/policy/sources/PnpPolicySource.d.ts +18 -24
- package/dist/policy/sources/PnpPolicySource.js +57 -29
- package/dist/runtime/SectionControllerBinding.d.ts +6 -2
- package/dist/runtime/SectionControllerBinding.js +21 -15
- package/dist/runtime/adapter/subscriber-fanout.d.ts +0 -1
- package/dist/runtime/adapter/subscriber-fanout.js +1 -4
- package/dist/runtime/composition-emit-scheduler.d.ts +3 -3
- package/dist/runtime/composition-emit-scheduler.js +1 -1
- package/dist/runtime/content-region.d.ts +0 -4
- package/dist/runtime/content-region.js +2 -2
- package/dist/runtime/core/SectionEngineCore.d.ts +0 -2
- package/dist/runtime/core/SectionEngineCore.js +0 -4
- package/dist/runtime/core/cohort.d.ts +0 -5
- package/dist/runtime/core/cohort.js +0 -9
- package/dist/runtime/core/engine-output.d.ts +3 -2
- package/dist/runtime/core/engine-transition.js +3 -2
- package/dist/runtime/registration-events.d.ts +11 -14
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-scope.d.ts +4 -4
- package/dist/runtime/shell-scope.js +15 -9
- package/dist/runtime/tool-host-contract.d.ts +0 -4
- package/dist/runtime/tool-host-contract.js +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +32 -61
- package/dist/services/ElementToolStateStore.js +37 -67
- package/dist/services/HighlightCoordinator.d.ts +9 -21
- package/dist/services/HighlightCoordinator.js +52 -80
- package/dist/services/RangeSerializer.d.ts +4 -35
- package/dist/services/RangeSerializer.js +4 -48
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +51 -57
- package/dist/services/TTSService.js +323 -284
- package/dist/services/ToolCoordinator.d.ts +6 -49
- package/dist/services/ToolCoordinator.js +25 -61
- package/dist/services/ToolRegistry.d.ts +83 -80
- package/dist/services/ToolRegistry.js +91 -67
- package/dist/services/ToolkitCoordinator.d.ts +83 -81
- package/dist/services/ToolkitCoordinator.js +186 -174
- package/dist/services/framework-error.d.ts +30 -7
- package/dist/services/framework-error.js +10 -13
- package/dist/services/interfaces.d.ts +51 -71
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.js +6 -8
- package/dist/services/spoken-audio-cards.d.ts +2 -4
- package/dist/services/spoken-audio-cards.js +2 -4
- package/dist/services/tool-config-validation.d.ts +5 -7
- package/dist/services/tool-config-validation.js +11 -26
- package/dist/services/tool-context.d.ts +39 -24
- package/dist/services/tool-context.js +158 -92
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +1 -2
- package/dist/services/tool-icons.js +2 -2
- package/dist/services/tool-instance-id.d.ts +8 -6
- package/dist/services/tool-instance-id.js +12 -18
- package/dist/services/tool-providers/TTSToolProvider.d.ts +6 -43
- package/dist/services/tool-providers/TTSToolProvider.js +7 -63
- package/dist/services/tool-providers/ToolProviderApi.d.ts +0 -7
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +7 -42
- package/dist/services/tool-providers/ToolProviderRegistry.js +21 -63
- package/dist/services/tool-request.d.ts +4 -0
- package/dist/services/tool-request.js +7 -2
- package/dist/services/toolbar-decided-tools.d.ts +2 -0
- package/dist/services/toolbar-decided-tools.js +2 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +18 -9
- package/dist/services/tools-config-normalizer.js +25 -19
- package/dist/services/tts/browser-provider.d.ts +2 -5
- package/dist/services/tts/browser-provider.js +13 -22
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +0 -7
- package/dist/services/tts/flat-tree.js +2 -2
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +10 -12
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.js +10 -29
- package/dist/services/tts/math-speech.d.ts +1 -1
- package/dist/services/tts/math-speech.js +3 -0
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +12 -1
- package/dist/services/tts/ssml/spoken-text.js +1 -1
- package/dist/services/tts/start-failure.d.ts +0 -7
- package/dist/services/tts/start-failure.js +1 -1
- package/dist/services/tts/text-segmentation.d.ts +5 -0
- package/dist/services/tts/text-segmentation.js +13 -10
- package/dist/services/tts-runtime-config.d.ts +32 -12
- package/dist/services/tts-runtime-config.js +14 -20
- package/dist/tools/content-capability-resolution.d.ts +2 -2
- package/dist/tools/registration.d.ts +7 -6
- package/dist/tools/registration.js +10 -9
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +6 -0
- package/dist/tools/tool-surface-host.js +1 -8
- package/package.json +8 -6
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +0 -2
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +0 -40
- package/dist/components/chunks/sre-engine-jd8cc08x.js +0 -1
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -11,8 +11,6 @@
|
|
|
11
11
|
* - Configurable colors and styles
|
|
12
12
|
* - Annotation persistence via RangeSerializer
|
|
13
13
|
*
|
|
14
|
-
* Part of PIE Assessment Toolkit.
|
|
15
|
-
*
|
|
16
14
|
* Browser Support:
|
|
17
15
|
* - Chrome/Edge 105+
|
|
18
16
|
* - Safari 17.2+
|
|
@@ -383,31 +381,55 @@ export class HighlightCoordinator {
|
|
|
383
381
|
this.applyAdaptiveTTSStyle();
|
|
384
382
|
this.setupThemeObservation();
|
|
385
383
|
}
|
|
384
|
+
/**
|
|
385
|
+
* Re-adapt the TTS colors when the page's theme moves: on the document
|
|
386
|
+
* element, on a `<pie-theme>` host, and when a `<pie-theme>` mounts, which
|
|
387
|
+
* themes its target as it connects.
|
|
388
|
+
*/
|
|
386
389
|
setupThemeObservation() {
|
|
387
390
|
if (typeof document === "undefined")
|
|
388
391
|
return;
|
|
389
392
|
if (typeof MutationObserver === "undefined")
|
|
390
393
|
return;
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
+
const themeHosts = document.getElementsByTagName("pie-theme");
|
|
395
|
+
const observedHosts = new WeakSet();
|
|
396
|
+
const observeNewHosts = () => {
|
|
397
|
+
let found = false;
|
|
398
|
+
for (const host of themeHosts) {
|
|
399
|
+
if (observedHosts.has(host))
|
|
400
|
+
continue;
|
|
401
|
+
observedHosts.add(host);
|
|
402
|
+
observer.observe(host, {
|
|
403
|
+
attributes: true,
|
|
404
|
+
attributeFilter: [
|
|
405
|
+
"theme",
|
|
406
|
+
"scheme",
|
|
407
|
+
"provider",
|
|
408
|
+
"variables",
|
|
409
|
+
"style",
|
|
410
|
+
"data-theme",
|
|
411
|
+
"data-color-scheme",
|
|
412
|
+
],
|
|
413
|
+
});
|
|
414
|
+
found = true;
|
|
415
|
+
}
|
|
416
|
+
return found;
|
|
417
|
+
};
|
|
418
|
+
const observer = new MutationObserver((records) => {
|
|
419
|
+
const mounted = observeNewHosts();
|
|
420
|
+
if (mounted || records.some((record) => record.type === "attributes")) {
|
|
421
|
+
this.applyAdaptiveTTSStyle();
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
this.themeObserver = observer;
|
|
425
|
+
observer.observe(document.documentElement, {
|
|
394
426
|
attributes: true,
|
|
395
427
|
attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
|
|
396
428
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
"theme",
|
|
402
|
-
"scheme",
|
|
403
|
-
"provider",
|
|
404
|
-
"variables",
|
|
405
|
-
"style",
|
|
406
|
-
"data-theme",
|
|
407
|
-
"data-color-scheme",
|
|
408
|
-
],
|
|
409
|
-
});
|
|
410
|
-
}
|
|
429
|
+
// Insertions, so a `<pie-theme>` mounted later is found. An insertion
|
|
430
|
+
// costs a walk of the live host list, which holds a handful at most.
|
|
431
|
+
observer.observe(document, { childList: true, subtree: true });
|
|
432
|
+
observeNewHosts();
|
|
411
433
|
}
|
|
412
434
|
parseColor(input) {
|
|
413
435
|
if (!input)
|
|
@@ -511,6 +533,8 @@ export class HighlightCoordinator {
|
|
|
511
533
|
: [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
|
|
512
534
|
let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
|
|
513
535
|
let bestScore = -Infinity;
|
|
536
|
+
// The first opacity whose blend clears 1.25:1 against the background and
|
|
537
|
+
// 2.4:1 against the text wins; otherwise the best weighted score.
|
|
514
538
|
for (const opacity of opacityCandidates) {
|
|
515
539
|
const blended = this.blend(accent, background, opacity);
|
|
516
540
|
const backgroundDelta = this.contrastRatio(blended, background);
|
|
@@ -593,7 +617,6 @@ export class HighlightCoordinator {
|
|
|
593
617
|
return;
|
|
594
618
|
if (typeof document === "undefined")
|
|
595
619
|
return; // SSR guard
|
|
596
|
-
// Check if styles already exist
|
|
597
620
|
if (document.getElementById("pie-highlight-styles"))
|
|
598
621
|
return;
|
|
599
622
|
const style = document.createElement("style");
|
|
@@ -633,12 +656,10 @@ export class HighlightCoordinator {
|
|
|
633
656
|
* lives in a font-driven pseudo-element a CSS range cannot paint) and
|
|
634
657
|
* whole-expression fallbacks.
|
|
635
658
|
*
|
|
636
|
-
* The supplied element is painted exactly
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
* genuine expression fallback (where the element already is the container)
|
|
641
|
-
* should paint the whole expression.
|
|
659
|
+
* The supplied element is painted exactly, with no walk up to a containing
|
|
660
|
+
* `<math>` / `<mjx-container>`: a resolved token paints as a token, and only
|
|
661
|
+
* an expression fallback, where the element already is the container, paints
|
|
662
|
+
* the whole expression.
|
|
642
663
|
*/
|
|
643
664
|
highlightTTSWordElement(element) {
|
|
644
665
|
if (!this.supported)
|
|
@@ -660,7 +681,6 @@ export class HighlightCoordinator {
|
|
|
660
681
|
if (!this.supported)
|
|
661
682
|
return;
|
|
662
683
|
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
663
|
-
// Clear previous sentence highlight
|
|
664
684
|
this.clearTTSSentence();
|
|
665
685
|
const highlight = sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
666
686
|
for (const range of ranges) {
|
|
@@ -689,12 +709,11 @@ export class HighlightCoordinator {
|
|
|
689
709
|
// The read-along (sentence) band legitimately covers whole equations and
|
|
690
710
|
// replaced elements, so it escalates to them.
|
|
691
711
|
static SENTENCE_FALLBACK_SELECTOR = "math, mjx-container, svg, img, canvas, [role='img']";
|
|
692
|
-
// The word layer
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
// glyph nodes
|
|
696
|
-
//
|
|
697
|
-
// fallback because a CSS range cannot paint them.
|
|
712
|
+
// The word layer falls back to the element only for replaced elements
|
|
713
|
+
// (svg/img/canvas), which a CSS range cannot paint. Escalating to the
|
|
714
|
+
// enclosing `<math>` / `<mjx-container>` flashed the whole equation whenever
|
|
715
|
+
// a word crossed several glyph nodes, so math is tracked per token, held or
|
|
716
|
+
// by region in the highlight pipeline.
|
|
698
717
|
static WORD_FALLBACK_SELECTOR = "svg, img, canvas, [role='img']";
|
|
699
718
|
highlightTTSElementFallbacks(ranges) {
|
|
700
719
|
if (typeof Element === "undefined")
|
|
@@ -796,10 +815,8 @@ export class HighlightCoordinator {
|
|
|
796
815
|
*/
|
|
797
816
|
addAnnotation(range, color = HighlightColor.YELLOW) {
|
|
798
817
|
const id = `annotation-${this.nextAnnotationId++}`;
|
|
799
|
-
// Clone the range to store
|
|
800
818
|
const clonedRange = range.cloneRange();
|
|
801
819
|
adoptHighlightStylesFor(clonedRange.startContainer);
|
|
802
|
-
// Store annotation data
|
|
803
820
|
const annotation = {
|
|
804
821
|
id,
|
|
805
822
|
range: clonedRange,
|
|
@@ -913,44 +930,6 @@ export class HighlightCoordinator {
|
|
|
913
930
|
// ============================================================================
|
|
914
931
|
// HighlightCoordinatorApi interface implementation
|
|
915
932
|
// ============================================================================
|
|
916
|
-
/**
|
|
917
|
-
* Highlight a range as the given type: the TTS types paint as
|
|
918
|
-
* {@link highlightTTSWord} and {@link highlightTTSSentence} do.
|
|
919
|
-
*/
|
|
920
|
-
highlightRange(range, type, color = HighlightColor.YELLOW) {
|
|
921
|
-
if (!this.supported)
|
|
922
|
-
return;
|
|
923
|
-
switch (type) {
|
|
924
|
-
case HighlightType.TTS_WORD:
|
|
925
|
-
this.highlightTTSWord([range]);
|
|
926
|
-
break;
|
|
927
|
-
case HighlightType.TTS_SENTENCE:
|
|
928
|
-
this.highlightTTSSentence([range]);
|
|
929
|
-
break;
|
|
930
|
-
case HighlightType.ANNOTATION:
|
|
931
|
-
// For annotations, use the existing method
|
|
932
|
-
this.addAnnotation(range, color);
|
|
933
|
-
break;
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
/**
|
|
937
|
-
* Clear highlights of a specific type (interface method)
|
|
938
|
-
*/
|
|
939
|
-
clearHighlights(type) {
|
|
940
|
-
if (!this.supported)
|
|
941
|
-
return;
|
|
942
|
-
switch (type) {
|
|
943
|
-
case HighlightType.TTS_WORD:
|
|
944
|
-
this.clearTTSWord();
|
|
945
|
-
break;
|
|
946
|
-
case HighlightType.TTS_SENTENCE:
|
|
947
|
-
this.clearTTSSentence();
|
|
948
|
-
break;
|
|
949
|
-
case HighlightType.ANNOTATION:
|
|
950
|
-
this.clearAnnotations();
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
933
|
/**
|
|
955
934
|
* Clear all highlights (interface method)
|
|
956
935
|
*/
|
|
@@ -960,12 +939,6 @@ export class HighlightCoordinator {
|
|
|
960
939
|
this.clearTTS();
|
|
961
940
|
this.clearAnnotations();
|
|
962
941
|
}
|
|
963
|
-
/**
|
|
964
|
-
* Check if CSS Highlight API is supported (interface method)
|
|
965
|
-
*/
|
|
966
|
-
isSupported() {
|
|
967
|
-
return this.supported;
|
|
968
|
-
}
|
|
969
942
|
/**
|
|
970
943
|
* Update TTS highlight style dynamically
|
|
971
944
|
*
|
|
@@ -980,7 +953,6 @@ export class HighlightCoordinator {
|
|
|
980
953
|
const styleEl = document.getElementById("pie-highlight-styles");
|
|
981
954
|
if (!styleEl)
|
|
982
955
|
return;
|
|
983
|
-
// Convert hex to rgba
|
|
984
956
|
const hexToRgb = (hex) => {
|
|
985
957
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
986
958
|
return result
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Uses CSS selector paths and node indices for robust serialization that
|
|
8
8
|
* survives content changes when possible. A path into an open shadow root names
|
|
9
9
|
* its host, then ` >>> `, then the path inside the root, so ranges in content
|
|
10
|
-
* that renders into shadow roots round-trip;
|
|
10
|
+
* that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
|
|
11
|
+
* selector path.
|
|
11
12
|
*/
|
|
12
13
|
/**
|
|
13
14
|
* Serialized form of a Range suitable for storage.
|
|
@@ -79,19 +80,9 @@ export declare class RangeSerializer {
|
|
|
79
80
|
* Uses a hybrid approach:
|
|
80
81
|
* - For element nodes: CSS selector path
|
|
81
82
|
* - For text nodes: parent selector + text node index
|
|
82
|
-
*
|
|
83
|
-
* @param node - Node to get path for
|
|
84
|
-
* @param root - Root element
|
|
85
|
-
* @returns Path string
|
|
86
83
|
*/
|
|
87
84
|
private getNodePath;
|
|
88
|
-
/**
|
|
89
|
-
* Get CSS selector path to an element.
|
|
90
|
-
*
|
|
91
|
-
* @param element - Element to get path for
|
|
92
|
-
* @param root - Root element
|
|
93
|
-
* @returns CSS selector path
|
|
94
|
-
*/
|
|
85
|
+
/** Get CSS selector path to an element. */
|
|
95
86
|
private getElementPath;
|
|
96
87
|
/** The path to `shadowRoot` itself: its host's path and an empty step into it. */
|
|
97
88
|
private getShadowRootPath;
|
|
@@ -100,28 +91,6 @@ export declare class RangeSerializer {
|
|
|
100
91
|
* {@link SHADOW_STEP} continues in the shadow root of what the step before named.
|
|
101
92
|
*/
|
|
102
93
|
private resolveElementPath;
|
|
103
|
-
/**
|
|
104
|
-
* Find a node by its path from root.
|
|
105
|
-
*
|
|
106
|
-
* @param path - Path string from getNodePath
|
|
107
|
-
* @param root - Root element
|
|
108
|
-
* @returns Node, or null if not found
|
|
109
|
-
*/
|
|
94
|
+
/** The node a getNodePath path names, or null if not found. */
|
|
110
95
|
private findNodeByPath;
|
|
111
|
-
/**
|
|
112
|
-
* Batch serialize multiple ranges.
|
|
113
|
-
*
|
|
114
|
-
* @param ranges - Ranges to serialize
|
|
115
|
-
* @param root - Root element
|
|
116
|
-
* @returns Array of serialized ranges
|
|
117
|
-
*/
|
|
118
|
-
serializeMany(ranges: Range[], root: Element): SerializedRange[];
|
|
119
|
-
/**
|
|
120
|
-
* Batch deserialize multiple ranges.
|
|
121
|
-
*
|
|
122
|
-
* @param data - Array of serialized ranges
|
|
123
|
-
* @param root - Root element
|
|
124
|
-
* @returns Array of ranges (nulls for invalid ranges)
|
|
125
|
-
*/
|
|
126
|
-
deserializeMany(data: SerializedRange[], root: Element): (Range | null)[];
|
|
127
96
|
}
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* Uses CSS selector paths and node indices for robust serialization that
|
|
8
8
|
* survives content changes when possible. A path into an open shadow root names
|
|
9
9
|
* its host, then ` >>> `, then the path inside the root, so ranges in content
|
|
10
|
-
* that renders into shadow roots round-trip;
|
|
10
|
+
* that renders into shadow roots round-trip; a path with no ` >>> ` is a plain
|
|
11
|
+
* selector path.
|
|
11
12
|
*/
|
|
12
13
|
import { isShadowRootNode } from "./tts/flat-tree.js";
|
|
13
14
|
/** Separates a shadow host's path from the path inside its shadow root. */
|
|
@@ -69,11 +70,9 @@ export class RangeSerializer {
|
|
|
69
70
|
const range = new Range();
|
|
70
71
|
range.setStart(startNode, data.startOffset);
|
|
71
72
|
range.setEnd(endNode, data.endOffset);
|
|
72
|
-
// Validate text hasn't changed
|
|
73
73
|
if (range.toString() === data.text) {
|
|
74
74
|
return range;
|
|
75
75
|
}
|
|
76
|
-
// Text changed, range is invalid
|
|
77
76
|
return null;
|
|
78
77
|
}
|
|
79
78
|
catch (error) {
|
|
@@ -88,22 +87,16 @@ export class RangeSerializer {
|
|
|
88
87
|
* Uses a hybrid approach:
|
|
89
88
|
* - For element nodes: CSS selector path
|
|
90
89
|
* - For text nodes: parent selector + text node index
|
|
91
|
-
*
|
|
92
|
-
* @param node - Node to get path for
|
|
93
|
-
* @param root - Root element
|
|
94
|
-
* @returns Path string
|
|
95
90
|
*/
|
|
96
91
|
getNodePath(node, root) {
|
|
97
92
|
if (node === root) {
|
|
98
93
|
return "";
|
|
99
94
|
}
|
|
100
|
-
// Handle text nodes
|
|
101
95
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
102
96
|
const parent = node.parentNode;
|
|
103
97
|
if (!parent) {
|
|
104
98
|
throw new Error("Text node has no parent");
|
|
105
99
|
}
|
|
106
|
-
// Get index of this text node among its siblings
|
|
107
100
|
const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
|
|
108
101
|
const index = textNodes.indexOf(node);
|
|
109
102
|
const parentPath = isShadowRootNode(parent)
|
|
@@ -111,7 +104,6 @@ export class RangeSerializer {
|
|
|
111
104
|
: this.getElementPath(parent, root);
|
|
112
105
|
return `${parentPath}::text[${index}]`;
|
|
113
106
|
}
|
|
114
|
-
// Handle element nodes
|
|
115
107
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
116
108
|
return this.getElementPath(node, root);
|
|
117
109
|
}
|
|
@@ -120,13 +112,7 @@ export class RangeSerializer {
|
|
|
120
112
|
}
|
|
121
113
|
throw new Error(`Unsupported node type: ${node.nodeType}`);
|
|
122
114
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Get CSS selector path to an element.
|
|
125
|
-
*
|
|
126
|
-
* @param element - Element to get path for
|
|
127
|
-
* @param root - Root element
|
|
128
|
-
* @returns CSS selector path
|
|
129
|
-
*/
|
|
115
|
+
/** Get CSS selector path to an element. */
|
|
130
116
|
getElementPath(element, root) {
|
|
131
117
|
if (element === root) {
|
|
132
118
|
return "";
|
|
@@ -207,24 +193,16 @@ export class RangeSerializer {
|
|
|
207
193
|
}
|
|
208
194
|
return scope;
|
|
209
195
|
}
|
|
210
|
-
/**
|
|
211
|
-
* Find a node by its path from root.
|
|
212
|
-
*
|
|
213
|
-
* @param path - Path string from getNodePath
|
|
214
|
-
* @param root - Root element
|
|
215
|
-
* @returns Node, or null if not found
|
|
216
|
-
*/
|
|
196
|
+
/** The node a getNodePath path names, or null if not found. */
|
|
217
197
|
findNodeByPath(path, root) {
|
|
218
198
|
if (path === "") {
|
|
219
199
|
return root;
|
|
220
200
|
}
|
|
221
|
-
// Handle text node paths
|
|
222
201
|
if (path.includes("::text[")) {
|
|
223
202
|
const marker = path.lastIndexOf("::text[");
|
|
224
203
|
const elementPath = path.slice(0, marker);
|
|
225
204
|
const textPart = path.slice(marker + "::text[".length);
|
|
226
205
|
const textIndex = Number.parseInt(textPart.replace("]", ""), 10);
|
|
227
|
-
// Find parent element
|
|
228
206
|
let parent;
|
|
229
207
|
try {
|
|
230
208
|
parent = this.resolveElementPath(elementPath, root);
|
|
@@ -235,11 +213,9 @@ export class RangeSerializer {
|
|
|
235
213
|
}
|
|
236
214
|
if (!parent)
|
|
237
215
|
return null;
|
|
238
|
-
// Find text node by index
|
|
239
216
|
const textNodes = Array.from(parent.childNodes).filter((n) => n.nodeType === Node.TEXT_NODE);
|
|
240
217
|
return textNodes[textIndex] || null;
|
|
241
218
|
}
|
|
242
|
-
// Handle element paths
|
|
243
219
|
try {
|
|
244
220
|
return this.resolveElementPath(path, root);
|
|
245
221
|
}
|
|
@@ -248,24 +224,4 @@ export class RangeSerializer {
|
|
|
248
224
|
return null;
|
|
249
225
|
}
|
|
250
226
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Batch serialize multiple ranges.
|
|
253
|
-
*
|
|
254
|
-
* @param ranges - Ranges to serialize
|
|
255
|
-
* @param root - Root element
|
|
256
|
-
* @returns Array of serialized ranges
|
|
257
|
-
*/
|
|
258
|
-
serializeMany(ranges, root) {
|
|
259
|
-
return ranges.map((range) => this.serialize(range, root));
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Batch deserialize multiple ranges.
|
|
263
|
-
*
|
|
264
|
-
* @param data - Array of serialized ranges
|
|
265
|
-
* @param root - Root element
|
|
266
|
-
* @returns Array of ranges (nulls for invalid ranges)
|
|
267
|
-
*/
|
|
268
|
-
deserializeMany(data, root) {
|
|
269
|
-
return data.map((item) => this.deserialize(item, root));
|
|
270
|
-
}
|
|
271
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
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
//
|
|
179
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
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}`;
|