| /* |
| * 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_FAMILY) |
| |
| #import "WebVisiblePosition.h" |
| #import "WebVisiblePositionInternal.h" |
| |
| #import <WebCore/DocumentMarkerController.h> |
| #import <WebCore/Editing.h> |
| #import <WebCore/FrameSelection.h> |
| #import <WebCore/HTMLTextFormControlElement.h> |
| #import <WebCore/Node.h> |
| #import <WebCore/Position.h> |
| #import <WebCore/Range.h> |
| #import <WebCore/RenderTextControl.h> |
| #import <WebCore/RenderedDocumentMarker.h> |
| #import <WebCore/TextBoundaries.h> |
| #import <WebCore/TextFlags.h> |
| #import <WebCore/TextGranularity.h> |
| #import <WebCore/TextIterator.h> |
| #import <WebCore/VisiblePosition.h> |
| #import <WebCore/VisibleUnits.h> |
| |
| |
| #import "DOMNodeInternal.h" |
| #import "DOMRangeInternal.h" |
| |
| using namespace WebCore; |
| |
| //------------------- |
| |
| @implementation WebVisiblePosition (Internal) |
| |
| // Since VisiblePosition isn't refcounted, we have to use new and delete on a copy. |
| |
| + (WebVisiblePosition *)_wrapVisiblePosition:(VisiblePosition)visiblePosition |
| { |
| WebVisiblePosition *vp = [[WebVisiblePosition alloc] init]; |
| VisiblePosition *copy = new VisiblePosition(visiblePosition); |
| vp->_internal = reinterpret_cast<WebObjectInternal *>(copy); |
| return [vp autorelease]; |
| } |
| |
| // Returns nil if visible position is null. |
| + (WebVisiblePosition *)_wrapVisiblePositionIfValid:(VisiblePosition)visiblePosition |
| { |
| return (visiblePosition.isNotNull() ? [WebVisiblePosition _wrapVisiblePosition:visiblePosition] : nil); |
| } |
| |
| - (VisiblePosition)_visiblePosition |
| { |
| VisiblePosition *vp = reinterpret_cast<VisiblePosition *>(_internal); |
| return *vp; |
| } |
| |
| @end |
| |
| @implementation WebVisiblePosition |
| |
| - (void)dealloc |
| { |
| VisiblePosition *vp = reinterpret_cast<VisiblePosition *>(_internal); |
| delete vp; |
| _internal = nil; |
| [super dealloc]; |
| } |
| |
| // FIXME: Overriding isEqual: without overriding hash will cause trouble if this ever goes into an NSSet or is the key in an NSDictionary, |
| // since two equal objects could have different hashes. |
| - (BOOL)isEqual:(id)other |
| { |
| if (![other isKindOfClass:[WebVisiblePosition class]]) |
| return NO; |
| return [self _visiblePosition] == [(WebVisiblePosition *)other _visiblePosition]; |
| } |
| |
| - (NSComparisonResult)compare:(WebVisiblePosition *)other |
| { |
| VisiblePosition myVP = [self _visiblePosition]; |
| VisiblePosition otherVP = [other _visiblePosition]; |
| |
| if (myVP == otherVP) |
| return NSOrderedSame; |
| else if (myVP < otherVP) |
| return NSOrderedAscending; |
| else |
| return NSOrderedDescending; |
| } |
| |
| - (int)distanceFromPosition:(WebVisiblePosition *)other |
| { |
| return distanceBetweenPositions([self _visiblePosition], [other _visiblePosition]); |
| } |
| |
| - (NSString *)description |
| { |
| |
| NSMutableString *description = [NSMutableString stringWithString:[super description]]; |
| VisiblePosition vp = [self _visiblePosition]; |
| int offset = vp.deepEquivalent().offsetInContainerNode(); |
| |
| [description appendFormat:@"(offset=%d, context=([%c|%c], [u+%04x|u+%04x])", offset, vp.characterBefore(), vp.characterAfter(), |
| vp.characterBefore(), vp.characterAfter()]; |
| |
| return description; |
| } |
| |
| |
| - (TextDirection)textDirection |
| { |
| // TODO: implement |
| return TextDirection::LTR; |
| } |
| |
| - (BOOL)directionIsDownstream:(WebTextAdjustmentDirection)direction |
| { |
| if (direction == WebTextAdjustmentBackward) |
| return NO; |
| |
| if (direction == WebTextAdjustmentForward) |
| return YES; |
| |
| |
| if ([self textDirection] == TextDirection::LTR) |
| return (direction == WebTextAdjustmentRight); |
| return (direction == WebTextAdjustmentLeft); |
| } |
| |
| - (WebVisiblePosition *)positionByMovingInDirection:(WebTextAdjustmentDirection)direction amount:(UInt32)amount withAffinityDownstream:(BOOL)affinityDownstream |
| { |
| VisiblePosition vp = [self _visiblePosition]; |
| |
| vp.setAffinity(affinityDownstream ? DOWNSTREAM : VP_UPSTREAM_IF_POSSIBLE); |
| |
| switch (direction) { |
| case WebTextAdjustmentForward: { |
| for (UInt32 i = 0; i < amount; i++) |
| vp = vp.next(); |
| break; |
| } |
| case WebTextAdjustmentBackward: { |
| for (UInt32 i = 0; i < amount; i++) |
| vp = vp.previous(); |
| break; |
| } |
| case WebTextAdjustmentRight: { |
| for (UInt32 i = 0; i < amount; i++) |
| vp = vp.right(); |
| break; |
| } |
| case WebTextAdjustmentLeft: { |
| for (UInt32 i = 0; i < amount; i++) |
| vp = vp.left(); |
| break; |
| } |
| case WebTextAdjustmentUp: { |
| int xOffset = vp.lineDirectionPointForBlockDirectionNavigation(); |
| for (UInt32 i = 0; i < amount; i++) |
| vp = previousLinePosition(vp, xOffset); |
| break; |
| } |
| case WebTextAdjustmentDown: { |
| int xOffset = vp.lineDirectionPointForBlockDirectionNavigation(); |
| for (UInt32 i = 0; i < amount; i++) |
| vp = nextLinePosition(vp, xOffset); |
| break; |
| } |
| default: { |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| } |
| return [WebVisiblePosition _wrapVisiblePositionIfValid:vp]; |
| } |
| |
| - (WebVisiblePosition *)positionByMovingInDirection:(WebTextAdjustmentDirection)direction amount:(UInt32)amount |
| |
| { |
| return [self positionByMovingInDirection:direction amount:amount withAffinityDownstream:YES]; |
| } |
| |
| static inline TextGranularity toTextGranularity(WebTextGranularity webGranularity) |
| { |
| TextGranularity granularity; |
| |
| switch (webGranularity) { |
| case WebTextGranularityCharacter: |
| granularity = CharacterGranularity; |
| break; |
| |
| case WebTextGranularityWord: |
| granularity = WordGranularity; |
| break; |
| |
| case WebTextGranularitySentence: |
| granularity = SentenceGranularity; |
| break; |
| |
| case WebTextGranularityLine: |
| granularity = LineGranularity; |
| break; |
| |
| case WebTextGranularityParagraph: |
| granularity = ParagraphGranularity; |
| break; |
| |
| case WebTextGranularityAll: |
| granularity = DocumentGranularity; |
| break; |
| |
| default: |
| ASSERT_NOT_REACHED(); |
| break; |
| } |
| |
| return granularity; |
| } |
| |
| static inline SelectionDirection toSelectionDirection(WebTextAdjustmentDirection direction) |
| { |
| SelectionDirection result; |
| |
| switch (direction) { |
| case WebTextAdjustmentForward: |
| result = DirectionForward; |
| break; |
| |
| case WebTextAdjustmentBackward: |
| result = DirectionBackward; |
| break; |
| |
| case WebTextAdjustmentRight: |
| result = DirectionRight; |
| break; |
| |
| case WebTextAdjustmentLeft: |
| result = DirectionLeft; |
| break; |
| |
| case WebTextAdjustmentUp: |
| result = DirectionLeft; |
| break; |
| |
| case WebTextAdjustmentDown: |
| result = DirectionRight; |
| break; |
| } |
| |
| return result; |
| } |
| |
| // Returnes YES only if a position is at a boundary of a text unit of the specified granularity in the particular direction. |
| - (BOOL)atBoundaryOfGranularity:(WebTextGranularity)granularity inDirection:(WebTextAdjustmentDirection)direction |
| { |
| return atBoundaryOfGranularity([self _visiblePosition], toTextGranularity(granularity), toSelectionDirection(direction)); |
| } |
| |
| // Returns the next boundary position of a text unit of the given granularity in the given direction, or nil if there is no such position. |
| - (WebVisiblePosition *)positionOfNextBoundaryOfGranularity:(WebTextGranularity)granularity inDirection:(WebTextAdjustmentDirection)direction |
| { |
| return [WebVisiblePosition _wrapVisiblePositionIfValid:positionOfNextBoundaryOfGranularity([self _visiblePosition], toTextGranularity(granularity), toSelectionDirection(direction))]; |
| } |
| |
| // Returns YES if position is within a text unit of the given granularity. If the position is at a boundary, returns YES only if |
| // if the boundary is part of the text unit in the given direction. |
| - (BOOL)withinTextUnitOfGranularity:(WebTextGranularity)granularity inDirectionIfAtBoundary:(WebTextAdjustmentDirection)direction |
| { |
| return withinTextUnitOfGranularity([self _visiblePosition], toTextGranularity(granularity), toSelectionDirection(direction)); |
| } |
| |
| // Returns range of the enclosing text unit of the given granularity, or nil if there is no such enclosing unit. Whether a boundary position |
| // is enclosed depends on the given direction, using the same rule as -[WebVisiblePosition withinTextUnitOfGranularity:inDirectionAtBoundary:]. |
| - (DOMRange *)enclosingTextUnitOfGranularity:(WebTextGranularity)granularity inDirectionIfAtBoundary:(WebTextAdjustmentDirection)direction |
| { |
| return kit(enclosingTextUnitOfGranularity([self _visiblePosition], toTextGranularity(granularity), toSelectionDirection(direction)).get()); |
| } |
| |
| - (WebVisiblePosition *)positionAtStartOrEndOfWord |
| { |
| // Ripped from WebCore::Frame::moveSelectionToStartOrEndOfCurrentWord |
| |
| // Note: this is the iOS notion, not the unicode notion. |
| // Here, a word starts with non-whitespace or at the start of a line and |
| // ends at the next whitespace, or at the end of a line. |
| |
| // Selection rule: If the selection is before the first character or |
| // just after the first character of a word that is longer than one |
| // character, move to the start of the word. Otherwise, move to the |
| // end of the word. |
| |
| VisiblePosition pos = [self _visiblePosition]; |
| VisiblePosition originalPos(pos); |
| |
| UChar32 ch = pos.characterAfter(); |
| bool isComplex = requiresContextForWordBoundary(ch); |
| if (isComplex) { |
| // for complex layout, find word around insertion point |
| VisiblePosition visibleWordStart = startOfWord(pos); |
| Position wordStart = visibleWordStart.deepEquivalent(); |
| |
| // place caret in front of word if pos is within first 2 characters of word (see Selection Rule above) |
| if (wordStart.deprecatedEditingOffset() + 1 >= pos.deepEquivalent().deprecatedEditingOffset()) { |
| pos = wordStart; |
| } else { |
| // calculate end of word to insert caret after word |
| VisiblePosition visibleWordEnd = endOfWord(pos); |
| Position wordEnd = visibleWordEnd.deepEquivalent(); |
| pos = wordEnd; |
| } |
| } else { |
| UChar32 c = pos.characterAfter(); |
| CFCharacterSetRef set = CFCharacterSetGetPredefined(kCFCharacterSetWhitespaceAndNewline); |
| if (c == 0 || CFCharacterSetIsLongCharacterMember(set, c)) { |
| // search backward for a non-space |
| while (1) { |
| if (pos.isNull() || isStartOfLine(pos)) |
| break; |
| c = pos.characterBefore(); |
| if (!CFCharacterSetIsLongCharacterMember(set, c)) |
| break; |
| pos = pos.previous(CannotCrossEditingBoundary); // stay in editable content |
| } |
| } |
| else { |
| // See how far the selection extends into the current word. |
| // Follow the rule stated above. |
| int index = 0; |
| while (1) { |
| if (pos.isNull() || isStartOfLine(pos)) |
| break; |
| c = pos.characterBefore(); |
| if (CFCharacterSetIsLongCharacterMember(set, c)) |
| break; |
| pos = pos.previous(CannotCrossEditingBoundary); // stay in editable content |
| index++; |
| if (index > 1) |
| break; |
| } |
| if (index > 1) { |
| // search forward for a space |
| pos = originalPos; |
| while (1) { |
| if (pos.isNull() || isEndOfLine(pos)) |
| break; |
| c = pos.characterAfter(); |
| if (CFCharacterSetIsLongCharacterMember(set, c)) |
| break; |
| pos = pos.next(CannotCrossEditingBoundary); // stay in editable content |
| } |
| } |
| } |
| } |
| |
| return [WebVisiblePosition _wrapVisiblePositionIfValid:pos]; |
| } |
| |
| - (BOOL)isEditable |
| { |
| return isEditablePosition([self _visiblePosition].deepEquivalent()); |
| } |
| |
| - (BOOL)requiresContextForWordBoundary |
| { |
| VisiblePosition vp = [self _visiblePosition]; |
| return requiresContextForWordBoundary(vp.characterAfter()) || requiresContextForWordBoundary(vp.characterBefore()); |
| } |
| |
| - (BOOL)atAlphaNumericBoundaryInDirection:(WebTextAdjustmentDirection)direction |
| { |
| static CFCharacterSetRef set = CFCharacterSetGetPredefined(kCFCharacterSetAlphaNumeric); |
| VisiblePosition pos = [self _visiblePosition]; |
| UChar32 charBefore = pos.characterBefore(); |
| UChar32 charAfter = pos.characterAfter(); |
| bool before = CFCharacterSetIsCharacterMember(set, charBefore); |
| bool after = CFCharacterSetIsCharacterMember(set, charAfter); |
| return [self directionIsDownstream:direction] ? (before && !after) : (!before && after); |
| } |
| |
| - (DOMRange *)enclosingRangeWithDictationPhraseAlternatives:(NSArray **)alternatives |
| { |
| ASSERT(alternatives); |
| if (!alternatives) |
| return nil; |
| |
| // *alternatives should not already point to an array. |
| ASSERT(!(*alternatives)); |
| *alternatives = nil; |
| |
| VisiblePosition p = [self _visiblePosition]; |
| if (p.isNull()) |
| return nil; |
| |
| int o = p.deepEquivalent().deprecatedEditingOffset(); |
| if (o < 0) |
| return nil; |
| unsigned offset = o; |
| |
| Node* node = p.deepEquivalent().anchorNode(); |
| Document& document = node->document(); |
| |
| const auto& markers = document.markers().markersFor(*node, DocumentMarker::DictationPhraseWithAlternatives); |
| if (markers.isEmpty()) |
| return nil; |
| |
| for (size_t i = 0; i < markers.size(); i++) { |
| const DocumentMarker* marker = markers[i]; |
| if (marker->startOffset() <= offset && marker->endOffset() >= offset) { |
| const Vector<String>& markerAlternatives = marker->alternatives(); |
| *alternatives = [NSMutableArray arrayWithCapacity:markerAlternatives.size()]; |
| for (size_t j = 0; j < markerAlternatives.size(); j++) |
| [(NSMutableArray *)*alternatives addObject:(NSString *)(markerAlternatives[j])]; |
| |
| auto range = Range::create(document, node, marker->startOffset(), node, marker->endOffset()); |
| return kit(range.ptr()); |
| } |
| } |
| |
| return nil; |
| } |
| |
| - (DOMRange *)enclosingRangeWithCorrectionIndicator |
| { |
| VisiblePosition p = [self _visiblePosition]; |
| if (p.isNull()) |
| return nil; |
| |
| int o = p.deepEquivalent().deprecatedEditingOffset(); |
| if (o < 0) |
| return nil; |
| unsigned offset = o; |
| |
| Node* node = p.deepEquivalent().anchorNode(); |
| Document& document = node->document(); |
| |
| const auto& markers = document.markers().markersFor(*node, DocumentMarker::Spelling); |
| if (markers.isEmpty()) |
| return nil; |
| |
| for (size_t i = 0; i < markers.size(); i++) { |
| const DocumentMarker* marker = markers[i]; |
| if (marker->startOffset() <= offset && marker->endOffset() >= offset) { |
| auto range = Range::create(document, node, marker->startOffset(), node, marker->endOffset()); |
| return kit(range.ptr()); |
| } |
| } |
| |
| return nil; |
| } |
| |
| - (NSSelectionAffinity)affinity |
| { |
| return (NSSelectionAffinity)[self _visiblePosition].affinity(); |
| } |
| |
| - (void)setAffinity:(NSSelectionAffinity)affinity |
| { |
| reinterpret_cast<VisiblePosition *>(_internal)->setAffinity((WebCore::EAffinity)affinity); |
| } |
| |
| @end |
| |
| @implementation DOMRange (VisiblePositionExtensions) |
| |
| - (WebVisiblePosition *)startPosition |
| { |
| Range *range = core(self); |
| return [WebVisiblePosition _wrapVisiblePosition:VisiblePosition(range->startPosition())]; |
| } |
| |
| - (WebVisiblePosition *)endPosition |
| { |
| Range *range = core(self); |
| return [WebVisiblePosition _wrapVisiblePosition:VisiblePosition(range->endPosition())]; |
| } |
| |
| - (DOMRange *)enclosingWordRange |
| { |
| VisibleSelection selection([self.startPosition _visiblePosition], [self.endPosition _visiblePosition]); |
| selection = FrameSelection::wordSelectionContainingCaretSelection(selection); |
| WebVisiblePosition *start = [WebVisiblePosition _wrapVisiblePosition:selection.visibleStart()]; |
| WebVisiblePosition *end = [WebVisiblePosition _wrapVisiblePosition:selection.visibleEnd()]; |
| return [DOMRange rangeForFirstPosition:start second:end]; |
| } |
| |
| + (DOMRange *)rangeForFirstPosition:(WebVisiblePosition *)first second:(WebVisiblePosition *)second |
| { |
| VisiblePosition firstVP = [first _visiblePosition]; |
| VisiblePosition secondVP = [second _visiblePosition]; |
| |
| if (firstVP.isNull() || secondVP.isNull()) |
| return nil; |
| |
| RefPtr<Range> range; |
| if (firstVP < secondVP) { |
| range = Range::create(firstVP.deepEquivalent().deprecatedNode()->document(), |
| firstVP, secondVP); |
| } else { |
| range = Range::create(firstVP.deepEquivalent().deprecatedNode()->document(), |
| secondVP, firstVP); |
| } |
| |
| |
| return kit(range.get()); |
| } |
| |
| @end |
| |
| @implementation DOMNode (VisiblePositionExtensions) |
| |
| - (DOMRange *)rangeOfContents |
| { |
| DOMRange *range = [[self ownerDocument] createRange]; |
| [range setStart:self offset:0]; |
| [range setEnd:self offset:[[self childNodes] length]]; |
| return range; |
| } |
| |
| - (WebVisiblePosition *)startPosition |
| { |
| // When in editable content, we need to calculate the startPosition from the beginning of the |
| // editable area. |
| Node* node = core(self); |
| if (node->isContentEditable()) { |
| VisiblePosition vp(createLegacyEditingPosition(node, 0), VP_DEFAULT_AFFINITY); |
| return [WebVisiblePosition _wrapVisiblePosition:startOfEditableContent(vp)]; |
| } |
| return [[self rangeOfContents] startPosition]; |
| } |
| |
| - (WebVisiblePosition *)endPosition |
| { |
| // When in editable content, we need to calculate the endPosition from the end of the |
| // editable area. |
| Node* node = core(self); |
| if (node->isContentEditable()) { |
| VisiblePosition vp(createLegacyEditingPosition(node, 0), VP_DEFAULT_AFFINITY); |
| return [WebVisiblePosition _wrapVisiblePosition:endOfEditableContent(vp)]; |
| } |
| return [[self rangeOfContents] endPosition]; |
| } |
| |
| @end |
| |
| @implementation DOMHTMLInputElement (VisiblePositionExtensions) |
| |
| - (WebVisiblePosition *)startPosition |
| { |
| Node* node = core(self); |
| RenderObject* object = node->renderer(); |
| if (!is<RenderTextControl>(object)) |
| return [super startPosition]; |
| |
| VisiblePosition visiblePosition = downcast<RenderTextControl>(*object).textFormControlElement().visiblePositionForIndex(0); |
| return [WebVisiblePosition _wrapVisiblePosition:visiblePosition]; |
| } |
| |
| - (WebVisiblePosition *)endPosition |
| { |
| Node* node = core(self); |
| RenderObject* object = node->renderer(); |
| if (!is<RenderTextControl>(object)) |
| return [super endPosition]; |
| |
| RenderTextControl& textControl = downcast<RenderTextControl>(*object); |
| VisiblePosition visiblePosition = textControl.textFormControlElement().visiblePositionForIndex(textControl.textFormControlElement().value().length()); |
| return [WebVisiblePosition _wrapVisiblePosition:visiblePosition]; |
| } |
| |
| @end |
| |
| @implementation DOMHTMLTextAreaElement (VisiblePositionExtensions) |
| |
| - (WebVisiblePosition *)startPosition |
| { |
| Node* node = core(self); |
| RenderObject* object = node->renderer(); |
| if (!object) |
| return [super startPosition]; |
| |
| VisiblePosition visiblePosition = downcast<RenderTextControl>(*object).textFormControlElement().visiblePositionForIndex(0); |
| return [WebVisiblePosition _wrapVisiblePosition:visiblePosition]; |
| } |
| |
| - (WebVisiblePosition *)endPosition |
| { |
| Node* node = core(self); |
| RenderObject* object = node->renderer(); |
| if (!object) |
| return [super endPosition]; |
| |
| RenderTextControl& textControl = downcast<RenderTextControl>(*object); |
| VisiblePosition visiblePosition = textControl.textFormControlElement().visiblePositionForIndex(textControl.textFormControlElement().value().length()); |
| return [WebVisiblePosition _wrapVisiblePosition:visiblePosition]; |
| } |
| |
| @end |
| |
| #endif // PLATFORM(IOS_FAMILY) |