Skip to content

Commit

Permalink
Merge branch 'FLUID-6278'
Browse files Browse the repository at this point in the history
* FLUID-6278: (24 commits)
  FLUID-6278: Updating comment
  FLUID-6278: Cleaned up mockTTS speak override.
  FLUID-6278: Updating tests for changes to boundary event handling.
  FLUID-6278: Ignoring boundary events for "sentence" boundaries.
  FLUID-6278: Expanding test coverage for Orator tests.
  FLUID-6278: Linting
  FLUID-6278: Cleaning and updating lang values for TTS related demos.
  FLUID-6278: Updating syllabification with changes to textNodeParser
  FLUID-6278: fixing/updating tests
  FLUID-6278: Moving checkTTSSupport to tests.
  FLUID-6278: Updating TextToSpeech tests.
  FLUID-6278: Updating TextNodeParser test, and added a related bug fix.
  FLUID-6278: Refactoring and comment updates.
  FLUID-6278: Updating comments
  FLUID-6278: Refactoring of fluid.textNodeParser.parser
  FLUID-6278: In progress commit. selectionReader working.
  FLUID-6278: In progress work to update selection reader
  FLUID-6278: Broke out parseQueue array increment into separate function
  FLUID-6278: Some linting
  FLUID-6278: Updated code comments
  ...
  • Loading branch information
amb26 committed Oct 31, 2019
2 parents 3d3b22b + 2a53dd8 commit f8da9a3
Show file tree
Hide file tree
Showing 17 changed files with 942 additions and 382 deletions.
2 changes: 1 addition & 1 deletion demos/orator/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en-CA">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
Expand Down
2 changes: 1 addition & 1 deletion demos/prefsFramework/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en-US">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
Expand Down
2 changes: 1 addition & 1 deletion src/components/orator/css/Orator.css
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
color: #000000;
background-color: #EED400;
/* Used to make the background appear larger so that it stands out in a block of text */
outline: 0.2rem solid #EED400;
padding: 0.2rem 0;
}


Expand Down
390 changes: 289 additions & 101 deletions src/components/orator/js/Orator.js

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions src/components/textToSpeech/js/MockTTS.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
},
// override the speak invoker to return the utterance component instead of the SpeechSynthesisUtterance instance
speak: {
func: "{that}.invokeSpeechSynthesisFunc",
args: ["speak", "{that}.queue.0"]
funcName: "fluid.mock.textToSpeech.speakOverride",
args: ["{that}"]
}
},
distributeOptions: {
Expand All @@ -87,6 +87,11 @@ https://github.com/fluid-project/infusion/raw/master/Infusion-LICENSE.txt
}
});

fluid.mock.textToSpeech.speakOverride = function (that) {
var utterance = that.queue[that.queue.length - 1];
that.invokeSpeechSynthesisFunc("speak", utterance);
};

fluid.mock.textToSpeech.invokeStub = function (that, method, args) {
args = fluid.makeArray(args);
args.unshift(that);
Expand Down
140 changes: 83 additions & 57 deletions src/components/textToSpeech/js/TextToSpeech.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
* Adds a lister to a window event for each event defined on the component.
* The name must match a valid window event.
*
* @param {Component} that - the component itself
* @param {Component} that - an instance of `fluid.window`
*/
fluid.window.bindEvents = function (that) {
fluid.each(that.options.events, function (type, eventName) {
Expand All @@ -61,42 +61,6 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
return !!(window && window.speechSynthesis);
};

/**
* Ensures that TTS is supported in the browser, including cases where the
* feature is detected, but where the underlying audio engine is missing.
* For example in VMs on SauceLabs, the behaviour for browsers which report that the speechSynthesis
* API is implemented is for the `onstart` event of an utterance to never fire. If we don't receive this
* event within a timeout, this API's behaviour is to return a promise which rejects.
*
* @param {Number} delay - A time in milliseconds to wait for the speechSynthesis to fire its onStart event
* by default it is 5000ms (5s). This is crux of the test, as it needs time to attempt to run the speechSynthesis.
* @return {fluid.promise} - A promise which will resolve if the TTS is supported (the onstart event is fired within the delay period)
* or be rejected otherwise.
*/
fluid.textToSpeech.checkTTSSupport = function (delay) {
var promise = fluid.promise();
if (fluid.textToSpeech.isSupported()) {
// MS Edge speech synthesizer won't speak if the text string is blank,
// so this must contain actual text
var toSpeak = new SpeechSynthesisUtterance("short"); // short text to attempt to speak
toSpeak.volume = 0; // mutes the Speech Synthesizer
// Same timeout as the timeout in the IoC testing framework
var timeout = setTimeout(function () {
fluid.textToSpeech.invokeSpeechSynthesisFunc("cancel");
promise.reject();
}, delay || 5000);
toSpeak.onend = function () {
clearTimeout(timeout);
fluid.textToSpeech.invokeSpeechSynthesisFunc("cancel");
promise.resolve();
};
fluid.textToSpeech.invokeSpeechSynthesisFunc("speak", toSpeak);
} else {
fluid.invokeLater(promise.reject);
}
return promise;
};

/*********************************************************************************************
* fluid.textToSpeech component
*********************************************************************************************/
Expand Down Expand Up @@ -137,22 +101,39 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
options: {
listeners: {
"onBoundary.relay": "{textToSpeech}.events.utteranceOnBoundary.fire",
"onEnd.relay": "{textToSpeech}.events.utteranceOnEnd.fire",
"onError.relay": "{textToSpeech}.events.utteranceOnError.fire",
"onEnd.relay": {
listener: "{textToSpeech}.events.utteranceOnEnd.fire",
priority: "before:resolvePromise"
},
"onError.relay": {
listener: "{textToSpeech}.events.utteranceOnError.fire",
priority: "before:rejectPromise"
},
"onMark.relay": "{textToSpeech}.events.utteranceOnMark.fire",
"onPause.relay": "{textToSpeech}.events.utteranceOnPause.fire",
"onResume.relay": "{textToSpeech}.events.utteranceOnResume.fire",
"onStart.relay": "{textToSpeech}.events.utteranceOnStart.fire",
"onCreate.followPromise": {
funcName: "fluid.promise.follow",
args: ["{that}.promise", "{that}.options.onSpeechQueuePromise"]
},
"onCreate.queue": {
"this": "{fluid.textToSpeech}.queue",
method: "push",
args: ["{that}"]
args: ["{that}"],
priority: "after:followPromise"
},
"onCreate.speak": {
listener: "{textToSpeech}.speak",
args: ["{that}.utterance"],
priority: "after:queue"
},
"onEnd.destroy": {
func: "{that}.destroy",
priority: "last"
}
},
onSpeechQueuePromise: "{arguments}.2",
utterance: "{arguments}.0"
}
}
Expand Down Expand Up @@ -191,6 +172,10 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
funcName: "fluid.textToSpeech.queueSpeech",
args: ["{that}", "{arguments}.0", "{arguments}.1", "{arguments}.2"]
},
queueSpeechSequence: {
funcName: "fluid.textToSpeech.queueSpeechSequence",
args: ["{that}", "{arguments}.0", "{arguments}.1"]
},
cancel: {
funcName: "fluid.textToSpeech.cancel",
args: ["{that}"]
Expand All @@ -211,15 +196,11 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
},
speak: {
func: "{that}.invokeSpeechSynthesisFunc",
args: ["speak", "{that}.queue.0.utterance"]
args: ["speak", "{arguments}.0"]
},
invokeSpeechSynthesisFunc: "fluid.textToSpeech.invokeSpeechSynthesisFunc"
},
listeners: {
"onSpeechQueued.speak": {
func: "{that}.speak",
priority: "last"
},
"utteranceOnStart.speaking": {
changePath: "speaking",
value: true,
Expand Down Expand Up @@ -314,13 +295,14 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
* Assembles the utterance options and fires onSpeechQueued which will kick off the creation of an utterance
* component. If "interrupt" is true, this utterance will replace any existing ones.
*
* @param {Component} that - the component
* @param {fluid.textToSpeech} that - an instance of the component
* @param {String} text - the text to be synthesized
* @param {Boolean} interrupt - used to indicate if this text should be queued or replace existing utterances
* @param {UtteranceOpts} options - options to configure the SpeechSynthesis utterance with. It is merged on top of the
* utteranceOpts from the component's model.
* @param {UtteranceOpts} options - options to configure the {SpeechSynthesisUtterance} with. It is merged on top of
* the `utteranceOpts` from the component's model.
*
* @return {Promise} - returns a promise that is resolved after the onSpeechQueued event has fired.
* @return {Promise} - returns a promise that is resolved/rejected from the related `fluid.textToSpeech.utterance`
* instance.
*/
fluid.textToSpeech.queueSpeech = function (that, text, interrupt, options) {
var promise = fluid.promise();
Expand All @@ -333,17 +315,54 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
// The setTimeout is needed for Safari to fully cancel out the previous speech.
// Without this the synthesizer gets confused and may play multiple utterances at once.
setTimeout(function () {
that.events.onSpeechQueued.fire(utteranceOpts, interrupt);
promise.resolve(text);
that.events.onSpeechQueued.fire(utteranceOpts, interrupt, promise);
}, 100);
return promise;
};

/**
* Values to configure the SpeechSynthesis Utterance with.
* See: https://w3c.github.io/speech-api/speechapi.html#utterance-attributes
*
* @typedef {Object} Speech
* @property {String} text - the text to Synthesize
* @property {UtteranceOpts} options - options to configure the {SpeechSynthesisUtterance} with. It is merged on top
* of the `utteranceOpts` from the component's model.
*/

/**
* Queues a {Speech[]}, calling `that.queueSpeech` for each. This is useful for sets of text that should be
* synthesized with differing {UtteranceOpts}, but still treated as an atomic unit. For example, if a set of text
* includes words from different languages.
*
* @param {fluid.textToSpeech} that - an instance of the component
* @param {Speech[]} speeches - the set of text to queue as a unit
* @param {Boolean} interrupt - used to indicate if the related text should be queued or replace existing
* utterances
*
* @return {Promise} - returns a promise that is resolved/rejected after all of the speeches have finish or any
* have been rejected.
*/
fluid.textToSpeech.queueSpeechSequence = function (that, speeches, interrupt) {
var sequence = fluid.transform(speeches, function (speech, index) {
var toInterrupt = interrupt && !index; // only interrupt on the first string
return that.queueSpeech(speech.text, toInterrupt, speech.options);
});
return fluid.promise.sequence(sequence);
};

/**
* Manually fires the `onEnd` event of each remaining `fluid.textToSpeech.utterance` component in the queue. This
* is required because if the SpeechSynthesis is cancelled remaining {SpeechSynthesisUtterance} are ignored and do
* not fire their `onend` event.
*
* @param {fluid.textToSpeech} that - an instance of the component
*/
fluid.textToSpeech.cancel = function (that) {
// Safari does not fire the onend event from an utterance when the speech synthesis is cancelled.
// Manually triggering the onEnd event for each utterance as we empty the queue, before calling cancel.
while (that.queue.length) {
var utterance = that.queue.shift();
var utterance = that.queue[0];
utterance.events.onEnd.fire();
}

Expand All @@ -364,6 +383,11 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
funcName: "fluid.textToSpeech.utterance.construct",
args: ["{that}", "{that}.options.utteranceEventMap", "{that}.options.utterance"]
}
},
promise: {
expander: {
funcName: "fluid.promise"
}
}
},
model: {
Expand Down Expand Up @@ -399,7 +423,9 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
"onBoundary.updateModel": {
changePath: "boundary",
value: "{arguments}.0.charIndex"
}
},
"onEnd.resolvePromise": "{that}.promise.resolve",
"onError.rejectPromise": "{that}.promise.reject"
}
});

Expand All @@ -408,11 +434,11 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
* event provided in the utteranceEventMap, any corresponding event binding passed in directly through the
* utteranceOpts will be rebound as component event listeners with the "external" namespace.
*
* @param {Component} that - the component
* @param {Object} utteranceEventMap - a mapping from SpeechSynthesisUtterance events to component events.
* @param {UtteranceOpts} utteranceOpts - options to configure the SpeechSynthesis utterance with.
* @param {fluid.textToSpeech.utterance} that - an instance of the component
* @param {Object} utteranceEventMap - a mapping from {SpeechSynthesisUtterance} events to component events.
* @param {UtteranceOpts} utteranceOpts - options to configure the {SpeechSynthesisUtterance} with.
*
* @return {SpeechSynthesisUtterance} - returns the created SpeechSynthesisUtterance object
* @return {SpeechSynthesisUtterance} - returns the created {SpeechSynthesisUtterance} object
*/
fluid.textToSpeech.utterance.construct = function (that, utteranceEventMap, utteranceOpts) {
var utterance = new SpeechSynthesisUtterance();
Expand Down
37 changes: 29 additions & 8 deletions src/framework/core/js/TextNodeParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,38 +99,59 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
return $(elm).closest("[lang]").attr("lang");
};

/**
* The parsed information of text node, including: the node itself, its specified language, and its index within its
* parent.
*
* @typedef {Object} TextNodeData
* @property {DomNode} node - The current child node being parsed
* @property {Integer} childIndex - The index of the child node being parsed relative to its parent
* @property {String} lang - a valid BCP 47 language code
*/

/**
* Recursively parses a DOM element and it's sub elements and fires the `onParsedTextNode` event for each text node
* found. The event is fired with the text node, language and index of the text node in the list of its parent's
* child nodes..
*
* Note: elements that return `false` to `that.hasTextToRead` are ignored.
* Note: elements that return `false` from `that.hasTextToRead` are ignored.
*
* @param {Component} that - an instance of `fluid.textNodeParser`
* @param {fluid.textNodeParser} that - an instance of the component
* @param {jQuery|DomElement} elm - the DOM node to parse
* @param {String} lang - a valid BCP 47 language code.
* @param {Event} afterParseEvent - the event to fire after parsing has completed.
*
* @return {TextNodeData[]} the array of parsed elements. Only text nodes for elements that have passed the
* `that.hasTextToRead` check will be included.
*/
fluid.textNodeParser.parse = function (that, elm, lang, afterParseEvent) {
elm = fluid.unwrap(elm);
lang = lang || that.getLang(elm);
var parsed = [];

if (that.hasTextToRead(elm)) {
var childNodes = elm.childNodes;
var elementLang = elm.getAttribute("lang") || lang;
var elementLang = elm.getAttribute("lang") || lang || that.getLang(elm);;

$.each(childNodes, function (childIndex, childNode) {
childNodes.forEach(function (childNode, childIndex) {
if (childNode.nodeType === Node.TEXT_NODE) {
that.events.onParsedTextNode.fire(childNode, elementLang, childIndex);
var textNodeData = {
node: childNode,
lang: elementLang,
childIndex: childIndex
};
parsed.push(textNodeData);
that.events.onParsedTextNode.fire(textNodeData);
} else if (childNode.nodeType === Node.ELEMENT_NODE) {
fluid.textNodeParser.parse(that, childNode, elementLang);
parsed = parsed.concat(fluid.textNodeParser.parse(that, childNode, elementLang));
}
});
}

if (afterParseEvent) {
afterParseEvent(that);
afterParseEvent(that, parsed);
}

return parsed;
};

})(jQuery, fluid_3_0_0);
2 changes: 1 addition & 1 deletion src/framework/preferences/js/SyllabificationEnactor.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ var fluid_3_0_0 = fluid_3_0_0 || {};
},
"onParsedTextNode.syllabify": {
listener: "{that}.apply",
args: ["{arguments}.0", "{arguments}.1"]
args: ["{arguments}.0.node", "{arguments}.0.lang"]
},
"onNodeAdded.syllabify": {
listener: "{that}.parse",
Expand Down
3 changes: 2 additions & 1 deletion tests/component-tests/orator/html/Orator-test.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ <h2 id="qunit-userAgent"></h2>
Text <span class="flc-orator-domReader-test-wrap">wrapped text</span>
</div>
<p class="flc-orator-domReader-test" lang="en-CA">
Read<span></span>ing <strong>text</strong> from <em>DOM</em><span style="display: none;">hidden</span>
Read<span></span>ing <strong lang="en-US">text</strong> from <em>DOM</em><span style="display: none;">hidden</span>
</p>
<p class="flc-orator-selectionReader-test">
<span class="flc-orator-selectionReader-test-selection">Selection Test</span>
<span class="flc-orator-selectionReader-test-selectionTwo">Other Text</span>
<span class="flc-orator-selectionReader-test-selectionThree">Change <span lang="en-US" class="flc-orator-selectionReader-test-selectionFour">Language</span></span>
</p>
<div class="flc-orator-test">
<article class="flc-orator-content">
Expand Down
Loading

0 comments on commit f8da9a3

Please sign in to comment.