@pie-players/pie-assessment-toolkit 0.3.74 → 0.3.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +349 -313
- package/dist/attempt/TestSession.d.ts +1 -20
- package/dist/attempt/TestSession.js +8 -99
- 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-7tt7v2xp.js +4 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-97hx60z4.js} +1 -1
- package/dist/components/chunks/ItemToolBar-bxwyhr5p.js +2 -0
- package/dist/components/chunks/ItemToolBar-jx38w99s.js +2 -0
- package/dist/components/chunks/ItemToolBar-rd2bb4p6.js +40 -0
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-jd8cc08x.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 +27 -6
- package/dist/index.d.ts +22 -47
- package/dist/index.js +12 -36
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +77 -32
- package/dist/policy/core/ToolPolicyEngine.js +136 -46
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +75 -54
- package/dist/policy/core/decision-types.d.ts +65 -34
- package/dist/policy/core/feature-decision.d.ts +16 -9
- package/dist/policy/core/feature-decision.js +14 -8
- package/dist/policy/core/pnp-policy-inputs.d.ts +24 -33
- package/dist/policy/core/pnp-policy-inputs.js +28 -56
- package/dist/policy/core/provenance.d.ts +9 -4
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +44 -61
- package/dist/policy/sources/PnpPolicySource.js +117 -118
- package/dist/runtime/SectionControllerBinding.d.ts +279 -0
- package/dist/runtime/SectionControllerBinding.js +450 -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 -2
- package/dist/runtime/adapter/subscriber-fanout.js +1 -2
- 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 +9 -15
- package/dist/runtime/composition-emit-scheduler.js +9 -15
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +17 -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 -2
- package/dist/runtime/core/SectionEngineCore.js +2 -2
- package/dist/runtime/core/cohort.d.ts +1 -1
- package/dist/runtime/core/cohort.js +1 -1
- 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 +16 -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 +71 -51
- 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 +11 -0
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- 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 +188 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -10
- package/dist/runtime/tool-host-contract.js +11 -11
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/ElementToolStateStore.d.ts +14 -10
- package/dist/services/ElementToolStateStore.js +37 -13
- package/dist/services/HighlightCoordinator.d.ts +21 -21
- package/dist/services/HighlightCoordinator.js +431 -325
- package/dist/services/RangeSerializer.d.ts +10 -1
- package/dist/services/RangeSerializer.js +87 -14
- package/dist/services/TTSService.d.ts +117 -70
- package/dist/services/TTSService.js +668 -578
- package/dist/services/ToolCoordinator.d.ts +43 -60
- package/dist/services/ToolCoordinator.js +164 -206
- package/dist/services/ToolRegistry.d.ts +74 -57
- package/dist/services/ToolRegistry.js +104 -114
- package/dist/services/ToolkitCoordinator.d.ts +183 -189
- package/dist/services/ToolkitCoordinator.js +477 -431
- 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 +22 -7
- package/dist/services/framework-error.js +9 -7
- package/dist/services/interfaces.d.ts +158 -129
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +82 -0
- package/dist/services/spoken-audio-cards.d.ts +0 -2
- package/dist/services/spoken-audio-cards.js +0 -2
- 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 +4 -5
- package/dist/services/tool-config-validation.js +36 -61
- package/dist/services/tool-context.d.ts +0 -8
- package/dist/services/tool-context.js +0 -12
- package/dist/services/tool-icons.d.ts +4 -3
- package/dist/services/tool-icons.js +4 -3
- package/dist/services/tool-instance-id.d.ts +0 -1
- package/dist/services/tool-instance-id.js +0 -6
- package/dist/services/tool-providers/TTSToolProvider.d.ts +18 -19
- package/dist/services/tool-providers/TTSToolProvider.js +37 -34
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +30 -26
- package/dist/services/tool-providers/ToolProviderRegistry.js +119 -93
- package/dist/services/tool-request.d.ts +27 -12
- package/dist/services/tool-request.js +24 -14
- package/dist/services/toolbar-decided-tools.d.ts +15 -0
- package/dist/services/toolbar-decided-tools.js +28 -0
- package/dist/services/tools-config-normalizer.d.ts +17 -22
- package/dist/services/tools-config-normalizer.js +2 -10
- package/dist/services/tts/browser-provider.d.ts +38 -2
- package/dist/services/tts/browser-provider.js +276 -140
- package/dist/services/tts/flat-tree.d.ts +71 -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/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +12 -4
- 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/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +32 -18
- 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 +11 -0
- package/dist/services/tts/math-speech.js +51 -18
- package/dist/services/tts/provider-options.d.ts +41 -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 +11 -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 +7 -0
- package/dist/services/tts/text-segmentation.js +47 -0
- package/dist/services/tts-runtime-config.d.ts +25 -17
- package/dist/services/tts-runtime-config.js +16 -95
- package/dist/tools/content-capability-resolution.d.ts +7 -9
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +58 -0
- package/dist/tools/registration.js +54 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +2 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +44 -42
- 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 +13 -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/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/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
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* @see https://developer.mozilla.org/en-US/docs/Web/API/CSS_Custom_Highlight_API
|
|
22
22
|
*/
|
|
23
23
|
import { RangeSerializer } from "./RangeSerializer.js";
|
|
24
|
+
import { composedContains, composedParentElement, isShadowRootNode, } from "./tts/flat-tree.js";
|
|
24
25
|
/**
|
|
25
26
|
* Highlight types
|
|
26
27
|
*/
|
|
@@ -42,235 +43,13 @@ export var HighlightColor;
|
|
|
42
43
|
HighlightColor["ORANGE"] = "orange";
|
|
43
44
|
HighlightColor["UNDERLINE"] = "underline";
|
|
44
45
|
})(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 = `
|
|
46
|
+
/**
|
|
47
|
+
* The highlight stylesheet: the document's copy in `<style id="pie-highlight-styles">`,
|
|
48
|
+
* and each shadow root's that holds a highlighted range. `::highlight()` rules
|
|
49
|
+
* paint text in the tree whose styles hold them, so text in a shadow root needs
|
|
50
|
+
* its own copy; Firefox paints none of it from the document's.
|
|
51
|
+
*/
|
|
52
|
+
const HIGHLIGHT_STYLES = `
|
|
274
53
|
/* TTS highlights - temporary */
|
|
275
54
|
::highlight(tts-word) {
|
|
276
55
|
background-color: var(--pie-tts-word-highlight, color-mix(in srgb, var(--pie-missing, #ffeb3b) 68%, transparent));
|
|
@@ -481,33 +260,374 @@ export class HighlightCoordinator {
|
|
|
481
260
|
}
|
|
482
261
|
}
|
|
483
262
|
`;
|
|
263
|
+
/**
|
|
264
|
+
* Page-wide, so every copy of the toolkit on a page shares one constructed sheet
|
|
265
|
+
* and adopts it into a shadow root once. The first copy to build it supplies its
|
|
266
|
+
* rules, as the first to append the document's `<style>` does.
|
|
267
|
+
*/
|
|
268
|
+
const SHARED_SHEET_SLOT = Symbol.for("@pie-players/pie-assessment-toolkit/highlight-stylesheet");
|
|
269
|
+
const ADOPTED_SHEET_MARKER = Symbol.for("@pie-players/pie-assessment-toolkit/highlight-stylesheet-adopted");
|
|
270
|
+
const isSheetLike = (value) => !!value && typeof value.replaceSync === "function";
|
|
271
|
+
const sharedHighlightSheet = () => {
|
|
272
|
+
const slot = globalThis;
|
|
273
|
+
const existing = slot[SHARED_SHEET_SLOT];
|
|
274
|
+
if (isSheetLike(existing))
|
|
275
|
+
return existing;
|
|
276
|
+
const Sheet = globalThis
|
|
277
|
+
.CSSStyleSheet;
|
|
278
|
+
if (typeof Sheet !== "function")
|
|
279
|
+
return null;
|
|
280
|
+
try {
|
|
281
|
+
const sheet = new Sheet();
|
|
282
|
+
if (!isSheetLike(sheet))
|
|
283
|
+
return null;
|
|
284
|
+
sheet.replaceSync(HIGHLIGHT_STYLES);
|
|
285
|
+
slot[SHARED_SHEET_SLOT] = sheet;
|
|
286
|
+
return sheet;
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* Adopts the highlight stylesheet into the shadow root holding `node`, once per
|
|
294
|
+
* root. Appended, so the root's own sheets stay in place; re-adopted if the
|
|
295
|
+
* root's owner replaced its sheets since.
|
|
296
|
+
*/
|
|
297
|
+
const adoptHighlightStylesFor = (node) => {
|
|
298
|
+
const found = node?.getRootNode?.();
|
|
299
|
+
if (!isShadowRootNode(found))
|
|
300
|
+
return;
|
|
301
|
+
const root = found;
|
|
302
|
+
const sheets = root.adoptedStyleSheets;
|
|
303
|
+
if (!sheets || typeof sheets !== "object")
|
|
304
|
+
return;
|
|
305
|
+
const current = Array.from(sheets);
|
|
306
|
+
const adopted = root[ADOPTED_SHEET_MARKER];
|
|
307
|
+
if (adopted && current.includes(adopted))
|
|
308
|
+
return;
|
|
309
|
+
const sheet = sharedHighlightSheet();
|
|
310
|
+
if (!sheet)
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
root.adoptedStyleSheets = [...current, sheet];
|
|
314
|
+
root[ADOPTED_SHEET_MARKER] = sheet;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// A root that refuses the sheet paints from the document's rules where the
|
|
318
|
+
// browser allows it.
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const TTS_WORD_HIGHLIGHT = "tts-word";
|
|
322
|
+
const TTS_SENTENCE_HIGHLIGHT = "tts-sentence";
|
|
323
|
+
const annotationHighlightName = (color) => `annotation-${color}`;
|
|
324
|
+
/**
|
|
325
|
+
* The page's highlight registered under `name`. Every coordinator on a page
|
|
326
|
+
* paints into the same one, because the page-wide `::highlight()` rules select
|
|
327
|
+
* it by that name, and removes only the ranges it added.
|
|
328
|
+
*/
|
|
329
|
+
const sharedHighlight = (name) => {
|
|
330
|
+
const registered = CSS.highlights.get(name);
|
|
331
|
+
if (registered)
|
|
332
|
+
return registered;
|
|
333
|
+
const highlight = new Highlight();
|
|
334
|
+
CSS.highlights.set(name, highlight);
|
|
335
|
+
return highlight;
|
|
336
|
+
};
|
|
337
|
+
const removeRanges = (name, ranges) => {
|
|
338
|
+
const highlight = CSS.highlights.get(name);
|
|
339
|
+
if (!highlight)
|
|
340
|
+
return;
|
|
341
|
+
for (const range of ranges)
|
|
342
|
+
highlight.delete(range);
|
|
343
|
+
};
|
|
344
|
+
/** The element the first range starts in, which the TTS colors adapt to. */
|
|
345
|
+
const startElementOf = (ranges) => {
|
|
346
|
+
const source = ranges[0]?.startContainer;
|
|
347
|
+
if (!source)
|
|
348
|
+
return null;
|
|
349
|
+
return source.nodeType === Node.ELEMENT_NODE
|
|
350
|
+
? source
|
|
351
|
+
: composedParentElement(source);
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* What the TTS colors derive from. The colors are custom properties on the root
|
|
355
|
+
* element, which every coordinator on the page writes, so the coordinators share
|
|
356
|
+
* these inputs: any one's refresh writes the same values, and two theme
|
|
357
|
+
* observers cannot keep undoing each other's writes.
|
|
358
|
+
*/
|
|
359
|
+
const ttsStyleInputs = { source: null, override: null };
|
|
360
|
+
export class HighlightCoordinator {
|
|
361
|
+
// The ranges this coordinator painted into the shared TTS highlights.
|
|
362
|
+
ttsWordRanges = new Set();
|
|
363
|
+
ttsSentenceRanges = new Set();
|
|
364
|
+
ttsWordElementHighlights = new Set();
|
|
365
|
+
ttsSentenceElementHighlights = new Set();
|
|
366
|
+
annotations = new Map();
|
|
367
|
+
// The element this coordinator last adapted the TTS colors to.
|
|
368
|
+
ttsStyleSource = null;
|
|
369
|
+
nextAnnotationId = 1;
|
|
370
|
+
supported = false;
|
|
371
|
+
rangeSerializer;
|
|
372
|
+
themeObserver = null;
|
|
373
|
+
constructor() {
|
|
374
|
+
this.rangeSerializer = new RangeSerializer();
|
|
375
|
+
// SSR guard
|
|
376
|
+
if (typeof CSS === "undefined" || !("highlights" in CSS)) {
|
|
377
|
+
console.warn("CSS Custom Highlight API not supported (SSR or unsupported browser)");
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.supported = true;
|
|
381
|
+
this.initializeHighlights();
|
|
382
|
+
this.registerStyles();
|
|
383
|
+
this.applyAdaptiveTTSStyle();
|
|
384
|
+
this.setupThemeObservation();
|
|
385
|
+
}
|
|
386
|
+
setupThemeObservation() {
|
|
387
|
+
if (typeof document === "undefined")
|
|
388
|
+
return;
|
|
389
|
+
if (typeof MutationObserver === "undefined")
|
|
390
|
+
return;
|
|
391
|
+
const refresh = () => this.applyAdaptiveTTSStyle();
|
|
392
|
+
this.themeObserver = new MutationObserver(refresh);
|
|
393
|
+
this.themeObserver.observe(document.documentElement, {
|
|
394
|
+
attributes: true,
|
|
395
|
+
attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
|
|
396
|
+
});
|
|
397
|
+
for (const host of document.querySelectorAll("pie-theme")) {
|
|
398
|
+
this.themeObserver.observe(host, {
|
|
399
|
+
attributes: true,
|
|
400
|
+
attributeFilter: [
|
|
401
|
+
"theme",
|
|
402
|
+
"scheme",
|
|
403
|
+
"provider",
|
|
404
|
+
"variables",
|
|
405
|
+
"style",
|
|
406
|
+
"data-theme",
|
|
407
|
+
"data-color-scheme",
|
|
408
|
+
],
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
parseColor(input) {
|
|
413
|
+
if (!input)
|
|
414
|
+
return null;
|
|
415
|
+
const value = input.trim();
|
|
416
|
+
if (!value)
|
|
417
|
+
return null;
|
|
418
|
+
const hexMatch = /^#([a-f\d]{3}|[a-f\d]{6})$/i.exec(value);
|
|
419
|
+
if (hexMatch) {
|
|
420
|
+
const hex = hexMatch[1];
|
|
421
|
+
if (hex.length === 3) {
|
|
422
|
+
return [
|
|
423
|
+
parseInt(hex[0] + hex[0], 16),
|
|
424
|
+
parseInt(hex[1] + hex[1], 16),
|
|
425
|
+
parseInt(hex[2] + hex[2], 16),
|
|
426
|
+
];
|
|
427
|
+
}
|
|
428
|
+
return [
|
|
429
|
+
parseInt(hex.slice(0, 2), 16),
|
|
430
|
+
parseInt(hex.slice(2, 4), 16),
|
|
431
|
+
parseInt(hex.slice(4, 6), 16),
|
|
432
|
+
];
|
|
433
|
+
}
|
|
434
|
+
const rgbMatch = /^rgba?\((.+)\)$/i.exec(value);
|
|
435
|
+
if (rgbMatch) {
|
|
436
|
+
const normalized = rgbMatch[1].replace(/\//g, ",");
|
|
437
|
+
const parts = normalized
|
|
438
|
+
.split(/[,\s]+/)
|
|
439
|
+
.map((part) => part.trim())
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
const r = Number(parts[0]);
|
|
442
|
+
const g = Number(parts[1]);
|
|
443
|
+
const b = Number(parts[2]);
|
|
444
|
+
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
445
|
+
return [r, g, b];
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
// Browser parser fallback (covers formats like oklch()).
|
|
449
|
+
if (typeof document !== "undefined") {
|
|
450
|
+
const parserEl = document.createElement("span");
|
|
451
|
+
parserEl.style.color = value;
|
|
452
|
+
if (typeof parserEl.style.color === "string" && parserEl.style.color) {
|
|
453
|
+
document.body?.appendChild(parserEl);
|
|
454
|
+
const resolved = typeof getComputedStyle === "function"
|
|
455
|
+
? getComputedStyle(parserEl).color
|
|
456
|
+
: "";
|
|
457
|
+
parserEl.remove();
|
|
458
|
+
const normalizedResolved = resolved.trim();
|
|
459
|
+
// Guard against recursive loops when computed style returns the same
|
|
460
|
+
// unresolved function syntax (observed with certain color formats).
|
|
461
|
+
if (normalizedResolved && normalizedResolved !== value) {
|
|
462
|
+
return this.parseColor(resolved);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
relativeLuminance([r, g, b]) {
|
|
469
|
+
const toLinear = (channel) => {
|
|
470
|
+
const n = channel / 255;
|
|
471
|
+
return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
|
|
472
|
+
};
|
|
473
|
+
const lr = toLinear(r);
|
|
474
|
+
const lg = toLinear(g);
|
|
475
|
+
const lb = toLinear(b);
|
|
476
|
+
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
477
|
+
}
|
|
478
|
+
contrastRatio(a, b) {
|
|
479
|
+
const la = this.relativeLuminance(a);
|
|
480
|
+
const lb = this.relativeLuminance(b);
|
|
481
|
+
const lighter = Math.max(la, lb);
|
|
482
|
+
const darker = Math.min(la, lb);
|
|
483
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
484
|
+
}
|
|
485
|
+
blend(fg, bg, alpha) {
|
|
486
|
+
return [
|
|
487
|
+
Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
|
|
488
|
+
Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
|
|
489
|
+
Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
|
|
490
|
+
];
|
|
491
|
+
}
|
|
492
|
+
resolveAdaptiveTTSStyle(sourceEl) {
|
|
493
|
+
const fallbackMissing = [255, 235, 59];
|
|
494
|
+
const fallbackText = [17, 24, 39];
|
|
495
|
+
const fallbackBackground = [255, 255, 255];
|
|
496
|
+
const target = sourceEl ||
|
|
497
|
+
(typeof document !== "undefined" ? document.documentElement : null);
|
|
498
|
+
const computed = target && typeof getComputedStyle === "function"
|
|
499
|
+
? getComputedStyle(target)
|
|
500
|
+
: null;
|
|
501
|
+
const background = this.parseColor(computed?.getPropertyValue("--pie-background") ||
|
|
502
|
+
computed?.backgroundColor) || fallbackBackground;
|
|
503
|
+
const text = this.parseColor(computed?.getPropertyValue("--pie-text")) || fallbackText;
|
|
504
|
+
const override = ttsStyleInputs.override;
|
|
505
|
+
const accent = override
|
|
506
|
+
? this.parseColor(override.color) || fallbackMissing
|
|
507
|
+
: this.parseColor(computed?.getPropertyValue("--pie-missing")) ||
|
|
508
|
+
fallbackMissing;
|
|
509
|
+
const opacityCandidates = override
|
|
510
|
+
? [Math.max(0.3, Math.min(0.95, override.opacity))]
|
|
511
|
+
: [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
|
|
512
|
+
let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
|
|
513
|
+
let bestScore = -Infinity;
|
|
514
|
+
for (const opacity of opacityCandidates) {
|
|
515
|
+
const blended = this.blend(accent, background, opacity);
|
|
516
|
+
const backgroundDelta = this.contrastRatio(blended, background);
|
|
517
|
+
const textContrast = this.contrastRatio(blended, text);
|
|
518
|
+
const score = backgroundDelta * 1.2 + textContrast * 0.8;
|
|
519
|
+
if (backgroundDelta >= 1.25 && textContrast >= 2.4) {
|
|
520
|
+
selectedOpacity = opacity;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
if (score > bestScore) {
|
|
524
|
+
bestScore = score;
|
|
525
|
+
selectedOpacity = opacity;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const sentenceOpacity = Math.max(0.24, Math.min(0.85, selectedOpacity * 0.55));
|
|
529
|
+
const underlineOpacity = Math.max(0.55, Math.min(0.95, selectedOpacity + 0.2));
|
|
530
|
+
const shadowOpacity = Math.max(0.22, Math.min(0.6, selectedOpacity * 0.45));
|
|
531
|
+
const underlineColor = text;
|
|
532
|
+
const underlineBlend = this.blend(underlineColor, background, underlineOpacity);
|
|
533
|
+
const underlineDelta = this.contrastRatio(underlineBlend, background);
|
|
534
|
+
const fallbackUnderline = this.relativeLuminance(background) > 0.45 ? [0, 0, 0] : [255, 255, 255];
|
|
535
|
+
const finalUnderline = underlineDelta >= 1.35 ? underlineColor : fallbackUnderline;
|
|
536
|
+
const wordHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${selectedOpacity})`;
|
|
537
|
+
const sentenceHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${sentenceOpacity})`;
|
|
538
|
+
const wordUnderline = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${underlineOpacity})`;
|
|
539
|
+
const wordShadow = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${shadowOpacity})`;
|
|
540
|
+
return {
|
|
541
|
+
wordHighlight,
|
|
542
|
+
sentenceHighlight,
|
|
543
|
+
wordUnderline,
|
|
544
|
+
wordShadow,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Adapt the TTS colors to `sourceEl`, the element being highlighted. Without
|
|
549
|
+
* one, to the element highlighted last on the page while it stays connected,
|
|
550
|
+
* else to the document.
|
|
551
|
+
*/
|
|
552
|
+
applyAdaptiveTTSStyle(sourceEl) {
|
|
553
|
+
if (typeof document === "undefined")
|
|
554
|
+
return;
|
|
555
|
+
if (sourceEl) {
|
|
556
|
+
this.ttsStyleSource = sourceEl;
|
|
557
|
+
ttsStyleInputs.source = sourceEl;
|
|
558
|
+
}
|
|
559
|
+
const source = sourceEl ??
|
|
560
|
+
(ttsStyleInputs.source?.isConnected ? ttsStyleInputs.source : null);
|
|
561
|
+
const vars = this.resolveAdaptiveTTSStyle(source);
|
|
562
|
+
const style = document.documentElement.style;
|
|
563
|
+
const properties = [
|
|
564
|
+
["--pie-tts-word-highlight", vars.wordHighlight],
|
|
565
|
+
["--pie-tts-sentence-highlight", vars.sentenceHighlight],
|
|
566
|
+
["--pie-tts-line-highlight", vars.sentenceHighlight],
|
|
567
|
+
["--pie-tts-word-underline", vars.wordUnderline],
|
|
568
|
+
["--pie-tts-word-shadow", vars.wordShadow],
|
|
569
|
+
];
|
|
570
|
+
for (const [name, value] of properties) {
|
|
571
|
+
// An unchanged value is not rewritten: the theme observer watches this
|
|
572
|
+
// element's style, and a write would notify it again.
|
|
573
|
+
if (style.getPropertyValue?.(name) === value)
|
|
574
|
+
continue;
|
|
575
|
+
style.setProperty(name, value);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** Register the shared highlights, unless another coordinator already did. */
|
|
579
|
+
initializeHighlights() {
|
|
580
|
+
if (!this.supported)
|
|
581
|
+
return;
|
|
582
|
+
sharedHighlight(TTS_WORD_HIGHLIGHT);
|
|
583
|
+
sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
584
|
+
for (const color of Object.values(HighlightColor)) {
|
|
585
|
+
sharedHighlight(annotationHighlightName(color));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Register CSS styles for highlights
|
|
590
|
+
*/
|
|
591
|
+
registerStyles() {
|
|
592
|
+
if (!this.supported)
|
|
593
|
+
return;
|
|
594
|
+
if (typeof document === "undefined")
|
|
595
|
+
return; // SSR guard
|
|
596
|
+
// Check if styles already exist
|
|
597
|
+
if (document.getElementById("pie-highlight-styles"))
|
|
598
|
+
return;
|
|
599
|
+
const style = document.createElement("style");
|
|
600
|
+
style.id = "pie-highlight-styles";
|
|
601
|
+
style.textContent = HIGHLIGHT_STYLES;
|
|
484
602
|
document.head.appendChild(style);
|
|
485
603
|
}
|
|
486
604
|
/**
|
|
487
|
-
* Highlight
|
|
605
|
+
* Highlight the word being read (temporary), replacing the previous one.
|
|
606
|
+
* Every range paints, so a word split across inline elements highlights
|
|
607
|
+
* whole. A range that covers elements also marks the replaced elements in it
|
|
608
|
+
* (svg, img, canvas), which a CSS range cannot paint.
|
|
488
609
|
*
|
|
489
|
-
* @param
|
|
490
|
-
* @param startOffset Start position in text node
|
|
491
|
-
* @param endOffset End position in text node
|
|
610
|
+
* @param ranges The word's ranges: one per tree it spans
|
|
492
611
|
*/
|
|
493
|
-
highlightTTSWord(
|
|
494
|
-
if (!this.
|
|
612
|
+
highlightTTSWord(ranges) {
|
|
613
|
+
if (!this.supported)
|
|
495
614
|
return;
|
|
496
|
-
this.applyAdaptiveTTSStyle(
|
|
497
|
-
// Clear previous word highlight
|
|
615
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
498
616
|
this.clearTTSWord();
|
|
499
|
-
|
|
500
|
-
const range
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
617
|
+
const highlight = sharedHighlight(TTS_WORD_HIGHLIGHT);
|
|
618
|
+
for (const range of ranges) {
|
|
619
|
+
adoptHighlightStylesFor(range.startContainer);
|
|
620
|
+
highlight.add(range);
|
|
621
|
+
this.ttsWordRanges.add(range);
|
|
622
|
+
}
|
|
623
|
+
this.highlightTTSWordElementFallbacks(ranges.filter((range) => range.startContainer !== range.endContainer ||
|
|
624
|
+
range.startContainer.nodeType !== Node.TEXT_NODE));
|
|
505
625
|
}
|
|
506
626
|
/**
|
|
507
627
|
* Highlight a single element as the active TTS word (temporary).
|
|
508
628
|
*
|
|
509
|
-
* Unlike {@link highlightTTSWord} (which paints
|
|
510
|
-
*
|
|
629
|
+
* Unlike {@link highlightTTSWord} (which paints CSS ranges over text) this
|
|
630
|
+
* marks the element itself via `data-pie-tts-word-element`. It is used
|
|
511
631
|
* for atomic targets that have no direct text node to range over — most
|
|
512
632
|
* notably MathJax CHTML tokens (e.g. `<mjx-mi><mjx-c/></mjx-mi>`, whose glyph
|
|
513
633
|
* lives in a font-driven pseudo-element a CSS range cannot paint) and
|
|
@@ -521,9 +641,10 @@ export class HighlightCoordinator {
|
|
|
521
641
|
* should paint the whole expression.
|
|
522
642
|
*/
|
|
523
643
|
highlightTTSWordElement(element) {
|
|
524
|
-
if (!this.
|
|
644
|
+
if (!this.supported)
|
|
525
645
|
return;
|
|
526
646
|
this.applyAdaptiveTTSStyle(element);
|
|
647
|
+
adoptHighlightStylesFor(element);
|
|
527
648
|
// Clear previous word highlight (CSS range and any element attributes)
|
|
528
649
|
this.clearTTSWord();
|
|
529
650
|
element.setAttribute("data-pie-tts-word-element", "true");
|
|
@@ -536,18 +657,16 @@ export class HighlightCoordinator {
|
|
|
536
657
|
* @param ranges Line (or sentence) ranges to paint with `--pie-tts-line-highlight`
|
|
537
658
|
*/
|
|
538
659
|
highlightTTSSentence(ranges) {
|
|
539
|
-
if (!this.
|
|
660
|
+
if (!this.supported)
|
|
540
661
|
return;
|
|
541
|
-
|
|
542
|
-
const sourceElement = source?.nodeType === Node.ELEMENT_NODE
|
|
543
|
-
? source
|
|
544
|
-
: source?.parentElement;
|
|
545
|
-
this.applyAdaptiveTTSStyle(sourceElement);
|
|
662
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
546
663
|
// Clear previous sentence highlight
|
|
547
664
|
this.clearTTSSentence();
|
|
548
|
-
|
|
665
|
+
const highlight = sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
549
666
|
for (const range of ranges) {
|
|
550
|
-
|
|
667
|
+
adoptHighlightStylesFor(range.startContainer);
|
|
668
|
+
highlight.add(range);
|
|
669
|
+
this.ttsSentenceRanges.add(range);
|
|
551
670
|
}
|
|
552
671
|
this.highlightTTSElementFallbacks(ranges);
|
|
553
672
|
}
|
|
@@ -557,11 +676,12 @@ export class HighlightCoordinator {
|
|
|
557
676
|
* @param elements Visible elements to paint with `--pie-tts-line-highlight`
|
|
558
677
|
*/
|
|
559
678
|
highlightTTSSentenceElements(elements) {
|
|
560
|
-
if (!this.
|
|
679
|
+
if (!this.supported)
|
|
561
680
|
return;
|
|
562
681
|
this.applyAdaptiveTTSStyle(elements[0] || null);
|
|
563
682
|
this.clearTTSSentence();
|
|
564
683
|
for (const element of elements) {
|
|
684
|
+
adoptHighlightStylesFor(element);
|
|
565
685
|
element.setAttribute("data-pie-tts-sentence-element", "true");
|
|
566
686
|
this.ttsSentenceElementHighlights.add(element);
|
|
567
687
|
}
|
|
@@ -587,36 +707,38 @@ export class HighlightCoordinator {
|
|
|
587
707
|
this.highlightElementFallbacks(ranges, "data-pie-tts-word-element", this.ttsWordElementHighlights, HighlightCoordinator.WORD_FALLBACK_SELECTOR);
|
|
588
708
|
}
|
|
589
709
|
highlightElementFallbacks(ranges, attributeName, trackedElements, selector) {
|
|
710
|
+
const elementAt = (node) => node.nodeType === Node.ELEMENT_NODE
|
|
711
|
+
? node
|
|
712
|
+
: composedParentElement(node);
|
|
590
713
|
for (const range of ranges) {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
714
|
+
// A range over a shadow root's top-level children has the root as its
|
|
715
|
+
// common ancestor, and the root is what to search.
|
|
716
|
+
const common = range.commonAncestorContainer;
|
|
717
|
+
const root = isShadowRootNode(common)
|
|
718
|
+
? common
|
|
719
|
+
: elementAt(common);
|
|
594
720
|
if (!root)
|
|
595
721
|
continue;
|
|
596
722
|
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;
|
|
723
|
+
const rangeStartElement = elementAt(range.startContainer);
|
|
724
|
+
const rangeEndElement = elementAt(range.endContainer);
|
|
603
725
|
for (const element of [rangeStartElement, rangeEndElement]) {
|
|
604
726
|
let current = element;
|
|
605
727
|
while (current) {
|
|
606
728
|
if (current.matches?.(selector))
|
|
607
729
|
candidates.push(current);
|
|
608
|
-
current = current
|
|
730
|
+
current = composedParentElement(current);
|
|
609
731
|
}
|
|
610
732
|
}
|
|
611
|
-
if (root.matches?.(selector)) {
|
|
733
|
+
if (!isShadowRootNode(root) && root.matches?.(selector)) {
|
|
612
734
|
candidates.push(root);
|
|
613
735
|
}
|
|
614
736
|
candidates.push(...Array.from(root.querySelectorAll(selector)));
|
|
615
737
|
for (const element of new Set(candidates)) {
|
|
616
738
|
try {
|
|
617
739
|
if (!range.intersectsNode(element) &&
|
|
618
|
-
!element
|
|
619
|
-
!element
|
|
740
|
+
!composedContains(element, rangeStartElement) &&
|
|
741
|
+
!composedContains(element, rangeEndElement)) {
|
|
620
742
|
continue;
|
|
621
743
|
}
|
|
622
744
|
}
|
|
@@ -632,9 +754,10 @@ export class HighlightCoordinator {
|
|
|
632
754
|
* Clear TTS word highlight
|
|
633
755
|
*/
|
|
634
756
|
clearTTSWord() {
|
|
635
|
-
if (!this.
|
|
757
|
+
if (!this.supported)
|
|
636
758
|
return;
|
|
637
|
-
this.
|
|
759
|
+
removeRanges(TTS_WORD_HIGHLIGHT, this.ttsWordRanges);
|
|
760
|
+
this.ttsWordRanges.clear();
|
|
638
761
|
for (const element of this.ttsWordElementHighlights) {
|
|
639
762
|
element.removeAttribute("data-pie-tts-word-element");
|
|
640
763
|
}
|
|
@@ -644,9 +767,10 @@ export class HighlightCoordinator {
|
|
|
644
767
|
* Clear TTS sentence highlight
|
|
645
768
|
*/
|
|
646
769
|
clearTTSSentence() {
|
|
647
|
-
if (!this.
|
|
770
|
+
if (!this.supported)
|
|
648
771
|
return;
|
|
649
|
-
this.
|
|
772
|
+
removeRanges(TTS_SENTENCE_HIGHLIGHT, this.ttsSentenceRanges);
|
|
773
|
+
this.ttsSentenceRanges.clear();
|
|
650
774
|
for (const element of this.ttsSentenceElementHighlights) {
|
|
651
775
|
element.removeAttribute("data-pie-tts-sentence-element");
|
|
652
776
|
}
|
|
@@ -658,6 +782,10 @@ export class HighlightCoordinator {
|
|
|
658
782
|
clearTTS() {
|
|
659
783
|
this.clearTTSWord();
|
|
660
784
|
this.clearTTSSentence();
|
|
785
|
+
if (ttsStyleInputs.source === this.ttsStyleSource) {
|
|
786
|
+
ttsStyleInputs.source = null;
|
|
787
|
+
}
|
|
788
|
+
this.ttsStyleSource = null;
|
|
661
789
|
}
|
|
662
790
|
/**
|
|
663
791
|
* Add an annotation highlight (persistent)
|
|
@@ -670,6 +798,7 @@ export class HighlightCoordinator {
|
|
|
670
798
|
const id = `annotation-${this.nextAnnotationId++}`;
|
|
671
799
|
// Clone the range to store
|
|
672
800
|
const clonedRange = range.cloneRange();
|
|
801
|
+
adoptHighlightStylesFor(clonedRange.startContainer);
|
|
673
802
|
// Store annotation data
|
|
674
803
|
const annotation = {
|
|
675
804
|
id,
|
|
@@ -679,11 +808,9 @@ export class HighlightCoordinator {
|
|
|
679
808
|
timestamp: Date.now(),
|
|
680
809
|
};
|
|
681
810
|
this.annotations.set(id, annotation);
|
|
682
|
-
//
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
if (colorHighlight) {
|
|
686
|
-
colorHighlight.add(clonedRange);
|
|
811
|
+
// The same range object, so it can later be deleted by reference.
|
|
812
|
+
if (this.supported) {
|
|
813
|
+
sharedHighlight(annotationHighlightName(color)).add(clonedRange);
|
|
687
814
|
}
|
|
688
815
|
return id;
|
|
689
816
|
}
|
|
@@ -698,26 +825,24 @@ export class HighlightCoordinator {
|
|
|
698
825
|
console.warn(`[HighlightCoordinator] Annotation ${id} not found`);
|
|
699
826
|
return;
|
|
700
827
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
const deleted = colorHighlight.delete(annotation.range);
|
|
706
|
-
console.log(`[HighlightCoordinator] Highlight.delete() returned:`, deleted);
|
|
828
|
+
if (this.supported) {
|
|
829
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
830
|
+
annotation.range,
|
|
831
|
+
]);
|
|
707
832
|
}
|
|
708
|
-
// Clean up
|
|
709
833
|
this.annotations.delete(id);
|
|
710
|
-
console.log(`[HighlightCoordinator] Annotation ${id} removed from map. Remaining:`, this.annotations.size);
|
|
711
834
|
}
|
|
712
835
|
/**
|
|
713
836
|
* Remove all annotations
|
|
714
837
|
*/
|
|
715
838
|
clearAnnotations() {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
839
|
+
if (this.supported) {
|
|
840
|
+
for (const annotation of this.annotations.values()) {
|
|
841
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
842
|
+
annotation.range,
|
|
843
|
+
]);
|
|
844
|
+
}
|
|
719
845
|
}
|
|
720
|
-
// Clear annotation data
|
|
721
846
|
this.annotations.clear();
|
|
722
847
|
}
|
|
723
848
|
/**
|
|
@@ -745,17 +870,10 @@ export class HighlightCoordinator {
|
|
|
745
870
|
const oldColor = annotation.color;
|
|
746
871
|
if (oldColor === newColor)
|
|
747
872
|
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
873
|
annotation.color = newColor;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
newColorHighlight.add(annotation.range);
|
|
874
|
+
if (this.supported) {
|
|
875
|
+
removeRanges(annotationHighlightName(oldColor), [annotation.range]);
|
|
876
|
+
sharedHighlight(annotationHighlightName(newColor)).add(annotation.range);
|
|
759
877
|
}
|
|
760
878
|
}
|
|
761
879
|
/**
|
|
@@ -796,34 +914,19 @@ export class HighlightCoordinator {
|
|
|
796
914
|
// HighlightCoordinatorApi interface implementation
|
|
797
915
|
// ============================================================================
|
|
798
916
|
/**
|
|
799
|
-
* Highlight a
|
|
800
|
-
*
|
|
801
|
-
* Generic method that adapts to the specific highlight type.
|
|
802
|
-
* For more control, use the specific methods like highlightTTSWord().
|
|
917
|
+
* Highlight a range as the given type: the TTS types paint as
|
|
918
|
+
* {@link highlightTTSWord} and {@link highlightTTSSentence} do.
|
|
803
919
|
*/
|
|
804
920
|
highlightRange(range, type, color = HighlightColor.YELLOW) {
|
|
805
921
|
if (!this.supported)
|
|
806
922
|
return;
|
|
807
923
|
switch (type) {
|
|
808
924
|
case HighlightType.TTS_WORD:
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
-
}
|
|
925
|
+
this.highlightTTSWord([range]);
|
|
926
|
+
break;
|
|
927
|
+
case HighlightType.TTS_SENTENCE:
|
|
928
|
+
this.highlightTTSSentence([range]);
|
|
825
929
|
break;
|
|
826
|
-
}
|
|
827
930
|
case HighlightType.ANNOTATION:
|
|
828
931
|
// For annotations, use the existing method
|
|
829
932
|
this.addAnnotation(range, color);
|
|
@@ -891,24 +994,27 @@ export class HighlightCoordinator {
|
|
|
891
994
|
const rgb = hexToRgb(color);
|
|
892
995
|
if (!rgb)
|
|
893
996
|
return;
|
|
894
|
-
|
|
997
|
+
ttsStyleInputs.override = {
|
|
895
998
|
color: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
896
999
|
opacity: Math.max(0.2, Math.min(0.95, opacity)),
|
|
1000
|
+
owner: this,
|
|
897
1001
|
};
|
|
898
1002
|
this.applyAdaptiveTTSStyle();
|
|
899
1003
|
}
|
|
900
1004
|
/**
|
|
901
|
-
*
|
|
1005
|
+
* Remove this coordinator's ranges and stop its observer.
|
|
902
1006
|
*
|
|
903
|
-
* The stylesheet
|
|
904
|
-
*
|
|
905
|
-
* single bounded style element installed for the lifetime of the document.
|
|
1007
|
+
* The stylesheet and the registered highlights are page-wide and shared by
|
|
1008
|
+
* every live coordinator, so they stay installed for the document's lifetime.
|
|
906
1009
|
*/
|
|
907
1010
|
destroy() {
|
|
908
1011
|
if (!this.supported)
|
|
909
1012
|
return;
|
|
910
1013
|
this.clearTTS();
|
|
911
1014
|
this.clearAnnotations();
|
|
1015
|
+
if (ttsStyleInputs.override?.owner === this) {
|
|
1016
|
+
ttsStyleInputs.override = null;
|
|
1017
|
+
}
|
|
912
1018
|
this.themeObserver?.disconnect();
|
|
913
1019
|
this.themeObserver = null;
|
|
914
1020
|
}
|