blob: 540716ba47ba713e14182f8c617625682cd67609 [file] [log] [blame]
// Copyright 2017 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.
var AutomationNode = chrome.automation.AutomationNode;
var RoleType = chrome.automation.RoleType;
/**
* Gets the first ancestor of a node which is a paragraph or is not inline,
* or get the root node if none is found.
* @param { AutomationNode } node The node to get the parent for.
* @return { ?AutomationNode } the parent paragraph or null if there is none.
*/
function getFirstBlockAncestor(node) {
let parent = node.parent;
let root = node.root;
while (parent != null) {
if (parent == root) {
return parent;
}
if (parent.role == RoleType.PARAGRAPH) {
return parent;
}
if (parent.display !== undefined && parent.display != 'inline' &&
parent.role != RoleType.STATIC_TEXT) {
return parent;
}
parent = parent.parent;
}
return null;
}
/**
* Determines whether two nodes are in the same block-like ancestor, i.e.
* whether they are in the same paragraph.
* @param { AutomationNode|undefined } first The first node to compare.
* @param { AutomationNode|undefined } second The second node to compare.
* @return { boolean } whether two nodes are in the same paragraph.
*/
function inSameParagraph(first, second) {
if (first === undefined || second === undefined) {
return false;
}
// TODO: Clean up this check after crbug.com/774308 is resolved.
// At that point we will only need to check for display:block or inline-block.
if (((first.display == 'block' || first.display == 'inline-block') &&
first.role != RoleType.STATIC_TEXT &&
first.role != RoleType.INLINE_TEXT_BOX) ||
((second.display == 'block' || second.display == 'inline-block') &&
second.role != RoleType.STATIC_TEXT &&
second.role != RoleType.INLINE_TEXT_BOX)) {
// 'block' or 'inline-block' elements cannot be in the same paragraph.
return false;
}
let firstBlock = getFirstBlockAncestor(first);
let secondBlock = getFirstBlockAncestor(second);
return firstBlock != undefined && firstBlock == secondBlock;
}
/**
* Determines whether a string is only whitespace.
* @param { string } name A string to test
* @return { boolean } whether the string is only whitespace
*/
function isWhitespace(name) {
if (name.length == 0) {
return true;
}
// Search for one or more whitespace characters
let re = /^\s+$/;
return re.exec(name) != null;
}
/**
* Builds information about nodes in a group until it reaches the end of the
* group. It may return a NodeGroup with a single node, or a large group
* representing a paragraph of inline nodes.
* @param { Array<AutomationNode> } nodes List of automation nodes to use.
* @param { number } index The index into nodes at which to start.
* @return { NodeGroup } info about the node group
*/
function buildNodeGroup(nodes, index) {
let node = nodes[index];
let next = nodes[index + 1];
let result = new NodeGroup(index, getFirstBlockAncestor(nodes[index]));
// TODO: Don't skip nodes. Instead, go through every node in
// this paragraph from the first to the last in the nodes list.
// This will catch nodes at the edges of the user's selection like
// short links at the beginning or ends of sentences.
//
// While next node is in the same paragraph as this node AND is
// a text type node, continue building the paragraph.
while (index < nodes.length) {
if (node.name !== undefined && !isWhitespace(node.name)) {
result.nodes.push(new NodeGroupItem(node, result.text.length));
result.text += node.name + ' ';
}
if (!inSameParagraph(node, next)) {
break;
}
index += 1;
node = next;
next = nodes[index + 1];
}
result.endIndex = index;
return result;
}
/**
* Class representing a node group, which may be a single node or a
* full paragraph of nodes.
*
* @param {number} startIndex The index of the first node within
* @param {?AutomationNode} blockParent The first block ancestor of
* this group. This may be the paragraph parent, for example.
* @constructor
*/
function NodeGroup(startIndex, blockParent) {
/**
* Full text of this paragraph.
* @type {string|undefined}
*/
this.text = '';
/**
* List of nodes in this paragraph in order.
* @type {Array<NodeGroupItem>}
*/
this.nodes = [];
/**
* The block parent of this NodeGroup, if there is one.
* @type {?AutomationNode}
*/
this.blockParent = blockParent;
/**
* The index of the first node in this paragraph from the list of
* nodes originally selected by the user.
* @type {number}
*/
this.startIndex = startIndex;
/**
* The index of the last node in this paragraph from the list of
* nodes originally selected by the user.
* @type {number}
*/
this.endIndex = -1;
}
/**
* Class representing an automation node within a block of text, like
* a paragraph. Each Item in a NodeGroup has a start index within the
* total text, as well as the original AutomationNode it was associated
* with.
*
* @param {AutomationNode} node The AutomationNode associated with this item
* @param {number} startChar The index into the NodeGroup's text string where
* this item begins.
* @constructor
*/
function NodeGroupItem(node, startChar) {
/**
* @type {AutomationNode}
*/
this.node = node;
/**
* The index into the NodeGroup's text string that is the first character
* of the text of this automation node.
* @type {number}
*/
this.startChar = startChar;
}