@pie-players/pie-assessment-toolkit 1.0.0-beta.6 → 1.0.0-beta.7
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/dist/attempt/TestSession.d.ts +7 -0
- package/dist/attempt/TestSession.js +3 -0
- package/dist/components/PieAssessmentToolkit.custom-element.js +7 -7
- package/dist/context/assessment-toolkit-context.d.ts +8 -0
- package/dist/runtime/content-language.d.ts +13 -2
- package/dist/runtime/content-language.js +31 -5
- package/dist/services/AccessibilityCatalogResolver.js +9 -5
- package/dist/services/TTSService.d.ts +4 -3
- package/dist/services/TTSService.js +8 -5
- package/dist/services/tts/math-control-names.d.ts +3 -1
- package/dist/services/tts/math-control-names.js +42 -2
- package/package.json +8 -8
|
@@ -73,6 +73,14 @@ export interface AssessmentToolkitRuntimeContext {
|
|
|
73
73
|
* an interface in another.
|
|
74
74
|
*/
|
|
75
75
|
contentLanguage?: string;
|
|
76
|
+
/**
|
|
77
|
+
* The content language the item and passage players stamp on content that
|
|
78
|
+
* declares none: {@link contentLanguage} when the host names one, else
|
|
79
|
+
* `auto` (`lang=""` with `dir="auto"`) when the host sets an interface
|
|
80
|
+
* locale, else unset, which leaves undeclared content inheriting the page's
|
|
81
|
+
* language and direction. See `defaultContentLanguage`.
|
|
82
|
+
*/
|
|
83
|
+
contentLanguageDefault?: string;
|
|
76
84
|
/**
|
|
77
85
|
* Reports a failure that keeps the section's content from loading as the
|
|
78
86
|
* section controller's `section-error`, with source `section-runtime`.
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
/** The content language when neither the markup nor the host names one. */
|
|
2
2
|
export declare const DEFAULT_CONTENT_LANGUAGE = "en-US";
|
|
3
3
|
/**
|
|
4
|
-
* The nearest
|
|
5
|
-
*
|
|
4
|
+
* The nearest `lang` attribute at or above `node`, climbing through shadow
|
|
5
|
+
* hosts, and ending after `boundary` when one is given. An empty `lang` names
|
|
6
|
+
* the language unknown and ends the climb with no language, as it ends the
|
|
7
|
+
* browser's own inheritance: the players stamp it on content that declares
|
|
8
|
+
* none, so the interface's `lang` above never reads as the content's.
|
|
6
9
|
*/
|
|
7
10
|
export declare const findLangAttribute: (node: Node | null | undefined, boundary?: Element | null) => string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The language the content `root` holds is declared in: the `lang` its
|
|
13
|
+
* outermost `lang`-carrying descendants share, or undefined when they disagree,
|
|
14
|
+
* one is empty, or there are none. The players stamp `lang` on the content root
|
|
15
|
+
* they render, below the shell a whole-item read targets, so this is how such
|
|
16
|
+
* a read finds the language an item declared through `env.locale`.
|
|
17
|
+
*/
|
|
18
|
+
export declare const findHeldContentLanguage: (root: Element) => string | undefined;
|
|
8
19
|
export interface ContentLanguageOptions {
|
|
9
20
|
/**
|
|
10
21
|
* Where the `lang` climb ends, inclusive. Defaults to the nearest shell scope
|
|
@@ -3,8 +3,11 @@ import { findShellScopeHost } from "./content-region.js";
|
|
|
3
3
|
/** The content language when neither the markup nor the host names one. */
|
|
4
4
|
export const DEFAULT_CONTENT_LANGUAGE = "en-US";
|
|
5
5
|
/**
|
|
6
|
-
* The nearest
|
|
7
|
-
*
|
|
6
|
+
* The nearest `lang` attribute at or above `node`, climbing through shadow
|
|
7
|
+
* hosts, and ending after `boundary` when one is given. An empty `lang` names
|
|
8
|
+
* the language unknown and ends the climb with no language, as it ends the
|
|
9
|
+
* browser's own inheritance: the players stamp it on content that declares
|
|
10
|
+
* none, so the interface's `lang` above never reads as the content's.
|
|
8
11
|
*/
|
|
9
12
|
export const findLangAttribute = (node, boundary) => {
|
|
10
13
|
let current = !node
|
|
@@ -13,15 +16,38 @@ export const findLangAttribute = (node, boundary) => {
|
|
|
13
16
|
? node
|
|
14
17
|
: composedParentElement(node);
|
|
15
18
|
while (current) {
|
|
16
|
-
const lang = current.getAttribute("lang")
|
|
17
|
-
if (lang)
|
|
18
|
-
return lang;
|
|
19
|
+
const lang = current.getAttribute("lang");
|
|
20
|
+
if (lang !== null)
|
|
21
|
+
return lang.trim() || undefined;
|
|
19
22
|
if (boundary && current === boundary)
|
|
20
23
|
return undefined;
|
|
21
24
|
current = composedParentElement(current);
|
|
22
25
|
}
|
|
23
26
|
return undefined;
|
|
24
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* The language the content `root` holds is declared in: the `lang` its
|
|
30
|
+
* outermost `lang`-carrying descendants share, or undefined when they disagree,
|
|
31
|
+
* one is empty, or there are none. The players stamp `lang` on the content root
|
|
32
|
+
* they render, below the shell a whole-item read targets, so this is how such
|
|
33
|
+
* a read finds the language an item declared through `env.locale`.
|
|
34
|
+
*/
|
|
35
|
+
export const findHeldContentLanguage = (root) => {
|
|
36
|
+
let language;
|
|
37
|
+
for (const element of root.querySelectorAll("[lang]")) {
|
|
38
|
+
const outer = element.parentElement?.closest("[lang]");
|
|
39
|
+
if (outer && outer !== root && root.contains(outer))
|
|
40
|
+
continue;
|
|
41
|
+
const lang = element.getAttribute("lang")?.trim();
|
|
42
|
+
if (!lang)
|
|
43
|
+
return undefined;
|
|
44
|
+
if (language === undefined)
|
|
45
|
+
language = lang;
|
|
46
|
+
else if (language.toLowerCase() !== lang.toLowerCase())
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return language;
|
|
50
|
+
};
|
|
25
51
|
/**
|
|
26
52
|
* The language the content at `target` is named as being in: the nearest `lang`
|
|
27
53
|
* between it and its shell scope host, else the host's `content-language` input.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { languageTagScriptLookupSequence, languageTagScriptsMatch, normalizeLanguageTag, } from "@pie-players/pie-players-shared/i18n/language-tags";
|
|
2
2
|
import { catalogOwnerContextFor, collectOwnerCatalogRegistrations, } from "./catalog-owner.js";
|
|
3
3
|
import { sanitizeSsmlString } from "./SSMLExtractor.js";
|
|
4
4
|
/**
|
|
@@ -538,14 +538,18 @@ export class AccessibilityCatalogResolver {
|
|
|
538
538
|
// Language rungs, most specific first: the requested language, then the
|
|
539
539
|
// default language, then any.
|
|
540
540
|
//
|
|
541
|
-
// Each requested tag expands into its
|
|
541
|
+
// Each requested tag expands into its script-aware lookup sequence over
|
|
542
542
|
// normalized tags, so `es-MX` tries `es-mx` and then `es` before falling
|
|
543
543
|
// through to the default, and a POSIX `es_ES` card (what the Learnosity
|
|
544
|
-
// transform emits) matches an `es-ES` request on its own rung.
|
|
544
|
+
// transform emits) matches an `es-ES` request on its own rung. A shorter
|
|
545
|
+
// card serves only in the request's script: `zh-TW` tries `zh-hant`, and
|
|
546
|
+
// passes a bare `zh` card by, which is Simplified.
|
|
545
547
|
const languageRungs = [];
|
|
546
548
|
const pushLookupRungs = (tag) => {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
+
const exact = normalizeLanguageTag(tag);
|
|
550
|
+
for (const step of languageTagScriptLookupSequence(tag)) {
|
|
551
|
+
languageRungs.push((card) => normalizeLanguageTag(card.language) === step &&
|
|
552
|
+
(step === exact || languageTagScriptsMatch(tag, card.language)));
|
|
549
553
|
}
|
|
550
554
|
};
|
|
551
555
|
if (language) {
|
|
@@ -199,9 +199,10 @@ export declare class TTSService {
|
|
|
199
199
|
private getTextProcessingOptions;
|
|
200
200
|
/**
|
|
201
201
|
* The language a read of `node` is in. Its content language is the nearest
|
|
202
|
-
* `lang` between it and its shell, else
|
|
203
|
-
*
|
|
204
|
-
*
|
|
202
|
+
* `lang` between it and its shell, else for an element the `lang` the content
|
|
203
|
+
* roots it holds share, else the language the speak names, else a pinned
|
|
204
|
+
* `lang_id`. Its locale is that content language, else the host's configured
|
|
205
|
+
* locale, else en-US.
|
|
205
206
|
*/
|
|
206
207
|
private resolveReadLanguage;
|
|
207
208
|
/**
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
import { applyMediaFragment, enforceMediaFragment, } from "@pie-players/pie-players-shared/media";
|
|
21
21
|
import { createPieLogger, isTtsDebugEnabled, } from "@pie-players/pie-players-shared/pie";
|
|
22
22
|
import { resolveSpokenAudioMedia, } from "./spoken-audio-cards.js";
|
|
23
|
-
import { DEFAULT_CONTENT_LANGUAGE, findContentLanguage, findLangAttribute, } from "../runtime/content-language.js";
|
|
23
|
+
import { DEFAULT_CONTENT_LANGUAGE, findContentLanguage, findHeldContentLanguage, findLangAttribute, } from "../runtime/content-language.js";
|
|
24
24
|
import { isSsmlDocument } from "./tts/ssml/spoken-text.js";
|
|
25
25
|
import { composedContains, composedParentElement, flatQuerySelectorAll, flatTextContent, flatTreeChildNodes, flatTreeParentElement, flatTreeTextNodes, rangeHoldsTextPosition, rangeIntersectsComposedNode, } from "./tts/flat-tree.js";
|
|
26
26
|
import { collectRangeTextForSpeech, collectVisibleTextAndMap, isElementHiddenForTTS, isNodeExcludedFromSpeech, isNodeSuppressedForTTS, normalizeTextForSpeech, rangeHoldsSpeakableElement, textInRange, } from "./tts/text-processing.js";
|
|
@@ -688,12 +688,15 @@ export class TTSService {
|
|
|
688
688
|
}
|
|
689
689
|
/**
|
|
690
690
|
* The language a read of `node` is in. Its content language is the nearest
|
|
691
|
-
* `lang` between it and its shell, else
|
|
692
|
-
*
|
|
693
|
-
*
|
|
691
|
+
* `lang` between it and its shell, else for an element the `lang` the content
|
|
692
|
+
* roots it holds share, else the language the speak names, else a pinned
|
|
693
|
+
* `lang_id`. Its locale is that content language, else the host's configured
|
|
694
|
+
* locale, else en-US.
|
|
694
695
|
*/
|
|
695
696
|
resolveReadLanguage(node, named) {
|
|
696
|
-
const contentLanguage = findContentLanguage(node
|
|
697
|
+
const contentLanguage = findContentLanguage(node) ??
|
|
698
|
+
(node.nodeType === 1 ? findHeldContentLanguage(node) : undefined) ??
|
|
699
|
+
(named?.trim() || undefined) ??
|
|
697
700
|
optionalString(this.providerOptions().lang_id);
|
|
698
701
|
return {
|
|
699
702
|
contentLanguage,
|
|
@@ -16,6 +16,8 @@ export interface MathControlNamesOptions {
|
|
|
16
16
|
* that speech in the name of the control around it, among the control's other
|
|
17
17
|
* text. English reads ClearSpeak, other languages MathSpeak, in the language of
|
|
18
18
|
* the content. The label replaces the elements' own; it stays when SRE cannot
|
|
19
|
-
* speak the math.
|
|
19
|
+
* speak the math. Math in a control named by `aria-labelledby` from elements
|
|
20
|
+
* that do not hold it gets no label, as the elements' renderer gives it none.
|
|
21
|
+
* Returns the function that stops observing.
|
|
20
22
|
*/
|
|
21
23
|
export declare function observeMathControlNames(root: Element, options?: MathControlNamesOptions): () => void;
|
|
@@ -44,6 +44,41 @@ const fractionStyle = (math) => Array.from(math.getElementsByTagName("mfrac")).e
|
|
|
44
44
|
? "Fraction_Over"
|
|
45
45
|
: "Fraction_General";
|
|
46
46
|
const isEnglish = (language) => !language || language.toLowerCase().split("-")[0] === "en";
|
|
47
|
+
/** The element a control names: the control a `label` labels, or the control itself. */
|
|
48
|
+
const namedElement = (control) => {
|
|
49
|
+
if (control.localName !== "label")
|
|
50
|
+
return control;
|
|
51
|
+
const label = control;
|
|
52
|
+
if (label.control)
|
|
53
|
+
return label.control;
|
|
54
|
+
const forId = label.getAttribute("for");
|
|
55
|
+
const root = label.getRootNode();
|
|
56
|
+
return ((forId ? root.getElementById?.(forId) : null) ??
|
|
57
|
+
label.querySelector("input, select, textarea, button"));
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Whether the control's name leaves `container` out: the control is named by
|
|
61
|
+
* `aria-labelledby` from elements that do not hold the math. multiple-choice
|
|
62
|
+
* names a choice holding math that way, from its key and a note ("A. Pick the
|
|
63
|
+
* answer below"), so the screen reader reads the MathML in the content, where
|
|
64
|
+
* the student can explore it and a braille display shows it as math. A label
|
|
65
|
+
* there would be read in browse mode instead. Ids that resolve to nothing say
|
|
66
|
+
* nothing about the name, so they leave the math labelled.
|
|
67
|
+
*/
|
|
68
|
+
const isNamedFromElsewhere = (control, container) => {
|
|
69
|
+
const named = namedElement(control);
|
|
70
|
+
const ids = named
|
|
71
|
+
?.getAttribute("aria-labelledby")
|
|
72
|
+
?.split(/\s+/)
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
if (!named || !ids?.length)
|
|
75
|
+
return false;
|
|
76
|
+
const root = named.getRootNode();
|
|
77
|
+
const labels = ids
|
|
78
|
+
.map((id) => root.getElementById?.(id) ?? null)
|
|
79
|
+
.filter((element) => element !== null);
|
|
80
|
+
return (labels.length > 0 && !labels.some((element) => element.contains(container)));
|
|
81
|
+
};
|
|
47
82
|
/**
|
|
48
83
|
* Labels each typeset expression inside a control under `root`, open shadow
|
|
49
84
|
* roots included, with SRE's speech, as MathJax adds it and again whenever
|
|
@@ -51,7 +86,9 @@ const isEnglish = (language) => !language || language.toLowerCase().split("-")[0
|
|
|
51
86
|
* that speech in the name of the control around it, among the control's other
|
|
52
87
|
* text. English reads ClearSpeak, other languages MathSpeak, in the language of
|
|
53
88
|
* the content. The label replaces the elements' own; it stays when SRE cannot
|
|
54
|
-
* speak the math.
|
|
89
|
+
* speak the math. Math in a control named by `aria-labelledby` from elements
|
|
90
|
+
* that do not hold it gets no label, as the elements' renderer gives it none.
|
|
91
|
+
* Returns the function that stops observing.
|
|
55
92
|
*/
|
|
56
93
|
export function observeMathControlNames(root, options = {}) {
|
|
57
94
|
if (typeof MutationObserver === "undefined")
|
|
@@ -70,7 +107,8 @@ export function observeMathControlNames(root, options = {}) {
|
|
|
70
107
|
return result.usedMathSpeech ? result.speechText : null;
|
|
71
108
|
};
|
|
72
109
|
const name = (container) => {
|
|
73
|
-
|
|
110
|
+
const control = flatTreeClosest(container, CONTROLS);
|
|
111
|
+
if (!control || isNamedFromElsewhere(control, container))
|
|
74
112
|
return;
|
|
75
113
|
const math = flatQuerySelector(container, "math");
|
|
76
114
|
const mathml = math && canonicalizeMathML(math.outerHTML);
|
|
@@ -88,6 +126,8 @@ export function observeMathControlNames(root, options = {}) {
|
|
|
88
126
|
void pending.then((label) => {
|
|
89
127
|
if (stopped || !label || !container.isConnected)
|
|
90
128
|
return;
|
|
129
|
+
if (isNamedFromElsewhere(control, container))
|
|
130
|
+
return;
|
|
91
131
|
if (container.getAttribute("aria-label") !== label) {
|
|
92
132
|
container.setAttribute("aria-label", label);
|
|
93
133
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pie-players/pie-assessment-toolkit",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "PIE assessment toolkit: composable services + reference implementation for assessment players and tool coordination",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,16 +69,16 @@
|
|
|
69
69
|
"test": "bun test"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@pie-players/pie-calculator": "1.0.0-beta.
|
|
73
|
-
"@pie-players/pie-context": "1.0.0-beta.
|
|
74
|
-
"@pie-players/pie-players-shared": "1.0.0-beta.
|
|
75
|
-
"@pie-players/pie-tts": "1.0.0-beta.
|
|
72
|
+
"@pie-players/pie-calculator": "1.0.0-beta.7",
|
|
73
|
+
"@pie-players/pie-context": "1.0.0-beta.7",
|
|
74
|
+
"@pie-players/pie-players-shared": "1.0.0-beta.7",
|
|
75
|
+
"@pie-players/pie-tts": "1.0.0-beta.7",
|
|
76
76
|
"speech-rule-engine": "5.0.0-rc.4"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@biomejs/biome": "^2.
|
|
80
|
-
"@happy-dom/global-registrator": "^
|
|
81
|
-
"@pie-players/tts-client-server": "1.0.0-beta.
|
|
79
|
+
"@biomejs/biome": "^2.6.2",
|
|
80
|
+
"@happy-dom/global-registrator": "^21.0.4",
|
|
81
|
+
"@pie-players/tts-client-server": "1.0.0-beta.7",
|
|
82
82
|
"svelte": "^5.57.2",
|
|
83
83
|
"svelte-check": "^4.7.6",
|
|
84
84
|
"typescript": "^5.9.3"
|