| /* |
| * Copyright (C) 2004, 2005, 2006, 2007, 2008, 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 COMPUTER, INC. ``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 COMPUTER, INC. OR |
| * 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. |
| */ |
| |
| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/editing/visible_units.h" |
| |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/ephemeral_range.h" |
| #include "third_party/blink/renderer/core/editing/text_segments.h" |
| #include "third_party/blink/renderer/core/editing/visible_position.h" |
| #include "third_party/blink/renderer/core/layout/layout_block_flow.h" |
| #include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h" |
| #include "third_party/blink/renderer/platform/text/text_boundaries.h" |
| #include "third_party/blink/renderer/platform/text/text_break_iterator.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Helpers used during word movement |
| static bool IsLineBreak(UChar ch) { |
| return ch == kNewlineCharacter || ch == kCarriageReturnCharacter; |
| } |
| |
| static bool IsWordBreak(UChar ch) { |
| return WTF::unicode::IsAlphanumeric(ch) || IsLineBreak(ch) || |
| ch == kLowLineCharacter || WTF::unicode::IsPunct(ch); |
| } |
| |
| PositionInFlatTree EndOfWordPositionInternal(const PositionInFlatTree& position, |
| WordSide side) { |
| class Finder final : public TextSegments::Finder { |
| STACK_ALLOCATED(); |
| |
| public: |
| Finder(WordSide side) : side_(side) {} |
| |
| private: |
| Position Find(const String text, unsigned offset) final { |
| DCHECK_LE(offset, text.length()); |
| if (!is_first_time_) |
| return FindInternal(text, offset); |
| is_first_time_ = false; |
| if (side_ == kPreviousWordIfOnBoundary) { |
| if (offset == 0) |
| return Position::Before(0); |
| return FindInternal(text, offset - 1); |
| } |
| if (offset == text.length()) |
| return Position::After(offset); |
| return FindInternal(text, offset); |
| } |
| |
| static Position FindInternal(const String text, unsigned offset) { |
| DCHECK_LE(offset, text.length()); |
| TextBreakIterator* it = WordBreakIterator(text.Span16()); |
| const int result = it->following(offset); |
| if (result == kTextBreakDone || result == 0) |
| return Position(); |
| return Position::After(result - 1); |
| } |
| |
| const WordSide side_; |
| bool is_first_time_ = true; |
| } finder(side); |
| return TextSegments::FindBoundaryForward(position, &finder); |
| } |
| |
| PositionInFlatTree NextWordPositionInternal( |
| const PositionInFlatTree& position, |
| PlatformWordBehavior platform_word_behavior) { |
| class Finder final : public TextSegments::Finder { |
| STACK_ALLOCATED(); |
| |
| public: |
| Finder(PlatformWordBehavior platform_word_behavior) |
| : platform_word_behavior_(platform_word_behavior) {} |
| |
| private: |
| Position Find(const String text, unsigned offset) final { |
| DCHECK_LE(offset, text.length()); |
| if (!is_first_time_ && static_cast<unsigned>(offset) < text.length()) { |
| // These conditions check if we found a valid word break position after |
| // another iteration of scanning contents from the position that was |
| // passed to this function. Ex: |Hello |World|\n |foo |bar |
| // When we are after World|, the first iteration of this loop after call |
| // to TextSegments::Finder::find will return empty position as there |
| // aren't any meaningful word in that inline_content. In the next |
| // iteration of this loop, it fetches the word |foo, so we return the |
| // current position as we don't want to skip this valid position by |
| // advancing from this position and return |bar instead. |
| if (IsWordBreak(text[offset])) |
| return SkipWhitespaceIfNeeded(text, offset); |
| } |
| is_first_time_ = false; |
| if (offset == text.length() || text.length() == 0) |
| return Position(); |
| TextBreakIterator* it = WordBreakIterator(text.Span16()); |
| for (int runner = it->following(offset); runner != kTextBreakDone; |
| runner = it->following(runner)) { |
| // Move after line break |
| if (IsLineBreak(text[runner])) |
| return SkipWhitespaceIfNeeded(text, runner); |
| // Accumulate punctuation runs |
| if (static_cast<unsigned>(runner) < text.length() && |
| WTF::unicode::IsPunct(text[runner])) { |
| if (WTF::unicode::IsAlphanumeric(text[runner - 1])) |
| return SkipWhitespaceIfNeeded(text, runner); |
| continue; |
| } |
| // We stop searching in the following conditions: |
| // 1. When the character preceding the break is |
| // alphanumeric or punctuations or underscore or linebreaks. |
| // Only on Windows: |
| // 2. When the character preceding the break is a whitespace and |
| // the character following it is an alphanumeric or punctuations |
| // or underscore or linebreaks. |
| if (static_cast<unsigned>(runner) < text.length() && |
| IsWordBreak(text[runner - 1])) |
| return SkipWhitespaceIfNeeded(text, runner); |
| else if (platform_word_behavior_ == |
| PlatformWordBehavior::kWordSkipSpaces && |
| static_cast<unsigned>(runner) < text.length() && |
| IsWhitespace(text[runner - 1]) && IsWordBreak(text[runner])) |
| return SkipWhitespaceIfNeeded(text, runner); |
| } |
| if (text[text.length() - 1] != kNewlineCharacter) |
| return Position::After(text.length() - 1); |
| return Position(); |
| } |
| |
| Position SkipWhitespaceIfNeeded(const String text, int offset) { |
| DCHECK_NE(offset, kTextBreakDone); |
| // On Windows next word should skip trailing whitespaces but not line |
| // break |
| if (platform_word_behavior_ == PlatformWordBehavior::kWordSkipSpaces) { |
| for (unsigned runner = static_cast<unsigned>(offset); |
| runner < text.length(); ++runner) { |
| if (!(IsWhitespace(text[runner]) || |
| WTF::unicode::Direction(text[runner]) == |
| WTF::unicode::kWhiteSpaceNeutral) || |
| IsLineBreak(text[runner])) |
| return Position::Before(runner); |
| } |
| } |
| return Position::Before(offset); |
| } |
| |
| const PlatformWordBehavior platform_word_behavior_; |
| bool is_first_time_ = true; |
| } finder(platform_word_behavior); |
| return TextSegments::FindBoundaryForward(position, &finder); |
| } |
| |
| PositionInFlatTree PreviousWordPositionInternal( |
| const PositionInFlatTree& position) { |
| class Finder final : public TextSegments::Finder { |
| STACK_ALLOCATED(); |
| |
| private: |
| Position Find(const String text, unsigned offset) final { |
| DCHECK_LE(offset, text.length()); |
| if (!is_first_time_ && text.length() > 0 && |
| static_cast<unsigned>(offset) <= text.length()) { |
| // These conditions check if we found a valid word break position after |
| // another iteration of scanning contents from the position that was |
| // passed to this function. Ex: |Hello |World|\n |foo |bar |
| // When we are before |foo, the first iteration of this loop after call |
| // to TextSegments::Finder::find will return empty position as there |
| // aren't any meaningful word in that inline_content. In the next |
| // iteration of this loop, it fetches the word World|, so we return the |
| // current position as we don't want to skip this valid position by |
| // advancing from this position and return |World instead. |
| if (IsWordBreak(text[offset - 1])) |
| return Position::Before(offset); |
| } |
| is_first_time_ = false; |
| if (!offset || text.length() == 0) |
| return Position(); |
| TextBreakIterator* it = WordBreakIterator(text.Span16()); |
| int punct_runner = -1; |
| for (int runner = it->preceding(offset); runner != kTextBreakDone; |
| runner = it->preceding(runner)) { |
| // Accumulate punctuation runs |
| if (static_cast<unsigned>(runner) < text.length() && |
| WTF::unicode::IsPunct(text[runner])) { |
| if (WTF::unicode::IsAlphanumeric(text[runner - 1])) |
| return Position::Before(runner); |
| punct_runner = runner; |
| continue; |
| } |
| |
| if (punct_runner >= 0) |
| return Position::Before(punct_runner); |
| // We stop searching when the character following the break is |
| // alphanumeric or punctuations or underscore or linebreaks. |
| if (static_cast<unsigned>(runner) < text.length() && |
| IsWordBreak(text[runner])) |
| return Position::Before(runner); |
| } |
| return Position::Before(0); |
| } |
| bool is_first_time_ = true; |
| } finder; |
| return TextSegments::FindBoundaryBackward(position, &finder); |
| } |
| |
| PositionInFlatTree StartOfWordPositionInternal( |
| const PositionInFlatTree& position, |
| WordSide side) { |
| class Finder final : public TextSegments::Finder { |
| STACK_ALLOCATED(); |
| |
| public: |
| Finder(WordSide side) : side_(side) {} |
| |
| private: |
| Position Find(const String text, unsigned offset) final { |
| DCHECK_LE(offset, text.length()); |
| if (!is_first_time_) |
| return FindInternal(text, offset); |
| is_first_time_ = false; |
| if (side_ == kNextWordIfOnBoundary) { |
| if (offset == text.length()) |
| return Position::After(text.length()); |
| return FindInternal(text, offset + 1); |
| } |
| if (!offset) |
| return Position::Before(offset); |
| return FindInternal(text, offset); |
| } |
| |
| static Position FindInternal(const String text, unsigned offset) { |
| DCHECK_LE(offset, text.length()); |
| TextBreakIterator* it = WordBreakIterator(text.Span16()); |
| const int result = it->preceding(offset); |
| if (result == kTextBreakDone) |
| return Position(); |
| return Position::Before(result); |
| } |
| |
| const WordSide side_; |
| bool is_first_time_ = true; |
| } finder(side); |
| return TextSegments::FindBoundaryBackward(position, &finder); |
| } |
| } // namespace |
| |
| PositionInFlatTree EndOfWordPosition(const PositionInFlatTree& start, |
| WordSide side) { |
| return AdjustForwardPositionToAvoidCrossingEditingBoundaries( |
| PositionInFlatTreeWithAffinity( |
| EndOfWordPositionInternal(start, side)), |
| start) |
| .GetPosition(); |
| } |
| |
| Position EndOfWordPosition(const Position& position, WordSide side) { |
| return ToPositionInDOMTree( |
| EndOfWordPosition(ToPositionInFlatTree(position), side)); |
| } |
| |
| // ---- |
| // TODO(editing-dev): Because of word boundary can not be an upstream position, |
| // we should make this function to return |PositionInFlatTree|. |
| PositionInFlatTreeWithAffinity NextWordPosition( |
| const PositionInFlatTree& start, |
| PlatformWordBehavior platform_word_behavior) { |
| const PositionInFlatTree next = |
| NextWordPositionInternal(start, platform_word_behavior); |
| // Note: The word boundary can not be upstream position. |
| const PositionInFlatTreeWithAffinity adjusted = |
| AdjustForwardPositionToAvoidCrossingEditingBoundaries( |
| PositionInFlatTreeWithAffinity(next), start); |
| DCHECK_EQ(adjusted.Affinity(), TextAffinity::kDownstream); |
| return adjusted; |
| } |
| |
| PositionWithAffinity NextWordPosition( |
| const Position& start, |
| PlatformWordBehavior platform_word_behavior) { |
| const PositionInFlatTreeWithAffinity& next = |
| NextWordPosition(ToPositionInFlatTree(start), platform_word_behavior); |
| return ToPositionInDOMTreeWithAffinity(next); |
| } |
| |
| PositionInFlatTreeWithAffinity PreviousWordPosition( |
| const PositionInFlatTree& start) { |
| const PositionInFlatTree prev = PreviousWordPositionInternal(start); |
| return AdjustBackwardPositionToAvoidCrossingEditingBoundaries( |
| PositionInFlatTreeWithAffinity(prev), start); |
| } |
| |
| PositionWithAffinity PreviousWordPosition(const Position& start) { |
| const PositionInFlatTreeWithAffinity& prev = |
| PreviousWordPosition(ToPositionInFlatTree(start)); |
| return ToPositionInDOMTreeWithAffinity(prev); |
| } |
| |
| PositionInFlatTree StartOfWordPosition(const PositionInFlatTree& position, |
| WordSide side) { |
| const PositionInFlatTree start = StartOfWordPositionInternal(position, side); |
| return AdjustBackwardPositionToAvoidCrossingEditingBoundaries( |
| PositionInFlatTreeWithAffinity(start), position) |
| .GetPosition(); |
| } |
| |
| Position StartOfWordPosition(const Position& position, WordSide side) { |
| return ToPositionInDOMTree( |
| StartOfWordPosition(ToPositionInFlatTree(position), side)); |
| } |
| |
| } // namespace blink |