Commit d485fe03 authored by Lei Shi's avatar Lei Shi Committed by Commit Bot

Filter out overflow text in Select-to-Speak

When converting nodes into NodeGroup, we check each word one by one. If the word is out of the bound of its parent, we replace the
word with equal length space characters. The resulted text have the same length of the original text, and TTS will ignore overflow texts as they are empty now.

Bug: 775659
Change-Id: Iec3a296344b1c522ec255d56bfcbf51a68d50f5f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2342127Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Commit-Queue: Lei Shi <leileilei@google.com>
Cr-Commit-Position: refs/heads/master@{#799307}
parent 99792c43
......@@ -408,7 +408,7 @@ IN_PROC_BROWSER_TEST_F(SelectToSpeakTest,
DISABLED_ContinuesReadingDuringResize) {
ActivateSelectToSpeakInWindowBounds(
"data:text/html;charset=utf-8,<p>First paragraph</p>"
"<div id='resize' style='width:300px; font-size: 10em'>"
"<div id='resize' style='width:300px; font-size: 1em'>"
"<p>Second paragraph is longer than 300 pixels and will wrap when "
"resized</p></div>");
......
......@@ -75,6 +75,7 @@ source_set("browser_tests") {
js2gtest("select_to_speak_extjs_tests") {
test_type = "extension"
sources = [
"paragraph_utils_overflow_test.js",
"select_to_speak_keystroke_selection_test.js",
"select_to_speak_mouse_selection_test.js",
"select_to_speak_prefs_test.js",
......
......@@ -121,6 +121,117 @@ class ParagraphUtils {
return node.name ? node.name : '';
}
/**
* Gets the text to be read aloud for a particular node.
* Compared with the bounds of the blockParent, the overflow
* words of the text will be replaced with empty space.
* @param {!ParagraphUtils.NodeGroupItem} nodeGroupItem
* @param {?AutomationNode} blockParent
* @return {string} The text to read for this node.
*/
static getNodeNameWithoutOverflowWords(nodeGroupItem, blockParent) {
const unclippedText = ParagraphUtils.getNodeName(nodeGroupItem.node);
if (blockParent == null || blockParent.location == null) {
return unclippedText;
}
// Get the bounds of blockparent.
const bounds = blockParent.location;
const leftBound = bounds.left;
const rightBound = bounds.left + bounds.width;
const topBound = bounds.top;
const bottomBound = bounds.top + bounds.height;
const nodeBounds = nodeGroupItem.node.unclippedLocation;
const nodeLeftBound = nodeBounds.left;
const nodeRightBound = nodeBounds.left + nodeBounds.width;
const nodeTopBound = nodeBounds.top;
const nodeBottomBound = nodeBounds.top + nodeBounds.height;
// If the node bounds are entirely within the blockparent bounds, return
// the unclipped text.
if (nodeLeftBound >= leftBound && nodeRightBound <= rightBound &&
nodeTopBound >= topBound && nodeBottomBound <= bottomBound) {
return unclippedText;
}
// If the node bounds are entirely out of the blockparent bounds, return
// empty text with the same length of the unclipped text.
if (nodeLeftBound >= rightBound || nodeRightBound <= leftBound ||
nodeTopBound >= nodeBottomBound || nodeBottomBound <= topBound) {
return ' '.repeat(unclippedText.length);
}
// Go through words one by one and construct the output text.
let outputText = unclippedText;
let index = 0;
while (index < unclippedText.length) {
// The index of the first char of the word. The startIndex is guaranteed
// to be equal or lager than the input index, and is capped at
// nodeGroupItem.name.length.
const startIndex = WordUtils.getNextWordStart(
unclippedText,
index,
nodeGroupItem,
true /* ignoreStartChar */,
);
// The index of the last char of the word + 1. The endIndex is guaranteed
// to be larger than the input startIndex, and is capped at
// unclippedText.length.
const endIndex = WordUtils.getNextWordEnd(
unclippedText,
startIndex,
nodeGroupItem,
true /* ignoreStartChar */,
);
// Prepare the index for the next word. The endIndex is guarantted to be
// larger than the original index, and is capped at unclippedText.length,
// so the while loop will be stopped.
index = endIndex;
// If nodeGroupItem.hasInlineText is true, the nodeGroupItem is a
// staticText node that has inlineTextBox children, and we need to select
// a child inlineTextBox node corresponding to the startIndex. We also
// need to offset the query indexes for the inlineTextBox node. If
// nodeGroupItem.hasInlineText is false, the node of the NodeGroupItem
// should not be an inlineTextBox node, and we assigns the node and
// indexes directly from the nodeGroupItem.
let node;
let boundQueryStartIndex;
let boundQueryEndIndex;
if (nodeGroupItem.hasInlineText) {
node = ParagraphUtils.findInlineTextNodeByCharacterIndex(
nodeGroupItem.node, startIndex);
const charIndexInParent =
ParagraphUtils.getStartCharIndexInParent(node);
boundQueryStartIndex = startIndex - charIndexInParent;
boundQueryEndIndex = endIndex - charIndexInParent;
} else {
console.assert(
nodeGroupItem.node.role != RoleType.INLINE_TEXT_BOX,
'NodeGroupItem.node should not be an inlineTextBox node');
node = nodeGroupItem.node;
boundQueryStartIndex = startIndex;
boundQueryEndIndex = endIndex;
}
node.unclippedBoundsForRange(
boundQueryStartIndex, boundQueryEndIndex, (b) => {
// If the word is entirely out of the blockparent bounds,
// replace the word with space characters.
if (b.left + b.width <= leftBound || b.left >= rightBound ||
b.top >= bottomBound || b.top + b.height <= topBound) {
outputText = outputText.substr(0, startIndex) +
' '.repeat(endIndex - startIndex) +
outputText.substr(endIndex);
}
});
}
return outputText;
}
/**
* Determines the index into the parent name at which the inlineTextBox
* node name begins.
......@@ -178,8 +289,8 @@ class ParagraphUtils {
static buildNodeGroup(nodes, index, splitOnLanguage) {
let node = nodes[index];
let next = nodes[index + 1];
const result = new ParagraphUtils.NodeGroup(
ParagraphUtils.getFirstBlockAncestor(nodes[index]));
const blockParent = ParagraphUtils.getFirstBlockAncestor(nodes[index]);
const result = new ParagraphUtils.NodeGroup(blockParent);
let staticTextParent = null;
let currentLanguage = undefined;
// TODO: Don't skip nodes. Instead, go through every node in
......@@ -220,7 +331,9 @@ class ParagraphUtils {
new ParagraphUtils.NodeGroupItem(node, result.text.length, false);
}
if (newNode) {
result.text += ParagraphUtils.getNodeName(newNode.node) + ' ';
result.text += ParagraphUtils.getNodeNameWithoutOverflowWords(
newNode, blockParent) +
' ';
result.nodes.push(newNode);
}
}
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
GEN_INCLUDE(['select_to_speak_e2e_test_base.js']);
/**
* Browser tests for select-to-speak's feature to filter out overflow text.
*/
SelectToSpeakParagraphOverflowTest = class extends SelectToSpeakE2ETest {
generateHorizentalOverflowText(text) {
return (
'<div style="width: 50px; overflow: hidden">' +
'<p style="width: 200px">$text</p>'.replace('$text', text) + '</div>');
}
generateVerticalOverflowText(visibleText, overflowText) {
return (
'<div style="height: 30px; overflow: hidden">' +
'<p>$text</p>'.replace('$text', visibleText) +
'<p>$text</p>'.replace('$text', overflowText) + '</div>');
}
generateEntirelyOverflowText(text) {
return (
'<div style="width: 50px; overflow:hidden">' +
'<p style="width: 1000px">' +
'<span>long text to keep the second node overflow entirely</span>' +
'$text</p>'.replace('$text', text) + '</div>');
}
generateVisibleText(text) {
return (
'<div style="width: 50px">' +
'<p style="width: 200px">$text</p>'.replace('$text', text) + '</div>');
}
};
TEST_F(
'SelectToSpeakParagraphOverflowTest',
'ReplaceseHorizentalOverflowTextWithSpace', function() {
const inputText = 'This text overflows partially';
this.runWithLoadedTree(
this.generateHorizentalOverflowText(inputText), function(root) {
const overflowText = root.find({
role: 'inlineTextBox',
attributes: {name: inputText},
});
var nodeGroup = ParagraphUtils.buildNodeGroup(
[overflowText], 0, false /* do not split on language */
);
// The output text should have the same length of the input text
// plus a space character at the end.
assertEquals(nodeGroup.text.length, inputText.length + 1);
// The output text should have less non-empty characters compared
// to the input text, as any overflow word will be replaced as
// space characters.
assertTrue(
nodeGroup.text.replace(/ /g, '').length <
inputText.replace(/ /g, '').length);
});
});
TEST_F(
'SelectToSpeakParagraphOverflowTest',
'ReplaceseVerticalOverflowTextWithSpace', function() {
const visibleText = 'This text is visible';
const overflowText = 'This text overflows';
this.runWithLoadedTree(
this.generateVerticalOverflowText(visibleText, overflowText),
function(root) {
// Find the visible text.
const visibleTextNode = root.find({
role: 'inlineTextBox',
attributes: {name: visibleText},
});
var nodeGroup = ParagraphUtils.buildNodeGroup(
[visibleTextNode], 0, false /* do not split on language */
);
// The output text should have the same length of the visible text
// plus a space character at the end.
assertEquals(nodeGroup.text.length, visibleText.length + 1);
// The output text should be the same of the input text.
assertEquals(
nodeGroup.text.replace(/ /g, ''),
visibleText.replace(/ /g, ''));
// Find the overflow text.
const overflowTextNode = root.find({
role: 'inlineTextBox',
attributes: {name: overflowText},
});
var nodeGroup = ParagraphUtils.buildNodeGroup(
[overflowTextNode], 0, false /* do not split on language */
);
// The output text should have the same length of the overflow text
// plus a space character at the end.
assertEquals(nodeGroup.text.length, overflowText.length + 1);
// The output text should only have space characters.
assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
});
});
TEST_F(
'SelectToSpeakParagraphOverflowTest',
'ReplacesEntirelyOverflowTextWithSpace', function() {
const inputText = 'This text overflows entirely';
this.runWithLoadedTree(
this.generateEntirelyOverflowText(inputText), function(root) {
const overflowText = root.find({
role: 'inlineTextBox',
attributes: {name: inputText},
});
var nodeGroup = ParagraphUtils.buildNodeGroup(
[overflowText], 0, false /* do not split on language */
);
// The output text should have the same length of the input text
// plus a space character at the end.
assertEquals(nodeGroup.text.length, inputText.length + 1);
// The output text should have zero non-empty character.
assertEquals(nodeGroup.text.replace(/ /g, '').length, 0);
});
});
TEST_F('SelectToSpeakParagraphOverflowTest', 'OutputsVisibleText', function() {
const inputText = 'This text is visible';
this.runWithLoadedTree(this.generateVisibleText(inputText), function(root) {
const visibleText = root.find({
role: 'inlineTextBox',
attributes: {name: inputText},
});
var nodeGroup = ParagraphUtils.buildNodeGroup(
[visibleText], 0, false /* do not split on language */
);
// The output text should have the same length of the input text plus a
// space character at the end.
assertEquals(nodeGroup.text.length, inputText.length + 1);
// The output text should have same non-empty words as the input text.
assertEquals(nodeGroup.text.replace(/ /g, ''), inputText.replace(/ /g, ''));
});
});
......@@ -15,22 +15,27 @@ class WordUtils {
* searching.
* @param {ParagraphUtils.NodeGroupItem} nodeGroupItem The node whose name we
* are searching through.
* @param {boolean?} ignoreStartChar When set to true, the search will only
* consider the index within the input node and ignore
* nodeGroupItem.startChar offsets. This is useful when we only search
* within the input nodeGroupItem, instead of the parent nodeGroup.
* @return {number} The index of the next word's start
*/
static getNextWordStart(text, indexAfter, nodeGroupItem) {
static getNextWordStart(
text, indexAfter, nodeGroupItem, ignoreStartChar = false) {
if (nodeGroupItem.hasInlineText && nodeGroupItem.node.children.length > 0) {
const startChar = ignoreStartChar ? 0 : nodeGroupItem.startChar;
const node = ParagraphUtils.findInlineTextNodeByCharacterIndex(
nodeGroupItem.node, indexAfter - nodeGroupItem.startChar);
nodeGroupItem.node, indexAfter - startChar);
const startCharInParent = ParagraphUtils.getStartCharIndexInParent(node);
for (var i = 0; i < node.wordStarts.length; i++) {
if (node.wordStarts[i] + nodeGroupItem.startChar + startCharInParent <
indexAfter) {
if (node.wordStarts[i] + startChar + startCharInParent < indexAfter) {
continue;
}
return node.wordStarts[i] + nodeGroupItem.startChar + startCharInParent;
return node.wordStarts[i] + startChar + startCharInParent;
}
// Default: We are just off the edge of this node.
return node.name.length + nodeGroupItem.startChar + startCharInParent;
return node.name.length + startChar + startCharInParent;
} else {
// Try to parse using a regex, which is imperfect.
// Fall back to the given index if we can't find a match.
......@@ -47,20 +52,24 @@ class WordUtils {
* searching.
* @param {ParagraphUtils.NodeGroupItem} nodeGroupItem The node whose name we
* are searching through.
* @param {boolean?} ignoreStartChar When set to true, the search will only
* consider the index within the input node and ignore
* nodeGroupItem.startChar offsets. This is useful when we only search
* within the input nodeGroupItem, instead of the parent nodeGroup.
* @return {number} The index of the next word's end
*/
static getNextWordEnd(text, indexAfter, nodeGroupItem) {
static getNextWordEnd(
text, indexAfter, nodeGroupItem, ignoreStartChar = false) {
if (nodeGroupItem.hasInlineText && nodeGroupItem.node.children.length > 0) {
const startChar = ignoreStartChar ? 0 : nodeGroupItem.startChar;
const node = ParagraphUtils.findInlineTextNodeByCharacterIndex(
nodeGroupItem.node, indexAfter - nodeGroupItem.startChar + 1);
nodeGroupItem.node, indexAfter - startChar + 1);
const startCharInParent = ParagraphUtils.getStartCharIndexInParent(node);
for (var i = 0; i < node.wordEnds.length; i++) {
if (node.wordEnds[i] + nodeGroupItem.startChar + startCharInParent - 1 <
indexAfter) {
if (node.wordEnds[i] + startChar + startCharInParent - 1 < indexAfter) {
continue;
}
const result =
node.wordEnds[i] + nodeGroupItem.startChar + startCharInParent;
const result = node.wordEnds[i] + startChar + startCharInParent;
return text.length > result ? result : text.length;
}
// Default.
......
......@@ -54,6 +54,30 @@ TEST_F('SelectToSpeakWordUtilsUnitTest', 'getNextWordStart', function() {
10, WordUtils.getNextWordStart('once upon a kitty cat', 10, node));
});
TEST_F(
'SelectToSpeakWordUtilsUnitTest', 'getNextWordStartIgnoresStartCharOffset',
function() {
const inlineText = {
wordStarts: [0, 5, 10, 16],
name: 'once upon kitty cat'
};
const staticText = {children: [inlineText], name: 'once upon kitty cat'};
const node = {node: staticText, startChar: 9, hasInlineText: true};
// If search within the node and ignore starChar offset, returns the index
// of character "k".
assertEquals(
10, WordUtils.getNextWordStart('once upon kitty cat', 9, node, true));
// If consider the starChar offset, returns the index of character "o"
// with offset.
assertEquals(
9, WordUtils.getNextWordStart('once upon kitty cat', 9, node, false));
// Should return the default if the inlineText children are missing.
staticText.children = [];
assertEquals(
10, WordUtils.getNextWordStart('once upon a kitty cat', 10, node));
});
TEST_F(
'SelectToSpeakWordUtilsUnitTest', 'getNextWordStartMultipleChildren',
function() {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment