define(['mathlive/core/mathAtom',
'mathlive/core/definitions',
'mathlive/editor/editor-popover'],
function(MathAtom, Definitions, Popover) {
const PRONUNCIATION = {
'\\alpha': ' alpha ',
'\\mu': ' mew ',
'\\sigma': ' sigma ',
'\\pi': ' pie ',
'\\imaginaryI': ' eye ',
'\\sum': ' Summation ',
'\\prod': ' Product ',
'|': ' Vertical bar',
'(': ' Open paren [[slnc 150]]',
')': ' [[slnc 150]] Close paren',
'=': ' [[slnc 150]] equals ',
'\\lt': ' [[slnc 150]] is less than ',
'\\le': ' [[slnc 150]] is less than or equal to ',
'\\gt': ' [[slnc 150]] is greater than ',
'\\ge': ' [[slnc 150]] is greater than or equal to ',
'\\geq': ' [[slnc 150]]is greater than or equal to ',
'\\leq': ' [[slnc 150]]is less than or equal to ',
'!': ' factorial ',
'\\sin': ' sine ',
'\\cos': ' cosine ',
'\u200b': '',
'\u2212': ' minus ',
'\\colon': '[[slnc 150]] such that [[slnc 200]]',
'\\hbar': 'etch bar',
'\\iff': ' if and only if ',
'\\land': ' and ',
'\\lor': ' or ',
'\\neg': ' not ',
'\\div': 'divided by',
'\\forall': ' for all ',
'\\exists': ' there exists ',
'\\nexists': ' there does not exists ',
'\\in': ' element of ',
'\\N': ' the set [[char LTRL]]n[[char NORM]]',
'\\C': ' the set [[char LTRL]]c[[char NORM]]',
'\\Z': ' the set [[char LTRL]]z[[char NORM]]',
'\\Q': ' the set [[char LTRL]]q[[char NORM]]',
'\\infty': ' infinity ',
'\\nabla': ' nabla ',
'\\partial': ' partial derivative of ',
'\\cdots': ' dot dot dot ',
'\\lbrace': 'left brace',
'\\{': 'left brace',
'\\rbrace': 'right brace',
'\\}': 'right brace',
'\\langle': 'left angle bracket',
'\\rangle': 'right angle bracket',
'\\lfloor': 'left floor',
'\\rfloor': 'right floor',
'\\lceil': 'left ceiling',
'\\rceil': 'right ceiling',
'\\vert': 'vertical bar',
'\\mvert': 'divides',
'\\lvert': 'left vertical bar',
'\\rvert': 'right vertical bar',
'\\lbrack': 'left bracket',
'\\rbrack': 'right bracket',
}
function getSpokenName(latex) {
let result = Popover.NOTES[latex];
if (!result && latex.charAt(0) === '\\') {
result = latex.replace('\\', '');
}
// If we got more than one result (from NOTES),
// pick the first one.
if (Array.isArray(result)) {
result = result[0];
}
return result;
}
function platform(p) {
let result = 'other';
if (navigator && navigator.platform && navigator.userAgent) {
if (/^(mac)/i.test(navigator.platform)) {
result = 'mac';
} else if (/^(win)/i.test(navigator.platform)) {
result = 'win';
} else if (/(android)/i.test(navigator.userAgent)) {
result = 'android';
} else if (/(iphone)/i.test(navigator.userAgent) ||
/(ipod)/i.test(navigator.userAgent) ||
/(ipad)/i.test(navigator.userAgent)) {
result = 'ios';
} else if (/\bCrOS\b/i.test(navigator.userAgent)) {
result = 'chromeos';
}
}
return result === p ? p : '!' + p;
}
function isAtomic(mathlist) {
let count = 0;
if (mathlist && Array.isArray(mathlist)) {
for (const atom of mathlist) {
if (atom.type !== 'first') {
count += 1;
}
}
}
return count === 1;
}
function atomicValue(mathlist) {
let result = '';
if (mathlist && Array.isArray(mathlist)) {
for (const atom of mathlist) {
if (atom.type !== 'first' && typeof atom.body === 'string') {
result += atom.body;
}
}
}
return result;
}
// See https://pdfs.semanticscholar.org/8887/25b82b8dbb45dd4dd69b36a65f092864adb0.pdf
// "<audio src='non_existing_file.au'>File could not be played.</audio>"
// "I am now <prosody rate='+0.06'>speaking 6% faster.</prosody>"
// https://stackoverflow.com/questions/16635653/ssml-using-chrome-tts
// https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/SpeechSynthesisProgrammingGuide/FineTuning/FineTuning.html#//apple_ref/doc/uid/TP40004365-CH5-SW3
// https://pdfs.semanticscholar.org/8887/25b82b8dbb45dd4dd69b36a65f092864adb0.pdf
MathAtom.MathAtom.prototype.toSpeakableText = function(atom) {
return MathAtom.toSpeakableFragment(atom, {markup: false});
}
MathAtom.toSpeakableFragment = function(atom, options) {
function letter(c) {
let result = '';
if (!options.markup) {
if (/[a-z]/.test(c)) {
result += " '" + c.toUpperCase() + "'";
} else if (/[A-Z]/.test(c)) {
result += " 'capital " + c.toUpperCase() + "'";
} else {
result += c;
}
} else {
if (/[a-z]/.test(c)) {
result += ' [[char LTRL]]' + c + '[[char NORM]]';
} else if (/[A-Z]/.test(c)) {
result += 'capital ' + c.toLowerCase() + '';
} else {
result += c;
}
}
return result;
}
function emph(s) {
// if (options.markup === 'ssml') {
// } else
if (options.markup) {
return '[[emph +]]' + s;
}
return s;
}
if (!atom) return '';
let result = '';
if (Array.isArray(atom)) {
for (let i = 0; i < atom.length; i++) {
if (i < atom.length - 2 &&
atom[i].type === 'mopen' &&
atom[i + 2].type === 'mclose' &&
atom[i + 1].type === 'mord') {
result += ' of ';
result += emph(MathAtom.toSpeakableFragment(atom[i + 1], options));
i += 2;
} else {
result += MathAtom.toSpeakableFragment(atom[i], options);
}
}
} else {
const markup = typeof options.markup === 'undefined' ? false : options.markup;
let numer = '';
let denom = '';
let body = '';
let supsubHandled = false;
switch(atom.type) {
case 'group':
case 'root':
result += MathAtom.toSpeakableFragment(atom.body, options);
break;
case 'genfrac':
numer = MathAtom.toSpeakableFragment(atom.numer, options);
denom = MathAtom.toSpeakableFragment(atom.denom, options);
if (isAtomic(atom.numer) && isAtomic(atom.denom)) {
const COMMON_FRACTIONS = {
'1/2': ' half ',
'1/3': ' one third ',
'2/3': ' two third',
'1/4': ' one quarter ',
'3/4': ' three quarter ',
'1/5': ' one fifth ',
'2/5': ' two fifths ',
'3/5': ' three fifths ',
'4/5': ' four fifths ',
'1/6': ' one sixth ',
'5/6': ' five sixts ',
'1/8': ' one eight ',
'3/8': ' three eights ',
'5/8': ' five eights ',
'7/8': ' seven eights ',
'1/9': ' one ninth ',
'2/9': ' two ninths ',
'4/9': ' four ninths ',
'5/9': ' five ninths ',
'7/9': ' seven ninths ',
'8/9': ' eight ninths ',
// '1/10': ' one tenth ',
// '1/12': ' one twelfth ',
'x/2': ' half [[char LTRL]] X [[char NORM]] ',
// '/2': ' half [[char LTRL]] X [[char NORM]] ',
// 'x/3': ' a third of [[char LTRL]] X [[char NORM]] ',
};
const commonFraction = COMMON_FRACTIONS[
atomicValue(atom.numer) + '/' + atomicValue(atom.denom)];
if (commonFraction) {
result = commonFraction;
} else {
result += numer + ' over ' + denom + ' ';
}
} else {
result += ' The fraction [[slnc 200]]' + numer + ', over [[slnc 150]]' + denom + ', End fraction';
}
break;
case 'surd':
body = MathAtom.toSpeakableFragment(atom.body, options);
if (!atom.index) {
if (isAtomic(atom.body)) {
result += ' square root of ' + body + ' , ';
} else {
result += ' The square root of ' + body + ', End square root';
}
} else {
let index = MathAtom.toSpeakableFragment(atom.index, options);
index = index.trim();
if (index === '3') {
result += ' The cube root of ' + body + ', End cube root';
} else if (index === 'n') {
result += ' The nth root of ' + body + ', End root';
} else {
result += ' root with index: ' + index + ', of :' + body + ', End root';
}
}
break;
case 'accent':
break;
case 'leftright':
result += atom.leftDelim;
result += MathAtom.toSpeakableFragment(atom.body, options);
result += atom.rightDelim;
break;
case 'line':
// @todo
break;
case 'rule':
// @todo
break;
case 'overunder':
// @todo
break;
case 'overlap':
// @todo
break;
case 'placeholder':
result += 'placeholder ' + atom.body;
break;
case 'delim':
case 'sizeddelim':
case 'mord':
case 'minner':
case 'mbin':
case 'mrel':
case 'mpunct':
case 'mopen':
case 'mclose':
case 'textord':
{
const command = atom.latex ? atom.latex.trim() : '' ;
if (command === '\\mathbin' || command === '\\mathrel' ||
command === '\\mathopen' || command === '\\mathclose' ||
command === '\\mathpunct' || command === '\\mathord' ||
command === '\\mathinner') {
result = MathAtom.toSpeakableFragment(atom.body, options);
break;
}
let atomValue = atom.body;
let latexValue = atom.latex;
if (atom.type === 'delim' || atom.type === 'sizeddelim') {
atomValue = latexValue = atom.delim;
}
if (options.mode === 'text') {
result += atomValue;
} else {
if (atom.type === 'mbin') {
result += '[[slnc 150]]';
}
if (atomValue) {
const value = PRONUNCIATION[atomValue] ||
(latexValue ? PRONUNCIATION[latexValue.trim()] : '');
if (value) {
result += ' ' + value;
} else {
const spokenName = latexValue ?
getSpokenName(latexValue.trim()) : '';
result += spokenName ? spokenName : letter(atomValue);
}
} else {
result += MathAtom.toSpeakableFragment(atom.body, options);
}
if (atom.type === 'mbin') {
result += '[[slnc 150]]';
}
}
break;
}
case 'mop':
// @todo
if (atom.body !== '\u200b') {
// Not ZERO-WIDTH
const trimLatex = atom.latex ? atom.latex.trim() : '' ;
if (trimLatex === '\\sum') {
if (atom.superscript && atom.subscript) {
let sup = MathAtom.toSpeakableFragment(atom.superscript, options);
sup = sup.trim();
let sub = MathAtom.toSpeakableFragment(atom.subscript, options);
sub = sub.trim();
result += ' The summation from ' + sub + ' to [[slnc 150]]' + sup + ' of [[slnc 150]]';
supsubHandled = true;
} else {
result += ' The summation of';
}
} else if (trimLatex === '\\prod') {
if (atom.superscript && atom.subscript) {
let sup = MathAtom.toSpeakableFragment(atom.superscript, options);
sup = sup.trim();
let sub = MathAtom.toSpeakableFragment(atom.subscript, options);
sub = sub.trim();
result += ' The product from ' + sub + ' to ' + sup + ' of [[slnc 150]]';
supsubHandled = true;
} else {
result += ' The product of ';
}
} else if (trimLatex === '\\int') {
if (atom.superscript && atom.subscript) {
let sup = MathAtom.toSpeakableFragment(atom.superscript, options);
sup = sup.trim();
let sub = MathAtom.toSpeakableFragment(atom.subscript, options);
sub = sub.trim();
result += ' The integral from ' + emph(sub) + ' to ' + emph(sup) + ' [[slnc 200]] of ';
supsubHandled = true;
} else {
result += ' integral ';
}
} else if (typeof atom.body === 'string') {
const value = PRONUNCIATION[atom.body] ||
PRONUNCIATION[atom.latex.trim()];
if (value) {
result += value;
} else {
result += ' ' + atom.body;
}
} else if (atom.latex && atom.latex.length > 0) {
if (atom.latex[0] === '\\') {
result += ' ' + atom.latex.substr(1);
} else {
result += ' ' + atom.latex;
}
}
}
break;
case 'font':
options.mode = 'text';
result += '[[slnc 150]]';
result += MathAtom.toSpeakableFragment(atom.body, options);
result += '[[slnc 150]]';
options.mode = 'math';
break;
case 'enclose':
body = MathAtom.toSpeakableFragment(atom.body, options);
if (isAtomic(atom.body)) {
result += ' crossed out ' + body + ' , ';
} else {
result += ' crossed out ' + body + ' End cross out';
}
break;
case 'space':
case 'spacing':
case 'color':
case 'sizing':
case 'mathstyle':
case 'box':
// @todo
break;
}
if (!supsubHandled && atom.superscript) {
let sup = MathAtom.toSpeakableFragment(atom.superscript, options);
sup = sup.trim();
if (isAtomic(atom.superscript)) {
if (sup === '\u2032') {
result += ' prime ';
} else if (sup === '2') {
result += ' squared ';
} else if (sup === '3') {
result += ' cubed ';
} else {
result += ' to the ' + sup + '; ';
}
} else {
result += ' raised to the [[pbas +4]]' + sup + ' [[pbas -4]] power. ';
}
}
if (!supsubHandled && atom.subscript) {
let sub = MathAtom.toSpeakableFragment(atom.subscript, options);
sub = sub.trim();
if (isAtomic(atom.subscript)) {
result += ' sub ' + sub;
} else {
result += ' subscript ' + sub + ' End subscript. ';
}
}
// If no markup was requested, remove any that we may have
if (markup === 'ssml') {
// @todo: convert VoiceOver markup to SSML
} else if (!markup) {
result = result.replace(/\[\[[^\]]*\]\]/g, '');
}
}
return result;
}
/**
* @param {MathAtom[]} [atoms] The atoms to represent as speakable text.
* If omitted, `this` is used.
* @param {Object.<string, any>} [options]
*/
MathAtom.toSpeakableText = function(atoms, options) {
if (!options) {
options = {
markup: false
}
}
let result = '';
if (options.markup === 'ssml') {
result = `<!-- ?xml version="1.0"? -->
<speak xmlns="http://www.w3.org/2001/10/synthesis"
version="1.0"><p><s xml:lang="en-US">`;
} else if (options.markup) {
if (platform('mac') === '!mac') {
options.markup = false;
}
}
result += MathAtom.toSpeakableFragment(atoms, options);
if (options.markup === 'ssml') {
result += '</s></p></speak>';
}
return result;
}
// Export the public interface for this module
return {}
})