@pie-players/pie-assessment-toolkit 0.3.63 → 0.3.65
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 +92 -1
- package/dist/components/ItemToolBar.custom-element.js +1 -1
- package/dist/components/PieAssessmentToolkit.custom-element.js +11 -11
- package/dist/components/SectionToolBar.custom-element.js +1 -1
- package/dist/components/chunks/ItemToolBar-cckwpz6c.js +51 -0
- package/dist/components/chunks/ItemToolBar-pryf0rtz.js +22 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.js +12 -4
- package/dist/policy/core/ToolPolicyEngine.d.ts +21 -0
- package/dist/policy/core/ToolPolicyEngine.js +27 -0
- package/dist/policy/core/feature-decision.d.ts +57 -0
- package/dist/policy/core/feature-decision.js +40 -0
- package/dist/policy/engine.d.ts +1 -0
- package/dist/policy/sources/PnpPolicySource.d.ts +22 -0
- package/dist/policy/sources/PnpPolicySource.js +41 -11
- package/dist/runtime/catalog-registration.d.ts +56 -1
- package/dist/runtime/catalog-registration.js +64 -31
- package/dist/runtime/composition-emit-scheduler.d.ts +78 -0
- package/dist/runtime/composition-emit-scheduler.js +154 -0
- package/dist/runtime/core/engine-resolver.d.ts +1 -1
- package/dist/services/AccessibilityCatalogResolver.d.ts +100 -4
- package/dist/services/AccessibilityCatalogResolver.js +183 -58
- package/dist/services/SSMLExtractor.js +28 -18
- package/dist/services/TTSService.d.ts +25 -0
- package/dist/services/TTSService.js +241 -45
- package/dist/services/ToolRegistry.d.ts +218 -8
- package/dist/services/ToolRegistry.js +124 -8
- package/dist/services/ToolkitCoordinator.d.ts +25 -3
- package/dist/services/ToolkitCoordinator.js +47 -6
- package/dist/services/catalog-media.d.ts +25 -0
- package/dist/services/catalog-media.js +101 -0
- package/dist/services/createDefaultToolRegistry.d.ts +25 -58
- package/dist/services/createDefaultToolRegistry.js +24 -104
- package/dist/services/defaultPersonalNeedsProfile.d.ts +20 -2
- package/dist/services/defaultPersonalNeedsProfile.js +21 -19
- package/dist/services/interfaces.d.ts +29 -2
- package/dist/services/pnp-standard-features.d.ts +1 -1
- package/dist/services/spoken-audio-cards.d.ts +54 -0
- package/dist/services/spoken-audio-cards.js +66 -0
- package/dist/services/tool-config-defaults.d.ts +7 -23
- package/dist/services/tool-config-defaults.js +7 -46
- package/dist/services/tool-config-validation.d.ts +1 -1
- package/dist/services/tool-config-validation.js +44 -4
- package/dist/services/tts/browser-provider.js +2 -1
- package/dist/services/tts/math-aware-text-processing.js +3 -3
- package/dist/services/tts/text-processing.d.ts +51 -0
- package/dist/services/tts/text-processing.js +117 -1
- package/dist/services/tts-runtime-config.js +7 -2
- package/dist/tools/internal.d.ts +34 -0
- package/dist/tools/internal.js +33 -0
- package/dist/tools/tool-tag-map.d.ts +15 -3
- package/dist/tools/tool-tag-map.js +21 -18
- package/package.json +14 -10
- package/dist/components/chunks/ItemToolBar-843902tp.js +0 -51
- package/dist/components/chunks/ItemToolBar-84nv78dy.js +0 -22
- package/dist/tools/registrations/accessibility-tools.d.ts +0 -34
- package/dist/tools/registrations/accessibility-tools.js +0 -217
- package/dist/tools/registrations/calculator.d.ts +0 -20
- package/dist/tools/registrations/calculator.js +0 -228
- package/dist/tools/registrations/interaction-tools.d.ts +0 -27
- package/dist/tools/registrations/interaction-tools.js +0 -143
- package/dist/tools/registrations/measurement-tools.d.ts +0 -24
- package/dist/tools/registrations/measurement-tools.js +0 -130
- package/dist/tools/registrations/subject-specific-tools.d.ts +0 -27
- package/dist/tools/registrations/subject-specific-tools.js +0 -158
- package/dist/tools/registrations/tts.d.ts +0 -21
- package/dist/tools/registrations/tts.js +0 -184
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded audio as a `spoken` catalog card.
|
|
3
|
+
*
|
|
4
|
+
* QTI 3 treats a recording and synthesized speech as the *same* support — both
|
|
5
|
+
* are `spoken`, and a card carries recorded audio through `qti-file-href` plus a
|
|
6
|
+
* MIME type — so this is not a new accommodation but the other form the existing
|
|
7
|
+
* one can take. Some programs prefer a human voice to synthesis; PIE's `spoken`
|
|
8
|
+
* card was string-only, so it had no way to say "play this file for this node".
|
|
9
|
+
*
|
|
10
|
+
* A node commonly carries both forms in the same language: the reading script
|
|
11
|
+
* *and* a recording of it. That is APIP's pattern and what QTI's migration
|
|
12
|
+
* guidance preserves, because the script is both the source the audio was
|
|
13
|
+
* generated from and the fallback for when the audio will not play. Resolution
|
|
14
|
+
* chooses between them with `CatalogLookupOptions.form`; playback treats the
|
|
15
|
+
* script as the recording's fallback.
|
|
16
|
+
*
|
|
17
|
+
* Validation posture matches sign-language cards: "treat as absent, never as
|
|
18
|
+
* partially valid". A malformed payload must not produce a silent player that
|
|
19
|
+
* looks like read-aloud is working.
|
|
20
|
+
*
|
|
21
|
+
* Part of PIE Assessment Toolkit.
|
|
22
|
+
*/
|
|
23
|
+
import { normalizeMediaFragment, normalizeMediaSources, trimmedOrUndefined, } from "./catalog-media.js";
|
|
24
|
+
/** Catalog type token for spoken alternates. Matches QTI 3's `support` value. */
|
|
25
|
+
export const SPOKEN_CATALOG_TYPE = "spoken";
|
|
26
|
+
/**
|
|
27
|
+
* Validate a `spoken` card's payload into something playable, or `null`.
|
|
28
|
+
*
|
|
29
|
+
* Silent when the card simply is not a recording — a card carrying `content` is
|
|
30
|
+
* a reading script, which is the overwhelmingly common case and not a fault.
|
|
31
|
+
* Loud when a card looks like it meant to be a recording and cannot be played,
|
|
32
|
+
* because that failure is otherwise invisible to everyone but the learner who
|
|
33
|
+
* needed it.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSpokenAudioMedia(card) {
|
|
36
|
+
if (!card)
|
|
37
|
+
return null;
|
|
38
|
+
const payload = card.payload;
|
|
39
|
+
if (!payload || typeof payload !== "object") {
|
|
40
|
+
// A script card, not a broken audio card. Resolution asks for the payload
|
|
41
|
+
// form as a *preference*, so getting a `content` card back here is normal.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const media = payload.media;
|
|
45
|
+
if (!media || typeof media !== "object") {
|
|
46
|
+
console.warn("[spoken-audio] card carries a `payload` with no `media`; recorded speech needs `media.sources`, so this card is ignored and read-aloud falls back to the script or generated speech");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
// A signing video filed under `spoken` is a mis-authored card, not an audio
|
|
50
|
+
// track to guess at. Refusing it keeps the two card types from quietly
|
|
51
|
+
// swapping roles.
|
|
52
|
+
if (media.kind !== undefined && media.kind !== "audio") {
|
|
53
|
+
console.warn(`[spoken-audio] card's media is kind "${media.kind}", not "audio"; a spoken card carries recorded speech, so this card is ignored`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const sources = normalizeMediaSources(media.sources);
|
|
57
|
+
if (sources.length === 0) {
|
|
58
|
+
console.warn("[spoken-audio] card's `media.sources` yielded no usable URL; read-aloud falls back to the script or generated speech");
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
sources,
|
|
63
|
+
fragment: normalizeMediaFragment(payload.fragment),
|
|
64
|
+
label: trimmedOrUndefined(media.label),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement defaults this package can hold.
|
|
3
|
+
*
|
|
4
|
+
* Only the empty one. `PACKAGED_TOOL_PLACEMENT` and
|
|
5
|
+
* `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` name capabilities, so they moved to
|
|
6
|
+
* the composition layer (`@pie-players/pie-default-tool-loaders`).
|
|
7
|
+
*/
|
|
1
8
|
export declare const DEFAULT_TOOL_PLACEMENT: {
|
|
2
9
|
readonly assessment: readonly [];
|
|
3
10
|
readonly section: readonly [];
|
|
@@ -6,26 +13,3 @@ export declare const DEFAULT_TOOL_PLACEMENT: {
|
|
|
6
13
|
readonly rubric: readonly [];
|
|
7
14
|
readonly element: readonly [];
|
|
8
15
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Complete placement preset for hosts that want to enable all packaged tools.
|
|
11
|
-
* Prefer explicit host configuration over implicit defaults.
|
|
12
|
-
*/
|
|
13
|
-
export declare const PACKAGED_TOOL_PLACEMENT: {
|
|
14
|
-
readonly assessment: readonly ["theme"];
|
|
15
|
-
readonly section: readonly ["theme"];
|
|
16
|
-
readonly item: readonly ["textToSpeech", "highlighter", "annotationToolbar", "graph", "periodicTable"];
|
|
17
|
-
readonly passage: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
|
|
18
|
-
readonly rubric: readonly ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"];
|
|
19
|
-
readonly element: readonly ["calculator", "answerEliminator", "textToSpeech", "ruler", "protractor", "highlighter", "annotationToolbar", "graph", "periodicTable"];
|
|
20
|
-
};
|
|
21
|
-
/**
|
|
22
|
-
* Opt-in section-player placement preset for hosts that want to expose every
|
|
23
|
-
* packaged tool once at its normal assessment surface. This is not exhaustive:
|
|
24
|
-
* `supportedLevels` still defines where tools can run, and hosts can still
|
|
25
|
-
* choose different placement for custom UX.
|
|
26
|
-
*/
|
|
27
|
-
export declare const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT: {
|
|
28
|
-
section: string[];
|
|
29
|
-
item: string[];
|
|
30
|
-
passage: string[];
|
|
31
|
-
};
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placement defaults this package can hold.
|
|
3
|
+
*
|
|
4
|
+
* Only the empty one. `PACKAGED_TOOL_PLACEMENT` and
|
|
5
|
+
* `SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT` name capabilities, so they moved to
|
|
6
|
+
* the composition layer (`@pie-players/pie-default-tool-loaders`).
|
|
7
|
+
*/
|
|
1
8
|
export const DEFAULT_TOOL_PLACEMENT = {
|
|
2
9
|
assessment: [],
|
|
3
10
|
section: [],
|
|
@@ -6,49 +13,3 @@ export const DEFAULT_TOOL_PLACEMENT = {
|
|
|
6
13
|
rubric: [],
|
|
7
14
|
element: [],
|
|
8
15
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Complete placement preset for hosts that want to enable all packaged tools.
|
|
11
|
-
* Prefer explicit host configuration over implicit defaults.
|
|
12
|
-
*/
|
|
13
|
-
export const PACKAGED_TOOL_PLACEMENT = {
|
|
14
|
-
assessment: ["theme"],
|
|
15
|
-
section: ["theme"],
|
|
16
|
-
item: [
|
|
17
|
-
"textToSpeech",
|
|
18
|
-
"highlighter",
|
|
19
|
-
"annotationToolbar",
|
|
20
|
-
"graph",
|
|
21
|
-
"periodicTable",
|
|
22
|
-
],
|
|
23
|
-
passage: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
|
|
24
|
-
rubric: ["textToSpeech", "highlighter", "annotationToolbar", "lineReader"],
|
|
25
|
-
element: [
|
|
26
|
-
"calculator",
|
|
27
|
-
"answerEliminator",
|
|
28
|
-
"textToSpeech",
|
|
29
|
-
"ruler",
|
|
30
|
-
"protractor",
|
|
31
|
-
"highlighter",
|
|
32
|
-
"annotationToolbar",
|
|
33
|
-
"graph",
|
|
34
|
-
"periodicTable",
|
|
35
|
-
],
|
|
36
|
-
};
|
|
37
|
-
/**
|
|
38
|
-
* Opt-in section-player placement preset for hosts that want to expose every
|
|
39
|
-
* packaged tool once at its normal assessment surface. This is not exhaustive:
|
|
40
|
-
* `supportedLevels` still defines where tools can run, and hosts can still
|
|
41
|
-
* choose different placement for custom UX.
|
|
42
|
-
*/
|
|
43
|
-
export const SECTION_PLAYER_PREFERRED_TOOL_PLACEMENT = {
|
|
44
|
-
section: [
|
|
45
|
-
"theme",
|
|
46
|
-
"graph",
|
|
47
|
-
"periodicTable",
|
|
48
|
-
"lineReader",
|
|
49
|
-
"ruler",
|
|
50
|
-
"protractor",
|
|
51
|
-
],
|
|
52
|
-
item: ["calculator", "textToSpeech", "answerEliminator", "annotationToolbar"],
|
|
53
|
-
passage: ["textToSpeech", "annotationToolbar"],
|
|
54
|
-
};
|
|
@@ -4,7 +4,7 @@ import type { ToolRegistry } from "./ToolRegistry.js";
|
|
|
4
4
|
export type ToolConfigStrictness = "off" | "warn" | "error";
|
|
5
5
|
export type ToolConfigDiagnosticSeverity = "warning" | "error";
|
|
6
6
|
export interface ToolConfigDiagnostic {
|
|
7
|
-
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unknownProviderKey" | "tools.removedProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
|
|
7
|
+
code: "tools.unknownToolId" | "tools.unsupportedLevel" | "tools.unplaceableActivation" | "tools.registryUnavailable" | "tools.unknownProviderKey" | "tools.removedProviderKey" | "tools.providerSanitizeFailed" | "tools.providerValidateFailed" | "tools.invalidProviderValidation";
|
|
8
8
|
severity: ToolConfigDiagnosticSeverity;
|
|
9
9
|
path: string;
|
|
10
10
|
message: string;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { normalizeToolsConfig, } from "./tools-config-normalizer.js";
|
|
2
2
|
import { frameworkErrorFromToolConfigDiagnostics, frameworkErrorFromUnknown, } from "./framework-error.js";
|
|
3
|
-
import { createPackagedToolRegistry } from "./createDefaultToolRegistry.js";
|
|
4
3
|
export function frameworkErrorFromToolConfigValidation(args) {
|
|
5
4
|
if (Array.isArray(args.diagnostics) && args.diagnostics.length > 0) {
|
|
6
5
|
return frameworkErrorFromToolConfigDiagnostics({
|
|
@@ -120,6 +119,20 @@ function collectPlacementDiagnostics(config, toolMap, diagnostics) {
|
|
|
120
119
|
}));
|
|
121
120
|
continue;
|
|
122
121
|
}
|
|
122
|
+
if (tool.activation === "region") {
|
|
123
|
+
// A region capability has no toolbar button, so placing it names a
|
|
124
|
+
// surface that will never render it. Reported at the placement rather
|
|
125
|
+
// than at render time, where the symptom is an absent accommodation and
|
|
126
|
+
// no error.
|
|
127
|
+
diagnostics.push(createDiagnostic({
|
|
128
|
+
code: "tools.unplaceableActivation",
|
|
129
|
+
severity: "error",
|
|
130
|
+
path: `placement.${level}`,
|
|
131
|
+
message: `Tool "${toolId}" renders into a host surface (activation "region") and cannot be placed on the ${level} toolbar. Remove it from placement.${level}; its availability comes from policy plus its content dependency.`,
|
|
132
|
+
toolId,
|
|
133
|
+
}));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
123
136
|
const supportsLevel = tool.supportedLevels.includes(level);
|
|
124
137
|
// Leniency policy: section-capable tools are tolerated in item placement.
|
|
125
138
|
// This avoids strict failures for common host config patterns while still
|
|
@@ -204,9 +217,27 @@ function throwValidationError(diagnostics, source) {
|
|
|
204
217
|
export function normalizeAndValidateToolsConfig(input, options = {}) {
|
|
205
218
|
const strictness = normalizeToolConfigStrictness(options.strictness);
|
|
206
219
|
const source = options.source ?? "tools";
|
|
207
|
-
const registryTools = getRegistryToolMap(options.toolRegistry
|
|
220
|
+
const registryTools = getRegistryToolMap(options.toolRegistry);
|
|
208
221
|
const normalized = normalizeToolsConfig(input);
|
|
209
222
|
const diagnostics = [];
|
|
223
|
+
// Tool-id and provider checks need a registry to check against, and every
|
|
224
|
+
// collector below returns early without one. Say so: this package no longer
|
|
225
|
+
// falls back to a packaged registry, so a caller that used to get id
|
|
226
|
+
// validation for free now gets none, and a silent downgrade from "your ids are
|
|
227
|
+
// valid" to "nobody looked" is the kind of change that surfaces as a typo
|
|
228
|
+
// reaching a learner.
|
|
229
|
+
const hasConfiguredTools = Object.values(normalized.placement).some((ids) => ids.length > 0) ||
|
|
230
|
+
Object.keys(normalized.providers).length > 0 ||
|
|
231
|
+
(normalized.policy.allowed?.length ?? 0) > 0 ||
|
|
232
|
+
(normalized.policy.blocked?.length ?? 0) > 0;
|
|
233
|
+
if (registryTools.size === 0 && hasConfiguredTools) {
|
|
234
|
+
diagnostics.push(createDiagnostic({
|
|
235
|
+
code: "tools.registryUnavailable",
|
|
236
|
+
severity: "warning",
|
|
237
|
+
path: "tools",
|
|
238
|
+
message: 'No tool registry was supplied, so tool ids, placement levels and provider config were not validated. Pass `toolRegistry` — for the packaged capability set, `createPackagedToolRegistry()` from "@pie-players/pie-default-tool-loaders".',
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
210
241
|
const hasRemovedTtsKey = Object.hasOwn(normalized.providers, "tts");
|
|
211
242
|
const nextProviders = {
|
|
212
243
|
...(normalized.providers || {}),
|
|
@@ -233,8 +264,17 @@ export function normalizeAndValidateToolsConfig(input, options = {}) {
|
|
|
233
264
|
if (strictness === "warn") {
|
|
234
265
|
emitWarnings(diagnostics, source);
|
|
235
266
|
}
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
// Severity decides what `strictness: "error"` rejects. Every diagnostic this
|
|
268
|
+
// function raised was `"error"` until `tools.registryUnavailable`, which
|
|
269
|
+
// reports that validation could not run rather than that the config is wrong —
|
|
270
|
+
// throwing on it would turn "no registry supplied" from a host's existing,
|
|
271
|
+
// working setup into a construction failure.
|
|
272
|
+
const blocking = diagnostics.filter((entry) => entry.severity === "error");
|
|
273
|
+
if (strictness === "error" && blocking.length > 0) {
|
|
274
|
+
throwValidationError(blocking, source);
|
|
275
|
+
}
|
|
276
|
+
if (strictness === "error" && blocking.length === 0) {
|
|
277
|
+
emitWarnings(diagnostics, source);
|
|
238
278
|
}
|
|
239
279
|
return {
|
|
240
280
|
config: nextConfig,
|
|
@@ -26,7 +26,8 @@ const findBrowserVoice = (voices, preferredName) => {
|
|
|
26
26
|
const languagePrefix = language.split("-")[0] || "en";
|
|
27
27
|
const matchesLanguage = (voice) => {
|
|
28
28
|
const voiceLanguage = normalizeLanguageCode(voice.lang);
|
|
29
|
-
return voiceLanguage === language ||
|
|
29
|
+
return (voiceLanguage === language ||
|
|
30
|
+
voiceLanguage.startsWith(`${languagePrefix}-`));
|
|
30
31
|
};
|
|
31
32
|
const ranked = [
|
|
32
33
|
(voice) => voice.localService && matchesLanguage(voice),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { canonicalizeMathML } from "./mathml-sanitization.js";
|
|
2
|
-
import { collectVisibleTextAndMap,
|
|
2
|
+
import { collectVisibleTextAndMap, isNodeExcludedFromSpeech, normalizeTextForSpeech, shouldInsertWordBoundarySpace, } from "./text-processing.js";
|
|
3
3
|
const createAccumulator = (options) => ({
|
|
4
4
|
chars: [],
|
|
5
5
|
map: new Map(),
|
|
@@ -226,7 +226,7 @@ const textFallbackFromMathML = (mathml) => {
|
|
|
226
226
|
const collectVisibleMathFallback = (element, canonicalMathML, options) => {
|
|
227
227
|
const mathAcc = createAccumulator(options);
|
|
228
228
|
const visit = (node) => {
|
|
229
|
-
if (
|
|
229
|
+
if (isNodeExcludedFromSpeech(node, element))
|
|
230
230
|
return;
|
|
231
231
|
if (node.nodeType === 3) {
|
|
232
232
|
appendTextNode(mathAcc, node);
|
|
@@ -284,7 +284,7 @@ const collectMathAware = (root, options) => {
|
|
|
284
284
|
textChunkSourceElement = undefined;
|
|
285
285
|
};
|
|
286
286
|
const processNode = (node) => {
|
|
287
|
-
if (
|
|
287
|
+
if (isNodeExcludedFromSpeech(node, root))
|
|
288
288
|
return;
|
|
289
289
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
290
290
|
const sourceElement = resolveTextChunkSourceElement(node, root);
|
|
@@ -11,6 +11,57 @@ export declare const normalizeTextForSpeech: (text: string) => string;
|
|
|
11
11
|
export declare const createSpeechAlignmentTokenPattern: () => RegExp;
|
|
12
12
|
export declare const isElementHiddenForTTS: (element: Element) => boolean;
|
|
13
13
|
export declare const isNodeHiddenForTTS: (node: Node, root?: Element | null) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Marks content that must be shown but never spoken — items where reading *is*
|
|
16
|
+
* the construct, such as decoding and spelling, where speaking the node hands
|
|
17
|
+
* over the answer.
|
|
18
|
+
*
|
|
19
|
+
* Not a PNP field: `prohibitedSupports` is the learner declining a support, while
|
|
20
|
+
* this is the item saying "not here, for anyone", so it overrides an entitlement
|
|
21
|
+
* rather than yielding to it.
|
|
22
|
+
*
|
|
23
|
+
* Shape follows QTI 3's `data-qti-suppress-tts` — an attribute on the content
|
|
24
|
+
* element, single-valued, vocabulary below. Element placement is what makes it
|
|
25
|
+
* work on undocked nodes and enforceable in the selection read-aloud path, which
|
|
26
|
+
* consults no catalog. The name follows PIE's `data-tts-*` family, and PIE reads
|
|
27
|
+
* only this spelling; importers map QTI's.
|
|
28
|
+
*/
|
|
29
|
+
export declare const TTS_SUPPRESS_ATTRIBUTE = "data-tts-suppress";
|
|
30
|
+
/**
|
|
31
|
+
* Whether this element forbids machine read-aloud of itself and its subtree.
|
|
32
|
+
*
|
|
33
|
+
* Unrecognized and empty values suppress rather than pass through, and say so once
|
|
34
|
+
* per distinct value: a typo that fell through would speak a word the item was
|
|
35
|
+
* measuring, invalidating the score with no visible symptom, whereas
|
|
36
|
+
* over-suppressing only withholds speech an author had already marked as withheld.
|
|
37
|
+
*/
|
|
38
|
+
export declare const isElementSuppressedForTTS: (element: Element) => boolean;
|
|
39
|
+
export declare const isNodeSuppressedForTTS: (node: Node, root?: Element | null) => boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The predicate every speech-producing path filters on: hidden *or* suppressed.
|
|
42
|
+
*
|
|
43
|
+
* Kept distinct from `isNodeHiddenForTTS`, which stays a question about
|
|
44
|
+
* visibility — suppressed content is visible on purpose, and the highlight
|
|
45
|
+
* geometry resolvers that ask "can the candidate see this" must keep getting
|
|
46
|
+
* the visibility answer rather than this one.
|
|
47
|
+
*/
|
|
48
|
+
export declare const isNodeExcludedFromSpeech: (node: Node, root?: Element | null) => boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Text of a range with the parts that must not be spoken removed.
|
|
51
|
+
*
|
|
52
|
+
* `Range.toString()` is not usable for speech: it is pure character extraction
|
|
53
|
+
* and honours no DOM filter at all, so it happily returns suppressed — and
|
|
54
|
+
* hidden — text. The selection read-aloud path is a text-in path rather than a
|
|
55
|
+
* DOM walk, which makes this the only place its content can be filtered.
|
|
56
|
+
*
|
|
57
|
+
* `filtered` reports whether anything was dropped, so a caller can tell "the
|
|
58
|
+
* candidate selected nothing speakable" apart from "the candidate selected
|
|
59
|
+
* nothing".
|
|
60
|
+
*/
|
|
61
|
+
export declare const collectRangeTextForSpeech: (range: Range, root: Element) => {
|
|
62
|
+
text: string;
|
|
63
|
+
filtered: boolean;
|
|
64
|
+
};
|
|
14
65
|
export declare const shouldInsertWordBoundarySpace: (previousChar: string | null, nextChar: string | null, options?: TextProcessingOptions) => boolean;
|
|
15
66
|
export declare const collectVisibleTextAndMap: (element: Element, options?: TextProcessingOptions) => {
|
|
16
67
|
text: string;
|
|
@@ -62,6 +62,122 @@ export const isNodeHiddenForTTS = (node, root) => {
|
|
|
62
62
|
}
|
|
63
63
|
return false;
|
|
64
64
|
};
|
|
65
|
+
/**
|
|
66
|
+
* Marks content that must be shown but never spoken — items where reading *is*
|
|
67
|
+
* the construct, such as decoding and spelling, where speaking the node hands
|
|
68
|
+
* over the answer.
|
|
69
|
+
*
|
|
70
|
+
* Not a PNP field: `prohibitedSupports` is the learner declining a support, while
|
|
71
|
+
* this is the item saying "not here, for anyone", so it overrides an entitlement
|
|
72
|
+
* rather than yielding to it.
|
|
73
|
+
*
|
|
74
|
+
* Shape follows QTI 3's `data-qti-suppress-tts` — an attribute on the content
|
|
75
|
+
* element, single-valued, vocabulary below. Element placement is what makes it
|
|
76
|
+
* work on undocked nodes and enforceable in the selection read-aloud path, which
|
|
77
|
+
* consults no catalog. The name follows PIE's `data-tts-*` family, and PIE reads
|
|
78
|
+
* only this spelling; importers map QTI's.
|
|
79
|
+
*/
|
|
80
|
+
export const TTS_SUPPRESS_ATTRIBUTE = "data-tts-suppress";
|
|
81
|
+
const SUPPRESSES_COMPUTER_READ_ALOUD = new Set(["computer-read-aloud", "all"]);
|
|
82
|
+
// `screen-reader` is in the vocabulary but is not ours: it asks the delivery
|
|
83
|
+
// engine to hide the node from assistive technology, which is the host's job
|
|
84
|
+
// (and is what `aria-hidden` above already covers on the way in). A node marked
|
|
85
|
+
// only `screen-reader` is still legitimately machine-read aloud.
|
|
86
|
+
const SUPPRESS_VALUES = new Set([
|
|
87
|
+
...SUPPRESSES_COMPUTER_READ_ALOUD,
|
|
88
|
+
"screen-reader",
|
|
89
|
+
]);
|
|
90
|
+
const warnedSuppressValues = new Set();
|
|
91
|
+
/**
|
|
92
|
+
* Whether this element forbids machine read-aloud of itself and its subtree.
|
|
93
|
+
*
|
|
94
|
+
* Unrecognized and empty values suppress rather than pass through, and say so once
|
|
95
|
+
* per distinct value: a typo that fell through would speak a word the item was
|
|
96
|
+
* measuring, invalidating the score with no visible symptom, whereas
|
|
97
|
+
* over-suppressing only withholds speech an author had already marked as withheld.
|
|
98
|
+
*/
|
|
99
|
+
export const isElementSuppressedForTTS = (element) => {
|
|
100
|
+
const raw = element.getAttribute?.(TTS_SUPPRESS_ATTRIBUTE);
|
|
101
|
+
if (raw === null || raw === undefined)
|
|
102
|
+
return false;
|
|
103
|
+
const value = raw.trim().toLowerCase();
|
|
104
|
+
if (SUPPRESSES_COMPUTER_READ_ALOUD.has(value))
|
|
105
|
+
return true;
|
|
106
|
+
if (SUPPRESS_VALUES.has(value))
|
|
107
|
+
return false;
|
|
108
|
+
if (!warnedSuppressValues.has(value)) {
|
|
109
|
+
warnedSuppressValues.add(value);
|
|
110
|
+
console.warn(`[tts] ${TTS_SUPPRESS_ATTRIBUTE}="${raw}" is not one of ${Array.from(SUPPRESS_VALUES).join(", ")}; suppressing read-aloud for this content anyway, because a suppression attribute that fails open would leak the answer to items where reading is the construct. Correct the value to silence this.`);
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
export const isNodeSuppressedForTTS = (node, root) => {
|
|
115
|
+
let current = node.nodeType === 1
|
|
116
|
+
? node
|
|
117
|
+
: node.parentElement;
|
|
118
|
+
while (current) {
|
|
119
|
+
if (isElementSuppressedForTTS(current))
|
|
120
|
+
return true;
|
|
121
|
+
if (root && current === root)
|
|
122
|
+
break;
|
|
123
|
+
current = current.parentElement;
|
|
124
|
+
}
|
|
125
|
+
return false;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* The predicate every speech-producing path filters on: hidden *or* suppressed.
|
|
129
|
+
*
|
|
130
|
+
* Kept distinct from `isNodeHiddenForTTS`, which stays a question about
|
|
131
|
+
* visibility — suppressed content is visible on purpose, and the highlight
|
|
132
|
+
* geometry resolvers that ask "can the candidate see this" must keep getting
|
|
133
|
+
* the visibility answer rather than this one.
|
|
134
|
+
*/
|
|
135
|
+
export const isNodeExcludedFromSpeech = (node, root) => isNodeHiddenForTTS(node, root) || isNodeSuppressedForTTS(node);
|
|
136
|
+
/**
|
|
137
|
+
* Text of a range with the parts that must not be spoken removed.
|
|
138
|
+
*
|
|
139
|
+
* `Range.toString()` is not usable for speech: it is pure character extraction
|
|
140
|
+
* and honours no DOM filter at all, so it happily returns suppressed — and
|
|
141
|
+
* hidden — text. The selection read-aloud path is a text-in path rather than a
|
|
142
|
+
* DOM walk, which makes this the only place its content can be filtered.
|
|
143
|
+
*
|
|
144
|
+
* `filtered` reports whether anything was dropped, so a caller can tell "the
|
|
145
|
+
* candidate selected nothing speakable" apart from "the candidate selected
|
|
146
|
+
* nothing".
|
|
147
|
+
*/
|
|
148
|
+
export const collectRangeTextForSpeech = (range, root) => {
|
|
149
|
+
if (typeof document === "undefined" ||
|
|
150
|
+
typeof document.createTreeWalker !==
|
|
151
|
+
"function" ||
|
|
152
|
+
typeof NodeFilter === "undefined" ||
|
|
153
|
+
typeof range.intersectsNode !== "function") {
|
|
154
|
+
// Degraded, and deliberately not silent about the difference: callers still
|
|
155
|
+
// enforce whole-selection suppression from the range's common ancestor, so
|
|
156
|
+
// the construct guard holds even here. What is lost is per-node filtering
|
|
157
|
+
// of a selection that only partly overlaps suppressed content.
|
|
158
|
+
return { text: range.toString(), filtered: false };
|
|
159
|
+
}
|
|
160
|
+
const parts = [];
|
|
161
|
+
let filtered = false;
|
|
162
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
163
|
+
let current = walker.nextNode();
|
|
164
|
+
while (current) {
|
|
165
|
+
const textNode = current;
|
|
166
|
+
if (range.intersectsNode(textNode)) {
|
|
167
|
+
if (isNodeExcludedFromSpeech(textNode, root)) {
|
|
168
|
+
filtered = true;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const raw = textNode.textContent || "";
|
|
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));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
current = walker.nextNode();
|
|
178
|
+
}
|
|
179
|
+
return { text: parts.join(""), filtered };
|
|
180
|
+
};
|
|
65
181
|
export const shouldInsertWordBoundarySpace = (previousChar, nextChar, options) => {
|
|
66
182
|
if (!previousChar || !nextChar)
|
|
67
183
|
return false;
|
|
@@ -111,7 +227,7 @@ export const collectVisibleTextAndMap = (element, options) => {
|
|
|
111
227
|
while (current) {
|
|
112
228
|
const textNode = current;
|
|
113
229
|
const parent = textNode.parentElement;
|
|
114
|
-
if (parent && !
|
|
230
|
+
if (parent && !isNodeExcludedFromSpeech(textNode, element)) {
|
|
115
231
|
const raw = textNode.textContent || "";
|
|
116
232
|
const firstVisibleMatch = raw.match(/\S/);
|
|
117
233
|
const firstVisibleChar = firstVisibleMatch ? firstVisibleMatch[0] : null;
|
|
@@ -117,7 +117,11 @@ export const normalizeTTSSpeedControlOptions = (value) => {
|
|
|
117
117
|
if (rate === undefined || dedupedRates.has(rate))
|
|
118
118
|
continue;
|
|
119
119
|
dedupedRates.add(rate);
|
|
120
|
-
const defaultLabel = rate === 1
|
|
120
|
+
const defaultLabel = rate === 1
|
|
121
|
+
? "Normal"
|
|
122
|
+
: typeof entry === "number"
|
|
123
|
+
? formatSpeedLabel(rate)
|
|
124
|
+
: formatSpeedLabel(rate);
|
|
121
125
|
const label = typeof entry === "number"
|
|
122
126
|
? defaultLabel
|
|
123
127
|
: trimOptionalText(record.label) || defaultLabel;
|
|
@@ -131,7 +135,8 @@ export const normalizeTTSSpeedControlOptions = (value) => {
|
|
|
131
135
|
label,
|
|
132
136
|
ariaLabel,
|
|
133
137
|
isDefault: false,
|
|
134
|
-
requestedDefault: record.default === true ||
|
|
138
|
+
requestedDefault: record.default === true ||
|
|
139
|
+
record.isDefault === true,
|
|
135
140
|
});
|
|
136
141
|
}
|
|
137
142
|
if (!normalized.length) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-authoring surface.
|
|
3
|
+
*
|
|
4
|
+
* What a package outside this one needs to write a `ToolRegistration`: the
|
|
5
|
+
* registration types, the context predicates a tool answers `isVisibleInContext`
|
|
6
|
+
* with, scoped-id and element-creation helpers, the toolbar button/overlay
|
|
7
|
+
* helpers, and the provider descriptors for the two capabilities that ship a
|
|
8
|
+
* provider.
|
|
9
|
+
*
|
|
10
|
+
* A separate entry point rather than additions to `.` for the same reason
|
|
11
|
+
* `runtime/internal` and `policy/internal` exist: this is a surface for sibling
|
|
12
|
+
* packages in this repo, not a contract offered to hosts. Widening `.` with two
|
|
13
|
+
* dozen registration-authoring helpers would make every one of them something a
|
|
14
|
+
* host could reasonably expect us to keep.
|
|
15
|
+
*
|
|
16
|
+
* It exists because the packaged registrations moved out to the composition layer
|
|
17
|
+
* (`@pie-players/pie-default-tool-loaders`) so core stops naming capabilities.
|
|
18
|
+
* A host writing its own capability package imports from here too, which is the
|
|
19
|
+
* point — the mechanism is the same one our own registrations use.
|
|
20
|
+
*/
|
|
21
|
+
export type { HostedToolContext, HostedToolSize, ResolvedToolContext, ToolActivation, ToolContentDependency, ToolContentDependencyContext, ToolModuleLoader, ToolProviderDescriptor, ToolRegistration, ToolRenderElement, ToolSingletonScope, ToolSurfaceRenderContext, ToolSurfaceRenderResult, ToolSurfaceServices, ToolToolbarButtonDefinition, ToolToolbarRenderResult, ToolWindowShellConfig, ToolbarContext, } from "../services/ToolRegistry.js";
|
|
22
|
+
export { ToolRegistry } from "../services/ToolRegistry.js";
|
|
23
|
+
export type { ToolContext, ToolLevel } from "../services/tool-context.js";
|
|
24
|
+
export { hasChoiceInteraction, hasMathContent, hasReadableText, hasScienceContent, } from "../services/tool-context.js";
|
|
25
|
+
export { createScopedToolId } from "../services/tool-instance-id.js";
|
|
26
|
+
export type { ToolComponentFactory, ToolComponentFactoryMap, ToolComponentOverrides, ToolTagMap, } from "./tool-tag-map.js";
|
|
27
|
+
export { createToolElement, resolveToolTag, toToolIdFromTag, } from "./tool-tag-map.js";
|
|
28
|
+
export { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "./registrations/toolbar-registration-helpers.js";
|
|
29
|
+
export type { ElementToolStateStoreApi, ToolCoordinatorApi, ToolkitCoordinatorApi, TtsServiceApi, } from "../services/interfaces.js";
|
|
30
|
+
export type { ToolPlacementConfig, ToolProviderConfig, } from "../services/tools-config-normalizer.js";
|
|
31
|
+
export type { ToolConfigDiagnostic } from "../services/tool-config-validation.js";
|
|
32
|
+
export { DesmosToolProvider, TTSToolProvider, } from "../services/tool-providers/index.js";
|
|
33
|
+
export type { NormalizedTTSSpeedOption } from "../services/tts-runtime-config.js";
|
|
34
|
+
export { buildRuntimeTTSConfig, normalizeTTSLayoutMode, normalizeTTSSpeedControlOptions, resolveRuntimeProvider, resolveTTSBackend, resolveTTSHostToolbarLayout, resolveTTSLayoutMode, resolveTTSRuntimeSettings, resolveTransportMode, } from "../services/tts-runtime-config.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registration-authoring surface.
|
|
3
|
+
*
|
|
4
|
+
* What a package outside this one needs to write a `ToolRegistration`: the
|
|
5
|
+
* registration types, the context predicates a tool answers `isVisibleInContext`
|
|
6
|
+
* with, scoped-id and element-creation helpers, the toolbar button/overlay
|
|
7
|
+
* helpers, and the provider descriptors for the two capabilities that ship a
|
|
8
|
+
* provider.
|
|
9
|
+
*
|
|
10
|
+
* A separate entry point rather than additions to `.` for the same reason
|
|
11
|
+
* `runtime/internal` and `policy/internal` exist: this is a surface for sibling
|
|
12
|
+
* packages in this repo, not a contract offered to hosts. Widening `.` with two
|
|
13
|
+
* dozen registration-authoring helpers would make every one of them something a
|
|
14
|
+
* host could reasonably expect us to keep.
|
|
15
|
+
*
|
|
16
|
+
* It exists because the packaged registrations moved out to the composition layer
|
|
17
|
+
* (`@pie-players/pie-default-tool-loaders`) so core stops naming capabilities.
|
|
18
|
+
* A host writing its own capability package imports from here too, which is the
|
|
19
|
+
* point — the mechanism is the same one our own registrations use.
|
|
20
|
+
*/
|
|
21
|
+
export { ToolRegistry } from "../services/ToolRegistry.js";
|
|
22
|
+
export { hasChoiceInteraction, hasMathContent, hasReadableText, hasScienceContent, } from "../services/tool-context.js";
|
|
23
|
+
// Scoped tool instance ids, so two placements of one tool do not share state.
|
|
24
|
+
export { createScopedToolId } from "../services/tool-instance-id.js";
|
|
25
|
+
export { createToolElement, resolveToolTag, toToolIdFromTag, } from "./tool-tag-map.js";
|
|
26
|
+
// Toolbar button/overlay wiring shared by every toolbar-toggle registration.
|
|
27
|
+
export { applyOverlaySurface, createScopedVisibilityBinding, syncButtonAndOverlayVisibility, } from "./registrations/toolbar-registration-helpers.js";
|
|
28
|
+
// Provider descriptors for the two packaged capabilities that ship one. These
|
|
29
|
+
// stay here rather than moving with the registrations because they are written
|
|
30
|
+
// against the `pie-calculator` and `pie-tts` contract packages, which the
|
|
31
|
+
// toolkit's own `TTSService` and provider registry also depend on.
|
|
32
|
+
export { DesmosToolProvider, TTSToolProvider, } from "../services/tool-providers/index.js";
|
|
33
|
+
export { buildRuntimeTTSConfig, normalizeTTSLayoutMode, normalizeTTSSpeedControlOptions, resolveRuntimeProvider, resolveTTSBackend, resolveTTSHostToolbarLayout, resolveTTSLayoutMode, resolveTTSRuntimeSettings, resolveTransportMode, } from "../services/tts-runtime-config.js";
|
|
@@ -14,10 +14,22 @@ export interface ToolComponentOverrides {
|
|
|
14
14
|
toolComponentFactories?: Partial<ToolComponentFactoryMap>;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* Resolve the element tag for a tool from the overrides in play.
|
|
18
|
+
*
|
|
19
|
+
* There is no built-in map to fall back to. One lived here, naming eleven
|
|
20
|
+
* capabilities, and a core module holding that catalogue is why a host could not
|
|
21
|
+
* add a twelfth without editing this package — the map was overridable, but the
|
|
22
|
+
* default was core-resident. The packaged map is `PACKAGED_TOOL_TAG_MAP` in
|
|
23
|
+
* `@pie-players/pie-default-tool-loaders`, installed onto a registry through
|
|
24
|
+
* `ToolRegistry.setComponentOverrides`.
|
|
25
|
+
*
|
|
26
|
+
* An unmapped toolId falls through to itself, which is only a valid tag if the
|
|
27
|
+
* host's tool id already looks like one. Every packaged id is a single camelCase
|
|
28
|
+
* word, so in practice an unmapped packaged tool throws here rather than
|
|
29
|
+
* rendering a bogus element — the failure names the missing mapping, because
|
|
30
|
+
* "custom element names must include a hyphen" is a true statement about the
|
|
31
|
+
* wrong thing.
|
|
19
32
|
*/
|
|
20
|
-
export declare const DEFAULT_TOOL_TAG_MAP: ToolTagMap;
|
|
21
33
|
export declare const resolveToolTag: (toolId: string, overrides?: ToolComponentOverrides) => string;
|
|
22
34
|
export declare const createToolElement: (toolId: string, context: ToolContext, toolbarContext: ToolbarContext, overrides?: ToolComponentOverrides) => HTMLElement;
|
|
23
35
|
export declare const toToolIdFromTag: (tagName: string, overrides?: ToolComponentOverrides) => string | undefined;
|
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
import { validateCustomElementTag } from "@pie-players/pie-players-shared/pie/tag-names";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Resolve the element tag for a tool from the overrides in play.
|
|
4
|
+
*
|
|
5
|
+
* There is no built-in map to fall back to. One lived here, naming eleven
|
|
6
|
+
* capabilities, and a core module holding that catalogue is why a host could not
|
|
7
|
+
* add a twelfth without editing this package — the map was overridable, but the
|
|
8
|
+
* default was core-resident. The packaged map is `PACKAGED_TOOL_TAG_MAP` in
|
|
9
|
+
* `@pie-players/pie-default-tool-loaders`, installed onto a registry through
|
|
10
|
+
* `ToolRegistry.setComponentOverrides`.
|
|
11
|
+
*
|
|
12
|
+
* An unmapped toolId falls through to itself, which is only a valid tag if the
|
|
13
|
+
* host's tool id already looks like one. Every packaged id is a single camelCase
|
|
14
|
+
* word, so in practice an unmapped packaged tool throws here rather than
|
|
15
|
+
* rendering a bogus element — the failure names the missing mapping, because
|
|
16
|
+
* "custom element names must include a hyphen" is a true statement about the
|
|
17
|
+
* wrong thing.
|
|
5
18
|
*/
|
|
6
|
-
export const DEFAULT_TOOL_TAG_MAP = {
|
|
7
|
-
calculator: "pie-tool-calculator",
|
|
8
|
-
textToSpeech: "pie-tool-text-to-speech",
|
|
9
|
-
ruler: "pie-tool-ruler",
|
|
10
|
-
protractor: "pie-tool-protractor",
|
|
11
|
-
answerEliminator: "pie-tool-answer-eliminator",
|
|
12
|
-
highlighter: "pie-tool-annotation-toolbar",
|
|
13
|
-
lineReader: "pie-tool-line-reader",
|
|
14
|
-
theme: "pie-tool-theme",
|
|
15
|
-
annotationToolbar: "pie-tool-annotation-toolbar",
|
|
16
|
-
graph: "pie-tool-graph",
|
|
17
|
-
periodicTable: "pie-tool-periodic-table",
|
|
18
|
-
};
|
|
19
19
|
export const resolveToolTag = (toolId, overrides) => {
|
|
20
|
-
const mapped = overrides?.toolTagMap?.[toolId]
|
|
21
|
-
|
|
20
|
+
const mapped = overrides?.toolTagMap?.[toolId];
|
|
21
|
+
if (mapped === undefined && !toolId.includes("-")) {
|
|
22
|
+
throw new Error(`No element tag is mapped for tool "${toolId}". Install a tag map on the registry via setComponentOverrides({ toolTagMap }) — for the packaged capabilities, PACKAGED_TOOL_TAG_MAP from "@pie-players/pie-default-tool-loaders".`);
|
|
23
|
+
}
|
|
24
|
+
return validateCustomElementTag(mapped ?? toolId, `tool component tag for "${toolId}"`);
|
|
22
25
|
};
|
|
23
26
|
const createDefaultToolElement = (tagName) => document.createElement(tagName);
|
|
24
27
|
export const createToolElement = (toolId, context, toolbarContext, overrides) => {
|
|
@@ -31,6 +34,6 @@ export const createToolElement = (toolId, context, toolbarContext, overrides) =>
|
|
|
31
34
|
};
|
|
32
35
|
export const toToolIdFromTag = (tagName, overrides) => {
|
|
33
36
|
const validTag = validateCustomElementTag(tagName, "tool component tag");
|
|
34
|
-
const map =
|
|
37
|
+
const map = overrides?.toolTagMap ?? {};
|
|
35
38
|
return Object.entries(map).find(([, tag]) => tag === validTag)?.[0];
|
|
36
39
|
};
|