diff --git a/components/bin/build b/components/bin/build index dfb0c86c1..be0504011 100755 --- a/components/bin/build +++ b/components/bin/build @@ -39,7 +39,7 @@ const EXPORTPATTERN = /(^export(?:\s+default)?(?:\s+abstract)?\s+(?:[^ {*}]+\s+(?:enum\s+)?[a-zA-Z0-9_.$]+|\{.* as .*\}))/m; const EXPORT_IGNORE = ['type', 'interface']; -const EXPORT_PROCESS = ['let', 'const', 'var', 'function', 'class', 'namespace', 'as']; +const EXPORT_PROCESS = ['let', 'const', 'var', 'function', 'class', 'namespace', 'enum', 'as']; /** * The module type to use ('cjs' or 'mjs') @@ -159,7 +159,7 @@ function processParts(parts) { for (let i = 1; i < parts.length; i += 2) { const words = parts[i].split(/\s+/); const n = words.length; - const type = (words[n - 2] === 'enum' ? words[n - 3] : words[n - 2]); + const type = (words[n - 2] === 'enum' && n > 3 ? words[n - 3] : words[n - 2]); const name = words[n - 1].replace(/\}$/, ''); if (words[1] === 'default' || type === 'default') { diff --git a/components/mjs/a11y/explorer/config.json b/components/mjs/a11y/explorer/config.json index 4070ae12c..bca3e99e3 100644 --- a/components/mjs/a11y/explorer/config.json +++ b/components/mjs/a11y/explorer/config.json @@ -6,7 +6,6 @@ "webpack": { "name": "a11y/explorer", "libs": [ - "components/src/ui/menu/lib", "components/src/a11y/semantic-enrich/lib", "components/src/a11y/sre/lib", "components/src/input/mml/lib", diff --git a/components/mjs/a11y/explorer/explorer.js b/components/mjs/a11y/explorer/explorer.js index 9f38cce67..646ea55f9 100644 --- a/components/mjs/a11y/explorer/explorer.js +++ b/components/mjs/a11y/explorer/explorer.js @@ -1,17 +1,7 @@ import './lib/explorer.js'; -import {combineDefaults} from '#js/components/global.js'; import {ExplorerHandler} from '#js/a11y/explorer.js'; if (MathJax.startup && typeof window !== 'undefined') { - if (MathJax.config.options && MathJax.config.options.enableExplorer !== false) { - combineDefaults(MathJax.config, 'options', { - menuOptions: { - settings: { - explorer: true - } - } - }); - } MathJax.startup.extendHandler(handler => ExplorerHandler(handler)); } diff --git a/components/mjs/a11y/semantic-enrich/config.json b/components/mjs/a11y/semantic-enrich/config.json index dd4068cfe..bdbef7eb1 100644 --- a/components/mjs/a11y/semantic-enrich/config.json +++ b/components/mjs/a11y/semantic-enrich/config.json @@ -1,7 +1,11 @@ { "build": { "component": "a11y/semantic-enrich", - "targets": ["a11y/semantic-enrich.ts"] + "targets": [ + "a11y/semantic-enrich.ts", + "a11y/speech/SpeechUtil.ts", + "a11y/speech/GeneratorPool.ts" + ] }, "webpack": { "name": "a11y/semantic-enrich", diff --git a/components/mjs/dependencies.js b/components/mjs/dependencies.js index ed6df0094..74b2be456 100644 --- a/components/mjs/dependencies.js +++ b/components/mjs/dependencies.js @@ -18,7 +18,7 @@ export const dependencies = { 'a11y/semantic-enrich': ['input/mml', 'a11y/sre'], 'a11y/complexity': ['a11y/semantic-enrich'], - 'a11y/explorer': ['a11y/semantic-enrich', 'ui/menu'], + 'a11y/explorer': ['a11y/semantic-enrich'], '[mml]/mml3': ['input/mml'], '[tex]/all-packages': ['input/tex-base'], '[tex]/action': ['input/tex-base', '[tex]/newcommand'], diff --git a/components/mjs/ui/menu/config.json b/components/mjs/ui/menu/config.json index 645cfe8e5..4fffd9231 100644 --- a/components/mjs/ui/menu/config.json +++ b/components/mjs/ui/menu/config.json @@ -1,7 +1,7 @@ { "build": { "component": "ui/menu", - "targets": ["ui/menu"], + "targets": ["ui/menu", "a11y/speech/SpeechMenu.ts"], "excludeSubdirs": true }, "webpack": { diff --git a/ts/a11y/explorer.ts b/ts/a11y/explorer.ts index a20e77938..484385aea 100644 --- a/ts/a11y/explorer.ts +++ b/ts/a11y/explorer.ts @@ -203,7 +203,7 @@ export function ExplorerMathDocumentMixin this.document.options.a11y[exKey])) { + explorer.Attach(); + this.attached.push(key); + } else { + explorer.Detach(); + } + continue; } - if (this.document.options.a11y[key]) { + if (a11y[key] || (key === 'speech' && (a11y.braille || a11y.keyMagnifier))) { explorer.Attach(); this.attached.push(key); } else { diff --git a/ts/a11y/explorer/KeyExplorer.ts b/ts/a11y/explorer/KeyExplorer.ts index 096d0f53e..7946557dc 100644 --- a/ts/a11y/explorer/KeyExplorer.ts +++ b/ts/a11y/explorer/KeyExplorer.ts @@ -178,6 +178,7 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo const prev = this.node.querySelector(prevNav); if (prev) { prev.removeAttribute('tabindex'); + this.FocusOut(null); } this.current = clicked; if (!this.triggerLinkMouse()) { @@ -204,6 +205,9 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo * @override */ public FocusOut(_event: FocusEvent) { + // This guard is to FF and Safari, where focus in fires only once on + // keyboard. + if (!this.active) return; this.generators.CleanUp(this.current); if (!this.move) { this.Stop(); @@ -474,8 +478,9 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo // the root node by default. this.current = this.node.childNodes[0] as HTMLElement; } + const options = this.document.options; let promise = Sre.sreReady(); - if (this.generators.update(this.document.options)) { + if (this.generators.update(options)) { promise = promise.then( () => this.Speech() ); @@ -483,15 +488,15 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo this.current.setAttribute('tabindex', '0'); this.current.focus(); super.Start(); - if (this.document.options.a11y.subtitles) { + if (options.a11y.subtitles && options.a11y.speech && options.enableSpeech) { promise.then( () => this.region.Show(this.node, this.highlighter)); } - if (this.document.options.a11y.viewBraille) { + if (options.a11y.viewBraille && options.a11y.braille && options.enableBraille) { promise.then( () => this.brailleRegion.Show(this.node, this.highlighter)); } - if (this.document.options.a11y.keyMagnifier) { + if (options.a11y.keyMagnifier) { this.magnifyRegion.Show(this.node, this.highlighter); } this.Update(); @@ -566,8 +571,8 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo } } if (this.active) { - this.stopEvent(event); if (this.Move(event)) { + this.stopEvent(event); this.Update(); return; } @@ -642,7 +647,8 @@ export class SpeechExplorer extends AbstractExplorer implements KeyExplo public semanticFocus() { const node = this.current || this.node; const id = node.getAttribute('data-semantic-id'); - const stree = this.generators.speechGenerator.getRebuilt().stree; + const stree = this.generators.speechGenerator.getRebuilt()?.stree; + if (!stree) return null; const snode = stree.root.querySelectorAll((x: any) => x.id.toString() === id)[0]; return snode || stree.root; } diff --git a/ts/a11y/explorer/Region.ts b/ts/a11y/explorer/Region.ts index 3be434267..bb0425527 100644 --- a/ts/a11y/explorer/Region.ts +++ b/ts/a11y/explorer/Region.ts @@ -417,6 +417,12 @@ export class SpeechRegion extends LiveRegion { setTimeout(() => { if (this.voiceRequest) { resolve(true); + } else { + // This case is to make FF and Safari work. + setTimeout(() => { + this.voiceRequest = true; + resolve(true); + }, 100); } }, 100); }); diff --git a/ts/a11y/semantic-enrich.ts b/ts/a11y/semantic-enrich.ts index b18546ee5..fcbf7fad8 100644 --- a/ts/a11y/semantic-enrich.ts +++ b/ts/a11y/semantic-enrich.ts @@ -118,6 +118,11 @@ export interface EnrichedMathItem extends MathItem { * @param {MathDocument} document The document where enrichment is occurring */ attachSpeech(document: MathDocument): void; + + /** + * @param {MathDocument} document The MathDocument for the MathItem + */ + unEnrich(document: MathDocument): void; } /** @@ -201,6 +206,19 @@ export function EnrichedMathItemMixin) { + const mml = this.inputData.originalMml; + if (!mml) return; + const math = new document.options.MathItem('', MmlJax); + math.math = mml; + math.display = this.display; + math.compile(document); + this.root = math.root; + } + /** * Correct the selection values for the maction items from the original MathML */ @@ -248,25 +266,28 @@ export function EnrichedMathItemMixin extends AbstractMathDocument, math: EnrichedMathItem, err: Error): void; + + /** + * @param {EnrichedMathDocument} doc The MathDocument for the error + * @paarm {EnrichedMathItem} math The MathItem causing the error + * @param {Error} err The error being processed + */ + speechError(doc: EnrichedMathDocument, math: EnrichedMathItem, err: Error): void; } /** @@ -343,6 +371,9 @@ export function EnrichedMathDocumentMixin, math: EnrichedMathItem, err: Error) => doc.enrichError(doc, math, err), + speechError: (doc: EnrichedMathDocument, + math: EnrichedMathItem, + err: Error) => doc.speechError(doc, math, err), renderActions: expandable({ ...BaseDocument.OPTIONS.renderActions, enrich: [STATE.ENRICHED], @@ -416,6 +447,12 @@ export function EnrichedMathDocumentMixin, _math: EnrichedMathItem, err: Error) { + console.warn('Speech generation error:', err); + } + /** * @override */ @@ -423,6 +460,11 @@ export function EnrichedMathDocumentMixin= STATE.COMPILED) { + for (const item of this.math) { + (item as EnrichedMathItem).unEnrich(this); + } + } } if (state < STATE.ATTACHSPEECH) { this.processed.clear('attach-speech'); diff --git a/ts/a11y/speech/GeneratorPool.ts b/ts/a11y/speech/GeneratorPool.ts index 63ff8e0fa..2cd76d3d8 100644 --- a/ts/a11y/speech/GeneratorPool.ts +++ b/ts/a11y/speech/GeneratorPool.ts @@ -370,13 +370,17 @@ export class GeneratorPool { this.dummyList.forEach(attr => this.copyAttributes(xml, node, attr)); } } - const speech = this.getLabel(node); - if (speech) { - this.adaptor.setAttribute(node, 'aria-label', buildSpeech(speech, locale)[0]); + if (this.options.a11y.speech) { + const speech = this.getLabel(node); + if (speech) { + this.adaptor.setAttribute(node, 'aria-label', buildSpeech(speech, locale)[0]); + } } - const braille = this.adaptor.getAttribute(node, 'data-semantic-braille'); - if (braille) { - this.adaptor.setAttribute(node, 'aria-braillelabel', braille); + if (this.options.a11y.braille) { + const braille = this.adaptor.getAttribute(node, 'data-semantic-braille'); + if (braille) { + this.adaptor.setAttribute(node, 'aria-braillelabel', braille); + } } const xmlChildren = Array.from(xml.childNodes); Array.from(this.adaptor.childNodes(node)).forEach( diff --git a/ts/a11y/speech/SpeechMenu.ts b/ts/a11y/speech/SpeechMenu.ts index 0e6b8dbd2..7bf741358 100644 --- a/ts/a11y/speech/SpeechMenu.ts +++ b/ts/a11y/speech/SpeechMenu.ts @@ -169,23 +169,25 @@ export function clearspeakMenu(menu: MJContextMenu, sub: Submenu) { let locale = menu.pool.lookup('locale').getValue() as string; const box = csSelectionBox(menu, locale); let items: Object[] = []; - const explorer = (menu.mathItem as ExplorerMathItem)?.explorers?.speech; - const semantic = explorer?.semanticFocus(); - const previous = Sre.clearspeakPreferences.currentPreference(); - items = items.concat(basePreferences(previous)); - if (semantic) { - const smart = Sre.clearspeakPreferences.relevantPreferences(semantic); - items = items.concat(smartPreferences(previous, smart, locale)); - } - if (box) { - items.splice(2, 0, box); + if (menu.settings.speech) { + const explorer = (menu.mathItem as ExplorerMathItem)?.explorers?.speech; + const semantic = explorer?.semanticFocus(); + const previous = Sre.clearspeakPreferences.currentPreference(); + items = items.concat(basePreferences(previous)); + if (semantic) { + const smart = Sre.clearspeakPreferences.relevantPreferences(semantic); + items = items.concat(smartPreferences(previous, smart, locale)); + } + if (box) { + items.splice(2, 0, box); + } } return menu.factory.get('subMenu')(menu.factory, { items: items, id: 'Clearspeak' }, sub); } -MJContextMenu.DynamicSubmenus.set('Clearspeak', clearspeakMenu); +MJContextMenu.DynamicSubmenus.set('Clearspeak', [clearspeakMenu, 'speech']); let LOCALE_MENU: SubMenu = null; /** @@ -209,4 +211,4 @@ export function localeMenu(menu: MJContextMenu, sub: Submenu) { items: radios, id: 'Language'}, sub); return LOCALE_MENU; } -MJContextMenu.DynamicSubmenus.set('A11yLanguage', localeMenu); +MJContextMenu.DynamicSubmenus.set('A11yLanguage', [localeMenu, 'speech']); diff --git a/ts/ui/menu/MJContextMenu.ts b/ts/ui/menu/MJContextMenu.ts index e22eff1b8..ccc6dfbf8 100644 --- a/ts/ui/menu/MJContextMenu.ts +++ b/ts/ui/menu/MJContextMenu.ts @@ -22,6 +22,7 @@ */ import {MathItem} from '../../core/MathItem.js'; +import {OptionList} from '../../util/Options.js'; import {JaxList} from './Menu.js'; import {ContextMenu, SubMenu, Submenu, Menu, Item} from './mj-context-menu.js'; @@ -38,13 +39,18 @@ export class MJContextMenu extends ContextMenu { * Static map to hold methods for re-computing dynamic submenus. * @type {Map SubMenu> = new Map(); + public static DynamicSubmenus: Map SubMenu, string]> = new Map(); /** * The MathItem that has posted the menu */ public mathItem: MathItem = null; + /** + * The document options + */ + public settings: OptionList; + /** * The error message for the current MathItem */ @@ -69,6 +75,7 @@ export class MJContextMenu extends ContextMenu { this.getOriginalMenu(); this.getSemanticsMenu(); this.getSpeechMenu(); + this.getBrailleMenu(); this.getSvgMenu(); this.getErrorMessage(); this.dynamicSubmenus(); @@ -84,6 +91,7 @@ export class MJContextMenu extends ContextMenu { */ public unpost() { super.unpost(); + this.mathItem.typesetRoot.blur(); this.mathItem = null; } @@ -99,11 +107,13 @@ export class MJContextMenu extends ContextMenu { let menu = this as Menu; let item = null as Item; for (const name of names) { - if (menu) { - item = menu.find(name); - menu = (item instanceof Submenu ? item.submenu : null); - } else { - item = null; + if (!menu) return null; + for (item of menu.items) { + if (item.id === name) { + menu = (item instanceof Submenu ? item.submenu : null); + break; + } + menu = item = null; } } return item; @@ -135,7 +145,7 @@ export class MJContextMenu extends ContextMenu { * Enable/disable the semantics settings item */ protected getSemanticsMenu() { - const semantics = this.findID('Settings', 'semantics'); + const semantics = this.findID('Settings', 'MathmlIncludes', 'semantics'); this.mathItem.inputJax.name === 'MathML' ? semantics.disable() : semantics.enable(); } @@ -148,6 +158,15 @@ export class MJContextMenu extends ContextMenu { this.findID('Copy', 'Speech')[speech ? 'enable' : 'disable'](); } + /** + * Enable/disable the Braille menus + */ + protected getBrailleMenu() { + const braille = this.mathItem.outputData.braille; + this.findID('Show', 'Braille')[braille ? 'enable' : 'disable'](); + this.findID('Copy', 'Braille')[braille ? 'enable' : 'disable'](); + } + /** * Enable/disable the svg menus */ @@ -179,12 +198,12 @@ export class MJContextMenu extends ContextMenu { * Renews the dynamic submenus. */ public dynamicSubmenus() { - for (const [id, method] of MJContextMenu.DynamicSubmenus) { + for (const [id, [method, option]] of MJContextMenu.DynamicSubmenus) { const menu = this.find(id) as Submenu; if (!menu) continue; const sub = method(this, menu); menu.submenu = sub; - if (sub.items.length) { + if (sub.items.length && (!option || this.settings[option])) { menu.enable(); } else { menu.disable(); diff --git a/ts/ui/menu/Menu.ts b/ts/ui/menu/Menu.ts index b1945185a..784371b80 100644 --- a/ts/ui/menu/Menu.ts +++ b/ts/ui/menu/Menu.ts @@ -43,7 +43,6 @@ import * as MenuUtil from './MenuUtil.js'; import {Info, Parser, Rule, CssStyles, Submenu} from './mj-context-menu.js'; - /*==========================================================================*/ /** @@ -78,20 +77,21 @@ export interface MenuSettings { breakInline: boolean; autocollapse: boolean; collapsible: boolean; + enrich: boolean; inTabOrder: boolean; assistiveMml: boolean; // A11y settings backgroundColor: string; backgroundOpacity: string; braille: boolean; - explorer: boolean; + brailleCode: string; foregroundColor: string; foregroundOpacity: string; highlight: string; - locale: string; infoPrefix: boolean; infoRole: boolean; infoType: boolean; + locale: string; magnification: string; magnify: string; speech: boolean; @@ -140,9 +140,13 @@ export class Menu { breakInline: true, autocollapse: false, collapsible: false, + enrich: true, inTabOrder: true, assistiveMml: false, - explorer: false + speech: true, + braille: true, + brailleCode: 'nemeth', + speechRules: 'mathspeek-default' }, jax: { CHTML: null, @@ -298,14 +302,15 @@ export class Menu { ' as MathML or in its original format, to the clipboard', ' (in browsers that support that).

', '

Math Settings: These give you control over features of MathJax,', - ' such the size of the mathematics, and the mechanism used', - ' to display equations.

', + ' such the size of the mathematics, the mechanism used to display equations,', + ' how to handle equations that are too wide, and the language to use for', + ' MathJax\'s menus and error messages (not yet implemented in v4).', + '

', '

Accessibility: MathJax can work with screen', ' readers to make mathematics accessible to the visually impaired.', - ' Turn on the explorer to enable generation of speech strings', - ' and the ability to investigate expressions interactively.

', - '

Language: This menu lets you select the language used by MathJax', - ' for its menus and warning messages. (Not yet implemented in version 3.)

', + ' Turn on speech or braille generation to enable creation of speech strings', + ' and the ability to investigate expressions interactively. You can control', + ' the style of the explorer in its menu.

', '', '

Math Zoom: If you are having difficulty reading an', ' equation, MathJax can enlarge it to help you see it better, or', @@ -387,6 +392,20 @@ export class Menu { '' ); + /** + * The "Show As Speech Text" info box + */ + protected brailleText = new SelectableInfo( + 'MathJax Braille Code', + () => { + if (!this.menu.mathItem) return ''; + return '

' + + this.formatSource(this.menu.mathItem.outputData.braille) + + '
'; + }, + '' + ); + /** * The "Show As Error Message" info box */ @@ -442,13 +461,11 @@ export class Menu { const jax = this.document.outputJax; this.jax[jax.name] = jax; this.settings.renderer = jax.name; - if (MathJax._.a11y && MathJax._.a11y.explorer) { - Object.assign(this.settings, this.document.options.a11y); - } this.settings.scale = jax.options.scale; this.defaultSettings = Object.assign({}, this.settings); this.settings.overflow = - jax.options.displayOverflow.substring(0, 1).toUpperCase() + jax.options.displayOverflow.substring(1).toLowerCase(); + jax.options.displayOverflow.substring(0, 1).toUpperCase() + + jax.options.displayOverflow.substring(1).toLowerCase(); this.settings.breakInline = jax.options.linebreaks.inline; } @@ -478,20 +495,18 @@ export class Menu { this.variable('ctrl'), this.variable('shift'), this.variable ('scale', scale => this.setScale(scale)), - this.variable('explorer', explore => this.setExplorer(explore)), + this.a11yVar('speech', speech => this.setSpeech(speech)), + this.a11yVar('braille', braille => this.setBraille(braille)), + this.variable('brailleCode', code => this.setBrailleCode(code)), this.a11yVar ('highlight'), this.a11yVar ('backgroundColor'), this.a11yVar ('backgroundOpacity'), this.a11yVar ('foregroundColor'), this.a11yVar ('foregroundOpacity'), - this.a11yVar('speech'), this.a11yVar('subtitles'), - this.a11yVar('braille'), this.a11yVar('viewBraille'), this.a11yVar('voicing'), - this.a11yVar('locale', value => { - MathJax._.a11y.sre.Sre.setupEngine({locale: value as string}); - }), + this.a11yVar('locale', locale => this.setLocale(locale)), this.a11yVar('speechRules', value => { const [domain, style] = value.split('-'); this.document.options.sre.domain = domain; @@ -505,6 +520,7 @@ export class Menu { this.a11yVar('infoPrefix'), this.variable('autocollapse'), this.variable('collapsible', collapse => this.setCollapsible(collapse)), + this.variable('enrich', enrich => this.setEnrichment(enrich)), this.variable('inTabOrder', tab => this.setTabOrder(tab)), this.variable('assistiveMml', mml => this.setAssistiveMml(mml)) ], @@ -514,6 +530,7 @@ export class Menu { this.command('Original', 'Original Form', () => this.originalText.post()), this.rule(), this.command('Speech', 'Speech Text', () => this.speechText.post(), {disabled: true}), + this.command('Braille', 'Braille Code', () => this.brailleText.post(), {disabled: true}), this.command('SVG', 'SVG Image', () => this.postSvgImage(), {disabled: true}), this.submenu('ShowAnnotation', 'Annotation'), this.rule(), @@ -524,6 +541,7 @@ export class Menu { this.command('Original', 'Original Form', () => this.copyOriginal()), this.rule(), this.command('Speech', 'Speech Text', () => this.copySpeechText(), {disabled: true}), + this.command('Braille', 'Braille Code', () => this.copyBrailleText(), {disabled: true}), this.command('SVG', 'SVG Image', () => this.copySvgImage(), {disabled: true}), this.submenu('CopyAnnotation', 'Annotation'), this.rule(), @@ -546,6 +564,7 @@ export class Menu { this.checkbox('texHints', 'TeX hints', 'texHints'), this.checkbox('semantics', 'Original as annotation', 'semantics') ]), + this.submenu('Language', 'Language'), this.rule(), this.submenu('ZoomTrigger', 'Zoom Trigger', [ this.command('ZoomNow', 'Zoom Once Now', () => this.zoom(null, '', this.menu.mathItem)), @@ -568,30 +587,37 @@ export class Menu { this.rule(), this.command('Reset', 'Reset to defaults', () => this.resetDefaults()) ]), - this.submenu('Accessibility', 'Accessibility', [ - this.checkbox('Activate', 'Activate', 'explorer'), - this.submenu('Speech', 'Speech', [ - this.checkbox('Speech', 'Speech Output', 'speech'), - this.checkbox('Subtitles', 'Speech Subtitles', 'subtitles'), - this.checkbox('Auto Voicing', 'Auto Voicing', 'voicing'), - this.checkbox('Braille', 'Braille Output', 'braille'), - this.checkbox('View Braille', 'Braille Subtitles', 'viewBraille'), - this.rule(), - this.submenu('A11yLanguage', 'Language'), - this.rule(), - this.submenu('Mathspeak', 'Mathspeak Rules', this.radioGroup('speechRules', [ - ['mathspeak-default', 'Verbose'], - ['mathspeak-brief', 'Brief'], - ['mathspeak-sbrief', 'Superbrief'] - ])), - this.submenu('Clearspeak', 'Clearspeak Rules', this.radioGroup('speechRules', [ - ['clearspeak-default', 'Auto'] - ])), - this.submenu('ChromeVox', 'ChromeVox Rules', this.radioGroup('speechRules', [ - ['chromevox-default', 'Standard'], - ['chromevox-alternative', 'Alternative'] - ])) - ]), + this.rule(), + this.label('Accessibility', '\xA0\xA0 Accessibility:'), + this.submenu('Speech', '\xA0 \xA0 Speech', [ + this.checkbox('Generate', 'Generate', 'speech'), + this.checkbox('Subtitles', 'Show Subtitles', 'subtitles'), + this.checkbox('Auto Voicing', 'Auto Voicing', 'voicing'), + this.rule(), + this.label('Rules', 'Rules:'), + this.submenu('Mathspeak', 'Mathspeak', this.radioGroup('speechRules', [ + ['mathspeak-default', 'Verbose'], + ['mathspeak-brief', 'Brief'], + ['mathspeak-sbrief', 'Superbrief'] + ])), + this.submenu('Clearspeak', 'Clearspeak', this.radioGroup('speechRules', [ + ['clearspeak-default', 'Auto'] + ])), + this.submenu('ChromeVox', 'ChromeVox', this.radioGroup('speechRules', [ + ['chromevox-default', 'Standard'], + ['chromevox-alternative', 'Alternative'] + ])), + this.rule(), + this.submenu('A11yLanguage', 'Language') + ]), + this.submenu('Braille', '\xA0 \xA0 Braille', [ + this.checkbox('Generate', 'Generate', 'braille'), + this.checkbox('Subtitles', 'Show Subtitles', 'viewBraille'), + this.rule(), + this.label('Code', 'Code Format:'), + this.radioGroup('brailleCode', [['nemeth', 'Nemeth'], ['ueb', 'UEB'], ['euro', 'Euro']]) + ]), + this.submenu('Explorer', '\xA0 \xA0 Explorer', [ this.submenu('Highlight', 'Highlight', [ this.submenu('Background', 'Background', this.radioGroup('backgroundColor', [ ['Blue'], ['Red'], ['Green'], ['Yellow'], ['Cyan'], ['Magenta'], ['White'], ['Black'] @@ -627,23 +653,47 @@ export class Menu { this.checkbox('Type', 'Type', 'infoType'), this.checkbox('Role', 'Role', 'infoRole'), this.checkbox('Prefix', 'Prefix', 'infoPrefix') - ], true), - this.rule(), + ], true) + ]), + this.submenu('Options', '\xA0 \xA0 Options', [ + this.checkbox('Enrich', 'Semantic Enrichment', 'enrich'), this.checkbox('Collapsible', 'Collapsible Math', 'collapsible'), this.checkbox('AutoCollapse', 'Auto Collapse', 'autocollapse', {disabled: true}), this.rule(), this.checkbox('InTabOrder', 'Include in Tab Order', 'inTabOrder'), this.checkbox('AssistiveMml', 'Include Hidden MathML', 'assistiveMml') ]), - this.submenu('Language', 'Language'), this.rule(), this.command('About', 'About MathJax', () => this.about.post()), this.command('Help', 'MathJax Help', () => this.help.post()) ] }) as MJContextMenu; const menu = this.menu; + menu.settings = this.settings; menu.findID('Settings', 'Overflow', 'Elide').disable(); + menu.findID('Braille', 'ueb').hide(); menu.setJax(this.jax); + this.attachDialogMenus(menu); + this.checkLoadableItems(); + this.enableAccessibilityItems('Speech', this.settings.speech); + this.enableAccessibilityItems('Braille', this.settings.braille); + this.setAccessibilityMenus(); + const cache: [string, string][] = []; + MJContextMenu.DynamicSubmenus.set( + 'ShowAnnotation', + [AnnotationMenu.showAnnotations( + this.annotationBox, this.options.annotationTypes, cache), '']); + MJContextMenu.DynamicSubmenus.set( + 'CopyAnnotation', + [AnnotationMenu.copyAnnotations(cache), '']); + CssStyles.addInfoStyles(this.document.document as any); + CssStyles.addMenuStyles(this.document.document as any); + } + + /** + * @param {MJContextMenu} menu The menu to attach + */ + protected attachDialogMenus(menu: MJContextMenu) { this.about.attachMenu(menu); this.help.attachMenu(menu); this.originalText.attachMenu(menu); @@ -651,20 +701,9 @@ export class Menu { this.originalText.attachMenu(menu); this.svgImage.attachMenu(menu); this.speechText.attachMenu(menu); + this.brailleText.attachMenu(menu); this.errorMessage.attachMenu(menu); this.zoomBox.attachMenu(menu); - this.checkLoadableItems(); - this.enableExplorerItems(this.settings.explorer); - const cache: [string, string][] = []; - MJContextMenu.DynamicSubmenus.set( - 'ShowAnnotation', - AnnotationMenu.showAnnotations( - this.annotationBox, this.options.annotationTypes, cache)); - MJContextMenu.DynamicSubmenus.set( - 'CopyAnnotation', - AnnotationMenu.copyAnnotations(cache)); - CssStyles.addInfoStyles(this.document.document as any); - CssStyles.addMenuStyles(this.document.document as any); } /** @@ -675,13 +714,11 @@ export class Menu { */ protected checkLoadableItems() { if (MathJax && MathJax._ && MathJax.loader && MathJax.startup) { - if (this.settings.collapsible && (!MathJax._.a11y || !MathJax._.a11y.complexity)) { - this.loadA11y('complexity'); - } - if (this.settings.explorer && (!MathJax._.a11y || !MathJax._.a11y.explorer)) { + if ((this.settings.enrich || this.settings.collapsible || this.settings.speech || this.settings.braille) && + (!MathJax._?.a11y?.['semantic-enrich'])) { this.loadA11y('explorer'); } - if (this.settings.assistiveMml && (!MathJax._.a11y || !MathJax._.a11y['assistive-mml'])) { + if (this.settings.assistiveMml && !MathJax._?.a11y?.['assistive-mml']) { this.loadA11y('assistive-mml'); } } else { @@ -691,22 +728,26 @@ export class Menu { menu.findID('Settings', 'Renderer', name).disable(); } } - menu.findID('Accessibility', 'Activate').disable(); - menu.findID('Accessibility', 'AutoCollapse').disable(); - menu.findID('Accessibility', 'Collapsible').disable(); + menu.findID('Speech').disable(); + menu.findID('Braille').disable(); + menu.findID('Explorer').disable(); + menu.findID('Options', 'AutoCollapse').disable(); + menu.findID('Options', 'Collapsible').disable(); + menu.findID('Options', 'Enrich').disable(); + menu.findID('Options', 'AssistiveMml').disable(); } } /** - * Enable/disable the Explorer submenu items + * Enable/disable an assistive submenu's items * * @param {boolean} enable True to enable, false to disable */ - protected enableExplorerItems(enable: boolean) { - const menu = (this.menu.findID('Accessibility', 'Activate') as Submenu).menu; + protected enableAccessibilityItems(name: string, enable: boolean) { + const menu = (this.menu.findID(name) as Submenu).submenu; for (const item of menu.items.slice(1)) { - if (item instanceof Rule) break; - enable ? item.enable() : item.disable(); + if (item instanceof Rule) continue; + enable && (!(item instanceof Submenu) || item.submenu.items.length) ? item.enable() : item.disable(); } } @@ -753,7 +794,7 @@ export class Menu { * @param {{[key: string]: any}} options The options. */ protected setA11y(options: {[key: string]: any}) { - if (MathJax._.a11y && MathJax._.a11y.explorer) { + if (MathJax._?.a11y?.explorer) { MathJax._.a11y.explorer_ts.setA11yOptions(this.document, options); } } @@ -764,7 +805,7 @@ export class Menu { * @return {any} The value of the option */ protected getA11y(option: string): any { - if (MathJax._.a11y && MathJax._.a11y.explorer) { + if (MathJax._?.a11y?.explorer) { if (this.document.options.a11y[option] !== undefined) { return this.document.options.a11y[option]; } @@ -780,16 +821,27 @@ export class Menu { */ protected applySettings() { this.setTabOrder(this.settings.inTabOrder); - this.document.options.enableAssistiveMml = this.settings.assistiveMml; + const options = this.document.options; + options.enableAssistiveMml = this.settings.assistiveMml; + options.enableSpeech = this.settings.speech; + options.enableBraille = this.settings.braille; + options.enableExplorer = this.settings.enrich; const renderer = this.settings.renderer.replace(/[^a-zA-Z0-9]/g, '') || 'CHTML'; - const promise = (renderer !== this.defaultSettings.renderer ? - this.setRenderer(renderer, false) : - Promise.resolve()); + const promise = (Menu._loadingPromise || Promise.resolve()).then( + () => (renderer !== this.defaultSettings.renderer ? + this.setRenderer(renderer, false) : + Promise.resolve()) + ); promise.then(() => { - this.document.options.enableExplorer = this.settings.explorer; - this.document.outputJax.options.scale = parseFloat(this.settings.scale); - this.document.outputJax.options.displayOverflow = this.settings.overflow.toLowerCase(); - this.document.outputJax.options.linebreaks.inline = this.settings.breakInline; + const settings = this.settings; + const options = this.document.outputJax.options; + options.scale = parseFloat(settings.scale); + options.displayOverflow = settings.overflow.toLowerCase(); + options.linebreaks.inline = settings.breakInline; + if (!settings.speechRules) { + const sre = this.document.options.sre; + settings.speechRules = `${sre.domain || 'mathspeak'}-${sre.style || 'default'}`; + } }); } @@ -874,7 +926,7 @@ export class Menu { */ protected setAssistiveMml(mml: boolean) { this.document.options.enableAssistiveMml = mml; - if (!mml || (MathJax._.a11y && MathJax._.a11y['assistive-mml'])) { + if (!mml || MathJax._?.a11y?.['assistive-mml']) { this.rerender(); } else { this.loadA11y('assistive-mml'); @@ -882,13 +934,68 @@ export class Menu { } /** - * @param {boolean} explore True to enable the explorer, false to not + * Enable/disable assistive menus based on enrichment setting */ - protected setExplorer(explore: boolean) { - this.enableExplorerItems(explore); - this.document.options.enableExplorer = explore; - if (!explore || (MathJax._.a11y && MathJax._.a11y.explorer)) { - this.rerender(this.settings.collapsible ? STATE.RERENDER : STATE.COMPILED); + protected setAccessibilityMenus() { + const enable = this.settings.enrich; + const method = (enable ? 'enable' : 'disable'); + ['Speech', 'Braille', 'Explorer'].forEach(id => this.menu.findID(id)[method]()); + if (!enable) { + this.settings.collapsible = false; + this.document.options.enableCollapsible = false; + } + } + + /** + * @param {boolean} speech True to enable speech, false to not + */ + protected setSpeech(speech: boolean) { + this.enableAccessibilityItems('Speech', speech); + this.document.options.enableSpeech = speech; + if (!speech || MathJax._?.a11y?.['semantic-enrich']) { + this.rerender(STATE.COMPILED); + } else { + this.loadA11y('explorer'); + } + } + + /** + * @param {boolean} braille True to enable braille, false to not + */ + protected setBraille(braille: boolean) { + this.enableAccessibilityItems('Braille', braille); + this.document.options.enableBraille = braille; + if (!braille || MathJax._?.a11y?.['semantic-enrich']) { + this.rerender(STATE.COMPILED); + } else { + this.loadA11y('explorer'); + } + } + + /** + * @param {string} code The Braille code format (nemeth or euro) + */ + protected setBrailleCode(code: string) { + this.document.options.sre.braille = code; + this.rerender(STATE.COMPILED); + } + + /** + * @param {string} locale The speech locale + */ + protected setLocale(locale: string) { + this.document.options.sre.locale = locale; + this.rerender(STATE.COMPILED); + } + + /** + * @param {boolean} enrich True to enable enriched math, false to not + */ + protected setEnrichment(enrich: boolean) { + this.document.options.enableEnrichment = this.document.options.enableExplorer = enrich; + this.setAccessibilityMenus(); + if (!enrich || MathJax._?.a11y?.['semantic-enrich']) { + this.rerender(STATE.COMPILED); } else { this.loadA11y('explorer'); } @@ -899,10 +1006,17 @@ export class Menu { */ protected setCollapsible(collapse: boolean) { this.document.options.enableComplexity = collapse; - if (!collapse || (MathJax._.a11y && MathJax._.a11y.complexity)) { + if (collapse && !this.settings.enrich) { + this.settings.enrich = true; + this.setEnrichment(true); + } + if (!collapse || MathJax._?.a11y?.complexity) { this.rerender(STATE.COMPILED); } else { this.loadA11y('complexity'); + if (!MathJax._?.a11y?.explorer) { + this.loadA11y('explorer'); + } } } @@ -1015,6 +1129,7 @@ export class Menu { const document = this.document; this.document = startup.document = startup.getDocument(); this.document.menu = this; + this.setA11y(this.settings); this.document.outputJax.reset(); this.transferMathList(document); this.document.processed = document.processed; @@ -1027,7 +1142,6 @@ export class Menu { }); } - /** * @param {MenuMathDocument} document The original document whose list is to be transferred */ @@ -1088,7 +1202,7 @@ export class Menu { * @param {boolean} breaks True if there are inline breaks * @returns {Promise} A promise returning the serialized SVG */ - protected typesetSVG(math: HTMLMATHITEM, cache: string, breaks: boolean): Promise { + protected async typesetSVG(math: HTMLMATHITEM, cache: string, breaks: boolean): Promise { const jax = this.jax.SVG as SVG; const div = jax.html('div'); if (cache === 'global') { @@ -1111,7 +1225,7 @@ export class Menu { math.root = root; jax.options.fontCache = cache; return this.formatSvg(jax.adaptor.innerHTML(div)); - }) + }); } /** @@ -1220,6 +1334,13 @@ export class Menu { MenuUtil.copyToClipboard(this.menu.mathItem.outputData.speech); } + /** + * Copy the speech text to the clipboard + */ + protected copyBrailleText() { + MenuUtil.copyToClipboard(this.menu.mathItem.outputData.braille); + } + /** * Copy the error message to the clipboard */ @@ -1285,9 +1406,7 @@ export class Menu { getter: () => this.getA11y(name), setter: (value: T) => { (this.settings as any)[name] = value; - let options: {[key: string]: any} = {}; - options[name] = value; - this.setA11y(options); + this.setA11y({[name]: value}); action && action(value); this.saveUserSettings(); } diff --git a/ts/ui/menu/MenuHandler.ts b/ts/ui/menu/MenuHandler.ts index d811a2f23..384702c94 100644 --- a/ts/ui/menu/MenuHandler.ts +++ b/ts/ui/menu/MenuHandler.ts @@ -172,6 +172,8 @@ export function MenuMathDocumentMixin( // enableEnrichment: true, enableComplexity: true, + enableSpeech: true, + enableBraille: true, enableExplorer: true, enrichSpeech: 'none', enrichError: (_doc: MenuMathDocument, _math: MenuMathItem, err: Error) => @@ -203,11 +205,20 @@ export function MenuMathDocumentMixin( constructor(...args: any[]) { super(...args); this.menu = new this.options.MenuClass(this, this.options.menuOptions); + const ProcessBits = (this.constructor as typeof BaseDocument).ProcessBits; if (!ProcessBits.has('context-menu')) { ProcessBits.allocate('context-menu'); } this.options.MathItem = MenuMathItemMixin(this.options.MathItem); + + const settings = this.menu.settings; + const options = this.options; + const enrich = options.enableEnrichment = settings.enrich; + options.enableSpeech = settings.speech && enrich; + options.enableBraille = settings.braille && enrich; + options.enableComplexity = settings.collapsible && enrich; + options.enableExplorer = enrich; } /** @@ -234,14 +245,10 @@ export function MenuMathDocumentMixin( if (this.menu.isLoading) { mathjax.retryAfter(this.menu.loadingPromise.catch((err) => console.log(err))); } - const settings = this.menu.settings; - if (settings.collapsible) { - this.options.enableComplexity = true; + if (this.options.enableComplexity) { this.menu.checkComponent('a11y/complexity'); } - if (settings.explorer) { - this.options.enableEnrichment = true; - this.options.enableExplorer = true; + if (this.options.enableExplorer) { this.menu.checkComponent('a11y/explorer'); } return this;