Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: Rewrite of the Explorer to use ARIA trees #987

Closed
wants to merge 68 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
d9e9e70
Merge branch 'linebreaks' into v4.0-alpha
dpvc Sep 21, 2022
4742aad
Merge branch 'issue2902' into v4.0-alpha
dpvc Sep 21, 2022
6e1848e
Merge branch 'issue2924' into v4.0-alpha
dpvc Sep 21, 2022
263083b
Merge branch 'issue2899' into v4.0-alpha
dpvc Sep 21, 2022
f77376b
Merge branch 'issue2920' into v4.0-alpha
dpvc Sep 21, 2022
5b5a0af
Merge branch 'fix-stretchy-widths' into v4.0-alpha
dpvc Sep 21, 2022
e9933f9
Merge branch 'fix-entities' into v4.0-alpha
dpvc Sep 21, 2022
39b5bab
Merge branch 'new-default-font' into v4.0-alpha
dpvc Sep 21, 2022
97c44cb
Merge remote-tracking branch 'origin/explorer_rewrite' into v4.0-alpha
dpvc Sep 21, 2022
575d588
Very initial attempt at getting LaTeX into mml output.
zorkow Sep 22, 2022
1f6aee5
Switch to sre v4.1.0-beta.2
dpvc Sep 25, 2022
b759071
Change version to 4.0.0-alpha.1
dpvc Sep 25, 2022
9397e78
Remove -full configurations
dpvc Sep 25, 2022
0d04d6f
Turn off assistiveMML and add setup for explorer in contextual menu
dpvc Sep 26, 2022
6c1fde5
Make a utility for loading SRE, and use it in combined configurations
dpvc Sep 26, 2022
8c6d279
Don't need tex-chtml-full-speech, since speech is on for all components
dpvc Sep 26, 2022
d10f7dc
Allow semantic-enrich to enrich HTML-in-MML
dpvc Sep 26, 2022
0c6116b
Update to latest mathjax-modern font
dpvc Sep 27, 2022
d1a3331
Merge branch 'v4.0-alpha' into sre_latex_braille
zorkow Sep 28, 2022
85f5ecd
Include npm link commands in travis build process.
dpvc Oct 2, 2022
9d13709
Update npm api_key
dpvc Oct 2, 2022
8fcfa3e
Initial LaTeX attempt.
zorkow Oct 10, 2022
e10483b
Includes environments and closing braces.
zorkow Oct 11, 2022
36c87ed
Merge branch 'explorer_rewrite' into sre_latex_braille
zorkow Nov 15, 2022
aa3705e
Merge branch 'rewrite_dynamic_submenus' into sre_latex_braille
zorkow Nov 15, 2022
6f1b28e
Merge tag '4.0.0-alpha.1' into sre_latex_braille
zorkow Nov 15, 2022
a5bc9ae
Additional nesting.
zorkow Dec 1, 2022
f44e3de
Sub sup cases mostly working.
zorkow Dec 15, 2022
4002833
Adds the final subsup case.
zorkow Dec 20, 2022
f0c1d57
Initial attempt at fixing over items.
zorkow Dec 24, 2022
fed09f8
Changes Braille to Euro for the time being.
zorkow Feb 23, 2023
93e7f31
Merge branch 'develop' into sre_latex_braille_ext
zorkow Mar 26, 2023
b66db9c
Merge branch 'develop' into euro_braille_merge
zorkow Jun 19, 2023
3478ad8
Reinstates euro as primary braille.
zorkow Jun 19, 2023
ec1d0c7
Some new explorer experiments.
zorkow Jun 23, 2023
2f3947e
Merge branch 'develop' into explorer_rewrite_2
zorkow Jul 16, 2023
229121f
Updates chtml focus outline.
zorkow Jul 17, 2023
044e7ac
Adds corrected types for Walker.
zorkow Jul 17, 2023
a30c03d
Merge branch 'develop' into explorer_rewrite_2
zorkow Jul 25, 2023
e1dc71b
Walker cleanup.
zorkow Jul 25, 2023
448a9d5
Initial attempt at providing subtitles.
zorkow Jul 28, 2023
f0182f2
Refactors ssml computations to speech util.
zorkow Jul 30, 2023
75d2748
SSML extraction works.
zorkow Jul 31, 2023
322febf
Initial regions mainly working.
zorkow Jul 31, 2023
2ca47b4
Messing with regions.
zorkow Aug 1, 2023
bd729de
Merge branch 'sre_latex_braille_ext' into explorer_rewrite_euro
zorkow Aug 1, 2023
6b7747d
Remove as much as possible.
zorkow Aug 3, 2023
7dec3d7
Basics of new key explorer working.
zorkow Aug 4, 2023
337d4a8
Link triggering on click.
zorkow Aug 5, 2023
e1cfff0
Corrects keyboard triggering of links.
zorkow Aug 5, 2023
42eb5b2
Improved tabindex and focus handling.
zorkow Aug 5, 2023
cd5199d
Removes unused Walker.
zorkow Aug 10, 2023
7a27d32
Refactor set aria function.
zorkow Aug 11, 2023
94467c3
Refactoring speech computation to utilities.
zorkow Aug 11, 2023
7651510
Refactoring moves to class level methods.
zorkow Aug 12, 2023
44db969
Better setting of speech and braille.
zorkow Aug 12, 2023
39168e0
Key explorer cleanup.
zorkow Aug 13, 2023
023df73
Refactors generator to be attached to item.
zorkow Aug 13, 2023
f0f833d
Reinstantiates auto voicing.
zorkow Aug 13, 2023
80e83a7
Code cleanup.
zorkow Aug 14, 2023
335e6be
Merge branch 'develop' into sre_latex_braille_ext
zorkow Aug 14, 2023
cc801cb
Code cleanup and missing commenting.
zorkow Aug 14, 2023
9bc8367
Merge branch 'sre_latex_braille_ext' into explorer_rewrite_tmp
zorkow Aug 14, 2023
7927b02
Code cleanup.
zorkow Aug 14, 2023
026eb03
Starts explorer on focus as suggested by @pkra.
zorkow Sep 6, 2023
297d699
Merge branch 'develop' into explorer_rewrite_tmp
zorkow Dec 6, 2023
9cfc3fe
PR #987 incorporate review suggestions
zorkow Dec 6, 2023
30ebd5c
fixes the incorrect braille components
zorkow Dec 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions ts/a11y/SpeechUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*************************************************************
*
* Copyright (c) 2018-2023 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 Provides utility functions for speech handling.
*
* @author v.sorge@mathjax.org (Volker Sorge)
*/

import {MmlNode} from '../core/MmlTree/MmlNode.js';
zorkow marked this conversation as resolved.
Show resolved Hide resolved
import Sre from './sre.js';

const ProsodyKeys = [ 'pitch', 'rate', 'volume' ];

interface ProsodyElement {
[propName: string]: string | boolean | number;
pitch?: number;
rate?: number;
volume?: number;
}

export interface SsmlElement extends ProsodyElement {
pause?: string;
text?: string;
mark?: string;
character?: boolean;
kind?: string;
}

/**
* Parses a string containing an ssml structure into a list of text strings
* with associated ssml annotation elements.
*
* @param {string} speech The speech string.
* @return {[string, SsmlElement[]]} The annotation structure.
*/
export function ssmlParsing(speech: string): [string, SsmlElement[]] {
let xml = Sre.parseDOM(speech);
let instr: SsmlElement[] = [];
let text: String[] = [];
recurseSsml(Array.from(xml.childNodes), instr, text);
return [text.join(' '), instr];
}

/**
* Tail recursive combination of SSML components.
*
* @param {Node[]} nodes A list of SSML nodes.
* @param {SsmlElement[]} instr Accumulator for collating Ssml annotation
* elements.
* @param {String[]} text A list of text elements.
* @param {ProsodyElement?} prosody The currently active prosody elements.
*/
function recurseSsml(nodes: Node[], instr: SsmlElement[], text: String[],
prosody: ProsodyElement = {}) {
for (let node of nodes) {
if (node.nodeType === 3) {
let content = node.textContent.trim();
if (content) {
text.push(content);
instr.push(Object.assign({text: content}, prosody));
}
continue;
}
if (node.nodeType === 1) {
let element = node as Element;
let tag = element.tagName;
if (tag === 'speak') {
continue;
}
if (tag === 'prosody') {
recurseSsml(
Array.from(node.childNodes), instr, text,
getProsody(element, prosody));
continue;
}
switch (tag) {
case 'break':
instr.push({pause: element.getAttribute('time')});
break;
case 'mark':
instr.push({mark: element.getAttribute('name')});
break;
case 'say-as':
let txt = element.textContent;
instr.push(Object.assign({text: txt, character: true}, prosody));
text.push(txt);
break;
}
}
}
}

/**
* Maps prosody types to scaling functions.
*/
// TODO: These should be tweaked after more testing.
const combinePros: {[key: string]: (x: number, sign: string) => number} = {
pitch: (x: number, _sign: string) => 1 * (x / 100),
volume: (x: number, _sign: string) => .5 * (x / 100),
rate: (x: number, _sign: string) => 1 * (x / 100)
};

/**
* Retrieves prosody annotations from and SSML node.
* @param {Element} element The SSML node.
* @param {ProsodyElement} prosody The prosody annotation.
*/
function getProsody(element: Element, prosody: ProsodyElement) {
let combine: ProsodyElement = {};
for (let pros of ProsodyKeys) {
if (element.hasAttribute(pros)) {
let [sign, value] = extractProsody(element.getAttribute(pros));
if (!sign) {
// TODO: Sort out the base value. It is .5 for volume!
combine[pros] = (pros === 'volume') ? .5 : 1;
continue;
}
let orig = prosody[pros] as number;
orig = orig ? orig : ((pros === 'volume') ? .5 : 1);
zorkow marked this conversation as resolved.
Show resolved Hide resolved
let relative = combinePros[pros](parseInt(value, 10), sign);
combine[pros] = (sign === '-') ? orig - relative : orig + relative;
}
}
return combine;
}

/**
* Extracts the prosody value from an attribute.
*/
const prosodyRegexp = /([\+-]?)([0-9]+)%/;

/**
* Extracts the prosody value from an attribute.
* @param {string} attr
*/
function extractProsody(attr: string) {
let match = attr.match(prosodyRegexp);
if (!match) {
console.warn('Something went wrong with the prosody matching.');
return ['', '100'];
}
return [match[1], match[2]];
}

/**
* Computes the aria-label from the node.
* @param {MmlNode} node The Math element.
* @param {string=} sep The speech separator. Defaults to space.
*/
function getLabel(node: MmlNode, sep: string = ' ') {
const attributes = node.attributes;
const speech = attributes.getExplicit('data-semantic-speech') as string;
if (!speech) {
return '';
}
const label = [speech];
const prefix = attributes.getExplicit('data-semantic-prefix') as string;
if (prefix) {
label.unshift(prefix);
}
// TODO: check if we need this or if it is automatic by the screen readers.
const postfix = attributes.getExplicit('data-semantic-postfix') as string;
if (postfix) {
label.push(postfix);
}
// TODO: Do we need to merge wrt. locale in SRE.
return label.join(sep);
}

/**
* Builds speechs from SSML markup strings.
*
* @param {string} speech The speech string.
* @param {string=} locale An optional locale.
* @param {string=} rate The base speech rate.
* @return {[string, SsmlElement[]]} The speech with the ssml annotation structure
*/
export function buildSpeech(speech: string, locale: string = 'en',
rate: string = '100'): [string, SsmlElement[]] {
return ssmlParsing('<?xml version="1.0"?><speak version="1.1"' +
' xmlns="http://www.w3.org/2001/10/synthesis"' +
` xml:lang="${locale}">` +
`<prosody rate="${rate}%">${speech}`+
'</prosody></speak>');
}

/**
* Retrieve and sets aria and braille labels recursively.
* @param {MmlNode} node The root node to search from.
*/
export function setAria(node: MmlNode, locale: string) {
const attributes = node.attributes;
if (!attributes) return;
const speech = getLabel(node);
if (speech) {
attributes.set('aria-label', buildSpeech(speech, locale)[0]);
}
const braille = node.attributes.getExplicit('data-semantic-braille') as string;
if (braille) {
attributes.set('aria-braillelabel', braille);
}
for (let child of node.childNodes) {
setAria(child, locale);
}
}

/**
* Creates a honking sound.
*/
export function honk() {
let ac = new AudioContext();
let os = ac.createOscillator();
os.frequency.value = 300;
os.connect(ac.destination);
os.start(ac.currentTime);
os.stop(ac.currentTime + .05);
}
7 changes: 5 additions & 2 deletions ts/a11y/explorer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function ExplorerMathItemMixin<B extends Constructor<HTMLMATHITEM>>(
if (!this.explorers) {
this.explorers = new ExplorerPool();
}
this.explorers.init(document, node, mml);
this.explorers.init(document, node, mml, this);
}
this.state(STATE.EXPLORER);
}
Expand Down Expand Up @@ -199,7 +199,9 @@ export function ExplorerMathDocumentMixin<B extends MathDocumentConstructor<HTML
}),
sre: expandable({
...BaseDocument.OPTIONS.sre,
speech: 'shallow', // overrides option in EnrichedMathDocument
speech: 'none', // None as speech is explicitly computed
structure: true, // Generates full aria structure
aria: true,
}),
a11y: {
align: 'top', // placement of magnified expression
Expand Down Expand Up @@ -259,6 +261,7 @@ export function ExplorerMathDocumentMixin<B extends MathDocumentConstructor<HTML
* @return {ExplorerMathDocument} The MathDocument (so calls can be chained)
*/
public explorable(): ExplorerMathDocument {
this.options.enableSpeech = true;
if (!this.processed.isSet('explorer')) {
if (this.options.enableExplorer) {
if (!this.explorerRegions) {
Expand Down
34 changes: 7 additions & 27 deletions ts/a11y/explorer/ExplorerPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/

import {LiveRegion, SpeechRegion, ToolTip, HoverRegion} from './Region.js';
import type { ExplorerMathDocument } from '../explorer.js';
import type { ExplorerMathDocument, ExplorerMathItem } from '../explorer.js';

import {Explorer} from './Explorer.js';
import * as ke from './KeyExplorer.js';
Expand Down Expand Up @@ -88,32 +88,11 @@ type ExplorerInit = (doc: ExplorerMathDocument, pool: ExplorerPool,
let allExplorers: {[options: string]: ExplorerInit} = {
speech: (doc: ExplorerMathDocument, pool: ExplorerPool, node: HTMLElement, ...rest: any[]) => {
let explorer = ke.SpeechExplorer.create(
doc, pool, doc.explorerRegions.speechRegion, node, ...rest) as ke.SpeechExplorer;
explorer.speechGenerator.setOptions({
automark: true as any, markup: 'ssml',
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});
}
doc, pool, doc.explorerRegions.speechRegion, node,
doc.explorerRegions.brailleRegion, doc.explorerRegions.magnifier, rest[0], rest[1]) as ke.SpeechExplorer;
explorer.sound = true;
explorer.showRegion = 'subtitles';
return explorer;
},
braille: (doc: ExplorerMathDocument, pool: ExplorerPool, node: HTMLElement, ...rest: any[]) => {
let explorer = ke.SpeechExplorer.create(
doc, pool, doc.explorerRegions.brailleRegion, node, ...rest) as ke.SpeechExplorer;
explorer.speechGenerator.setOptions({automark: false as any, markup: 'none',
locale: 'nemeth', domain: 'default',
style: 'default', modality: 'braille'});
explorer.showRegion = 'viewBraille';
return explorer;
},
keyMagnifier: (doc: ExplorerMathDocument, pool: ExplorerPool, node: HTMLElement, ...rest: any[]) =>
ke.Magnifier.create(doc, pool, doc.explorerRegions.magnifier, node, ...rest),
mouseMagnifier: (doc: ExplorerMathDocument, pool: ExplorerPool, node: HTMLElement, ..._rest: any[]) =>
me.ContentHoverer.create(doc, pool, doc.explorerRegions.magnifier, node,
(x: HTMLElement) => x.hasAttribute('data-semantic-type'),
Expand Down Expand Up @@ -212,13 +191,14 @@ export class ExplorerPool {
* @param mml The corresponding Mathml node as a string.
*/
public init(document: ExplorerMathDocument,
node: HTMLElement, mml: string) {
node: HTMLElement, mml: string,
item: ExplorerMathItem) {
this.document = document;
this.mml = mml;
this.node = node;
this.setPrimaryHighlighter();
for (let key of Object.keys(allExplorers)) {
this.explorers[key] = allExplorers[key](this.document, this, this.node, this.mml);
this.explorers[key] = allExplorers[key](this.document, this, this.node, this.mml, item);
}
this.setSecondaryHighlighter();
this.attach();
Expand All @@ -233,7 +213,7 @@ export class ExplorerPool {
let keyExplorers = [];
for (let key of Object.keys(this.explorers)) {
let explorer = this.explorers[key];
if (explorer instanceof ke.AbstractKeyExplorer) {
if (explorer instanceof ke.SpeechExplorer) {
explorer.AddEvents();
explorer.stoppable = false;
keyExplorers.unshift(explorer);
Expand Down
Loading