@pie-players/pie-assessment-toolkit 0.3.73 → 0.3.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +469 -318
- package/dist/attempt/AssessmentSession.d.ts +1 -5
- package/dist/attempt/AssessmentSession.js +0 -29
- 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.d.ts +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.d.ts +1 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +13 -13
- package/dist/components/SectionToolBar.custom-element.d.ts +1 -0
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-7tt7v2xp.js +4 -0
- package/dist/components/chunks/ItemToolBar-97hx60z4.js +2 -0
- 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 +32 -6
- package/dist/index.d.ts +23 -48
- package/dist/index.js +12 -37
- package/dist/policy/core/PolicySource.d.ts +1 -1
- package/dist/policy/core/ToolPolicyEngine.d.ts +85 -34
- package/dist/policy/core/ToolPolicyEngine.js +148 -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 +14 -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 +28 -10
- package/dist/runtime/tool-host-contract.js +31 -10
- 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 +22 -28
- package/dist/services/HighlightCoordinator.js +431 -327
- package/dist/services/RangeSerializer.d.ts +10 -1
- package/dist/services/RangeSerializer.js +87 -14
- package/dist/services/TTSService.d.ts +130 -69
- package/dist/services/TTSService.js +713 -570
- package/dist/services/ToolCoordinator.d.ts +43 -66
- package/dist/services/ToolCoordinator.js +164 -208
- package/dist/services/ToolRegistry.d.ts +78 -54
- package/dist/services/ToolRegistry.js +107 -105
- package/dist/services/ToolkitCoordinator.d.ts +261 -175
- package/dist/services/ToolkitCoordinator.js +667 -433
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +11 -1
- package/dist/services/catalog-owner.js +25 -0
- package/dist/services/create-tools-config.d.ts +2 -2
- 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 +30 -8
- package/dist/services/framework-error.js +9 -7
- package/dist/services/interfaces.d.ts +158 -129
- package/dist/services/section-controller-types.d.ts +15 -0
- 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 +1 -3
- 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 +14 -4
- package/dist/services/tool-config-validation.js +63 -71
- package/dist/services/tool-context.d.ts +0 -8
- package/dist/services/tool-context.js +29 -25
- 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 +41 -80
- package/dist/services/tool-providers/TTSToolProvider.js +81 -40
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -4
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +32 -28
- package/dist/services/tool-providers/ToolProviderRegistry.js +121 -107
- package/dist/services/tool-providers/index.d.ts +0 -9
- package/dist/services/tool-providers/index.js +2 -4
- 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 +37 -23
- 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/catalog-span-alignment.d.ts +2 -17
- package/dist/services/tts/catalog-span-alignment.js +17 -142
- 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/generated-speech/types.d.ts +0 -12
- package/dist/services/tts/highlight-pipeline/boundary-normalizer.js +12 -62
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +38 -5
- package/dist/services/tts/highlight-pipeline/normalize-speech-chunks.js +3 -6
- 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-alignment/index.d.ts +0 -5
- package/dist/services/tts/math-alignment/index.js +5 -25
- package/dist/services/tts/math-alignment/speech-tokenizer.d.ts +9 -1
- package/dist/services/tts/math-alignment/speech-tokenizer.js +38 -36
- package/dist/services/tts/math-alignment/types.d.ts +4 -2
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +74 -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 +16 -1
- package/dist/services/tts/math-speech.js +55 -19
- package/dist/services/tts/provider-options.d.ts +41 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/sre-engine.d.ts +7 -0
- package/dist/services/tts/sre-engine.js +11 -0
- package/dist/services/tts/sre-locales.d.ts +14 -0
- package/dist/services/tts/sre-locales.js +57 -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 -1
- package/dist/services/tts/text-processing.js +46 -34
- 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 +44 -18
- package/dist/services/tts-runtime-config.js +22 -93
- 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 +24 -59
- 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-byq80z42.js +0 -40
- package/dist/components/chunks/ItemToolBar-d36g3aw4.js +0 -49
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -51
- 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 -80
- package/dist/runtime/section-runtime-engine-host-context.js +0 -95
- 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/ToolConfigResolver.d.ts +0 -161
- package/dist/services/ToolConfigResolver.js +0 -229
- package/dist/services/catalog-media.d.ts +0 -56
- package/dist/services/catalog-media.js +0 -190
- package/dist/services/createDefaultToolRegistry.d.ts +0 -53
- package/dist/services/createDefaultToolRegistry.js +0 -45
- 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/tool-providers/CortexToolProvider.d.ts +0 -13
- package/dist/services/tool-providers/CortexToolProvider.js +0 -32
- package/dist/services/tool-providers/DesmosToolProvider.d.ts +0 -14
- package/dist/services/tool-providers/DesmosToolProvider.js +0 -32
- package/dist/services/tool-providers/GeoGebraToolProvider.d.ts +0 -16
- package/dist/services/tool-providers/GeoGebraToolProvider.js +0 -32
- package/dist/services/tool-providers/LazyCalculatorToolProvider.d.ts +0 -35
- package/dist/services/tool-providers/LazyCalculatorToolProvider.js +0 -95
- package/dist/services/tts/visual-line-ranges.d.ts +0 -18
- package/dist/services/tts/visual-line-ranges.js +0 -226
- package/dist/tools/client.d.ts +0 -12
- package/dist/tools/client.js +0 -14
- package/dist/tools/index.d.ts +0 -8
- package/dist/tools/index.js +0 -10
- package/dist/tools/internal.d.ts +0 -43
- package/dist/tools/internal.js +0 -41
- package/dist/tools/response-discovery.d.ts +0 -61
- package/dist/tools/response-discovery.js +0 -113
- package/dist/tools/types.d.ts +0 -337
- package/dist/tools/types.js +0 -12
- package/dist/tools/variant-resolver.d.ts +0 -41
- package/dist/tools/variant-resolver.js +0 -207
|
@@ -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,237 +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
|
-
// Shared highlights per color (one Highlight object per color, contains all ranges)
|
|
53
|
-
colorHighlights = new Map();
|
|
54
|
-
nextAnnotationId = 1;
|
|
55
|
-
supported = false;
|
|
56
|
-
rangeSerializer;
|
|
57
|
-
themeObserver = null;
|
|
58
|
-
explicitTTSColorOverride = null;
|
|
59
|
-
constructor(config = {}) {
|
|
60
|
-
this.config = config;
|
|
61
|
-
this.rangeSerializer = new RangeSerializer();
|
|
62
|
-
// SSR guard
|
|
63
|
-
if (typeof CSS === "undefined" || !("highlights" in CSS)) {
|
|
64
|
-
console.warn("CSS Custom Highlight API not supported (SSR or unsupported browser)");
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
this.supported = true;
|
|
68
|
-
this.initializeHighlights();
|
|
69
|
-
this.registerStyles();
|
|
70
|
-
this.applyAdaptiveTTSStyle();
|
|
71
|
-
this.setupThemeObservation();
|
|
72
|
-
}
|
|
73
|
-
setupThemeObservation() {
|
|
74
|
-
if (typeof document === "undefined")
|
|
75
|
-
return;
|
|
76
|
-
if (typeof MutationObserver === "undefined")
|
|
77
|
-
return;
|
|
78
|
-
const refresh = () => this.applyAdaptiveTTSStyle();
|
|
79
|
-
this.themeObserver = new MutationObserver(refresh);
|
|
80
|
-
this.themeObserver.observe(document.documentElement, {
|
|
81
|
-
attributes: true,
|
|
82
|
-
attributeFilter: ["style", "data-theme", "data-color-scheme", "class"],
|
|
83
|
-
});
|
|
84
|
-
for (const host of document.querySelectorAll("pie-theme")) {
|
|
85
|
-
this.themeObserver.observe(host, {
|
|
86
|
-
attributes: true,
|
|
87
|
-
attributeFilter: [
|
|
88
|
-
"theme",
|
|
89
|
-
"scheme",
|
|
90
|
-
"provider",
|
|
91
|
-
"variables",
|
|
92
|
-
"style",
|
|
93
|
-
"data-theme",
|
|
94
|
-
"data-color-scheme",
|
|
95
|
-
],
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
parseColor(input) {
|
|
100
|
-
if (!input)
|
|
101
|
-
return null;
|
|
102
|
-
const value = input.trim();
|
|
103
|
-
if (!value)
|
|
104
|
-
return null;
|
|
105
|
-
const hexMatch = /^#([a-f\d]{3}|[a-f\d]{6})$/i.exec(value);
|
|
106
|
-
if (hexMatch) {
|
|
107
|
-
const hex = hexMatch[1];
|
|
108
|
-
if (hex.length === 3) {
|
|
109
|
-
return [
|
|
110
|
-
parseInt(hex[0] + hex[0], 16),
|
|
111
|
-
parseInt(hex[1] + hex[1], 16),
|
|
112
|
-
parseInt(hex[2] + hex[2], 16),
|
|
113
|
-
];
|
|
114
|
-
}
|
|
115
|
-
return [
|
|
116
|
-
parseInt(hex.slice(0, 2), 16),
|
|
117
|
-
parseInt(hex.slice(2, 4), 16),
|
|
118
|
-
parseInt(hex.slice(4, 6), 16),
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
const rgbMatch = /^rgba?\((.+)\)$/i.exec(value);
|
|
122
|
-
if (rgbMatch) {
|
|
123
|
-
const normalized = rgbMatch[1].replace(/\//g, ",");
|
|
124
|
-
const parts = normalized
|
|
125
|
-
.split(/[,\s]+/)
|
|
126
|
-
.map((part) => part.trim())
|
|
127
|
-
.filter(Boolean);
|
|
128
|
-
const r = Number(parts[0]);
|
|
129
|
-
const g = Number(parts[1]);
|
|
130
|
-
const b = Number(parts[2]);
|
|
131
|
-
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
|
|
132
|
-
return [r, g, b];
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
// Browser parser fallback (covers formats like oklch()).
|
|
136
|
-
if (typeof document !== "undefined") {
|
|
137
|
-
const parserEl = document.createElement("span");
|
|
138
|
-
parserEl.style.color = value;
|
|
139
|
-
if (typeof parserEl.style.color === "string" && parserEl.style.color) {
|
|
140
|
-
document.body?.appendChild(parserEl);
|
|
141
|
-
const resolved = typeof getComputedStyle === "function"
|
|
142
|
-
? getComputedStyle(parserEl).color
|
|
143
|
-
: "";
|
|
144
|
-
parserEl.remove();
|
|
145
|
-
const normalizedResolved = resolved.trim();
|
|
146
|
-
// Guard against recursive loops when computed style returns the same
|
|
147
|
-
// unresolved function syntax (observed with certain color formats).
|
|
148
|
-
if (normalizedResolved && normalizedResolved !== value) {
|
|
149
|
-
return this.parseColor(resolved);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return null;
|
|
154
|
-
}
|
|
155
|
-
relativeLuminance([r, g, b]) {
|
|
156
|
-
const toLinear = (channel) => {
|
|
157
|
-
const n = channel / 255;
|
|
158
|
-
return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
|
|
159
|
-
};
|
|
160
|
-
const lr = toLinear(r);
|
|
161
|
-
const lg = toLinear(g);
|
|
162
|
-
const lb = toLinear(b);
|
|
163
|
-
return 0.2126 * lr + 0.7152 * lg + 0.0722 * lb;
|
|
164
|
-
}
|
|
165
|
-
contrastRatio(a, b) {
|
|
166
|
-
const la = this.relativeLuminance(a);
|
|
167
|
-
const lb = this.relativeLuminance(b);
|
|
168
|
-
const lighter = Math.max(la, lb);
|
|
169
|
-
const darker = Math.min(la, lb);
|
|
170
|
-
return (lighter + 0.05) / (darker + 0.05);
|
|
171
|
-
}
|
|
172
|
-
blend(fg, bg, alpha) {
|
|
173
|
-
return [
|
|
174
|
-
Math.round(fg[0] * alpha + bg[0] * (1 - alpha)),
|
|
175
|
-
Math.round(fg[1] * alpha + bg[1] * (1 - alpha)),
|
|
176
|
-
Math.round(fg[2] * alpha + bg[2] * (1 - alpha)),
|
|
177
|
-
];
|
|
178
|
-
}
|
|
179
|
-
resolveAdaptiveTTSStyle(sourceEl) {
|
|
180
|
-
const fallbackMissing = [255, 235, 59];
|
|
181
|
-
const fallbackText = [17, 24, 39];
|
|
182
|
-
const fallbackBackground = [255, 255, 255];
|
|
183
|
-
const target = sourceEl ||
|
|
184
|
-
(typeof document !== "undefined" ? document.documentElement : null);
|
|
185
|
-
const computed = target && typeof getComputedStyle === "function"
|
|
186
|
-
? getComputedStyle(target)
|
|
187
|
-
: null;
|
|
188
|
-
const background = this.parseColor(computed?.getPropertyValue("--pie-background") ||
|
|
189
|
-
computed?.backgroundColor) || fallbackBackground;
|
|
190
|
-
const text = this.parseColor(computed?.getPropertyValue("--pie-text")) || fallbackText;
|
|
191
|
-
const accent = this.explicitTTSColorOverride
|
|
192
|
-
? this.parseColor(this.explicitTTSColorOverride.color) || fallbackMissing
|
|
193
|
-
: this.parseColor(computed?.getPropertyValue("--pie-missing")) ||
|
|
194
|
-
fallbackMissing;
|
|
195
|
-
const opacityCandidates = this.explicitTTSColorOverride
|
|
196
|
-
? [Math.max(0.3, Math.min(0.95, this.explicitTTSColorOverride.opacity))]
|
|
197
|
-
: [0.8, 0.72, 0.68, 0.62, 0.56, 0.5];
|
|
198
|
-
let selectedOpacity = opacityCandidates[opacityCandidates.length - 1];
|
|
199
|
-
let bestScore = -Infinity;
|
|
200
|
-
for (const opacity of opacityCandidates) {
|
|
201
|
-
const blended = this.blend(accent, background, opacity);
|
|
202
|
-
const backgroundDelta = this.contrastRatio(blended, background);
|
|
203
|
-
const textContrast = this.contrastRatio(blended, text);
|
|
204
|
-
const score = backgroundDelta * 1.2 + textContrast * 0.8;
|
|
205
|
-
if (backgroundDelta >= 1.25 && textContrast >= 2.4) {
|
|
206
|
-
selectedOpacity = opacity;
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
if (score > bestScore) {
|
|
210
|
-
bestScore = score;
|
|
211
|
-
selectedOpacity = opacity;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const sentenceOpacity = Math.max(0.24, Math.min(0.85, selectedOpacity * 0.55));
|
|
215
|
-
const underlineOpacity = Math.max(0.55, Math.min(0.95, selectedOpacity + 0.2));
|
|
216
|
-
const shadowOpacity = Math.max(0.22, Math.min(0.6, selectedOpacity * 0.45));
|
|
217
|
-
const underlineColor = text;
|
|
218
|
-
const underlineBlend = this.blend(underlineColor, background, underlineOpacity);
|
|
219
|
-
const underlineDelta = this.contrastRatio(underlineBlend, background);
|
|
220
|
-
const fallbackUnderline = this.relativeLuminance(background) > 0.45 ? [0, 0, 0] : [255, 255, 255];
|
|
221
|
-
const finalUnderline = underlineDelta >= 1.35 ? underlineColor : fallbackUnderline;
|
|
222
|
-
const wordHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${selectedOpacity})`;
|
|
223
|
-
const sentenceHighlight = `rgba(${accent[0]}, ${accent[1]}, ${accent[2]}, ${sentenceOpacity})`;
|
|
224
|
-
const wordUnderline = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${underlineOpacity})`;
|
|
225
|
-
const wordShadow = `rgba(${finalUnderline[0]}, ${finalUnderline[1]}, ${finalUnderline[2]}, ${shadowOpacity})`;
|
|
226
|
-
return {
|
|
227
|
-
wordHighlight,
|
|
228
|
-
sentenceHighlight,
|
|
229
|
-
wordUnderline,
|
|
230
|
-
wordShadow,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
applyAdaptiveTTSStyle(sourceEl) {
|
|
234
|
-
if (typeof document === "undefined")
|
|
235
|
-
return;
|
|
236
|
-
const vars = this.resolveAdaptiveTTSStyle(sourceEl);
|
|
237
|
-
document.documentElement.style.setProperty("--pie-tts-word-highlight", vars.wordHighlight);
|
|
238
|
-
document.documentElement.style.setProperty("--pie-tts-sentence-highlight", vars.sentenceHighlight);
|
|
239
|
-
document.documentElement.style.setProperty("--pie-tts-line-highlight", vars.sentenceHighlight);
|
|
240
|
-
document.documentElement.style.setProperty("--pie-tts-word-underline", vars.wordUnderline);
|
|
241
|
-
document.documentElement.style.setProperty("--pie-tts-word-shadow", vars.wordShadow);
|
|
242
|
-
}
|
|
243
|
-
/**
|
|
244
|
-
* Initialize CSS Custom Highlights
|
|
245
|
-
*/
|
|
246
|
-
initializeHighlights() {
|
|
247
|
-
if (!this.supported)
|
|
248
|
-
return;
|
|
249
|
-
// Create highlight registries for TTS
|
|
250
|
-
this.ttsWordHighlight = new Highlight();
|
|
251
|
-
this.ttsSentenceHighlight = new Highlight();
|
|
252
|
-
// Register TTS highlights with CSS
|
|
253
|
-
CSS.highlights.set("tts-word", this.ttsWordHighlight);
|
|
254
|
-
CSS.highlights.set("tts-sentence", this.ttsSentenceHighlight);
|
|
255
|
-
// Create shared highlights for each annotation color
|
|
256
|
-
for (const color of Object.values(HighlightColor)) {
|
|
257
|
-
const highlight = new Highlight();
|
|
258
|
-
this.colorHighlights.set(color, highlight);
|
|
259
|
-
CSS.highlights.set(`annotation-${color}`, highlight);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
/**
|
|
263
|
-
* Register CSS styles for highlights
|
|
264
|
-
*/
|
|
265
|
-
registerStyles() {
|
|
266
|
-
if (!this.supported)
|
|
267
|
-
return;
|
|
268
|
-
if (typeof document === "undefined")
|
|
269
|
-
return; // SSR guard
|
|
270
|
-
// Check if styles already exist
|
|
271
|
-
if (document.getElementById("pie-highlight-styles"))
|
|
272
|
-
return;
|
|
273
|
-
const style = document.createElement("style");
|
|
274
|
-
style.id = "pie-highlight-styles";
|
|
275
|
-
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 = `
|
|
276
53
|
/* TTS highlights - temporary */
|
|
277
54
|
::highlight(tts-word) {
|
|
278
55
|
background-color: var(--pie-tts-word-highlight, color-mix(in srgb, var(--pie-missing, #ffeb3b) 68%, transparent));
|
|
@@ -483,33 +260,374 @@ export class HighlightCoordinator {
|
|
|
483
260
|
}
|
|
484
261
|
}
|
|
485
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;
|
|
486
602
|
document.head.appendChild(style);
|
|
487
603
|
}
|
|
488
604
|
/**
|
|
489
|
-
* 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.
|
|
490
609
|
*
|
|
491
|
-
* @param
|
|
492
|
-
* @param startOffset Start position in text node
|
|
493
|
-
* @param endOffset End position in text node
|
|
610
|
+
* @param ranges The word's ranges: one per tree it spans
|
|
494
611
|
*/
|
|
495
|
-
highlightTTSWord(
|
|
496
|
-
if (!this.
|
|
612
|
+
highlightTTSWord(ranges) {
|
|
613
|
+
if (!this.supported)
|
|
497
614
|
return;
|
|
498
|
-
this.applyAdaptiveTTSStyle(
|
|
499
|
-
// Clear previous word highlight
|
|
615
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
500
616
|
this.clearTTSWord();
|
|
501
|
-
|
|
502
|
-
const range
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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));
|
|
507
625
|
}
|
|
508
626
|
/**
|
|
509
627
|
* Highlight a single element as the active TTS word (temporary).
|
|
510
628
|
*
|
|
511
|
-
* Unlike {@link highlightTTSWord} (which paints
|
|
512
|
-
*
|
|
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
|
|
513
631
|
* for atomic targets that have no direct text node to range over — most
|
|
514
632
|
* notably MathJax CHTML tokens (e.g. `<mjx-mi><mjx-c/></mjx-mi>`, whose glyph
|
|
515
633
|
* lives in a font-driven pseudo-element a CSS range cannot paint) and
|
|
@@ -523,9 +641,10 @@ export class HighlightCoordinator {
|
|
|
523
641
|
* should paint the whole expression.
|
|
524
642
|
*/
|
|
525
643
|
highlightTTSWordElement(element) {
|
|
526
|
-
if (!this.
|
|
644
|
+
if (!this.supported)
|
|
527
645
|
return;
|
|
528
646
|
this.applyAdaptiveTTSStyle(element);
|
|
647
|
+
adoptHighlightStylesFor(element);
|
|
529
648
|
// Clear previous word highlight (CSS range and any element attributes)
|
|
530
649
|
this.clearTTSWord();
|
|
531
650
|
element.setAttribute("data-pie-tts-word-element", "true");
|
|
@@ -538,18 +657,16 @@ export class HighlightCoordinator {
|
|
|
538
657
|
* @param ranges Line (or sentence) ranges to paint with `--pie-tts-line-highlight`
|
|
539
658
|
*/
|
|
540
659
|
highlightTTSSentence(ranges) {
|
|
541
|
-
if (!this.
|
|
660
|
+
if (!this.supported)
|
|
542
661
|
return;
|
|
543
|
-
|
|
544
|
-
const sourceElement = source?.nodeType === Node.ELEMENT_NODE
|
|
545
|
-
? source
|
|
546
|
-
: source?.parentElement;
|
|
547
|
-
this.applyAdaptiveTTSStyle(sourceElement);
|
|
662
|
+
this.applyAdaptiveTTSStyle(startElementOf(ranges));
|
|
548
663
|
// Clear previous sentence highlight
|
|
549
664
|
this.clearTTSSentence();
|
|
550
|
-
|
|
665
|
+
const highlight = sharedHighlight(TTS_SENTENCE_HIGHLIGHT);
|
|
551
666
|
for (const range of ranges) {
|
|
552
|
-
|
|
667
|
+
adoptHighlightStylesFor(range.startContainer);
|
|
668
|
+
highlight.add(range);
|
|
669
|
+
this.ttsSentenceRanges.add(range);
|
|
553
670
|
}
|
|
554
671
|
this.highlightTTSElementFallbacks(ranges);
|
|
555
672
|
}
|
|
@@ -559,11 +676,12 @@ export class HighlightCoordinator {
|
|
|
559
676
|
* @param elements Visible elements to paint with `--pie-tts-line-highlight`
|
|
560
677
|
*/
|
|
561
678
|
highlightTTSSentenceElements(elements) {
|
|
562
|
-
if (!this.
|
|
679
|
+
if (!this.supported)
|
|
563
680
|
return;
|
|
564
681
|
this.applyAdaptiveTTSStyle(elements[0] || null);
|
|
565
682
|
this.clearTTSSentence();
|
|
566
683
|
for (const element of elements) {
|
|
684
|
+
adoptHighlightStylesFor(element);
|
|
567
685
|
element.setAttribute("data-pie-tts-sentence-element", "true");
|
|
568
686
|
this.ttsSentenceElementHighlights.add(element);
|
|
569
687
|
}
|
|
@@ -589,36 +707,38 @@ export class HighlightCoordinator {
|
|
|
589
707
|
this.highlightElementFallbacks(ranges, "data-pie-tts-word-element", this.ttsWordElementHighlights, HighlightCoordinator.WORD_FALLBACK_SELECTOR);
|
|
590
708
|
}
|
|
591
709
|
highlightElementFallbacks(ranges, attributeName, trackedElements, selector) {
|
|
710
|
+
const elementAt = (node) => node.nodeType === Node.ELEMENT_NODE
|
|
711
|
+
? node
|
|
712
|
+
: composedParentElement(node);
|
|
592
713
|
for (const range of ranges) {
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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);
|
|
596
720
|
if (!root)
|
|
597
721
|
continue;
|
|
598
722
|
const candidates = [];
|
|
599
|
-
const rangeStartElement = range.startContainer
|
|
600
|
-
|
|
601
|
-
: range.startContainer.parentElement;
|
|
602
|
-
const rangeEndElement = range.endContainer.nodeType === Node.ELEMENT_NODE
|
|
603
|
-
? range.endContainer
|
|
604
|
-
: range.endContainer.parentElement;
|
|
723
|
+
const rangeStartElement = elementAt(range.startContainer);
|
|
724
|
+
const rangeEndElement = elementAt(range.endContainer);
|
|
605
725
|
for (const element of [rangeStartElement, rangeEndElement]) {
|
|
606
726
|
let current = element;
|
|
607
727
|
while (current) {
|
|
608
728
|
if (current.matches?.(selector))
|
|
609
729
|
candidates.push(current);
|
|
610
|
-
current = current
|
|
730
|
+
current = composedParentElement(current);
|
|
611
731
|
}
|
|
612
732
|
}
|
|
613
|
-
if (root.matches?.(selector)) {
|
|
733
|
+
if (!isShadowRootNode(root) && root.matches?.(selector)) {
|
|
614
734
|
candidates.push(root);
|
|
615
735
|
}
|
|
616
736
|
candidates.push(...Array.from(root.querySelectorAll(selector)));
|
|
617
737
|
for (const element of new Set(candidates)) {
|
|
618
738
|
try {
|
|
619
739
|
if (!range.intersectsNode(element) &&
|
|
620
|
-
!element
|
|
621
|
-
!element
|
|
740
|
+
!composedContains(element, rangeStartElement) &&
|
|
741
|
+
!composedContains(element, rangeEndElement)) {
|
|
622
742
|
continue;
|
|
623
743
|
}
|
|
624
744
|
}
|
|
@@ -634,9 +754,10 @@ export class HighlightCoordinator {
|
|
|
634
754
|
* Clear TTS word highlight
|
|
635
755
|
*/
|
|
636
756
|
clearTTSWord() {
|
|
637
|
-
if (!this.
|
|
757
|
+
if (!this.supported)
|
|
638
758
|
return;
|
|
639
|
-
this.
|
|
759
|
+
removeRanges(TTS_WORD_HIGHLIGHT, this.ttsWordRanges);
|
|
760
|
+
this.ttsWordRanges.clear();
|
|
640
761
|
for (const element of this.ttsWordElementHighlights) {
|
|
641
762
|
element.removeAttribute("data-pie-tts-word-element");
|
|
642
763
|
}
|
|
@@ -646,9 +767,10 @@ export class HighlightCoordinator {
|
|
|
646
767
|
* Clear TTS sentence highlight
|
|
647
768
|
*/
|
|
648
769
|
clearTTSSentence() {
|
|
649
|
-
if (!this.
|
|
770
|
+
if (!this.supported)
|
|
650
771
|
return;
|
|
651
|
-
this.
|
|
772
|
+
removeRanges(TTS_SENTENCE_HIGHLIGHT, this.ttsSentenceRanges);
|
|
773
|
+
this.ttsSentenceRanges.clear();
|
|
652
774
|
for (const element of this.ttsSentenceElementHighlights) {
|
|
653
775
|
element.removeAttribute("data-pie-tts-sentence-element");
|
|
654
776
|
}
|
|
@@ -660,6 +782,10 @@ export class HighlightCoordinator {
|
|
|
660
782
|
clearTTS() {
|
|
661
783
|
this.clearTTSWord();
|
|
662
784
|
this.clearTTSSentence();
|
|
785
|
+
if (ttsStyleInputs.source === this.ttsStyleSource) {
|
|
786
|
+
ttsStyleInputs.source = null;
|
|
787
|
+
}
|
|
788
|
+
this.ttsStyleSource = null;
|
|
663
789
|
}
|
|
664
790
|
/**
|
|
665
791
|
* Add an annotation highlight (persistent)
|
|
@@ -672,6 +798,7 @@ export class HighlightCoordinator {
|
|
|
672
798
|
const id = `annotation-${this.nextAnnotationId++}`;
|
|
673
799
|
// Clone the range to store
|
|
674
800
|
const clonedRange = range.cloneRange();
|
|
801
|
+
adoptHighlightStylesFor(clonedRange.startContainer);
|
|
675
802
|
// Store annotation data
|
|
676
803
|
const annotation = {
|
|
677
804
|
id,
|
|
@@ -681,11 +808,9 @@ export class HighlightCoordinator {
|
|
|
681
808
|
timestamp: Date.now(),
|
|
682
809
|
};
|
|
683
810
|
this.annotations.set(id, annotation);
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (colorHighlight) {
|
|
688
|
-
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);
|
|
689
814
|
}
|
|
690
815
|
return id;
|
|
691
816
|
}
|
|
@@ -700,26 +825,24 @@ export class HighlightCoordinator {
|
|
|
700
825
|
console.warn(`[HighlightCoordinator] Annotation ${id} not found`);
|
|
701
826
|
return;
|
|
702
827
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const deleted = colorHighlight.delete(annotation.range);
|
|
708
|
-
console.log(`[HighlightCoordinator] Highlight.delete() returned:`, deleted);
|
|
828
|
+
if (this.supported) {
|
|
829
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
830
|
+
annotation.range,
|
|
831
|
+
]);
|
|
709
832
|
}
|
|
710
|
-
// Clean up
|
|
711
833
|
this.annotations.delete(id);
|
|
712
|
-
console.log(`[HighlightCoordinator] Annotation ${id} removed from map. Remaining:`, this.annotations.size);
|
|
713
834
|
}
|
|
714
835
|
/**
|
|
715
836
|
* Remove all annotations
|
|
716
837
|
*/
|
|
717
838
|
clearAnnotations() {
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
839
|
+
if (this.supported) {
|
|
840
|
+
for (const annotation of this.annotations.values()) {
|
|
841
|
+
removeRanges(annotationHighlightName(annotation.color), [
|
|
842
|
+
annotation.range,
|
|
843
|
+
]);
|
|
844
|
+
}
|
|
721
845
|
}
|
|
722
|
-
// Clear annotation data
|
|
723
846
|
this.annotations.clear();
|
|
724
847
|
}
|
|
725
848
|
/**
|
|
@@ -747,17 +870,10 @@ export class HighlightCoordinator {
|
|
|
747
870
|
const oldColor = annotation.color;
|
|
748
871
|
if (oldColor === newColor)
|
|
749
872
|
return;
|
|
750
|
-
// Remove from old color highlight
|
|
751
|
-
const oldColorHighlight = this.colorHighlights.get(oldColor);
|
|
752
|
-
if (oldColorHighlight) {
|
|
753
|
-
oldColorHighlight.delete(annotation.range);
|
|
754
|
-
}
|
|
755
|
-
// Update color
|
|
756
873
|
annotation.color = newColor;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
newColorHighlight.add(annotation.range);
|
|
874
|
+
if (this.supported) {
|
|
875
|
+
removeRanges(annotationHighlightName(oldColor), [annotation.range]);
|
|
876
|
+
sharedHighlight(annotationHighlightName(newColor)).add(annotation.range);
|
|
761
877
|
}
|
|
762
878
|
}
|
|
763
879
|
/**
|
|
@@ -798,34 +914,19 @@ export class HighlightCoordinator {
|
|
|
798
914
|
// HighlightCoordinatorApi interface implementation
|
|
799
915
|
// ============================================================================
|
|
800
916
|
/**
|
|
801
|
-
* Highlight a
|
|
802
|
-
*
|
|
803
|
-
* Generic method that adapts to the specific highlight type.
|
|
804
|
-
* 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.
|
|
805
919
|
*/
|
|
806
920
|
highlightRange(range, type, color = HighlightColor.YELLOW) {
|
|
807
921
|
if (!this.supported)
|
|
808
922
|
return;
|
|
809
923
|
switch (type) {
|
|
810
924
|
case HighlightType.TTS_WORD:
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
: this.ttsSentenceHighlight;
|
|
816
|
-
if (highlight) {
|
|
817
|
-
highlight.clear();
|
|
818
|
-
highlight.add(range);
|
|
819
|
-
if (type === HighlightType.TTS_WORD) {
|
|
820
|
-
for (const element of this.ttsWordElementHighlights) {
|
|
821
|
-
element.removeAttribute("data-pie-tts-word-element");
|
|
822
|
-
}
|
|
823
|
-
this.ttsWordElementHighlights.clear();
|
|
824
|
-
this.highlightTTSWordElementFallbacks([range]);
|
|
825
|
-
}
|
|
826
|
-
}
|
|
925
|
+
this.highlightTTSWord([range]);
|
|
926
|
+
break;
|
|
927
|
+
case HighlightType.TTS_SENTENCE:
|
|
928
|
+
this.highlightTTSSentence([range]);
|
|
827
929
|
break;
|
|
828
|
-
}
|
|
829
930
|
case HighlightType.ANNOTATION:
|
|
830
931
|
// For annotations, use the existing method
|
|
831
932
|
this.addAnnotation(range, color);
|
|
@@ -893,24 +994,27 @@ export class HighlightCoordinator {
|
|
|
893
994
|
const rgb = hexToRgb(color);
|
|
894
995
|
if (!rgb)
|
|
895
996
|
return;
|
|
896
|
-
|
|
997
|
+
ttsStyleInputs.override = {
|
|
897
998
|
color: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`,
|
|
898
999
|
opacity: Math.max(0.2, Math.min(0.95, opacity)),
|
|
1000
|
+
owner: this,
|
|
899
1001
|
};
|
|
900
1002
|
this.applyAdaptiveTTSStyle();
|
|
901
1003
|
}
|
|
902
1004
|
/**
|
|
903
|
-
*
|
|
1005
|
+
* Remove this coordinator's ranges and stop its observer.
|
|
904
1006
|
*
|
|
905
|
-
* The stylesheet
|
|
906
|
-
*
|
|
907
|
-
* 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.
|
|
908
1009
|
*/
|
|
909
1010
|
destroy() {
|
|
910
1011
|
if (!this.supported)
|
|
911
1012
|
return;
|
|
912
1013
|
this.clearTTS();
|
|
913
1014
|
this.clearAnnotations();
|
|
1015
|
+
if (ttsStyleInputs.override?.owner === this) {
|
|
1016
|
+
ttsStyleInputs.override = null;
|
|
1017
|
+
}
|
|
914
1018
|
this.themeObserver?.disconnect();
|
|
915
1019
|
this.themeObserver = null;
|
|
916
1020
|
}
|