blob: 93878883ce8d5f707b73b02d87604c85cedcfaeb [file] [log] [blame]
/*
* Copyright (C) 2004-2020 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. ``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
* 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.
*/
#include "config.h"
#include "VisibleUnits.h"
#include "Document.h"
#include "Editing.h"
#include "HTMLBRElement.h"
#include "HTMLElement.h"
#include "HTMLNames.h"
#include "InlineIteratorBox.h"
#include "InlineIteratorLine.h"
#include "InlineRunAndOffset.h"
#include "NodeTraversal.h"
#include "Range.h"
#include "RenderBlockFlow.h"
#include "RenderObject.h"
#include "RenderedPosition.h"
#include "Text.h"
#include "TextBoundaries.h"
#include "TextIterator.h"
#include "VisibleSelection.h"
#include <unicode/ubrk.h>
#include <wtf/text/TextBreakIterator.h>
namespace WebCore {
using namespace HTMLNames;
using namespace WTF::Unicode;
static Node* previousLeafWithSameEditability(Node* node, EditableType editableType)
{
bool editable = hasEditableStyle(*node, editableType);
node = previousLeafNode(node);
while (node) {
if (editable == hasEditableStyle(*node, editableType))
return node;
node = previousLeafNode(node);
}
return nullptr;
}
static Node* nextLeafWithSameEditability(Node* node, EditableType editableType)
{
if (!node)
return nullptr;
bool editable = hasEditableStyle(*node, editableType);
node = nextLeafNode(node);
while (node) {
if (editable == hasEditableStyle(*node, editableType))
return node;
node = nextLeafNode(node);
}
return nullptr;
}
// FIXME: consolidate with code in previousLinePosition.
static Position previousLineCandidatePosition(Node* node, const VisiblePosition& visiblePosition, EditableType editableType)
{
auto* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent(), editableType);
Node* previousNode = previousLeafWithSameEditability(node, editableType);
while (previousNode && (!previousNode->renderer() || inSameLine(firstPositionInOrBeforeNode(previousNode), visiblePosition)))
previousNode = previousLeafWithSameEditability(previousNode, editableType);
while (previousNode && !previousNode->isShadowRoot()) {
if (highestEditableRoot(firstPositionInOrBeforeNode(previousNode), editableType) != highestRoot)
break;
Position pos = previousNode->hasTagName(brTag) ? positionBeforeNode(previousNode) :
makeDeprecatedLegacyPosition(previousNode, caretMaxOffset(*previousNode));
if (pos.isCandidate())
return pos;
previousNode = previousLeafWithSameEditability(previousNode, editableType);
}
return Position();
}
static Position nextLineCandidatePosition(Node* node, const VisiblePosition& visiblePosition, EditableType editableType)
{
auto* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent(), editableType);
Node* nextNode = nextLeafWithSameEditability(node, editableType);
while (nextNode && (!nextNode->renderer() || inSameLine(firstPositionInOrBeforeNode(nextNode), visiblePosition)))
nextNode = nextLeafWithSameEditability(nextNode, ContentIsEditable);
while (nextNode && !nextNode->isShadowRoot()) {
if (highestEditableRoot(firstPositionInOrBeforeNode(nextNode), editableType) != highestRoot)
break;
Position pos;
pos = makeDeprecatedLegacyPosition(nextNode, caretMinOffset(*nextNode));
if (pos.isCandidate())
return pos;
nextNode = nextLeafWithSameEditability(nextNode, editableType);
}
return Position();
}
static bool isTextOrLineBreakRun(InlineIterator::LeafBoxIterator run)
{
return run && (run->isText() || run->renderer().isBR());
}
static InlineIterator::LeafBoxIterator previousTextOrLineBreakRun(InlineIterator::LeafBoxIterator run)
{
while (run) {
run.traversePreviousOnLineInLogicalOrder();
if (isTextOrLineBreakRun(run))
return run;
}
return { };
}
static InlineIterator::LeafBoxIterator nextTextOrLineBreakRun(InlineIterator::LeafBoxIterator run)
{
while (run) {
run.traverseNextOnLineInLogicalOrder();
if (isTextOrLineBreakRun(run))
return run;
}
return { };
}
static InlineIterator::LeafBoxIterator startTextOrLineBreakRun(InlineIterator::LineIterator line)
{
auto run = line->logicalStartRun();
if (isTextOrLineBreakRun(run))
return run;
return nextTextOrLineBreakRun(run);
}
static InlineIterator::LeafBoxIterator endTextOrLineBreakRun(InlineIterator::LineIterator line)
{
auto run = line->logicalEndRun();
if (isTextOrLineBreakRun(run))
return run;
return previousTextOrLineBreakRun(run);
}
static const InlineIterator::LeafBoxIterator logicallyPreviousRun(const VisiblePosition& visiblePosition, InlineIterator::LeafBoxIterator startRun, bool& previousBoxInDifferentLine)
{
if (auto previousRun = previousTextOrLineBreakRun(startRun))
return previousRun;
if (auto previousLine = startRun->line()->previous()) {
// FIXME: Why isn't previousBoxInDifferentLine set here?
if (auto previousRun = endTextOrLineBreakRun(previousLine))
return previousRun;
}
while (1) {
auto* startNode = startRun->renderer().nonPseudoNode();
if (!startNode)
break;
Position position = previousLineCandidatePosition(startNode, visiblePosition, ContentIsEditable);
if (position.isNull())
break;
RenderedPosition renderedPosition(position, Affinity::Downstream);
auto previousLine = renderedPosition.line();
if (!previousLine)
break;
if (previousLine != startRun->line()) {
if (auto previousRun = endTextOrLineBreakRun(previousLine)) {
previousBoxInDifferentLine = true;
return previousRun;
}
}
startRun = previousLine->logicalStartRun();
}
return { };
}
static const InlineIterator::LeafBoxIterator logicallyNextRun(const VisiblePosition& visiblePosition, InlineIterator::LeafBoxIterator startRun, bool& nextBoxInDifferentLine)
{
if (auto nextRun = nextTextOrLineBreakRun(startRun))
return nextRun;
if (auto nextLine = startRun->line()->next()) {
// FIXME: Why isn't previousBoxInDifferentLine set here?
if (auto nextRun = startTextOrLineBreakRun(nextLine))
return nextRun;
}
while (1) {
auto* startNode = startRun->renderer().nonPseudoNode();
if (!startNode)
break;
Position position = nextLineCandidatePosition(startNode, visiblePosition, ContentIsEditable);
if (position.isNull())
break;
RenderedPosition renderedPosition(position, Affinity::Downstream);
auto nextLine = renderedPosition.line();
if (!nextLine)
break;
if (nextLine != startRun->line()) {
if (auto nextRun = startTextOrLineBreakRun(nextLine)) {
nextBoxInDifferentLine = true;
return nextRun;
}
}
startRun = nextLine->logicalEndRun();
}
return { };
}
static UBreakIterator* wordBreakIteratorForMinOffsetBoundary(const VisiblePosition& visiblePosition, InlineIterator::TextBoxIterator textRun,
unsigned& previousRunLength, bool& previousRunInDifferentLine, Vector<UChar, 1024>& string)
{
previousRunInDifferentLine = false;
auto previousRun = logicallyPreviousRun(visiblePosition, textRun, previousRunInDifferentLine);
while (previousRun && !previousRun->isText()) {
ASSERT(previousRun->renderer().isBR());
previousRunInDifferentLine = true;
previousRun = logicallyPreviousRun(visiblePosition, previousRun, previousRunInDifferentLine);
}
string.clear();
if (previousRun) {
auto& previousTextRun = downcast<InlineIterator::TextBoxIterator>(previousRun);
previousRunLength = previousTextRun->length();
append(string, previousTextRun->text());
}
append(string, textRun->text());
return wordBreakIterator(StringView(string.data(), string.size()));
}
static UBreakIterator* wordBreakIteratorForMaxOffsetBoundary(const VisiblePosition& visiblePosition, InlineIterator::TextBoxIterator textRun,
bool& nextRunInDifferentLine, Vector<UChar, 1024>& string)
{
nextRunInDifferentLine = false;
auto nextRun = logicallyNextRun(visiblePosition, textRun, nextRunInDifferentLine);
while (nextRun && !nextRun->isText()) {
ASSERT(nextRun->renderer().isBR());
nextRunInDifferentLine = true;
nextRun = logicallyNextRun(visiblePosition, nextRun, nextRunInDifferentLine);
}
string.clear();
append(string, textRun->text());
if (nextRun) {
auto& nextTextRun = downcast<InlineIterator::TextBoxIterator>(nextRun);
append(string, nextTextRun->text());
}
return wordBreakIterator(StringView(string.data(), string.size()));
}
static bool isLogicalStartOfWord(UBreakIterator* iter, int position, bool hardLineBreak)
{
bool boundary = hardLineBreak ? true : ubrk_isBoundary(iter, position);
if (!boundary)
return false;
ubrk_following(iter, position);
// isWordTextBreak returns true after moving across a word and false after moving across a punctuation/space.
return isWordTextBreak(iter);
}
static bool islogicalEndOfWord(UBreakIterator* iter, int position, bool hardLineBreak)
{
bool boundary = ubrk_isBoundary(iter, position);
return (hardLineBreak || boundary) && isWordTextBreak(iter);
}
enum CursorMovementDirection { MoveLeft, MoveRight };
static VisiblePosition visualWordPosition(const VisiblePosition& visiblePosition, CursorMovementDirection direction,
bool skipsSpaceWhenMovingRight)
{
if (visiblePosition.isNull() || !visiblePosition.deepEquivalent().document())
return VisiblePosition();
visiblePosition.deepEquivalent().document()->updateLayoutIgnorePendingStylesheets();
TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent());
InlineIterator::LeafBoxIterator previouslyVisitedRun;
VisiblePosition current = visiblePosition;
std::optional<VisiblePosition> previousPosition;
UBreakIterator* iter = nullptr;
Vector<UChar, 1024> string;
while (1) {
VisiblePosition adjacentCharacterPosition = direction == MoveRight ? current.right(true) : current.left(true);
if (adjacentCharacterPosition == current || adjacentCharacterPosition.isNull())
return VisiblePosition();
// FIXME: This is a workaround for webkit.org/b/167138.
if (previousPosition && adjacentCharacterPosition == previousPosition.value())
return VisiblePosition();
// FIXME: Why force the use of upstream affinity here instead of VisiblePosition::inlineRunAndOffset, which will get affinity from adjacentCharacterPosition?
auto [run, offsetInRun] = adjacentCharacterPosition.deepEquivalent().inlineRunAndOffset(Affinity::Upstream);
if (!run)
break;
if (!run->isText()) {
current = adjacentCharacterPosition;
continue;
}
auto& textRun = downcast<InlineIterator::TextBoxIterator>(run);
unsigned previousRunLength = 0;
bool previousRunInDifferentLine = false;
bool nextRunInDifferentLine = false;
bool movingIntoNewRun = previouslyVisitedRun != run;
if (offsetInRun == textRun->minimumCaretOffset())
iter = wordBreakIteratorForMinOffsetBoundary(adjacentCharacterPosition, textRun, previousRunLength, previousRunInDifferentLine, string);
else if (offsetInRun == textRun->maximumCaretOffset())
iter = wordBreakIteratorForMaxOffsetBoundary(adjacentCharacterPosition, textRun, nextRunInDifferentLine, string);
else if (movingIntoNewRun) {
iter = wordBreakIterator(textRun->text());
previouslyVisitedRun = run;
}
if (!iter)
break;
ubrk_first(iter);
int offsetInIterator = offsetInRun - textRun->start() + previousRunLength;
bool isWordBreak;
bool boxHasSameDirectionalityAsBlock = run->direction() == blockDirection;
bool movingBackward = (direction == MoveLeft && run->direction() == TextDirection::LTR) || (direction == MoveRight && run->direction() == TextDirection::RTL);
if ((skipsSpaceWhenMovingRight && boxHasSameDirectionalityAsBlock)
|| (!skipsSpaceWhenMovingRight && movingBackward)) {
bool logicalStartInRenderer = offsetInRun == textRun->start() && previousRunInDifferentLine;
isWordBreak = isLogicalStartOfWord(iter, offsetInIterator, logicalStartInRenderer);
if (isWordBreak && offsetInRun == run->maximumCaretOffset() && nextRunInDifferentLine)
isWordBreak = false;
} else {
bool logicalEndInRenderer = offsetInRun == textRun->end() && nextRunInDifferentLine;
isWordBreak = islogicalEndOfWord(iter, offsetInIterator, logicalEndInRenderer);
if (isWordBreak && offsetInRun == run->minimumCaretOffset() && previousRunInDifferentLine)
isWordBreak = false;
}
if (isWordBreak)
return adjacentCharacterPosition;
previousPosition = current;
current = adjacentCharacterPosition;
}
return VisiblePosition();
}
VisiblePosition leftWordPosition(const VisiblePosition& visiblePosition, bool skipsSpaceWhenMovingRight)
{
VisiblePosition leftWordBreak = visualWordPosition(visiblePosition, MoveLeft, skipsSpaceWhenMovingRight);
leftWordBreak = visiblePosition.honorEditingBoundaryAtOrBefore(leftWordBreak);
// FIXME: How should we handle a non-editable position?
if (leftWordBreak.isNull() && isEditablePosition(visiblePosition.deepEquivalent())) {
TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent());
leftWordBreak = blockDirection == TextDirection::LTR ? startOfEditableContent(visiblePosition) : endOfEditableContent(visiblePosition);
}
return leftWordBreak;
}
VisiblePosition rightWordPosition(const VisiblePosition& visiblePosition, bool skipsSpaceWhenMovingRight)
{
VisiblePosition rightWordBreak = visualWordPosition(visiblePosition, MoveRight, skipsSpaceWhenMovingRight);
rightWordBreak = visiblePosition.honorEditingBoundaryAtOrBefore(rightWordBreak);
// FIXME: How should we handle a non-editable position?
if (rightWordBreak.isNull() && isEditablePosition(visiblePosition.deepEquivalent())) {
TextDirection blockDirection = directionOfEnclosingBlock(visiblePosition.deepEquivalent());
rightWordBreak = blockDirection == TextDirection::LTR ? endOfEditableContent(visiblePosition) : startOfEditableContent(visiblePosition);
}
return rightWordBreak;
}
static void prepend(Vector<UChar, 1024>& buffer, StringView string)
{
unsigned oldSize = buffer.size();
unsigned length = string.length();
buffer.grow(oldSize + length);
memmove(buffer.data() + length, buffer.data(), oldSize * sizeof(UChar));
for (unsigned i = 0; i < length; ++i)
buffer[i] = string[i];
}
static void prependRepeatedCharacter(Vector<UChar, 1024>& buffer, UChar character, unsigned count)
{
unsigned oldSize = buffer.size();
buffer.grow(oldSize + count);
memmove(buffer.data() + count, buffer.data(), oldSize * sizeof(UChar));
for (unsigned i = 0; i < count; ++i)
buffer[i] = character;
}
static void appendRepeatedCharacter(Vector<UChar, 1024>& buffer, UChar character, unsigned count)
{
unsigned oldSize = buffer.size();
buffer.grow(oldSize + count);
for (unsigned i = 0; i < count; ++i)
buffer[oldSize + i] = character;
}
unsigned suffixLengthForRange(const SimpleRange& forwardsScanRange, Vector<UChar, 1024>& string)
{
unsigned suffixLength = 0;
TextIterator forwardsIterator(forwardsScanRange);
while (!forwardsIterator.atEnd()) {
StringView text = forwardsIterator.text();
unsigned i = endOfFirstWordBoundaryContext(text);
append(string, text.substring(0, i));
suffixLength += i;
if (i < text.length())
break;
forwardsIterator.advance();
}
return suffixLength;
}
unsigned prefixLengthForRange(const SimpleRange& backwardsScanRange, Vector<UChar, 1024>& string)
{
unsigned prefixLength = 0;
SimplifiedBackwardsTextIterator backwardsIterator(backwardsScanRange);
while (!backwardsIterator.atEnd()) {
StringView text = backwardsIterator.text();
int i = startOfLastWordBoundaryContext(text);
prepend(string, text.substring(i));
prefixLength += text.length() - i;
if (i > 0)
break;
backwardsIterator.advance();
}
return prefixLength;
}
unsigned backwardSearchForBoundaryWithTextIterator(SimplifiedBackwardsTextIterator& it, Vector<UChar, 1024>& string, unsigned suffixLength, BoundarySearchFunction searchFunction)
{
unsigned next = 0;
bool needMoreContext = false;
while (!it.atEnd()) {
bool inTextSecurityMode = it.node() && it.node()->renderer() && it.node()->renderer()->style().textSecurity() != TextSecurity::None;
// iterate to get chunks until the searchFunction returns a non-zero value.
if (!inTextSecurityMode)
prepend(string, it.text());
else {
// Treat bullets used in the text security mode as regular characters when looking for boundaries
prependRepeatedCharacter(string, 'x', it.text().length());
}
if (string.size() > suffixLength) {
next = searchFunction(StringView(string.data(), string.size()), string.size() - suffixLength, MayHaveMoreContext, needMoreContext);
if (next > 1) // FIXME: This is a work around for https://webkit.org/b/115070. We need to provide more contexts in general case.
break;
}
it.advance();
}
if (needMoreContext && string.size() > suffixLength) {
// The last search returned the beginning of the buffer and asked for more context,
// but there is no earlier text. Force a search with what's available.
next = searchFunction(StringView(string.data(), string.size()), string.size() - suffixLength, DontHaveMoreContext, needMoreContext);
ASSERT(!needMoreContext);
}
return next;
}
unsigned forwardSearchForBoundaryWithTextIterator(TextIterator& it, Vector<UChar, 1024>& string, unsigned prefixLength, BoundarySearchFunction searchFunction)
{
unsigned next = 0;
bool needMoreContext = false;
while (!it.atEnd()) {
bool inTextSecurityMode = it.node() && it.node()->renderer() && it.node()->renderer()->style().textSecurity() != TextSecurity::None;
// Keep asking the iterator for chunks until the search function
// returns an end value not equal to the length of the string passed to it.
if (!inTextSecurityMode)
append(string, it.text());
else {
// Treat bullets used in the text security mode as regular characters when looking for boundaries
appendRepeatedCharacter(string, 'x', it.text().length());
}
if (string.size() > prefixLength) {
next = searchFunction(StringView(string.data(), string.size()), prefixLength, MayHaveMoreContext, needMoreContext);
if (next != string.size())
break;
}
it.advance();
}
if (needMoreContext && string.size() > prefixLength) {
// The last search returned the end of the buffer and asked for more context,
// but there is no further text. Force a search with what's available.
next = searchFunction(StringView(string.data(), string.size()), prefixLength, DontHaveMoreContext, needMoreContext);
ASSERT(!needMoreContext);
}
return next;
}
enum class NeedsContextAtParagraphStart { Yes, No };
static VisiblePosition previousBoundary(const VisiblePosition& position, BoundarySearchFunction searchFunction,
NeedsContextAtParagraphStart needsContextAtParagraphStart = NeedsContextAtParagraphStart::No)
{
auto boundary = position.deepEquivalent().parentEditingBoundary();
if (!boundary)
return { };
Vector<UChar, 1024> string;
unsigned suffixLength = 0;
auto searchRange = makeSimpleRange(makeBoundaryPointBeforeNodeContents(*boundary), position);
if (!searchRange)
return { };
if (needsContextAtParagraphStart == NeedsContextAtParagraphStart::Yes && isStartOfParagraph(position)) {
auto forwardsScanRange = makeSimpleRange(searchRange->start, endOfParagraph(position));
if (!forwardsScanRange)
return { };
for (TextIterator forwardsIterator(*forwardsScanRange); !forwardsIterator.atEnd(); forwardsIterator.advance())
append(string, forwardsIterator.text());
suffixLength = string.size();
} else if (requiresContextForWordBoundary(position.characterBefore())) {
auto forwardsScanRange = makeSimpleRange(searchRange->end, makeBoundaryPointAfterNodeContents(*boundary));
suffixLength = suffixLengthForRange(forwardsScanRange, string);
}
SimplifiedBackwardsTextIterator it(*searchRange);
unsigned next = backwardSearchForBoundaryWithTextIterator(it, string, suffixLength, searchFunction);
if (!next)
return it.atEnd() ? makeDeprecatedLegacyPosition(searchRange->start) : position;
auto& node = (it.atEnd() ? *searchRange : it.range()).start.container.get();
if ((!suffixLength && is<Text>(node) && next <= downcast<Text>(node).length()) || (node.renderer() && node.renderer()->isBR() && !next)) {
// The next variable contains a usable index into a text node.
return makeDeprecatedLegacyPosition(&node, next);
}
// Use the character iterator to translate the next value into a DOM position.
BackwardsCharacterIterator charIt(*searchRange);
if (next < string.size() - suffixLength)
charIt.advance(string.size() - suffixLength - next);
// FIXME: charIt can get out of shadow host.
return makeDeprecatedLegacyPosition(charIt.range().end);
}
static VisiblePosition nextBoundary(const VisiblePosition& c, BoundarySearchFunction searchFunction)
{
Position pos = c.deepEquivalent();
Node* boundary = pos.parentEditingBoundary();
if (!boundary)
return VisiblePosition();
Document& boundaryDocument = boundary->document();
Vector<UChar, 1024> string;
unsigned prefixLength = 0;
if (requiresContextForWordBoundary(c.characterAfter())) {
auto backwardsScanRange = makeSimpleRange(makeBoundaryPointBeforeNodeContents(boundaryDocument), c);
if (!backwardsScanRange)
return { };
prefixLength = prefixLengthForRange(*backwardsScanRange, string);
}
auto searchRange = makeSimpleRange(c, makeBoundaryPointAfterNodeContents(*boundary));
if (!searchRange)
return { };
TextIterator it(*searchRange, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
unsigned next = forwardSearchForBoundaryWithTextIterator(it, string, prefixLength, searchFunction);
if (it.atEnd() && next == string.size())
pos = makeDeprecatedLegacyPosition(searchRange->end);
else if (next > prefixLength) {
// Use the character iterator to translate the next value into a DOM position.
CharacterIterator charIt(*searchRange, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
charIt.advance(next - prefixLength - 1);
if (charIt.atEnd())
return { };
auto characterRange = charIt.range();
pos = makeDeprecatedLegacyPosition(characterRange.end);
if (charIt.text()[0] == '\n') {
// FIXME: workaround for collapsed range (where only start position is correct) emitted for some emitted newlines (see rdar://5192593)
if (VisiblePosition(pos) == VisiblePosition(makeDeprecatedLegacyPosition(characterRange.start))) {
charIt.advance(1);
pos = makeDeprecatedLegacyPosition(charIt.range().start);
}
}
}
return VisiblePosition(pos, Affinity::Upstream);
}
// ---------
unsigned startWordBoundary(StringView text, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext)
{
ASSERT(offset);
if (mayHaveMoreContext && !startOfLastWordBoundaryContext(text.substring(0, offset))) {
needMoreContext = true;
return 0;
}
needMoreContext = false;
int start, end;
U16_BACK_1(text, 0, offset);
findWordBoundary(text, offset, &start, &end);
return start;
}
VisiblePosition startOfWord(const VisiblePosition& c, EWordSide side)
{
// FIXME: This returns a null VP for c at the start of the document
// and side == LeftWordIfOnBoundary
VisiblePosition p = c;
if (side == RightWordIfOnBoundary) {
// at paragraph end, the startofWord is the current position
if (isEndOfParagraph(c))
return c;
p = c.next();
if (p.isNull())
return c;
}
return previousBoundary(p, startWordBoundary);
}
unsigned endWordBoundary(StringView text, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext)
{
ASSERT(offset <= text.length());
if (mayHaveMoreContext && endOfFirstWordBoundaryContext(text.substring(offset)) == text.length() - offset) {
needMoreContext = true;
return text.length();
}
needMoreContext = false;
int end;
findEndWordBoundary(text, offset, &end);
return end;
}
VisiblePosition endOfWord(const VisiblePosition& c, EWordSide side)
{
VisiblePosition p = c;
if (side == LeftWordIfOnBoundary) {
if (isStartOfParagraph(c))
return c;
p = c.previous();
if (p.isNull())
return c;
} else if (isEndOfParagraph(c))
return c;
return nextBoundary(p, endWordBoundary);
}
static unsigned previousWordPositionBoundary(StringView text, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext)
{
if (mayHaveMoreContext && !startOfLastWordBoundaryContext(text.substring(0, offset))) {
needMoreContext = true;
return 0;
}
needMoreContext = false;
return findNextWordFromIndex(text, offset, false);
}
VisiblePosition previousWordPosition(const VisiblePosition& position)
{
return position.honorEditingBoundaryAtOrBefore(previousBoundary(position, previousWordPositionBoundary));
}
static unsigned nextWordPositionBoundary(StringView text, unsigned offset, BoundarySearchContextAvailability mayHaveMoreContext, bool& needMoreContext)
{
if (mayHaveMoreContext && endOfFirstWordBoundaryContext(text.substring(offset)) == text.length() - offset) {
needMoreContext = true;
return text.length();
}
needMoreContext = false;
return findNextWordFromIndex(text, offset, true);
}
VisiblePosition nextWordPosition(const VisiblePosition& position)
{
return position.honorEditingBoundaryAtOrAfter(nextBoundary(position, nextWordPositionBoundary));
}
bool isStartOfWord(const VisiblePosition& p)
{
return p.isNotNull() && p == startOfWord(p, RightWordIfOnBoundary);
}
// ---------
enum LineEndpointComputationMode { UseLogicalOrdering, UseInlineBoxOrdering };
static VisiblePosition startPositionForLine(const VisiblePosition& c, LineEndpointComputationMode mode)
{
if (c.isNull())
return VisiblePosition();
auto line = RenderedPosition(c).line();
if (!line) {
// There are VisiblePositions at offset 0 in blocks without
// RootInlineBoxes, like empty editable blocks and bordered blocks.
Position p = c.deepEquivalent();
if (p.deprecatedNode()->renderer() && p.deprecatedNode()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset())
return c;
return VisiblePosition();
}
Node* startNode = nullptr;
auto startRun = mode == UseLogicalOrdering ? line->logicalStartRunWithNode() : line->firstRun();
// Generated content (e.g. list markers and CSS :before and :after pseudoelements) have no corresponding DOM element,
// and so cannot be represented by a VisiblePosition. Use whatever follows instead.
while (true) {
if (!startRun)
return VisiblePosition();
startNode = startRun->renderer().nonPseudoNode();
if (startNode)
break;
if (mode == UseLogicalOrdering)
startRun.traverseNextOnLineInLogicalOrder();
else
startRun.traverseNextOnLine();
}
return is<Text>(*startNode) ? Position(downcast<Text>(startNode), downcast<InlineIterator::TextBox>(*startRun).start())
: positionBeforeNode(startNode);
}
static VisiblePosition startOfLine(const VisiblePosition& c, LineEndpointComputationMode mode, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
// TODO: this is the current behavior that might need to be fixed.
// Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
VisiblePosition visPos = startPositionForLine(c, mode);
if (mode == UseLogicalOrdering) {
if (Node* editableRoot = highestEditableRoot(c.deepEquivalent())) {
if (!editableRoot->contains(visPos.deepEquivalent().containerNode())) {
VisiblePosition newPosition = firstPositionInNode(editableRoot);
if (reachedBoundary)
*reachedBoundary = c == newPosition;
return newPosition;
}
}
}
return c.honorEditingBoundaryAtOrBefore(visPos, reachedBoundary);
}
// FIXME: Rename this function to reflect the fact it ignores bidi levels.
VisiblePosition startOfLine(const VisiblePosition& currentPosition)
{
return startOfLine(currentPosition, UseInlineBoxOrdering, nullptr);
}
VisiblePosition logicalStartOfLine(const VisiblePosition& currentPosition, bool* reachedBoundary)
{
return startOfLine(currentPosition, UseLogicalOrdering, reachedBoundary);
}
static VisiblePosition endPositionForLine(const VisiblePosition& c, LineEndpointComputationMode mode)
{
if (c.isNull())
return VisiblePosition();
auto line = RenderedPosition(c).line();
if (!line) {
// There are VisiblePositions at offset 0 in blocks without
// RootInlineBoxes, like empty editable blocks and bordered blocks.
Position p = c.deepEquivalent();
if (p.deprecatedNode()->renderer() && p.deprecatedNode()->renderer()->isRenderBlock() && !p.deprecatedEditingOffset())
return c;
return VisiblePosition();
}
Node* endNode = nullptr;
auto endRun = mode == UseLogicalOrdering ? line->logicalEndRunWithNode() : line->lastRun();
// Generated content (e.g. list markers and CSS :before and :after pseudoelements) have no corresponding DOM element,
// and so cannot be represented by a VisiblePosition. Use whatever precedes instead.
while (true) {
if (!endRun)
return VisiblePosition();
endNode = endRun->renderer().nonPseudoNode();
if (endNode)
break;
if (mode == UseLogicalOrdering)
endRun.traversePreviousOnLineInLogicalOrder();
else
endRun.traversePreviousOnLine();
}
Position pos;
if (is<HTMLBRElement>(*endNode))
pos = positionBeforeNode(endNode);
else if (is<InlineIterator::TextBox>(*endRun) && is<Text>(*endNode)) {
auto& endTextRun = downcast<InlineIterator::TextBox>(*endRun);
int endOffset = endTextRun.start();
if (!endTextRun.isLineBreak())
endOffset += endTextRun.length();
pos = Position(downcast<Text>(endNode), endOffset);
} else
pos = positionAfterNode(endNode);
return VisiblePosition(pos, Affinity::Upstream);
}
static bool inSameLogicalLine(const VisiblePosition& a, const VisiblePosition& b)
{
return a.isNotNull() && logicalStartOfLine(a) == logicalStartOfLine(b);
}
static VisiblePosition endOfLine(const VisiblePosition& c, LineEndpointComputationMode mode, bool* reachedBoundary)
{
if (reachedBoundary)
*reachedBoundary = false;
// TODO: this is the current behavior that might need to be fixed.
// Please refer to https://bugs.webkit.org/show_bug.cgi?id=49107 for detail.
VisiblePosition visPos = endPositionForLine(c, mode);
if (mode == UseLogicalOrdering) {
// Make sure the end of line is at the same line as the given input position. For a wrapping line, the logical end
// position for the not-last-2-lines might incorrectly hand back the logical beginning of the next line.
// For example, <div contenteditable dir="rtl" style="line-break:before-white-space">abcdefg abcdefg abcdefg
// a abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg abcdefg </div>
// In this case, use the previous position of the computed logical end position.
if (!inSameLogicalLine(c, visPos))
visPos = visPos.previous();
if (Node* editableRoot = highestEditableRoot(c.deepEquivalent())) {
if (!editableRoot->contains(visPos.deepEquivalent().containerNode())) {
VisiblePosition newPosition = lastPositionInNode(editableRoot);
if (reachedBoundary)
*reachedBoundary = c == newPosition;
return newPosition;
}
}
return c.honorEditingBoundaryAtOrAfter(visPos, reachedBoundary);
}
// Make sure the end of line is at the same line as the given input position. Else use the previous position to
// obtain end of line. This condition happens when the input position is before the space character at the end
// of a soft-wrapped non-editable line. In this scenario, endPositionForLine would incorrectly hand back a position
// in the next line instead. This fix is to account for the discrepancy between lines with webkit-line-break:after-white-space style
// versus lines without that style, which would break before a space by default.
if (!inSameLine(c, visPos)) {
visPos = c.previous();
if (visPos.isNull())
return VisiblePosition();
visPos = endPositionForLine(visPos, UseInlineBoxOrdering);
}
return c.honorEditingBoundaryAtOrAfter(visPos, reachedBoundary);
}
// FIXME: Rename this function to reflect the fact it ignores bidi levels.
VisiblePosition endOfLine(const VisiblePosition& currentPosition)
{
return endOfLine(currentPosition, UseInlineBoxOrdering, nullptr);
}
VisiblePosition logicalEndOfLine(const VisiblePosition& currentPosition, bool* reachedBoundary)
{
return endOfLine(currentPosition, UseLogicalOrdering, reachedBoundary);
}
bool inSameLine(const VisiblePosition& a, const VisiblePosition& b)
{
return a.isNotNull() && startOfLine(a) == startOfLine(b);
}
bool isStartOfLine(const VisiblePosition& p)
{
return p.isNotNull() && p == startOfLine(p);
}
bool isEndOfLine(const VisiblePosition& p)
{
return p.isNotNull() && p == endOfLine(p);
}
bool isLogicalEndOfLine(const VisiblePosition& p)
{
return p.isNotNull() && p == logicalEndOfLine(p);
}
static inline IntPoint absoluteLineDirectionPointToLocalPointInBlock(InlineIterator::LineIterator& line, int lineDirectionPoint)
{
auto& containingBlock = line->containingBlock();
FloatPoint absoluteBlockPoint = containingBlock.localToAbsolute(FloatPoint()) - toFloatSize(containingBlock.scrollPosition());
if (containingBlock.isHorizontalWritingMode())
return IntPoint(lineDirectionPoint - absoluteBlockPoint.x(), line->blockDirectionPointInLine());
return IntPoint(line->blockDirectionPointInLine(), lineDirectionPoint - absoluteBlockPoint.y());
}
static Element* rootEditableOrDocumentElement(Node& node, EditableType editableType)
{
if (hasEditableStyle(node, editableType))
return editableRootForPosition(firstPositionInOrBeforeNode(&node), editableType);
return node.document().documentElement();
}
VisiblePosition previousLinePosition(const VisiblePosition& visiblePosition, int lineDirectionPoint, EditableType editableType)
{
Position p = visiblePosition.deepEquivalent();
Node* node = p.deprecatedNode();
if (!node)
return VisiblePosition();
node->document().updateLayoutIgnorePendingStylesheets();
RenderObject* renderer = node->renderer();
if (!renderer)
return VisiblePosition();
InlineIterator::LineIterator line;
if (auto run = visiblePosition.inlineRunAndOffset().run) {
line = run->line()->previous();
// We want to skip zero height boxes.
// This could happen in case it is a LegacyRootInlineBox with trailing floats.
if (!line || !line->logicalHeight() || !line->firstRun())
line = { };
}
if (!line) {
Position position = previousLineCandidatePosition(node, visiblePosition, editableType);
if (position.isNotNull()) {
RenderedPosition renderedPosition(position);
line = renderedPosition.line();
if (!line)
return position;
}
}
if (line) {
// FIXME: Can be wrong for multi-column layout and with transforms.
auto pointInLine = absoluteLineDirectionPointToLocalPointInBlock(line, lineDirectionPoint);
auto& renderer = line->closestRunForPoint(pointInLine, isEditablePosition(p))->renderer();
Node* node = renderer.node();
if (node && editingIgnoresContent(*node))
return positionInParentBeforeNode(node);
return const_cast<RenderObject&>(renderer).positionForPoint(pointInLine, nullptr);
}
// Could not find a previous line. This means we must already be on the first line.
// Move to the start of the content in this block, which effectively moves us
// to the start of the line we're on.
Element* rootElement = rootEditableOrDocumentElement(*node, editableType);
if (!rootElement)
return VisiblePosition();
return firstPositionInNode(rootElement);
}
VisiblePosition nextLinePosition(const VisiblePosition& visiblePosition, int lineDirectionPoint, EditableType editableType)
{
Position p = visiblePosition.deepEquivalent();
Node* node = p.deprecatedNode();
if (!node)
return VisiblePosition();
node->document().updateLayoutIgnorePendingStylesheets();
if (!node->renderer())
return VisiblePosition();
InlineIterator::LineIterator line;
if (auto run = visiblePosition.inlineRunAndOffset().run) {
line = run->line()->next();
// We want to skip zero height boxes.
// This could happen in case it is a LegacyRootInlineBox with trailing floats.
if (!line || !line->logicalHeight() || !line->firstRun())
line = { };
}
if (!line) {
// FIXME: We need do the same in previousLinePosition.
Node* child = node->traverseToChildAt(p.deprecatedEditingOffset());
node = child ? child : node->lastDescendant();
Position position = nextLineCandidatePosition(node, visiblePosition, editableType);
if (position.isNotNull()) {
RenderedPosition renderedPosition(position);
line = renderedPosition.line();
if (!line)
return position;
}
}
if (line) {
// FIXME: Can be wrong for multi-column layout and with transforms.
auto pointInLine = absoluteLineDirectionPointToLocalPointInBlock(line, lineDirectionPoint);
auto& renderer = line->closestRunForPoint(pointInLine, isEditablePosition(p))->renderer();
Node* node = renderer.node();
if (node && editingIgnoresContent(*node))
return positionInParentBeforeNode(node);
return const_cast<RenderObject&>(renderer).positionForPoint(pointInLine, nullptr);
}
// Could not find a next line. This means we must already be on the last line.
// Move to the end of the content in this block, which effectively moves us
// to the end of the line we're on.
Element* rootElement = rootEditableOrDocumentElement(*node, editableType);
if (!rootElement)
return VisiblePosition();
return lastPositionInNode(rootElement);
}
// ---------
unsigned startSentenceBoundary(StringView text, unsigned, BoundarySearchContextAvailability, bool&)
{
// FIXME: The following function can return -1; we don't handle that.
return ubrk_preceding(sentenceBreakIterator(text), text.length());
}
VisiblePosition startOfSentence(const VisiblePosition& position)
{
return previousBoundary(position, startSentenceBoundary, NeedsContextAtParagraphStart::Yes);
}
unsigned endSentenceBoundary(StringView text, unsigned, BoundarySearchContextAvailability, bool&)
{
return ubrk_next(sentenceBreakIterator(text));
}
VisiblePosition endOfSentence(const VisiblePosition& position)
{
// FIXME: This includes the space after the punctuation that marks the end of the sentence.
return nextBoundary(position, endSentenceBoundary);
}
static unsigned previousSentencePositionBoundary(StringView text, unsigned, BoundarySearchContextAvailability, bool&)
{
// FIXME: This is identical to startSentenceBoundary. I'm pretty sure that's not right.
// FIXME: The following function can return -1; we don't handle that.
return ubrk_preceding(sentenceBreakIterator(text), text.length());
}
VisiblePosition previousSentencePosition(const VisiblePosition& position)
{
return position.honorEditingBoundaryAtOrBefore(previousBoundary(position, previousSentencePositionBoundary));
}
static unsigned nextSentencePositionBoundary(StringView text, unsigned, BoundarySearchContextAvailability, bool&)
{
// FIXME: This is identical to endSentenceBoundary.
// That isn't right. This function needs to move to the equivalent position in the following sentence.
return ubrk_following(sentenceBreakIterator(text), 0);
}
VisiblePosition nextSentencePosition(const VisiblePosition& position)
{
return position.honorEditingBoundaryAtOrAfter(nextBoundary(position, nextSentencePositionBoundary));
}
Node* findStartOfParagraph(Node* startNode, Node* highestRoot, Node* startBlock, int& offset, Position::AnchorType& type, EditingBoundaryCrossingRule boundaryCrossingRule)
{
Node* node = startNode;
Node* n = startNode;
while (n) {
if (boundaryCrossingRule == CannotCrossEditingBoundary && !Position::nodeIsUserSelectAll(n) && n->hasEditableStyle() != startNode->hasEditableStyle())
break;
if (boundaryCrossingRule == CanSkipOverEditingBoundary) {
while (n && n->hasEditableStyle() != startNode->hasEditableStyle())
n = NodeTraversal::previousPostOrder(*n, startBlock);
if (!n || !n->isDescendantOf(highestRoot))
break;
}
RenderObject* r = n->renderer();
if (!r) {
n = NodeTraversal::previousPostOrder(*n, startBlock);
continue;
}
const RenderStyle& style = r->style();
if (style.visibility() != Visibility::Visible) {
n = NodeTraversal::previousPostOrder(*n, startBlock);
continue;
}
if (r->isBR() || isBlock(n))
break;
if (is<RenderText>(*r) && downcast<RenderText>(*r).hasRenderedText()) {
ASSERT_WITH_SECURITY_IMPLICATION(is<Text>(*n));
type = Position::PositionIsOffsetInAnchor;
if (style.preserveNewline()) {
StringImpl& text = downcast<RenderText>(*r).text();
int i = text.length();
int o = offset;
if (n == startNode && o < i)
i = std::max(0, o);
while (--i >= 0) {
if (text[i] == '\n') {
offset = i + 1;
return n;
}
}
}
node = n;
offset = 0;
n = NodeTraversal::previousPostOrder(*n, startBlock);
} else if (editingIgnoresContent(*n) || isRenderedTable(n)) {
node = n;
type = Position::PositionIsBeforeAnchor;
n = n->previousSibling() ? n->previousSibling() : NodeTraversal::previousPostOrder(*n, startBlock);
} else
n = NodeTraversal::previousPostOrder(*n, startBlock);
}
return node;
}
Node* findEndOfParagraph(Node* startNode, Node* highestRoot, Node* stayInsideBlock, int& offset, Position::AnchorType& type, EditingBoundaryCrossingRule boundaryCrossingRule)
{
Node* node = startNode;
Node* n = startNode;
while (n) {
if (boundaryCrossingRule == CannotCrossEditingBoundary && !Position::nodeIsUserSelectAll(n) && n->hasEditableStyle() != startNode->hasEditableStyle())
break;
if (boundaryCrossingRule == CanSkipOverEditingBoundary) {
while (n && n->hasEditableStyle() != startNode->hasEditableStyle())
n = NodeTraversal::next(*n, stayInsideBlock);
if (!n || !n->isDescendantOf(highestRoot))
break;
}
RenderObject* r = n->renderer();
if (!r) {
n = NodeTraversal::next(*n, stayInsideBlock);
continue;
}
const RenderStyle& style = r->style();
if (style.visibility() != Visibility::Visible) {
n = NodeTraversal::next(*n, stayInsideBlock);
continue;
}
// FIXME: This is wrong when startNode is a block. We should return a position after the block.
if (r->isBR() || isBlock(n))
break;
// FIXME: We avoid returning a position where the renderer can't accept the caret.
if (is<RenderText>(*r) && downcast<RenderText>(*r).hasRenderedText()) {
ASSERT_WITH_SECURITY_IMPLICATION(is<Text>(*n));
type = Position::PositionIsOffsetInAnchor;
if (style.preserveNewline()) {
StringImpl& text = downcast<RenderText>(*r).text();
int o = n == startNode ? offset : 0;
int length = text.length();
for (int i = o; i < length; ++i) {
if (text[i] == '\n') {
offset = i;
return n;
}
}
}
node = n;
offset = r->caretMaxOffset();
n = NodeTraversal::next(*n, stayInsideBlock);
} else if (editingIgnoresContent(*n) || isRenderedTable(n)) {
node = n;
type = Position::PositionIsAfterAnchor;
n = NodeTraversal::nextSkippingChildren(*n, stayInsideBlock);
} else
n = NodeTraversal::next(*n, stayInsideBlock);
}
return node;
}
VisiblePosition startOfParagraph(const VisiblePosition& c, EditingBoundaryCrossingRule boundaryCrossingRule)
{
Position p = c.deepEquivalent();
auto* startNode = p.deprecatedNode();
if (!startNode)
return VisiblePosition();
if (isRenderedAsNonInlineTableImageOrHR(startNode))
return positionBeforeNode(startNode);
Node* startBlock = enclosingBlock(startNode);
auto* highestRoot = highestEditableRoot(p);
int offset = p.deprecatedEditingOffset();
Position::AnchorType type = p.anchorType();
auto* node = findStartOfParagraph(startNode, highestRoot, startBlock, offset, type, boundaryCrossingRule);
if (is<Text>(node))
return Position(downcast<Text>(node), offset);
if (type == Position::PositionIsOffsetInAnchor) {
ASSERT(type == Position::PositionIsOffsetInAnchor || !offset);
return Position(node, offset, type);
}
return Position(node, type);
}
VisiblePosition endOfParagraph(const VisiblePosition& c, EditingBoundaryCrossingRule boundaryCrossingRule)
{
if (c.isNull())
return VisiblePosition();
Position p = c.deepEquivalent();
auto* startNode = p.deprecatedNode();
if (isRenderedAsNonInlineTableImageOrHR(startNode))
return positionAfterNode(startNode);
auto* startBlock = enclosingBlock(startNode);
auto* stayInsideBlock = startBlock;
auto* highestRoot = highestEditableRoot(p);
int offset = p.deprecatedEditingOffset();
Position::AnchorType type = p.anchorType();
auto* node = findEndOfParagraph(startNode, highestRoot, stayInsideBlock, offset, type, boundaryCrossingRule);
if (is<Text>(node))
return Position(downcast<Text>(node), offset);
if (type == Position::PositionIsOffsetInAnchor)
return Position(node, offset, type);
return Position(node, type);
}
// FIXME: isStartOfParagraph(startOfNextParagraph(pos)) is not always true
VisiblePosition startOfNextParagraph(const VisiblePosition& visiblePosition)
{
VisiblePosition paragraphEnd(endOfParagraph(visiblePosition, CanSkipOverEditingBoundary));
VisiblePosition afterParagraphEnd(paragraphEnd.next(CannotCrossEditingBoundary));
// The position after the last position in the last cell of a table
// is not the start of the next paragraph.
if (isFirstPositionAfterTable(afterParagraphEnd))
return afterParagraphEnd.next(CannotCrossEditingBoundary);
return afterParagraphEnd;
}
bool inSameParagraph(const VisiblePosition& a, const VisiblePosition& b, EditingBoundaryCrossingRule boundaryCrossingRule)
{
return a.isNotNull() && startOfParagraph(a, boundaryCrossingRule) == startOfParagraph(b, boundaryCrossingRule);
}
bool isStartOfParagraph(const VisiblePosition& pos, EditingBoundaryCrossingRule boundaryCrossingRule)
{
return pos.isNotNull() && pos == startOfParagraph(pos, boundaryCrossingRule);
}
bool isEndOfParagraph(const VisiblePosition& pos, EditingBoundaryCrossingRule boundaryCrossingRule)
{
return pos.isNotNull() && pos == endOfParagraph(pos, boundaryCrossingRule);
}
bool isBlankParagraph(const VisiblePosition& position)
{
return isStartOfParagraph(position) && startOfParagraph(position.next()) != startOfParagraph(position);
}
VisiblePosition previousParagraphPosition(const VisiblePosition& p, int x)
{
VisiblePosition pos = p;
do {
VisiblePosition n = previousLinePosition(pos, x);
if (n.isNull() || n == pos)
break;
pos = n;
} while (inSameParagraph(p, pos));
return pos;
}
VisiblePosition nextParagraphPosition(const VisiblePosition& p, int x)
{
VisiblePosition pos = p;
do {
VisiblePosition n = nextLinePosition(pos, x);
if (n.isNull() || n == pos)
break;
pos = n;
} while (inSameParagraph(p, pos));
return pos;
}
// ---------
VisiblePosition startOfBlock(const VisiblePosition& visiblePosition, EditingBoundaryCrossingRule rule)
{
Position position = visiblePosition.deepEquivalent();
Node* startBlock;
if (!position.containerNode() || !(startBlock = enclosingBlock(position.containerNode(), rule)))
return VisiblePosition();
return firstPositionInNode(startBlock);
}
VisiblePosition endOfBlock(const VisiblePosition& visiblePosition, EditingBoundaryCrossingRule rule)
{
Position position = visiblePosition.deepEquivalent();
Node* endBlock;
if (!position.containerNode() || !(endBlock = enclosingBlock(position.containerNode(), rule)))
return VisiblePosition();
return lastPositionInNode(endBlock);
}
bool inSameBlock(const VisiblePosition& a, const VisiblePosition& b)
{
return !a.isNull() && enclosingBlock(a.deepEquivalent().containerNode()) == enclosingBlock(b.deepEquivalent().containerNode());
}
bool isStartOfBlock(const VisiblePosition& pos)
{
return pos.isNotNull() && pos == startOfBlock(pos, CanCrossEditingBoundary);
}
bool isEndOfBlock(const VisiblePosition& pos)
{
return pos.isNotNull() && pos == endOfBlock(pos, CanCrossEditingBoundary);
}
// ---------
VisiblePosition startOfDocument(const Node* node)
{
if (!node || !node->document().documentElement())
return VisiblePosition();
// The canonicalization of the position at (documentElement, 0) can turn the visible
// position to null, even when there's a valid candidate to be had, because the root HTML element
// is not content editable. So we construct directly from the valid candidate.
Position firstCandidate = nextCandidate(makeDeprecatedLegacyPosition(node->document().documentElement(), 0));
if (firstCandidate.isNull())
return VisiblePosition();
return VisiblePosition(firstCandidate);
}
VisiblePosition startOfDocument(const VisiblePosition& c)
{
return startOfDocument(c.deepEquivalent().deprecatedNode());
}
VisiblePosition endOfDocument(const Node* node)
{
if (!node || !node->document().documentElement())
return VisiblePosition();
// (As above, in startOfDocument.) The canonicalization can reject valid visible positions
// when descending from the root element, so we construct the visible position directly from a
// valid candidate.
Position lastPosition = makeDeprecatedLegacyPosition(node->document().documentElement(), node->document().documentElement()->countChildNodes());
Position lastCandidate = previousCandidate(lastPosition);
if (lastCandidate.isNull())
return VisiblePosition();
return VisiblePosition(lastCandidate);
}
VisiblePosition endOfDocument(const VisiblePosition& c)
{
return endOfDocument(c.deepEquivalent().deprecatedNode());
}
bool inSameDocument(const VisiblePosition& a, const VisiblePosition& b)
{
Position ap = a.deepEquivalent();
Node* an = ap.deprecatedNode();
if (!an)
return false;
Position bp = b.deepEquivalent();
Node* bn = bp.deprecatedNode();
if (an == bn)
return true;
return &an->document() == &bn->document();
}
bool isStartOfDocument(const VisiblePosition& p)
{
return p.isNotNull() && p.previous(CanCrossEditingBoundary).isNull();
}
bool isEndOfDocument(const VisiblePosition& p)
{
return p.isNotNull() && p.next(CanCrossEditingBoundary).isNull();
}
// ---------
VisiblePosition startOfEditableContent(const VisiblePosition& visiblePosition)
{
auto* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent());
if (!highestRoot)
return { };
return firstPositionInNode(highestRoot);
}
VisiblePosition endOfEditableContent(const VisiblePosition& visiblePosition)
{
auto* highestRoot = highestEditableRoot(visiblePosition.deepEquivalent());
if (!highestRoot)
return { };
return lastPositionInNode(highestRoot);
}
bool isEndOfEditableOrNonEditableContent(const VisiblePosition& p)
{
return p.isNotNull() && p.next().isNull();
}
VisiblePosition leftBoundaryOfLine(const VisiblePosition& c, TextDirection direction, bool* reachedBoundary)
{
return direction == TextDirection::LTR ? logicalStartOfLine(c, reachedBoundary) : logicalEndOfLine(c, reachedBoundary);
}
VisiblePosition rightBoundaryOfLine(const VisiblePosition& c, TextDirection direction, bool* reachedBoundary)
{
return direction == TextDirection::LTR ? logicalEndOfLine(c, reachedBoundary) : logicalStartOfLine(c, reachedBoundary);
}
static bool directionIsDownstream(SelectionDirection direction)
{
if (direction == SelectionDirection::Backward)
return false;
if (direction == SelectionDirection::Forward)
return true;
// FIXME: this code doesn't take into account the original direction of the element.
// I'm not fixing this now because I'm afraid there is some code in UIKit relying on
// this wrong behavior.
return direction == SelectionDirection::Right;
}
bool atBoundaryOfGranularity(const VisiblePosition& vp, TextGranularity granularity, SelectionDirection direction)
{
if (granularity == TextGranularity::CharacterGranularity)
return true;
VisiblePosition boundary;
bool useDownstream = directionIsDownstream(direction);
switch (granularity) {
case TextGranularity::WordGranularity:
// visible_units claims erroneously that the start and the end
// of a paragraph are the end and start of a word, respectively.
if ((useDownstream && isStartOfParagraph(vp)) || (!useDownstream && isEndOfParagraph(vp)))
return false;
// Note that "Left" and "Right" in this context apparently mean "upstream/previous" and "downstream/next".
boundary = useDownstream ? endOfWord(vp, LeftWordIfOnBoundary) : startOfWord(vp, RightWordIfOnBoundary);
break;
case TextGranularity::SentenceGranularity:
boundary = useDownstream ? endOfSentence(previousSentencePosition(vp)) : startOfSentence(nextSentencePosition(vp));
break;
case TextGranularity::LineGranularity:
boundary = vp;
boundary.setAffinity(useDownstream ? Affinity::Upstream : Affinity::Downstream);
boundary = useDownstream ? endOfLine(boundary) : startOfLine(boundary);
break;
case TextGranularity::ParagraphGranularity:
boundary = useDownstream ? endOfParagraph(vp) : startOfParagraph(vp);
break;
case TextGranularity::DocumentGranularity:
boundary = useDownstream ? endOfDocument(vp) : startOfDocument(vp);
break;
default:
ASSERT_NOT_REACHED();
break;
}
return vp == boundary;
}
bool withinTextUnitOfGranularity(const VisiblePosition& vp, TextGranularity granularity, SelectionDirection direction)
{
if (granularity == TextGranularity::CharacterGranularity || granularity == TextGranularity::DocumentGranularity)
return true;
bool useDownstream = directionIsDownstream(direction);
VisiblePosition prevBoundary;
VisiblePosition nextBoundary;
switch (granularity) {
case TextGranularity::WordGranularity:
// Note that "Left" and "Right" in this context apparently mean "upstream/previous" and "downstream/next".
prevBoundary = startOfWord(vp, (useDownstream ? RightWordIfOnBoundary : LeftWordIfOnBoundary));
nextBoundary = endOfWord(vp, (useDownstream ? RightWordIfOnBoundary : LeftWordIfOnBoundary));
// Workaround for <rdar://problem/7259611> Word boundary code on iPhone gives different results than desktop
if (endOfWord(prevBoundary, RightWordIfOnBoundary) != nextBoundary)
return false;
break;
case TextGranularity::SentenceGranularity:
prevBoundary = startOfSentence(vp);
nextBoundary = endOfSentence(vp);
break;
case TextGranularity::LineGranularity:
prevBoundary = startOfLine(vp);
nextBoundary = endOfLine(vp);
if (prevBoundary == nextBoundary) {
nextBoundary = nextLinePosition(nextBoundary, 0);
nextBoundary.setAffinity(Affinity::Upstream);
if (!inSameLine(prevBoundary, nextBoundary))
nextBoundary = vp.next();
}
break;
case TextGranularity::ParagraphGranularity:
prevBoundary = startOfParagraph(vp);
nextBoundary = endOfParagraph(vp);
break;
default:
ASSERT_NOT_REACHED();
break;
}
if (prevBoundary == nextBoundary)
return false;
if (vp == prevBoundary)
return useDownstream;
if (vp == nextBoundary)
return !useDownstream;
return (prevBoundary < vp && vp < nextBoundary);
}
static VisiblePosition nextCharacterBoundaryInDirection(const VisiblePosition& vp, SelectionDirection direction, EditingBoundaryCrossingRule rule)
{
return directionIsDownstream(direction) ? vp.next(rule) : vp.previous(rule);
}
static VisiblePosition nextWordBoundaryInDirection(const VisiblePosition& vp, SelectionDirection direction)
{
bool useDownstream = directionIsDownstream(direction);
bool withinUnitOfGranularity = withinTextUnitOfGranularity(vp, TextGranularity::WordGranularity, direction);
VisiblePosition result;
if (useDownstream) {
if (withinUnitOfGranularity)
result = endOfWord(vp, RightWordIfOnBoundary);
else {
VisiblePosition start = startOfWord(vp, RightWordIfOnBoundary);
if (start > vp && start != endOfWord(start))
result = start;
else {
// Do same thing as backwards traveling below.
start = vp;
while (true) {
result = startOfWord(nextWordPosition(start), RightWordIfOnBoundary);
if (result == start)
break;
// We failed to find a word boundary.
if (result.isNull() || result < start)
return VisiblePosition();
// We consider successs also the case where start is before element and result is after.
// This covers moving past images like words.
if (result != endOfWord(result)
|| (result.deepEquivalent().anchorNode() == start.deepEquivalent().anchorNode()
&& result.deepEquivalent().anchorType() == Position::PositionIsAfterAnchor
&& start.deepEquivalent().anchorType() == Position::PositionIsBeforeAnchor))
break;
start = result;
}
}
}
} else {
if (withinUnitOfGranularity)
result = startOfWord(vp, LeftWordIfOnBoundary);
else {
// This is complicated because:
// When given "Blah blah.|", endOfWord is "Blah blah|.", and previousWordPosition is "Blah| blah."
// When given "Blah blah. |", endOfWord is "Blah blah.| ", and previousWordPosition is "Blah |blah. ".
VisiblePosition end = endOfWord(vp, LeftWordIfOnBoundary);
if (end < vp && end != startOfWord(end))
result = end;
else {
end = vp;
while (true) {
result = endOfWord(previousWordPosition(end), RightWordIfOnBoundary);
if (result == end)
break;
if (result.isNull() || result > end)
return VisiblePosition();
if (result != startOfWord(result))
break;
end = result;
}
}
}
}
if (result == vp)
return VisiblePosition();
return result;
}
static VisiblePosition nextSentenceBoundaryInDirection(const VisiblePosition& vp, SelectionDirection direction)
{
bool useDownstream = directionIsDownstream(direction);
bool withinUnitOfGranularity = withinTextUnitOfGranularity(vp, TextGranularity::SentenceGranularity, direction);
VisiblePosition result;
if (withinUnitOfGranularity)
result = useDownstream ? endOfSentence(vp) : startOfSentence(vp);
else {
result = useDownstream ? nextSentencePosition(vp) : previousSentencePosition(vp);
if (result.isNull() || result == vp)
return VisiblePosition();
result = useDownstream ? startOfSentence(vp) : endOfSentence(vp);
}
if (result == vp)
return VisiblePosition();
// Positions can only be compared if they are in the same tree scope.
ASSERT_IMPLIES(areVisiblePositionsInSameTreeScope(result, vp), useDownstream ? (result > vp) : (result < vp));
return result;
}
static VisiblePosition nextLineBoundaryInDirection(const VisiblePosition& vp, SelectionDirection direction)
{
bool useDownstream = directionIsDownstream(direction);
VisiblePosition result = vp;
if (useDownstream) {
result.setAffinity(Affinity::Downstream);
result = isEndOfLine(result) ? startOfLine(nextLinePosition(result, result.lineDirectionPointForBlockDirectionNavigation())) : endOfLine(result);
} else {
result.setAffinity(Affinity::Upstream);
result = isStartOfLine(result) ? endOfLine(previousLinePosition(result, result.lineDirectionPointForBlockDirectionNavigation())) : startOfLine(result);
}
return result;
}
static VisiblePosition nextParagraphBoundaryInDirection(const VisiblePosition& position, SelectionDirection direction)
{
auto useDownstream = directionIsDownstream(direction);
auto lineDirection = position.lineDirectionPointForBlockDirectionNavigation();
if (atBoundaryOfGranularity(position, TextGranularity::ParagraphGranularity, direction))
return useDownstream ? startOfParagraph(nextParagraphPosition(position, lineDirection)) : endOfParagraph(previousParagraphPosition(position, lineDirection));
ASSERT(withinTextUnitOfGranularity(position, TextGranularity::ParagraphGranularity, direction));
return useDownstream ? endOfParagraph(position) : startOfParagraph(position);
}
static VisiblePosition nextDocumentBoundaryInDirection(const VisiblePosition& vp, SelectionDirection direction)
{
return directionIsDownstream(direction) ? endOfDocument(vp) : startOfDocument(vp);
}
VisiblePosition positionOfNextBoundaryOfGranularity(const VisiblePosition& vp, TextGranularity granularity, SelectionDirection direction)
{
switch (granularity) {
case TextGranularity::CharacterGranularity:
return nextCharacterBoundaryInDirection(vp, direction, CanCrossEditingBoundary);
case TextGranularity::WordGranularity:
return nextWordBoundaryInDirection(vp, direction);
case TextGranularity::SentenceGranularity:
return nextSentenceBoundaryInDirection(vp, direction);
case TextGranularity::LineGranularity:
return nextLineBoundaryInDirection(vp, direction);
case TextGranularity::ParagraphGranularity:
return nextParagraphBoundaryInDirection(vp, direction);
case TextGranularity::DocumentGranularity:
return nextDocumentBoundaryInDirection(vp, direction);
default:
ASSERT_NOT_REACHED();
return VisiblePosition();
}
}
std::optional<SimpleRange> enclosingTextUnitOfGranularity(const VisiblePosition& vp, TextGranularity granularity, SelectionDirection direction)
{
// This is particularly inefficient. We could easily obtain the answer with the boundaries computed below.
if (!withinTextUnitOfGranularity(vp, granularity, direction))
return std::nullopt;
VisiblePosition prevBoundary;
VisiblePosition nextBoundary;
bool useDownstream = directionIsDownstream(direction);
switch (granularity) {
case TextGranularity::CharacterGranularity:
prevBoundary = vp;
nextBoundary = prevBoundary.next();
break;
case TextGranularity::WordGranularity:
// NB: "Left" and "Right" in this context apparently mean "upstream/previous" and "downstream/next".
if (useDownstream) {
prevBoundary = startOfWord(vp, RightWordIfOnBoundary);
nextBoundary = endOfWord(vp, RightWordIfOnBoundary);
} else {
prevBoundary = startOfWord(vp, LeftWordIfOnBoundary);
nextBoundary = endOfWord(vp, LeftWordIfOnBoundary);
}
break;
case TextGranularity::SentenceGranularity:
prevBoundary = startOfSentence(vp);
nextBoundary = endOfSentence(vp);
break;
case TextGranularity::LineGranularity:
prevBoundary = startOfLine(vp);
nextBoundary = endOfLine(vp);
if (prevBoundary == nextBoundary) {
nextBoundary = nextLinePosition(nextBoundary, 0);
nextBoundary.setAffinity(Affinity::Upstream);
if (!inSameLine(prevBoundary, nextBoundary))
nextBoundary = vp.next();
}
break;
case TextGranularity::ParagraphGranularity:
prevBoundary = startOfParagraph(vp);
nextBoundary = endOfParagraph(vp);
break;
case TextGranularity::DocumentGranularity:
prevBoundary = startOfDocument(vp);
nextBoundary = endOfDocument(vp);
break;
default:
ASSERT_NOT_REACHED();
return std::nullopt;
}
if (prevBoundary.isNull() || nextBoundary.isNull())
return std::nullopt;
if (vp < prevBoundary || vp > nextBoundary)
return std::nullopt;
return makeSimpleRange(prevBoundary, nextBoundary);
}
std::ptrdiff_t distanceBetweenPositions(const VisiblePosition& a, const VisiblePosition& b)
{
if (a.isNull() || b.isNull())
return 0;
return a < b ? -characterCount(*makeSimpleRange(a, b)) : characterCount(*makeSimpleRange(b, a));
}
void charactersAroundPosition(const VisiblePosition& position, UChar32& oneAfter, UChar32& oneBefore, UChar32& twoBefore)
{
const int maxCharacters = 3;
UChar32 characters[maxCharacters] = { 0 };
if (position.isNull() || isStartOfDocument(position))
return;
VisiblePosition startPosition = position;
VisiblePosition endPosition = position;
VisiblePosition nextPosition = nextCharacterBoundaryInDirection(position, SelectionDirection::Forward, CannotCrossEditingBoundary);
if (nextPosition.isNotNull())
endPosition = nextPosition;
VisiblePosition previousPosition = nextCharacterBoundaryInDirection(position, SelectionDirection::Backward, CannotCrossEditingBoundary);
if (previousPosition.isNotNull()) {
startPosition = previousPosition;
previousPosition = nextCharacterBoundaryInDirection(previousPosition, SelectionDirection::Backward, CannotCrossEditingBoundary);
if (previousPosition.isNotNull())
startPosition = previousPosition;
}
if (startPosition != endPosition) {
String characterString = plainText(*makeSimpleRange(startPosition, endPosition)).replace(noBreakSpace, ' ');
for (int i = characterString.length() - 1, index = 0; i >= 0 && index < maxCharacters; --i) {
if (!index && nextPosition.isNull())
index++;
characters[index++] = characterString[i];
}
}
oneAfter = characters[0];
oneBefore = characters[1];
twoBefore = characters[2];
}
std::optional<SimpleRange> wordRangeFromPosition(const VisiblePosition& position)
{
if (position.isNull())
return std::nullopt;
if (auto range = enclosingTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Backward))
return range;
if (auto range = enclosingTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward))
return range;
VisiblePosition currentPosition = position;
do {
currentPosition = positionOfNextBoundaryOfGranularity(currentPosition, TextGranularity::WordGranularity, SelectionDirection::Backward);
} while (currentPosition.isNotNull() && !atBoundaryOfGranularity(currentPosition, TextGranularity::WordGranularity, SelectionDirection::Backward));
if (currentPosition.isNull())
currentPosition = positionOfNextBoundaryOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward);
return makeSimpleRange(currentPosition, position);
}
VisiblePosition closestWordBoundaryForPosition(const VisiblePosition& position)
{
VisiblePosition result;
if (atBoundaryOfGranularity(position, TextGranularity::LineGranularity, SelectionDirection::Forward)) {
// Don't cross line boundaries.
result = position;
} else if (withinTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward)) {
// The position lies within a word.
if (auto wordRange = enclosingTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward)) {
result = makeDeprecatedLegacyPosition(wordRange->start);
if (distanceBetweenPositions(position, result) > 1)
result = makeDeprecatedLegacyPosition(wordRange->end);
}
} else if (atBoundaryOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Backward)) {
// The position is at the end of a word.
result = position;
} else {
// The position is not within a word.
// Go to the next boundary.
result = positionOfNextBoundaryOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward);
// If there is no such boundary we go to the end of the element.
if (result.isNull())
result = endOfEditableContent(position);
}
return result;
}
std::optional<SimpleRange> rangeExpandedByCharactersInDirectionAtWordBoundary(const VisiblePosition& position, int numberOfCharactersToExpand, SelectionDirection direction)
{
Position start = position.deepEquivalent();
Position end = position.deepEquivalent();
for (int i = 0; i < numberOfCharactersToExpand; ++i) {
if (direction == SelectionDirection::Backward)
start = start.previous(Character);
else
end = end.next(Character);
}
if (direction == SelectionDirection::Backward && !atBoundaryOfGranularity(start, TextGranularity::WordGranularity, SelectionDirection::Backward))
start = startOfWord(start).deepEquivalent();
if (direction == SelectionDirection::Forward && !atBoundaryOfGranularity(end, TextGranularity::WordGranularity, SelectionDirection::Forward))
end = endOfWord(end).deepEquivalent();
return makeSimpleRange(start, end);
}
std::optional<SimpleRange> rangeExpandedAroundPositionByCharacters(const VisiblePosition& position, int numberOfCharactersToExpand)
{
Position start = position.deepEquivalent();
Position end = position.deepEquivalent();
for (int i = 0; i < numberOfCharactersToExpand; ++i) {
start = start.previous(Character);
end = end.next(Character);
}
return makeSimpleRange(start, end);
}
std::pair<VisiblePosition, WithinWordBoundary> wordBoundaryForPositionWithoutCrossingLine(const VisiblePosition& position)
{
if (atBoundaryOfGranularity(position, TextGranularity::LineGranularity, SelectionDirection::Forward))
return { position, WithinWordBoundary::No };
if (withinTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward)) {
auto adjustedPosition = position;
if (auto wordRange = enclosingTextUnitOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward)) {
adjustedPosition = makeDeprecatedLegacyPosition(wordRange->start);
if (distanceBetweenPositions(position, adjustedPosition) > 1)
adjustedPosition = makeDeprecatedLegacyPosition(wordRange->end);
}
return { adjustedPosition, WithinWordBoundary::Yes };
}
if (atBoundaryOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Backward))
return { position, WithinWordBoundary::No };
auto nextWordBoundary = positionOfNextBoundaryOfGranularity(position, TextGranularity::WordGranularity, SelectionDirection::Forward);
return { nextWordBoundary.isNotNull() ? nextWordBoundary : endOfEditableContent(position), WithinWordBoundary::No };
}
}