diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts
index eb34e58fe8..b0ca8efd73 100644
--- a/ts/a11y/explorer/KeyExplorer.ts
+++ b/ts/a11y/explorer/KeyExplorer.ts
@@ -334,7 +334,7 @@ export class SpeechExplorer
/**
* The anchors in this expression
*/
- protected anchors: HTMLElement[];
+ protected anchors: HTMLElement[] = [];
/**
* The elements that are focusable for tab navigation
@@ -1183,6 +1183,7 @@ export class SpeechExplorer
description += ', ' + localize('ForHelp');
}
speech += description;
+ speech = speech.replace(/^, /, '');
}
this.speak(
speech,
@@ -1221,7 +1222,7 @@ export class SpeechExplorer
}
/**
- * Create a new speech node and sets its needed attributes,
+ * Create a new speech node and set its needed attributes,
* then add it to the container and focus it. If there is
* and old speech node, remove it after a delay (the delay
* is needed for Orca on Linux).
@@ -1237,6 +1238,12 @@ export class SpeechExplorer
ssml: string[] = null,
description: string = this.none
) {
+ if (!this.node.getAttribute('data-speech-attached') && speech) {
+ speech = localize('Word/Math');
+ }
+ if (!this.node.getAttribute('data-braille-attached') && braille) {
+ braille = localize('Word/Math');
+ }
const oldspeech = this.speech;
const speechNode = (this.speech = document.createElement('mjx-speech'));
speechNode.setAttribute('role', this.role);
@@ -1303,9 +1310,10 @@ export class SpeechExplorer
container.setAttribute('has-speech', 'true');
}
const description = item.roleDescription;
- const speech =
+ const speech = (
(container.getAttribute(SemAttr.SPEECH) || '') +
- (description && description !== this.none ? ', ' + description : '');
+ (description && description !== this.none ? ', ' + description : '')
+ ).replace(/^, /, '');
this.img?.remove();
this.img = this.document.adaptor.node('mjx-speech', {
'aria-label': speech,
diff --git a/ts/a11y/explorer/__locales__/de.json b/ts/a11y/explorer/__locales__/de.json
index 5223cd92c3..8649c3ba98 100644
--- a/ts/a11y/explorer/__locales__/de.json
+++ b/ts/a11y/explorer/__locales__/de.json
@@ -4,14 +4,15 @@
"Help/Text1": "
Ausdrücke mit %1 erkunden
\n\nDie mathematischen Formeln auf dieser Seite werden von MathJax dargestellt, das sowohl den von Screenreadern vorgelesenen Text als auch das visuelle Layout für sehende Nutzer generiert.
\n\nVon MathJax gesetzte Ausdrücke können interaktiv erkundet werden und sind fokussierbar. Sie können die Tab-Taste verwenden, um zu einem gesetzten Ausdruck zu springen%2%3. Zunächst wird der Ausdruck vollständig vorgelesen, aber Sie können die folgenden Tasten verwenden, um den Ausdruck weiter zu erkunden:
",
"Help/Text2": "Über das MathJax-Kontextmenü können Sie die Sprach- oder Braille-Ausgabe für mathematische Ausdrücke aktivieren oder deaktivieren, die Sprache für die Sprachausgabe festlegen sowie weitere Funktionen von MathJax einstellen. Insbesondere können Sie im Untermenü „Explorer“ festlegen, wie die mathematischen Ausdrücke auf der Seite gekennzeichnet werden sollen (z. B. durch die Ansage „Mathematik“, wenn der Ausdruck vorgelesen wird) und ob eine Meldung darüber angezeigt werden soll, dass der Buchstabe „h“ dieses Dialogfeld öffnet. Wenn Sie die Sprachausgabe und die Braille-Anzeige deaktivieren, werden der Ausdrucks-Explorer, dessen Highlighting und das Hilfe-Symbol deaktiviert.
\n\nDie Unterstützung für taktile Braille-Geräte variiert je nach Screenreader, Browser und Betriebssystem. Wenn Sie ein Braille-Ausgabegerät verwenden, müssen Sie möglicherweise die Option „Mit Sprachausgabe kombinieren“ im Untermenü „Braille“ des Kontextmenüs auswählen, um anstelle der Sprachausgabe auf Ihrem Braille-Gerät eine Nemeth- oder Euro-Braille-Ausgabe zu erhalten. %1
\n\nDas Kontextmenü bietet außerdem Optionen zum Anzeigen oder Kopieren einer MathML-Version des Ausdrucks oder seines ursprünglichen Quellformats, zum Erstellen einer SVG-Version des Ausdrucks sowie zum Anzeigen verschiedener weiterer Informationen.
\n\nWenn Sie schließlich im Optionenmenü den Eintrag „MathML versteckt einbinden“ auswählen, wird die Sprach- und Braille-Generierung von MathJax deaktiviert und stattdessen visuell unsichtbares MathML verwendet, das von einigen Screenreadern vorgelesen werden kann, wobei diese Funktion nicht von allen Screenreadern und Betriebssystemen unterstützt wird. Durch Auswahl der Sprach- oder Braille-Generierung in den entsprechenden Untermenüs wird das unsichtbare MathML wieder entfernt.
\n\nWeitere Hilfe finden Sie in der MathJax-Dokumentation zur Barrierefreiheit.
",
"Help/Title": "Hilfe zum MathJax Expression Explorer",
+ "JumpTo": "Springe zu Zeile %1 und Spalte",
"Mac/Select": "oder die VoiceOver-Pfeiltasten, um einen Ausdruck auszuwählen",
"Mac/Title": "macOS und iOS mit VoiceOver",
- "JumpTo": "Springe zu Zeile %1 und Spalte",
"PositionMarked": "Position markiert",
"Unix/Select": "Orca sollte automatisch in den Fokusmodus wechseln. Ist dies nicht der Fall, kannst du den Fokusmodus mit der Tastenkombination „Orca+a“ ein- oder ausschalten. Beachte außerdem, dass du mit „Orca+Pfeiltasten“ Ausdrücke auch im Durchsichtmodus untersuchen kannst.",
"Unix/Title": "Unix mit Orca",
"Unknown/Title": "ein Screenreader",
"Win/Braille": "NVDA-Benutzer müssen diese Option auswählen, während JAWS-Benutzer die Braille-Ausgabe erhalten sollten, ohne diese Einstellung zu ändern.",
"Win/Select": "Der Screenreader sollte automatisch in den Fokus- oder Formularmodus wechseln, sobald der Ausdruck den Browserfokus erhält. Ist dies nicht der Fall, können Sie den Fokusmodus in NVDA mit NVDA+Leertaste umschalten; bei JAWS sollte die Eingabetaste den Formularmodus aktivieren, während die Plus-Taste des Ziffernblocks ihn beendet. Beachten Sie außerdem, dass Sie die NVDA- oder JAWS-Taste zusammen mit den Pfeiltasten verwenden können, um den Ausdruck auch im Browse-Modus zu erkunden, und dass Sie mit NVDA+Umschalt+Pfeiltasten aus einem Ausdruck herausnavigieren können, der in NVDA den Fokus hat.",
- "Win/Title": "Windows mit NVDA oder JAWS"
+ "Win/Title": "Windows mit NVDA oder JAWS",
+ "Word/Math": "Mathematik"
}
diff --git a/ts/a11y/explorer/__locales__/en.json b/ts/a11y/explorer/__locales__/en.json
index 39ca9e6629..9f424c5e9b 100644
--- a/ts/a11y/explorer/__locales__/en.json
+++ b/ts/a11y/explorer/__locales__/en.json
@@ -4,14 +4,15 @@
"Help/Text1": "Exploring expressions with %1
\n\nThe mathematics on this page is being rendered by MathJax, which generates both the text spoken by screen readers, as well as the visual layout for sighted users.
\n\nExpressions typeset by MathJax can be explored interactively, and are focusable. You can use the Tab key to move to a typeset expression%2%3. Initially, the expression will be read in full, but you can use the following keys to explore the expression further:
",
"Help/Text2": "The MathJax contextual menu allows you to enable or disable speech or Braille generation for mathematical expressions, the language to use for the spoken mathematics, and other features of MathJax. In particular, the Explorer submenu allows you to specify how the mathematics should be identified in the page (e.g., by saying \"math\" when the expression is spoken), and whether or not to include a message about the letter \"h\" bringing up this dialog box. Turning off speech and Braille will disable the expression explorer, its highlighting, and its help icon.
\n\nSupport for tactile Braille devices varies across screen readers, browsers, and operative systems. If you are using a Braille output device, you may need to select the \"Combine with Speech\" option in the contextual menu's Braille submenu in order to obtain Nemeth or Euro Braille output rather than the speech text on your Braille device. %1
\n\nThe contextual menu also provides options for viewing or copying a MathML version of the expression or its original source format, creating an SVG version of the expression, and viewing various other information.
\n\nFinally, selecting the \"Insert Hidden MathML\" item from the options submenu will turn of MathJax's speech and Braille generation and instead use visually hidden MathML that some screen readers can voice, though support for this is not universal across all screen readers and operating systems. Selecting speech or Braille generation in their submenus will remove the hidden MathML again.
\n\nFor more help, see the MathJax accessibility documentation.
",
"Help/Title": "MathJax Expression Explorer Help",
+ "JumpTo": "Jump to row %1 and column",
"Mac/Select": "or the VoiceOver arrow keys to select an expression",
"Mac/Title": "MacOS and iOS using VoiceOver",
- "JumpTo": "Jump to row %1 and column",
"PositionMarked": "Position marked",
"Unix/Select": "Orca should enter focus mode automatically. If not, use the Orca+a key to toggle focus mode on or off. Also note that you can use Orca+arrow keys to explore expressions even in browse mode",
"Unix/Title": "Unix using Orca",
"Unknown/Title": "a Screen Reader",
"Win/Braille": "NVDA users need to select this option, while JAWS users should be able to get Braille output without changing this setting.",
"Win/Select": "The screen reader should enter focus or forms mode automatically when the expression gets the browser focus, but if not, you can toggle focus mode using NVDA+space in NVDA; for JAWS, Enter should start forms mode while Numpad Plus leaves it. Also note that you can use the NVDA or JAWS key plus the arrow keys to explore the expression even in browse mode, and you can use NVDA+shift+arrow keys to navigate out of an expression that has the focus in NVDA",
- "Win/Title": "Windows using NVDA or JAWS"
+ "Win/Title": "Windows using NVDA or JAWS",
+ "Word/Math": "math"
}
diff --git a/ts/a11y/speech.ts b/ts/a11y/speech.ts
index 5a593bb0bd..6ad9ba6045 100644
--- a/ts/a11y/speech.ts
+++ b/ts/a11y/speech.ts
@@ -190,12 +190,12 @@ export interface SpeechMathDocument extends EnrichedMathDocument<
/**
* @param {SpeechMathDocument} doc The MathDocument for the error
* @param {SpeechMathItem} math The MathItem causing the error
- * @param {Error} err The error being processed
+ * @param {string} err The error message being processed
*/
speechError(
doc: SpeechMathDocument,
math: SpeechMathItem,
- err: Error
+ err: string
): void;
/**
@@ -234,7 +234,7 @@ export function SpeechMathDocumentMixin<
speechError: (
doc: SpeechMathDocument,
math: SpeechMathItem,
- err: Error
+ err: string
) => doc.speechError(doc, math, err),
renderActions: expandable({
...EnrichedMathDocument.OPTIONS.renderActions,
@@ -316,9 +316,11 @@ export function SpeechMathDocumentMixin<
public speechError(
_doc: SpeechMathDocument,
_math: SpeechMathItem,
- err: Error
+ err: string
) {
- console.warn(localize('SpeechError'), err);
+ if (err) {
+ console.warn(localize('SpeechError'), err);
+ }
}
/**
diff --git a/ts/a11y/speech/GeneratorPool.ts b/ts/a11y/speech/GeneratorPool.ts
index 82da3e344a..8a6490c396 100644
--- a/ts/a11y/speech/GeneratorPool.ts
+++ b/ts/a11y/speech/GeneratorPool.ts
@@ -27,6 +27,7 @@ import { buildLabel, SemAttr } from '../speech/SpeechUtil.js';
import { DOMAdaptor } from '../../core/DOMAdaptor.js';
import { SpeechMathItem } from '../speech.js';
import { WorkerHandler } from './WebWorker.js';
+import { localize } from '../speech/__locales__/Component.js';
/**
* @template N The HTMLElement node class
@@ -212,6 +213,10 @@ export class GeneratorPool {
*/
public getLabel(node: N, _center: string = '', sep: string = ' '): string {
const adaptor = this.adaptor;
+ if (!this.webworker.ready) {
+ // return document.querySelector('.mjx-selected').textContent;
+ return localize('NoSpeech');
+ }
return (
buildLabel(
adaptor.getAttribute(node, SemAttr.SPEECH_SSML),
@@ -231,6 +236,10 @@ export class GeneratorPool {
*/
public getBraille(node: N): string {
const adaptor = this.adaptor;
+ if (!this.webworker.ready) {
+ // return document.querySelector('.mjx-selected').textContent;
+ return localize('NoBraille');
+ }
return (
adaptor.getAttribute(node, 'aria-braillelabel') ||
adaptor.getAttribute(node, SemAttr.BRAILLE)
diff --git a/ts/a11y/speech/WebWorker.ts b/ts/a11y/speech/WebWorker.ts
index fd5fca20a1..e5d8f47bc7 100644
--- a/ts/a11y/speech/WebWorker.ts
+++ b/ts/a11y/speech/WebWorker.ts
@@ -26,6 +26,7 @@ import { OptionList } from '../../util/Options.js';
import { Message, ClientCommand, Structure } from './MessageTypes.js';
import { SpeechMathItem } from '../speech.js';
import { SemAttr } from './SpeechUtil.js';
+import { StyleJsonData } from '../../util/StyleJson.js';
import { Locale } from '../../util/Locale.js';
import { localize, COMPONENT } from './__locales__/Component.js';
@@ -64,6 +65,19 @@ export class WorkerHandler {
*/
protected worker: minWorker;
+ /**
+ * The styles to use for the warning live region
+ */
+ protected liveStyles: StyleJsonData = {
+ display: 'inline-block',
+ position: 'absolute',
+ top: 0,
+ left: '-10px',
+ width: '1px',
+ height: '1px',
+ overflow: 'hidden',
+ };
+
/**
* The adaptor to work with typeset nodes.
*
@@ -502,8 +516,12 @@ export class WorkerHandler {
*/
public Terminate(): Promise | void {
this.debug('Terminating pending tasks');
+ let cmd = '';
for (const task of this.tasks) {
- task.reject(localize('Worker/Terminate', task.cmd.data.cmd));
+ task.reject(
+ task.cmd.cmd === cmd ? '' : localize('Worker/Terminate', task.cmd.cmd)
+ );
+ cmd = task.cmd.cmd;
}
this.tasks = [];
this.debug('Terminating worker');
@@ -515,12 +533,21 @@ export class WorkerHandler {
* restarted, if desired.
*/
public async Stop() {
- if (!this.worker) {
- Locale.throw(COMPONENT, 'Worker/NotStarted');
+ if (this.worker) {
+ await this.Terminate();
+ this.worker = null;
+ this.ready = false;
}
- await this.Terminate();
- this.worker = null;
- this.ready = false;
+ }
+
+ public LiveWarn(message: string) {
+ const adaptor = this.adaptor;
+ const live = adaptor.node('mjx-worker-live', {
+ 'aria-live': 'assertive',
+ style: this.liveStyles,
+ });
+ adaptor.insert(live, adaptor.firstChild(adaptor.body()));
+ setTimeout(() => adaptor.append(live, adaptor.text(message)), 1000);
}
/**
@@ -615,5 +642,41 @@ export class WorkerHandler {
console.log('Log:', data);
}
},
+
+ /**
+ * The webworker failed to start up properly, so report an error
+ *
+ * @param {WorkerHandler} handler The active handler for the worker.
+ * @param {Message} data The data received from the worker.
+ */
+ Failed(handler: WorkerHandler, data: Message) {
+ const { path, worker } = handler.options;
+ Locale.warn(
+ COMPONENT,
+ data.includes(worker) ? 'Worker/Failed' : 'Worker/FailedFrom',
+ data,
+ `${path}/${worker}`
+ );
+ handler.LiveWarn(localize('Worker/Warning'));
+ handler.Stop();
+ },
+
+ /**
+ * A webworker map failed to load, so report an error
+ *
+ * @param {WorkerHandler} handler The active handler for the worker.
+ * @param {Message} data The data received from the worker.
+ */
+ Maps(handler: WorkerHandler, data: Message) {
+ const { maps } = handler.options;
+ Locale.warn(
+ COMPONENT,
+ data.includes(maps) ? 'Worker/Maps' : 'Worker/MapsFrom',
+ data,
+ maps
+ );
+ handler.LiveWarn(localize('Worker/Warning'));
+ handler.Stop();
+ },
};
}
diff --git a/ts/a11y/speech/__locales__/de.json b/ts/a11y/speech/__locales__/de.json
index df2d79cd84..2d3ae97785 100644
--- a/ts/a11y/speech/__locales__/de.json
+++ b/ts/a11y/speech/__locales__/de.json
@@ -1,8 +1,14 @@
{
+ "NoBraille": "Braille nicht verfügbar",
+ "NoSpeech": "Sprache nicht verfügbar",
"ProsodyError": "Beim Prosody-Abgleich ist ein Fehler aufgetreten",
"SpeechError": "Fehler bei der Sprachgenerierung:",
"Worker/Cancelled": "Task %1 abgebrochen",
+ "Worker/Failed": "MathJax Webworker konnte nicht geladen werden: %1",
+ "Worker/FailedFrom": "MathJax Webworker konnte nicht von %2 geladen werden: %1",
+ "Worker/Warning": "Warnung: MathJax kann für mathematische Formeln auf dieser Seite keine Sprachausgabe erzeugen.",
+ "Worker/Maps": "MathJax Webworker konnte die mathematische Zuordnung nicht laden: %1",
+ "Worker/MapsFrom": "MathJax Webworker konnte die mathematische Zuordnung nicht von %2 laden: %1",
"Worker/NotStarted": "Worker wurde nicht gestartet",
- "Worker/Started": "Worker bereits gestartet",
"Worker/Terminate": "%1 durch Beendigung des WorkerHandlers abgebrochen"
}
diff --git a/ts/a11y/speech/__locales__/en.json b/ts/a11y/speech/__locales__/en.json
index 874246021a..22d1db2ae6 100644
--- a/ts/a11y/speech/__locales__/en.json
+++ b/ts/a11y/speech/__locales__/en.json
@@ -1,8 +1,14 @@
{
+ "NoBraille": "Braille not available",
+ "NoSpeech": "Speech not available",
"ProsodyError": "Something went wrong with the prosody matching",
"SpeechError": "Speech generation error:",
"Worker/Cancelled": "Task %1 cancelled",
+ "Worker/Failed": "MathJax webworker failed to load: %1",
+ "Worker/FailedFrom": "MathJax webworker failed to load from %2: %1",
+ "Worker/Warning": "Warning: MathJax is not able to generate speech for the mathematics on this page",
+ "Worker/Maps": "MathJax webworker failed to load math map: %1",
+ "Worker/MapsFrom": "MathJax webworker failed to load math map from %2: %1",
"Worker/NotStarted": "Worker has not been started",
- "Worker/Started": "Worker already started",
"Worker/Terminate": "%1 cancelled by WorkerHandler termination"
}
diff --git a/ts/a11y/sre/speech-worker.ts b/ts/a11y/sre/speech-worker.ts
index 71ae9fc2ec..d282baaea5 100644
--- a/ts/a11y/sre/speech-worker.ts
+++ b/ts/a11y/sre/speech-worker.ts
@@ -84,8 +84,14 @@ declare const SRE: any;
if (!global.require) {
await import(/* webpackIgnore: true */ './require.mjs');
}
- global.getMap = (file: string) =>
- Promise.resolve(JSON.stringify(global.require(file)));
+ global.getMap = (file: string) => {
+ try {
+ return Promise.resolve(JSON.stringify(global.require(file)));
+ } catch (err) {
+ Client('Maps', err.message.split(/\n/)[0]);
+ return {};
+ }
+ };
} else {
//
// For web workers, make global be the self object
@@ -97,7 +103,10 @@ declare const SRE: any;
global.getMap = (file: string) =>
fetch(file)
.then((data) => data.json())
- .catch((err) => console.log(err));
+ .catch((err) => {
+ Client('Maps', err.message);
+ return {};
+ });
}
//
diff --git a/ts/adaptors/HTMLAdaptor.ts b/ts/adaptors/HTMLAdaptor.ts
index d77f5648ae..b637216db8 100644
--- a/ts/adaptors/HTMLAdaptor.ts
+++ b/ts/adaptors/HTMLAdaptor.ts
@@ -641,7 +641,11 @@ export class HTMLAdaptor<
const file = `${path}/${worker}`;
const content = `
self.maps = '${quoted(maps)}';
- importScripts('${quoted(file)}');
+ try {
+ importScripts('${quoted(file)}');
+ } catch (e) {
+ postMessage({cmd: 'Failed', data: e.message});
+ }
`;
const url = URL.createObjectURL(
new Blob([content], { type: 'text/javascript' })
diff --git a/ts/adaptors/NodeMixin.ts b/ts/adaptors/NodeMixin.ts
index 48d1f61494..11526fb57e 100644
--- a/ts/adaptors/NodeMixin.ts
+++ b/ts/adaptors/NodeMixin.ts
@@ -203,13 +203,19 @@ export function NodeMixin>(
this.worker.terminate();
}
}
- const { path, maps } = options;
- const url = `${path}/${options.worker}`;
+ const { path, maps, worker: file } = options;
+ const url = `${path}/${file}`;
const worker = new LiteWorker(url, {
type: 'module',
workerData: { maps },
});
worker.addEventListener('message', listener);
+ worker.addEventListener('error', (err) => {
+ const event = new MessageEvent('error', {
+ data: { cmd: 'Failed', data: err.message },
+ });
+ listener(event);
+ });
return worker;
}
};
diff --git a/ts/util/AsyncLoad.ts b/ts/util/AsyncLoad.ts
index fdd59f1bc6..60321387ca 100644
--- a/ts/util/AsyncLoad.ts
+++ b/ts/util/AsyncLoad.ts
@@ -62,6 +62,9 @@ export function resolvePath(
relative: (name: string) => string,
absolute: (name: string) => string = (name) => name
): string {
+ if (name.substring(0, 5) === 'node:') {
+ return name.substring(5);
+ }
const Package =
typeof MathJax === 'undefined'
? null