Title: [200258] trunk
Revision
200258
Author
n_w...@apple.com
Date
2016-04-29 13:05:07 -0700 (Fri, 29 Apr 2016)

Log Message

AX: CharacterOffset not working correctly with composed characters and collapsed white spaces
https://bugs.webkit.org/show_bug.cgi?id=157190

Reviewed by Chris Fleizach.

Source/WebCore:

When navigating emoji, next/previous text marker call is only moving by one character. Fixed it by
using the helper function in Position to get the real character count for the composed character sequence.
Also there's another issue with collapsed white spaces, TextIterator emits only one space. So we have to
use the actual space length to create the CharacterOffset in order to generate valid Range object from it.

New test cases in accessibility/text-marker/text-marker-previous-next.html.

* accessibility/AXObjectCache.cpp:
(WebCore::AXObjectCache::traverseToOffsetInRange):
(WebCore::AXObjectCache::textMarkerDataForNextCharacterOffset):
(WebCore::AXObjectCache::textMarkerDataForPreviousCharacterOffset):
(WebCore::AXObjectCache::nextNode):
(WebCore::AXObjectCache::characterOffsetFromVisiblePosition):
(WebCore::AXObjectCache::nextCharacterOffset):
(WebCore::AXObjectCache::previousCharacterOffset):
(WebCore::AXObjectCache::startCharacterOffsetOfWord):

LayoutTests:

* accessibility/mac/text-marker-word-nav.html:
* accessibility/text-marker/text-marker-previous-next-expected.txt:
* accessibility/text-marker/text-marker-previous-next.html:

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (200257 => 200258)


--- trunk/LayoutTests/ChangeLog	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/LayoutTests/ChangeLog	2016-04-29 20:05:07 UTC (rev 200258)
@@ -1,3 +1,14 @@
+2016-04-29  Nan Wang  <n_w...@apple.com>
+
+        AX: CharacterOffset not working correctly with composed characters and collapsed white spaces
+        https://bugs.webkit.org/show_bug.cgi?id=157190
+
+        Reviewed by Chris Fleizach.
+
+        * accessibility/mac/text-marker-word-nav.html:
+        * accessibility/text-marker/text-marker-previous-next-expected.txt:
+        * accessibility/text-marker/text-marker-previous-next.html:
+
 2016-04-29  Ryan Haddad  <ryanhad...@apple.com>
 
         Marking fast/ruby/ruby-expansion-cjk.html and fast/ruby/ruby-expansion-cjk-4.html as flaky on Mac

Modified: trunk/LayoutTests/accessibility/mac/text-marker-word-nav.html (200257 => 200258)


--- trunk/LayoutTests/accessibility/mac/text-marker-word-nav.html	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/LayoutTests/accessibility/mac/text-marker-word-nav.html	2016-04-29 20:05:07 UTC (rev 200258)
@@ -67,7 +67,7 @@
         
         // Check the case with span
         // At "T" in "Thisis", should return the word as "Thisislongword".
-        currentMarker = advanceAndVerify(currentMarker, 2, text);
+        currentMarker = advanceAndVerify(currentMarker, 1, text);
         // At " " before "I", the word should be "I'll".
         currentMarker = advanceAndVerify(currentMarker, 14, text);
         // At " " before "try", the word should excludes "."

Modified: trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next-expected.txt (200257 => 200258)


--- trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next-expected.txt	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next-expected.txt	2016-04-29 20:05:07 UTC (rev 200258)
@@ -5,6 +5,10 @@
 
 can't select
 abc de f
+😃😏
+
+a b
+
 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".
@@ -28,6 +32,15 @@
 PASS text2.accessibilityElementForTextMarker(currentMarker).isEqual(text2.childAtIndex(2)) is true
 PASS text.stringForTextMarkerRange(markerRange) is 'f'
 PASS text.stringForTextMarkerRange(markerRange) is 'a'
+PASS text.textMarkerRangeLength(emojiTextMarkerRange) is 4
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is '😏'
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is '😃'
+PASS text.textMarkerRangeLength(collapsedWhitespaceMarkerRange) is 3
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is 'a'
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is ' '
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is 'b'
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is ' '
+PASS text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current)) is 'a'
 PASS successfullyParsed is true
 
 TEST COMPLETE

Modified: trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next.html (200257 => 200258)


--- trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next.html	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/LayoutTests/accessibility/text-marker/text-marker-previous-next.html	2016-04-29 20:05:07 UTC (rev 200258)
@@ -2,6 +2,7 @@
 <html>
 <head>
 <script src=""
+<meta charset="UTF-16">
 </head>
 <style>
 .userselect { user-select: none; -webkit-user-select: none; }
@@ -26,6 +27,10 @@
 f
 </div>
 
+<p id="text5">😃😏<p>
+
+<p id="text6">a     b</p>
+
 <p id="description"></p>
 <div id="console"></div>
 
@@ -71,8 +76,8 @@
         markerRange = text.textMarkerRangeForMarkers(previousMarker, currentMarker);
         shouldBe("text.stringForTextMarkerRange(markerRange)", "newline");
         
-        // Traverse backwards two more character, it will land at the last character of "text".
-        result = backwards(2, previousMarker, currentMarker, text);
+        // Traverse backwards one more character, it will land at the last character of "text".
+        result = backwards(1, previousMarker, currentMarker, text);
         previousMarker = result.previous;
         currentMarker = result.current;
         markerRange = text.textMarkerRangeForMarkers(previousMarker, currentMarker);
@@ -93,8 +98,8 @@
         markerRange = text2.textMarkerRangeForMarkers(previousMarker, currentMarker);
         shouldBe("text2.stringForTextMarkerRange(markerRange)", "'d'");
         
-        // Traverse backwards 8 characters, it will land at the last character of "text1".
-        result = backwards(8, previousMarker, currentMarker, text2);
+        // Traverse backwards 6 characters, it will land at the last character of "text1".
+        result = backwards(6, previousMarker, currentMarker, text2);
         previousMarker = result.previous;
         currentMarker = result.current;
         markerRange = text2.textMarkerRangeForMarkers(previousMarker, currentMarker);
@@ -163,6 +168,33 @@
         markerRange = text.textMarkerRangeForMarkers(startMarker, currentMarker)
         shouldBe("text.stringForTextMarkerRange(markerRange)", "'a'");
         
+        // Test case with emoji.
+        text = accessibilityController.accessibleElementById("text5");
+        var emojiTextMarkerRange = text.textMarkerRangeForElement(text);
+        shouldBe("text.textMarkerRangeLength(emojiTextMarkerRange)", "4");
+        // Make sure navigating next/previous text marker is by emoji.
+        startMarker = text.startTextMarkerForTextMarkerRange(emojiTextMarkerRange);
+        result = forward(2, previousMarker, startMarker, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "'😏'");
+        result = backwards(1, result.previous, result.current, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "'😃'");
+        
+        // Test case with collapsed whitespace.
+        text = accessibilityController.accessibleElementById("text6");
+        var collapsedWhitespaceMarkerRange = text.textMarkerRangeForElement(text);
+        shouldBe("text.textMarkerRangeLength(collapsedWhitespaceMarkerRange)", "3");
+        startMarker = text.startTextMarkerForTextMarkerRange(collapsedWhitespaceMarkerRange);
+        result = forward(1, previousMarker, startMarker, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "'a'");
+        result = forward(1, result.previous, result.current, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "' '");
+        result = forward(1, result.previous, result.current, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "'b'");
+        result = backwards(1, result.previous, result.current, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "' '");
+        result = backwards(1, result.previous, result.current, text);
+        shouldBe("text.stringForTextMarkerRange(text.textMarkerRangeForMarkers(result.previous, result.current))", "'a'");
+        
         function forward(count, previousMarker, currentMarker, obj) {
             for (var i = 0; i < count; i++) {
                 previousMarker = currentMarker;

Modified: trunk/Source/WebCore/ChangeLog (200257 => 200258)


--- trunk/Source/WebCore/ChangeLog	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/Source/WebCore/ChangeLog	2016-04-29 20:05:07 UTC (rev 200258)
@@ -1,3 +1,27 @@
+2016-04-29  Nan Wang  <n_w...@apple.com>
+
+        AX: CharacterOffset not working correctly with composed characters and collapsed white spaces
+        https://bugs.webkit.org/show_bug.cgi?id=157190
+
+        Reviewed by Chris Fleizach.
+
+        When navigating emoji, next/previous text marker call is only moving by one character. Fixed it by
+        using the helper function in Position to get the real character count for the composed character sequence.
+        Also there's another issue with collapsed white spaces, TextIterator emits only one space. So we have to 
+        use the actual space length to create the CharacterOffset in order to generate valid Range object from it.
+
+        New test cases in accessibility/text-marker/text-marker-previous-next.html.
+
+        * accessibility/AXObjectCache.cpp:
+        (WebCore::AXObjectCache::traverseToOffsetInRange):
+        (WebCore::AXObjectCache::textMarkerDataForNextCharacterOffset):
+        (WebCore::AXObjectCache::textMarkerDataForPreviousCharacterOffset):
+        (WebCore::AXObjectCache::nextNode):
+        (WebCore::AXObjectCache::characterOffsetFromVisiblePosition):
+        (WebCore::AXObjectCache::nextCharacterOffset):
+        (WebCore::AXObjectCache::previousCharacterOffset):
+        (WebCore::AXObjectCache::startCharacterOffsetOfWord):
+
 2016-04-28  Jer Noble  <jer.no...@apple.com>
 
         WebPlaybackControlsManager should not be owned by the WebPlaybackSessionInterfaceMac.

Modified: trunk/Source/WebCore/accessibility/AXObjectCache.cpp (200257 => 200258)


--- trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2016-04-29 20:01:42 UTC (rev 200257)
+++ trunk/Source/WebCore/accessibility/AXObjectCache.cpp	2016-04-29 20:05:07 UTC (rev 200258)
@@ -1479,7 +1479,7 @@
     bool toNodeEnd = option & TraverseOptionToNodeEnd;
     
     int offsetInCharacter = 0;
-    int offsetSoFar = 0;
+    int cumulativeOffset = 0;
     int remaining = 0;
     int lastLength = 0;
     Node* currentNode = nullptr;
@@ -1494,8 +1494,8 @@
         lastStartOffset = range->startOffset();
         if (offset > 0 || toNodeEnd) {
             if (AccessibilityObject::replacedNodeNeedsCharacter(currentNode) || (currentNode->renderer() && currentNode->renderer()->isBR()))
-                offsetSoFar++;
-            lastLength = offsetSoFar;
+                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.
@@ -1510,19 +1510,27 @@
     // 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;
+    // When text node has collapsed whitespaces, we need to treat it differently since text iterator
+    // will omit the collapsed spaces and make the offset inaccurate.
+    Node* collapsedWhitespaceNode = nullptr;
     for (; !iterator.atEnd(); iterator.advance()) {
         int currentLength = iterator.text().length();
         bool hasReplacedNodeOrBR = false;
         
         Node& node = iterator.range()->startContainer();
         currentNode = &node;
+        
+        // The offset of node with collapsed whitespaces has been calcualted in the first iteration.
+        if (currentNode == collapsedWhitespaceNode)
+            continue;
+        
         // 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++;
+                cumulativeOffset++;
                 currentLength++;
                 currentNode = childNode;
                 hasReplacedNodeOrBR = true;
@@ -1545,8 +1553,30 @@
                         continue;
                     }
                 }
+            } else if (currentNode->isTextNode() && currentNode->renderer()) {
+                // When there's collapsed whitespace, the text iterator will only count those spaces as one single space.
+                // Here we use the RenderText to get the actual length.
+                RenderText* renderedText = downcast<RenderText>(currentNode->renderer());
+                int currentStartOffset = iterator.range()->startOffset();
+                if (renderedText->style().isCollapsibleWhiteSpace(iterator.text()[currentLength - 1])  && currentLength + currentStartOffset != renderedText->caretMaxOffset()) {
+                    int appendLength = (&range->endContainer() == currentNode ? range->endOffset() : (int)renderedText->text()->length()) - currentStartOffset;
+                    lastStartOffset = currentStartOffset;
+                    cumulativeOffset += appendLength;
+                    lastLength = appendLength;
+                    
+                    // Break early if we have advanced enough characters.
+                    if (!toNodeEnd && cumulativeOffset >= offset) {
+                        offsetInCharacter = offset - (cumulativeOffset - lastLength);
+                        finished = true;
+                        break;
+                    }
+                    
+                    collapsedWhitespaceNode = currentNode;
+                    continue;
+                }
+                
             }
-            offsetSoFar += currentLength;
+            cumulativeOffset += currentLength;
         }
 
         if (currentNode == previousNode)
@@ -1557,8 +1587,8 @@
         }
         
         // Break early if we have advanced enough characters.
-        if (!toNodeEnd && offsetSoFar >= offset) {
-            offsetInCharacter = offset - (offsetSoFar - lastLength);
+        if (!toNodeEnd && cumulativeOffset >= offset) {
+            offsetInCharacter = offset - (cumulativeOffset - lastLength);
             finished = true;
             break;
         }
@@ -1568,7 +1598,7 @@
     if (!finished) {
         offsetInCharacter = lastLength;
         if (!toNodeEnd)
-            remaining = offset - offsetSoFar;
+            remaining = offset - cumulativeOffset;
     }
     
     return CharacterOffset(currentNode, lastStartOffset, offsetInCharacter, remaining);
@@ -1850,22 +1880,36 @@
 {
     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);
+    } 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);
-    } while (textMarkerData.ignored);
+        
+        // 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
@@ -1924,27 +1968,32 @@
         return CharacterOffset();
     
     // Use nextVisiblePosition to calculate how many characters we need to traverse to the current position.
-    VisiblePositionRange vpRange = obj->visiblePositionRange();
-    VisiblePosition vp = vpRange.start;
+    VisiblePositionRange visiblePositionRange = obj->visiblePositionRange();
+    VisiblePosition visiblePosition = visiblePositionRange.start;
     int characterOffset = 0;
-    Position vpDeepPos = vp.deepEquivalent();
+    Position currentPosition = visiblePosition.deepEquivalent();
     
     VisiblePosition previousVisiblePos;
-    while (!vpDeepPos.isNull() && !deepPos.equals(vpDeepPos)) {
-        previousVisiblePos = vp;
-        vp = obj->nextVisiblePosition(vp);
-        vpDeepPos = vp.deepEquivalent();
+    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 (vpDeepPos.equals(previousVisiblePos.deepEquivalent()))
+        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 (vpDeepPos.deprecatedNode() != previousVisiblePos.deepEquivalent().deprecatedNode()) {
-            if (vp.characterBefore() == '\n')
+        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;
         }
     }
     
@@ -2006,7 +2055,9 @@
     if (characterOffset.isNull())
         return CharacterOffset();
     
-    CharacterOffset next = characterOffsetForNodeAndOffset(*characterOffset.node, characterOffset.offset + 1);
+    // 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.
     bool isReplacedOrBR = isReplacedNodeOrBR(characterOffset.node) || isReplacedNodeOrBR(next.node);
@@ -2025,7 +2076,9 @@
     if (!ignorePreviousNodeEnd && !characterOffset.offset)
         return characterOffsetForNodeAndOffset(*characterOffset.node, 0);
     
-    return characterOffsetForNodeAndOffset(*characterOffset.node, characterOffset.offset - 1, TraverseOptionIncludeStart);
+    // 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)
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to