@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.
- package/README.md +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- 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/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -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 +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- 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 +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- 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+
|
|
@@ -21,6 +19,7 @@
|
|
|
21
19
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API
|
|
22
20
|
*/
|
|
23
21
|
import { RangeSerializer } from "./RangeSerializer.js";
|
|
22
|
+
import { composedContains, composedParentElement, isShadowRootNode, } from "./tts/flat-tree.js";
|
|
24
23
|
/**
|
|
25
24
|
* Highlight types
|
|
26
25
|
*/
|
|
@@ -42,235 +41,13 @@ export var HighlightColor;
|
|
|
42
41
|
HighlightColor["ORANGE"] = "orange";
|
|
43
42
|
HighlightColor["UNDERLINE"] = "underline";
|
|
44
43
|
})(HighlightColor || (HighlightColor = {}));
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
colorHighlights = new Map();
|
|
53
|
-
nextAnnotationId = 1;
|
|
54
|
-
supported = false;
|
|
55
|
-
rangeSerializer;
|
|
56
|
-
themeObserver = null;
|
|
57
|
-
explicitTTSColorOverride = null;
|
|
58
|
-
constructor() {
|
|
59
|
-
this.rangeSerializer = new RangeSerializer();
|
|
60
|
-
// SSR guard
|
|
61
|
-
if (typeof CSS === "undefined" || !("highlights" in CSS)) {
|
|
62
|
-
console.warn("CSS Custom Highlight API not supported (SSR or unsupported browser)");
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
this.supported = true;
|
|
66
|
-
this.initializeHighlights();
|
|
67
|
-
this.registerStyles();
|
|
68
|
-
this.applyAdaptiveTTSStyle();
|
|
69
|
-
this.setupThemeObservation();
|
|
70
|
-
}
|
|
71
|
-
setupThemeObservation() {
|
|
72
|
-
if (typeof document === "undefined")
|
|
73
|
-
return;
|
|
74
|
-
if (typeof MutationObserver === "undefined")
|
|
75
|
-
return;
|
|
76
|
-
const refresh = () => this.applyAdaptiveTTSStyle();
|
|
77
|
-
this.themeObserver = new MutationObserver(refresh);
|
|
78
|
-
this.themeObserver.observe(document.documentElement, {
|
|
79
|
-
attributes: true,
|
|
80
|
-
attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
|
|
81
|
-
});
|
|
82
|
-
for (const host of document.querySelectorAll("pie-theme")) {
|
|
83
|
-
this.themeObserver.observe(host, {
|
|
84
|
-
attributes: true,
|
|
85
|
-
attributeFilter: [
|
|
86
|
-
"theme",
|
|
87
|
-
"scheme",
|
|
88
|
-
"provider",
|
|
89
|
-
"variables",
|
|
90
|
-
"style",
|
|
91
|
-
"data-theme",
|
|
92
|
-
"data-color-scheme",
|
|
93
|
-
],
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
parseColor(input) {
|
|
98
|
-
if (!input)
|
|
99
|
-
return null;
|
|
100
|
-
const value = input.trim();
|
|
101
|
-
if (!value)
|
|
102
|
-
return null;
|
|
103
|
-
const hexMatch = /^#([a-f\d]{3}|[a-f\d]{6})$/i.exec(value);
|
|
104
|
-
if (hexMatch) {
|
|
105
|
-
const hex = hexMatch[1];
|
|
106
|
-
if (hex.length === 3) {
|
|
107
|
-
return [
|
|
108
|
-
parseInt(hex[0] + hex[0], 16),
|
|
109
|
-
parseInt(hex[1] + hex[1], 16),
|
|
110
|
-
parseInt(hex[2] + hex[2], 16),
|
|
111
|
-
];
|
|
112
|
-
}
|
|
113
|
-
return [
|
|
114
|
-
parseInt(hex.slice(0, 2), 16),
|
|
115
|
-
parseInt(hex.slice(2, 4), 16),
|
|
116
|
-
parseInt(hex.slice(4, 6), 16),
|
|
117
|
-
];
|
|
118
|
-
}
|
|
119
|
-
const rgbMatch = /^rgba?\((.+)\)$/i.exec(value);
|
|
120
|
-
if (rgbMatch) {
|
|
121
|
-
const normalized = rgbMatch[1].replace(/\//g, ",");
|
|
122
|
-
const parts = normalized
|
|
123
|
-
.split(/[,\s]+/)
|
|
124
|
-
.map((part) => part.trim())
|
|
125
|
-
.filter(Boolean);
|
|
126
|
-
const r = Number(parts[0]);
|
|
127
|
-
const g = Number(parts[1]);
|
|
128
|
-
const b = Number(parts[2]);
|
|
129
|
-
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
130
|
-
return [r, g, b];
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Browser parser fallback (covers formats like oklch()).
|
|
134
|
-
if (typeof document !== "undefined") {
|
|
135
|
-
const parserEl = document.createElement("span");
|
|
136
|
-
parserEl.style.color = value;
|
|
137
|
-
if (typeof parserEl.style.color === "string" && parserEl.style.color) {
|
|
138
|
-
document.body?.appendChild(parserEl);
|
|
139
|
-
const resolved = typeof getComputedStyle === "function"
|
|
140
|
-
? getComputedStyle(parserEl).color
|
|
141
|
-
: "";
|
|
142
|
-
parserEl.remove();
|
|
143
|
-
const normalizedResolved = resolved.trim();
|
|
144
|
-
// Guard against recursive loops when computed style returns the same
|
|
145
|
-
// unresolved function syntax (observed with certain color formats).
|
|
146
|
-
if (normalizedResolved && normalizedResolved !== value) {
|
|
147
|
-
return this.parseColor(resolved);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
relativeLuminance([r, g, b]) {
|
|
154
|
-
const toLinear = (channel) => {
|
|
155
|
-
const n = channel / 255;
|
|
156
|
-
return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
|
|
157
|
-
};
|
|
158
|
-
const lr = toLinear(r);
|
|
159
|
-
const lg = toLinear(g);
|
|
160
|
-
const lb = toLinear(b);
|
|
161
|
-
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
162
|
-
}
|
|
163
|
-
contrastRatio(a, b) {
|
|
164
|
-
const la = this.relativeLuminance(a);
|
|
165
|
-
const lb = this.relativeLuminance(b);
|
|
166
|
-
const lighter = Math.max(la, lb);
|
|
167
|
-
const darker = Math.min(la, lb);
|
|
168
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
169
|
-
}
|
|
170
|
-
blend(fg, bg, alpha) {
|
|
171
|
-
return [
|
|
172
|
-
Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
|
|
173
|
-
Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
|
|
174
|
-
Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
|
|
175
|
-
];
|
|
176
|
-
}
|
|
177
|
-
resolveAdaptiveTTSStyle(sourceEl) {
|
|
178
|
-
const fallbackMissing = [255, 235, 59];
|
|
179
|
-
const fallbackText = [17, 24, 39];
|
|
180
|
-
const fallbackBackground = [255, 255, 255];
|
|
181
|
-
const target = sourceEl ||
|
|
182
|
-
(typeof document !== "undefined" ? document.documentElement : null);
|
|
183
|
-
const computed = target && typeof getComputedStyle === "function"
|
|
184
|
-
? getComputedStyle(target)
|
|
185
|
-
: null;
|
|
186
|
-
const background = this.parseColor(computed?.getPropertyValue("--pie-background") ||
|
|
187
|
-
computed?.backgroundColor) || fallbackBackground;
|
|
188
|
-
const text = this.parseColor(computed?.getPropertyValue("--pie-text")) || fallbackText;
|
|
189
|
-
const accent = this.explicitTTSColorOverride
|
|
190
|
-
? this.parseColor(this.explicitTTSColorOverride.color) || fallbackMissing
|
|
191
|
-
: this.parseColor(computed?.getPropertyValue("--pie-missing")) ||
|
|
192
|
-
fallbackMissing;
|
|
193
|
-
const opacityCandidates = this.explicitTTSColorOverride
|
|
194
|
-
? [Math.max(0.3, Math.min(0.95, this.explicitTTSColorOverride.opacity))]
|
|
195
|
-
: [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
|
|
196
|
-
let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
|
|
197
|
-
let bestScore = -Infinity;
|
|
198
|
-
for (const opacity of opacityCandidates) {
|
|
199
|
-
const blended = this.blend(accent, background, opacity);
|
|
200
|
-
const backgroundDelta = this.contrastRatio(blended, background);
|
|
201
|
-
const textContrast = this.contrastRatio(blended, text);
|
|
202
|
-
const score = backgroundDelta * 1.2 + textContrast * 0.8;
|
|
203
|
-
if (backgroundDelta >= 1.25 && textContrast >= 2.4) {
|
|
204
|
-
selectedOpacity = opacity;
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
if (score > bestScore) {
|
|
208
|
-
bestScore = score;
|
|
209
|
-
selectedOpacity = opacity;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
const sentenceOpacity = Math.max(0.24, Math.min(0.85, selectedOpacity * 0.55));
|
|
213
|
-
const underlineOpacity = Math.max(0.55, Math.min(0.95, selectedOpacity + 0.2));
|
|
214
|
-
const shadowOpacity = Math.max(0.22, Math.min(0.6, selectedOpacity * 0.45));
|
|
215
|
-
const underlineColor = text;
|
|
216
|
-
const underlineBlend = this.blend(underlineColor, background, underlineOpacity);
|
|
217
|
-
const underlineDelta = this.contrastRatio(underlineBlend, background);
|
|
218
|
-
const fallbackUnderline = this.relativeLuminance(background) > 0.45 ? [0, 0, 0] : [255, 255, 255];
|
|
219
|
-
const finalUnderline = underlineDelta >= 1.35 ? underlineColor : fallbackUnderline;
|
|
220
|
-
const wordHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${selectedOpacity})`;
|
|
221
|
-
const sentenceHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${sentenceOpacity})`;
|
|
222
|
-
const wordUnderline = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${underlineOpacity})`;
|
|
223
|
-
const wordShadow = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${shadowOpacity})`;
|
|
224
|
-
return {
|
|
225
|
-
wordHighlight,
|
|
226
|
-
sentenceHighlight,
|
|
227
|
-
wordUnderline,
|
|
228
|
-
wordShadow,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
applyAdaptiveTTSStyle(sourceEl) {
|
|
232
|
-
if (typeof document === "undefined")
|
|
233
|
-
return;
|
|
234
|
-
const vars = this.resolveAdaptiveTTSStyle(sourceEl);
|
|
235
|
-
document.documentElement.style.setProperty("--pie-tts-word-highlight", vars.wordHighlight);
|
|
236
|
-
document.documentElement.style.setProperty("--pie-tts-sentence-highlight", vars.sentenceHighlight);
|
|
237
|
-
document.documentElement.style.setProperty("--pie-tts-line-highlight", vars.sentenceHighlight);
|
|
238
|
-
document.documentElement.style.setProperty("--pie-tts-word-underline", vars.wordUnderline);
|
|
239
|
-
document.documentElement.style.setProperty("--pie-tts-word-shadow", vars.wordShadow);
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Initialize CSS Custom Highlights
|
|
243
|
-
*/
|
|
244
|
-
initializeHighlights() {
|
|
245
|
-
if (!this.supported)
|
|
246
|
-
return;
|
|
247
|
-
// Create highlight registries for TTS
|
|
248
|
-
this.ttsWordHighlight = new Highlight();
|
|
249
|
-
this.ttsSentenceHighlight = new Highlight();
|
|
250
|
-
// Register TTS highlights with CSS
|
|
251
|
-
CSS.highlights.set("tts-word", this.ttsWordHighlight);
|
|
252
|
-
CSS.highlights.set("tts-sentence", this.ttsSentenceHighlight);
|
|
253
|
-
// Create shared highlights for each annotation color
|
|
254
|
-
for (const color of Object.values(HighlightColor)) {
|
|
255
|
-
const highlight = new Highlight();
|
|
256
|
-
this.colorHighlights.set(color, highlight);
|
|
257
|
-
CSS.highlights.set(`annotation-${color}`, highlight);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Register CSS styles for highlights
|
|
262
|
-
*/
|
|
263
|
-
registerStyles() {
|
|
264
|
-
if (!this.supported)
|
|
265
|
-
return;
|
|
266
|
-
if (typeof document === "undefined")
|
|
267
|
-
return; // SSR guard
|
|
268
|
-
// Check if styles already exist
|
|
269
|
-
if (document.getElementById("pie-highlight-styles"))
|
|
270
|
-
return;
|
|
271
|
-
const style = document.createElement("style");
|
|
272
|
-
style.id = "pie-highlight-styles";
|
|
273
|
-
style.textContent = `
|
|
44
|
+
/**
|
|
45
|
+
* The highlight stylesheet: the document's copy in `<style id="pie-highlight-styles">`,
|
|
46
|
+
* and each shadow root's that holds a highlighted range. `::highlight()` rules
|
|
47
|
+
* paint text in the tree whose styles hold them, so text in a shadow root needs
|
|
48
|
+
* its own copy; Firefox paints none of it from the document's.
|
|
49
|
+
*/
|
|
50
|
+
const HIGHLIGHT_STYLES = `
|
|
274
51
|
/* TTS highlights - temporary */
|
|
275
52
|
::highlight(tts-word) {
|
|
276
53
|
background-color: var(--pie-tts-word-highlight, color-mix(in srgb, var(--pie-missing, #ffeb3b) 68%, transparent));
|
|
@@ -481,49 +258,414 @@ export class HighlightCoordinator {
|
|
|
481
258
|
}
|
|
482
259
|
}
|
|
483
260
|
`;
|
|
261
|
+
/**
|
|
262
|
+
* Page-wide, so every copy of the toolkit on a page shares one constructed sheet
|
|
263
|
+
* and adopts it into a shadow root once. The first copy to build it supplies its
|
|
264
|
+
* rules, as the first to append the document's `<style>` does.
|
|
265
|
+
*/
|
|
266
|
+
const SHARED_SHEET_SLOT = Symbol.for("@pie-players/pie-assessment-toolkit/highlight-stylesheet");
|
|
267
|
+
const ADOPTED_SHEET_MARKER = Symbol.for("@pie-players/pie-assessment-toolkit/highlight-stylesheet-adopted");
|
|
268
|
+
const isSheetLike = (value) => !!value && typeof value.replaceSync === "function";
|
|
269
|
+
const sharedHighlightSheet = () => {
|
|
270
|
+
const slot = globalThis;
|
|
271
|
+
const existing = slot[SHARED_SHEET_SLOT];
|
|
272
|
+
if (isSheetLike(existing))
|
|
273
|
+
return existing;
|
|
274
|
+
const Sheet = globalThis
|
|
275
|
+
.CSSStyleSheet;
|
|
276
|
+
if (typeof Sheet !== "function")
|
|
277
|
+
return null;
|
|
278
|
+
try {
|
|
279
|
+
const sheet = new Sheet();
|
|
280
|
+
if (!isSheetLike(sheet))
|
|
281
|
+
return null;
|
|
282
|
+
sheet.replaceSync(HIGHLIGHT_STYLES);
|
|
283
|
+
slot[SHARED_SHEET_SLOT] = sheet;
|
|
284
|
+
return sheet;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Adopts the highlight stylesheet into the shadow root holding `node`, once per
|
|
292
|
+
* root. Appended, so the root's own sheets stay in place; re-adopted if the
|
|
293
|
+
* root's owner replaced its sheets since.
|
|
294
|
+
*/
|
|
295
|
+
const adoptHighlightStylesFor = (node) => {
|
|
296
|
+
const found = node?.getRootNode?.();
|
|
297
|
+
if (!isShadowRootNode(found))
|
|
298
|
+
return;
|
|
299
|
+
const root = found;
|
|
300
|
+
const sheets = root.adoptedStyleSheets;
|
|
301
|
+
if (!sheets || typeof sheets !== "object")
|
|
302
|
+
return;
|
|
303
|
+
const current = Array.from(sheets);
|
|
304
|
+
const adopted = root[ADOPTED_SHEET_MARKER];
|
|
305
|
+
if (adopted && current.includes(adopted))
|
|
306
|
+
return;
|
|
307
|
+
const sheet = sharedHighlightSheet();
|
|
308
|
+
if (!sheet)
|
|
309
|
+
return;
|
|
310
|
+
try {
|
|
311
|
+
root.adoptedStyleSheets = [...current, sheet];
|
|
312
|
+
root[ADOPTED_SHEET_MARKER] = sheet;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// A root that refuses the sheet paints from the document's rules where the
|
|
316
|
+
// browser allows it.
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
const TTS_WORD_HIGHLIGHT = "tts-word";
|
|
320
|
+
const TTS_SENTENCE_HIGHLIGHT = "tts-sentence";
|
|
321
|
+
const annotationHighlightName = (color) => `annotation-${color}`;
|
|
322
|
+
/**
|
|
323
|
+
* The page's highlight registered under `name`. Every coordinator on a page
|
|
324
|
+
* paints into the same one, because the page-wide `::highlight()` rules select
|
|
325
|
+
* it by that name, and removes only the ranges it added.
|
|
326
|
+
*/
|
|
327
|
+
const sharedHighlight = (name) => {
|
|
328
|
+
const registered = CSS.highlights.get(name);
|
|
329
|
+
if (registered)
|
|
330
|
+
return registered;
|
|
331
|
+
const highlight = new Highlight();
|
|
332
|
+
CSS.highlights.set(name, highlight);
|
|
333
|
+
return highlight;
|
|
334
|
+
};
|
|
335
|
+
const removeRanges = (name, ranges) => {
|
|
336
|
+
const highlight = CSS.highlights.get(name);
|
|
337
|
+
if (!highlight)
|
|
338
|
+
return;
|
|
339
|
+
for (const range of ranges)
|
|
340
|
+
highlight.delete(range);
|
|
341
|
+
};
|
|
342
|
+
/** The element the first range starts in, which the TTS colors adapt to. */
|
|
343
|
+
const startElementOf = (ranges) => {
|
|
344
|
+
const source = ranges[0]?.startContainer;
|
|
345
|
+
if (!source)
|
|
346
|
+
return null;
|
|
347
|
+
return source.nodeType === Node.ELEMENT_NODE
|
|
348
|
+
? source
|
|
349
|
+
: composedParentElement(source);
|
|
350
|
+
};
|
|
351
|
+
/**
|
|
352
|
+
* What the TTS colors derive from. The colors are custom properties on the root
|
|
353
|
+
* element, which every coordinator on the page writes, so the coordinators share
|
|
354
|
+
* these inputs: any one's refresh writes the same values, and two theme
|
|
355
|
+
* observers cannot keep undoing each other's writes.
|
|
356
|
+
*/
|
|
357
|
+
const ttsStyleInputs = { source: null, override: null };
|
|
358
|
+
export class HighlightCoordinator {
|
|
359
|
+
// The ranges this coordinator painted into the shared TTS highlights.
|
|
360
|
+
ttsWordRanges = new Set();
|
|
361
|
+
ttsSentenceRanges = new Set();
|
|
362
|
+
ttsWordElementHighlights = new Set();
|
|
363
|
+
ttsSentenceElementHighlights = new Set();
|
|
364
|
+
annotations = new Map();
|
|
365
|
+
// The element this coordinator last adapted the TTS colors to.
|
|
366
|
+
ttsStyleSource = null;
|
|
367
|
+
nextAnnotationId = 1;
|
|
368
|
+
supported = false;
|
|
369
|
+
rangeSerializer;
|
|
370
|
+
themeObserver = null;
|
|
371
|
+
constructor() {
|
|
372
|
+
this.rangeSerializer = new RangeSerializer();
|
|
373
|
+
// SSR guard
|
|
374
|
+
if (typeof CSS === "undefined" || !("highlights" in CSS)) {
|
|
375
|
+
console.warn("CSS Custom Highlight API not supported (SSR or unsupported browser)");
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
this.supported = true;
|
|
379
|
+
this.initializeHighlights();
|
|
380
|
+
this.registerStyles();
|
|
381
|
+
this.applyAdaptiveTTSStyle();
|
|
382
|
+
this.setupThemeObservation();
|
|
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
|
+
*/
|
|
389
|
+
setupThemeObservation() {
|
|
390
|
+
if (typeof document === "undefined")
|
|
391
|
+
return;
|
|
392
|
+
if (typeof MutationObserver === "undefined")
|
|
393
|
+
return;
|
|
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, {
|
|
426
|
+
attributes: true,
|
|
427
|
+
attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
|
|
428
|
+
});
|
|
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();
|
|
433
|
+
}
|
|
434
|
+
parseColor(input) {
|
|
435
|
+
if (!input)
|
|
436
|
+
return null;
|
|
437
|
+
const value = input.trim();
|
|
438
|
+
if (!value)
|
|
439
|
+
return null;
|
|
440
|
+
const hexMatch = /^#([a-f\d]{3}|[a-f\d]{6})$/i.exec(value);
|
|
441
|
+
if (hexMatch) {
|
|
442
|
+
const hex = hexMatch[1];
|
|
443
|
+
if (hex.length === 3) {
|
|
444
|
+
return [
|
|
445
|
+
parseInt(hex[0] + hex[0], 16),
|
|
446
|
+
parseInt(hex[1] + hex[1], 16),
|
|
447
|
+
parseInt(hex[2] + hex[2], 16),
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
return [
|
|
451
|
+
parseInt(hex.slice(0, 2), 16),
|
|
452
|
+
parseInt(hex.slice(2, 4), 16),
|
|
453
|
+
parseInt(hex.slice(4, 6), 16),
|
|
454
|
+
];
|
|
455
|
+
}
|
|
456
|
+
const rgbMatch = /^rgba?\((.+)\)$/i.exec(value);
|
|
457
|
+
if (rgbMatch) {
|
|
458
|
+
const normalized = rgbMatch[1].replace(/\//g, ",");
|
|
459
|
+
const parts = normalized
|
|
460
|
+
.split(/[,\s]+/)
|
|
461
|
+
.map((part) => part.trim())
|
|
462
|
+
.filter(Boolean);
|
|
463
|
+
const r = Number(parts[0]);
|
|
464
|
+
const g = Number(parts[1]);
|
|
465
|
+
const b = Number(parts[2]);
|
|
466
|
+
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
467
|
+
return [r, g, b];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
// Browser parser fallback (covers formats like oklch()).
|
|
471
|
+
if (typeof document !== "undefined") {
|
|
472
|
+
const parserEl = document.createElement("span");
|
|
473
|
+
parserEl.style.color = value;
|
|
474
|
+
if (typeof parserEl.style.color === "string" && parserEl.style.color) {
|
|
475
|
+
document.body?.appendChild(parserEl);
|
|
476
|
+
const resolved = typeof getComputedStyle === "function"
|
|
477
|
+
? getComputedStyle(parserEl).color
|
|
478
|
+
: "";
|
|
479
|
+
parserEl.remove();
|
|
480
|
+
const normalizedResolved = resolved.trim();
|
|
481
|
+
// Guard against recursive loops when computed style returns the same
|
|
482
|
+
// unresolved function syntax (observed with certain color formats).
|
|
483
|
+
if (normalizedResolved && normalizedResolved !== value) {
|
|
484
|
+
return this.parseColor(resolved);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return null;
|
|
489
|
+
}
|
|
490
|
+
relativeLuminance([r, g, b]) {
|
|
491
|
+
const toLinear = (channel) => {
|
|
492
|
+
const n = channel / 255;
|
|
493
|
+
return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
|
|
494
|
+
};
|
|
495
|
+
const lr = toLinear(r);
|
|
496
|
+
const lg = toLinear(g);
|
|
497
|
+
const lb = toLinear(b);
|
|
498
|
+
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
499
|
+
}
|
|
500
|
+
contrastRatio(a, b) {
|
|
501
|
+
const la = this.relativeLuminance(a);
|
|
502
|
+
const lb = this.relativeLuminance(b);
|
|
503
|
+
const lighter = Math.max(la, lb);
|
|
504
|
+
const darker = Math.min(la, lb);
|
|
505
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
506
|
+
}
|
|
507
|
+
blend(fg, bg, alpha) {
|
|
508
|
+
return [
|
|
509
|
+
Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
|
|
510
|
+
Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
|
|
511
|
+
Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
|
|
512
|
+
];
|
|
513
|
+
}
|
|
514
|
+
resolveAdaptiveTTSStyle(sourceEl) {
|
|
515
|
+
const fallbackMissing = [255, 235, 59];
|
|
516
|
+
const fallbackText = [17, 24, 39];
|
|
517
|
+
const fallbackBackground = [255, 255, 255];
|
|
518
|
+
const target = sourceEl ||
|
|
519
|
+
(typeof document !== "undefined" ? document.documentElement : null);
|
|
520
|
+
const computed = target && typeof getComputedStyle === "function"
|
|
521
|
+
? getComputedStyle(target)
|
|
522
|
+
: null;
|
|
523
|
+
const background = this.parseColor(computed?.getPropertyValue("--pie-background") ||
|
|
524
|
+
computed?.backgroundColor) || fallbackBackground;
|
|
525
|
+
const text = this.parseColor(computed?.getPropertyValue("--pie-text")) || fallbackText;
|
|
526
|
+
const override = ttsStyleInputs.override;
|
|
527
|
+
const accent = override
|
|
528
|
+
? this.parseColor(override.color) || fallbackMissing
|
|
529
|
+
: this.parseColor(computed?.getPropertyValue("--pie-missing")) ||
|
|
530
|
+
fallbackMissing;
|
|
531
|
+
const opacityCandidates = override
|
|
532
|
+
? [Math.max(0.3, Math.min(0.95, override.opacity))]
|
|
533
|
+
: [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
|
|
534
|
+
let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
|
|
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.
|
|
538
|
+
for (const opacity of opacityCandidates) {
|
|
539
|
+
const blended = this.blend(accent, background, opacity);
|
|
540
|
+
const backgroundDelta = this.contrastRatio(blended, background);
|
|
541
|
+
const textContrast = this.contrastRatio(blended, text);
|
|
542
|
+
const score = backgroundDelta * 1.2 + textContrast * 0.8;
|
|
543
|
+
if (backgroundDelta >= 1.25 && textContrast >= 2.4) {
|
|
544
|
+
selectedOpacity = opacity;
|
|
545
|
+
break;
|
|
546
|
+
}
|
|
547
|
+
if (score > bestScore) {
|
|
548
|
+
bestScore = score;
|
|
549
|
+
selectedOpacity = opacity;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const sentenceOpacity = Math.max(0.24, Math.min(0.85, selectedOpacity * 0.55));
|
|
553
|
+
const underlineOpacity = Math.max(0.55, Math.min(0.95, selectedOpacity + 0.2));
|
|
554
|
+
const shadowOpacity = Math.max(0.22, Math.min(0.6, selectedOpacity * 0.45));
|
|
555
|
+
const underlineColor = text;
|
|
556
|
+
const underlineBlend = this.blend(underlineColor, background, underlineOpacity);
|
|
557
|
+
const underlineDelta = this.contrastRatio(underlineBlend, background);
|
|
558
|
+
const fallbackUnderline = this.relativeLuminance(background) > 0.45 ? [0, 0, 0] : [255, 255, 255];
|
|
559
|
+
const finalUnderline = underlineDelta >= 1.35 ? underlineColor : fallbackUnderline;
|
|
560
|
+
const wordHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${selectedOpacity})`;
|
|
561
|
+
const sentenceHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${sentenceOpacity})`;
|
|
562
|
+
const wordUnderline = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${underlineOpacity})`;
|
|
563
|
+
const wordShadow = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${shadowOpacity})`;
|
|
564
|
+
return {
|
|
565
|
+
wordHighlight,
|
|
566
|
+
sentenceHighlight,
|
|
567
|
+
wordUnderline,
|
|
568
|
+
wordShadow,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Adapt the TTS colors to `sourceEl`, the element being highlighted. Without
|
|
573
|
+
* one, to the element highlighted last on the page while it stays connected,
|
|
574
|
+
* else to the document.
|
|
575
|
+
*/
|
|
576
|
+
applyAdaptiveTTSStyle(sourceEl) {
|
|
577
|
+
if (typeof document === "undefined")
|
|
578
|
+
return;
|
|
579
|
+
if (sourceEl) {
|
|
580
|
+
this.ttsStyleSource = sourceEl;
|
|
581
|
+
ttsStyleInputs.source = sourceEl;
|
|
582
|
+
}
|
|
583
|
+
const source = sourceEl ??
|
|
584
|
+
(ttsStyleInputs.source?.isConnected ? ttsStyleInputs.source : null);
|
|
585
|
+
const vars = this.resolveAdaptiveTTSStyle(source);
|
|
586
|
+
const style = document.documentElement.style;
|
|
587
|
+
const properties = [
|
|
588
|
+
["--pie-tts-word-highlight", vars.wordHighlight],
|
|
589
|
+
["--pie-tts-sentence-highlight", vars.sentenceHighlight],
|
|
590
|
+
["--pie-tts-line-highlight", vars.sentenceHighlight],
|
|
591
|
+
["--pie-tts-word-underline", vars.wordUnderline],
|
|
592
|
+
["--pie-tts-word-shadow", vars.wordShadow],
|
|
593
|
+
];
|
|
594
|
+
for (const [name, value] of properties) {
|
|
595
|
+
// An unchanged value is not rewritten: the theme observer watches this
|
|
596
|
+
// element's style, and a write would notify it again.
|
|
597
|
+
if (style.getPropertyValue?.(name) === value)
|
|
598
|
+
continue;
|
|
599
|
+
style.setProperty(name, value);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
/** Register the shared highlights, unless another coordinator already did. */
|
|
603
|
+
initializeHighlights() {
|
|
604
|
+
if (!this.supported)
|
|
605
|
+
return;
|
|
606
|
+
sharedHighlight(TTS_WORD_HIGHLIGHT);
|
|
607
|
+
sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
608
|
+
for (const color of Object.values(HighlightColor)) {
|
|
609
|
+
sharedHighlight(annotationHighlightName(color));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Register CSS styles for highlights
|
|
614
|
+
*/
|
|
615
|
+
registerStyles() {
|
|
616
|
+
if (!this.supported)
|
|
617
|
+
return;
|
|
618
|
+
if (typeof document === "undefined")
|
|
619
|
+
return; // SSR guard
|
|
620
|
+
if (document.getElementById("pie-highlight-styles"))
|
|
621
|
+
return;
|
|
622
|
+
const style = document.createElement("style");
|
|
623
|
+
style.id = "pie-highlight-styles";
|
|
624
|
+
style.textContent = HIGHLIGHT_STYLES;
|
|
484
625
|
document.head.appendChild(style);
|
|
485
626
|
}
|
|
486
627
|
/**
|
|
487
|
-
* Highlight
|
|
628
|
+
* Highlight the word being read (temporary), replacing the previous one.
|
|
629
|
+
* Every range paints, so a word split across inline elements highlights
|
|
630
|
+
* whole. A range that covers elements also marks the replaced elements in it
|
|
631
|
+
* (svg, img, canvas), which a CSS range cannot paint.
|
|
488
632
|
*
|
|
489
|
-
* @param
|
|
490
|
-
* @param startOffset Start position in text node
|
|
491
|
-
* @param endOffset End position in text node
|
|
633
|
+
* @param ranges The word's ranges: one per tree it spans
|
|
492
634
|
*/
|
|
493
|
-
highlightTTSWord(
|
|
494
|
-
if (!this.
|
|
635
|
+
highlightTTSWord(ranges) {
|
|
636
|
+
if (!this.supported)
|
|
495
637
|
return;
|
|
496
|
-
this.applyAdaptiveTTSStyle(
|
|
497
|
-
// Clear previous word highlight
|
|
638
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
498
639
|
this.clearTTSWord();
|
|
499
|
-
|
|
500
|
-
const range
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
640
|
+
const highlight = sharedHighlight(TTS_WORD_HIGHLIGHT);
|
|
641
|
+
for (const range of ranges) {
|
|
642
|
+
adoptHighlightStylesFor(range.startContainer);
|
|
643
|
+
highlight.add(range);
|
|
644
|
+
this.ttsWordRanges.add(range);
|
|
645
|
+
}
|
|
646
|
+
this.highlightTTSWordElementFallbacks(ranges.filter((range) => range.startContainer !== range.endContainer ||
|
|
647
|
+
range.startContainer.nodeType !== Node.TEXT_NODE));
|
|
505
648
|
}
|
|
506
649
|
/**
|
|
507
650
|
* Highlight a single element as the active TTS word (temporary).
|
|
508
651
|
*
|
|
509
|
-
* Unlike {@link highlightTTSWord} (which paints
|
|
510
|
-
*
|
|
652
|
+
* Unlike {@link highlightTTSWord} (which paints CSS ranges over text) this
|
|
653
|
+
* marks the element itself via `data-pie-tts-word-element`. It is used
|
|
511
654
|
* for atomic targets that have no direct text node to range over — most
|
|
512
655
|
* notably MathJax CHTML tokens (e.g. `<mjx-mi><mjx-c/></mjx-mi>`, whose glyph
|
|
513
656
|
* lives in a font-driven pseudo-element a CSS range cannot paint) and
|
|
514
657
|
* whole-expression fallbacks.
|
|
515
658
|
*
|
|
516
|
-
* The supplied element is painted exactly
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* genuine expression fallback (where the element already is the container)
|
|
521
|
-
* 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.
|
|
522
663
|
*/
|
|
523
664
|
highlightTTSWordElement(element) {
|
|
524
|
-
if (!this.
|
|
665
|
+
if (!this.supported)
|
|
525
666
|
return;
|
|
526
667
|
this.applyAdaptiveTTSStyle(element);
|
|
668
|
+
adoptHighlightStylesFor(element);
|
|
527
669
|
// Clear previous word highlight (CSS range and any element attributes)
|
|
528
670
|
this.clearTTSWord();
|
|
529
671
|
element.setAttribute("data-pie-tts-word-element", "true");
|
|
@@ -536,18 +678,15 @@ export class HighlightCoordinator {
|
|
|
536
678
|
* @param ranges Line (or sentence) ranges to paint with `--pie-tts-line-highlight`
|
|
537
679
|
*/
|
|
538
680
|
highlightTTSSentence(ranges) {
|
|
539
|
-
if (!this.
|
|
681
|
+
if (!this.supported)
|
|
540
682
|
return;
|
|
541
|
-
|
|
542
|
-
const sourceElement = source?.nodeType === Node.ELEMENT_NODE
|
|
543
|
-
? source
|
|
544
|
-
: source?.parentElement;
|
|
545
|
-
this.applyAdaptiveTTSStyle(sourceElement);
|
|
546
|
-
// Clear previous sentence highlight
|
|
683
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
547
684
|
this.clearTTSSentence();
|
|
548
|
-
|
|
685
|
+
const highlight = sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
549
686
|
for (const range of ranges) {
|
|
550
|
-
|
|
687
|
+
adoptHighlightStylesFor(range.startContainer);
|
|
688
|
+
highlight.add(range);
|
|
689
|
+
this.ttsSentenceRanges.add(range);
|
|
551
690
|
}
|
|
552
691
|
this.highlightTTSElementFallbacks(ranges);
|
|
553
692
|
}
|
|
@@ -557,11 +696,12 @@ export class HighlightCoordinator {
|
|
|
557
696
|
* @param elements Visible elements to paint with `--pie-tts-line-highlight`
|
|
558
697
|
*/
|
|
559
698
|
highlightTTSSentenceElements(elements) {
|
|
560
|
-
if (!this.
|
|
699
|
+
if (!this.supported)
|
|
561
700
|
return;
|
|
562
701
|
this.applyAdaptiveTTSStyle(elements[0] || null);
|
|
563
702
|
this.clearTTSSentence();
|
|
564
703
|
for (const element of elements) {
|
|
704
|
+
adoptHighlightStylesFor(element);
|
|
565
705
|
element.setAttribute("data-pie-tts-sentence-element", "true");
|
|
566
706
|
this.ttsSentenceElementHighlights.add(element);
|
|
567
707
|
}
|
|
@@ -569,12 +709,11 @@ export class HighlightCoordinator {
|
|
|
569
709
|
// The read-along (sentence) band legitimately covers whole equations and
|
|
570
710
|
// replaced elements, so it escalates to them.
|
|
571
711
|
static SENTENCE_FALLBACK_SELECTOR = "math, mjx-container, svg, img, canvas, [role='img']";
|
|
572
|
-
// The word layer
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
// glyph nodes
|
|
576
|
-
//
|
|
577
|
-
// 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.
|
|
578
717
|
static WORD_FALLBACK_SELECTOR = "svg, img, canvas, [role='img']";
|
|
579
718
|
highlightTTSElementFallbacks(ranges) {
|
|
580
719
|
if (typeof Element === "undefined")
|
|
@@ -587,36 +726,38 @@ export class HighlightCoordinator {
|
|
|
587
726
|
this.highlightElementFallbacks(ranges, "data-pie-tts-word-element", this.ttsWordElementHighlights, HighlightCoordinator.WORD_FALLBACK_SELECTOR);
|
|
588
727
|
}
|
|
589
728
|
highlightElementFallbacks(ranges, attributeName, trackedElements, selector) {
|
|
729
|
+
const elementAt = (node) => node.nodeType === Node.ELEMENT_NODE
|
|
730
|
+
? node
|
|
731
|
+
: composedParentElement(node);
|
|
590
732
|
for (const range of ranges) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
733
|
+
// A range over a shadow root's top-level children has the root as its
|
|
734
|
+
// common ancestor, and the root is what to search.
|
|
735
|
+
const common = range.commonAncestorContainer;
|
|
736
|
+
const root = isShadowRootNode(common)
|
|
737
|
+
? common
|
|
738
|
+
: elementAt(common);
|
|
594
739
|
if (!root)
|
|
595
740
|
continue;
|
|
596
741
|
const candidates = [];
|
|
597
|
-
const rangeStartElement = range.startContainer
|
|
598
|
-
|
|
599
|
-
: range.startContainer.parentElement;
|
|
600
|
-
const rangeEndElement = range.endContainer.nodeType === Node.ELEMENT_NODE
|
|
601
|
-
? range.endContainer
|
|
602
|
-
: range.endContainer.parentElement;
|
|
742
|
+
const rangeStartElement = elementAt(range.startContainer);
|
|
743
|
+
const rangeEndElement = elementAt(range.endContainer);
|
|
603
744
|
for (const element of [rangeStartElement, rangeEndElement]) {
|
|
604
745
|
let current = element;
|
|
605
746
|
while (current) {
|
|
606
747
|
if (current.matches?.(selector))
|
|
607
748
|
candidates.push(current);
|
|
608
|
-
current = current
|
|
749
|
+
current = composedParentElement(current);
|
|
609
750
|
}
|
|
610
751
|
}
|
|
611
|
-
if (root.matches?.(selector)) {
|
|
752
|
+
if (!isShadowRootNode(root) && root.matches?.(selector)) {
|
|
612
753
|
candidates.push(root);
|
|
613
754
|
}
|
|
614
755
|
candidates.push(...Array.from(root.querySelectorAll(selector)));
|
|
615
756
|
for (const element of new Set(candidates)) {
|
|
616
757
|
try {
|
|
617
758
|
if (!range.intersectsNode(element) &&
|
|
618
|
-
!element
|
|
619
|
-
!element
|
|
759
|
+
!composedContains(element, rangeStartElement) &&
|
|
760
|
+
!composedContains(element, rangeEndElement)) {
|
|
620
761
|
continue;
|
|
621
762
|
}
|
|
622
763
|
}
|
|
@@ -632,9 +773,10 @@ export class HighlightCoordinator {
|
|
|
632
773
|
* Clear TTS word highlight
|
|
633
774
|
*/
|
|
634
775
|
clearTTSWord() {
|
|
635
|
-
if (!this.
|
|
776
|
+
if (!this.supported)
|
|
636
777
|
return;
|
|
637
|
-
this.
|
|
778
|
+
removeRanges(TTS_WORD_HIGHLIGHT, this.ttsWordRanges);
|
|
779
|
+
this.ttsWordRanges.clear();
|
|
638
780
|
for (const element of this.ttsWordElementHighlights) {
|
|
639
781
|
element.removeAttribute("data-pie-tts-word-element");
|
|
640
782
|
}
|
|
@@ -644,9 +786,10 @@ export class HighlightCoordinator {
|
|
|
644
786
|
* Clear TTS sentence highlight
|
|
645
787
|
*/
|
|
646
788
|
clearTTSSentence() {
|
|
647
|
-
if (!this.
|
|
789
|
+
if (!this.supported)
|
|
648
790
|
return;
|
|
649
|
-
this.
|
|
791
|
+
removeRanges(TTS_SENTENCE_HIGHLIGHT, this.ttsSentenceRanges);
|
|
792
|
+
this.ttsSentenceRanges.clear();
|
|
650
793
|
for (const element of this.ttsSentenceElementHighlights) {
|
|
651
794
|
element.removeAttribute("data-pie-tts-sentence-element");
|
|
652
795
|
}
|
|
@@ -658,6 +801,10 @@ export class HighlightCoordinator {
|
|
|
658
801
|
clearTTS() {
|
|
659
802
|
this.clearTTSWord();
|
|
660
803
|
this.clearTTSSentence();
|
|
804
|
+
if (ttsStyleInputs.source === this.ttsStyleSource) {
|
|
805
|
+
ttsStyleInputs.source = null;
|
|
806
|
+
}
|
|
807
|
+
this.ttsStyleSource = null;
|
|
661
808
|
}
|
|
662
809
|
/**
|
|
663
810
|
* Add an annotation highlight (persistent)
|
|
@@ -668,9 +815,8 @@ export class HighlightCoordinator {
|
|
|
668
815
|
*/
|
|
669
816
|
addAnnotation(range, color = HighlightColor.YELLOW) {
|
|
670
817
|
const id = `annotation-${this.nextAnnotationId++}`;
|
|
671
|
-
// Clone the range to store
|
|
672
818
|
const clonedRange = range.cloneRange();
|
|
673
|
-
|
|
819
|
+
adoptHighlightStylesFor(clonedRange.startContainer);
|
|
674
820
|
const annotation = {
|
|
675
821
|
id,
|
|
676
822
|
range: clonedRange,
|
|
@@ -679,11 +825,9 @@ export class HighlightCoordinator {
|
|
|
679
825
|
timestamp: Date.now(),
|
|
680
826
|
};
|
|
681
827
|
this.annotations.set(id, annotation);
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (colorHighlight) {
|
|
686
|
-
colorHighlight.add(clonedRange);
|
|
828
|
+
// The same range object, so it can later be deleted by reference.
|
|
829
|
+
if (this.supported) {
|
|
830
|
+
sharedHighlight(annotationHighlightName(color)).add(clonedRange);
|
|
687
831
|
}
|
|
688
832
|
return id;
|
|
689
833
|
}
|
|
@@ -698,26 +842,24 @@ export class HighlightCoordinator {
|
|
|
698
842
|
console.warn(`[HighlightCoordinator] Annotation ${id} not found`);
|
|
699
843
|
return;
|
|
700
844
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const deleted = colorHighlight.delete(annotation.range);
|
|
706
|
-
console.log(`[HighlightCoordinator] Highlight.delete() returned:`, deleted);
|
|
845
|
+
if (this.supported) {
|
|
846
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
847
|
+
annotation.range,
|
|
848
|
+
]);
|
|
707
849
|
}
|
|
708
|
-
// Clean up
|
|
709
850
|
this.annotations.delete(id);
|
|
710
|
-
console.log(`[HighlightCoordinator] Annotation ${id} removed from map. Remaining:`, this.annotations.size);
|
|
711
851
|
}
|
|
712
852
|
/**
|
|
713
853
|
* Remove all annotations
|
|
714
854
|
*/
|
|
715
855
|
clearAnnotations() {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
856
|
+
if (this.supported) {
|
|
857
|
+
for (const annotation of this.annotations.values()) {
|
|
858
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
859
|
+
annotation.range,
|
|
860
|
+
]);
|
|
861
|
+
}
|
|
719
862
|
}
|
|
720
|
-
// Clear annotation data
|
|
721
863
|
this.annotations.clear();
|
|
722
864
|
}
|
|
723
865
|
/**
|
|
@@ -745,17 +887,10 @@ export class HighlightCoordinator {
|
|
|
745
887
|
const oldColor = annotation.color;
|
|
746
888
|
if (oldColor === newColor)
|
|
747
889
|
return;
|
|
748
|
-
// Remove from old color highlight
|
|
749
|
-
const oldColorHighlight = this.colorHighlights.get(oldColor);
|
|
750
|
-
if (oldColorHighlight) {
|
|
751
|
-
oldColorHighlight.delete(annotation.range);
|
|
752
|
-
}
|
|
753
|
-
// Update color
|
|
754
890
|
annotation.color = newColor;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
newColorHighlight.add(annotation.range);
|
|
891
|
+
if (this.supported) {
|
|
892
|
+
removeRanges(annotationHighlightName(oldColor), [annotation.range]);
|
|
893
|
+
sharedHighlight(annotationHighlightName(newColor)).add(annotation.range);
|
|
759
894
|
}
|
|
760
895
|
}
|
|
761
896
|
/**
|
|
@@ -795,59 +930,6 @@ export class HighlightCoordinator {
|
|
|
795
930
|
// ============================================================================
|
|
796
931
|
// HighlightCoordinatorApi interface implementation
|
|
797
932
|
// ============================================================================
|
|
798
|
-
/**
|
|
799
|
-
* Highlight a text range (interface method)
|
|
800
|
-
*
|
|
801
|
-
* Generic method that adapts to the specific highlight type.
|
|
802
|
-
* For more control, use the specific methods like highlightTTSWord().
|
|
803
|
-
*/
|
|
804
|
-
highlightRange(range, type, color = HighlightColor.YELLOW) {
|
|
805
|
-
if (!this.supported)
|
|
806
|
-
return;
|
|
807
|
-
switch (type) {
|
|
808
|
-
case HighlightType.TTS_WORD:
|
|
809
|
-
case HighlightType.TTS_SENTENCE: {
|
|
810
|
-
// For TTS, add to appropriate highlight
|
|
811
|
-
const highlight = type === HighlightType.TTS_WORD
|
|
812
|
-
? this.ttsWordHighlight
|
|
813
|
-
: this.ttsSentenceHighlight;
|
|
814
|
-
if (highlight) {
|
|
815
|
-
highlight.clear();
|
|
816
|
-
highlight.add(range);
|
|
817
|
-
if (type === HighlightType.TTS_WORD) {
|
|
818
|
-
for (const element of this.ttsWordElementHighlights) {
|
|
819
|
-
element.removeAttribute("data-pie-tts-word-element");
|
|
820
|
-
}
|
|
821
|
-
this.ttsWordElementHighlights.clear();
|
|
822
|
-
this.highlightTTSWordElementFallbacks([range]);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
break;
|
|
826
|
-
}
|
|
827
|
-
case HighlightType.ANNOTATION:
|
|
828
|
-
// For annotations, use the existing method
|
|
829
|
-
this.addAnnotation(range, color);
|
|
830
|
-
break;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
/**
|
|
834
|
-
* Clear highlights of a specific type (interface method)
|
|
835
|
-
*/
|
|
836
|
-
clearHighlights(type) {
|
|
837
|
-
if (!this.supported)
|
|
838
|
-
return;
|
|
839
|
-
switch (type) {
|
|
840
|
-
case HighlightType.TTS_WORD:
|
|
841
|
-
this.clearTTSWord();
|
|
842
|
-
break;
|
|
843
|
-
case HighlightType.TTS_SENTENCE:
|
|
844
|
-
this.clearTTSSentence();
|
|
845
|
-
break;
|
|
846
|
-
case HighlightType.ANNOTATION:
|
|
847
|
-
this.clearAnnotations();
|
|
848
|
-
break;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
933
|
/**
|
|
852
934
|
* Clear all highlights (interface method)
|
|
853
935
|
*/
|
|
@@ -857,12 +939,6 @@ export class HighlightCoordinator {
|
|
|
857
939
|
this.clearTTS();
|
|
858
940
|
this.clearAnnotations();
|
|
859
941
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Check if CSS Highlight API is supported (interface method)
|
|
862
|
-
*/
|
|
863
|
-
isSupported() {
|
|
864
|
-
return this.supported;
|
|
865
|
-
}
|
|
866
942
|
/**
|
|
867
943
|
* Update TTS highlight style dynamically
|
|
868
944
|
*
|
|
@@ -877,7 +953,6 @@ export class HighlightCoordinator {
|
|
|
877
953
|
const styleEl = document.getElementById("pie-highlight-styles");
|
|
878
954
|
if (!styleEl)
|
|
879
955
|
return;
|
|
880
|
-
// Convert hex to rgba
|
|
881
956
|
const hexToRgb = (hex) => {
|
|
882
957
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
883
958
|
return result
|
|
@@ -891,24 +966,27 @@ export class HighlightCoordinator {
|
|
|
891
966
|
const rgb = hexToRgb(color);
|
|
892
967
|
if (!rgb)
|
|
893
968
|
return;
|
|
894
|
-
|
|
969
|
+
ttsStyleInputs.override = {
|
|
895
970
|
color: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
896
971
|
opacity: Math.max(0.2, Math.min(0.95, opacity)),
|
|
972
|
+
owner: this,
|
|
897
973
|
};
|
|
898
974
|
this.applyAdaptiveTTSStyle();
|
|
899
975
|
}
|
|
900
976
|
/**
|
|
901
|
-
*
|
|
977
|
+
* Remove this coordinator's ranges and stop its observer.
|
|
902
978
|
*
|
|
903
|
-
* The stylesheet
|
|
904
|
-
*
|
|
905
|
-
* single bounded style element installed for the lifetime of the document.
|
|
979
|
+
* The stylesheet and the registered highlights are page-wide and shared by
|
|
980
|
+
* every live coordinator, so they stay installed for the document's lifetime.
|
|
906
981
|
*/
|
|
907
982
|
destroy() {
|
|
908
983
|
if (!this.supported)
|
|
909
984
|
return;
|
|
910
985
|
this.clearTTS();
|
|
911
986
|
this.clearAnnotations();
|
|
987
|
+
if (ttsStyleInputs.override?.owner === this) {
|
|
988
|
+
ttsStyleInputs.override = null;
|
|
989
|
+
}
|
|
912
990
|
this.themeObserver?.disconnect();
|
|
913
991
|
this.themeObserver = null;
|
|
914
992
|
}
|