@pie-players/pie-assessment-toolkit 0.3.74 → 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +658 -714
- package/custom-elements.json +1315 -0
- package/dist/attempt/TestSession.d.ts +1 -21
- package/dist/attempt/TestSession.js +16 -102
- package/dist/components/ItemScope.custom-element.d.ts +1 -0
- package/dist/components/ItemScope.custom-element.js +1 -0
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +14 -14
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-3zrc4p3d.js +4 -0
- package/dist/components/chunks/ItemToolBar-66hcwmfw.js +2 -0
- package/dist/components/chunks/ItemToolBar-9gqhfxtn.js +40 -0
- package/dist/components/chunks/ItemToolBar-hzc5wyjp.js +2 -0
- package/dist/components/chunks/{ItemToolBar-gmrryv2v.js → ItemToolBar-kkj69ptr.js} +1 -1
- package/dist/components/chunks/ItemToolBar-vdkyn9f3.js +2 -0
- package/dist/components/chunks/sre-engine-bj997w4b.js +1 -0
- package/dist/components/item-scope-element.d.ts +2 -0
- package/dist/components/item-scope-element.js +2 -0
- package/dist/context/assessment-toolkit-context.d.ts +33 -6
- package/dist/index.d.ts +25 -50
- package/dist/index.js +15 -45
- package/dist/policy/core/PolicySource.d.ts +2 -2
- package/dist/policy/core/ToolPolicyEngine.d.ts +118 -34
- package/dist/policy/core/ToolPolicyEngine.js +203 -56
- package/dist/policy/core/compose-decision.d.ts +28 -7
- package/dist/policy/core/compose-decision.js +95 -74
- package/dist/policy/core/decision-types.d.ts +98 -55
- package/dist/policy/core/feature-decision.d.ts +28 -19
- package/dist/policy/core/feature-decision.js +15 -11
- package/dist/policy/core/pnp-policy-inputs.d.ts +29 -35
- package/dist/policy/core/pnp-policy-inputs.js +40 -88
- package/dist/policy/core/provenance.d.ts +10 -5
- package/dist/policy/core/provenance.js +1 -1
- package/dist/policy/core/tool-parameters.d.ts +11 -0
- package/dist/policy/core/tool-parameters.js +15 -0
- package/dist/policy/engine.d.ts +7 -23
- package/dist/policy/engine.js +5 -20
- package/dist/policy/sources/PnpPolicySource.d.ts +43 -68
- package/dist/policy/sources/PnpPolicySource.js +133 -132
- package/dist/runtime/SectionControllerBinding.d.ts +283 -0
- package/dist/runtime/SectionControllerBinding.js +456 -0
- package/dist/runtime/SectionRuntimeEngine.d.ts +26 -289
- package/dist/runtime/SectionRuntimeEngine.js +30 -372
- package/dist/runtime/adapter/SectionEngineAdapter.d.ts +19 -80
- package/dist/runtime/adapter/SectionEngineAdapter.js +19 -92
- package/dist/runtime/adapter/dom-event-bridge.d.ts +15 -30
- package/dist/runtime/adapter/dom-event-bridge.js +17 -44
- package/dist/runtime/adapter/subscriber-fanout.d.ts +1 -3
- package/dist/runtime/adapter/subscriber-fanout.js +2 -6
- package/dist/runtime/catalog-context.d.ts +19 -0
- package/dist/runtime/catalog-context.js +28 -0
- package/dist/runtime/composition-emit-scheduler.d.ts +12 -18
- package/dist/runtime/composition-emit-scheduler.js +10 -16
- package/dist/runtime/content-language.d.ts +32 -0
- package/dist/runtime/content-language.js +46 -0
- package/dist/runtime/content-region.d.ts +13 -0
- package/dist/runtime/content-region.js +29 -0
- package/dist/runtime/content-registration.d.ts +13 -0
- package/dist/runtime/content-registration.js +25 -0
- package/dist/runtime/core/SectionEngineCore.d.ts +2 -4
- package/dist/runtime/core/SectionEngineCore.js +2 -6
- package/dist/runtime/core/cohort.d.ts +1 -6
- package/dist/runtime/core/cohort.js +1 -10
- package/dist/runtime/core/engine-input.d.ts +18 -30
- package/dist/runtime/core/engine-input.js +11 -12
- package/dist/runtime/core/engine-output.d.ts +17 -40
- package/dist/runtime/core/engine-output.js +9 -21
- package/dist/runtime/core/engine-readiness.d.ts +5 -37
- package/dist/runtime/core/engine-readiness.js +8 -41
- package/dist/runtime/core/engine-resolver.d.ts +46 -74
- package/dist/runtime/core/engine-resolver.js +18 -43
- package/dist/runtime/core/engine-state.d.ts +28 -36
- package/dist/runtime/core/engine-state.js +18 -21
- package/dist/runtime/core/engine-transition.d.ts +14 -19
- package/dist/runtime/core/engine-transition.js +74 -53
- package/dist/runtime/engine.d.ts +16 -19
- package/dist/runtime/engine.js +12 -19
- package/dist/runtime/page-warnings.d.ts +13 -0
- package/dist/runtime/page-warnings.js +22 -0
- package/dist/runtime/policy-input-forwarding.d.ts +25 -0
- package/dist/runtime/policy-input-forwarding.js +25 -0
- package/dist/runtime/registration-events.d.ts +22 -14
- package/dist/runtime/runtime-event-claim.d.ts +8 -0
- package/dist/runtime/runtime-event-claim.js +15 -0
- package/dist/runtime/runtime-id.d.ts +1 -1
- package/dist/runtime/runtime-id.js +1 -1
- package/dist/runtime/shell-event-bridge.d.ts +45 -0
- package/dist/runtime/shell-event-bridge.js +127 -0
- package/dist/runtime/shell-registration.d.ts +73 -0
- package/dist/runtime/shell-registration.js +95 -0
- package/dist/runtime/shell-scope.d.ts +68 -0
- package/dist/runtime/shell-scope.js +194 -0
- package/dist/runtime/tool-host-contract.d.ts +8 -14
- package/dist/runtime/tool-host-contract.js +12 -12
- package/dist/runtime/unclaimed-registration-watch.d.ts +7 -0
- package/dist/runtime/unclaimed-registration-watch.js +23 -0
- package/dist/services/AccessibilityCatalogResolver.d.ts +21 -31
- package/dist/services/AccessibilityCatalogResolver.js +29 -42
- package/dist/services/ElementToolStateStore.d.ts +42 -67
- package/dist/services/ElementToolStateStore.js +65 -71
- package/dist/services/HighlightCoordinator.d.ts +28 -40
- package/dist/services/HighlightCoordinator.js +459 -381
- package/dist/services/RangeSerializer.d.ts +11 -33
- package/dist/services/RangeSerializer.js +88 -59
- package/dist/services/SSMLExtractor.d.ts +8 -31
- package/dist/services/SSMLExtractor.js +18 -40
- package/dist/services/TTSService.d.ts +159 -119
- package/dist/services/TTSService.js +937 -822
- package/dist/services/ToolCoordinator.d.ts +42 -102
- package/dist/services/ToolCoordinator.js +178 -256
- package/dist/services/ToolRegistry.d.ts +152 -133
- package/dist/services/ToolRegistry.js +192 -178
- package/dist/services/ToolkitCoordinator.d.ts +233 -237
- package/dist/services/ToolkitCoordinator.js +615 -557
- package/dist/services/audio-handoff.d.ts +6 -7
- package/dist/services/audio-handoff.js +20 -14
- package/dist/services/catalog-owner.d.ts +0 -1
- package/dist/services/framework-error-bus.d.ts +3 -6
- package/dist/services/framework-error-bus.js +3 -6
- package/dist/services/framework-error.d.ts +51 -13
- package/dist/services/framework-error.js +19 -20
- package/dist/services/interfaces.d.ts +189 -184
- package/dist/services/section-controller-types.d.ts +7 -7
- package/dist/services/section-session-assignment.d.ts +21 -0
- package/dist/services/section-session-assignment.js +80 -0
- package/dist/services/spoken-audio-cards.d.ts +2 -6
- package/dist/services/spoken-audio-cards.js +2 -6
- package/dist/services/tool-config-defaults.d.ts +3 -3
- package/dist/services/tool-config-defaults.js +3 -3
- package/dist/services/tool-config-validation.d.ts +9 -12
- package/dist/services/tool-config-validation.js +47 -87
- package/dist/services/tool-context.d.ts +7 -31
- package/dist/services/tool-context.js +99 -65
- package/dist/services/tool-failure.d.ts +30 -0
- package/dist/services/tool-failure.js +11 -0
- package/dist/services/tool-icons.d.ts +5 -5
- package/dist/services/tool-icons.js +6 -5
- package/dist/services/tool-instance-id.d.ts +8 -7
- package/dist/services/tool-instance-id.js +12 -24
- package/dist/services/tool-providers/TTSToolProvider.d.ts +20 -58
- package/dist/services/tool-providers/TTSToolProvider.js +32 -85
- package/dist/services/tool-providers/ToolProviderApi.d.ts +10 -11
- package/dist/services/tool-providers/ToolProviderRegistry.d.ts +29 -60
- package/dist/services/tool-providers/ToolProviderRegistry.js +128 -144
- package/dist/services/tool-request.d.ts +31 -12
- package/dist/services/tool-request.js +31 -16
- package/dist/services/toolbar-decided-tools.d.ts +16 -0
- package/dist/services/toolbar-decided-tools.js +29 -0
- package/dist/services/toolbar-items.js +2 -5
- package/dist/services/tools-config-normalizer.d.ts +33 -29
- package/dist/services/tools-config-normalizer.js +26 -28
- package/dist/services/tts/browser-provider.d.ts +39 -6
- package/dist/services/tts/browser-provider.js +280 -160
- package/dist/services/tts/catalog-span-alignment.js +14 -7
- package/dist/services/tts/flat-tree.d.ts +64 -0
- package/dist/services/tts/flat-tree.js +269 -0
- package/dist/services/tts/generated-speech/assemble-plan.d.ts +3 -1
- package/dist/services/tts/generated-speech/assemble-plan.js +3 -1
- package/dist/services/tts/generated-speech/dom/build-dom-plan.d.ts +2 -0
- package/dist/services/tts/generated-speech/dom/build-dom-plan.js +1 -1
- package/dist/services/tts/generated-speech/dom/to-chunk-inputs.d.ts +2 -2
- package/dist/services/tts/generated-speech/index.d.ts +1 -1
- package/dist/services/tts/generated-speech/index.js +1 -1
- package/dist/services/tts/generated-speech/math-speech-cache.d.ts +3 -1
- package/dist/services/tts/generated-speech/math-speech-cache.js +5 -1
- package/dist/services/tts/generated-speech/types.d.ts +1 -1
- package/dist/services/tts/generated-speech/types.js +1 -1
- package/dist/services/tts/highlight-pipeline/create-highlight-plan.js +22 -16
- package/dist/services/tts/highlight-pipeline/prose-boundary-resolver.js +6 -4
- package/dist/services/tts/highlight-pipeline/readable-region.js +3 -2
- package/dist/services/tts/highlight-pipeline/types.d.ts +5 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.d.ts +12 -0
- package/dist/services/tts/highlight-pipeline/visible-map-range.js +73 -7
- package/dist/services/tts/highlight-target-resolver.d.ts +12 -0
- package/dist/services/tts/math-aware-text-processing.d.ts +5 -1
- package/dist/services/tts/math-aware-text-processing.js +42 -47
- package/dist/services/tts/math-control-names.d.ts +21 -0
- package/dist/services/tts/math-control-names.js +142 -0
- package/dist/services/tts/math-speech.d.ts +12 -1
- package/dist/services/tts/math-speech.js +54 -18
- package/dist/services/tts/mathml-sanitization.d.ts +7 -0
- package/dist/services/tts/mathml-sanitization.js +9 -1
- package/dist/services/tts/provider-options.d.ts +52 -0
- package/dist/services/tts/provider-options.js +1 -0
- package/dist/services/tts/ssml/spoken-text.d.ts +2 -26
- package/dist/services/tts/ssml/spoken-text.js +29 -0
- package/dist/services/tts/start-failure.d.ts +4 -0
- package/dist/services/tts/start-failure.js +19 -0
- package/dist/services/tts/text-processing.d.ts +9 -0
- package/dist/services/tts/text-processing.js +46 -33
- package/dist/services/tts/text-segmentation.d.ts +12 -0
- package/dist/services/tts/text-segmentation.js +60 -10
- package/dist/services/tts-runtime-config.d.ts +54 -26
- package/dist/services/tts-runtime-config.js +27 -112
- package/dist/tools/content-capability-resolution.d.ts +9 -11
- package/dist/tools/content-capability-resolution.js +8 -45
- package/dist/tools/registration.d.ts +59 -0
- package/dist/tools/registration.js +55 -0
- package/dist/tools/registrations/toolbar-registration-helpers.d.ts +6 -0
- package/dist/tools/registrations/toolbar-registration-helpers.js +8 -2
- package/dist/tools/tool-surface-host.d.ts +6 -10
- package/dist/tools/tool-surface-host.js +45 -50
- package/dist/tools/tool-tag-map.d.ts +5 -15
- package/dist/tools/tool-tag-map.js +6 -20
- package/dist/utils/structural-equality.d.ts +11 -0
- package/dist/utils/structural-equality.js +47 -0
- package/package.json +15 -29
- package/dist/attempt/adapters/activity-to-test-attempt-session.d.ts +0 -49
- package/dist/attempt/adapters/activity-to-test-attempt-session.js +0 -138
- package/dist/components/chunks/ItemToolBar-b9dz5jvd.js +0 -40
- package/dist/components/chunks/ItemToolBar-h4xap0t0.js +0 -4
- package/dist/components/chunks/ItemToolBar-wr2myaah.js +0 -2
- package/dist/components/chunks/sre-engine-wf9s43gg.js +0 -1
- package/dist/context/runtime-context-consumer.d.ts +0 -13
- package/dist/context/runtime-context-consumer.js +0 -18
- package/dist/item-loader.d.ts +0 -44
- package/dist/item-loader.js +0 -61
- package/dist/policy/core/engine-context.d.ts +0 -29
- package/dist/policy/core/engine-context.js +0 -27
- package/dist/policy/core/policy-source-tag.d.ts +0 -31
- package/dist/policy/core/policy-source-tag.js +0 -28
- package/dist/policy/internal.d.ts +0 -25
- package/dist/policy/internal.js +0 -25
- package/dist/runtime/adapter/coordinator-bridge.d.ts +0 -92
- package/dist/runtime/adapter/coordinator-bridge.js +0 -126
- package/dist/runtime/adapter/framework-error-bridge.d.ts +0 -36
- package/dist/runtime/adapter/framework-error-bridge.js +0 -46
- package/dist/runtime/adapter/instrumentation-bridge.d.ts +0 -31
- package/dist/runtime/adapter/instrumentation-bridge.js +0 -38
- package/dist/runtime/core/engine-stage-derivation.d.ts +0 -18
- package/dist/runtime/core/engine-stage-derivation.js +0 -29
- package/dist/runtime/engine-context.d.ts +0 -39
- package/dist/runtime/engine-context.js +0 -31
- package/dist/runtime/internal.d.ts +0 -43
- package/dist/runtime/internal.js +0 -45
- package/dist/runtime/section-runtime-engine-host-context.d.ts +0 -74
- package/dist/runtime/section-runtime-engine-host-context.js +0 -57
- package/dist/runtime/stage-emit-gate.d.ts +0 -92
- package/dist/runtime/stage-emit-gate.js +0 -82
- package/dist/services/ContextVariableStore.d.ts +0 -113
- package/dist/services/ContextVariableStore.js +0 -206
- package/dist/services/I18nService.d.ts +0 -47
- package/dist/services/I18nService.js +0 -84
- package/dist/services/ThemeProvider.d.ts +0 -95
- package/dist/services/ThemeProvider.js +0 -334
- package/dist/services/defaultPersonalNeedsProfile.d.ts +0 -21
- package/dist/services/defaultPersonalNeedsProfile.js +0 -26
- package/dist/services/pnp-standard-features.d.ts +0 -238
- package/dist/services/pnp-standard-features.js +0 -338
- package/dist/services/tts-control-events.d.ts +0 -13
- package/dist/services/tts-control-events.js +0 -7
- package/dist/tools/client.d.ts +0 -10
- package/dist/tools/client.js +0 -10
- package/dist/tools/internal.d.ts +0 -44
- package/dist/tools/internal.js +0 -39
- package/dist/utils/logger.d.ts +0 -6
- package/dist/utils/logger.js +0 -10
|
@@ -1,19 +1,56 @@
|
|
|
1
|
+
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
1
2
|
import { setSreStartupLocaleSource, sreLocaleSource } from "./sre-locales.js";
|
|
2
3
|
import { normalizeTextForSpeech } from "./text-processing.js";
|
|
3
|
-
|
|
4
|
+
// Debug lines need PIE_TTS_DEBUG=1 or globalThis.__PIE_TTS_DEBUG__ = true.
|
|
5
|
+
const logger = createPieLogger("tts-math-speech", isTtsDebugEnabled);
|
|
6
|
+
// SRE configures one global engine, so each setupEngine + toSpeech pair runs to
|
|
7
|
+
// completion before the next starts; concurrent reads would otherwise speak
|
|
8
|
+
// under each other's settings.
|
|
4
9
|
let sreOperationQueue = Promise.resolve();
|
|
5
10
|
const normalizeLocale = (language) => (language || "en").split("-")[0].toLowerCase() || "en";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Loads the speech-rule-engine once per page. A failed load is dropped, so the
|
|
13
|
+
* next call tries again; meanwhile math reads as its visible text. The first
|
|
14
|
+
* failure warns, later ones log at debug.
|
|
15
|
+
*/
|
|
16
|
+
export const createSreLoader = (importEngine) => {
|
|
17
|
+
let load = null;
|
|
18
|
+
let warned = false;
|
|
19
|
+
return (engineOptions) => {
|
|
20
|
+
if (!load) {
|
|
21
|
+
setSreStartupLocaleSource(engineOptions);
|
|
22
|
+
const attempt = importEngine();
|
|
23
|
+
load = attempt;
|
|
24
|
+
attempt.catch((error) => {
|
|
25
|
+
if (load === attempt)
|
|
26
|
+
load = null;
|
|
27
|
+
if (warned) {
|
|
28
|
+
logger.debug("speech-rule-engine failed to load again", error);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
warned = true;
|
|
32
|
+
logger.warn("speech-rule-engine failed to load; math reads as its visible text until a later read loads it.", error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return load;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const defaultLoadSre = createSreLoader(() => import("./sre-engine.js").then(({ default: candidate }) => {
|
|
39
|
+
if (!candidate || typeof candidate.toSpeech !== "function") {
|
|
40
|
+
throw new Error("speech-rule-engine did not expose toSpeech");
|
|
15
41
|
}
|
|
16
|
-
return
|
|
42
|
+
return candidate;
|
|
43
|
+
}));
|
|
44
|
+
/**
|
|
45
|
+
* One speech-rule-engine load for a whole read: its equations share the
|
|
46
|
+
* attempt, so a failed load costs a read one attempt.
|
|
47
|
+
*/
|
|
48
|
+
export const createReadSreLoad = (mathSpeech) => {
|
|
49
|
+
let attempt = null;
|
|
50
|
+
return () => {
|
|
51
|
+
attempt ??= defaultLoadSre(normalizeSREMathSpeechOptions(mathSpeech)?.engineOptions);
|
|
52
|
+
return attempt;
|
|
53
|
+
};
|
|
17
54
|
};
|
|
18
55
|
// SRE's ClearSpeak rule set is English-only; MathSpeak is localized to many more
|
|
19
56
|
// locales. Use ClearSpeak for English (highest-quality math prose) and MathSpeak
|
|
@@ -47,9 +84,9 @@ const setupSre = async (sre, language, markup, mathSpeech) => {
|
|
|
47
84
|
...engineOptions,
|
|
48
85
|
locale,
|
|
49
86
|
domain: normalizedMathSpeech?.domain || domainForLocale(locale),
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
// SRE keeps a style it was given until it is given another, so a call
|
|
88
|
+
// without one resets it: math control names set ClearSpeak preferences.
|
|
89
|
+
style: normalizedMathSpeech?.style || "default",
|
|
53
90
|
modality: "speech",
|
|
54
91
|
markup,
|
|
55
92
|
});
|
|
@@ -80,7 +117,7 @@ const generateMathSsml = async (sre, mathml, language, mathSpeech) => {
|
|
|
80
117
|
return ssml.includes("<speak") ? ssml : null;
|
|
81
118
|
}
|
|
82
119
|
catch (error) {
|
|
83
|
-
|
|
120
|
+
logger.debug("math SSML generation failed; using plain", {
|
|
84
121
|
message: error instanceof Error ? error.message : String(error),
|
|
85
122
|
});
|
|
86
123
|
return null;
|
|
@@ -99,8 +136,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
|
|
|
99
136
|
let sre = null;
|
|
100
137
|
let usedMathSpeech = false;
|
|
101
138
|
let usedFallback = false;
|
|
102
|
-
const loadSre = options.loadSre ||
|
|
103
|
-
(() => defaultLoadSre(normalizeSREMathSpeechOptions(options.mathSpeech)?.engineOptions));
|
|
139
|
+
const loadSre = options.loadSre || createReadSreLoad(options.mathSpeech);
|
|
104
140
|
const speechParts = [];
|
|
105
141
|
for (const chunk of chunks) {
|
|
106
142
|
if (chunk.type === "text") {
|
|
@@ -122,7 +158,7 @@ export const resolveMathSpeechFromChunks = async (chunks, options = {}) => {
|
|
|
122
158
|
}
|
|
123
159
|
catch (error) {
|
|
124
160
|
usedFallback = true;
|
|
125
|
-
|
|
161
|
+
logger.debug("math speech generation failed; using visible fallback", {
|
|
126
162
|
message: error instanceof Error ? error.message : String(error),
|
|
127
163
|
});
|
|
128
164
|
speechParts.push(chunk.fallbackText);
|
|
@@ -1 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MathML re-serialized from an allow-list, with a MathML xmlns on the root.
|
|
3
|
+
* Null for empty or unparseable input, no DOMParser, a non-<math> root, a
|
|
4
|
+
* disallowed element or a node other than element/text/comment, or an xmlns
|
|
5
|
+
* other than MathML's. Comments, attributes off the allow-list and values
|
|
6
|
+
* containing javascript: or data:text/html are dropped.
|
|
7
|
+
*/
|
|
1
8
|
export declare const canonicalizeMathML: (input: string) => string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML";
|
|
2
|
+
const XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml";
|
|
2
3
|
const ALLOWED_MATHML_ELEMENTS = new Set([
|
|
3
4
|
"math",
|
|
4
5
|
"maction",
|
|
@@ -109,7 +110,7 @@ const isAllowedMathElement = (element) => {
|
|
|
109
110
|
const namespace = element.namespaceURI;
|
|
110
111
|
return (!namespace ||
|
|
111
112
|
namespace === MATHML_NAMESPACE ||
|
|
112
|
-
namespace ===
|
|
113
|
+
namespace === XHTML_NAMESPACE);
|
|
113
114
|
};
|
|
114
115
|
const serializeMathNode = (node) => {
|
|
115
116
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
@@ -153,6 +154,13 @@ const parseMathML = (input) => {
|
|
|
153
154
|
}
|
|
154
155
|
return null;
|
|
155
156
|
};
|
|
157
|
+
/**
|
|
158
|
+
* MathML re-serialized from an allow-list, with a MathML xmlns on the root.
|
|
159
|
+
* Null for empty or unparseable input, no DOMParser, a non-<math> root, a
|
|
160
|
+
* disallowed element or a node other than element/text/comment, or an xmlns
|
|
161
|
+
* other than MathML's. Comments, attributes off the allow-list and values
|
|
162
|
+
* containing javascript: or data:text/html are dropped.
|
|
163
|
+
*/
|
|
156
164
|
export const canonicalizeMathML = (input) => {
|
|
157
165
|
const trimmed = String(input || "").trim();
|
|
158
166
|
if (!trimmed)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { TTSConfig, TTSProviderOptions } from "@pie-players/pie-tts";
|
|
2
|
+
import type { SREMathSpeechOptions } from "./math-speech.js";
|
|
3
|
+
import type { BoundarySpacingMode } from "./text-processing.js";
|
|
4
|
+
/**
|
|
5
|
+
* The provider options the toolkit reads, on top of the ones every provider
|
|
6
|
+
* receives. Hosts configure them as free-form options, so readers still check
|
|
7
|
+
* each value at runtime.
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolkitTTSProviderOptions extends TTSProviderOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Highlight words or sentences while reading. Defaults by provider
|
|
12
|
+
* capability. A playback that sends no word boundaries highlights its
|
|
13
|
+
* sentence in either mode.
|
|
14
|
+
*/
|
|
15
|
+
highlightMode?: "word" | "sentence";
|
|
16
|
+
/** Locale for text processing; the toolkit sets it per speak. */
|
|
17
|
+
locale?: string;
|
|
18
|
+
/** Speech-rule-engine options for math speech. */
|
|
19
|
+
mathSpeech?: SREMathSpeechOptions;
|
|
20
|
+
/** Sentence and word segmentation of the browser provider. */
|
|
21
|
+
segmenter?: {
|
|
22
|
+
/** `regexOnly` segments without `Intl.Segmenter`. */
|
|
23
|
+
mode?: "regexOnly";
|
|
24
|
+
locale?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Pauses at structural boundaries: `baseMs` scaled by a strength unit, clamped. */
|
|
27
|
+
structuralPauses?: {
|
|
28
|
+
baseMs?: number | string;
|
|
29
|
+
units?: {
|
|
30
|
+
minor?: number | string;
|
|
31
|
+
section?: number | string;
|
|
32
|
+
major?: number | string;
|
|
33
|
+
};
|
|
34
|
+
minMs?: number | string;
|
|
35
|
+
maxMs?: number | string;
|
|
36
|
+
};
|
|
37
|
+
textNormalization?: {
|
|
38
|
+
boundarySpacingMode?: BoundarySpacingMode;
|
|
39
|
+
locale?: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** A provider configuration as the toolkit reads it. */
|
|
43
|
+
export interface ToolkitTTSConfig extends TTSConfig {
|
|
44
|
+
providerOptions?: ToolkitTTSProviderOptions;
|
|
45
|
+
/**
|
|
46
|
+
* Highlight math token by token; `false` highlights each formula as one
|
|
47
|
+
* block. The highlight pipeline reads it, and providers ignore it.
|
|
48
|
+
*
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
mathTokenHighlighting?: boolean;
|
|
52
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,29 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Strips SSML markup to the words a TTS engine would actually voice, while
|
|
5
|
-
* recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
|
|
6
|
-
* normalized spoken-text offsets. This is the single source of truth for SSML
|
|
7
|
-
* stripping used by:
|
|
8
|
-
* - the catalog span aligner (`catalog-span-alignment.ts`),
|
|
9
|
-
* - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
|
|
10
|
-
* - the runtime generated-speech path (PIE-623).
|
|
11
|
-
*
|
|
12
|
-
* Keeping one implementation guarantees provider word-boundary offsets (which
|
|
13
|
-
* are into the raw SSML string) map to the same spoken text on every path.
|
|
14
|
-
*
|
|
15
|
-
* Tag handling:
|
|
16
|
-
* - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
|
|
17
|
-
* `voice`, `lang`) are transparent; their text content flows through.
|
|
18
|
-
* `p`/`s` open tags inject a word boundary space.
|
|
19
|
-
* - `break` injects a space (a pause is a word boundary).
|
|
20
|
-
* - `sub alias="…"` substitutes the alias text (and skips the original).
|
|
21
|
-
* `sub` with no alias marks the content unsupported for word tracking.
|
|
22
|
-
* - Semantic tags that change *what* is spoken without leaving trackable
|
|
23
|
-
* text (`audio`, `phoneme`) mark the content unsupported.
|
|
24
|
-
* - Any other unknown element marks the content unsupported (degrade to
|
|
25
|
-
* coarse region highlighting rather than mis-track).
|
|
26
|
-
*/
|
|
1
|
+
/** Whether `text` is an SSML document: it opens with a `<speak>` element. */
|
|
2
|
+
export declare const isSsmlDocument: (text: string) => boolean;
|
|
27
3
|
export interface ExtractedSpokenText {
|
|
28
4
|
spokenText: string;
|
|
29
5
|
rawToSpokenOffsetMap: Map<number, number>;
|
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { normalizeTextForSpeech } from "../text-processing.js";
|
|
2
|
+
/**
|
|
3
|
+
* Shared SSML → spoken-text extractor.
|
|
4
|
+
*
|
|
5
|
+
* Strips SSML markup to the words a TTS engine would actually voice, while
|
|
6
|
+
* recording a `rawToSpokenOffsetMap` from raw-SSML character offsets to
|
|
7
|
+
* normalized spoken-text offsets. This is the single source of truth for SSML
|
|
8
|
+
* stripping used by:
|
|
9
|
+
* - the catalog span aligner (`catalog-span-alignment.ts`),
|
|
10
|
+
* - the math speech tokenizer (`math-alignment/speech-tokenizer.ts`),
|
|
11
|
+
* - the runtime generated-speech path.
|
|
12
|
+
*
|
|
13
|
+
* Keeping one implementation guarantees provider word-boundary offsets (which
|
|
14
|
+
* are into the raw SSML string) map to the same spoken text on every path.
|
|
15
|
+
*
|
|
16
|
+
* Tag handling:
|
|
17
|
+
* - Structural tags (`speak`, `p`, `s`, `emphasis`, `prosody`, `say-as`,
|
|
18
|
+
* `voice`, `lang`) are transparent; their text content flows through.
|
|
19
|
+
* `p`/`s` open tags inject a word boundary space.
|
|
20
|
+
* - `break` injects a space (a pause is a word boundary).
|
|
21
|
+
* - `sub alias="…"` substitutes the alias text (and skips the original).
|
|
22
|
+
* `sub` with no alias marks the content unsupported for word tracking.
|
|
23
|
+
* - Semantic tags that change *what* is spoken without leaving trackable
|
|
24
|
+
* text (`audio`, `phoneme`) mark the content unsupported.
|
|
25
|
+
* - Any other unknown element marks the content unsupported (degrade to
|
|
26
|
+
* coarse region highlighting rather than mis-track).
|
|
27
|
+
*/
|
|
28
|
+
const SSML_DOCUMENT = /^\s*<speak[\s/>]/i;
|
|
29
|
+
/** Whether `text` is an SSML document: it opens with a `<speak>` element. */
|
|
30
|
+
export const isSsmlDocument = (text) => SSML_DOCUMENT.test(text);
|
|
2
31
|
const STRUCTURAL_SSML_TAGS = new Set([
|
|
3
32
|
"speak",
|
|
4
33
|
"p",
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Whether a speak rejected because speech could not start. */
|
|
2
|
+
export declare function isTTSStartFailure(error: unknown): boolean;
|
|
3
|
+
/** Marks `cause` as a start failure, keeping its message and stack. */
|
|
4
|
+
export declare function toTTSStartFailure(cause: unknown): Error;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `code` a speak's rejection carries when speech could not start, as
|
|
3
|
+
* distinct from a failure while speaking. Read by shape: several copies of the
|
|
4
|
+
* toolkit can share a page, so an error class from one fails `instanceof` in
|
|
5
|
+
* another.
|
|
6
|
+
*/
|
|
7
|
+
const TTS_START_FAILED_CODE = "tts-start-failed";
|
|
8
|
+
/** Whether a speak rejected because speech could not start. */
|
|
9
|
+
export function isTTSStartFailure(error) {
|
|
10
|
+
return (typeof error === "object" &&
|
|
11
|
+
error !== null &&
|
|
12
|
+
error.code === TTS_START_FAILED_CODE);
|
|
13
|
+
}
|
|
14
|
+
/** Marks `cause` as a start failure, keeping its message and stack. */
|
|
15
|
+
export function toTTSStartFailure(cause) {
|
|
16
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
17
|
+
error.code = TTS_START_FAILED_CODE;
|
|
18
|
+
return error;
|
|
19
|
+
}
|
|
@@ -62,6 +62,15 @@ export declare const collectRangeTextForSpeech: (range: Range, root: Element) =>
|
|
|
62
62
|
text: string;
|
|
63
63
|
filtered: boolean;
|
|
64
64
|
};
|
|
65
|
+
/** The part of `textNode`'s text that `range` holds, for a node it intersects. */
|
|
66
|
+
export declare const textInRange: (textNode: Text, range: Range) => string;
|
|
67
|
+
/**
|
|
68
|
+
* Whether `range` holds all of `element`'s speakable text: its first and last
|
|
69
|
+
* speakable characters, which a contiguous range holding both holds everything
|
|
70
|
+
* between. An element with no speakable text, an image for one, is held when the
|
|
71
|
+
* range touches it, since no part of it can be selected alone.
|
|
72
|
+
*/
|
|
73
|
+
export declare const rangeHoldsSpeakableElement: (range: Range, element: Element, root?: Element | null) => boolean;
|
|
65
74
|
export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
|
|
66
75
|
export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
|
|
67
76
|
text: string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { flatTreeParentElement, flatTreeTextNodes, rangeHoldsTextPosition, rangeIntersectsComposedNode, } from "./flat-tree.js";
|
|
1
2
|
export const normalizeTextForSpeech = (text) => text.trim().replace(/\s+/g, " ");
|
|
2
3
|
// Single source of truth for aligning spoken text to visible / math text.
|
|
3
4
|
// Letters are matched with `\p{L}` so the tokenizer is Unicode-aware — accented
|
|
@@ -50,15 +51,15 @@ export const isElementHiddenForTTS = (element) => {
|
|
|
50
51
|
return false;
|
|
51
52
|
};
|
|
52
53
|
export const isNodeHiddenForTTS = (node, root) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// Up the flat tree, so a node in a shadow root inherits its host's
|
|
55
|
+
// visibility and a slotted node the visibility of the slot rendering it.
|
|
56
|
+
let current = node.nodeType === 1 ? node : flatTreeParentElement(node);
|
|
56
57
|
while (current) {
|
|
57
58
|
if (isElementHiddenForTTS(current))
|
|
58
59
|
return true;
|
|
59
60
|
if (root && current === root)
|
|
60
61
|
break;
|
|
61
|
-
current = current
|
|
62
|
+
current = flatTreeParentElement(current);
|
|
62
63
|
}
|
|
63
64
|
return false;
|
|
64
65
|
};
|
|
@@ -112,15 +113,14 @@ export const isElementSuppressedForTTS = (element) => {
|
|
|
112
113
|
return true;
|
|
113
114
|
};
|
|
114
115
|
export const isNodeSuppressedForTTS = (node, root) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
: node.parentElement;
|
|
116
|
+
// Through shadow hosts as well: suppression on a host covers its shadow tree.
|
|
117
|
+
let current = node.nodeType === 1 ? node : flatTreeParentElement(node);
|
|
118
118
|
while (current) {
|
|
119
119
|
if (isElementSuppressedForTTS(current))
|
|
120
120
|
return true;
|
|
121
121
|
if (root && current === root)
|
|
122
122
|
break;
|
|
123
|
-
current = current
|
|
123
|
+
current = flatTreeParentElement(current);
|
|
124
124
|
}
|
|
125
125
|
return false;
|
|
126
126
|
};
|
|
@@ -146,11 +146,7 @@ export const isNodeExcludedFromSpeech = (node, root) => isNodeHiddenForTTS(node,
|
|
|
146
146
|
* nothing".
|
|
147
147
|
*/
|
|
148
148
|
export const collectRangeTextForSpeech = (range, root) => {
|
|
149
|
-
if (typeof
|
|
150
|
-
typeof document.createTreeWalker !==
|
|
151
|
-
"function" ||
|
|
152
|
-
typeof NodeFilter === "undefined" ||
|
|
153
|
-
typeof range.intersectsNode !== "function") {
|
|
149
|
+
if (typeof range.intersectsNode !== "function") {
|
|
154
150
|
// Degraded, and deliberately not silent about the difference: callers still
|
|
155
151
|
// enforce whole-selection suppression from the range's common ancestor, so
|
|
156
152
|
// the construct guard holds even here. What is lost is per-node filtering
|
|
@@ -159,25 +155,49 @@ export const collectRangeTextForSpeech = (range, root) => {
|
|
|
159
155
|
}
|
|
160
156
|
const parts = [];
|
|
161
157
|
let filtered = false;
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
while (current) {
|
|
165
|
-
const textNode = current;
|
|
166
|
-
if (range.intersectsNode(textNode)) {
|
|
158
|
+
for (const textNode of flatTreeTextNodes(root)) {
|
|
159
|
+
if (rangeIntersectsComposedNode(range, textNode)) {
|
|
167
160
|
if (isNodeExcludedFromSpeech(textNode, root)) {
|
|
168
161
|
filtered = true;
|
|
169
162
|
}
|
|
170
163
|
else {
|
|
171
|
-
|
|
172
|
-
const start = textNode === range.startContainer ? range.startOffset : 0;
|
|
173
|
-
const end = textNode === range.endContainer ? range.endOffset : raw.length;
|
|
174
|
-
parts.push(raw.slice(start, end));
|
|
164
|
+
parts.push(textInRange(textNode, range));
|
|
175
165
|
}
|
|
176
166
|
}
|
|
177
|
-
current = walker.nextNode();
|
|
178
167
|
}
|
|
179
168
|
return { text: parts.join(""), filtered };
|
|
180
169
|
};
|
|
170
|
+
/** The part of `textNode`'s text that `range` holds, for a node it intersects. */
|
|
171
|
+
export const textInRange = (textNode, range) => {
|
|
172
|
+
const raw = textNode.textContent || "";
|
|
173
|
+
const start = textNode === range.startContainer ? range.startOffset : 0;
|
|
174
|
+
const end = textNode === range.endContainer ? range.endOffset : raw.length;
|
|
175
|
+
return raw.slice(start, end);
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Whether `range` holds all of `element`'s speakable text: its first and last
|
|
179
|
+
* speakable characters, which a contiguous range holding both holds everything
|
|
180
|
+
* between. An element with no speakable text, an image for one, is held when the
|
|
181
|
+
* range touches it, since no part of it can be selected alone.
|
|
182
|
+
*/
|
|
183
|
+
export const rangeHoldsSpeakableElement = (range, element, root) => {
|
|
184
|
+
let first = null;
|
|
185
|
+
let last = null;
|
|
186
|
+
for (const textNode of flatTreeTextNodes(element)) {
|
|
187
|
+
if (isNodeExcludedFromSpeech(textNode, root))
|
|
188
|
+
continue;
|
|
189
|
+
const raw = textNode.textContent || "";
|
|
190
|
+
const firstOffset = raw.search(/\S/);
|
|
191
|
+
if (firstOffset === -1)
|
|
192
|
+
continue;
|
|
193
|
+
first ??= { node: textNode, offset: firstOffset };
|
|
194
|
+
last = { node: textNode, offset: raw.trimEnd().length - 1 };
|
|
195
|
+
}
|
|
196
|
+
if (!first || !last)
|
|
197
|
+
return rangeIntersectsComposedNode(range, element);
|
|
198
|
+
return (rangeHoldsTextPosition(range, first.node, first.offset) &&
|
|
199
|
+
rangeHoldsTextPosition(range, last.node, last.offset));
|
|
200
|
+
};
|
|
181
201
|
export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) => {
|
|
182
202
|
if (!previousChar || !nextChar)
|
|
183
203
|
return false;
|
|
@@ -210,10 +230,7 @@ export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) =
|
|
|
210
230
|
};
|
|
211
231
|
export const collectVisibleTextAndMap = (element, options) => {
|
|
212
232
|
const map = new Map();
|
|
213
|
-
if (typeof document === "undefined"
|
|
214
|
-
typeof document.createTreeWalker !==
|
|
215
|
-
"function" ||
|
|
216
|
-
typeof NodeFilter === "undefined") {
|
|
233
|
+
if (typeof document === "undefined") {
|
|
217
234
|
return { text: "", map };
|
|
218
235
|
}
|
|
219
236
|
const outChars = [];
|
|
@@ -222,11 +239,8 @@ export const collectVisibleTextAndMap = (element, options) => {
|
|
|
222
239
|
let lastCharWasWhitespace = false;
|
|
223
240
|
let lastMapped = null;
|
|
224
241
|
let previousVisibleChar = null;
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
while (current) {
|
|
228
|
-
const textNode = current;
|
|
229
|
-
const parent = textNode.parentElement;
|
|
242
|
+
for (const textNode of flatTreeTextNodes(element)) {
|
|
243
|
+
const parent = flatTreeParentElement(textNode);
|
|
230
244
|
if (parent && !isNodeExcludedFromSpeech(textNode, element)) {
|
|
231
245
|
const raw = textNode.textContent || "";
|
|
232
246
|
const firstVisibleMatch = raw.match(/\S/);
|
|
@@ -274,7 +288,6 @@ export const collectVisibleTextAndMap = (element, options) => {
|
|
|
274
288
|
}
|
|
275
289
|
}
|
|
276
290
|
}
|
|
277
|
-
current = walker.nextNode();
|
|
278
291
|
}
|
|
279
292
|
const text = outChars.join("").trimEnd();
|
|
280
293
|
while (map.has(text.length)) {
|
|
@@ -6,4 +6,16 @@ export interface SentenceSegmentationOptions {
|
|
|
6
6
|
locale?: string;
|
|
7
7
|
useSentenceSegmenter?: boolean;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* `text` split into sentences at their offsets in it, by Intl.Segmenter unless
|
|
11
|
+
* the policy disables it or it is missing. The segmenter path drops
|
|
12
|
+
* whitespace-only segments; the regex fallback keeps every match.
|
|
13
|
+
*/
|
|
9
14
|
export declare const segmentSentences: (text: string, options?: SentenceSegmentationOptions) => SentenceSegment[];
|
|
15
|
+
/**
|
|
16
|
+
* `text` in pieces of at most `maxLength` characters: whole sentences while they
|
|
17
|
+
* fit, a sentence too long for one piece cut at its last space that fits, and a
|
|
18
|
+
* word too long for one piece cut where the limit falls. Each piece is a trimmed
|
|
19
|
+
* substring of `text`, at its offset there.
|
|
20
|
+
*/
|
|
21
|
+
export declare const splitTextToLength: (text: string, maxLength: number, options?: SentenceSegmentationOptions) => SentenceSegment[];
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `text` split into sentences at their offsets in it, by Intl.Segmenter unless
|
|
3
|
+
* the policy disables it or it is missing. The segmenter path drops
|
|
4
|
+
* whitespace-only segments; the regex fallback keeps every match.
|
|
5
|
+
*/
|
|
1
6
|
export const segmentSentences = (text, options) => {
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const Segmenter = globalThis.Intl?.Segmenter;
|
|
8
|
-
if (typeof Segmenter === "function") {
|
|
7
|
+
const Segmenter = options?.useSentenceSegmenter === false
|
|
8
|
+
? undefined
|
|
9
|
+
: globalThis.Intl?.Segmenter;
|
|
10
|
+
if (typeof Segmenter === "function") {
|
|
11
|
+
try {
|
|
9
12
|
const segmenter = new Segmenter(options?.locale, {
|
|
10
13
|
granularity: "sentence",
|
|
11
14
|
});
|
|
@@ -18,9 +21,9 @@ export const segmentSentences = (text, options) => {
|
|
|
18
21
|
if (parsed.length > 0)
|
|
19
22
|
return parsed;
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
catch {
|
|
25
|
+
// Fall through to regex segmentation.
|
|
26
|
+
}
|
|
24
27
|
}
|
|
25
28
|
const sentenceRegex = /[^.!?]+(?:[.!?]+|$)/g;
|
|
26
29
|
const raw = text.match(sentenceRegex) || [text];
|
|
@@ -35,3 +38,50 @@ export const segmentSentences = (text, options) => {
|
|
|
35
38
|
}
|
|
36
39
|
return parsed.length > 0 ? parsed : [{ text, offset: 0 }];
|
|
37
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* `text` in pieces of at most `maxLength` characters: whole sentences while they
|
|
43
|
+
* fit, a sentence too long for one piece cut at its last space that fits, and a
|
|
44
|
+
* word too long for one piece cut where the limit falls. Each piece is a trimmed
|
|
45
|
+
* substring of `text`, at its offset there.
|
|
46
|
+
*/
|
|
47
|
+
export const splitTextToLength = (text, maxLength, options) => {
|
|
48
|
+
const limit = Math.max(1, Math.floor(maxLength));
|
|
49
|
+
const pieces = [];
|
|
50
|
+
let start = -1;
|
|
51
|
+
let end = -1;
|
|
52
|
+
const flush = () => {
|
|
53
|
+
if (start >= 0)
|
|
54
|
+
pieces.push({ text: text.slice(start, end), offset: start });
|
|
55
|
+
start = -1;
|
|
56
|
+
};
|
|
57
|
+
const append = (spanStart, spanEnd) => {
|
|
58
|
+
if (start >= 0 && spanEnd - start <= limit) {
|
|
59
|
+
end = spanEnd;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
flush();
|
|
63
|
+
let cursor = spanStart;
|
|
64
|
+
while (spanEnd - cursor > limit) {
|
|
65
|
+
const window = text.slice(cursor, cursor + limit + 1);
|
|
66
|
+
const space = window.search(/\s\S*$/);
|
|
67
|
+
const cut = space > 0 ? cursor + space : cursor + limit;
|
|
68
|
+
pieces.push({ text: text.slice(cursor, cut).trimEnd(), offset: cursor });
|
|
69
|
+
cursor = cut;
|
|
70
|
+
while (cursor < spanEnd && /\s/.test(text[cursor]))
|
|
71
|
+
cursor++;
|
|
72
|
+
}
|
|
73
|
+
if (cursor < spanEnd) {
|
|
74
|
+
start = cursor;
|
|
75
|
+
end = spanEnd;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
for (const sentence of segmentSentences(text, options)) {
|
|
79
|
+
const leading = sentence.text.search(/\S/);
|
|
80
|
+
if (leading === -1)
|
|
81
|
+
continue;
|
|
82
|
+
const spanStart = sentence.offset + leading;
|
|
83
|
+
append(spanStart, sentence.offset + sentence.text.trimEnd().length);
|
|
84
|
+
}
|
|
85
|
+
flush();
|
|
86
|
+
return pieces;
|
|
87
|
+
};
|