WebFrameIOS.mm   [plain text]


/*
 * Copyright (C) 2009 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 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.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#if PLATFORM(IOS)

#import "WebFrameIOS.h"

#import <WebCore/Editor.h>
#import <WebCore/Element.h>
#import <WebCore/DocumentMarkerController.h>
#import <WebCore/EventHandler.h>
#import <WebCore/FloatRect.h>
#import <WebCore/Frame.h>
#import <WebCore/FrameSelection.h>
#import <WebCore/FrameSnapshotting.h>
#import <WebCore/FrameView.h>
#import <WebCore/HitTestResult.h>
#import <WebCore/htmlediting.h>
#import <WebCore/Position.h>
#import <WebCore/Range.h>
#import <WebCore/RenderObject.h>
#import <WebCore/RenderText.h>
#import <WebCore/RenderedDocumentMarker.h>
#import <WebCore/SelectionRect.h>
#import <WebCore/TextBoundaries.h>
#import <WebCore/TextFlags.h>
#import <WebCore/VisiblePosition.h>
#import <WebCore/VisibleUnits.h>
#import <WebKitLegacy/DOM.h>
#import <WebKitLegacy/DOMRange.h>
#import <WebKitLegacy/DOMUIKitExtensions.h>
#import <WebKitLegacy/WebSelectionRect.h>
#import <WebKitLegacy/WebVisiblePosition.h>
#import <unicode/uchar.h>

#import "DOMNodeInternal.h"
#import "DOMRangeInternal.h"
#import "WebFrameInternal.h"
#import "WebUIKitDelegate.h"
#import "WebViewPrivate.h"
#import "WebVisiblePosition.h"
#import "WebVisiblePositionInternal.h"

using namespace WebCore;

//-------------------

@interface WebFrame (WebSecretsIKnowAbout)
- (VisiblePosition)_visiblePositionForPoint:(CGPoint)point;
@end

@implementation WebFrame (WebFrameIOS)

//-------------------

- (WebCore::Frame *)coreFrame
{
    return _private->coreFrame;
}

- (VisiblePosition)visiblePositionForPoint:(CGPoint)point
{
    return [self _visiblePositionForPoint:point];
}

- (VisiblePosition)closestWordBoundary:(VisiblePosition)position
{
    VisiblePosition start = startOfWord(position);
    VisiblePosition end = endOfWord(position);
    int startDistance = position.deepEquivalent().deprecatedEditingOffset() - start.deepEquivalent().deprecatedEditingOffset();
    int endDistance = end.deepEquivalent().deprecatedEditingOffset() - position.deepEquivalent().deprecatedEditingOffset() ;
    return (startDistance < endDistance) ? start : end;    
}

//-------------------

- (void)clearSelection
{
    Frame *frame = [self coreFrame];
    if (frame)
        frame->selection().clearCurrentSelection();
    
}

- (WebTextSelectionState)selectionState
{
    WebTextSelectionState state = WebTextSelectionStateNone;

    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();

    if (frameSelection.isCaret())
        state = WebTextSelectionStateCaret;
    else if (frameSelection.isRange())
        state = WebTextSelectionStateRange;

    return state;
}

- (BOOL)hasSelection
{
    WebTextSelectionState state = [self selectionState];
    return state == WebTextSelectionStateCaret || state == WebTextSelectionStateRange;
}

- (CGRect)caretRectForPosition:(WebVisiblePosition *)position
{
    return position ? [position _visiblePosition].absoluteCaretBounds() : CGRectZero;
}

- (CGRect)closestCaretRectInMarkedTextRangeForPoint:(CGPoint)point
{
    Frame *frame = [self coreFrame];
    Range *markedTextRange = frame->editor().compositionRange().get();
    VisibleSelection markedTextRangeSelection = markedTextRange ? VisibleSelection(*markedTextRange) : VisibleSelection();

    IntRect result;

    if (markedTextRangeSelection.isRange()) {
        VisiblePosition start(markedTextRangeSelection.start());
        VisiblePosition end(markedTextRangeSelection.end());

        // Adjust pos and give it an appropriate affinity.
        VisiblePosition pos;
        Vector<IntRect> intRects;
        markedTextRange->textRects(intRects, NO);
        unsigned size = intRects.size();
        CGRect firstRect = intRects[0];
        CGRect lastRect  = intRects[size-1];
        if (point.y < firstRect.origin.y) {
            point.y = firstRect.origin.y;
            pos = [self visiblePositionForPoint:point];
            pos.setAffinity(UPSTREAM);
        }
        else if (point.y >= lastRect.origin.y) {
            point.y = lastRect.origin.y;
            pos = [self visiblePositionForPoint:point];
            pos.setAffinity(DOWNSTREAM);
        }
        else {
            pos = [self visiblePositionForPoint:point];
        }
        
        if (pos == start || pos < start) {
            start.setAffinity(UPSTREAM);
            //result = start.next().absoluteCaretBounds();
            result = start.absoluteCaretBounds(); //<rdar://problem/6716185>: Allow placing the caaret to the left of the first character.
        } else if (pos > end) {
            end.setAffinity(DOWNSTREAM);
            result = end.absoluteCaretBounds();
        } else {
            result = pos.absoluteCaretBounds();
        }
    } else {
        VisiblePosition pos = [self visiblePositionForPoint:point];
        result = pos.absoluteCaretBounds();
    }
    
    return (CGRect) result;    
}


- (void)collapseSelection
{
    if ([self selectionState] == WebTextSelectionStateRange) {
        Frame *frame = [self coreFrame];
        FrameSelection& frameSelection = frame->selection();
        VisiblePosition end(frameSelection.selection().end());
        frameSelection.moveTo(end);
    }
}

- (void)extendSelection:(BOOL)start
{
    if ([self selectionState] == WebTextSelectionStateRange) {
        Frame *frame = [self coreFrame];
        const VisibleSelection& originalSelection = frame->selection().selection();
        if (start) {
            VisiblePosition start = startOfWord(originalSelection.start());
            frame->selection().moveTo(start, originalSelection.end());
        } else {
            VisiblePosition end = endOfWord(originalSelection.end());
            frame->selection().moveTo(originalSelection.start(), end);
        }
    }    
}

- (NSArray *)selectionRectsForCoreRange:(Range *)range
{
    if (!range)
        return nil;
    
    Vector<SelectionRect> rects;
    range->collectSelectionRects(rects);
    unsigned size = rects.size();
    
    NSMutableArray *result = [NSMutableArray arrayWithCapacity:size];
    for (unsigned i = 0; i < size; i++) {
        SelectionRect &coreRect = rects[i];
        WebSelectionRect *webRect = [WebSelectionRect selectionRect];
        webRect.rect = static_cast<CGRect>(coreRect.rect());
        webRect.writingDirection = coreRect.direction() == LTR ? WKWritingDirectionLeftToRight : WKWritingDirectionRightToLeft;
        webRect.isLineBreak = coreRect.isLineBreak();
        webRect.isFirstOnLine = coreRect.isFirstOnLine();
        webRect.isLastOnLine = coreRect.isLastOnLine();
        webRect.containsStart = coreRect.containsStart();
        webRect.containsEnd = coreRect.containsEnd();
        webRect.isInFixedPosition = coreRect.isInFixedPosition();
        webRect.isHorizontal = coreRect.isHorizontal();
        [result addObject:webRect];
    }
    
    return result;        
}

- (NSArray *)selectionRectsForRange:(DOMRange *)domRange
{
    return [self selectionRectsForCoreRange:core(domRange)];
}

- (NSArray *)selectionRects
{
    if (![self hasSelection])
        return nil;

    Frame *frame = [self coreFrame];
    return [self selectionRectsForCoreRange:frame->selection().toNormalizedRange().get()];
}

- (DOMRange *)wordAtPoint:(CGPoint)point
{
    VisiblePosition pos = [self visiblePositionForPoint:point];
    VisiblePosition start = startOfWord(pos);
    VisiblePosition end = endOfWord(pos);
    DOMRange *wordRange = kit(makeRange(start, end).get());
    return wordRange;   
}

- (WebVisiblePosition *)webVisiblePositionForPoint:(CGPoint)point
{
    return [WebVisiblePosition _wrapVisiblePosition:[self visiblePositionForPoint:point]];
}

- (void)setRangedSelectionBaseToCurrentSelection
{
    Frame *frame = [self coreFrame];
    frame->setRangedSelectionBaseToCurrentSelection();
}

- (void)setRangedSelectionBaseToCurrentSelectionStart
{
    Frame *frame = [self coreFrame];
    frame->setRangedSelectionBaseToCurrentSelectionStart();
}

- (void)setRangedSelectionBaseToCurrentSelectionEnd
{
    Frame *frame = [self coreFrame];
    frame->setRangedSelectionBaseToCurrentSelectionEnd();
}

- (void)clearRangedSelectionInitialExtent
{
    Frame *frame = [self coreFrame];
    frame->clearRangedSelectionInitialExtent();
}

- (void)setRangedSelectionInitialExtentToCurrentSelectionStart
{
    Frame *frame = [self coreFrame];
    frame->setRangedSelectionInitialExtentToCurrentSelectionStart();
}

- (void)setRangedSelectionInitialExtentToCurrentSelectionEnd
{
    Frame *frame = [self coreFrame];
    frame->setRangedSelectionInitialExtentToCurrentSelectionEnd();
}

- (void)setRangedSelectionWithExtentPoint:(CGPoint)point
{    
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition pos = [self visiblePositionForPoint:point];
    VisibleSelection base = frame->rangedSelectionBase();
    
    if (pos.isNull() || !base.isRange())
        return;
    
    VisiblePosition start(base.start());
    VisiblePosition end(base.end());    
    
    if (pos < start) {        
        frameSelection.moveTo(pos, end);
    } 
    else if (pos > end) {
        frameSelection.moveTo(start, pos);
    } 
    else {
        frameSelection.moveTo(start, end);
    }
}

- (BOOL)setRangedSelectionExtentPoint:(CGPoint)extentPoint baseIsStart:(BOOL)baseIsStart allowFlipping:(BOOL)allowFlipping
{
    Frame *frame = [self coreFrame];
    VisibleSelection rangedSelectionBase(frame->rangedSelectionBase());
    VisiblePosition baseStart(rangedSelectionBase.start(), rangedSelectionBase.affinity());
    VisiblePosition baseEnd;
    if (rangedSelectionBase.isNone()) {
        return baseIsStart;
    }
    else if (rangedSelectionBase.isCaret()) {
        baseEnd = baseStart;
    }
    else {
        baseEnd = VisiblePosition(rangedSelectionBase.end(), rangedSelectionBase.affinity());
    }

    VisiblePosition extent([self visiblePositionForPoint:extentPoint]);
    
    if (rangedSelectionBase.isRange() && baseStart < extent && extent < baseEnd) {
        frame->selection().moveTo(baseStart, baseEnd);
        return NO;
    }    
    
    CGRect caretRect = baseIsStart ? baseStart.absoluteCaretBounds() : baseEnd.absoluteCaretBounds();
    CGPoint basePoint = CGPointMake(caretRect.origin.x, caretRect.origin.y);

    static const CGFloat FlipMargin = 30;
    bool didFlipStartEnd = false;
    bool canFlipStartEnd = allowFlipping && 
        (fabs(basePoint.x - extentPoint.x) > FlipMargin || fabs(basePoint.y - extentPoint.y) > FlipMargin);

    VisiblePosition base;
    if (baseIsStart) {                
        base = baseStart;
        BOOL wouldFlip = (extent < base);
        if (wouldFlip) {
            if (!canFlipStartEnd) {
                // We're going to prevent flipping.  First try for a position on the same line.
                // If that fails, just choose something after the start.
                CGRect baseCaret = base.absoluteCaretBounds();
                bool baseIsHorizontal = baseCaret.size.height > baseCaret.size.width;
                CGPoint pointInLine = baseIsHorizontal ? CGPointMake(extentPoint.x, CGRectGetMidY(baseCaret)) :
                                                         CGPointMake(CGRectGetMidX(baseCaret), extentPoint.y);
                VisiblePosition positionInLine = [self visiblePositionForPoint:pointInLine];
                if (positionInLine.isNotNull() && positionInLine > base) {
                    extent = positionInLine;
                } else {
                    extent = base.next();
                }
            } else {
                didFlipStartEnd = YES;
            }
        }
        
        if (base == extent)
            extent = base.next();
    }
    else {
        base = baseEnd;
        BOOL wouldFlip = (extent > base);
        if (wouldFlip) {
            if (!canFlipStartEnd) {
                // We're going to prevent flipping.  First try for a position on the same line.
                // If that fails, just choose something before the end.
                CGRect baseCaret = base.absoluteCaretBounds();
                bool baseIsHorizontal = baseCaret.size.height > baseCaret.size.width;
                CGPoint pointInLine = baseIsHorizontal ? CGPointMake(extentPoint.x, CGRectGetMidY(baseCaret)) :
                                                         CGPointMake(CGRectGetMidX(baseCaret), extentPoint.y);
                VisiblePosition positionInLine = [self visiblePositionForPoint:pointInLine];
                if (positionInLine.isNotNull() && positionInLine < base) {
                    extent = positionInLine;
                } else {
                    extent = base.previous();
                }
            } else {
                didFlipStartEnd = YES;
            }
        }
        
        if (base == extent)
            extent = base.previous();
    }
    
    frame->selection().moveTo(base, extent);

    return didFlipStartEnd ? !baseIsStart : baseIsStart;
}

- (BOOL)setSelectionWithBasePoint:(CGPoint)basePoint extentPoint:(CGPoint)extentPoint baseIsStart:(BOOL)baseIsStart allowFlipping:(BOOL)allowFlipping
{
    // This function updates the selection using two points and an existing notion of
    // which is the base and which is the extent. However, if the allowFlipping argument
    // is YES, it will allow the base and extent positions to flip if the extent moves a
    // certain amount to the "other side" of the base. When a flip of start/end occurs
    // relative to base/extent,  this is reported back to the calling code, which is then
    // expected to take the flip into account in subsequent calls to this function (for at
    // least as long as a single, logical selection session continues).

    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition base([self visiblePositionForPoint:basePoint]);
    VisiblePosition extent([self visiblePositionForPoint:extentPoint]);

    static const CGFloat FlipMargin = 30;
    bool didFlipStartEnd = false;
    bool canFlipStartEnd = allowFlipping &&
                        ((baseIsStart &&  (extentPoint.x <= basePoint.x - FlipMargin || extentPoint.y <= basePoint.y - FlipMargin)) ||
                         (!baseIsStart && (extentPoint.x >= basePoint.x + FlipMargin || extentPoint.y >= basePoint.y + FlipMargin)));
    
                        
    if (extent == base) {
        extent = baseIsStart ? base.next() : base.previous();
    }
    else if (baseIsStart && extent < base) {
        if (canFlipStartEnd)
            didFlipStartEnd = true;
        else
            extent = base.next();
    }
    else if (!baseIsStart && extent > base) {
        if (canFlipStartEnd)
            didFlipStartEnd = true;
        else
            extent = base.previous();
    }

    frameSelection.moveTo(base, extent);

    return didFlipStartEnd ? !baseIsStart : baseIsStart;
}

- (BOOL)setSelectionWithBasePoint:(CGPoint)basePoint extentPoint:(CGPoint)extentPoint baseIsStart:(BOOL)baseIsStart
{
    return [self setSelectionWithBasePoint:basePoint extentPoint:extentPoint baseIsStart:baseIsStart allowFlipping:YES];
}

- (void)setSelectionWithFirstPoint:(CGPoint)firstPoint secondPoint:(CGPoint)secondPoint
{
    // We still support two-finger taps to make a selection, and these taps
    // don't care about base/extent.
    VisiblePosition first([self visiblePositionForPoint:firstPoint]);
    VisiblePosition second([self visiblePositionForPoint:secondPoint]);
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    frameSelection.moveTo(first, second);
}

- (void)ensureRangedSelectionContainsInitialStartPoint:(CGPoint)initialStartPoint initialEndPoint:(CGPoint)initialEndPoint
{
    // This method ensures that selection ends doesn't contract such that it no
    // longer contains these points. This is the desirable behavior when the
    // user does the tap-and-a-half + drag operation.
    Frame *frame = [self coreFrame];
    const VisibleSelection& originalSelection = frame->selection().selection();
    Position ensureStart([self visiblePositionForPoint:initialStartPoint].deepEquivalent());
    Position ensureEnd([self visiblePositionForPoint:initialEndPoint].deepEquivalent());
    if (originalSelection.start() > ensureStart)
        frame->selection().moveTo(ensureStart, originalSelection.end());
    else if (originalSelection.end() < ensureEnd)
        frame->selection().moveTo(originalSelection.start(), ensureEnd);
}

- (void)aggressivelyExpandSelectionToWordContainingCaretSelection
{
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition end = frameSelection.selection().visibleEnd();
    if (end == endOfDocument(end) && end != startOfDocument(end) && end == startOfLine(end))
        frameSelection.moveTo(end.previous(), end);

    [self expandSelectionToWordContainingCaretSelection];

    // This is a temporary hack until we get the improvements
    // I'm working on for RTL selection.
    if (frameSelection.granularity() == WordGranularity)
        frameSelection.moveTo(frameSelection.selection().start(), frameSelection.selection().end());
    
    if (frameSelection.selection().isCaret()) {
        VisiblePosition pos(frameSelection.selection().end());
        if (isStartOfLine(pos) && isEndOfLine(pos)) {
            VisiblePosition next(pos.next());
            if (next.isNotNull())
                frameSelection.moveTo(end, next);
        }
        else {
            while (pos.isNotNull()) {
                VisiblePosition wordStart(startOfWord(pos));
                if (wordStart != pos) {
                    frameSelection.moveTo(wordStart, frameSelection.selection().end());
                    break;
                }
                pos = pos.previous();
            }
        }
    }
}

- (void)expandSelectionToSentence
{
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition pos = frameSelection.selection().start();
    VisiblePosition start = startOfSentence(pos);
    VisiblePosition end = endOfSentence(pos);
    frameSelection.moveTo(start, end);
}

- (WKWritingDirection)selectionBaseWritingDirection
{
    Frame *frame = [self coreFrame];
    switch (frame->editor().baseWritingDirectionForSelectionStart()) {
    case LeftToRightWritingDirection:
        return WKWritingDirectionLeftToRight;

    case RightToLeftWritingDirection:
        return WKWritingDirectionRightToLeft;

    default:
        ASSERT_NOT_REACHED();
        break;
    }

    return WKWritingDirectionLeftToRight;
}

- (void)toggleBaseWritingDirection
{
    WKWritingDirection updated = WKWritingDirectionRightToLeft;
    switch ([self selectionBaseWritingDirection]) {
        case WKWritingDirectionLeftToRight:
            updated = WKWritingDirectionRightToLeft;
            break;
        case WKWritingDirectionRightToLeft:
            updated = WKWritingDirectionLeftToRight;
            break;
        default:
            // WebCore should never return anything else, including WKWritingDirectionNatural
            ASSERT_NOT_REACHED();
            break;
    }
    [self setBaseWritingDirection:updated];
}

- (void)setBaseWritingDirection:(WKWritingDirection)direction
{
    WKWritingDirection originalDirection = [self selectionBaseWritingDirection];

    Frame *frame = [self coreFrame];
    if (!frame->selection().selection().isContentEditable())
        return;
    
    WritingDirection wcDirection = LeftToRightWritingDirection;
    switch (direction) {
        case WKWritingDirectionNatural:
            wcDirection = NaturalWritingDirection;
            break;
        case WKWritingDirectionLeftToRight:
            wcDirection = LeftToRightWritingDirection;
            break;
        case WKWritingDirectionRightToLeft:
            wcDirection = RightToLeftWritingDirection;
            break;
        default:
            ASSERT_NOT_REACHED();
            break;
    }
    frame->editor().setBaseWritingDirection(wcDirection);
    
    if (originalDirection != [self selectionBaseWritingDirection])
        frame->editor().setTextAlignmentForChangedBaseWritingDirection(wcDirection);
}

- (void)moveSelectionToStart
{
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition start = startOfDocument(frameSelection.selection().start());
    frameSelection.moveTo(start);
}

- (void)moveSelectionToEnd
{
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition end =  endOfDocument(frameSelection.selection().end());
    frameSelection.moveTo(end);
}

- (void)moveSelectionToPoint:(CGPoint)point
{
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    VisiblePosition pos = [self _visiblePositionForPoint:point];
    frameSelection.moveTo(pos);
}

- (void)setSelectionGranularity:(WebTextGranularity)granularity
{
    TextGranularity wcGranularity = CharacterGranularity;
    switch (granularity) {
        case WebTextGranularityCharacter:
            wcGranularity = CharacterGranularity;
            break;
        case WebTextGranularityWord:
            wcGranularity = WordGranularity;
            break;
        case WebTextGranularitySentence:
            wcGranularity = SentenceGranularity;
            break;
        case WebTextGranularityParagraph:
            wcGranularity = ParagraphGranularity;
            break;
        case WebTextGranularityAll:
            // FIXME: Add DocumentGranularity.
            wcGranularity = ParagraphGranularity;
            break;
        default:
            ASSERT_NOT_REACHED();
            break;
    }
    FrameSelection& frameSelection = _private->coreFrame->selection();
    frameSelection.setSelection(frameSelection.selection(), wcGranularity);
}

static inline bool isAlphaNumericCharacter(UChar32 c)
{
    static CFCharacterSetRef set = CFCharacterSetGetPredefined(kCFCharacterSetAlphaNumeric);
    return CFCharacterSetIsCharacterMember(set, c);
}

static VisiblePosition SimpleSmartExtendStart(const VisiblePosition& start, const VisiblePosition& end, const VisibleSelection& initialExtent)
{
    VisiblePosition pos(start);
    VisiblePosition initialStart;
    if (initialExtent.isCaretOrRange())
        initialStart = VisiblePosition(initialExtent.start(), initialExtent.affinity());

    if (initialStart == start) {
        // No smarts needed. Leave selection where it is.
        return pos;
    }

    UChar32 charBefore = pos.characterBefore();
    UChar32 charAfter = pos.characterAfter();
    if (isAlphaNumericCharacter(charAfter) && !isAlphaNumericCharacter(charBefore)) {
        // This is a word start. Leave selection where it is.
        return pos;
    }
    
    if (isAlphaNumericCharacter(charBefore) && !isAlphaNumericCharacter(charAfter)) {
        // This is a word end. Nudge the selection to the next character before proceeding.
        pos = pos.next();
    }

    // Extend to the start of the word.
    // If this isn't where the start was initially, use this position.
    VisiblePosition wordStart(startOfWord(pos));
    if (wordStart != initialStart) {
        return wordStart;
    }
    // Conversely, if the initial start equals the current word start, then
    // run the rest of this function to see if the selection should extend
    // back to the next word.

    // Passed-in end must be at least three characters from initialStart or
    // must cross word boundary.
    // If this is where the start was initially, skip to the end of the word,
    // then iterate forward in the document until we hit an alphanumeric.
    VisiblePosition wordEnd(endOfWord(pos));
    pos = wordEnd;
    while (pos.isNotNull() && !isStartOfLine(pos) && !isEndOfLine(pos) && pos != end) {
        UChar32 c = pos.characterAfter();
        if (isAlphaNumericCharacter(c))
            break;
        pos = pos.next();
    }
    
    // Don't let the smart extension make the start equal the end.
    // Expand out to word boundary.
    if (pos == end)
        pos = wordStart;
    return pos;
}

static VisiblePosition SimpleSmartExtendEnd(const VisiblePosition& start, const VisiblePosition& end, const VisibleSelection& initialExtent)
{
    VisiblePosition pos(end);

    VisiblePosition initialEnd;
    if (initialExtent.isCaretOrRange())
        initialEnd = VisiblePosition(initialExtent.end(), initialExtent.affinity());
    
    if (initialEnd == end) {
        // No smarts needed. Leave selection where it is.
        return pos;
    }
    
    UChar32 charBefore = pos.characterBefore();
    UChar32 charAfter = pos.characterAfter();
    if (isAlphaNumericCharacter(charBefore) && !isAlphaNumericCharacter(charAfter)) {
        // This is a word end. Leave selection where it is.
        return pos;
    }

    if (!isAlphaNumericCharacter(charBefore) && isAlphaNumericCharacter(charAfter)) {
        // This is a word start. Nudge the selection to the previous character before proceeding.
        pos = pos.previous();
    }

    // Extend to the end of the word.
    // If this isn't where the end was initially, use this position.
    VisiblePosition wordEnd(endOfWord(pos));
    if (wordEnd != initialEnd && isAlphaNumericCharacter(wordEnd.characterBefore())) {
        return wordEnd;
    }
    // Conversely, if the initial end equals the current word end, then
    // run the rest of this function to see if the selection should extend
    // back to the previous word.

    // If this is where the end was initially, skip to the start of the word,
    // then iterate backward in the document until we hit an alphanumeric.
    VisiblePosition wordStart(startOfWord(pos));
    pos = wordStart;
    while (pos.isNotNull() && !isStartOfLine(pos) && !isEndOfLine(pos) && pos != start) {
        UChar32 c = pos.characterBefore();
        if (isAlphaNumericCharacter(c))
            break;
        pos = pos.previous();
    }

    // Don't let the smart extension make the end equal the start.
    // Expand out to word boundary.
    if (pos == start)
        pos = wordEnd;
    
    return pos;
}

- (void)smartExtendRangedSelection:(WebTextSmartExtendDirection)direction
{
    if ([self selectionState] != WebTextSelectionStateRange)
        return;
    
    Frame *frame = [self coreFrame];
    FrameSelection& frameSelection = frame->selection();
    EAffinity affinity = frameSelection.selection().affinity();
    VisiblePosition start(frameSelection.selection().start(), affinity);
    VisiblePosition end(frameSelection.selection().end(), affinity);
    VisiblePosition base(frame->rangedSelectionBase().base());  // should equal start or end

    // Base must equal start or end
    if (base != start && base != end)
        return;

    VisiblePosition extent(frameSelection.selection().extent(), affinity);
    
    // We don't yet support smart extension for languages which
    // require context for word boundary.
    if (requiresContextForWordBoundary(extent.characterAfter()) || 
        requiresContextForWordBoundary(extent.characterBefore()))
        return;

    // If the smart-extend direction is neither left nor right, do
    // not pass rangedSelectionInitialExtent to the smart extend functions.
    // This will have the effect of always extending out to include the
    // word which contains the extent.
    VisibleSelection initialExtent;
    if (direction != WebTextSmartExtendDirectionNone)
        initialExtent = frame->rangedSelectionInitialExtent();

    VisiblePosition smartExtent;
    if (base == end) {  // extend start
        smartExtent = SimpleSmartExtendStart(start, end, initialExtent);
    }
    else {  // base == start / extend end
        smartExtent = SimpleSmartExtendEnd(start, end, initialExtent);
    }

    if (smartExtent.isNotNull() && smartExtent != extent)
        frameSelection.moveTo(base, smartExtent);

}

- (WebVisiblePosition *)startPosition
{
    Frame *frame = [self coreFrame];
    Element *rootElement = frame->document()->documentElement();
    return [WebVisiblePosition _wrapVisiblePosition:startOfDocument(static_cast<Node*>(rootElement))];
}

- (WebVisiblePosition *)endPosition
{
    Frame *frame = [self coreFrame];
    Element *rootElement = frame->document()->documentElement();
    return [WebVisiblePosition _wrapVisiblePosition:endOfDocument(static_cast<Node*>(rootElement))];
}

- (BOOL)renderedCharactersExceed:(NSUInteger)threshold
{
    Frame *frame = [self coreFrame];
    return frame->view()->renderedCharactersExceed(threshold);
}

- (CGImageRef)imageForNode:(DOMNode *)node allowDownsampling:(BOOL)allowDownsampling drawContentBehindTransparentNodes:(BOOL)drawContentBehindTransparentNodes
{
    // FIXME: implement: <rdar://problem/15808709>
    return nullptr;
}

// Iterates backward through the document and returns the point at which untouched dictation results end.
- (WebVisiblePosition *)previousUnperturbedDictationResultBoundaryFromPosition:(WebVisiblePosition *)position
{
    VisiblePosition currentVisiblePosition = [position _visiblePosition];
    if (currentVisiblePosition.isNull())
        return position;
    
    Document& document = currentVisiblePosition.deepEquivalent().anchorNode()->document();

    id uikitDelegate = [[self webView] _UIKitDelegate];
    if (![uikitDelegate respondsToSelector:@selector(isUnperturbedDictationResultMarker:)])
        return position;
    
    while (currentVisiblePosition.isNotNull()) {
        WebVisiblePosition *currentWebVisiblePosition = [WebVisiblePosition _wrapVisiblePosition:currentVisiblePosition];
        
        Node *currentNode = currentVisiblePosition.deepEquivalent().anchorNode();
        int lastOffset = lastOffsetForEditing(currentNode);
        ASSERT(lastOffset >= 0);
        if (lastOffset < 0)
            return currentWebVisiblePosition;
        
        VisiblePosition previousVisiblePosition = currentVisiblePosition.previous();
        if (previousVisiblePosition.isNull())
            return currentWebVisiblePosition;
        
        RefPtr<Range> graphemeRange = Range::create(document, previousVisiblePosition.deepEquivalent(), currentVisiblePosition.deepEquivalent());
        
        auto markers = document.markers().markersInRange(graphemeRange.get(), DocumentMarker::DictationResult);
        if (markers.isEmpty())
            return currentWebVisiblePosition;
        
        // FIXME: Result markers should not overlap, so there should only ever be one for a single grapheme.
        // <rdar://problem/9810617> Too much document context is omitted when sending dictation hints because of problems with WebCore DocumentMarkers
        // ASSERT(markers.size() == 1);
        if (markers.size() > 1)
            return currentWebVisiblePosition;
        RenderedDocumentMarker* resultMarker = markers.at(0);
        
        // FIXME: WebCore doesn't always update markers correctly during editing. Bail if resultMarker extends off the edge of 
        // this node, because that means it's invalid.
        if (resultMarker->endOffset() > (unsigned)lastOffset)
            return currentWebVisiblePosition;
        
        if (![uikitDelegate isUnperturbedDictationResultMarker:resultMarker->metadata()])
            return currentWebVisiblePosition;
        
        if (resultMarker->startOffset() > 0)
            return [WebVisiblePosition _wrapVisiblePosition:VisiblePosition(createLegacyEditingPosition(currentNode, resultMarker->startOffset()))];
        
        currentVisiblePosition = VisiblePosition(createLegacyEditingPosition(currentNode, 0));
    }
    
    return position;
}

// Iterates forward through the document and returns the point at which untouched dictation results end.
- (WebVisiblePosition *)nextUnperturbedDictationResultBoundaryFromPosition:(WebVisiblePosition *)position
{
    VisiblePosition currentVisiblePosition = [position _visiblePosition];
    if (currentVisiblePosition.isNull())
        return position;
    
    Document& document = currentVisiblePosition.deepEquivalent().anchorNode()->document();
    
    id uikitDelegate = [[self webView] _UIKitDelegate];
    if (![uikitDelegate respondsToSelector:@selector(isUnperturbedDictationResultMarker:)])
        return position;
    
    while (currentVisiblePosition.isNotNull()) {
        WebVisiblePosition *currentWebVisiblePosition = [WebVisiblePosition _wrapVisiblePosition:currentVisiblePosition];
        
        Node *currentNode = currentVisiblePosition.deepEquivalent().anchorNode();
        int lastOffset = lastOffsetForEditing(currentNode);
        ASSERT(lastOffset >= 0);
        if (lastOffset < 0)
            return currentWebVisiblePosition;
        
        VisiblePosition nextVisiblePosition = currentVisiblePosition.next();
        if (nextVisiblePosition.isNull())
            return currentWebVisiblePosition;
        
        RefPtr<Range> graphemeRange = Range::create(document, currentVisiblePosition.deepEquivalent(), nextVisiblePosition.deepEquivalent());
        
        auto markers = document.markers().markersInRange(graphemeRange.get(), DocumentMarker::DictationResult);
        if (markers.isEmpty())
            return currentWebVisiblePosition;
        
        // FIXME: Result markers should not overlap, so there should only ever be one for a single grapheme.
        // <rdar://problem/9810617> Too much document context is omitted when sending dictation hints because of problems with WebCore DocumentMarkers
        //ASSERT(markers.size() == 1);
        if (markers.size() > 1)
            return currentWebVisiblePosition;
        DocumentMarker* resultMarker = markers.at(0);
        
        // FIXME: WebCore doesn't always update markers correctly during editing. Bail if resultMarker extends off the edge of 
        // this node, because that means it's invalid.
        if (resultMarker->endOffset() > static_cast<unsigned>(lastOffset))
            return currentWebVisiblePosition;
        
        if (![uikitDelegate isUnperturbedDictationResultMarker:resultMarker->metadata()])
            return currentWebVisiblePosition;
        
        if (resultMarker->endOffset() <= static_cast<unsigned>(lastOffset))
            return [WebVisiblePosition _wrapVisiblePosition:VisiblePosition(createLegacyEditingPosition(currentNode, resultMarker->endOffset()))];
        
        currentVisiblePosition = VisiblePosition(createLegacyEditingPosition(currentNode, lastOffset));
    }
    
    return position;
}

- (CGRect)elementRectAtPoint:(CGPoint)point
{
    Frame *frame = [self coreFrame];
    IntPoint adjustedPoint = frame->view()->windowToContents(roundedIntPoint(point));
    HitTestResult result = frame->eventHandler().hitTestResultAtPoint(adjustedPoint, HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AllowChildFrameContent);
    Node* hitNode = result.innerNode();
    if (!hitNode || !hitNode->renderer())
        return IntRect();
    return result.innerNodeFrame()->view()->contentsToWindow(hitNode->renderer()->absoluteBoundingBoxRect(true));
}

@end

#endif  // PLATFORM(IOS)