summaryrefslogtreecommitdiff
path: root/Source/WebCore/accessibility/AXObjectCache.cpp
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
committerLorry Tar Creator <lorry-tar-importer@lorry>2017-06-27 06:07:23 +0000
commit1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch)
tree46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/accessibility/AXObjectCache.cpp
parent32761a6cee1d0dee366b885b7b9c777e67885688 (diff)
downloadWebKitGtk-tarball-master.tar.gz
Diffstat (limited to 'Source/WebCore/accessibility/AXObjectCache.cpp')
-rw-r--r--Source/WebCore/accessibility/AXObjectCache.cpp1956
1 files changed, 1832 insertions, 124 deletions
diff --git a/Source/WebCore/accessibility/AXObjectCache.cpp b/Source/WebCore/accessibility/AXObjectCache.cpp
index fec39c7fb..0ceedd7d4 100644
--- a/Source/WebCore/accessibility/AXObjectCache.cpp
+++ b/Source/WebCore/accessibility/AXObjectCache.cpp
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008, 2009, 2010 Apple Inc. All rights reserved.
+ * Copyright (C) 2008, 2009, 2010, 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -10,7 +10,7 @@
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
- * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * 3. Neither the name of Apple Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
@@ -35,20 +35,23 @@
#include "AccessibilityARIAGrid.h"
#include "AccessibilityARIAGridCell.h"
#include "AccessibilityARIAGridRow.h"
+#include "AccessibilityAttachment.h"
#include "AccessibilityImageMapLink.h"
+#include "AccessibilityLabel.h"
#include "AccessibilityList.h"
#include "AccessibilityListBox.h"
#include "AccessibilityListBoxOption.h"
+#include "AccessibilityMathMLElement.h"
#include "AccessibilityMediaControls.h"
#include "AccessibilityMenuList.h"
#include "AccessibilityMenuListOption.h"
#include "AccessibilityMenuListPopup.h"
#include "AccessibilityProgressIndicator.h"
#include "AccessibilityRenderObject.h"
+#include "AccessibilitySVGElement.h"
#include "AccessibilitySVGRoot.h"
#include "AccessibilityScrollView.h"
#include "AccessibilityScrollbar.h"
-#include "AccessibilitySearchFieldButtons.h"
#include "AccessibilitySlider.h"
#include "AccessibilitySpinButton.h"
#include "AccessibilityTable.h"
@@ -56,37 +59,61 @@
#include "AccessibilityTableColumn.h"
#include "AccessibilityTableHeaderContainer.h"
#include "AccessibilityTableRow.h"
+#include "AccessibilityTree.h"
+#include "AccessibilityTreeItem.h"
#include "Document.h"
#include "Editor.h"
+#include "ElementIterator.h"
#include "FocusController.h"
#include "Frame.h"
#include "HTMLAreaElement.h"
+#include "HTMLCanvasElement.h"
#include "HTMLImageElement.h"
#include "HTMLInputElement.h"
#include "HTMLLabelElement.h"
#include "HTMLMeterElement.h"
#include "HTMLNames.h"
+#include "InlineElementBox.h"
+#include "MathMLElement.h"
#include "Page.h"
+#include "RenderAttachment.h"
+#include "RenderLineBreak.h"
#include "RenderListBox.h"
+#include "RenderMathMLOperator.h"
#include "RenderMenuList.h"
#include "RenderMeter.h"
#include "RenderProgress.h"
+#include "RenderSVGRoot.h"
#include "RenderSlider.h"
#include "RenderTable.h"
#include "RenderTableCell.h"
#include "RenderTableRow.h"
#include "RenderView.h"
+#include "SVGElement.h"
#include "ScrollView.h"
-#include <wtf/PassRefPtr.h>
+#include "TextBoundaries.h"
+#include "TextIterator.h"
+#include "htmlediting.h"
+#include <wtf/DataLog.h>
#if ENABLE(VIDEO)
#include "MediaControlElements.h"
#endif
+#if COMPILER(MSVC)
+// See https://msdn.microsoft.com/en-us/library/1wea5zwe.aspx
+#pragma warning(disable: 4701)
+#endif
+
namespace WebCore {
using namespace HTMLNames;
+// Post value change notifications for password fields or elements contained in password fields at a 40hz interval to thwart analysis of typing cadence
+static double AccessibilityPasswordValueChangeNotificationInterval = 0.025;
+static double AccessibilityLiveRegionChangedNotificationInterval = 0.020;
+static double AccessibilityFocusAriaModalNodeNotificationInterval = 0.050;
+
AccessibilityObjectInclusion AXComputedObjectAttributeCache::getIgnored(AXID id) const
{
HashMap<AXID, CachedAXObjectAttributes>::const_iterator it = m_idMapping.find(id);
@@ -104,7 +131,34 @@ void AXComputedObjectAttributeCache::setIgnored(AXID id, AccessibilityObjectIncl
m_idMapping.set(id, attributes);
}
}
-
+
+AccessibilityReplacedText::AccessibilityReplacedText(const VisibleSelection& selection)
+{
+ if (AXObjectCache::accessibilityEnabled()) {
+ m_replacedRange.startIndex.value = indexForVisiblePosition(selection.start(), m_replacedRange.startIndex.scope);
+ if (selection.isRange()) {
+ m_replacedText = AccessibilityObject::stringForVisiblePositionRange(selection);
+ m_replacedRange.endIndex.value = indexForVisiblePosition(selection.end(), m_replacedRange.endIndex.scope);
+ } else
+ m_replacedRange.endIndex = m_replacedRange.startIndex;
+ }
+}
+
+void AccessibilityReplacedText::postTextStateChangeNotification(AXObjectCache* cache, AXTextEditType type, const String& text, const VisibleSelection& selection)
+{
+ if (!cache)
+ return;
+ if (!AXObjectCache::accessibilityEnabled())
+ return;
+
+ VisiblePosition position = selection.start();
+ auto* node = highestEditableRoot(position.deepEquivalent(), HasEditableAXRole);
+ if (m_replacedText.length())
+ cache->postTextReplacementNotification(node, AXTextEditTypeDelete, m_replacedText, type, text, position);
+ else
+ cache->postTextStateChangeNotification(node, type, text, position);
+}
+
bool AXObjectCache::gAccessibilityEnabled = false;
bool AXObjectCache::gAccessibilityEnhancedUserInterfaceEnabled = false;
@@ -118,15 +172,33 @@ void AXObjectCache::disableAccessibility()
gAccessibilityEnabled = false;
}
+void AXObjectCache::setEnhancedUserInterfaceAccessibility(bool flag)
+{
+ gAccessibilityEnhancedUserInterfaceEnabled = flag;
+#if PLATFORM(MAC)
+#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101100
+ if (flag)
+ enableAccessibility();
+#endif
+#endif
+}
+
AXObjectCache::AXObjectCache(Document& document)
: m_document(document)
- , m_notificationPostTimer(this, &AXObjectCache::notificationPostTimerFired)
+ , m_notificationPostTimer(*this, &AXObjectCache::notificationPostTimerFired)
+ , m_passwordNotificationPostTimer(*this, &AXObjectCache::passwordNotificationPostTimerFired)
+ , m_liveRegionChangedPostTimer(*this, &AXObjectCache::liveRegionChangedNotificationPostTimerFired)
+ , m_focusAriaModalNodeTimer(*this, &AXObjectCache::focusAriaModalNodeTimerFired)
+ , m_currentAriaModalNode(nullptr)
{
+ findAriaModalNodes();
}
AXObjectCache::~AXObjectCache()
{
m_notificationPostTimer.stop();
+ m_liveRegionChangedPostTimer.stop();
+ m_focusAriaModalNodeTimer.stop();
for (const auto& object : m_objects.values()) {
detachWrapper(object.get(), CacheDestroyed);
@@ -135,46 +207,131 @@ AXObjectCache::~AXObjectCache()
}
}
+void AXObjectCache::findAriaModalNodes()
+{
+ // Traverse the DOM tree to look for the aria-modal=true nodes.
+ for (Element* element = ElementTraversal::firstWithin(document().rootNode()); element; element = ElementTraversal::nextIncludingPseudo(*element)) {
+
+ // Must have dialog or alertdialog role
+ if (!nodeHasRole(element, "dialog") && !nodeHasRole(element, "alertdialog"))
+ continue;
+ if (!equalLettersIgnoringASCIICase(element->attributeWithoutSynchronization(aria_modalAttr), "true"))
+ continue;
+
+ m_ariaModalNodesSet.add(element);
+ }
+
+ // Set the current valid aria-modal node if possible.
+ updateCurrentAriaModalNode();
+}
+
+void AXObjectCache::updateCurrentAriaModalNode()
+{
+ // There might be multiple nodes with aria-modal=true set.
+ // We use this function to pick the one we want.
+ m_currentAriaModalNode = nullptr;
+ if (m_ariaModalNodesSet.isEmpty())
+ return;
+
+ // We only care about the nodes which are visible.
+ ListHashSet<RefPtr<Node>> visibleNodes;
+ for (auto& object : m_ariaModalNodesSet) {
+ if (isNodeVisible(object))
+ visibleNodes.add(object);
+ }
+
+ if (visibleNodes.isEmpty())
+ return;
+
+ // If any of the node are keyboard focused, we want to pick that.
+ Node* focusedNode = document().focusedElement();
+ for (auto& object : visibleNodes) {
+ if (focusedNode != nullptr && focusedNode->isDescendantOf(object.get())) {
+ m_currentAriaModalNode = object.get();
+ break;
+ }
+ }
+
+ // If none of the nodes are focused, we want to pick the last dialog in the DOM.
+ if (!m_currentAriaModalNode)
+ m_currentAriaModalNode = visibleNodes.last().get();
+}
+
+bool AXObjectCache::isNodeVisible(Node* node) const
+{
+ if (!is<Element>(node))
+ return false;
+
+ RenderObject* renderer = node->renderer();
+ if (!renderer)
+ return false;
+ const RenderStyle& style = renderer->style();
+ if (style.display() == NONE || style.visibility() != VISIBLE)
+ return false;
+
+ // We also need to consider aria hidden status.
+ if (!isNodeAriaVisible(node))
+ return false;
+
+ return true;
+}
+
+Node* AXObjectCache::ariaModalNode()
+{
+ // This function returns the valid aria modal node.
+ if (m_ariaModalNodesSet.isEmpty())
+ return nullptr;
+
+ // Check the current valid aria modal node first.
+ // Usually when one dialog sets aria-modal=true, that dialog is the one we want.
+ if (isNodeVisible(m_currentAriaModalNode))
+ return m_currentAriaModalNode;
+
+ // Recompute the valid aria modal node when m_currentAriaModalNode is null or hidden.
+ updateCurrentAriaModalNode();
+ return isNodeVisible(m_currentAriaModalNode) ? m_currentAriaModalNode : nullptr;
+}
+
AccessibilityObject* AXObjectCache::focusedImageMapUIElement(HTMLAreaElement* areaElement)
{
// Find the corresponding accessibility object for the HTMLAreaElement. This should be
// in the list of children for its corresponding image.
if (!areaElement)
- return 0;
+ return nullptr;
HTMLImageElement* imageElement = areaElement->imageElement();
if (!imageElement)
- return 0;
+ return nullptr;
AccessibilityObject* axRenderImage = areaElement->document().axObjectCache()->getOrCreate(imageElement);
if (!axRenderImage)
- return 0;
+ return nullptr;
for (const auto& child : axRenderImage->children()) {
- if (!child->isImageMapLink())
+ if (!is<AccessibilityImageMapLink>(*child))
continue;
- if (toAccessibilityImageMapLink(child.get())->areaElement() == areaElement)
+ if (downcast<AccessibilityImageMapLink>(*child).areaElement() == areaElement)
return child.get();
}
- return 0;
+ return nullptr;
}
AccessibilityObject* AXObjectCache::focusedUIElementForPage(const Page* page)
{
if (!gAccessibilityEnabled)
- return 0;
+ return nullptr;
// get the focused node in the page
Document* focusedDocument = page->focusController().focusedOrMainFrame().document();
Element* focusedElement = focusedDocument->focusedElement();
- if (focusedElement && isHTMLAreaElement(focusedElement))
- return focusedImageMapUIElement(toHTMLAreaElement(focusedElement));
+ if (is<HTMLAreaElement>(focusedElement))
+ return focusedImageMapUIElement(downcast<HTMLAreaElement>(focusedElement));
AccessibilityObject* obj = focusedDocument->axObjectCache()->getOrCreate(focusedElement ? static_cast<Node*>(focusedElement) : focusedDocument);
if (!obj)
- return 0;
+ return nullptr;
if (obj->shouldFocusActiveDescendant()) {
if (AccessibilityObject* descendant = obj->activeDescendant())
@@ -191,12 +348,12 @@ AccessibilityObject* AXObjectCache::focusedUIElementForPage(const Page* page)
AccessibilityObject* AXObjectCache::get(Widget* widget)
{
if (!widget)
- return 0;
+ return nullptr;
AXID axID = m_widgetObjectMapping.get(widget);
ASSERT(!HashTraits<AXID>::isDeletedValue(axID));
if (!axID)
- return 0;
+ return nullptr;
return m_objects.get(axID);
}
@@ -204,12 +361,12 @@ AccessibilityObject* AXObjectCache::get(Widget* widget)
AccessibilityObject* AXObjectCache::get(RenderObject* renderer)
{
if (!renderer)
- return 0;
+ return nullptr;
AXID axID = m_renderObjectMapping.get(renderer);
ASSERT(!HashTraits<AXID>::isDeletedValue(axID));
if (!axID)
- return 0;
+ return nullptr;
return m_objects.get(axID);
}
@@ -217,7 +374,7 @@ AccessibilityObject* AXObjectCache::get(RenderObject* renderer)
AccessibilityObject* AXObjectCache::get(Node* node)
{
if (!node)
- return 0;
+ return nullptr;
AXID renderID = node->renderer() ? m_renderObjectMapping.get(node->renderer()) : 0;
ASSERT(!HashTraits<AXID>::isDeletedValue(renderID));
@@ -230,14 +387,14 @@ AccessibilityObject* AXObjectCache::get(Node* node)
// rendered, but later something changes and it gets a renderer (like if it's
// reparented).
remove(nodeID);
- return 0;
+ return nullptr;
}
if (renderID)
return m_objects.get(renderID);
if (!nodeID)
- return 0;
+ return nullptr;
return m_objects.get(nodeID);
}
@@ -246,13 +403,19 @@ AccessibilityObject* AXObjectCache::get(Node* node)
// FIXME: This should take a const char*, but one caller passes nullAtom.
bool nodeHasRole(Node* node, const String& role)
{
- if (!node || !node->isElementNode())
+ if (!node || !is<Element>(node))
return false;
- return equalIgnoringCase(toElement(node)->fastGetAttribute(roleAttr), role);
+ auto& roleValue = downcast<Element>(*node).attributeWithoutSynchronization(roleAttr);
+ if (role.isNull())
+ return roleValue.isEmpty();
+ if (roleValue.isEmpty())
+ return false;
+
+ return SpaceSplitString(roleValue, true).contains(role);
}
-static PassRefPtr<AccessibilityObject> createFromRenderer(RenderObject* renderer)
+static Ref<AccessibilityObject> createFromRenderer(RenderObject* renderer)
{
// FIXME: How could renderer->node() ever not be an Element?
Node* node = renderer->node();
@@ -264,62 +427,80 @@ static PassRefPtr<AccessibilityObject> createFromRenderer(RenderObject* renderer
return AccessibilityList::create(renderer);
// aria tables
- if (nodeHasRole(node, "grid") || nodeHasRole(node, "treegrid"))
+ if (nodeHasRole(node, "grid") || nodeHasRole(node, "treegrid") || nodeHasRole(node, "table"))
return AccessibilityARIAGrid::create(renderer);
if (nodeHasRole(node, "row"))
return AccessibilityARIAGridRow::create(renderer);
- if (nodeHasRole(node, "gridcell") || nodeHasRole(node, "columnheader") || nodeHasRole(node, "rowheader"))
+ if (nodeHasRole(node, "gridcell") || nodeHasRole(node, "cell") || nodeHasRole(node, "columnheader") || nodeHasRole(node, "rowheader"))
return AccessibilityARIAGridCell::create(renderer);
+ // aria tree
+ if (nodeHasRole(node, "tree"))
+ return AccessibilityTree::create(renderer);
+ if (nodeHasRole(node, "treeitem"))
+ return AccessibilityTreeItem::create(renderer);
+
+ if (node && is<HTMLLabelElement>(node) && nodeHasRole(node, nullAtom))
+ return AccessibilityLabel::create(renderer);
+
#if ENABLE(VIDEO)
// media controls
if (node && node->isMediaControlElement())
return AccessibilityMediaControl::create(renderer);
#endif
-#if ENABLE(SVG)
- if (renderer->isSVGRoot())
+ if (is<RenderSVGRoot>(*renderer))
return AccessibilitySVGRoot::create(renderer);
-#endif
-
- // Search field buttons
- if (node && node->isElementNode() && toElement(node)->isSearchFieldCancelButtonElement())
- return AccessibilitySearchFieldCancelButton::create(renderer);
- if (renderer->isBoxModelObject()) {
- RenderBoxModelObject* cssBox = toRenderBoxModelObject(renderer);
- if (cssBox->isListBox())
- return AccessibilityListBox::create(toRenderListBox(cssBox));
- if (cssBox->isMenuList())
- return AccessibilityMenuList::create(toRenderMenuList(cssBox));
+ if (is<SVGElement>(node))
+ return AccessibilitySVGElement::create(renderer);
+
+#if ENABLE(MATHML)
+ // The mfenced element creates anonymous RenderMathMLOperators which should be treated
+ // as MathML elements and assigned the MathElementRole so that platform logic regarding
+ // inclusion and role mapping is not bypassed.
+ bool isAnonymousOperator = renderer->isAnonymous() && is<RenderMathMLOperator>(*renderer);
+ if (isAnonymousOperator || is<MathMLElement>(node))
+ return AccessibilityMathMLElement::create(renderer, isAnonymousOperator);
+#endif
+
+ if (is<RenderBoxModelObject>(*renderer)) {
+ RenderBoxModelObject& cssBox = downcast<RenderBoxModelObject>(*renderer);
+ if (is<RenderListBox>(cssBox))
+ return AccessibilityListBox::create(&downcast<RenderListBox>(cssBox));
+ if (is<RenderMenuList>(cssBox))
+ return AccessibilityMenuList::create(&downcast<RenderMenuList>(cssBox));
// standard tables
- if (cssBox->isTable())
- return AccessibilityTable::create(toRenderTable(cssBox));
- if (cssBox->isTableRow())
- return AccessibilityTableRow::create(toRenderTableRow(cssBox));
- if (cssBox->isTableCell())
- return AccessibilityTableCell::create(toRenderTableCell(cssBox));
-
-#if ENABLE(PROGRESS_ELEMENT)
+ if (is<RenderTable>(cssBox))
+ return AccessibilityTable::create(&downcast<RenderTable>(cssBox));
+ if (is<RenderTableRow>(cssBox))
+ return AccessibilityTableRow::create(&downcast<RenderTableRow>(cssBox));
+ if (is<RenderTableCell>(cssBox))
+ return AccessibilityTableCell::create(&downcast<RenderTableCell>(cssBox));
+
// progress bar
- if (cssBox->isProgress())
- return AccessibilityProgressIndicator::create(toRenderProgress(cssBox));
+ if (is<RenderProgress>(cssBox))
+ return AccessibilityProgressIndicator::create(&downcast<RenderProgress>(cssBox));
+
+#if ENABLE(ATTACHMENT_ELEMENT)
+ if (is<RenderAttachment>(cssBox))
+ return AccessibilityAttachment::create(&downcast<RenderAttachment>(cssBox));
#endif
#if ENABLE(METER_ELEMENT)
- if (cssBox->isMeter())
- return AccessibilityProgressIndicator::create(toRenderMeter(cssBox));
+ if (is<RenderMeter>(cssBox))
+ return AccessibilityProgressIndicator::create(&downcast<RenderMeter>(cssBox));
#endif
// input type=range
- if (cssBox->isSlider())
- return AccessibilitySlider::create(toRenderSlider(cssBox));
+ if (is<RenderSlider>(cssBox))
+ return AccessibilitySlider::create(&downcast<RenderSlider>(cssBox));
}
return AccessibilityRenderObject::create(renderer);
}
-static PassRefPtr<AccessibilityObject> createFromNode(Node* node)
+static Ref<AccessibilityObject> createFromNode(Node* node)
{
return AccessibilityNodeObject::create(node);
}
@@ -327,16 +508,16 @@ static PassRefPtr<AccessibilityObject> createFromNode(Node* node)
AccessibilityObject* AXObjectCache::getOrCreate(Widget* widget)
{
if (!widget)
- return 0;
+ return nullptr;
if (AccessibilityObject* obj = get(widget))
return obj;
- RefPtr<AccessibilityObject> newObj = 0;
- if (widget->isFrameView())
- newObj = AccessibilityScrollView::create(toScrollView(widget));
- else if (widget->isScrollbar())
- newObj = AccessibilityScrollbar::create(toScrollbar(widget));
+ RefPtr<AccessibilityObject> newObj;
+ if (is<ScrollView>(*widget))
+ newObj = AccessibilityScrollView::create(downcast<ScrollView>(widget));
+ else if (is<Scrollbar>(*widget))
+ newObj = AccessibilityScrollbar::create(downcast<Scrollbar>(widget));
// Will crash later if we have two objects for the same widget.
ASSERT(!get(widget));
@@ -344,7 +525,7 @@ AccessibilityObject* AXObjectCache::getOrCreate(Widget* widget)
// Catch the case if an (unsupported) widget type is used. Only FrameView and ScrollBar are supported now.
ASSERT(newObj);
if (!newObj)
- return 0;
+ return nullptr;
getAXID(newObj.get());
@@ -358,7 +539,7 @@ AccessibilityObject* AXObjectCache::getOrCreate(Widget* widget)
AccessibilityObject* AXObjectCache::getOrCreate(Node* node)
{
if (!node)
- return 0;
+ return nullptr;
if (AccessibilityObject* obj = get(node))
return obj;
@@ -367,20 +548,25 @@ AccessibilityObject* AXObjectCache::getOrCreate(Node* node)
return getOrCreate(node->renderer());
if (!node->parentElement())
- return 0;
+ return nullptr;
// It's only allowed to create an AccessibilityObject from a Node if it's in a canvas subtree.
// Or if it's a hidden element, but we still want to expose it because of other ARIA attributes.
- bool inCanvasSubtree = node->parentElement()->isInCanvasSubtree();
- bool isHidden = !node->renderer() && isNodeAriaVisible(node);
+ bool inCanvasSubtree = lineageOfType<HTMLCanvasElement>(*node->parentElement()).first();
+ bool isHidden = isNodeAriaVisible(node);
bool insideMeterElement = false;
#if ENABLE(METER_ELEMENT)
- insideMeterElement = isHTMLMeterElement(node->parentElement());
+ insideMeterElement = is<HTMLMeterElement>(*node->parentElement());
#endif
if (!inCanvasSubtree && !isHidden && !insideMeterElement)
- return 0;
+ return nullptr;
+
+ // Fallback content is only focusable as long as the canvas is displayed and visible.
+ // Update the style before Element::isFocusable() gets called.
+ if (inCanvasSubtree)
+ node->document().updateStyleIfNeeded();
RefPtr<AccessibilityObject> newObj = createFromNode(node);
@@ -405,7 +591,7 @@ AccessibilityObject* AXObjectCache::getOrCreate(Node* node)
AccessibilityObject* AXObjectCache::getOrCreate(RenderObject* renderer)
{
if (!renderer)
- return 0;
+ return nullptr;
if (AccessibilityObject* obj = get(renderer))
return obj;
@@ -433,7 +619,7 @@ AccessibilityObject* AXObjectCache::getOrCreate(RenderObject* renderer)
AccessibilityObject* AXObjectCache::rootObject()
{
if (!gAccessibilityEnabled)
- return 0;
+ return nullptr;
return getOrCreate(m_document.view());
}
@@ -441,16 +627,16 @@ AccessibilityObject* AXObjectCache::rootObject()
AccessibilityObject* AXObjectCache::rootObjectForFrame(Frame* frame)
{
if (!gAccessibilityEnabled)
- return 0;
+ return nullptr;
if (!frame)
- return 0;
+ return nullptr;
return getOrCreate(frame->view());
}
AccessibilityObject* AXObjectCache::getOrCreate(AccessibilityRole role)
{
- RefPtr<AccessibilityObject> obj = 0;
+ RefPtr<AccessibilityObject> obj = nullptr;
// will be filled in...
switch (role) {
@@ -482,13 +668,13 @@ AccessibilityObject* AXObjectCache::getOrCreate(AccessibilityRole role)
obj = AccessibilitySpinButtonPart::create();
break;
default:
- obj = 0;
+ obj = nullptr;
}
if (obj)
getAXID(obj.get());
else
- return 0;
+ return nullptr;
m_objects.set(obj->axObjectID(), obj);
obj->init();
@@ -525,6 +711,8 @@ void AXObjectCache::remove(RenderObject* renderer)
AXID axID = m_renderObjectMapping.get(renderer);
remove(axID);
m_renderObjectMapping.remove(renderer);
+ if (is<RenderBlock>(*renderer))
+ m_deferredIsIgnoredChangeList.remove(downcast<RenderBlock>(renderer));
}
void AXObjectCache::remove(Node* node)
@@ -539,6 +727,12 @@ void AXObjectCache::remove(Node* node)
remove(axID);
m_nodeObjectMapping.remove(node);
+ // Cleanup for aria modal nodes.
+ if (m_currentAriaModalNode == node)
+ m_currentAriaModalNode = nullptr;
+ if (m_ariaModalNodesSet.contains(node))
+ m_ariaModalNodesSet.remove(node);
+
if (node->renderer()) {
remove(node->renderer());
return;
@@ -556,7 +750,7 @@ void AXObjectCache::remove(Widget* view)
}
-#if !PLATFORM(WIN) || OS(WINCE)
+#if !PLATFORM(WIN)
AXID AXObjectCache::platformGenerateAXID() const
{
static AXID lastUsedID = 0;
@@ -641,8 +835,30 @@ void AXObjectCache::handleMenuOpened(Node* node)
postNotification(getOrCreate(node), &document(), AXMenuOpened);
}
-void AXObjectCache::childrenChanged(Node* node)
+void AXObjectCache::handleLiveRegionCreated(Node* node)
+{
+ if (!is<Element>(node) || !node->renderer())
+ return;
+
+ Element* element = downcast<Element>(node);
+ String liveRegionStatus = element->attributeWithoutSynchronization(aria_liveAttr);
+ if (liveRegionStatus.isEmpty()) {
+ const AtomicString& ariaRole = element->attributeWithoutSynchronization(roleAttr);
+ if (!ariaRole.isEmpty())
+ liveRegionStatus = AccessibilityObject::defaultLiveRegionStatusForRole(AccessibilityObject::ariaRoleToWebCoreRole(ariaRole));
+ }
+
+ if (AccessibilityObject::liveRegionStatusIsEnabled(liveRegionStatus))
+ postNotification(getOrCreate(node), &document(), AXLiveRegionCreated);
+}
+
+void AXObjectCache::childrenChanged(Node* node, Node* newChild)
{
+ if (newChild) {
+ handleMenuOpened(newChild);
+ handleLiveRegionCreated(newChild);
+ }
+
childrenChanged(get(node));
}
@@ -650,8 +866,11 @@ void AXObjectCache::childrenChanged(RenderObject* renderer, RenderObject* newChi
{
if (!renderer)
return;
- if (newChild)
+
+ if (newChild) {
handleMenuOpened(newChild->node());
+ handleLiveRegionCreated(newChild->node());
+ }
childrenChanged(get(renderer));
}
@@ -664,15 +883,14 @@ void AXObjectCache::childrenChanged(AccessibilityObject* obj)
obj->childrenChanged();
}
-void AXObjectCache::notificationPostTimerFired(Timer<AXObjectCache>&)
+void AXObjectCache::notificationPostTimerFired()
{
Ref<Document> protectorForCacheOwner(m_document);
m_notificationPostTimer.stop();
- // In DRT, posting notifications has a tendency to immediately queue up other notifications, which can lead to unexpected behavior
+ // In tests, posting notifications has a tendency to immediately queue up other notifications, which can lead to unexpected behavior
// when the notification list is cleared at the end. Instead copy this list at the start.
- auto notifications = m_notificationsToPost;
- m_notificationsToPost.clear();
+ auto notifications = WTFMove(m_notificationsToPost);
for (const auto& note : notifications) {
AccessibilityObject* obj = note.first.get();
@@ -685,21 +903,42 @@ void AXObjectCache::notificationPostTimerFired(Timer<AXObjectCache>&)
#ifndef NDEBUG
// Make sure none of the render views are in the process of being layed out.
// Notifications should only be sent after the renderer has finished
- if (obj->isAccessibilityRenderObject()) {
- AccessibilityRenderObject* renderObj = toAccessibilityRenderObject(obj);
- RenderObject* renderer = renderObj->renderer();
- if (renderer)
+ if (is<AccessibilityRenderObject>(*obj)) {
+ if (auto* renderer = downcast<AccessibilityRenderObject>(*obj).renderer())
ASSERT(!renderer->view().layoutState());
}
#endif
AXNotification notification = note.second;
+
+ // Ensure that this menu really is a menu. We do this check here so that we don't have to create
+ // the axChildren when the menu is marked as opening.
+ if (notification == AXMenuOpened) {
+ obj->updateChildrenIfNecessary();
+ if (obj->roleValue() != MenuRole)
+ continue;
+ }
+
postPlatformNotification(obj, notification);
if (notification == AXChildrenChanged && obj->parentObjectIfExists() && obj->lastKnownIsIgnoredValue() != obj->accessibilityIsIgnored())
childrenChanged(obj->parentObject());
}
}
+
+void AXObjectCache::passwordNotificationPostTimerFired()
+{
+#if PLATFORM(COCOA)
+ m_passwordNotificationPostTimer.stop();
+
+ // In tests, posting notifications has a tendency to immediately queue up other notifications, which can lead to unexpected behavior
+ // when the notification list is cleared at the end. Instead copy this list at the start.
+ auto notifications = WTFMove(m_passwordNotificationsToPost);
+
+ for (auto& notification : notifications)
+ postTextStateChangePlatformNotification(notification.get(), AXTextEditTypeInsert, " ", VisiblePosition());
+#endif
+}
void AXObjectCache::postNotification(RenderObject* renderer, AXNotification notification, PostTarget postTarget, PostType postType)
{
@@ -777,7 +1016,7 @@ void AXObjectCache::handleMenuItemSelected(Node* node)
if (!nodeHasRole(node, "menuitem") && !nodeHasRole(node, "menuitemradio") && !nodeHasRole(node, "menuitemcheckbox"))
return;
- if (!toElement(node)->focused() && !equalIgnoringCase(toElement(node)->fastGetAttribute(aria_selectedAttr), "true"))
+ if (!downcast<Element>(*node).focused() && !equalLettersIgnoringASCIICase(downcast<Element>(*node).attributeWithoutSynchronization(aria_selectedAttr), "true"))
return;
postNotification(getOrCreate(node), &document(), AXMenuListItemSelected);
@@ -808,16 +1047,273 @@ void AXObjectCache::selectedChildrenChanged(RenderObject* renderer)
postNotification(renderer, AXSelectedChildrenChanged, TargetObservableParent);
}
-void AXObjectCache::nodeTextChangeNotification(Node* node, AXTextChange textChange, unsigned offset, const String& text)
+#ifndef NDEBUG
+void AXObjectCache::showIntent(const AXTextStateChangeIntent &intent)
+{
+ switch (intent.type) {
+ case AXTextStateChangeTypeUnknown:
+ dataLog("Unknown");
+ break;
+ case AXTextStateChangeTypeEdit:
+ dataLog("Edit::");
+ break;
+ case AXTextStateChangeTypeSelectionMove:
+ dataLog("Move::");
+ break;
+ case AXTextStateChangeTypeSelectionExtend:
+ dataLog("Extend::");
+ break;
+ case AXTextStateChangeTypeSelectionBoundary:
+ dataLog("Boundary::");
+ break;
+ }
+ switch (intent.type) {
+ case AXTextStateChangeTypeUnknown:
+ break;
+ case AXTextStateChangeTypeEdit:
+ switch (intent.change) {
+ case AXTextEditTypeUnknown:
+ dataLog("Unknown");
+ break;
+ case AXTextEditTypeDelete:
+ dataLog("Delete");
+ break;
+ case AXTextEditTypeInsert:
+ dataLog("Insert");
+ break;
+ case AXTextEditTypeDictation:
+ dataLog("DictationInsert");
+ break;
+ case AXTextEditTypeTyping:
+ dataLog("TypingInsert");
+ break;
+ case AXTextEditTypeCut:
+ dataLog("Cut");
+ break;
+ case AXTextEditTypePaste:
+ dataLog("Paste");
+ break;
+ case AXTextEditTypeAttributesChange:
+ dataLog("AttributesChange");
+ break;
+ }
+ break;
+ case AXTextStateChangeTypeSelectionMove:
+ case AXTextStateChangeTypeSelectionExtend:
+ case AXTextStateChangeTypeSelectionBoundary:
+ switch (intent.selection.direction) {
+ case AXTextSelectionDirectionUnknown:
+ dataLog("Unknown::");
+ break;
+ case AXTextSelectionDirectionBeginning:
+ dataLog("Beginning::");
+ break;
+ case AXTextSelectionDirectionEnd:
+ dataLog("End::");
+ break;
+ case AXTextSelectionDirectionPrevious:
+ dataLog("Previous::");
+ break;
+ case AXTextSelectionDirectionNext:
+ dataLog("Next::");
+ break;
+ case AXTextSelectionDirectionDiscontiguous:
+ dataLog("Discontiguous::");
+ break;
+ }
+ switch (intent.selection.direction) {
+ case AXTextSelectionDirectionUnknown:
+ case AXTextSelectionDirectionBeginning:
+ case AXTextSelectionDirectionEnd:
+ case AXTextSelectionDirectionPrevious:
+ case AXTextSelectionDirectionNext:
+ switch (intent.selection.granularity) {
+ case AXTextSelectionGranularityUnknown:
+ dataLog("Unknown");
+ break;
+ case AXTextSelectionGranularityCharacter:
+ dataLog("Character");
+ break;
+ case AXTextSelectionGranularityWord:
+ dataLog("Word");
+ break;
+ case AXTextSelectionGranularityLine:
+ dataLog("Line");
+ break;
+ case AXTextSelectionGranularitySentence:
+ dataLog("Sentence");
+ break;
+ case AXTextSelectionGranularityParagraph:
+ dataLog("Paragraph");
+ break;
+ case AXTextSelectionGranularityPage:
+ dataLog("Page");
+ break;
+ case AXTextSelectionGranularityDocument:
+ dataLog("Document");
+ break;
+ case AXTextSelectionGranularityAll:
+ dataLog("All");
+ break;
+ }
+ break;
+ case AXTextSelectionDirectionDiscontiguous:
+ break;
+ }
+ break;
+ }
+ dataLog("\n");
+}
+#endif
+
+void AXObjectCache::setTextSelectionIntent(const AXTextStateChangeIntent& intent)
+{
+ m_textSelectionIntent = intent;
+}
+
+void AXObjectCache::setIsSynchronizingSelection(bool isSynchronizing)
+{
+ m_isSynchronizingSelection = isSynchronizing;
+}
+
+static bool isPasswordFieldOrContainedByPasswordField(AccessibilityObject* object)
+{
+ return object && (object->isPasswordField() || object->isContainedByPasswordField());
+}
+
+void AXObjectCache::postTextStateChangeNotification(Node* node, const AXTextStateChangeIntent& intent, const VisibleSelection& selection)
{
if (!node)
return;
+#if PLATFORM(COCOA)
stopCachingComputedObjectAttributes();
- // Delegate on the right platform
- AccessibilityObject* obj = getOrCreate(node);
- nodeTextChangePlatformNotification(obj, textChange, offset, text);
+ postTextStateChangeNotification(getOrCreate(node), intent, selection);
+#else
+ postNotification(node->renderer(), AXObjectCache::AXSelectedTextChanged, TargetObservableParent);
+ UNUSED_PARAM(intent);
+ UNUSED_PARAM(selection);
+#endif
+}
+
+void AXObjectCache::postTextStateChangeNotification(const Position& position, const AXTextStateChangeIntent& intent, const VisibleSelection& selection)
+{
+ Node* node = position.deprecatedNode();
+ if (!node)
+ return;
+
+ stopCachingComputedObjectAttributes();
+
+#if PLATFORM(COCOA)
+ AccessibilityObject* object = getOrCreate(node);
+ if (object && object->accessibilityIsIgnored()) {
+ if (position.atLastEditingPositionForNode()) {
+ if (AccessibilityObject* nextSibling = object->nextSiblingUnignored(1))
+ object = nextSibling;
+ } else if (position.atFirstEditingPositionForNode()) {
+ if (AccessibilityObject* previousSibling = object->previousSiblingUnignored(1))
+ object = previousSibling;
+ }
+ }
+
+ postTextStateChangeNotification(object, intent, selection);
+#else
+ postTextStateChangeNotification(node, intent, selection);
+#endif
+}
+
+void AXObjectCache::postTextStateChangeNotification(AccessibilityObject* object, const AXTextStateChangeIntent& intent, const VisibleSelection& selection)
+{
+ stopCachingComputedObjectAttributes();
+
+#if PLATFORM(COCOA)
+ if (object) {
+ if (isPasswordFieldOrContainedByPasswordField(object))
+ return;
+
+ if (auto observableObject = object->observableObject())
+ object = observableObject;
+ }
+
+ const AXTextStateChangeIntent& newIntent = (intent.type == AXTextStateChangeTypeUnknown || (m_isSynchronizingSelection && m_textSelectionIntent.type != AXTextStateChangeTypeUnknown)) ? m_textSelectionIntent : intent;
+ postTextStateChangePlatformNotification(object, newIntent, selection);
+#else
+ UNUSED_PARAM(object);
+ UNUSED_PARAM(intent);
+ UNUSED_PARAM(selection);
+#endif
+
+ setTextSelectionIntent(AXTextStateChangeIntent());
+ setIsSynchronizingSelection(false);
+}
+
+void AXObjectCache::postTextStateChangeNotification(Node* node, AXTextEditType type, const String& text, const VisiblePosition& position)
+{
+ if (!node)
+ return;
+ if (type == AXTextEditTypeUnknown)
+ return;
+
+ stopCachingComputedObjectAttributes();
+
+ AccessibilityObject* object = getOrCreate(node);
+#if PLATFORM(COCOA)
+ if (object) {
+ if (enqueuePasswordValueChangeNotification(object))
+ return;
+ object = object->observableObject();
+ }
+
+ postTextStateChangePlatformNotification(object, type, text, position);
+#else
+ nodeTextChangePlatformNotification(object, textChangeForEditType(type), position.deepEquivalent().deprecatedEditingOffset(), text);
+#endif
+}
+
+void AXObjectCache::postTextReplacementNotification(Node* node, AXTextEditType deletionType, const String& deletedText, AXTextEditType insertionType, const String& insertedText, const VisiblePosition& position)
+{
+ if (!node)
+ return;
+ if (deletionType != AXTextEditTypeDelete)
+ return;
+ if (!(insertionType == AXTextEditTypeInsert || insertionType == AXTextEditTypeTyping || insertionType == AXTextEditTypeDictation || insertionType == AXTextEditTypePaste))
+ return;
+
+ stopCachingComputedObjectAttributes();
+
+ AccessibilityObject* object = getOrCreate(node);
+#if PLATFORM(COCOA)
+ if (object) {
+ if (enqueuePasswordValueChangeNotification(object))
+ return;
+ object = object->observableObject();
+ }
+
+ postTextReplacementPlatformNotification(object, deletionType, deletedText, insertionType, insertedText, position);
+#else
+ nodeTextChangePlatformNotification(object, textChangeForEditType(deletionType), position.deepEquivalent().deprecatedEditingOffset(), deletedText);
+ nodeTextChangePlatformNotification(object, textChangeForEditType(insertionType), position.deepEquivalent().deprecatedEditingOffset(), insertedText);
+#endif
+}
+
+bool AXObjectCache::enqueuePasswordValueChangeNotification(AccessibilityObject* object)
+{
+ if (!isPasswordFieldOrContainedByPasswordField(object))
+ return false;
+
+ AccessibilityObject* observableObject = object->observableObject();
+ if (!observableObject) {
+ ASSERT_NOT_REACHED();
+ // return true even though the enqueue didn't happen because this is a password field and caller shouldn't post a notification
+ return true;
+ }
+
+ m_passwordNotificationsToPost.add(observableObject);
+ if (!m_passwordNotificationPostTimer.isActive())
+ m_passwordNotificationPostTimer.startOneShot(AccessibilityPasswordValueChangeNotificationInterval);
+
+ return true;
}
void AXObjectCache::frameLoadingEventNotification(Frame* frame, AXLoadingEvent loadingEvent)
@@ -834,6 +1330,67 @@ void AXObjectCache::frameLoadingEventNotification(Frame* frame, AXLoadingEvent l
frameLoadingEventPlatformNotification(obj, loadingEvent);
}
+void AXObjectCache::postLiveRegionChangeNotification(AccessibilityObject* object)
+{
+ if (m_liveRegionChangedPostTimer.isActive())
+ m_liveRegionChangedPostTimer.stop();
+
+ if (!m_liveRegionObjectsSet.contains(object))
+ m_liveRegionObjectsSet.add(object);
+
+ m_liveRegionChangedPostTimer.startOneShot(AccessibilityLiveRegionChangedNotificationInterval);
+}
+
+void AXObjectCache::liveRegionChangedNotificationPostTimerFired()
+{
+ m_liveRegionChangedPostTimer.stop();
+
+ if (m_liveRegionObjectsSet.isEmpty())
+ return;
+
+ for (auto& object : m_liveRegionObjectsSet)
+ postNotification(object.get(), object->document(), AXObjectCache::AXLiveRegionChanged);
+ m_liveRegionObjectsSet.clear();
+}
+
+static AccessibilityObject* firstFocusableChild(AccessibilityObject* obj)
+{
+ if (!obj)
+ return nullptr;
+
+ for (auto* child = obj->firstChild(); child; child = child->nextSibling()) {
+ if (child->canSetFocusAttribute())
+ return child;
+ if (AccessibilityObject* focusable = firstFocusableChild(child))
+ return focusable;
+ }
+ return nullptr;
+}
+
+void AXObjectCache::focusAriaModalNode()
+{
+ if (m_focusAriaModalNodeTimer.isActive())
+ m_focusAriaModalNodeTimer.stop();
+
+ m_focusAriaModalNodeTimer.startOneShot(AccessibilityFocusAriaModalNodeNotificationInterval);
+}
+
+void AXObjectCache::focusAriaModalNodeTimerFired()
+{
+ if (!m_currentAriaModalNode)
+ return;
+
+ // Don't set focus if we are already focusing onto some element within
+ // the dialog.
+ if (m_currentAriaModalNode->contains(document().focusedElement()))
+ return;
+
+ if (AccessibilityObject* currentAriaModalNodeObject = getOrCreate(m_currentAriaModalNode)) {
+ if (AccessibilityObject* focusable = firstFocusableChild(currentAriaModalNodeObject))
+ focusable->setFocused(true);
+ }
+}
+
void AXObjectCache::handleScrollbarUpdate(ScrollView* view)
{
if (!view)
@@ -874,7 +1431,7 @@ void AXObjectCache::handleAttributeChanged(const QualifiedName& attrName, Elemen
handleAriaRoleChanged(element);
else if (attrName == altAttr || attrName == titleAttr)
textChanged(element);
- else if (attrName == forAttr && isHTMLLabelElement(element))
+ else if (attrName == forAttr && is<HTMLLabelElement>(*element))
labelChanged(element);
if (!attrName.localName().string().startsWith("aria-"))
@@ -895,17 +1452,44 @@ void AXObjectCache::handleAttributeChanged(const QualifiedName& attrName, Elemen
else if (attrName == aria_expandedAttr)
handleAriaExpandedChange(element);
else if (attrName == aria_hiddenAttr)
- childrenChanged(element->parentNode());
+ childrenChanged(element->parentNode(), element);
else if (attrName == aria_invalidAttr)
postNotification(element, AXObjectCache::AXInvalidStatusChanged);
+ else if (attrName == aria_modalAttr)
+ handleAriaModalChange(element);
else
postNotification(element, AXObjectCache::AXAriaAttributeChanged);
}
+void AXObjectCache::handleAriaModalChange(Node* node)
+{
+ if (!is<Element>(node))
+ return;
+
+ if (!nodeHasRole(node, "dialog") && !nodeHasRole(node, "alertdialog"))
+ return;
+
+ stopCachingComputedObjectAttributes();
+ if (equalLettersIgnoringASCIICase(downcast<Element>(*node).attributeWithoutSynchronization(aria_modalAttr), "true")) {
+ // Add the newly modified node to the modal nodes set, and set it to be the current valid aria modal node.
+ // We will recompute the current valid aria modal node in ariaModalNode() when this node is not visible.
+ m_ariaModalNodesSet.add(node);
+ m_currentAriaModalNode = node;
+ } else {
+ // Remove the node from the modal nodes set. There might be other visible modal nodes, so we recompute here.
+ m_ariaModalNodesSet.remove(node);
+ updateCurrentAriaModalNode();
+ }
+ if (m_currentAriaModalNode)
+ focusAriaModalNode();
+
+ startCachingComputedObjectAttributesUntilTreeMutates();
+}
+
void AXObjectCache::labelChanged(Element* element)
{
- ASSERT(isHTMLLabelElement(element));
- HTMLElement* correspondingControl = toHTMLLabelElement(element)->control();
+ ASSERT(is<HTMLLabelElement>(*element));
+ HTMLElement* correspondingControl = downcast<HTMLLabelElement>(*element).control();
textChanged(correspondingControl);
}
@@ -944,11 +1528,555 @@ VisiblePosition AXObjectCache::visiblePositionForTextMarkerData(TextMarkerData&
AXObjectCache* cache = renderer->document().axObjectCache();
if (!cache->isIDinUse(textMarkerData.axID))
return VisiblePosition();
+
+ return visiblePos;
+}
+
+CharacterOffset AXObjectCache::characterOffsetForTextMarkerData(TextMarkerData& textMarkerData)
+{
+ if (!isNodeInUse(textMarkerData.node))
+ return CharacterOffset();
+
+ if (textMarkerData.ignored)
+ return CharacterOffset();
+
+ CharacterOffset result = CharacterOffset(textMarkerData.node, textMarkerData.characterStartIndex, textMarkerData.characterOffset);
+ // When we are at a line wrap and the VisiblePosition is upstream, it means the text marker is at the end of the previous line.
+ // We use the previous CharacterOffset so that it will match the Range.
+ if (textMarkerData.affinity == UPSTREAM)
+ return previousCharacterOffset(result, false);
+ return result;
+}
+
+CharacterOffset AXObjectCache::traverseToOffsetInRange(RefPtr<Range>range, int offset, TraverseOption option, bool stayWithinRange)
+{
+ if (!range)
+ return CharacterOffset();
+
+ bool toNodeEnd = option & TraverseOptionToNodeEnd;
+ bool validateOffset = option & TraverseOptionValidateOffset;
+
+ int offsetInCharacter = 0;
+ int cumulativeOffset = 0;
+ int remaining = 0;
+ int lastLength = 0;
+ Node* currentNode = nullptr;
+ bool finished = false;
+ int lastStartOffset = 0;
+
+ TextIterator iterator(range.get(), TextIteratorEntersTextControls);
+
+ // When the range has zero length, there might be replaced node or brTag that we need to increment the characterOffset.
+ if (iterator.atEnd()) {
+ currentNode = &range->startContainer();
+ lastStartOffset = range->startOffset();
+ if (offset > 0 || toNodeEnd) {
+ if (AccessibilityObject::replacedNodeNeedsCharacter(currentNode) || (currentNode->renderer() && currentNode->renderer()->isBR()))
+ cumulativeOffset++;
+ lastLength = cumulativeOffset;
+
+ // When going backwards, stayWithinRange is false.
+ // Here when we don't have any character to move and we are going backwards, we traverse to the previous node.
+ if (!lastLength && toNodeEnd && !stayWithinRange) {
+ if (Node* preNode = previousNode(currentNode))
+ return traverseToOffsetInRange(rangeForNodeContents(preNode), offset, option);
+ return CharacterOffset();
+ }
+ }
+ }
+
+ // Sometimes text contents in a node are splitted into several iterations, so that iterator.range()->startOffset()
+ // might not be the correct character count. Here we use a previousNode object to keep track of that.
+ Node* previousNode = nullptr;
+ for (; !iterator.atEnd(); iterator.advance()) {
+ int currentLength = iterator.text().length();
+ bool hasReplacedNodeOrBR = false;
+
+ Node& node = iterator.range()->startContainer();
+ currentNode = &node;
+
+ // When currentLength == 0, we check if there's any replaced node.
+ // If not, we skip the node with no length.
+ if (!currentLength) {
+ int subOffset = iterator.range()->startOffset();
+ Node* childNode = node.traverseToChildAt(subOffset);
+ if (AccessibilityObject::replacedNodeNeedsCharacter(childNode)) {
+ cumulativeOffset++;
+ currentLength++;
+ currentNode = childNode;
+ hasReplacedNodeOrBR = true;
+ } else
+ continue;
+ } else {
+ // Ignore space, new line, tag node.
+ if (currentLength == 1) {
+ if (isSpaceOrNewline(iterator.text()[0])) {
+ // If the node has BR tag, we want to set the currentNode to it.
+ int subOffset = iterator.range()->startOffset();
+ Node* childNode = node.traverseToChildAt(subOffset);
+ if (childNode && childNode->renderer() && childNode->renderer()->isBR()) {
+ currentNode = childNode;
+ hasReplacedNodeOrBR = true;
+ } else if (currentNode->isShadowRoot()) {
+ // Since we are entering text controls, we should set the currentNode
+ // to be the shadow host when there's no content.
+ currentNode = currentNode->shadowHost();
+ continue;
+ } else if (currentNode != previousNode) {
+ // We should set the start offset and length for the current node in case this is the last iteration.
+ lastStartOffset = 1;
+ lastLength = 0;
+ continue;
+ }
+ }
+ }
+ cumulativeOffset += currentLength;
+ }
+
+ if (currentNode == previousNode) {
+ lastLength += currentLength;
+ lastStartOffset = iterator.range()->endOffset() - lastLength;
+ }
+ else {
+ lastLength = currentLength;
+ lastStartOffset = hasReplacedNodeOrBR ? 0 : iterator.range()->startOffset();
+ }
+
+ // Break early if we have advanced enough characters.
+ bool offsetLimitReached = validateOffset ? cumulativeOffset + lastStartOffset >= offset : cumulativeOffset >= offset;
+ if (!toNodeEnd && offsetLimitReached) {
+ offsetInCharacter = validateOffset ? std::max(offset - lastStartOffset, 0) : offset - (cumulativeOffset - lastLength);
+ finished = true;
+ break;
+ }
+ previousNode = currentNode;
+ }
+
+ if (!finished) {
+ offsetInCharacter = lastLength;
+ if (!toNodeEnd)
+ remaining = offset - cumulativeOffset;
+ }
+
+ // Sometimes when we are getting the end CharacterOffset of a line range, the TextIterator will emit an extra space at the end
+ // and make the character count greater than the Range's end offset.
+ if (toNodeEnd && currentNode->isTextNode() && currentNode == &range->endContainer() && static_cast<int>(range->endOffset()) < lastStartOffset + offsetInCharacter)
+ offsetInCharacter = range->endOffset() - lastStartOffset;
+
+ return CharacterOffset(currentNode, lastStartOffset, offsetInCharacter, remaining);
+}
+
+int AXObjectCache::lengthForRange(Range* range)
+{
+ if (!range)
+ return -1;
+
+ int length = 0;
+ for (TextIterator it(range); !it.atEnd(); it.advance()) {
+ // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
+ if (it.text().length())
+ length += it.text().length();
+ else {
+ // locate the node and starting offset for this replaced range
+ Node& node = it.range()->startContainer();
+ int offset = it.range()->startOffset();
+ if (AccessibilityObject::replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
+ ++length;
+ }
+ }
+
+ return length;
+}
+
+RefPtr<Range> AXObjectCache::rangeForNodeContents(Node* node)
+{
+ if (!node)
+ return nullptr;
+ Document* document = &node->document();
+ if (!document)
+ return nullptr;
+ auto range = Range::create(*document);
+ if (AccessibilityObject::replacedNodeNeedsCharacter(node)) {
+ // For replaced nodes without children, the node itself is included in the range.
+ if (range->selectNode(*node).hasException())
+ return nullptr;
+ } else {
+ if (range->selectNodeContents(*node).hasException())
+ return nullptr;
+ }
+ return WTFMove(range);
+}
+
+static bool isReplacedNodeOrBR(Node* node)
+{
+ return node && (AccessibilityObject::replacedNodeNeedsCharacter(node) || node->hasTagName(brTag));
+}
+
+static bool characterOffsetsInOrder(const CharacterOffset& characterOffset1, const CharacterOffset& characterOffset2)
+{
+ if (characterOffset1.isNull() || characterOffset2.isNull())
+ return false;
+
+ if (characterOffset1.node == characterOffset2.node)
+ return characterOffset1.offset <= characterOffset2.offset;
+
+ Node* node1 = characterOffset1.node;
+ Node* node2 = characterOffset2.node;
+ if (!node1->offsetInCharacters() && !isReplacedNodeOrBR(node1) && node1->hasChildNodes())
+ node1 = node1->traverseToChildAt(characterOffset1.offset);
+ if (!node2->offsetInCharacters() && !isReplacedNodeOrBR(node2) && node2->hasChildNodes())
+ node2 = node2->traverseToChildAt(characterOffset2.offset);
+
+ if (!node1 || !node2)
+ return false;
+
+ RefPtr<Range> range1 = AXObjectCache::rangeForNodeContents(node1);
+ RefPtr<Range> range2 = AXObjectCache::rangeForNodeContents(node2);
+
+ if (!range2)
+ return true;
+ if (!range1)
+ return false;
+ auto result = range1->compareBoundaryPoints(Range::START_TO_START, *range2);
+ if (result.hasException())
+ return true;
+ return result.releaseReturnValue() <= 0;
+}
+
+static Node* resetNodeAndOffsetForReplacedNode(Node* replacedNode, int& offset, int characterCount)
+{
+ // Use this function to include the replaced node itself in the range we are creating.
+ if (!replacedNode)
+ return nullptr;
+
+ RefPtr<Range> nodeRange = AXObjectCache::rangeForNodeContents(replacedNode);
+ int nodeLength = TextIterator::rangeLength(nodeRange.get());
+ offset = characterCount <= nodeLength ? replacedNode->computeNodeIndex() : replacedNode->computeNodeIndex() + 1;
+ return replacedNode->parentNode();
+}
+
+static bool setRangeStartOrEndWithCharacterOffset(Range& range, const CharacterOffset& characterOffset, bool isStart)
+{
+ if (characterOffset.isNull())
+ return false;
+
+ int offset = characterOffset.startIndex + characterOffset.offset;
+ Node* node = characterOffset.node;
+ ASSERT(node);
+
+ bool replacedNodeOrBR = isReplacedNodeOrBR(node);
+ // For the non text node that has no children, we should create the range with its parent, otherwise the range would be collapsed.
+ // Example: <div contenteditable="true"></div>, we want the range to include the div element.
+ bool noChildren = !replacedNodeOrBR && !node->isTextNode() && !node->hasChildNodes();
+ int characterCount = noChildren ? (isStart ? 0 : 1) : characterOffset.offset;
- if (deepPos.deprecatedNode() != textMarkerData.node || deepPos.deprecatedEditingOffset() != textMarkerData.offset)
+ if (replacedNodeOrBR || noChildren)
+ node = resetNodeAndOffsetForReplacedNode(node, offset, characterCount);
+
+ if (!node)
+ return false;
+
+ if (isStart) {
+ if (range.setStart(*node, offset).hasException())
+ return false;
+ } else {
+ if (range.setEnd(*node, offset).hasException())
+ return false;
+ }
+
+ return true;
+}
+
+RefPtr<Range> AXObjectCache::rangeForUnorderedCharacterOffsets(const CharacterOffset& characterOffset1, const CharacterOffset& characterOffset2)
+{
+ if (characterOffset1.isNull() || characterOffset2.isNull())
+ return nullptr;
+
+ bool alreadyInOrder = characterOffsetsInOrder(characterOffset1, characterOffset2);
+ CharacterOffset startCharacterOffset = alreadyInOrder ? characterOffset1 : characterOffset2;
+ CharacterOffset endCharacterOffset = alreadyInOrder ? characterOffset2 : characterOffset1;
+
+ auto result = Range::create(m_document);
+ if (!setRangeStartOrEndWithCharacterOffset(result, startCharacterOffset, true))
+ return nullptr;
+ if (!setRangeStartOrEndWithCharacterOffset(result, endCharacterOffset, false))
+ return nullptr;
+ return WTFMove(result);
+}
+
+void AXObjectCache::setTextMarkerDataWithCharacterOffset(TextMarkerData& textMarkerData, const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return;
+
+ Node* domNode = characterOffset.node;
+ if (is<HTMLInputElement>(*domNode) && downcast<HTMLInputElement>(*domNode).isPasswordField()) {
+ textMarkerData.ignored = true;
+ return;
+ }
+
+ RefPtr<AccessibilityObject> obj = this->getOrCreate(domNode);
+ if (!obj)
+ return;
+
+ // Convert to visible position.
+ VisiblePosition visiblePosition = visiblePositionFromCharacterOffset(characterOffset);
+ int vpOffset = 0;
+ if (!visiblePosition.isNull()) {
+ Position deepPos = visiblePosition.deepEquivalent();
+ vpOffset = deepPos.deprecatedEditingOffset();
+ }
+
+ textMarkerData.axID = obj.get()->axObjectID();
+ textMarkerData.node = domNode;
+ textMarkerData.characterOffset = characterOffset.offset;
+ textMarkerData.characterStartIndex = characterOffset.startIndex;
+ textMarkerData.offset = vpOffset;
+ textMarkerData.affinity = visiblePosition.affinity();
+
+ this->setNodeInUse(domNode);
+}
+
+CharacterOffset AXObjectCache::startOrEndCharacterOffsetForRange(RefPtr<Range> range, bool isStart)
+{
+ if (!range)
+ return CharacterOffset();
+
+ // When getting the end CharacterOffset at node boundary, we don't want to collapse to the previous node.
+ if (!isStart && !range->endOffset())
+ return characterOffsetForNodeAndOffset(range->endContainer(), 0, TraverseOptionIncludeStart);
+
+ // If it's end text marker, we want to go to the end of the range, and stay within the range.
+ bool stayWithinRange = !isStart;
+
+ Node& endNode = range->endContainer();
+ if (endNode.offsetInCharacters() && !isStart)
+ return traverseToOffsetInRange(rangeForNodeContents(&endNode), range->endOffset(), TraverseOptionValidateOffset);
+
+ Ref<Range> copyRange = *range;
+ // Change the start of the range, so the character offset starts from node beginning.
+ int offset = 0;
+ Node& node = copyRange->startContainer();
+ if (node.offsetInCharacters()) {
+ CharacterOffset nodeStartOffset = traverseToOffsetInRange(rangeForNodeContents(&node), range->startOffset(), TraverseOptionValidateOffset);
+ if (isStart)
+ return nodeStartOffset;
+ copyRange = Range::create(range->ownerDocument(), &range->startContainer(), 0, &range->endContainer(), range->endOffset());
+ offset += nodeStartOffset.offset;
+ }
+
+ return traverseToOffsetInRange(WTFMove(copyRange), offset, isStart ? TraverseOptionDefault : TraverseOptionToNodeEnd, stayWithinRange);
+}
+
+void AXObjectCache::startOrEndTextMarkerDataForRange(TextMarkerData& textMarkerData, RefPtr<Range> range, bool isStart)
+{
+ memset(&textMarkerData, 0, sizeof(TextMarkerData));
+
+ CharacterOffset characterOffset = startOrEndCharacterOffsetForRange(range, isStart);
+ if (characterOffset.isNull())
+ return;
+
+ setTextMarkerDataWithCharacterOffset(textMarkerData, characterOffset);
+}
+
+CharacterOffset AXObjectCache::characterOffsetForNodeAndOffset(Node& node, int offset, TraverseOption option)
+{
+ Node* domNode = &node;
+ if (!domNode)
+ return CharacterOffset();
+
+ bool toNodeEnd = option & TraverseOptionToNodeEnd;
+ bool includeStart = option & TraverseOptionIncludeStart;
+
+ // ignoreStart is used to determine if we should go to previous node or
+ // stay in current node when offset is 0.
+ if (!toNodeEnd && (offset < 0 || (!offset && !includeStart))) {
+ // Set the offset to the amount of characters we need to go backwards.
+ offset = - offset;
+ CharacterOffset charOffset = CharacterOffset();
+ while (offset >= 0 && charOffset.offset <= offset) {
+ offset -= charOffset.offset;
+ domNode = previousNode(domNode);
+ if (domNode) {
+ charOffset = characterOffsetForNodeAndOffset(*domNode, 0, TraverseOptionToNodeEnd);
+ } else
+ return CharacterOffset();
+ if (charOffset.offset == offset)
+ break;
+ }
+ if (offset > 0)
+ charOffset = characterOffsetForNodeAndOffset(*charOffset.node, charOffset.offset - offset, TraverseOptionIncludeStart);
+ return charOffset;
+ }
+
+ RefPtr<Range> range = rangeForNodeContents(domNode);
+
+ // Traverse the offset amount of characters forward and see if there's remaining offsets.
+ // Keep traversing to the next node when there's remaining offsets.
+ CharacterOffset characterOffset = traverseToOffsetInRange(range, offset, option);
+ while (!characterOffset.isNull() && characterOffset.remaining() && !toNodeEnd) {
+ domNode = nextNode(domNode);
+ if (!domNode)
+ return CharacterOffset();
+ range = rangeForNodeContents(domNode);
+ characterOffset = traverseToOffsetInRange(range, characterOffset.remaining(), option);
+ }
+
+ return characterOffset;
+}
+
+void AXObjectCache::textMarkerDataForCharacterOffset(TextMarkerData& textMarkerData, const CharacterOffset& characterOffset)
+{
+ memset(&textMarkerData, 0, sizeof(TextMarkerData));
+ setTextMarkerDataWithCharacterOffset(textMarkerData, characterOffset);
+}
+
+bool AXObjectCache::shouldSkipBoundary(const CharacterOffset& previous, const CharacterOffset& next)
+{
+ // Match the behavior of VisiblePosition, we should skip the node boundary when there's no visual space or new line character.
+ if (previous.isNull() || next.isNull())
+ return false;
+
+ if (previous.node == next.node)
+ return false;
+
+ if (next.startIndex > 0 || next.offset > 0)
+ return false;
+
+ CharacterOffset newLine = startCharacterOffsetOfLine(next);
+ if (next.isEqual(newLine))
+ return false;
+
+ return true;
+}
+
+void AXObjectCache::textMarkerDataForNextCharacterOffset(TextMarkerData& textMarkerData, const CharacterOffset& characterOffset)
+{
+ CharacterOffset next = characterOffset;
+ CharacterOffset previous = characterOffset;
+ bool shouldContinue;
+ do {
+ shouldContinue = false;
+ next = nextCharacterOffset(next, false);
+ if (shouldSkipBoundary(previous, next))
+ next = nextCharacterOffset(next, false);
+ textMarkerDataForCharacterOffset(textMarkerData, next);
+
+ // We should skip next CharactetOffset if it's visually the same.
+ if (!lengthForRange(rangeForUnorderedCharacterOffsets(previous, next).get()))
+ shouldContinue = true;
+ previous = next;
+ } while (textMarkerData.ignored || shouldContinue);
+}
+
+void AXObjectCache::textMarkerDataForPreviousCharacterOffset(TextMarkerData& textMarkerData, const CharacterOffset& characterOffset)
+{
+ CharacterOffset previous = characterOffset;
+ CharacterOffset next = characterOffset;
+ bool shouldContinue;
+ do {
+ shouldContinue = false;
+ previous = previousCharacterOffset(previous, false);
+ textMarkerDataForCharacterOffset(textMarkerData, previous);
+
+ // We should skip previous CharactetOffset if it's visually the same.
+ if (!lengthForRange(rangeForUnorderedCharacterOffsets(previous, next).get()))
+ shouldContinue = true;
+ next = previous;
+ } while (textMarkerData.ignored || shouldContinue);
+}
+
+Node* AXObjectCache::nextNode(Node* node) const
+{
+ if (!node)
+ return nullptr;
+
+ return NodeTraversal::nextSkippingChildren(*node);
+}
+
+Node* AXObjectCache::previousNode(Node* node) const
+{
+ if (!node)
+ return nullptr;
+
+ // First child of body shouldn't have previous node.
+ if (node->parentNode() && node->parentNode()->renderer() && node->parentNode()->renderer()->isBody() && !node->previousSibling())
+ return nullptr;
+
+ return NodeTraversal::previousSkippingChildren(*node);
+}
+
+VisiblePosition AXObjectCache::visiblePositionFromCharacterOffset(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
return VisiblePosition();
- return visiblePos;
+ // Create a collapsed range and use that to form a VisiblePosition, so that the case with
+ // composed characters will be covered.
+ auto range = rangeForUnorderedCharacterOffsets(characterOffset, characterOffset);
+ return range ? VisiblePosition(range->startPosition()) : VisiblePosition();
+}
+
+CharacterOffset AXObjectCache::characterOffsetFromVisiblePosition(const VisiblePosition& visiblePos)
+{
+ if (visiblePos.isNull())
+ return CharacterOffset();
+
+ Position deepPos = visiblePos.deepEquivalent();
+ Node* domNode = deepPos.deprecatedNode();
+ ASSERT(domNode);
+
+ if (domNode->offsetInCharacters())
+ return traverseToOffsetInRange(rangeForNodeContents(domNode), deepPos.deprecatedEditingOffset(), TraverseOptionValidateOffset);
+
+ RefPtr<AccessibilityObject> obj = this->getOrCreate(domNode);
+ if (!obj)
+ return CharacterOffset();
+
+ // Use nextVisiblePosition to calculate how many characters we need to traverse to the current position.
+ VisiblePositionRange visiblePositionRange = obj->visiblePositionRange();
+ VisiblePosition visiblePosition = visiblePositionRange.start;
+ int characterOffset = 0;
+ Position currentPosition = visiblePosition.deepEquivalent();
+
+ VisiblePosition previousVisiblePos;
+ while (!currentPosition.isNull() && !deepPos.equals(currentPosition)) {
+ previousVisiblePos = visiblePosition;
+ visiblePosition = obj->nextVisiblePosition(visiblePosition);
+ currentPosition = visiblePosition.deepEquivalent();
+ Position previousPosition = previousVisiblePos.deepEquivalent();
+ // Sometimes nextVisiblePosition will give the same VisiblePostion,
+ // we break here to avoid infinite loop.
+ if (currentPosition.equals(previousPosition))
+ break;
+ characterOffset++;
+
+ // When VisiblePostion moves to next node, it will count the leading line break as
+ // 1 offset, which we shouldn't include in CharacterOffset.
+ if (currentPosition.deprecatedNode() != previousPosition.deprecatedNode()) {
+ if (visiblePosition.characterBefore() == '\n')
+ characterOffset--;
+ } else {
+ // Sometimes VisiblePosition will move multiple characters, like emoji.
+ if (currentPosition.deprecatedNode()->offsetInCharacters())
+ characterOffset += currentPosition.offsetInContainerNode() - previousPosition.offsetInContainerNode() - 1;
+ }
+ }
+
+ // Sometimes when the node is a replaced node and is ignored in accessibility, we get a wrong CharacterOffset from it.
+ CharacterOffset result = traverseToOffsetInRange(rangeForNodeContents(obj->node()), characterOffset);
+ if (result.remainingOffset > 0 && !result.isNull() && isRendererReplacedElement(result.node->renderer()))
+ result.offset += result.remainingOffset;
+ return result;
+}
+
+AccessibilityObject* AXObjectCache::accessibilityObjectForTextMarkerData(TextMarkerData& textMarkerData)
+{
+ if (!isNodeInUse(textMarkerData.node))
+ return nullptr;
+
+ Node* domNode = textMarkerData.node;
+ return this->getOrCreate(domNode);
}
void AXObjectCache::textMarkerDataForVisiblePosition(TextMarkerData& textMarkerData, const VisiblePosition& visiblePos)
@@ -966,10 +2094,15 @@ void AXObjectCache::textMarkerDataForVisiblePosition(TextMarkerData& textMarkerD
if (!domNode)
return;
- if (domNode->isHTMLElement()) {
- HTMLInputElement* inputElement = domNode->toInputElement();
- if (inputElement && inputElement->isPasswordField())
- return;
+ if (is<HTMLInputElement>(*domNode) && downcast<HTMLInputElement>(*domNode).isPasswordField())
+ return;
+
+ // If the visible position has an anchor type referring to a node other than the anchored node, we should
+ // set the text marker data with CharacterOffset so that the offset will correspond to the node.
+ CharacterOffset characterOffset = characterOffsetFromVisiblePosition(visiblePos);
+ if (deepPos.anchorType() == Position::PositionIsAfterAnchor || deepPos.anchorType() == Position::PositionIsAfterChildren) {
+ textMarkerDataForCharacterOffset(textMarkerData, characterOffset);
+ return;
}
// find or create an accessibility object for this node
@@ -979,15 +2112,535 @@ void AXObjectCache::textMarkerDataForVisiblePosition(TextMarkerData& textMarkerD
textMarkerData.axID = obj.get()->axObjectID();
textMarkerData.node = domNode;
textMarkerData.offset = deepPos.deprecatedEditingOffset();
- textMarkerData.affinity = visiblePos.affinity();
+ textMarkerData.affinity = visiblePos.affinity();
+
+ textMarkerData.characterOffset = characterOffset.offset;
+ textMarkerData.characterStartIndex = characterOffset.startIndex;
cache->setNodeInUse(domNode);
}
+CharacterOffset AXObjectCache::nextCharacterOffset(const CharacterOffset& characterOffset, bool ignoreNextNodeStart)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ // We don't always move one 'character' at a time since there might be composed characters.
+ int nextOffset = Position::uncheckedNextOffset(characterOffset.node, characterOffset.offset);
+ CharacterOffset next = characterOffsetForNodeAndOffset(*characterOffset.node, nextOffset);
+
+ // To be consistent with VisiblePosition, we should consider the case that current node end to next node start counts 1 offset.
+ if (!ignoreNextNodeStart && !next.isNull() && !isReplacedNodeOrBR(next.node) && next.node != characterOffset.node) {
+ int length = TextIterator::rangeLength(rangeForUnorderedCharacterOffsets(characterOffset, next).get());
+ if (length > nextOffset - characterOffset.offset)
+ next = characterOffsetForNodeAndOffset(*next.node, 0, TraverseOptionIncludeStart);
+ }
+
+ return next;
+}
+
+CharacterOffset AXObjectCache::previousCharacterOffset(const CharacterOffset& characterOffset, bool ignorePreviousNodeEnd)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ // To be consistent with VisiblePosition, we should consider the case that current node start to previous node end counts 1 offset.
+ if (!ignorePreviousNodeEnd && !characterOffset.offset)
+ return characterOffsetForNodeAndOffset(*characterOffset.node, 0);
+
+ // We don't always move one 'character' a time since there might be composed characters.
+ int previousOffset = Position::uncheckedPreviousOffset(characterOffset.node, characterOffset.offset);
+ return characterOffsetForNodeAndOffset(*characterOffset.node, previousOffset, TraverseOptionIncludeStart);
+}
+
+CharacterOffset AXObjectCache::startCharacterOffsetOfWord(const CharacterOffset& characterOffset, EWordSide side)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ CharacterOffset c = characterOffset;
+ if (side == RightWordIfOnBoundary) {
+ CharacterOffset endOfParagraph = endCharacterOffsetOfParagraph(c);
+ if (c.isEqual(endOfParagraph))
+ return c;
+
+ // We should consider the node boundary that splits words. Otherwise VoiceOver won't see it as space.
+ c = nextCharacterOffset(characterOffset, false);
+ if (shouldSkipBoundary(characterOffset, c))
+ c = nextCharacterOffset(c, false);
+ if (c.isNull())
+ return characterOffset;
+ }
+
+ return previousBoundary(c, startWordBoundary);
+}
+
+CharacterOffset AXObjectCache::endCharacterOffsetOfWord(const CharacterOffset& characterOffset, EWordSide side)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ CharacterOffset c = characterOffset;
+ if (side == LeftWordIfOnBoundary) {
+ CharacterOffset startOfParagraph = startCharacterOffsetOfParagraph(c);
+ if (c.isEqual(startOfParagraph))
+ return c;
+
+ c = previousCharacterOffset(characterOffset);
+ if (c.isNull())
+ return characterOffset;
+ } else {
+ CharacterOffset endOfParagraph = endCharacterOffsetOfParagraph(characterOffset);
+ if (characterOffset.isEqual(endOfParagraph))
+ return characterOffset;
+ }
+
+ return nextBoundary(c, endWordBoundary);
+}
+
+CharacterOffset AXObjectCache::previousWordStartCharacterOffset(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ CharacterOffset previousOffset = previousCharacterOffset(characterOffset);
+ if (previousOffset.isNull())
+ return CharacterOffset();
+
+ return startCharacterOffsetOfWord(previousOffset, RightWordIfOnBoundary);
+}
+
+CharacterOffset AXObjectCache::nextWordEndCharacterOffset(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ CharacterOffset nextOffset = nextCharacterOffset(characterOffset);
+ if (nextOffset.isNull())
+ return CharacterOffset();
+
+ return endCharacterOffsetOfWord(nextOffset, LeftWordIfOnBoundary);
+}
+
+RefPtr<Range> AXObjectCache::leftWordRange(const CharacterOffset& characterOffset)
+{
+ CharacterOffset start = startCharacterOffsetOfWord(characterOffset, LeftWordIfOnBoundary);
+ CharacterOffset end = endCharacterOffsetOfWord(start);
+ return rangeForUnorderedCharacterOffsets(start, end);
+}
+
+RefPtr<Range> AXObjectCache::rightWordRange(const CharacterOffset& characterOffset)
+{
+ CharacterOffset start = startCharacterOffsetOfWord(characterOffset, RightWordIfOnBoundary);
+ CharacterOffset end = endCharacterOffsetOfWord(start);
+ return rangeForUnorderedCharacterOffsets(start, end);
+}
+
+static UChar32 characterForCharacterOffset(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull() || !characterOffset.node->isTextNode())
+ return 0;
+
+ UChar32 ch = 0;
+ unsigned offset = characterOffset.startIndex + characterOffset.offset;
+ if (offset < characterOffset.node->textContent().length())
+ U16_NEXT(characterOffset.node->textContent(), offset, characterOffset.node->textContent().length(), ch);
+ return ch;
+}
+
+UChar32 AXObjectCache::characterAfter(const CharacterOffset& characterOffset)
+{
+ return characterForCharacterOffset(nextCharacterOffset(characterOffset));
+}
+
+UChar32 AXObjectCache::characterBefore(const CharacterOffset& characterOffset)
+{
+ return characterForCharacterOffset(characterOffset);
+}
+
+static bool characterOffsetNodeIsBR(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return false;
+
+ return characterOffset.node->hasTagName(brTag);
+}
+
+static Node* parentEditingBoundary(Node* node)
+{
+ if (!node)
+ return nullptr;
+
+ Node* documentElement = node->document().documentElement();
+ if (!documentElement)
+ return nullptr;
+
+ Node* boundary = node;
+ while (boundary != documentElement && boundary->nonShadowBoundaryParentNode() && node->hasEditableStyle() == boundary->parentNode()->hasEditableStyle())
+ boundary = boundary->nonShadowBoundaryParentNode();
+
+ return boundary;
+}
+
+CharacterOffset AXObjectCache::nextBoundary(const CharacterOffset& characterOffset, BoundarySearchFunction searchFunction)
+{
+ if (characterOffset.isNull())
+ return { };
+
+ Node* boundary = parentEditingBoundary(characterOffset.node);
+ if (!boundary)
+ return { };
+
+ RefPtr<Range> searchRange = rangeForNodeContents(boundary);
+ if (!searchRange)
+ return { };
+
+ Vector<UChar, 1024> string;
+ unsigned prefixLength = 0;
+
+ if (requiresContextForWordBoundary(characterAfter(characterOffset))) {
+ auto backwardsScanRange = boundary->document().createRange();
+ if (!setRangeStartOrEndWithCharacterOffset(backwardsScanRange, characterOffset, false))
+ return { };
+ prefixLength = prefixLengthForRange(backwardsScanRange, string);
+ }
+
+ if (!setRangeStartOrEndWithCharacterOffset(*searchRange, characterOffset, true))
+ return { };
+ CharacterOffset end = startOrEndCharacterOffsetForRange(searchRange, false);
+
+ TextIterator it(searchRange.get(), TextIteratorEmitsObjectReplacementCharacters);
+ unsigned next = forwardSearchForBoundaryWithTextIterator(it, string, prefixLength, searchFunction);
+
+ if (it.atEnd() && next == string.size())
+ return end;
+
+ // We should consider the node boundary that splits words.
+ if (searchFunction == endWordBoundary && next - prefixLength == 1)
+ return nextCharacterOffset(characterOffset, false);
+
+ // The endSentenceBoundary function will include a line break at the end of the sentence.
+ if (searchFunction == endSentenceBoundary && string[next - 1] == '\n')
+ next--;
+
+ if (next > prefixLength)
+ return characterOffsetForNodeAndOffset(*characterOffset.node, characterOffset.offset + next - prefixLength);
+
+ return characterOffset;
+}
+
+CharacterOffset AXObjectCache::previousBoundary(const CharacterOffset& characterOffset, BoundarySearchFunction searchFunction)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ Node* boundary = parentEditingBoundary(characterOffset.node);
+ if (!boundary)
+ return CharacterOffset();
+
+ RefPtr<Range> searchRange = rangeForNodeContents(boundary);
+ Vector<UChar, 1024> string;
+ unsigned suffixLength = 0;
+
+ if (requiresContextForWordBoundary(characterBefore(characterOffset))) {
+ auto forwardsScanRange = boundary->document().createRange();
+ if (forwardsScanRange->setEndAfter(*boundary).hasException())
+ return { };
+ if (!setRangeStartOrEndWithCharacterOffset(forwardsScanRange, characterOffset, true))
+ return { };
+ suffixLength = suffixLengthForRange(forwardsScanRange, string);
+ }
+
+ if (!setRangeStartOrEndWithCharacterOffset(*searchRange, characterOffset, false))
+ return { };
+ CharacterOffset start = startOrEndCharacterOffsetForRange(searchRange, true);
+
+ SimplifiedBackwardsTextIterator it(*searchRange);
+ unsigned next = backwardSearchForBoundaryWithTextIterator(it, string, suffixLength, searchFunction);
+
+ if (!next)
+ return it.atEnd() ? start : characterOffset;
+
+ Node& node = it.atEnd() ? searchRange->startContainer() : it.range()->startContainer();
+
+ // SimplifiedBackwardsTextIterator ignores replaced elements.
+ if (AccessibilityObject::replacedNodeNeedsCharacter(characterOffset.node))
+ return characterOffsetForNodeAndOffset(*characterOffset.node, 0);
+ Node* nextSibling = node.nextSibling();
+ if (&node != characterOffset.node && AccessibilityObject::replacedNodeNeedsCharacter(nextSibling))
+ return startOrEndCharacterOffsetForRange(rangeForNodeContents(nextSibling), false);
+
+ if ((node.isTextNode() && static_cast<int>(next) <= node.maxCharacterOffset()) || (node.renderer() && node.renderer()->isBR() && !next)) {
+ // The next variable contains a usable index into a text node
+ if (node.isTextNode())
+ return traverseToOffsetInRange(rangeForNodeContents(&node), next, TraverseOptionValidateOffset);
+ return characterOffsetForNodeAndOffset(node, next, TraverseOptionIncludeStart);
+ }
+
+ int characterCount = characterOffset.offset - (string.size() - suffixLength - next);
+ // We don't want to go to the previous node if the node is at the start of a new line.
+ if (characterCount < 0 && (characterOffsetNodeIsBR(characterOffset) || string[string.size() - suffixLength - 1] == '\n'))
+ characterCount = 0;
+ return characterOffsetForNodeAndOffset(*characterOffset.node, characterCount, TraverseOptionIncludeStart);
+}
+
+CharacterOffset AXObjectCache::startCharacterOffsetOfParagraph(const CharacterOffset& characterOffset, EditingBoundaryCrossingRule boundaryCrossingRule)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ auto* startNode = characterOffset.node;
+
+ if (isRenderedAsNonInlineTableImageOrHR(startNode))
+ return startOrEndCharacterOffsetForRange(rangeForNodeContents(startNode), true);
+
+ auto* startBlock = enclosingBlock(startNode);
+ int offset = characterOffset.startIndex + characterOffset.offset;
+ auto* highestRoot = highestEditableRoot(firstPositionInOrBeforeNode(startNode));
+ Position::AnchorType type = Position::PositionIsOffsetInAnchor;
+
+ auto* node = findStartOfParagraph(startNode, highestRoot, startBlock, offset, type, boundaryCrossingRule);
+
+ if (type == Position::PositionIsOffsetInAnchor)
+ return characterOffsetForNodeAndOffset(*node, offset, TraverseOptionIncludeStart);
+
+ return startOrEndCharacterOffsetForRange(rangeForNodeContents(node), true);
+}
+
+CharacterOffset AXObjectCache::endCharacterOffsetOfParagraph(const CharacterOffset& characterOffset, EditingBoundaryCrossingRule boundaryCrossingRule)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ Node* startNode = characterOffset.node;
+ if (isRenderedAsNonInlineTableImageOrHR(startNode))
+ return startOrEndCharacterOffsetForRange(rangeForNodeContents(startNode), false);
+
+ Node* stayInsideBlock = enclosingBlock(startNode);
+ int offset = characterOffset.startIndex + characterOffset.offset;
+ Node* highestRoot = highestEditableRoot(firstPositionInOrBeforeNode(startNode));
+ Position::AnchorType type = Position::PositionIsOffsetInAnchor;
+
+ Node* node = findEndOfParagraph(startNode, highestRoot, stayInsideBlock, offset, type, boundaryCrossingRule);
+ if (type == Position::PositionIsOffsetInAnchor) {
+ if (node->isTextNode()) {
+ CharacterOffset startOffset = startOrEndCharacterOffsetForRange(rangeForNodeContents(node), true);
+ offset -= startOffset.startIndex;
+ }
+ return characterOffsetForNodeAndOffset(*node, offset, TraverseOptionIncludeStart);
+ }
+
+ return startOrEndCharacterOffsetForRange(rangeForNodeContents(node), false);
+}
+
+RefPtr<Range> AXObjectCache::paragraphForCharacterOffset(const CharacterOffset& characterOffset)
+{
+ CharacterOffset start = startCharacterOffsetOfParagraph(characterOffset);
+ CharacterOffset end = endCharacterOffsetOfParagraph(start);
+
+ return rangeForUnorderedCharacterOffsets(start, end);
+}
+
+CharacterOffset AXObjectCache::nextParagraphEndCharacterOffset(const CharacterOffset& characterOffset)
+{
+ // make sure we move off of a paragraph end
+ CharacterOffset next = nextCharacterOffset(characterOffset);
+
+ // We should skip the following BR node.
+ if (characterOffsetNodeIsBR(next) && !characterOffsetNodeIsBR(characterOffset))
+ next = nextCharacterOffset(next);
+
+ return endCharacterOffsetOfParagraph(next);
+}
+
+CharacterOffset AXObjectCache::previousParagraphStartCharacterOffset(const CharacterOffset& characterOffset)
+{
+ // make sure we move off of a paragraph start
+ CharacterOffset previous = previousCharacterOffset(characterOffset);
+
+ // We should skip the preceding BR node.
+ if (characterOffsetNodeIsBR(previous) && !characterOffsetNodeIsBR(characterOffset))
+ previous = previousCharacterOffset(previous);
+
+ return startCharacterOffsetOfParagraph(previous);
+}
+
+CharacterOffset AXObjectCache::startCharacterOffsetOfSentence(const CharacterOffset& characterOffset)
+{
+ return previousBoundary(characterOffset, startSentenceBoundary);
+}
+
+CharacterOffset AXObjectCache::endCharacterOffsetOfSentence(const CharacterOffset& characterOffset)
+{
+ return nextBoundary(characterOffset, endSentenceBoundary);
+}
+
+RefPtr<Range> AXObjectCache::sentenceForCharacterOffset(const CharacterOffset& characterOffset)
+{
+ CharacterOffset start = startCharacterOffsetOfSentence(characterOffset);
+ CharacterOffset end = endCharacterOffsetOfSentence(start);
+ return rangeForUnorderedCharacterOffsets(start, end);
+}
+
+CharacterOffset AXObjectCache::nextSentenceEndCharacterOffset(const CharacterOffset& characterOffset)
+{
+ // Make sure we move off of a sentence end.
+ return endCharacterOffsetOfSentence(nextCharacterOffset(characterOffset));
+}
+
+CharacterOffset AXObjectCache::previousSentenceStartCharacterOffset(const CharacterOffset& characterOffset)
+{
+ // Make sure we move off of a sentence start.
+ CharacterOffset previous = previousCharacterOffset(characterOffset);
+
+ // We should skip the preceding BR node.
+ if (characterOffsetNodeIsBR(previous) && !characterOffsetNodeIsBR(characterOffset))
+ previous = previousCharacterOffset(previous);
+
+ return startCharacterOffsetOfSentence(previous);
+}
+
+LayoutRect AXObjectCache::localCaretRectForCharacterOffset(RenderObject*& renderer, const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull()) {
+ renderer = nullptr;
+ return IntRect();
+ }
+
+ Node* node = characterOffset.node;
+
+ renderer = node->renderer();
+ if (!renderer)
+ return LayoutRect();
+
+ InlineBox* inlineBox = nullptr;
+ int caretOffset;
+ // Use a collapsed range to get the position.
+ RefPtr<Range> range = rangeForUnorderedCharacterOffsets(characterOffset, characterOffset);
+ if (!range)
+ return IntRect();
+
+ Position startPosition = range->startPosition();
+ startPosition.getInlineBoxAndOffset(DOWNSTREAM, inlineBox, caretOffset);
+
+ if (inlineBox)
+ renderer = &inlineBox->renderer();
+
+ if (is<RenderLineBreak>(renderer) && downcast<RenderLineBreak>(renderer)->inlineBoxWrapper() != inlineBox)
+ return IntRect();
+
+ return renderer->localCaretRect(inlineBox, caretOffset);
+}
+
+IntRect AXObjectCache::absoluteCaretBoundsForCharacterOffset(const CharacterOffset& characterOffset)
+{
+ RenderBlock* caretPainter = nullptr;
+
+ // First compute a rect local to the renderer at the selection start.
+ RenderObject* renderer = nullptr;
+ LayoutRect localRect = localCaretRectForCharacterOffset(renderer, characterOffset);
+
+ localRect = localCaretRectInRendererForRect(localRect, characterOffset.node, renderer, caretPainter);
+ return absoluteBoundsForLocalCaretRect(caretPainter, localRect);
+}
+
+CharacterOffset AXObjectCache::characterOffsetForPoint(const IntPoint &point, AccessibilityObject* obj)
+{
+ if (!obj)
+ return CharacterOffset();
+
+ VisiblePosition vp = obj->visiblePositionForPoint(point);
+ RefPtr<Range> range = makeRange(vp, vp);
+ return startOrEndCharacterOffsetForRange(range, true);
+}
+
+CharacterOffset AXObjectCache::characterOffsetForPoint(const IntPoint &point)
+{
+ RefPtr<Range> caretRange = m_document.caretRangeFromPoint(LayoutPoint(point));
+ return startOrEndCharacterOffsetForRange(caretRange, true);
+}
+
+CharacterOffset AXObjectCache::characterOffsetForBounds(const IntRect& rect, bool first)
+{
+ if (rect.isEmpty())
+ return CharacterOffset();
+
+ IntPoint corner = first ? rect.minXMinYCorner() : rect.maxXMaxYCorner();
+ CharacterOffset characterOffset = characterOffsetForPoint(corner);
+
+ if (rect.contains(absoluteCaretBoundsForCharacterOffset(characterOffset).center()))
+ return characterOffset;
+
+ // If the initial position is located outside the bounds adjust it incrementally as needed.
+ CharacterOffset nextCharOffset = nextCharacterOffset(characterOffset, false);
+ CharacterOffset previousCharOffset = previousCharacterOffset(characterOffset, false);
+ while (!nextCharOffset.isNull() || !previousCharOffset.isNull()) {
+ if (rect.contains(absoluteCaretBoundsForCharacterOffset(nextCharOffset).center()))
+ return nextCharOffset;
+ if (rect.contains(absoluteCaretBoundsForCharacterOffset(previousCharOffset).center()))
+ return previousCharOffset;
+
+ nextCharOffset = nextCharacterOffset(nextCharOffset, false);
+ previousCharOffset = previousCharacterOffset(previousCharOffset, false);
+ }
+
+ return CharacterOffset();
+}
+
+// FIXME: Remove VisiblePosition code after implementing this using CharacterOffset.
+CharacterOffset AXObjectCache::endCharacterOffsetOfLine(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ VisiblePosition vp = visiblePositionFromCharacterOffset(characterOffset);
+ VisiblePosition endLine = endOfLine(vp);
+
+ return characterOffsetFromVisiblePosition(endLine);
+}
+
+CharacterOffset AXObjectCache::startCharacterOffsetOfLine(const CharacterOffset& characterOffset)
+{
+ if (characterOffset.isNull())
+ return CharacterOffset();
+
+ VisiblePosition vp = visiblePositionFromCharacterOffset(characterOffset);
+ VisiblePosition startLine = startOfLine(vp);
+
+ return characterOffsetFromVisiblePosition(startLine);
+}
+
+CharacterOffset AXObjectCache::characterOffsetForIndex(int index, const AccessibilityObject* obj)
+{
+ if (!obj)
+ return CharacterOffset();
+
+ RefPtr<Range> range = obj->elementRange();
+ CharacterOffset start = startOrEndCharacterOffsetForRange(range, true);
+ CharacterOffset end = startOrEndCharacterOffsetForRange(range, false);
+ CharacterOffset result = start;
+ for (int i = 0; i < index; i++) {
+ result = nextCharacterOffset(result, false);
+ if (result.isEqual(end))
+ break;
+ }
+ return result;
+}
+
+int AXObjectCache::indexForCharacterOffset(const CharacterOffset& characterOffset, AccessibilityObject* obj)
+{
+ // Create a collapsed range so that we can get the VisiblePosition from it.
+ RefPtr<Range> range = rangeForUnorderedCharacterOffsets(characterOffset, characterOffset);
+ if (!range)
+ return 0;
+ VisiblePosition vp = range->startPosition();
+ return obj->indexForVisiblePosition(vp);
+}
+
const Element* AXObjectCache::rootAXEditableElement(const Node* node)
{
const Element* result = node->rootEditableElement();
- const Element* element = node->isElementNode() ? toElement(node) : node->parentElement();
+ const Element* element = is<Element>(*node) ? downcast<Element>(node) : node->parentElement();
for (; element; element = element->parentElement()) {
if (nodeIsTextControl(element))
@@ -999,20 +2652,18 @@ const Element* AXObjectCache::rootAXEditableElement(const Node* node)
void AXObjectCache::clearTextMarkerNodesInUse(Document* document)
{
- HashSet<Node*>::iterator it = m_textMarkerNodes.begin();
- HashSet<Node*>::iterator end = m_textMarkerNodes.end();
-
- // Check each node to see if it's inside the document being deleted.
+ if (!document)
+ return;
+
+ // Check each node to see if it's inside the document being deleted, of if it no longer belongs to a document.
HashSet<Node*> nodesToDelete;
- for (; it != end; ++it) {
- if (&(*it)->document() == document)
- nodesToDelete.add(*it);
+ for (const auto& node : m_textMarkerNodes) {
+ if (!node->isConnected() || &(node)->document() == document)
+ nodesToDelete.add(node);
}
- it = nodesToDelete.begin();
- end = nodesToDelete.end();
- for (; it != end; ++it)
- m_textMarkerNodes.remove(*it);
+ for (const auto& node : nodesToDelete)
+ m_textMarkerNodes.remove(node);
}
bool AXObjectCache::nodeIsTextControl(const Node* node)
@@ -1024,24 +2675,59 @@ bool AXObjectCache::nodeIsTextControl(const Node* node)
return axObject && axObject->isTextControl();
}
+void AXObjectCache::performDeferredIsIgnoredChange()
+{
+ for (auto* renderer : m_deferredIsIgnoredChangeList)
+ recomputeIsIgnored(renderer);
+ m_deferredIsIgnoredChangeList.clear();
+}
+
+void AXObjectCache::recomputeDeferredIsIgnored(RenderBlock& renderer)
+{
+ if (renderer.beingDestroyed())
+ return;
+ m_deferredIsIgnoredChangeList.add(&renderer);
+}
+
bool isNodeAriaVisible(Node* node)
{
if (!node)
return false;
-
- // To determine if a node is ARIA visible, we need to check the parent hierarchy to see if anyone specifies
- // aria-hidden explicitly.
+
+ // ARIA Node visibility is controlled by aria-hidden
+ // 1) if aria-hidden=true, the whole subtree is hidden
+ // 2) if aria-hidden=false, and the object is rendered, there's no effect
+ // 3) if aria-hidden=false, and the object is NOT rendered, then it must have
+ // aria-hidden=false on each parent until it gets to a rendered object
+ // 3b) a text node inherits a parents aria-hidden value
+ bool requiresAriaHiddenFalse = !node->renderer();
+ bool ariaHiddenFalsePresent = false;
for (Node* testNode = node; testNode; testNode = testNode->parentNode()) {
- if (testNode->isElementNode()) {
- const AtomicString& ariaHiddenValue = toElement(testNode)->fastGetAttribute(aria_hiddenAttr);
- if (equalIgnoringCase(ariaHiddenValue, "false"))
- return true;
- if (equalIgnoringCase(ariaHiddenValue, "true"))
+ if (is<Element>(*testNode)) {
+ const AtomicString& ariaHiddenValue = downcast<Element>(*testNode).attributeWithoutSynchronization(aria_hiddenAttr);
+ if (equalLettersIgnoringASCIICase(ariaHiddenValue, "true"))
+ return false;
+
+ bool ariaHiddenFalse = equalLettersIgnoringASCIICase(ariaHiddenValue, "false");
+ if (!testNode->renderer() && !ariaHiddenFalse)
return false;
+ if (!ariaHiddenFalsePresent && ariaHiddenFalse)
+ ariaHiddenFalsePresent = true;
+ // We should break early when it gets to a rendered object.
+ if (testNode->renderer())
+ break;
}
}
- return false;
+ return !requiresAriaHiddenFalse || ariaHiddenFalsePresent;
+}
+
+AccessibilityObject* AXObjectCache::rootWebArea()
+{
+ AccessibilityObject* rootObject = this->rootObject();
+ if (!rootObject || !rootObject->isAccessibilityScrollView())
+ return nullptr;
+ return downcast<AccessibilityScrollView>(*rootObject).webAreaObject();
}
AXAttributeCacheEnabler::AXAttributeCacheEnabler(AXObjectCache* cache)
@@ -1056,6 +2742,28 @@ AXAttributeCacheEnabler::~AXAttributeCacheEnabler()
if (m_cache)
m_cache->stopCachingComputedObjectAttributes();
}
+
+#if !PLATFORM(COCOA)
+AXTextChange AXObjectCache::textChangeForEditType(AXTextEditType type)
+{
+ switch (type) {
+ case AXTextEditTypeCut:
+ case AXTextEditTypeDelete:
+ return AXTextDeleted;
+ case AXTextEditTypeInsert:
+ case AXTextEditTypeDictation:
+ case AXTextEditTypeTyping:
+ case AXTextEditTypePaste:
+ return AXTextInserted;
+ case AXTextEditTypeAttributesChange:
+ return AXTextAttributesChanged;
+ case AXTextEditTypeUnknown:
+ break;
+ }
+ ASSERT_NOT_REACHED();
+ return AXTextInserted;
+}
+#endif
} // namespace WebCore