123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- /*************************************************************
- *
- * Copyright (c) 2018-2022 The MathJax Consortium
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- /**
- * @fileoverview Mixin that implements the Explorer
- *
- * @author dpvc@mathjax.org (Davide Cervone)
- */
- import {Handler} from '../core/Handler.js';
- import {MmlNode} from '../core/MmlTree/MmlNode.js';
- import {MathML} from '../input/mathml.js';
- import {STATE, newState} from '../core/MathItem.js';
- import {EnrichedMathItem, EnrichedMathDocument, EnrichHandler} from './semantic-enrich.js';
- import {MathDocumentConstructor} from '../core/MathDocument.js';
- import {OptionList, expandable} from '../util/Options.js';
- import {SerializedMmlVisitor} from '../core/MmlTree/SerializedMmlVisitor.js';
- import {MJContextMenu} from '../ui/menu/MJContextMenu.js';
- import {Explorer} from './explorer/Explorer.js';
- import * as ke from './explorer/KeyExplorer.js';
- import * as me from './explorer/MouseExplorer.js';
- import {TreeColorer, FlameColorer} from './explorer/TreeExplorer.js';
- import {LiveRegion, ToolTip, HoverRegion} from './explorer/Region.js';
- import {Submenu} from 'mj-context-menu/js/item_submenu.js';
- import Sre from './sre.js';
- /**
- * Generic constructor for Mixins
- */
- export type Constructor<T> = new(...args: any[]) => T;
- /**
- * Shorthands for types with HTMLElement, Text, and Document instead of generics
- */
- export type HANDLER = Handler<HTMLElement, Text, Document>;
- export type HTMLDOCUMENT = EnrichedMathDocument<HTMLElement, Text, Document>;
- export type HTMLMATHITEM = EnrichedMathItem<HTMLElement, Text, Document>;
- export type MATHML = MathML<HTMLElement, Text, Document>;
- /*==========================================================================*/
- /**
- * Add STATE value for having the Explorer added (after TYPESET and before INSERTED or CONTEXT_MENU)
- */
- newState('EXPLORER', 160);
- /**
- * The properties added to MathItem for the Explorer
- */
- export interface ExplorerMathItem extends HTMLMATHITEM {
- /**
- * @param {HTMLDocument} document The document where the Explorer is being added
- * @param {boolean} force True to force the explorer even if enableExplorer is false
- */
- explorable(document: HTMLDOCUMENT, force?: boolean): void;
- /**
- * @param {HTMLDocument} document The document where the Explorer is being added
- */
- attachExplorers(document: HTMLDOCUMENT): void;
- }
- /**
- * The mixin for adding the Explorer to MathItems
- *
- * @param {B} BaseMathItem The MathItem class to be extended
- * @param {Function} toMathML The function to serialize the internal MathML
- * @returns {ExplorerMathItem} The Explorer MathItem class
- *
- * @template B The MathItem class to extend
- */
- export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
- BaseMathItem: B,
- toMathML: (node: MmlNode) => string
- ): Constructor<ExplorerMathItem> & B {
- return class extends BaseMathItem {
- /**
- * The Explorer objects for this math item
- */
- protected explorers: {[key: string]: Explorer} = {};
- /**
- * The currently attached explorers
- */
- protected attached: string[] = [];
- /**
- * True when a rerendered element should restart these explorers
- */
- protected restart: string[] = [];
- /**
- * True when a rerendered element should regain the focus
- */
- protected refocus: boolean = false;
- /**
- * Save explorer id during rerendering.
- */
- protected savedId: string = null;
- /**
- * Add the explorer to the output for this math item
- *
- * @param {HTMLDocument} document The MathDocument for the MathItem
- * @param {boolean} force True to force the explorer even if enableExplorer is false
- */
- public explorable(document: ExplorerMathDocument, force: boolean = false) {
- if (this.state() >= STATE.EXPLORER) return;
- if (!this.isEscaped && (document.options.enableExplorer || force)) {
- const node = this.typesetRoot;
- const mml = toMathML(this.root);
- if (this.savedId) {
- this.typesetRoot.setAttribute('sre-explorer-id', this.savedId);
- this.savedId = null;
- }
- // Init explorers:
- this.explorers = initExplorers(document, node, mml);
- this.attachExplorers(document);
- }
- this.state(STATE.EXPLORER);
- }
- /**
- * Attaches the explorers that are currently meant to be active given
- * the document options. Detaches all others.
- * @param {ExplorerMathDocument} document The current document.
- */
- public attachExplorers(document: ExplorerMathDocument) {
- this.attached = [];
- let keyExplorers = [];
- for (let key of Object.keys(this.explorers)) {
- let explorer = this.explorers[key];
- if (explorer instanceof ke.AbstractKeyExplorer) {
- explorer.AddEvents();
- explorer.stoppable = false;
- keyExplorers.unshift(explorer);
- }
- if (document.options.a11y[key]) {
- explorer.Attach();
- this.attached.push(key);
- } else {
- explorer.Detach();
- }
- }
- // Ensure that the last currently attached key explorer stops propagating
- // key events.
- for (let explorer of keyExplorers) {
- if (explorer.attached) {
- explorer.stoppable = true;
- break;
- }
- }
- }
- /**
- * @override
- */
- public rerender(document: ExplorerMathDocument, start: number = STATE.RERENDER) {
- this.savedId = this.typesetRoot.getAttribute('sre-explorer-id');
- this.refocus = (window.document.activeElement === this.typesetRoot);
- for (let key of this.attached) {
- let explorer = this.explorers[key];
- if (explorer.active) {
- this.restart.push(key);
- explorer.Stop();
- }
- }
- super.rerender(document, start);
- }
- /**
- * @override
- */
- public updateDocument(document: ExplorerMathDocument) {
- super.updateDocument(document);
- this.refocus && this.typesetRoot.focus();
- this.restart.forEach(x => this.explorers[x].Start());
- this.restart = [];
- this.refocus = false;
- }
- };
- }
- /**
- * The functions added to MathDocument for the Explorer
- */
- export interface ExplorerMathDocument extends HTMLDOCUMENT {
- /**
- * The objects needed for the explorer
- */
- explorerRegions: ExplorerRegions;
- /**
- * Add the Explorer to the MathItems in the MathDocument
- *
- * @returns {MathDocument} The MathDocument (so calls can be chained)
- */
- explorable(): HTMLDOCUMENT;
- }
- /**
- * The mixin for adding the Explorer to MathDocuments
- *
- * @param {B} BaseDocument The MathDocument class to be extended
- * @returns {ExplorerMathDocument} The extended MathDocument class
- */
- export function ExplorerMathDocumentMixin<B extends MathDocumentConstructor<HTMLDOCUMENT>>(
- BaseDocument: B
- ): MathDocumentConstructor<ExplorerMathDocument> & B {
- return class extends BaseDocument {
- /**
- * @override
- */
- public static OPTIONS: OptionList = {
- ...BaseDocument.OPTIONS,
- enableExplorer: true,
- renderActions: expandable({
- ...BaseDocument.OPTIONS.renderActions,
- explorable: [STATE.EXPLORER]
- }),
- sre: expandable({
- ...BaseDocument.OPTIONS.sre,
- speech: 'shallow', // overrides option in EnrichedMathDocument
- }),
- a11y: {
- align: 'top', // placement of magnified expression
- backgroundColor: 'Blue', // color for background of selected sub-expression
- backgroundOpacity: 20, // opacity for background of selected sub-expression
- braille: false, // switch on Braille output
- flame: false, // color collapsible sub-expressions
- foregroundColor: 'Black', // color to use for text of selected sub-expression
- foregroundOpacity: 100, // opacity for text of selected sub-expression
- highlight: 'None', // type of highlighting for collapsible sub-expressions
- hover: false, // show collapsible sub-expression on mouse hovering
- infoPrefix: false, // show speech prefixes on mouse hovering
- infoRole: false, // show semantic role on mouse hovering
- infoType: false, // show semantic type on mouse hovering
- keyMagnifier: false, // switch on magnification via key exploration
- magnification: 'None', // type of magnification
- magnify: '400%', // percentage of magnification of zoomed expressions
- mouseMagnifier: false, // switch on magnification via mouse hovering
- speech: true, // switch on speech output
- subtitles: true, // show speech as a subtitle
- treeColoring: false, // tree color expression
- viewBraille: false // display Braille output as subtitles
- }
- };
- /**
- * The objects needed for the explorer
- */
- public explorerRegions: ExplorerRegions;
- /**
- * Extend the MathItem class used for this MathDocument
- * and create the visitor and explorer objects needed for the explorer
- *
- * @override
- * @constructor
- */
- constructor(...args: any[]) {
- super(...args);
- const ProcessBits = (this.constructor as typeof BaseDocument).ProcessBits;
- if (!ProcessBits.has('explorer')) {
- ProcessBits.allocate('explorer');
- }
- const visitor = new SerializedMmlVisitor(this.mmlFactory);
- const toMathML = ((node: MmlNode) => visitor.visitTree(node));
- this.options.MathItem = ExplorerMathItemMixin(this.options.MathItem, toMathML);
- // TODO: set backward compatibility options here.
- this.explorerRegions = initExplorerRegions(this);
- }
- /**
- * Add the Explorer to the MathItems in this MathDocument
- *
- * @return {ExplorerMathDocument} The MathDocument (so calls can be chained)
- */
- public explorable(): ExplorerMathDocument {
- if (!this.processed.isSet('explorer')) {
- if (this.options.enableExplorer) {
- for (const math of this.math) {
- (math as ExplorerMathItem).explorable(this);
- }
- }
- this.processed.set('explorer');
- }
- return this;
- }
- /**
- * @override
- */
- public state(state: number, restore: boolean = false) {
- super.state(state, restore);
- if (state < STATE.EXPLORER) {
- this.processed.clear('explorer');
- }
- return this;
- }
- };
- }
- /*==========================================================================*/
- /**
- * Add Explorer functions to a Handler instance
- *
- * @param {Handler} handler The Handler instance to enhance
- * @param {MathML} MmlJax A MathML input jax to be used for the semantic enrichment
- * @returns {Handler} The handler that was modified (for purposes of chainging extensions)
- */
- export function ExplorerHandler(handler: HANDLER, MmlJax: MATHML = null): HANDLER {
- if (!handler.documentClass.prototype.enrich && MmlJax) {
- handler = EnrichHandler(handler, MmlJax);
- }
- handler.documentClass = ExplorerMathDocumentMixin(handler.documentClass as any);
- return handler;
- }
- /*==========================================================================*/
- /**
- * The regions objects needed for the explorers.
- */
- export type ExplorerRegions = {
- speechRegion?: LiveRegion,
- brailleRegion?: LiveRegion,
- magnifier?: HoverRegion,
- tooltip1?: ToolTip,
- tooltip2?: ToolTip,
- tooltip3?: ToolTip
- };
- /**
- * Initializes the regions needed for a document.
- * @param {ExplorerMathDocument} document The current document.
- */
- function initExplorerRegions(document: ExplorerMathDocument) {
- return {
- speechRegion: new LiveRegion(document),
- brailleRegion: new LiveRegion(document),
- magnifier: new HoverRegion(document),
- tooltip1: new ToolTip(document),
- tooltip2: new ToolTip(document),
- tooltip3: new ToolTip(document)
- };
- }
- /**
- * Type of explorer initialization methods.
- * @type {(ExplorerMathDocument, HTMLElement, any[]): Explorer}
- */
- type ExplorerInit = (doc: ExplorerMathDocument,
- node: HTMLElement, ...rest: any[]) => Explorer;
- /**
- * Generation methods for all MathJax explorers available via option settings.
- */
- let allExplorers: {[options: string]: ExplorerInit} = {
- speech: (doc: ExplorerMathDocument, node: HTMLElement, ...rest: any[]) => {
- let explorer = ke.SpeechExplorer.create(
- doc, doc.explorerRegions.speechRegion, node, ...rest) as ke.SpeechExplorer;
- explorer.speechGenerator.setOptions({
- locale: doc.options.sre.locale, domain: doc.options.sre.domain,
- style: doc.options.sre.style, modality: 'speech'});
- // This weeds out the case of providing a non-existent locale option.
- let locale = explorer.speechGenerator.getOptions().locale;
- if (locale !== Sre.engineSetup().locale) {
- doc.options.sre.locale = Sre.engineSetup().locale;
- explorer.speechGenerator.setOptions({locale: doc.options.sre.locale});
- }
- explorer.showRegion = 'subtitles';
- return explorer;
- },
- braille: (doc: ExplorerMathDocument, node: HTMLElement, ...rest: any[]) => {
- let explorer = ke.SpeechExplorer.create(
- doc, doc.explorerRegions.brailleRegion, node, ...rest) as ke.SpeechExplorer;
- explorer.speechGenerator.setOptions({locale: 'nemeth', domain: 'default',
- style: 'default', modality: 'braille'});
- explorer.showRegion = 'viewBraille';
- return explorer;
- },
- keyMagnifier: (doc: ExplorerMathDocument, node: HTMLElement, ...rest: any[]) =>
- ke.Magnifier.create(doc, doc.explorerRegions.magnifier, node, ...rest),
- mouseMagnifier: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- me.ContentHoverer.create(doc, doc.explorerRegions.magnifier, node,
- (x: HTMLElement) => x.hasAttribute('data-semantic-type'),
- (x: HTMLElement) => x),
- hover: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- me.FlameHoverer.create(doc, null, node),
- infoType: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- me.ValueHoverer.create(doc, doc.explorerRegions.tooltip1, node,
- (x: HTMLElement) => x.hasAttribute('data-semantic-type'),
- (x: HTMLElement) => x.getAttribute('data-semantic-type')),
- infoRole: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- me.ValueHoverer.create(doc, doc.explorerRegions.tooltip2, node,
- (x: HTMLElement) => x.hasAttribute('data-semantic-role'),
- (x: HTMLElement) => x.getAttribute('data-semantic-role')),
- infoPrefix: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- me.ValueHoverer.create(doc, doc.explorerRegions.tooltip3, node,
- (x: HTMLElement) => x.hasAttribute('data-semantic-prefix'),
- (x: HTMLElement) => x.getAttribute('data-semantic-prefix')),
- flame: (doc: ExplorerMathDocument, node: HTMLElement, ..._rest: any[]) =>
- FlameColorer.create(doc, null, node),
- treeColoring: (doc: ExplorerMathDocument, node: HTMLElement, ...rest: any[]) =>
- TreeColorer.create(doc, null, node, ...rest)
- };
- /**
- * Initialises explorers for a document.
- * @param {ExplorerMathDocument} document The target document.
- * @param {HTMLElement} node The node explorers will be attached to.
- * @param {string} mml The corresponding Mathml node as a string.
- * @return {Explorer[]} A list of initialised explorers.
- */
- function initExplorers(document: ExplorerMathDocument, node: HTMLElement, mml: string): {[key: string]: Explorer} {
- let explorers: {[key: string]: Explorer} = {};
- for (let key of Object.keys(allExplorers)) {
- explorers[key] = allExplorers[key](document, node, mml);
- }
- return explorers;
- }
- /* Context Menu Interactions */
- /**
- * Sets a list of a11y options for a given document.
- * @param {HTMLDOCUMENT} document The current document.
- * @param {{[key: string]: any}} options Association list for a11y option value pairs.
- */
- export function setA11yOptions(document: HTMLDOCUMENT, options: {[key: string]: any}) {
- let sreOptions = Sre.engineSetup() as {[name: string]: string};
- for (let key in options) {
- if (document.options.a11y[key] !== undefined) {
- setA11yOption(document, key, options[key]);
- if (key === 'locale') {
- document.options.sre[key] = options[key];
- }
- continue;
- }
- if (sreOptions[key] !== undefined) {
- document.options.sre[key] = options[key];
- }
- }
- // Reinit explorers
- for (let item of document.math) {
- (item as ExplorerMathItem).attachExplorers(document as ExplorerMathDocument);
- }
- }
- /**
- * Sets a single a11y option for a menu name.
- * @param {HTMLDOCUMENT} document The current document.
- * @param {string} option The option name in the menu.
- * @param {string|boolean} value The new value.
- */
- export function setA11yOption(document: HTMLDOCUMENT, option: string, value: string | boolean) {
- switch (option) {
- case 'magnification':
- switch (value) {
- case 'None':
- document.options.a11y.magnification = value;
- document.options.a11y.keyMagnifier = false;
- document.options.a11y.mouseMagnifier = false;
- break;
- case 'Keyboard':
- document.options.a11y.magnification = value;
- document.options.a11y.keyMagnifier = true;
- document.options.a11y.mouseMagnifier = false;
- break;
- case 'Mouse':
- document.options.a11y.magnification = value;
- document.options.a11y.keyMagnifier = false;
- document.options.a11y.mouseMagnifier = true;
- break;
- }
- break;
- case 'highlight':
- switch (value) {
- case 'None':
- document.options.a11y.highlight = value;
- document.options.a11y.hover = false;
- document.options.a11y.flame = false;
- break;
- case 'Hover':
- document.options.a11y.highlight = value;
- document.options.a11y.hover = true;
- document.options.a11y.flame = false;
- break;
- case 'Flame':
- document.options.a11y.highlight = value;
- document.options.a11y.hover = false;
- document.options.a11y.flame = true;
- break;
- }
- break;
- default:
- document.options.a11y[option] = value;
- }
- }
- /**
- * Values for the ClearSpeak preference variables.
- */
- let csPrefsSetting: {[pref: string]: string} = {};
- /**
- * Generator of all variables for the Clearspeak Preference settings.
- * @param {MJContextMenu} menu The current context menu.
- * @param {string[]} prefs The preferences.
- */
- let csPrefsVariables = function(menu: MJContextMenu, prefs: string[]) {
- let srVariable = menu.pool.lookup('speechRules');
- for (let pref of prefs) {
- if (csPrefsSetting[pref]) continue;
- menu.factory.get('variable')(menu.factory, {
- name: 'csprf_' + pref,
- setter: (value: string) => {
- csPrefsSetting[pref] = value;
- srVariable.setValue(
- 'clearspeak-' +
- Sre.clearspeakPreferences.addPreference(
- Sre.clearspeakStyle(), pref, value)
- );
- },
- getter: () => { return csPrefsSetting[pref] || 'Auto'; }
- }, menu.pool);
- }
- };
- /**
- * Generate the selection box for the Clearspeak Preferences.
- * @param {MJContextMenu} menu The current context menu.
- * @param {string} locale The current locale.
- */
- let csSelectionBox = function(menu: MJContextMenu, locale: string) {
- let prefs = Sre.clearspeakPreferences.getLocalePreferences();
- let props = prefs[locale];
- if (!props) {
- let csEntry = menu.findID('Accessibility', 'Speech', 'Clearspeak');
- if (csEntry) {
- csEntry.disable();
- }
- return null;
- }
- csPrefsVariables(menu, Object.keys(props));
- let items = [];
- for (const prop of Object.getOwnPropertyNames(props)) {
- items.push({
- 'title': prop,
- 'values': props[prop].map(x => x.replace(RegExp('^' + prop + '_'), '')),
- 'variable': 'csprf_' + prop
- });
- }
- let sb = menu.factory.get('selectionBox')(menu.factory, {
- 'title': 'Clearspeak Preferences',
- 'signature': '',
- 'order': 'alphabetic',
- 'grid': 'square',
- 'selections': items
- }, menu);
- return {'type': 'command',
- 'id': 'ClearspeakPreferences',
- 'content': 'Select Preferences',
- 'action': () => sb.post(0, 0)};
- };
- /**
- * Creates dynamic clearspeak menu.
- * @param {MJContextMenu} menu The context menu.
- * @param {Submenu} sub The submenu to attach.
- */
- let csMenu = function(menu: MJContextMenu, sub: Submenu) {
- let locale = menu.pool.lookup('locale').getValue() as string;
- const box = csSelectionBox(menu, locale);
- let items: Object[] = [];
- try {
- items = Sre.clearspeakPreferences.smartPreferences(
- menu.mathItem, locale);
- } catch (e) {
- console.log(e);
- }
- if (box) {
- items.splice(2, 0, box);
- }
- return menu.factory.get('subMenu')(menu.factory, {
- items: items,
- id: 'Clearspeak'
- }, sub);
- };
- MJContextMenu.DynamicSubmenus.set('Clearspeak', csMenu);
- /**
- * Creates dynamic locale menu.
- * @param {MJContextMenu} menu The context menu.
- * @param {Submenu} sub The submenu to attach.
- */
- let language = function(menu: MJContextMenu, sub: Submenu) {
- let radios: {type: string, id: string,
- content: string, variable: string}[] = [];
- for (let lang of Sre.locales.keys()) {
- if (lang === 'nemeth') continue;
- radios.push({type: 'radio', id: lang,
- content: Sre.locales.get(lang) || lang, variable: 'locale'});
- }
- radios.sort((x, y) => x.content.localeCompare(y.content, 'en'));
- return menu.factory.get('subMenu')(menu.factory, {
- items: radios, id: 'Language'}, sub);
- };
- MJContextMenu.DynamicSubmenus.set('A11yLanguage', language);
|