AX: [Mac] Implement next/previous text marker functions using TextIterator
authorn_wang@apple.com <n_wang@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Jan 2016 00:56:13 +0000 (00:56 +0000)
committern_wang@apple.com <n_wang@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 19 Jan 2016 00:56:13 +0000 (00:56 +0000)
https://bugs.webkit.org/show_bug.cgi?id=152728

Reviewed by Chris Fleizach.

Source/WebCore:

The existing AXTextMarker based calls are implemented using visible position, and that introduced
some bugs which make VoiceOver working incorrectly on Mac sometimes. Since TextIterator uses rendering
position, we tried to use it to refactor those AXTextMarker based calls.
In this patch, I implemented functions to navigate to previous/next text marker using Range and TextIterator.
Also added a conversion between visible position and character offset to make sure unconverted text marker
related functions are still working correctly.

Tests: accessibility/mac/previous-next-text-marker.html
       accessibility/mac/text-marker-with-user-select-none.html

* accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::visiblePositionForTextMarkerData):
(WebCore::AXObjectCache::traverseToOffsetInRange):
(WebCore::AXObjectCache::lengthForRange):
(WebCore::AXObjectCache::rangeForNodeContents):
(WebCore::characterOffsetsInOrder):
(WebCore::AXObjectCache::rangeForUnorderedCharacterOffsets):
(WebCore::AXObjectCache::setTextMarkerDataWithCharacterOffset):
(WebCore::AXObjectCache::startOrEndTextMarkerDataForRange):
(WebCore::AXObjectCache::textMarkerDataForCharacterOffset):
(WebCore::AXObjectCache::nextNode):
(WebCore::AXObjectCache::previousNode):
(WebCore::AXObjectCache::visiblePositionFromCharacterOffset):
(WebCore::AXObjectCache::characterOffsetFromVisiblePosition):
(WebCore::AXObjectCache::accessibilityObjectForTextMarkerData):
(WebCore::AXObjectCache::textMarkerDataForVisiblePosition):
* accessibility/AXObjectCache.h:
(WebCore::CharacterOffset::CharacterOffset):
(WebCore::CharacterOffset::remaining):
(WebCore::CharacterOffset::isNull):
(WebCore::AXObjectCache::setNodeInUse):
(WebCore::AXObjectCache::removeNodeForUse):
(WebCore::AXObjectCache::isNodeInUse):
* accessibility/AccessibilityObject.cpp:
(WebCore::AccessibilityObject::selectionRange):
(WebCore::AccessibilityObject::elementRange):
(WebCore::AccessibilityObject::selectText):
(WebCore::AccessibilityObject::lineRangeForPosition):
(WebCore::AccessibilityObject::replacedNodeNeedsCharacter):
(WebCore::renderListItemContainerForNode):
(WebCore::listMarkerTextForNode):
(WebCore::AccessibilityObject::listMarkerTextForNodeAndPosition):
(WebCore::AccessibilityObject::stringForRange):
(WebCore::AccessibilityObject::stringForVisiblePositionRange):
(WebCore::replacedNodeNeedsCharacter): Deleted.
* accessibility/AccessibilityObject.h:
(WebCore::AccessibilityObject::visiblePositionRange):
(WebCore::AccessibilityObject::visiblePositionRangeForLine):
(WebCore::AccessibilityObject::boundsForVisiblePositionRange):
(WebCore::AccessibilityObject::setSelectedVisiblePositionRange):
* accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
(isTextMarkerIgnored):
(-[WebAccessibilityObjectWrapper accessibilityObjectForTextMarker:]):
(accessibilityObjectForTextMarker):
(-[WebAccessibilityObjectWrapper textMarkerRangeFromRange:]):
(textMarkerRangeFromRange):
(-[WebAccessibilityObjectWrapper startOrEndTextMarkerForRange:isStart:]):
(startOrEndTextmarkerForRange):
(-[WebAccessibilityObjectWrapper nextTextMarkerForNode:offset:]):
(-[WebAccessibilityObjectWrapper previousTextMarkerForNode:offset:]):
(-[WebAccessibilityObjectWrapper textMarkerForNode:offset:]):
(textMarkerForCharacterOffset):
(-[WebAccessibilityObjectWrapper rangeForTextMarkerRange:]):
(-[WebAccessibilityObjectWrapper characterOffsetForTextMarker:]):
(textMarkerForVisiblePosition):
(-[WebAccessibilityObjectWrapper accessibilityAttributeValue:forParameter:]):

Tools:

* WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm:
(WTR::AccessibilityUIElement::accessibilityElementForTextMarker):

LayoutTests:

* accessibility/mac/previous-next-text-marker-expected.txt: Added.
* accessibility/mac/previous-next-text-marker.html: Added.
* accessibility/mac/text-marker-with-user-select-none-expected.txt: Added.
* accessibility/mac/text-marker-with-user-select-none.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@195240 268f45cc-cd09-0410-ab3c-d52691b4dbfc

13 files changed:
LayoutTests/ChangeLog
LayoutTests/accessibility/mac/previous-next-text-marker-expected.txt [new file with mode: 0644]
LayoutTests/accessibility/mac/previous-next-text-marker.html [new file with mode: 0644]
LayoutTests/accessibility/mac/text-marker-with-user-select-none-expected.txt [new file with mode: 0644]
LayoutTests/accessibility/mac/text-marker-with-user-select-none.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/accessibility/AXObjectCache.cpp
Source/WebCore/accessibility/AXObjectCache.h
Source/WebCore/accessibility/AccessibilityObject.cpp
Source/WebCore/accessibility/AccessibilityObject.h
Source/WebCore/accessibility/mac/WebAccessibilityObjectWrapperMac.mm
Tools/ChangeLog
Tools/WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm

index 63529cf..5dacf29 100644 (file)
@@ -1,3 +1,15 @@
+2016-01-18  Nan Wang  <n_wang@apple.com>
+
+        AX: [Mac] Implement next/previous text marker functions using TextIterator
+        https://bugs.webkit.org/show_bug.cgi?id=152728
+
+        Reviewed by Chris Fleizach.
+
+        * accessibility/mac/previous-next-text-marker-expected.txt: Added.
+        * accessibility/mac/previous-next-text-marker.html: Added.
+        * accessibility/mac/text-marker-with-user-select-none-expected.txt: Added.
+        * accessibility/mac/text-marker-with-user-select-none.html: Added.
+
 2016-01-17  Simon Fraser  <simon.fraser@apple.com>
 
         More displaylist tests, and minor cleanup
diff --git a/LayoutTests/accessibility/mac/previous-next-text-marker-expected.txt b/LayoutTests/accessibility/mac/previous-next-text-marker-expected.txt
new file mode 100644 (file)
index 0000000..681dbee
--- /dev/null
@@ -0,0 +1,37 @@
+text
+
+text1
+c  d
+
+can't select
+This tests the next/previous text marker functions are implemented correctly.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS text.textMarkerRangeLength(textMarkerRange) is 4
+PASS text.accessibilityElementForTextMarker(startMarker).isEqual(text) is true
+PASS text.accessibilityElementForTextMarker(endMarker).isEqual(text) is true
+PASS element.stringValue is 'AXValue: '
+PASS element.stringValue is 'AXValue: text1'
+PASS element.stringValue is 'AXValue: '
+PASS element.stringValue is 'AXValue: text'
+PASS text2.textMarkerRangeLength(textMarkerRange2) is 5
+Object string for range: c [ATTACHMENT] d
+AXValue: c 
+AXValue: c 
+AXValue: 
+AXValue:  d
+AXValue: 
+AXValue: c 
+AXValue: c 
+AXValue: text1
+PASS text3.stringForTextMarkerRange(markerRange) is 'ect'
+PASS text3.stringForTextMarkerRange(markerRange) is 'sel'
+PASS !psw.accessibilityElementForTextMarker(start) is true
+PASS text2.accessibilityElementForTextMarker(currentMarker).isEqual(text3) is true
+PASS text2.accessibilityElementForTextMarker(currentMarker).isEqual(text2.childAtIndex(2)) is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/accessibility/mac/previous-next-text-marker.html b/LayoutTests/accessibility/mac/previous-next-text-marker.html
new file mode 100644 (file)
index 0000000..bd09019
--- /dev/null
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<style>
+.userselect { user-select: none; -webkit-user-select: none; }
+</style>
+<body id="body">
+
+<div id="text" tabindex="0">text</div>
+<br>
+text1
+
+<div id="text2">
+c <img src="#" aria-label="blah" style="background-color: #aaaaaa; width: 100px; height: 100px;"> d
+</div>
+
+<input type="password" id="psw">
+
+<div class="userselect" id="text3">can't select</div>
+
+<p id="description"></p>
+<div id="console"></div>
+
+<script>
+
+    description("This tests the next/previous text marker functions are implemented correctly.");
+    
+    if (window.accessibilityController) {
+        
+        var text = accessibilityController.accessibleElementById("text");
+        // Get the actual text node.
+        text = text.childAtIndex(0);
+        
+        // Check that we can get the start/end marker for this range.
+        var textMarkerRange = text.textMarkerRangeForElement(text);
+        shouldBe("text.textMarkerRangeLength(textMarkerRange)", "4");
+        
+        var startMarker = text.startTextMarkerForTextMarkerRange(textMarkerRange);
+        var endMarker = text.endTextMarkerForTextMarkerRange(textMarkerRange);
+        shouldBeTrue("text.accessibilityElementForTextMarker(startMarker).isEqual(text)");
+        shouldBeTrue("text.accessibilityElementForTextMarker(endMarker).isEqual(text)");
+        
+        // Check next text marker. (Advance 5 characters, it will land at <br>.)
+        var currentMarker = startMarker;
+        for (var i = 0; i < 5; i++) {
+            currentMarker = text.nextTextMarker(currentMarker);
+        }
+        var element = text.accessibilityElementForTextMarker(currentMarker);
+        shouldBe("element.stringValue", "'AXValue: '");
+        
+        // Advance one more character, it will lande at "t" in "text1".
+        currentMarker = text.nextTextMarker(currentMarker);
+        element = text.accessibilityElementForTextMarker(currentMarker);
+        shouldBe("element.stringValue", "'AXValue: text1'");
+        
+        // Check previous text marker. (Traverse backwards one character, it will land at <br>.)
+        currentMarker = text.previousTextMarker(currentMarker);
+        element = text.accessibilityElementForTextMarker(currentMarker);
+        shouldBe("element.stringValue", "'AXValue: '");
+        
+        // Traverse backwards one more character, it will land at the last character of "text".
+        currentMarker = text.previousTextMarker(currentMarker);
+        element = text.accessibilityElementForTextMarker(currentMarker);
+        shouldBe("element.stringValue", "'AXValue: text'");
+        
+        // Check the case with replaced node
+        var text2 = accessibilityController.accessibleElementById("text2");
+        var textMarkerRange2 = text2.textMarkerRangeForElement(text2);
+        shouldBe("text2.textMarkerRangeLength(textMarkerRange2)", "5");
+        var str = text2.stringForTextMarkerRange(textMarkerRange2).replace(String.fromCharCode(65532), "[ATTACHMENT]");
+        debug("Object string for range: " + str);
+        
+        currentMarker = text2.startTextMarkerForTextMarkerRange(textMarkerRange2);
+        // Advance 4 characters, it will land at first character of " d".
+        for (var i = 0; i < 4; i++) {
+            currentMarker = text2.nextTextMarker(currentMarker);
+            element = text2.accessibilityElementForTextMarker(currentMarker);
+            debug(element.stringValue);
+        }
+        
+        // Traverse backwards 4 characters, it will land at the last character of "text1".
+        for (var i = 0; i < 4; i++) {
+            currentMarker = text2.previousTextMarker(currentMarker);
+            element = text2.accessibilityElementForTextMarker(currentMarker);
+            debug(element.stringValue);
+        }
+        
+        // Check the case with user-select:none, nextTextMarker/previousTextMarker should still work.
+        var text3 = accessibilityController.accessibleElementById("text3");
+        text3 = text3.childAtIndex(0);
+        // Advance to land at user-select:none node.
+        var marker1, marker2;
+        for (var i = 0; i < 17; i++) {
+            currentMarker = text3.nextTextMarker(currentMarker);
+            // i == 13, it should land on "e", and i == 16, it should land on "t"
+            if (i == 13) {
+                marker1 = currentMarker;
+            }
+        }
+        marker2 = currentMarker;
+        var markerRange = text3.textMarkerRangeForMarkers(marker1, marker2);
+        shouldBe("text3.stringForTextMarkerRange(markerRange)", "'ect'");
+        // Iterate backwards the second marker for 6 characters, the range should be "sel"
+        for (var i = 0; i < 6; i++) {
+            currentMarker = text3.previousTextMarker(currentMarker);
+        }
+        marker2 = currentMarker;
+        markerRange = text3.textMarkerRangeForMarkers(marker1, marker2);
+        shouldBe("text3.stringForTextMarkerRange(markerRange)", "'sel'");
+
+        // Check the case with password field.
+        var psw = accessibilityController.accessibleElementById("psw");
+        var textMarkerRange3 = psw.textMarkerRangeForElement(psw);
+        var start = psw.startTextMarkerForTextMarkerRange(textMarkerRange3);
+        shouldBeTrue("!psw.accessibilityElementForTextMarker(start)");
+        
+        // Check next/previous text marker call will skip password field
+        // We start from text2 and advance 6 characters, it should skip the password field and land on text3.
+        currentMarker = text2.startTextMarkerForTextMarkerRange(textMarkerRange2);
+        for (var i = 0; i < 6; i++) {
+            currentMarker = text2.nextTextMarker(currentMarker);
+        }
+        shouldBeTrue("text2.accessibilityElementForTextMarker(currentMarker).isEqual(text3)");
+        // Check previous text marker, it should land on " d" node.
+        currentMarker = text2.previousTextMarker(currentMarker);
+        shouldBeTrue("text2.accessibilityElementForTextMarker(currentMarker).isEqual(text2.childAtIndex(2))");
+        
+    }
+
+</script>
+
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/LayoutTests/accessibility/mac/text-marker-with-user-select-none-expected.txt b/LayoutTests/accessibility/mac/text-marker-with-user-select-none-expected.txt
new file mode 100644 (file)
index 0000000..d5272d2
--- /dev/null
@@ -0,0 +1,16 @@
+hello test world test hello
+link to here
+test
+This tests that accessibility text markers still work even when user-select:none is set.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS textElement.textMarkerRangeLength(textMarkerRange) is 45
+PASS text is "hello test world test hello\nlink to here\ntest"
+PASS text is 'h'
+PASS element.isEqual(textElement.childAtIndex(0)) is true
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/accessibility/mac/text-marker-with-user-select-none.html b/LayoutTests/accessibility/mac/text-marker-with-user-select-none.html
new file mode 100644 (file)
index 0000000..46f9181
--- /dev/null
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
+<html>
+<head>
+<script src="../../resources/js-test-pre.js"></script>
+</head>
+<body id="body">
+
+<div id="text" style="-webkit-user-select:none">
+
+hello test <b>world</b> test hello<br>
+<a href="#">link</a> to <a href="#">here</a><br>
+test
+
+</div>
+
+
+
+<p id="description"></p>
+<div id="console"></div>
+
+<script>
+
+    description("This tests that accessibility text markers still work even when user-select:none is set.");
+
+    if (window.accessibilityController) {
+
+          var textElement = accessibilityController.accessibleElementById("text");
+          var textMarkerRange = textElement.textMarkerRangeForElement(textElement);
+          shouldBe("textElement.textMarkerRangeLength(textMarkerRange)", "45");
+
+          var startMarker = textElement.startTextMarkerForTextMarkerRange(textMarkerRange);
+          var endMarker = textElement.endTextMarkerForTextMarkerRange(textMarkerRange);
+          textMarkerRange = textElement.textMarkerRangeForMarkers(startMarker, endMarker);
+          var text = textElement.stringForTextMarkerRange(textMarkerRange);
+          shouldBeEqualToString("text", "hello test world test hello\nlink to here\ntest");
+
+          var nextMarker = textElement.nextTextMarker(startMarker);
+          textMarkerRange = textElement.textMarkerRangeForMarkers(startMarker, nextMarker);
+          text = textElement.stringForTextMarkerRange(textMarkerRange);
+          shouldBe("text", "'h'");
+          var element = textElement.accessibilityElementForTextMarker(nextMarker);
+          shouldBeTrue("element.isEqual(textElement.childAtIndex(0))");
+    }
+
+</script>
+
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
\ No newline at end of file
index f2a4513..583d557 100644 (file)
@@ -1,3 +1,77 @@
+2016-01-18  Nan Wang  <n_wang@apple.com>
+
+        AX: [Mac] Implement next/previous text marker functions using TextIterator
+        https://bugs.webkit.org/show_bug.cgi?id=152728
+
+        Reviewed by Chris Fleizach.
+
+        The existing AXTextMarker based calls are implemented using visible position, and that introduced
+        some bugs which make VoiceOver working incorrectly on Mac sometimes. Since TextIterator uses rendering
+        position, we tried to use it to refactor those AXTextMarker based calls.
+        In this patch, I implemented functions to navigate to previous/next text marker using Range and TextIterator.
+        Also added a conversion between visible position and character offset to make sure unconverted text marker
+        related functions are still working correctly.
+
+        Tests: accessibility/mac/previous-next-text-marker.html
+               accessibility/mac/text-marker-with-user-select-none.html
+
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::AXObjectCache::visiblePositionForTextMarkerData):
+        (WebCore::AXObjectCache::traverseToOffsetInRange):
+        (WebCore::AXObjectCache::lengthForRange):
+        (WebCore::AXObjectCache::rangeForNodeContents):
+        (WebCore::characterOffsetsInOrder):
+        (WebCore::AXObjectCache::rangeForUnorderedCharacterOffsets):
+        (WebCore::AXObjectCache::setTextMarkerDataWithCharacterOffset):
+        (WebCore::AXObjectCache::startOrEndTextMarkerDataForRange):
+        (WebCore::AXObjectCache::textMarkerDataForCharacterOffset):
+        (WebCore::AXObjectCache::nextNode):
+        (WebCore::AXObjectCache::previousNode):
+        (WebCore::AXObjectCache::visiblePositionFromCharacterOffset):
+        (WebCore::AXObjectCache::characterOffsetFromVisiblePosition):
+        (WebCore::AXObjectCache::accessibilityObjectForTextMarkerData):
+        (WebCore::AXObjectCache::textMarkerDataForVisiblePosition):
+        * accessibility/AXObjectCache.h:
+        (WebCore::CharacterOffset::CharacterOffset):
+        (WebCore::CharacterOffset::remaining):
+        (WebCore::CharacterOffset::isNull):
+        (WebCore::AXObjectCache::setNodeInUse):
+        (WebCore::AXObjectCache::removeNodeForUse):
+        (WebCore::AXObjectCache::isNodeInUse):
+        * accessibility/AccessibilityObject.cpp:
+        (WebCore::AccessibilityObject::selectionRange):
+        (WebCore::AccessibilityObject::elementRange):
+        (WebCore::AccessibilityObject::selectText):
+        (WebCore::AccessibilityObject::lineRangeForPosition):
+        (WebCore::AccessibilityObject::replacedNodeNeedsCharacter):
+        (WebCore::renderListItemContainerForNode):
+        (WebCore::listMarkerTextForNode):
+        (WebCore::AccessibilityObject::listMarkerTextForNodeAndPosition):
+        (WebCore::AccessibilityObject::stringForRange):
+        (WebCore::AccessibilityObject::stringForVisiblePositionRange):
+        (WebCore::replacedNodeNeedsCharacter): Deleted.
+        * accessibility/AccessibilityObject.h:
+        (WebCore::AccessibilityObject::visiblePositionRange):
+        (WebCore::AccessibilityObject::visiblePositionRangeForLine):
+        (WebCore::AccessibilityObject::boundsForVisiblePositionRange):
+        (WebCore::AccessibilityObject::setSelectedVisiblePositionRange):
+        * accessibility/mac/WebAccessibilityObjectWrapperMac.mm:
+        (isTextMarkerIgnored):
+        (-[WebAccessibilityObjectWrapper accessibilityObjectForTextMarker:]):
+        (accessibilityObjectForTextMarker):
+        (-[WebAccessibilityObjectWrapper textMarkerRangeFromRange:]):
+        (textMarkerRangeFromRange):
+        (-[WebAccessibilityObjectWrapper startOrEndTextMarkerForRange:isStart:]):
+        (startOrEndTextmarkerForRange):
+        (-[WebAccessibilityObjectWrapper nextTextMarkerForNode:offset:]):
+        (-[WebAccessibilityObjectWrapper previousTextMarkerForNode:offset:]):
+        (-[WebAccessibilityObjectWrapper textMarkerForNode:offset:]):
+        (textMarkerForCharacterOffset):
+        (-[WebAccessibilityObjectWrapper rangeForTextMarkerRange:]):
+        (-[WebAccessibilityObjectWrapper characterOffsetForTextMarker:]):
+        (textMarkerForVisiblePosition):
+        (-[WebAccessibilityObjectWrapper accessibilityAttributeValue:forParameter:]):
+
 2016-01-18  Olivier Blin  <olivier.blin@softathome.com>
 
         [Mac] Remove unused playerToPrivateMap()
index 538b955..19985fa 100644 (file)
@@ -81,6 +81,7 @@
 #include "RenderTableRow.h"
 #include "RenderView.h"
 #include "ScrollView.h"
+#include "TextIterator.h"
 #include <wtf/DataLog.h>
 
 #if ENABLE(VIDEO)
@@ -1418,6 +1419,332 @@ VisiblePosition AXObjectCache::visiblePositionForTextMarkerData(TextMarkerData&
     return visiblePos;
 }
 
+CharacterOffset AXObjectCache::traverseToOffsetInRange(RefPtr<Range>range, int offset, bool toNodeEnd, bool stayWithinRange)
+{
+    if (!range)
+        return CharacterOffset();
+    
+    int offsetInCharacter = 0;
+    int offsetSoFar = 0;
+    int remaining = 0;
+    int lastLength = 0;
+    Node* currentNode = nullptr;
+    bool finished = false;
+    int lastStartOffset = 0;
+    
+    TextIterator iterator(range.get());
+    
+    // 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()))
+                offsetSoFar++;
+            lastLength = offsetSoFar;
+            
+            // 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, toNodeEnd);
+                return CharacterOffset();
+            }
+        }
+    }
+    
+    for (; !iterator.atEnd(); iterator.advance()) {
+        int currentLength = iterator.text().length();
+        
+        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)) {
+                offsetSoFar++;
+                currentLength++;
+                currentNode = childNode;
+            } else
+                continue;
+        } else {
+            // Ignore space, new line, tag node.
+            if (currentLength == 1 && isSpaceOrNewline(iterator.text()[0]))
+                continue;
+            offsetSoFar += currentLength;
+        }
+
+        lastLength = currentLength;
+        lastStartOffset = iterator.range()->startOffset();
+        
+        // Break early if we have advanced enough characters.
+        if (!toNodeEnd && offsetSoFar >= offset) {
+            offsetInCharacter = offset - (offsetSoFar - currentLength);
+            finished = true;
+            break;
+        }
+    }
+    
+    if (!finished) {
+        offsetInCharacter = lastLength;
+        if (!toNodeEnd)
+            remaining = offset - offsetSoFar;
+    }
+    
+    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;
+    RefPtr<Range> range = Range::create(*document);
+    ExceptionCode ec = 0;
+    range->selectNodeContents(node, ec);
+    return ec ? nullptr : range;
+}
+
+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;
+    
+    RefPtr<Range> range1 = AXObjectCache::rangeForNodeContents(characterOffset1.node);
+    RefPtr<Range> range2 = AXObjectCache::rangeForNodeContents(characterOffset2.node);
+    return range1->compareBoundaryPoints(Range::START_TO_START, range2.get(), IGNORE_EXCEPTION) <= 0;
+}
+
+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;
+    
+    int endOffset = endCharacterOffset.offset;
+    
+    // endOffset can be out of bounds sometimes if the node is a replaced node or has brTag.
+    if (startCharacterOffset.node == endCharacterOffset.node) {
+        RefPtr<Range> nodeRange = AXObjectCache::rangeForNodeContents(startCharacterOffset.node);
+        int nodeLength = TextIterator::rangeLength(nodeRange.get());
+        if (endOffset > nodeLength)
+            endOffset = nodeLength;
+    }
+    
+    int startOffset = startCharacterOffset.startIndex + startCharacterOffset.offset;
+    endOffset = endCharacterOffset.startIndex + endOffset;
+    
+    // If start node is a replaced node and it has children, we want to include the replaced node itself in the range.
+    Node* startNode = startCharacterOffset.node;
+    if (AccessibilityObject::replacedNodeNeedsCharacter(startNode) && (startNode->hasChildNodes() || startNode != endCharacterOffset.node)) {
+        startOffset = startNode->computeNodeIndex();
+        startNode = startNode->parentNode();
+    }
+    
+    RefPtr<Range> result = Range::create(m_document);
+    ExceptionCode ecStart = 0, ecEnd = 0;
+    result->setStart(startNode, startOffset, ecStart);
+    result->setEnd(endCharacterOffset.node, endOffset, ecEnd);
+    if (ecStart || ecEnd)
+        return nullptr;
+    
+    return 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);
+    
+    // Convert to visible position.
+    VisiblePosition visiblePosition = visiblePositionFromCharacterOffset(obj.get(), 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);
+}
+
+void AXObjectCache::startOrEndTextMarkerDataForRange(TextMarkerData& textMarkerData, RefPtr<Range> range, bool isStart)
+{
+    memset(&textMarkerData, 0, sizeof(TextMarkerData));
+    
+    if (!range)
+        return;
+    
+    // If it's end text marker, we want to go to the end of the range, and stay within the range.
+    bool stayWithinRange = !isStart;
+    
+    // Change the start of the range, so the character offset starts from node beginning.
+    int offset = 0;
+    Node* node = &range->startContainer();
+    if (node->offsetInCharacters()) {
+        CharacterOffset nodeStartOffset = traverseToOffsetInRange(rangeForNodeContents(node), 0, false);
+        offset = std::max(range->startOffset() - nodeStartOffset.startIndex, 0);
+        range->setStart(node, nodeStartOffset.startIndex);
+    }
+    
+    CharacterOffset characterOffset = traverseToOffsetInRange(range, offset, !isStart, stayWithinRange);
+    setTextMarkerDataWithCharacterOffset(textMarkerData, characterOffset);
+}
+
+void AXObjectCache::textMarkerDataForCharacterOffset(TextMarkerData& textMarkerData, Node& node, int offset, bool toNodeEnd)
+{
+    memset(&textMarkerData, 0, sizeof(TextMarkerData));
+    
+    Node* domNode = &node;
+    if (!domNode)
+        return;
+    
+    // If offset <= 0, means we want to go to the previous node.
+    if (offset <= 0 && !toNodeEnd) {
+        // Set the offset to the amount of characters we need to go backwards.
+        offset = - offset + 1;
+        while (offset > 0 && textMarkerData.characterOffset <= offset) {
+            offset -= textMarkerData.characterOffset;
+            domNode = previousNode(domNode);
+            if (domNode) {
+                textMarkerDataForCharacterOffset(textMarkerData, *domNode, 0, true);
+                offset--;
+            } else
+                return;
+        }
+        if (offset > 0)
+            textMarkerDataForCharacterOffset(textMarkerData, *domNode, offset, false);
+        return;
+    }
+    
+    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, toNodeEnd);
+    while (!characterOffset.isNull() && characterOffset.remaining() && !toNodeEnd) {
+        domNode = nextNode(domNode);
+        if (!domNode)
+            return;
+        range = rangeForNodeContents(domNode);
+        characterOffset = traverseToOffsetInRange(range, characterOffset.remaining(), toNodeEnd);
+    }
+    
+    setTextMarkerDataWithCharacterOffset(textMarkerData, characterOffset);
+}
+
+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(AccessibilityObject* obj, const CharacterOffset& characterOffset)
+{
+    if (!obj)
+        return VisiblePosition();
+    
+    // nextVisiblePosition means advancing one character. Use this to calculate the character offset.
+    VisiblePositionRange vpRange = obj->visiblePositionRange();
+    VisiblePosition start = vpRange.start;
+    VisiblePosition result = start;
+    for (int i = 0; i < characterOffset.offset; i++)
+        result = obj->nextVisiblePosition(result);
+    
+    return result;
+}
+
+CharacterOffset AXObjectCache::characterOffsetFromVisiblePosition(AccessibilityObject* obj, const VisiblePosition& visiblePos)
+{
+    if (!obj)
+        return 0;
+    
+    // Use nextVisiblePosition to calculate how many characters we need to traverse to the current position.
+    Position deepPos = visiblePos.deepEquivalent();
+    VisiblePositionRange vpRange = obj->visiblePositionRange();
+    VisiblePosition vp = vpRange.start;
+    int characterOffset = 0;
+    Position vpDeepPos = vp.deepEquivalent();
+    
+    while (!vpDeepPos.isNull() && !deepPos.equals(vpDeepPos)) {
+        vp = obj->nextVisiblePosition(vp);
+        vpDeepPos = vp.deepEquivalent();
+        characterOffset++;
+    }
+    
+    return traverseToOffsetInRange(rangeForNodeContents(obj->node()), characterOffset, false);
+}
+
+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)
 {
     // This memory must be bzero'd so instances of TextMarkerData can be tested for byte-equivalence.
@@ -1443,7 +1770,12 @@ 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();
+    
+    // convert to character offset
+    CharacterOffset characterOffset = characterOffsetFromVisiblePosition(obj.get(), visiblePos);
+    textMarkerData.characterOffset = characterOffset.offset;
+    textMarkerData.characterStartIndex = characterOffset.startIndex;
     
     cache->setNodeInUse(domNode);
 }
index 9d24d4d..812f0a0 100644 (file)
@@ -51,9 +51,29 @@ struct TextMarkerData {
     AXID axID;
     Node* node;
     int offset;
+    int characterStartIndex;
+    int characterOffset;
+    bool ignored;
     EAffinity affinity;
 };
 
+struct CharacterOffset {
+    Node* node;
+    int startIndex;
+    int offset;
+    int remainingOffset;
+    
+    CharacterOffset(Node* n = nullptr, int startIndex = 0, int offset = 0, int remaining = 0)
+        : node(n)
+        , startIndex(startIndex)
+        , offset(offset)
+        , remainingOffset(remaining)
+    { }
+    
+    int remaining() const { return remainingOffset; }
+    bool isNull() const { return !node; }
+};
+
 class AXComputedObjectAttributeCache {
 public:
     AccessibilityObjectInclusion getIgnored(AXID) const;
@@ -164,6 +184,12 @@ public:
     // Text marker utilities.
     void textMarkerDataForVisiblePosition(TextMarkerData&, const VisiblePosition&);
     VisiblePosition visiblePositionForTextMarkerData(TextMarkerData&);
+    void textMarkerDataForCharacterOffset(TextMarkerData&, Node&, int, bool toNodeEnd = false);
+    void startOrEndTextMarkerDataForRange(TextMarkerData&, RefPtr<Range>, bool);
+    AccessibilityObject* accessibilityObjectForTextMarkerData(TextMarkerData&);
+    RefPtr<Range> rangeForUnorderedCharacterOffsets(const CharacterOffset&, const CharacterOffset&);
+    static RefPtr<Range> rangeForNodeContents(Node*);
+    static int lengthForRange(Range*);
 
     enum AXNotification {
         AXActiveDescendantChanged,
@@ -254,6 +280,13 @@ protected:
     void setNodeInUse(Node* n) { m_textMarkerNodes.add(n); }
     void removeNodeForUse(Node* n) { m_textMarkerNodes.remove(n); }
     bool isNodeInUse(Node* n) { return m_textMarkerNodes.contains(n); }
+    
+    Node* nextNode(Node*) const;
+    Node* previousNode(Node*) const;
+    CharacterOffset traverseToOffsetInRange(RefPtr<Range>, int, bool, bool stayWithinRange = false);
+    VisiblePosition visiblePositionFromCharacterOffset(AccessibilityObject*, const CharacterOffset&);
+    CharacterOffset characterOffsetFromVisiblePosition(AccessibilityObject*, const VisiblePosition&);
+    void setTextMarkerDataWithCharacterOffset(TextMarkerData&, const CharacterOffset&);
 
 private:
     AccessibilityObject* rootWebArea();
index 29dfa05..1566732 100644 (file)
@@ -715,6 +715,11 @@ RefPtr<Range> AccessibilityObject::selectionRange() const
     return Range::create(*frame->document());
 }
 
+RefPtr<Range> AccessibilityObject::elementRange() const
+{    
+    return AXObjectCache::rangeForNodeContents(node());
+}
+
 String AccessibilityObject::selectText(AccessibilitySelectTextCriteria* criteria)
 {
     ASSERT(criteria);
@@ -1203,7 +1208,7 @@ VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosi
     return VisiblePositionRange(startPosition, endPosition);
 }
 
-static bool replacedNodeNeedsCharacter(Node* replacedNode)
+bool AccessibilityObject::replacedNodeNeedsCharacter(Node* replacedNode)
 {
     // we should always be given a rendered node and a replaced node, but be safe
     // replaced nodes are either attachments (widgets) or images
@@ -1228,7 +1233,19 @@ static RenderListItem* renderListItemContainerForNode(Node* node)
     }
     return nullptr;
 }
+
+static String listMarkerTextForNode(Node* node)
+{
+    RenderListItem* listItem = renderListItemContainerForNode(node);
+    if (!listItem)
+        return String();
     
+    // If this is in a list item, we need to manually add the text for the list marker
+    // because a RenderListMarker does not have a Node equivalent and thus does not appear
+    // when iterating text.
+    return listItem->markerTextWithSuffix();
+}
+
 // Returns the text associated with a list marker if this node is contained within a list item.
 String AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, const VisiblePosition& visiblePositionStart) const
 {
@@ -1236,14 +1253,36 @@ String AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, const V
     if (!isStartOfLine(visiblePositionStart))
         return String();
 
-    RenderListItem* listItem = renderListItemContainerForNode(node);
-    if (!listItem)
+    return listMarkerTextForNode(node);
+}
+
+String AccessibilityObject::stringForRange(RefPtr<Range> range) const
+{
+    if (!range)
         return String();
-        
-    // If this is in a list item, we need to manually add the text for the list marker 
-    // because a RenderListMarker does not have a Node equivalent and thus does not appear
-    // when iterating text.
-    return listItem->markerTextWithSuffix();
+    
+    TextIterator it(range.get());
+    if (it.atEnd())
+        return String();
+    
+    StringBuilder builder;
+    for (; !it.atEnd(); it.advance()) {
+        // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
+        if (it.text().length()) {
+            // Add a textual representation for list marker text.
+            builder.append(listMarkerTextForNode(it.node()));
+            it.appendTextToStringBuilder(builder);
+        } else {
+            // locate the node and starting offset for this replaced range
+            Node& node = it.range()->startContainer();
+            ASSERT(&node == &it.range()->endContainer());
+            int offset = it.range()->startOffset();
+            if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
+                builder.append(objectReplacementCharacter);
+        }
+    }
+    
+    return builder.toString();
 }
 
 String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
index 9884ac0..165f888 100644 (file)
@@ -817,6 +817,9 @@ public:
     virtual VisiblePositionRange visiblePositionRange() const { return VisiblePositionRange(); }
     virtual VisiblePositionRange visiblePositionRangeForLine(unsigned) const { return VisiblePositionRange(); }
     
+    RefPtr<Range> elementRange() const;
+    static bool replacedNodeNeedsCharacter(Node* replacedNode);
+    
     VisiblePositionRange visiblePositionRangeForUnorderedPositions(const VisiblePosition&, const VisiblePosition&) const;
     VisiblePositionRange positionOfLeftWord(const VisiblePosition&) const;
     VisiblePositionRange positionOfRightWord(const VisiblePosition&) const;
@@ -829,6 +832,7 @@ public:
     VisiblePositionRange lineRangeForPosition(const VisiblePosition&) const;
 
     String stringForVisiblePositionRange(const VisiblePositionRange&) const;
+    String stringForRange(RefPtr<Range>) const;
     virtual IntRect boundsForVisiblePositionRange(const VisiblePositionRange&) const { return IntRect(); }
     int lengthForVisiblePositionRange(const VisiblePositionRange&) const;
     virtual void setSelectedVisiblePositionRange(const VisiblePositionRange&) const { }
index 9d11b34..60b84c4 100644 (file)
@@ -782,6 +782,136 @@ static AccessibilitySelectTextCriteria accessibilitySelectTextCriteriaForCriteri
 
 #pragma mark Text Marker helpers
 
+static bool isTextMarkerIgnored(id textMarker)
+{
+    if (!textMarker)
+        return false;
+    
+    TextMarkerData textMarkerData;
+    if (!wkGetBytesFromAXTextMarker(textMarker, &textMarkerData, sizeof(textMarkerData)))
+        return false;
+    
+    return textMarkerData.ignored;
+}
+
+- (AccessibilityObject*)accessibilityObjectForTextMarker:(id)textMarker
+{
+    return accessibilityObjectForTextMarker(m_object->axObjectCache(), textMarker);
+}
+
+static AccessibilityObject* accessibilityObjectForTextMarker(AXObjectCache* cache, id textMarker)
+{
+    if (!textMarker || !cache || isTextMarkerIgnored(textMarker))
+        return nullptr;
+    
+    TextMarkerData textMarkerData;
+    if (!wkGetBytesFromAXTextMarker(textMarker, &textMarkerData, sizeof(textMarkerData)))
+        return nullptr;
+    return cache->accessibilityObjectForTextMarkerData(textMarkerData);
+}
+
+- (id)textMarkerRangeFromRange:(const RefPtr<Range>)range
+{
+    return textMarkerRangeFromRange(m_object->axObjectCache(), range);
+}
+
+static id textMarkerRangeFromRange(AXObjectCache *cache, const RefPtr<Range> range)
+{
+    id startTextMarker = startOrEndTextmarkerForRange(cache, range, true);
+    id endTextMarker = startOrEndTextmarkerForRange(cache, range, false);
+    return textMarkerRangeFromMarkers(startTextMarker, endTextMarker);
+}
+
+- (id)startOrEndTextMarkerForRange:(const RefPtr<Range>)range isStart:(BOOL)isStart
+{
+    return startOrEndTextmarkerForRange(m_object->axObjectCache(), range, isStart);
+}
+
+static id startOrEndTextmarkerForRange(AXObjectCache* cache, RefPtr<Range> range, bool isStart)
+{
+    if (!cache)
+        return nil;
+    
+    TextMarkerData textMarkerData;
+    cache->startOrEndTextMarkerDataForRange(textMarkerData, range, isStart);
+    if (!textMarkerData.axID)
+        return nil;
+    
+    return CFBridgingRelease(wkCreateAXTextMarker(&textMarkerData, sizeof(textMarkerData)));
+}
+
+- (id)nextTextMarkerForNode:(Node&)node offset:(int)offset
+{
+    int nextOffset = offset + 1;
+    id textMarker = [self textMarkerForNode:node offset:nextOffset];
+    if (isTextMarkerIgnored(textMarker))
+        textMarker = [self nextTextMarkerForNode:node offset:nextOffset];
+    return textMarker;
+}
+
+- (id)previousTextMarkerForNode:(Node&)node offset:(int)offset
+{
+    int previousOffset = offset - 1;
+    id textMarker = [self textMarkerForNode:node offset:previousOffset];
+    if (isTextMarkerIgnored(textMarker))
+        textMarker = [self previousTextMarkerForNode:node offset:previousOffset];
+    return textMarker;
+}
+
+- (id)textMarkerForNode:(Node&)node offset:(int)offset
+{
+    return textMarkerForCharacterOffset(m_object->axObjectCache(), node, offset);
+}
+
+static id textMarkerForCharacterOffset(AXObjectCache* cache, Node& node, int offset, bool toNodeEnd = false)
+{
+    if (!cache)
+        return nil;
+    
+    Node* domNode = &node;
+    if (!domNode)
+        return nil;
+    
+    TextMarkerData textMarkerData;
+    cache->textMarkerDataForCharacterOffset(textMarkerData, node, offset, toNodeEnd);
+    if (!textMarkerData.axID && !textMarkerData.ignored)
+        return nil;
+    
+    return CFBridgingRelease(wkCreateAXTextMarker(&textMarkerData, sizeof(textMarkerData)));
+}
+
+- (RefPtr<Range>)rangeForTextMarkerRange:(id)textMarkerRange
+{
+    if (!textMarkerRange)
+        return nullptr;
+    
+    id startTextMarker = AXTextMarkerRangeStart(textMarkerRange);
+    id endTextMarker = AXTextMarkerRangeEnd(textMarkerRange);
+    
+    if (!startTextMarker || !endTextMarker)
+        return nullptr;
+    
+    AXObjectCache* cache = m_object->axObjectCache();
+    if (!cache)
+        return nullptr;
+    
+    CharacterOffset startCharacterOffset = [self characterOffsetForTextMarker:startTextMarker];
+    CharacterOffset endCharacterOffset = [self characterOffsetForTextMarker:endTextMarker];
+    return cache->rangeForUnorderedCharacterOffsets(startCharacterOffset, endCharacterOffset);
+}
+
+- (CharacterOffset)characterOffsetForTextMarker:(id)textMarker
+{
+    if (!textMarker || isTextMarkerIgnored(textMarker))
+        return CharacterOffset();
+    
+    TextMarkerData textMarkerData;
+    if (!wkGetBytesFromAXTextMarker(textMarker, &textMarkerData, sizeof(textMarkerData)))
+        return CharacterOffset();
+    
+    return CharacterOffset(textMarkerData.node, textMarkerData.characterStartIndex, textMarkerData.characterOffset);
+}
+
 static id textMarkerForVisiblePosition(AXObjectCache* cache, const VisiblePosition& visiblePos)
 {
     ASSERT(cache);
@@ -3828,16 +3958,15 @@ static void formatForDebugger(const VisiblePositionRange& range, char* buffer, u
     }
     
     if ([attribute isEqualToString:@"AXUIElementForTextMarker"]) {
-        VisiblePosition visiblePos = [self visiblePositionForTextMarker:(textMarker)];
-        AccessibilityObject* axObject = m_object->accessibilityObjectForPosition(visiblePos);
+        AccessibilityObject* axObject = [self accessibilityObjectForTextMarker:textMarker];
         if (!axObject)
             return nil;
         return axObject->wrapper();
     }
     
     if ([attribute isEqualToString:@"AXTextMarkerRangeForUIElement"]) {
-        VisiblePositionRange vpRange = uiElement.get()->visiblePositionRange();
-        return [self textMarkerRangeFromVisiblePositions:vpRange.start endPosition:vpRange.end];
+        RefPtr<Range> range = uiElement.get()->elementRange();
+        return [self textMarkerRangeFromRange:range];
     }
     
     if ([attribute isEqualToString:@"AXLineForTextMarker"]) {
@@ -3851,8 +3980,8 @@ static void formatForDebugger(const VisiblePositionRange& range, char* buffer, u
     }
     
     if ([attribute isEqualToString:@"AXStringForTextMarkerRange"]) {
-        VisiblePositionRange visiblePosRange = [self visiblePositionRangeForTextMarkerRange:textMarkerRange];
-        return m_object->stringForVisiblePositionRange(visiblePosRange);
+        RefPtr<Range> range = [self rangeForTextMarkerRange:textMarkerRange];
+        return m_object->stringForRange(range);
     }
     
     if ([attribute isEqualToString:@"AXTextMarkerForPosition"]) {
@@ -3895,20 +4024,23 @@ static void formatForDebugger(const VisiblePositionRange& range, char* buffer, u
         if (!AXObjectIsTextMarker(textMarker1) || !AXObjectIsTextMarker(textMarker2))
             return nil;
         
-        VisiblePosition visiblePos1 = [self visiblePositionForTextMarker:(textMarker1)];
-        VisiblePosition visiblePos2 = [self visiblePositionForTextMarker:(textMarker2)];
-        VisiblePositionRange vpRange = m_object->visiblePositionRangeForUnorderedPositions(visiblePos1, visiblePos2);
-        return [self textMarkerRangeFromVisiblePositions:vpRange.start endPosition:vpRange.end];
+        AXObjectCache* cache = m_object->axObjectCache();
+        if (!cache)
+            return nil;
+        CharacterOffset characterOffset1 = [self characterOffsetForTextMarker:textMarker1];
+        CharacterOffset characterOffset2 = [self characterOffsetForTextMarker:textMarker2];
+        RefPtr<Range> range = cache->rangeForUnorderedCharacterOffsets(characterOffset1, characterOffset2);
+        return [self textMarkerRangeFromRange:range];
     }
     
     if ([attribute isEqualToString:@"AXNextTextMarkerForTextMarker"]) {
-        VisiblePosition visiblePos = [self visiblePositionForTextMarker:(textMarker)];
-        return [self textMarkerForVisiblePosition:m_object->nextVisiblePosition(visiblePos)];
+        CharacterOffset characterOffset = [self characterOffsetForTextMarker:textMarker];
+        return [self nextTextMarkerForNode:*characterOffset.node offset:characterOffset.offset];
     }
     
     if ([attribute isEqualToString:@"AXPreviousTextMarkerForTextMarker"]) {
-        VisiblePosition visiblePos = [self visiblePositionForTextMarker:(textMarker)];
-        return [self textMarkerForVisiblePosition:m_object->previousVisiblePosition(visiblePos)];
+        CharacterOffset characterOffset = [self characterOffsetForTextMarker:textMarker];
+        return [self previousTextMarkerForNode:*characterOffset.node offset:characterOffset.offset];
     }
     
     if ([attribute isEqualToString:@"AXLeftWordTextMarkerRangeForTextMarker"]) {
@@ -3994,8 +4126,8 @@ static void formatForDebugger(const VisiblePositionRange& range, char* buffer, u
     }
     
     if ([attribute isEqualToString:@"AXLengthForTextMarkerRange"]) {
-        VisiblePositionRange visiblePosRange = [self visiblePositionRangeForTextMarkerRange:textMarkerRange];
-        int length = m_object->lengthForVisiblePositionRange(visiblePosRange);
+        RefPtr<Range> range = [self rangeForTextMarkerRange:textMarkerRange];
+        int length = AXObjectCache::lengthForRange(range.get());
         if (length < 0)
             return nil;
         return [NSNumber numberWithInt:length];
@@ -4003,13 +4135,13 @@ static void formatForDebugger(const VisiblePositionRange& range, char* buffer, u
     
     // Used only by DumpRenderTree (so far).
     if ([attribute isEqualToString:@"AXStartTextMarkerForTextMarkerRange"]) {
-        VisiblePositionRange visiblePosRange = [self visiblePositionRangeForTextMarkerRange:textMarkerRange];
-        return [self textMarkerForVisiblePosition:visiblePosRange.start];
+        RefPtr<Range> range = [self rangeForTextMarkerRange:textMarkerRange];
+        return [self startOrEndTextMarkerForRange:range isStart:YES];
     }
     
     if ([attribute isEqualToString:@"AXEndTextMarkerForTextMarkerRange"]) {
-        VisiblePositionRange visiblePosRange = [self visiblePositionRangeForTextMarkerRange:textMarkerRange];
-        return [self textMarkerForVisiblePosition:visiblePosRange.end];
+        RefPtr<Range> range = [self rangeForTextMarkerRange:textMarkerRange];
+        return [self startOrEndTextMarkerForRange:range isStart:NO];
     }
 
 #if ENABLE(TREE_DEBUGGING)
index eb7af72..2a41ede 100644 (file)
@@ -1,3 +1,13 @@
+2016-01-18  Nan Wang  <n_wang@apple.com>
+
+        AX: [Mac] Implement next/previous text marker functions using TextIterator
+        https://bugs.webkit.org/show_bug.cgi?id=152728
+
+        Reviewed by Chris Fleizach.
+
+        * WebKitTestRunner/InjectedBundle/mac/AccessibilityUIElementMac.mm:
+        (WTR::AccessibilityUIElement::accessibilityElementForTextMarker):
+
 2016-01-18  Csaba Osztrogon√°c  <ossy@webkit.org>
 
         [cmake] Add testair to the build system
index e01ba17..aad5671 100644 (file)
@@ -1801,7 +1801,8 @@ PassRefPtr<AccessibilityUIElement> AccessibilityUIElement::accessibilityElementF
 {
     BEGIN_AX_OBJC_EXCEPTIONS
     id uiElement = [m_element accessibilityAttributeValue:@"AXUIElementForTextMarker" forParameter:(id)marker->platformTextMarker()];
-    return AccessibilityUIElement::create(uiElement);
+    if (uiElement)
+        return AccessibilityUIElement::create(uiElement);
     END_AX_OBJC_EXCEPTIONS
     
     return nullptr;