@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
|
@@ -6,9 +6,81 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Part of PIE Assessment Toolkit.
|
|
8
8
|
*/
|
|
9
|
+
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
10
|
+
import { extractSpokenText, isSsmlDocument } from "./ssml/spoken-text.js";
|
|
9
11
|
import { segmentSentences as segmentTextToSentences } from "./text-segmentation.js";
|
|
12
|
+
const logger = createPieLogger("browser-tts-provider", isTtsDebugEnabled);
|
|
10
13
|
const NATIVE_START_TIMEOUT_MS = 5_000;
|
|
11
14
|
const VOICE_INVENTORY_TIMEOUT_MS = 2_000;
|
|
15
|
+
// The provider options the toolkit and this provider read. Every other option,
|
|
16
|
+
// and `voice`, belongs to the provider it was configured for: a Polly voice such
|
|
17
|
+
// as "Joanna" resolves to no browser voice, and an unresolved voice fails speak.
|
|
18
|
+
const PORTABLE_PROVIDER_OPTIONS = [
|
|
19
|
+
"__pieTelemetry",
|
|
20
|
+
"contentLanguage",
|
|
21
|
+
"highlightMode",
|
|
22
|
+
"locale",
|
|
23
|
+
"mathSpeech",
|
|
24
|
+
"segmenter",
|
|
25
|
+
"structuralPauses",
|
|
26
|
+
"textNormalization",
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* The part of another provider's configuration that keeps its meaning on
|
|
30
|
+
* browser speech. Every path that replaces a provider with browser speech
|
|
31
|
+
* starts it with this.
|
|
32
|
+
*/
|
|
33
|
+
export const browserFallbackConfig = (config) => {
|
|
34
|
+
const fallback = {};
|
|
35
|
+
if (config.rate !== undefined)
|
|
36
|
+
fallback.rate = config.rate;
|
|
37
|
+
if (config.pitch !== undefined)
|
|
38
|
+
fallback.pitch = config.pitch;
|
|
39
|
+
if (config.mathTokenHighlighting !== undefined) {
|
|
40
|
+
fallback.mathTokenHighlighting = config.mathTokenHighlighting;
|
|
41
|
+
}
|
|
42
|
+
const options = config.providerOptions;
|
|
43
|
+
if (options && typeof options === "object") {
|
|
44
|
+
const portable = {};
|
|
45
|
+
for (const key of PORTABLE_PROVIDER_OPTIONS) {
|
|
46
|
+
if (options[key] !== undefined)
|
|
47
|
+
portable[key] = options[key];
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(portable).length > 0)
|
|
50
|
+
fallback.providerOptions = portable;
|
|
51
|
+
}
|
|
52
|
+
return fallback;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* The Web Speech API reads SSML tags aloud, so a `<speak>` document is voiced
|
|
56
|
+
* as its spoken text. Boundary offsets still index the text as sent: that is
|
|
57
|
+
* the space the highlight pipeline resolves a raw-SSML chunk in.
|
|
58
|
+
*/
|
|
59
|
+
const toUtteranceText = (text) => {
|
|
60
|
+
if (!isSsmlDocument(text)) {
|
|
61
|
+
return { text, sourceOffset: (offset) => offset };
|
|
62
|
+
}
|
|
63
|
+
const { spokenText, rawToSpokenOffsetMap } = extractSpokenText(text);
|
|
64
|
+
const spokenToRaw = [];
|
|
65
|
+
for (const [raw, spoken] of rawToSpokenOffsetMap) {
|
|
66
|
+
const known = spokenToRaw[spoken];
|
|
67
|
+
if (known === undefined || raw < known)
|
|
68
|
+
spokenToRaw[spoken] = raw;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
text: spokenText,
|
|
72
|
+
// Alias text (`<sub alias>`) has no raw offset; it takes the nearest
|
|
73
|
+
// preceding one.
|
|
74
|
+
sourceOffset: (offset) => {
|
|
75
|
+
for (let index = offset; index >= 0; index--) {
|
|
76
|
+
const raw = spokenToRaw[index];
|
|
77
|
+
if (raw !== undefined)
|
|
78
|
+
return raw;
|
|
79
|
+
}
|
|
80
|
+
return 0;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
12
84
|
const normalizeLanguageCode = (value) => String(value || "")
|
|
13
85
|
.trim()
|
|
14
86
|
.toLowerCase();
|
|
@@ -18,19 +90,28 @@ const browserLanguage = () => {
|
|
|
18
90
|
: "";
|
|
19
91
|
return normalizeLanguageCode(navigatorLanguage || "en-US");
|
|
20
92
|
};
|
|
21
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Whether a voice speaks `contentLanguage`, or the browser's own language when
|
|
95
|
+
* the content names none: the same language, or the same primary subtag.
|
|
96
|
+
*/
|
|
97
|
+
export const browserVoiceMatchesLanguage = (voice, contentLanguage) => {
|
|
98
|
+
const language = normalizeLanguageCode(contentLanguage) || browserLanguage();
|
|
99
|
+
const languagePrefix = language.split("-")[0] || "en";
|
|
100
|
+
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
101
|
+
return (voiceLanguage === language || voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* The voice to read with: the configured one, else one for `contentLanguage`
|
|
105
|
+
* (the language of the content being read), else one for the browser's own
|
|
106
|
+
* language.
|
|
107
|
+
*/
|
|
108
|
+
export const findBrowserVoice = (voices, preferredVoice, contentLanguage) => {
|
|
22
109
|
if (preferredVoice) {
|
|
23
110
|
return (voices.find((voice) => voice.voiceURI === preferredVoice) ||
|
|
24
111
|
voices.find((voice) => voice.name === preferredVoice) ||
|
|
25
112
|
null);
|
|
26
113
|
}
|
|
27
|
-
const
|
|
28
|
-
const languagePrefix = language.split("-")[0] || "en";
|
|
29
|
-
const matchesLanguage = (voice) => {
|
|
30
|
-
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
31
|
-
return (voiceLanguage === language ||
|
|
32
|
-
voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
33
|
-
};
|
|
114
|
+
const matchesLanguage = (voice) => browserVoiceMatchesLanguage(voice, contentLanguage);
|
|
34
115
|
const ranked = [
|
|
35
116
|
(voice) => voice.localService && matchesLanguage(voice),
|
|
36
117
|
(voice) => voice.default && matchesLanguage(voice),
|
|
@@ -45,6 +126,64 @@ const findBrowserVoice = (voices, preferredVoice) => {
|
|
|
45
126
|
}
|
|
46
127
|
return voices[0] || null;
|
|
47
128
|
};
|
|
129
|
+
/**
|
|
130
|
+
* Waits for `synth` to publish its voice inventory, which browsers fill
|
|
131
|
+
* asynchronously after page load. Resolves `true` once `getVoices()` is
|
|
132
|
+
* non-empty, at once when it already is; `false` after `timeoutMs`, once
|
|
133
|
+
* `isCurrent` reports the wait superseded, or when `synth` offers no
|
|
134
|
+
* `voiceschanged` channel to wait on. A previous `onvoiceschanged` handler
|
|
135
|
+
* keeps running and is restored afterwards.
|
|
136
|
+
*/
|
|
137
|
+
export const waitForBrowserVoices = (synth, timeoutMs, isCurrent = () => true) => {
|
|
138
|
+
if (synth.getVoices().length > 0)
|
|
139
|
+
return Promise.resolve(true);
|
|
140
|
+
const canUseEventTarget = typeof synth.addEventListener === "function" &&
|
|
141
|
+
typeof synth.removeEventListener === "function";
|
|
142
|
+
if (!canUseEventTarget && !("onvoiceschanged" in synth)) {
|
|
143
|
+
return Promise.resolve(false);
|
|
144
|
+
}
|
|
145
|
+
return new Promise((resolve) => {
|
|
146
|
+
const previousHandler = canUseEventTarget ? null : synth.onvoiceschanged;
|
|
147
|
+
let settled = false;
|
|
148
|
+
const finish = (voicesAvailable) => {
|
|
149
|
+
if (settled)
|
|
150
|
+
return;
|
|
151
|
+
settled = true;
|
|
152
|
+
clearTimeout(timeout);
|
|
153
|
+
if (canUseEventTarget) {
|
|
154
|
+
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
155
|
+
}
|
|
156
|
+
else if (synth.onvoiceschanged === propertyHandler) {
|
|
157
|
+
synth.onvoiceschanged = previousHandler;
|
|
158
|
+
}
|
|
159
|
+
resolve(voicesAvailable);
|
|
160
|
+
};
|
|
161
|
+
const inspectVoiceInventory = () => {
|
|
162
|
+
if (!isCurrent())
|
|
163
|
+
finish(false);
|
|
164
|
+
else if (synth.getVoices().length > 0)
|
|
165
|
+
finish(true);
|
|
166
|
+
};
|
|
167
|
+
const onVoicesChanged = () => inspectVoiceInventory();
|
|
168
|
+
const propertyHandler = (event) => {
|
|
169
|
+
try {
|
|
170
|
+
previousHandler?.call(synth, event);
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
inspectVoiceInventory();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const timeout = setTimeout(() => finish(false), timeoutMs);
|
|
177
|
+
if (canUseEventTarget) {
|
|
178
|
+
synth.addEventListener("voiceschanged", onVoicesChanged);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
synth.onvoiceschanged = propertyHandler;
|
|
182
|
+
}
|
|
183
|
+
// Close the getVoices()/listener-registration race without polling.
|
|
184
|
+
inspectVoiceInventory();
|
|
185
|
+
});
|
|
186
|
+
};
|
|
48
187
|
const shouldAssignBrowserVoice = (voice) => !voice.default;
|
|
49
188
|
/**
|
|
50
189
|
* Browser TTS Provider
|
|
@@ -64,17 +203,20 @@ export class BrowserTTSProvider {
|
|
|
64
203
|
if (!("speechSynthesis" in window)) {
|
|
65
204
|
throw new Error("Browser does not support Speech Synthesis API");
|
|
66
205
|
}
|
|
206
|
+
// Only a configured voice needs the inventory; waiting otherwise would
|
|
207
|
+
// hold readiness on a browser that publishes no voices.
|
|
208
|
+
if (typeof config.voice === "string" && config.voice.trim()) {
|
|
209
|
+
await waitForBrowserVoices(window.speechSynthesis, VOICE_INVENTORY_TIMEOUT_MS);
|
|
210
|
+
}
|
|
67
211
|
return new BrowserTTSProviderImpl(config);
|
|
68
212
|
}
|
|
69
|
-
supportsFeature(feature) {
|
|
70
|
-
// Browser supports all TTS features
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
213
|
getCapabilities() {
|
|
74
214
|
return {
|
|
75
215
|
supportsPause: true,
|
|
76
216
|
supportsResume: true,
|
|
77
217
|
supportsWordBoundary: true,
|
|
218
|
+
// Boundary events depend on the voice: several network voices send none.
|
|
219
|
+
defaultHighlightMode: "sentence",
|
|
78
220
|
supportsVoiceSelection: true,
|
|
79
221
|
supportsRateControl: true,
|
|
80
222
|
supportsPitchControl: true,
|
|
@@ -95,99 +237,45 @@ class BrowserTTSProviderImpl {
|
|
|
95
237
|
utterance = null;
|
|
96
238
|
config = null;
|
|
97
239
|
_isPlaying = false;
|
|
240
|
+
// The run's pause, which outlasts the utterance it paused: a pause between
|
|
241
|
+
// utterances holds the next one until resume.
|
|
98
242
|
_isPaused = false;
|
|
99
243
|
speakRunId = 0;
|
|
100
|
-
|
|
244
|
+
// The run speaking now, between its utterances as well as during them.
|
|
245
|
+
activeRunId = null;
|
|
246
|
+
releasePauseHold = null;
|
|
101
247
|
settlePendingChunk = null;
|
|
248
|
+
settlePendingGap = null;
|
|
102
249
|
onPlaybackStart = undefined;
|
|
103
250
|
constructor(config) {
|
|
104
251
|
this.config = config;
|
|
105
252
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
if (canUseEventTarget) {
|
|
121
|
-
synth.removeEventListener("voiceschanged", onVoicesChanged);
|
|
122
|
-
}
|
|
123
|
-
else if (synth.onvoiceschanged === propertyHandler) {
|
|
124
|
-
synth.onvoiceschanged = previousHandler;
|
|
125
|
-
}
|
|
126
|
-
if (pendingSettlement &&
|
|
127
|
-
this.settlePendingVoiceWait === pendingSettlement) {
|
|
128
|
-
this.settlePendingVoiceWait = null;
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
const finish = (voicesAvailable, error) => {
|
|
132
|
-
if (settled)
|
|
133
|
-
return;
|
|
134
|
-
settled = true;
|
|
135
|
-
cleanup();
|
|
136
|
-
if (error) {
|
|
137
|
-
reject(error);
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
resolve(voicesAvailable);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
const inspectVoiceInventory = () => {
|
|
144
|
-
if (runId !== this.speakRunId) {
|
|
145
|
-
finish(false);
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
if (synth.getVoices().length > 0)
|
|
149
|
-
finish(true);
|
|
150
|
-
};
|
|
151
|
-
const onVoicesChanged = () => inspectVoiceInventory();
|
|
152
|
-
const propertyHandler = (event) => {
|
|
153
|
-
try {
|
|
154
|
-
previousHandler?.call(synth, event);
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
inspectVoiceInventory();
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
pendingSettlement = () => finish(false);
|
|
161
|
-
this.settlePendingVoiceWait = pendingSettlement;
|
|
162
|
-
timeout = setTimeout(() => finish(false, new Error(`Configured browser voice "${configuredVoice}" could not be resolved because the browser did not publish its voice inventory within ${VOICE_INVENTORY_TIMEOUT_MS / 1_000} seconds.`)), VOICE_INVENTORY_TIMEOUT_MS);
|
|
163
|
-
if (canUseEventTarget) {
|
|
164
|
-
synth.addEventListener("voiceschanged", onVoicesChanged);
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
synth.onvoiceschanged = propertyHandler;
|
|
168
|
-
}
|
|
169
|
-
// Close the getVoices()/listener-registration race without polling.
|
|
170
|
-
inspectVoiceInventory();
|
|
171
|
-
});
|
|
253
|
+
/** Whether run `runId` is still current once any pause between utterances ends. */
|
|
254
|
+
async waitWhilePaused(runId) {
|
|
255
|
+
while (this._isPaused && runId === this.speakRunId) {
|
|
256
|
+
await new Promise((resolve) => {
|
|
257
|
+
this.releasePauseHold = resolve;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return runId === this.speakRunId;
|
|
261
|
+
}
|
|
262
|
+
releaseHold() {
|
|
263
|
+
const release = this.releasePauseHold;
|
|
264
|
+
this.releasePauseHold = null;
|
|
265
|
+
release?.();
|
|
172
266
|
}
|
|
173
|
-
|
|
267
|
+
/** The voice to read with; `initialize()` already waited for the inventory. */
|
|
268
|
+
resolveBrowserVoice() {
|
|
174
269
|
const configuredVoice = typeof this.config?.voice === "string" ? this.config.voice.trim() : "";
|
|
175
|
-
|
|
270
|
+
const voices = speechSynthesis.getVoices();
|
|
176
271
|
if (configuredVoice && voices.length === 0) {
|
|
177
|
-
|
|
178
|
-
if (!voicesAvailable || runId !== this.speakRunId) {
|
|
179
|
-
return { shouldContinue: false, voice: null };
|
|
180
|
-
}
|
|
181
|
-
voices = speechSynthesis.getVoices();
|
|
182
|
-
}
|
|
183
|
-
if (runId !== this.speakRunId) {
|
|
184
|
-
return { shouldContinue: false, voice: null };
|
|
272
|
+
throw new Error(`Configured browser voice "${configuredVoice}" could not be resolved because the browser did not publish its voice inventory within ${VOICE_INVENTORY_TIMEOUT_MS / 1_000} seconds.`);
|
|
185
273
|
}
|
|
186
|
-
const voice = findBrowserVoice(voices, configuredVoice || undefined);
|
|
274
|
+
const voice = findBrowserVoice(voices, configuredVoice || undefined, this.getContentLocale());
|
|
187
275
|
if (configuredVoice && !voice) {
|
|
188
276
|
throw new Error(`Configured browser voice "${configuredVoice}" is unavailable. Select a voice exposed by this browser using its voiceURI or name.`);
|
|
189
277
|
}
|
|
190
|
-
return
|
|
278
|
+
return voice;
|
|
191
279
|
}
|
|
192
280
|
async speak(text) {
|
|
193
281
|
if (!this.config) {
|
|
@@ -196,19 +284,24 @@ class BrowserTTSProviderImpl {
|
|
|
196
284
|
// Invalidate any in-flight run and cancel current utterance.
|
|
197
285
|
this.stop();
|
|
198
286
|
const runId = this.speakRunId;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
287
|
+
this.activeRunId = runId;
|
|
288
|
+
try {
|
|
289
|
+
const voice = this.resolveBrowserVoice();
|
|
290
|
+
const utterance = toUtteranceText(text);
|
|
291
|
+
const chunks = this.splitIntoChunks(utterance.text);
|
|
292
|
+
for (const chunk of chunks) {
|
|
293
|
+
if (this._isPaused && !(await this.waitWhilePaused(runId)))
|
|
294
|
+
break;
|
|
295
|
+
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId, voice, utterance.sourceOffset);
|
|
296
|
+
if (!shouldContinue) {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
210
299
|
}
|
|
211
300
|
}
|
|
301
|
+
finally {
|
|
302
|
+
if (this.activeRunId === runId)
|
|
303
|
+
this.activeRunId = null;
|
|
304
|
+
}
|
|
212
305
|
}
|
|
213
306
|
async speakSegments(segments) {
|
|
214
307
|
if (!this.config) {
|
|
@@ -216,30 +309,45 @@ class BrowserTTSProviderImpl {
|
|
|
216
309
|
}
|
|
217
310
|
this.stop();
|
|
218
311
|
const runId = this.speakRunId;
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (runId !== this.speakRunId)
|
|
224
|
-
break;
|
|
225
|
-
const chunks = this.splitIntoChunks(segment.text);
|
|
226
|
-
for (const chunk of chunks) {
|
|
312
|
+
this.activeRunId = runId;
|
|
313
|
+
try {
|
|
314
|
+
const voice = this.resolveBrowserVoice();
|
|
315
|
+
for (const segment of segments) {
|
|
227
316
|
if (runId !== this.speakRunId)
|
|
228
317
|
break;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
318
|
+
const utterance = toUtteranceText(segment.text);
|
|
319
|
+
const chunks = this.splitIntoChunks(utterance.text);
|
|
320
|
+
for (const chunk of chunks) {
|
|
321
|
+
if (this._isPaused && !(await this.waitWhilePaused(runId)))
|
|
322
|
+
break;
|
|
323
|
+
const shouldContinue = await this.speakChunk(chunk.text, chunk.offset, runId, voice, (offset) => segment.startOffset + utterance.sourceOffset(offset));
|
|
324
|
+
if (!shouldContinue)
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
const pauseMsAfter = Math.max(0, Number(segment.pauseMsAfter || 0));
|
|
328
|
+
if (pauseMsAfter > 0 && runId === this.speakRunId) {
|
|
329
|
+
const shouldContinue = await this.waitForPause(pauseMsAfter, runId);
|
|
330
|
+
if (!shouldContinue)
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
238
333
|
}
|
|
239
334
|
}
|
|
335
|
+
finally {
|
|
336
|
+
if (this.activeRunId === runId)
|
|
337
|
+
this.activeRunId = null;
|
|
338
|
+
}
|
|
240
339
|
}
|
|
241
340
|
async waitForPause(pauseMs, runId) {
|
|
242
|
-
await new Promise((resolve) =>
|
|
341
|
+
await new Promise((resolve) => {
|
|
342
|
+
const settle = () => {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
if (this.settlePendingGap === settle)
|
|
345
|
+
this.settlePendingGap = null;
|
|
346
|
+
resolve();
|
|
347
|
+
};
|
|
348
|
+
const timer = setTimeout(settle, pauseMs);
|
|
349
|
+
this.settlePendingGap = settle;
|
|
350
|
+
});
|
|
243
351
|
return runId === this.speakRunId;
|
|
244
352
|
}
|
|
245
353
|
splitIntoChunks(text) {
|
|
@@ -277,13 +385,19 @@ class BrowserTTSProviderImpl {
|
|
|
277
385
|
}
|
|
278
386
|
return chunks.length ? chunks : [{ text, offset: 0 }];
|
|
279
387
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
388
|
+
/**
|
|
389
|
+
* The language of the content being read, which the toolkit sets per speak as
|
|
390
|
+
* `providerOptions.contentLanguage` when the content or the host names one.
|
|
391
|
+
*/
|
|
392
|
+
getContentLocale() {
|
|
393
|
+
const language = this.config?.providerOptions?.contentLanguage;
|
|
394
|
+
return typeof language === "string" && language.trim()
|
|
395
|
+
? language.trim()
|
|
396
|
+
: undefined;
|
|
283
397
|
}
|
|
284
398
|
getSegmentationPolicy() {
|
|
285
|
-
const providerOptions =
|
|
286
|
-
const segmenter =
|
|
399
|
+
const providerOptions = this.config?.providerOptions || {};
|
|
400
|
+
const segmenter = providerOptions.segmenter || {};
|
|
287
401
|
const mode = segmenter.mode;
|
|
288
402
|
const useSegmenter = mode !== "regexOnly";
|
|
289
403
|
const locale = typeof segmenter.locale === "string" && segmenter.locale.trim().length > 0
|
|
@@ -311,7 +425,11 @@ class BrowserTTSProviderImpl {
|
|
|
311
425
|
const match = slice.match(/^\s*([^\s]+)/);
|
|
312
426
|
return match?.[1]?.length || 1;
|
|
313
427
|
}
|
|
314
|
-
|
|
428
|
+
/**
|
|
429
|
+
* `chunkOffset` places the chunk in the utterance text; `sourceOffset` maps an
|
|
430
|
+
* utterance offset to the boundary position reported for it.
|
|
431
|
+
*/
|
|
432
|
+
async speakChunk(chunkText, chunkOffset, runId, voice, sourceOffset) {
|
|
315
433
|
return new Promise((resolve, reject) => {
|
|
316
434
|
if (runId !== this.speakRunId) {
|
|
317
435
|
resolve(false);
|
|
@@ -319,7 +437,11 @@ class BrowserTTSProviderImpl {
|
|
|
319
437
|
}
|
|
320
438
|
const utterance = new SpeechSynthesisUtterance(chunkText);
|
|
321
439
|
this.utterance = utterance;
|
|
322
|
-
// Apply config
|
|
440
|
+
// Apply config. The language tells the engine how to read the text, and
|
|
441
|
+
// lets it choose a voice when the default one is left in place.
|
|
442
|
+
const contentLocale = this.getContentLocale();
|
|
443
|
+
if (contentLocale)
|
|
444
|
+
utterance.lang = contentLocale;
|
|
323
445
|
if (voice && shouldAssignBrowserVoice(voice))
|
|
324
446
|
utterance.voice = voice;
|
|
325
447
|
if (this.config?.rate)
|
|
@@ -362,7 +484,6 @@ class BrowserTTSProviderImpl {
|
|
|
362
484
|
}
|
|
363
485
|
if (runId === this.speakRunId) {
|
|
364
486
|
this._isPlaying = false;
|
|
365
|
-
this._isPaused = false;
|
|
366
487
|
}
|
|
367
488
|
if (error) {
|
|
368
489
|
reject(error);
|
|
@@ -384,7 +505,9 @@ class BrowserTTSProviderImpl {
|
|
|
384
505
|
didStart = true;
|
|
385
506
|
clearStartTimeout();
|
|
386
507
|
this._isPlaying = true;
|
|
387
|
-
this.
|
|
508
|
+
// A pause that landed while the engine was starting holds this utterance.
|
|
509
|
+
if (this._isPaused)
|
|
510
|
+
speechSynthesis.pause();
|
|
388
511
|
try {
|
|
389
512
|
this.onPlaybackStart?.();
|
|
390
513
|
}
|
|
@@ -427,12 +550,9 @@ class BrowserTTSProviderImpl {
|
|
|
427
550
|
utterance.onboundary = (event) => {
|
|
428
551
|
if (runId !== this.speakRunId)
|
|
429
552
|
return;
|
|
430
|
-
|
|
553
|
+
logger.debug(`boundary event: ${event.name}, charIndex ${event.charIndex}, charLength ${event.charLength}`);
|
|
431
554
|
if (event.name !== "word" || !this.onWordBoundary)
|
|
432
555
|
return;
|
|
433
|
-
if (this.getHighlightMode() === "sentence") {
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
556
|
const charIndex = Math.max(0, Math.min(event.charIndex, Math.max(0, chunkText.length - 1)));
|
|
437
557
|
const reportedLength = Number(event.charLength || 0);
|
|
438
558
|
const inferredLength = this.inferWordLength(chunkText, charIndex);
|
|
@@ -445,9 +565,13 @@ class BrowserTTSProviderImpl {
|
|
|
445
565
|
const word = chunkText
|
|
446
566
|
.substring(charIndex, Math.min(chunkText.length, charIndex + wordLength))
|
|
447
567
|
.trim();
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
568
|
+
const utteranceStart = chunkOffset + charIndex;
|
|
569
|
+
const absoluteBoundaryStart = sourceOffset(utteranceStart);
|
|
570
|
+
const boundaryLength = sourceOffset(utteranceStart + wordLength - 1) +
|
|
571
|
+
1 -
|
|
572
|
+
absoluteBoundaryStart;
|
|
573
|
+
logger.debug(`word boundary "${word}" at ${absoluteBoundaryStart}`);
|
|
574
|
+
this.onWordBoundary(word, absoluteBoundaryStart, boundaryLength);
|
|
451
575
|
};
|
|
452
576
|
startTimeout = setTimeout(() => {
|
|
453
577
|
if (settled || didStart || runId !== this.speakRunId)
|
|
@@ -466,27 +590,39 @@ class BrowserTTSProviderImpl {
|
|
|
466
590
|
});
|
|
467
591
|
}
|
|
468
592
|
pause() {
|
|
469
|
-
if (this.
|
|
593
|
+
if (this._isPaused)
|
|
594
|
+
return;
|
|
595
|
+
if (this._isPlaying) {
|
|
470
596
|
speechSynthesis.pause();
|
|
597
|
+
this._isPaused = true;
|
|
598
|
+
}
|
|
599
|
+
else if (this.activeRunId === this.speakRunId) {
|
|
600
|
+
// Between utterances, or in a structural gap: the next one holds.
|
|
601
|
+
this._isPaused = true;
|
|
471
602
|
}
|
|
472
603
|
}
|
|
473
604
|
resume() {
|
|
474
|
-
if (this.
|
|
605
|
+
if (!this._isPaused)
|
|
606
|
+
return;
|
|
607
|
+
this._isPaused = false;
|
|
608
|
+
if (this._isPlaying)
|
|
475
609
|
speechSynthesis.resume();
|
|
476
|
-
|
|
610
|
+
this.releaseHold();
|
|
477
611
|
}
|
|
478
612
|
stop() {
|
|
479
613
|
const utterance = this.utterance;
|
|
480
614
|
const shouldCancel = this._isPlaying || utterance !== null;
|
|
481
615
|
this.speakRunId += 1;
|
|
482
|
-
this.
|
|
616
|
+
this.activeRunId = null;
|
|
483
617
|
this.settlePendingChunk?.();
|
|
618
|
+
this.settlePendingGap?.();
|
|
484
619
|
if (shouldCancel) {
|
|
485
620
|
speechSynthesis.cancel();
|
|
486
621
|
}
|
|
487
622
|
this.utterance = null;
|
|
488
623
|
this._isPlaying = false;
|
|
489
624
|
this._isPaused = false;
|
|
625
|
+
this.releaseHold();
|
|
490
626
|
}
|
|
491
627
|
isPlaying() {
|
|
492
628
|
return this._isPlaying && !this._isPaused;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tree read-aloud walks: the flat tree, which is the tree the page renders.
|
|
3
|
+
*
|
|
4
|
+
* An element with an open shadow root contributes that root's children in place
|
|
5
|
+
* of its own light children, and a `<slot>` inside a shadow root contributes the
|
|
6
|
+
* nodes assigned to it, flattened through nested slots, so a host's light
|
|
7
|
+
* children are reached once, at the slot that renders them. Closed shadow roots
|
|
8
|
+
* are invisible to script, so their hosts are walked as light DOM.
|
|
9
|
+
*
|
|
10
|
+
* Toolkit chrome is skipped wherever it appears: toolbars and tool elements
|
|
11
|
+
* render their controls into open shadow roots, and descending into them would
|
|
12
|
+
* read button labels as content. So are the empty `aria-hidden` anchors the
|
|
13
|
+
* shells and the toolkit keep beside their slots, and elements whose text never
|
|
14
|
+
* renders (`style`, `script`, `noscript`, `template`).
|
|
15
|
+
*
|
|
16
|
+
* Node types are compared as the spec's numbers, so this module reads no DOM
|
|
17
|
+
* global, at import or at call time.
|
|
18
|
+
*/
|
|
19
|
+
export declare const isShadowRootNode: (node: Node | null | undefined) => node is ShadowRoot;
|
|
20
|
+
/** Toolbars, tool elements and debug panels: chrome around content, never content. */
|
|
21
|
+
export declare const isToolkitChromeElement: (element: Element) => boolean;
|
|
22
|
+
/** `node`'s children in the flat tree, with skipped elements left out. */
|
|
23
|
+
export declare const flatTreeChildNodes: (node: Node) => Node[];
|
|
24
|
+
/**
|
|
25
|
+
* What a visitor returns: `"skip"` leaves the node's subtree out, `"stop"` ends
|
|
26
|
+
* the walk, anything else descends.
|
|
27
|
+
*/
|
|
28
|
+
export type FlatTreeVisitResult = "skip" | "stop" | undefined;
|
|
29
|
+
/** Visits `root`'s flat-tree descendants in rendering order; `root` itself is not visited. */
|
|
30
|
+
export declare const walkFlatTree: (root: Node, visit: (node: Node) => FlatTreeVisitResult) => void;
|
|
31
|
+
/** `root`'s text nodes in rendering order. */
|
|
32
|
+
export declare const flatTreeTextNodes: (root: Node) => Text[];
|
|
33
|
+
/** `textContent` over the flat tree: shadow content in, skipped elements out. */
|
|
34
|
+
export declare const flatTextContent: (root: Node) => string;
|
|
35
|
+
/** Elements under `root` matching `selector`, in rendering order. */
|
|
36
|
+
export declare const flatQuerySelectorAll: (root: Node, selector: string) => Element[];
|
|
37
|
+
export declare const flatQuerySelector: (root: Node, selector: string) => Element | null;
|
|
38
|
+
/**
|
|
39
|
+
* `node`'s parent in the flat tree: the slot rendering it, else its parent, with
|
|
40
|
+
* a shadow root standing for its host.
|
|
41
|
+
*/
|
|
42
|
+
export declare const flatTreeParentNode: (node: Node) => Node | null;
|
|
43
|
+
export declare const flatTreeParentElement: (node: Node) => Element | null;
|
|
44
|
+
/** The nearest element at or above `node` in the flat tree that matches `selector`. */
|
|
45
|
+
export declare const flatTreeClosest: (node: Node, selector: string) => Element | null;
|
|
46
|
+
/**
|
|
47
|
+
* `node`'s parent element, continuing from a shadow root to its host. This is the
|
|
48
|
+
* chain inheritance follows (`lang`, regions an author marks up), and for slotted
|
|
49
|
+
* content it stays in the light tree the author wrote.
|
|
50
|
+
*/
|
|
51
|
+
export declare const composedParentElement: (node: Node) => Element | null;
|
|
52
|
+
/**
|
|
53
|
+
* The nearest element at or above `node` on the composed chain that matches
|
|
54
|
+
* `selector`, or null. With `boundary`, the climb ends after testing it.
|
|
55
|
+
*/
|
|
56
|
+
export declare const composedClosest: (node: Node, selector: string, boundary?: Element | null) => Element | null;
|
|
57
|
+
/** `contains` across shadow boundaries: whether `node` is `container` or below it. */
|
|
58
|
+
export declare const composedContains: (container: Node | null | undefined, node: Node | null | undefined) => boolean;
|
|
59
|
+
/**
|
|
60
|
+
* `node`, or the shadow host above it, that lies in the tree rooted at
|
|
61
|
+
* `treeRoot`; null when `node` is not inside that tree at any depth.
|
|
62
|
+
*/
|
|
63
|
+
export declare const retargetToTree: (node: Node, treeRoot: Node) => Node | null;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the character at `offset` in `node` lies inside `range`. A live range
|
|
66
|
+
* holds nodes of one tree only, so a node in a shadow tree below it counts as
|
|
67
|
+
* inside when the range holds that tree's host whole.
|
|
68
|
+
*/
|
|
69
|
+
export declare const rangeHoldsTextPosition: (range: Range, node: Text, offset: number) => boolean;
|
|
70
|
+
/** Whether `range` intersects `node`, counting a shadow tree below the range as {@link rangeHoldsTextPosition} does. */
|
|
71
|
+
export declare const rangeIntersectsComposedNode: (range: Range, node: Node) => boolean;
|