| /* |
| * Copyright (C) 2004-2025 Apple Inc. All rights reserved. |
| * Copyright (C) 2015-2018 Google 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 "Position.h" |
| |
| #include "BoundaryPoint.h" |
| #include "CSSComputedStyleDeclaration.h" |
| #include "ContainerNodeInlines.h" |
| #include "EditingInlines.h" |
| #include "ElementInlines.h" |
| #include "HTMLBRElement.h" |
| #include "HTMLBodyElement.h" |
| #include "HTMLHtmlElement.h" |
| #include "HTMLNames.h" |
| #include "HTMLTableElement.h" |
| #include "InlineIteratorLineBox.h" |
| #include "InlineIteratorLogicalOrderTraversal.h" |
| #include "InlineIteratorTextBox.h" |
| #include "InlineRunAndOffset.h" |
| #include "Logging.h" |
| #include "NodeTraversal.h" |
| #include "PositionIterator.h" |
| #include "RenderBlock.h" |
| #include "RenderBlockFlow.h" |
| #include "RenderBoxInlines.h" |
| #include "RenderFlexibleBox.h" |
| #include "RenderGrid.h" |
| #include "RenderInline.h" |
| #include "RenderIterator.h" |
| #include "RenderLineBreak.h" |
| #include "RenderObjectInlines.h" |
| #include "RenderText.h" |
| #include "SVGElementTypeHelpers.h" |
| #include "SVGTextElement.h" |
| #include "Text.h" |
| #include "TextIterator.h" |
| #include "VisiblePosition.h" |
| #include "VisibleUnits.h" |
| #include <stdio.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/text/CString.h> |
| #include <wtf/text/MakeString.h> |
| #include <wtf/text/TextStream.h> |
| #include <wtf/unicode/CharacterNames.h> |
| |
| #if ENABLE(TREE_DEBUGGING) |
| #include <wtf/text/StringBuilder.h> |
| #endif |
| |
| namespace WebCore { |
| |
| using namespace HTMLNames; |
| |
| static bool hasInlineRun(RenderObject& renderer) |
| { |
| if (auto* renderBox = dynamicDowncast<RenderBox>(renderer); renderBox && InlineIterator::boxFor(*renderBox)) |
| return true; |
| if (auto* renderText = dynamicDowncast<RenderText>(renderer); renderText && InlineIterator::lineLeftmostTextBoxFor(*renderText)) |
| return true; |
| if (auto* renderLineBreak = dynamicDowncast<RenderLineBreak>(renderer); renderLineBreak && InlineIterator::boxFor(*renderLineBreak)) |
| return true; |
| return false; |
| } |
| |
| static Node* nextRenderedEditable(SUPPRESS_UNCHECKED_LOCAL Node* node) |
| { |
| while ((node = nextLeafNode(node))) { |
| CheckedPtr renderer = node->renderer(); |
| if (!renderer || !node->hasEditableStyle()) |
| continue; |
| if (hasInlineRun(*renderer)) |
| return node; |
| } |
| return nullptr; |
| } |
| |
| static Node* previousRenderedEditable(SUPPRESS_UNCHECKED_LOCAL Node* node) |
| { |
| while ((node = previousLeafNode(node))) { |
| CheckedPtr renderer = node->renderer(); |
| if (!renderer || !node->hasEditableStyle()) |
| continue; |
| if (hasInlineRun(*renderer)) |
| return node; |
| } |
| return nullptr; |
| } |
| |
| Position::Position(RefPtr<Node>&& anchorNode, unsigned offset, LegacyEditingPositionFlag) |
| : m_anchorNode(WTF::move(anchorNode)) |
| , m_offset(offset) |
| , m_anchorType(anchorTypeForLegacyEditingPosition(m_anchorNode.get(), m_offset)) |
| , m_isLegacyEditingPosition(true) |
| { |
| ASSERT(!m_anchorNode || !m_anchorNode->isShadowRoot() || m_anchorNode == containerNode()); |
| ASSERT(!m_anchorNode || !m_anchorNode->isPseudoElement()); |
| } |
| |
| Position::Position(RefPtr<Node>&& anchorNode, AnchorType anchorType) |
| : m_anchorNode(WTF::move(anchorNode)) |
| , m_offset(0) |
| , m_anchorType(anchorType) |
| , m_isLegacyEditingPosition(false) |
| { |
| ASSERT(!m_anchorNode || !m_anchorNode->isShadowRoot() || m_anchorNode == containerNode()); |
| ASSERT(!m_anchorNode || !m_anchorNode->isPseudoElement()); |
| ASSERT(anchorType != PositionIsOffsetInAnchor); |
| ASSERT(!((anchorType == PositionIsBeforeChildren || anchorType == PositionIsAfterChildren) |
| && (is<Text>(*m_anchorNode) || editingIgnoresContent(*m_anchorNode)))); |
| } |
| |
| Position::Position(RefPtr<Node>&& anchorNode, unsigned offset, AnchorType anchorType) |
| : m_anchorNode(WTF::move(anchorNode)) |
| , m_offset(offset) |
| , m_anchorType(anchorType) |
| , m_isLegacyEditingPosition(false) |
| { |
| ASSERT(anchorType == PositionIsOffsetInAnchor); |
| } |
| |
| Position::Position(RefPtr<Text>&& textNode, unsigned offset) |
| : m_anchorNode(WTF::move(textNode)) |
| , m_offset(offset) |
| , m_anchorType(PositionIsOffsetInAnchor) |
| , m_isLegacyEditingPosition(false) |
| { |
| ASSERT(m_anchorNode); |
| } |
| |
| void Position::moveToPosition(Node* node, unsigned offset) |
| { |
| ASSERT(!editingIgnoresContent(*node)); |
| ASSERT(anchorType() == PositionIsOffsetInAnchor || m_isLegacyEditingPosition); |
| m_anchorNode = node; |
| m_offset = offset; |
| if (m_isLegacyEditingPosition) |
| m_anchorType = anchorTypeForLegacyEditingPosition(m_anchorNode.get(), m_offset); |
| } |
| |
| void Position::moveToOffset(unsigned offset) |
| { |
| ASSERT(anchorType() == PositionIsOffsetInAnchor || m_isLegacyEditingPosition); |
| m_offset = offset; |
| if (m_isLegacyEditingPosition) |
| m_anchorType = anchorTypeForLegacyEditingPosition(m_anchorNode.get(), m_offset); |
| } |
| |
| Node* Position::containerNode() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| |
| switch (anchorType()) { |
| case PositionIsBeforeChildren: |
| case PositionIsAfterChildren: |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode.get(); |
| case PositionIsBeforeAnchor: |
| case PositionIsAfterAnchor: |
| return m_anchorNode->parentNode(); |
| } |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| Text* Position::containerText() const |
| { |
| switch (anchorType()) { |
| case PositionIsOffsetInAnchor: |
| return dynamicDowncast<Text>(m_anchorNode.get()); |
| case PositionIsBeforeAnchor: |
| case PositionIsAfterAnchor: |
| return nullptr; |
| case PositionIsBeforeChildren: |
| case PositionIsAfterChildren: |
| ASSERT(!m_anchorNode || !is<Text>(*m_anchorNode)); |
| return nullptr; |
| } |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| RefPtr<Text> Position::protectedContainerText() const |
| { |
| return containerText(); |
| } |
| |
| Element* Position::containerOrParentElement() const |
| { |
| auto* container = containerNode(); |
| if (!container) |
| return nullptr; |
| if (auto* element = dynamicDowncast<Element>(*container)) |
| return element; |
| return container->parentElement(); |
| } |
| |
| int Position::computeOffsetInContainerNode() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| |
| switch (anchorType()) { |
| case PositionIsBeforeChildren: |
| return 0; |
| case PositionIsAfterChildren: |
| return m_anchorNode->length(); |
| case PositionIsOffsetInAnchor: |
| return m_offset; |
| case PositionIsBeforeAnchor: |
| return m_anchorNode->computeNodeIndex(); |
| case PositionIsAfterAnchor: |
| return m_anchorNode->computeNodeIndex() + 1; |
| } |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| int Position::offsetForPositionAfterAnchor() const |
| { |
| ASSERT(m_anchorType == PositionIsAfterAnchor || m_anchorType == PositionIsAfterChildren); |
| ASSERT(!m_isLegacyEditingPosition); |
| RefPtr anchorNode = this->anchorNode(); |
| ASSERT(anchorNode); |
| return anchorNode ? lastOffsetForEditing(*anchorNode) : 0; |
| } |
| |
| // Neighbor-anchored positions are invalid DOM positions, so they need to be |
| // fixed up before handing them off to the Range object. |
| Position Position::parentAnchoredEquivalent() const |
| { |
| RefPtr anchorNode = this->anchorNode(); |
| if (!anchorNode) |
| return { }; |
| |
| // FIXME: This should only be necessary for legacy positions, but is also needed for positions before and after Tables |
| if (!m_offset && (m_anchorType != PositionIsAfterAnchor && m_anchorType != PositionIsAfterChildren)) { |
| if (anchorNode->parentNode() && (editingIgnoresContent(*anchorNode) || isRenderedTable(anchorNode.get()))) |
| return positionInParentBeforeNode(anchorNode.get()); |
| return Position(anchorNode.get(), 0, PositionIsOffsetInAnchor); |
| } |
| |
| if (!anchorNode->isCharacterDataNode() |
| && (m_anchorType == PositionIsAfterAnchor || m_anchorType == PositionIsAfterChildren || static_cast<unsigned>(m_offset) == anchorNode->countChildNodes()) |
| && (editingIgnoresContent(*anchorNode) || isRenderedTable(anchorNode.get())) |
| && containerNode()) { |
| return positionInParentAfterNode(anchorNode.get()); |
| } |
| |
| return { containerNode(), static_cast<unsigned>(computeOffsetInContainerNode()), PositionIsOffsetInAnchor }; |
| } |
| |
| RefPtr<Node> Position::firstNode() const |
| { |
| RefPtr container { containerNode() }; |
| if (!container) |
| return nullptr; |
| if (is<CharacterData>(*container)) |
| return container; |
| if (auto* node = computeNodeAfterPosition()) |
| return node; |
| if (!computeOffsetInContainerNode()) |
| return container; |
| return NodeTraversal::nextSkippingChildren(*container); |
| } |
| |
| Node* Position::computeNodeBeforePosition() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| |
| switch (anchorType()) { |
| case PositionIsBeforeChildren: |
| return nullptr; |
| case PositionIsAfterChildren: |
| return m_anchorNode->lastChild(); |
| case PositionIsOffsetInAnchor: |
| return m_offset ? m_anchorNode->traverseToChildAt(m_offset - 1) : nullptr; |
| case PositionIsBeforeAnchor: |
| return m_anchorNode->previousSibling(); |
| case PositionIsAfterAnchor: |
| return m_anchorNode.get(); |
| } |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| Node* Position::computeNodeAfterPosition() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| |
| switch (anchorType()) { |
| case PositionIsBeforeChildren: |
| return m_anchorNode->firstChild(); |
| case PositionIsAfterChildren: |
| return nullptr; |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode->traverseToChildAt(m_offset); |
| case PositionIsBeforeAnchor: |
| return m_anchorNode.get(); |
| case PositionIsAfterAnchor: |
| return m_anchorNode->nextSibling(); |
| } |
| ASSERT_NOT_REACHED(); |
| return nullptr; |
| } |
| |
| Position::AnchorType Position::anchorTypeForLegacyEditingPosition(Node* anchorNode, unsigned offset) |
| { |
| if (anchorNode && editingIgnoresContent(*anchorNode)) { |
| if (offset == 0) |
| return Position::PositionIsBeforeAnchor; |
| return Position::PositionIsAfterAnchor; |
| } |
| return Position::PositionIsOffsetInAnchor; |
| } |
| |
| // FIXME: This method is confusing (does it return anchorNode() or containerNode()?) and should be renamed or removed |
| RefPtr<Element> Position::anchorElementAncestor() const |
| { |
| for (RefPtr node = anchorNode(); node; node = node->parentNode()) { |
| if (auto* element = dynamicDowncast<Element>(*node)) |
| return element; |
| } |
| return nullptr; |
| } |
| |
| Position Position::previous(PositionMoveType moveType) const |
| { |
| RefPtr node = deprecatedNode(); |
| if (!node) |
| return *this; |
| |
| // FIXME: Negative offsets shouldn't be allowed. We should catch this earlier. |
| ASSERT(deprecatedEditingOffset() >= 0); |
| |
| unsigned offset = deprecatedEditingOffset(); |
| |
| if (anchorType() == PositionIsBeforeAnchor) { |
| node = containerNode(); |
| if (!node) |
| return *this; |
| |
| offset = computeOffsetInContainerNode(); |
| } |
| |
| if (offset > 0) { |
| if (RefPtr child = node->traverseToChildAt(offset - 1)) |
| return lastPositionInOrAfterNode(child.get()); |
| |
| // There are two reasons child might be 0: |
| // 1) The node is node like a text node that is not an element, and therefore has no children. |
| // Going backward one character at a time is correct. |
| // 2) The old offset was a bogus offset like (<br>, 1), and there is no child. |
| // Going from 1 to 0 is correct. |
| switch (moveType) { |
| case CodePoint: |
| return makeDeprecatedLegacyPosition(WTF::move(node), offset - 1); |
| case Character: { |
| auto previousOffset = uncheckedPreviousOffset(node.get(), offset); |
| return makeDeprecatedLegacyPosition(WTF::move(node), previousOffset); |
| } case BackwardDeletion: { |
| auto previousOffset = uncheckedPreviousOffsetForBackwardDeletion(node.get(), offset); |
| return makeDeprecatedLegacyPosition(WTF::move(node), previousOffset); |
| } |
| } |
| } |
| |
| RefPtr parent = node->parentNode(); |
| if (!parent) |
| return *this; |
| |
| if (positionBeforeOrAfterNodeIsCandidate(*node)) |
| return positionBeforeNode(node.get()); |
| |
| RefPtr previousSibling = node->previousSibling(); |
| if (previousSibling && positionBeforeOrAfterNodeIsCandidate(*previousSibling)) |
| return positionAfterNode(previousSibling.get()); |
| |
| return makeContainerOffsetPosition(WTF::move(parent), node->computeNodeIndex()); |
| } |
| |
| Position Position::next(PositionMoveType moveType) const |
| { |
| ASSERT(moveType != BackwardDeletion); |
| |
| RefPtr node = deprecatedNode(); |
| if (!node) |
| return *this; |
| |
| // FIXME: Negative offsets shouldn't be allowed. We should catch this earlier. |
| ASSERT(deprecatedEditingOffset() >= 0); |
| |
| unsigned offset = deprecatedEditingOffset(); |
| |
| if (anchorType() == PositionIsAfterAnchor) { |
| node = containerNode(); |
| if (!node) |
| return *this; |
| |
| offset = computeOffsetInContainerNode(); |
| } |
| |
| RefPtr child = node->traverseToChildAt(offset); |
| if (child || (!node->hasChildNodes() && offset < static_cast<unsigned>(lastOffsetForEditing(*node)))) { |
| if (child) |
| return firstPositionInOrBeforeNode(child.get()); |
| |
| // There are two reasons child might be 0: |
| // 1) The node is node like a text node that is not an element, and therefore has no children. |
| // Going forward one character at a time is correct. |
| // 2) The new offset is a bogus offset like (<br>, 1), and there is no child. |
| // Going from 0 to 1 is correct. |
| auto nextOffset = moveType == Character ? uncheckedNextOffset(node.get(), offset) : offset + 1; |
| return makeDeprecatedLegacyPosition(WTF::move(node), nextOffset); |
| } |
| |
| RefPtr parent = node->parentNode(); |
| if (!parent) |
| return *this; |
| |
| if (isRenderedTable(node.get()) || editingIgnoresContent(*node)) |
| return positionAfterNode(node.get()); |
| |
| RefPtr nextSibling = node->nextSibling(); |
| if (nextSibling && positionBeforeOrAfterNodeIsCandidate(*nextSibling)) |
| return positionBeforeNode(nextSibling.get()); |
| |
| return makeContainerOffsetPosition(WTF::move(parent), node->computeNodeIndex() + 1); |
| } |
| |
| int Position::uncheckedPreviousOffset(const Node* n, unsigned current) |
| { |
| return n->renderer() ? n->renderer()->previousOffset(current) : current - 1; |
| } |
| |
| int Position::uncheckedPreviousOffsetForBackwardDeletion(const Node* n, unsigned current) |
| { |
| return n->renderer() ? n->renderer()->previousOffsetForBackwardDeletion(current) : current - 1; |
| } |
| |
| int Position::uncheckedNextOffset(const Node* n, unsigned current) |
| { |
| return n->renderer() ? n->renderer()->nextOffset(current) : current + 1; |
| } |
| |
| bool Position::atFirstEditingPositionForNode() const |
| { |
| if (isNull()) |
| return true; |
| // FIXME: Position before anchor shouldn't be considered as at the first editing position for node |
| // since that position resides outside of the node. |
| switch (m_anchorType) { |
| case PositionIsOffsetInAnchor: |
| return !m_offset; |
| case PositionIsBeforeChildren: |
| case PositionIsBeforeAnchor: |
| return true; |
| case PositionIsAfterChildren: |
| case PositionIsAfterAnchor: |
| return !lastOffsetForEditing(*protectedDeprecatedNode()); |
| } |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| bool Position::atLastEditingPositionForNode() const |
| { |
| if (isNull()) |
| return true; |
| // FIXME: Position after anchor shouldn't be considered as at the first editing position for node since that position resides outside of the node. |
| return m_anchorType == PositionIsAfterAnchor || m_anchorType == PositionIsAfterChildren || m_offset >= static_cast<unsigned>(lastOffsetForEditing(*protectedDeprecatedNode())); |
| } |
| |
| // A position is considered at editing boundary if one of the following is true: |
| // 1. It is the first position in the node and the next visually equivalent position |
| // is non editable. |
| // 2. It is the last position in the node and the previous visually equivalent position |
| // is non editable. |
| // 3. It is an editable position and both the next and previous visually equivalent |
| // positions are both non editable. |
| bool Position::atEditingBoundary() const |
| { |
| Position nextPosition = downstream(CanCrossEditingBoundary); |
| if (atFirstEditingPositionForNode() && nextPosition.isNotNull() && !nextPosition.deprecatedNode()->hasEditableStyle()) |
| return true; |
| |
| Position prevPosition = upstream(CanCrossEditingBoundary); |
| if (atLastEditingPositionForNode() && prevPosition.isNotNull() && !prevPosition.deprecatedNode()->hasEditableStyle()) |
| return true; |
| |
| return nextPosition.isNotNull() && !nextPosition.deprecatedNode()->hasEditableStyle() |
| && prevPosition.isNotNull() && !prevPosition.deprecatedNode()->hasEditableStyle(); |
| } |
| |
| RefPtr<Node> Position::parentEditingBoundary() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| |
| RefPtr documentElement = m_anchorNode->document().documentElement(); |
| if (!documentElement) |
| return nullptr; |
| |
| RefPtr boundary = m_anchorNode; |
| while (boundary != documentElement && boundary->nonShadowBoundaryParentNode() && m_anchorNode->hasEditableStyle() == boundary->parentNode()->hasEditableStyle()) |
| boundary = boundary->nonShadowBoundaryParentNode(); |
| |
| return boundary; |
| } |
| |
| |
| bool Position::atStartOfTree() const |
| { |
| if (isNull()) |
| return true; |
| |
| RefPtr container = containerNode(); |
| if (container && container->parentNode()) |
| return false; |
| |
| switch (m_anchorType) { |
| case PositionIsOffsetInAnchor: |
| return !m_offset; |
| case PositionIsBeforeAnchor: |
| return !m_anchorNode->previousSibling(); |
| case PositionIsAfterAnchor: |
| return false; |
| case PositionIsBeforeChildren: |
| return true; |
| case PositionIsAfterChildren: |
| return !lastOffsetForEditing(*protectedAnchorNode()); |
| } |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| bool Position::atEndOfTree() const |
| { |
| if (isNull()) |
| return true; |
| |
| RefPtr container = containerNode(); |
| if (container && container->parentNode()) |
| return false; |
| |
| switch (m_anchorType) { |
| case PositionIsOffsetInAnchor: |
| return m_offset >= static_cast<unsigned>(lastOffsetForEditing(*protectedAnchorNode())); |
| case PositionIsBeforeAnchor: |
| return false; |
| case PositionIsAfterAnchor: |
| return !m_anchorNode->nextSibling(); |
| case PositionIsBeforeChildren: |
| return !lastOffsetForEditing(*protectedAnchorNode()); |
| case PositionIsAfterChildren: |
| return true; |
| } |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| // return first preceding DOM position rendered at a different location, or "this" |
| Position Position::previousCharacterPosition(Affinity affinity) const |
| { |
| if (isNull()) |
| return { }; |
| |
| RefPtr fromRootEditableElement = deprecatedNode()->rootEditableElement(); |
| |
| bool atStartOfLine = isStartOfLine(VisiblePosition(*this, affinity)); |
| bool rendered = isCandidate(); |
| |
| Position currentPosition = *this; |
| while (!currentPosition.atStartOfTree()) { |
| currentPosition = currentPosition.previous(); |
| |
| if (currentPosition.deprecatedNode()->rootEditableElement() != fromRootEditableElement) |
| return *this; |
| |
| if (atStartOfLine || !rendered) { |
| if (currentPosition.isCandidate()) |
| return currentPosition; |
| } else if (rendersInDifferentPosition(currentPosition)) |
| return currentPosition; |
| } |
| |
| return *this; |
| } |
| |
| // Whether or not [node, 0] and [node, lastOffsetForEditing(node)] are their own VisiblePositions. |
| // If true, adjacent candidates are visually distinct. |
| // FIXME: Disregard nodes with renderers that have no height, as we do in isCandidate. |
| // FIXME: Share code with isCandidate, if possible. |
| static bool endsOfNodeAreVisuallyDistinctPositions(Node* node) |
| { |
| if (!node || !node->renderer()) |
| return false; |
| |
| if (!node->renderer()->isInline()) |
| return true; |
| |
| // Don't include inline tables. |
| if (is<HTMLTableElement>(*node)) |
| return false; |
| |
| if (!node->renderer()->isBlockLevelReplacedOrAtomicInline() || !canHaveChildrenForEditing(*node) || !downcast<RenderBox>(*node->renderer()).height()) |
| return false; |
| |
| // There is a VisiblePosition inside an empty inline-block container. |
| if (!node->hasChildNodes()) |
| return true; |
| |
| return !Position::hasRenderedNonAnonymousDescendantsWithHeight(downcast<RenderElement>(*node->renderer())); |
| } |
| |
| static Node* enclosingVisualBoundary(SUPPRESS_UNCHECKED_LOCAL Node* node) |
| { |
| while (node && !endsOfNodeAreVisuallyDistinctPositions(node)) |
| node = node->parentNode(); |
| |
| return node; |
| } |
| |
| // upstream() and downstream() want to return positions that are either in a |
| // text node or at just before a non-text node. This method checks for that. |
| static bool isStreamer(const PositionIterator& pos) |
| { |
| RefPtr node = pos.node(); |
| if (!node) |
| return true; |
| |
| if (isAtomicNode(node.get())) |
| return true; |
| |
| return pos.atStartOfNode(); |
| } |
| |
| // This function and downstream() are used for moving back and forth between visually equivalent candidates. |
| // For example, for the text node "foo bar" where whitespace is collapsible, there are two candidates |
| // that map to the VisiblePosition between 'b' and the space. This function will return the left candidate |
| // and downstream() will return the right one. |
| // Also, upstream() will return [boundary, 0] for any of the positions from [boundary, 0] to the first candidate |
| // in boundary, where endsOfNodeAreVisuallyDistinctPositions(boundary) is true. |
| Position Position::upstream(EditingBoundaryCrossingRule rule) const |
| { |
| RefPtr startNode = deprecatedNode(); |
| if (!startNode) |
| return { }; |
| |
| // iterate backward from there, looking for a qualified position |
| RefPtr boundary = enclosingVisualBoundary(startNode.get()); |
| // FIXME: PositionIterator should respect Before and After positions. |
| RefPtr anchorNode = m_anchorNode; |
| PositionIterator lastVisible = m_anchorType == PositionIsAfterAnchor ? makeDeprecatedLegacyPosition(anchorNode.get(), caretMaxOffset(*anchorNode)) : *this; |
| PositionIterator currentPosition = lastVisible; |
| bool startEditable = startNode->hasEditableStyle(); |
| RefPtr lastNode = startNode; |
| bool boundaryCrossed = false; |
| for (; !currentPosition.atStart(); currentPosition.decrement()) { |
| Ref currentNode = *currentPosition.node(); |
| |
| // Don't check for an editability change if we haven't moved to a different node, |
| // to avoid the expense of computing hasEditableStyle(). |
| if (currentNode.ptr() != lastNode) { |
| // Don't change editability. |
| bool currentEditable = currentNode->hasEditableStyle(); |
| if (startEditable != currentEditable) { |
| if (rule == CannotCrossEditingBoundary) |
| break; |
| boundaryCrossed = true; |
| } |
| lastNode = currentNode.ptr(); |
| } |
| |
| // There is no caret position in non-text svg elements. |
| if (currentNode->isSVGElement() && !is<SVGTextElement>(currentNode)) |
| continue; |
| |
| // If we've moved to a position that is visually distinct, return the last saved position. There |
| // is code below that terminates early if we're *about* to move to a visually distinct position. |
| if (endsOfNodeAreVisuallyDistinctPositions(currentNode.ptr()) && currentNode.ptr() != boundary) |
| return lastVisible; |
| |
| // skip position in unrendered or invisible node |
| CheckedPtr renderer = currentNode->renderer(); |
| if (!renderer || renderer->style().visibility() != Visibility::Visible) |
| continue; |
| |
| if (rule == CanCrossEditingBoundary && boundaryCrossed) { |
| lastVisible = currentPosition; |
| break; |
| } |
| |
| // track last visible streamer position |
| if (isStreamer(currentPosition)) |
| lastVisible = currentPosition; |
| |
| // Don't move past a position that is visually distinct. We could rely on code above to terminate and |
| // return lastVisible on the next iteration, but we terminate early to avoid doing a computeNodeIndex() call. |
| if (endsOfNodeAreVisuallyDistinctPositions(currentNode.ptr()) && currentPosition.atStartOfNode()) |
| return lastVisible; |
| |
| // Return position after tables and nodes which have content that can be ignored. |
| if (editingIgnoresContent(currentNode) || isRenderedTable(currentNode.ptr())) { |
| if (currentPosition.atEndOfNode()) |
| return positionAfterNode(currentNode.ptr()); |
| continue; |
| } |
| |
| // return current position if it is in rendered text |
| if (auto* textRenderer = dynamicDowncast<RenderText>(*renderer)) { |
| auto [firstTextBox, orderCache] = InlineIterator::firstTextBoxInLogicalOrderFor(*textRenderer); |
| if (!firstTextBox) |
| continue; |
| |
| if (currentNode.ptr() != startNode) { |
| // This assertion fires in layout tests in the case-transform.html test because |
| // of a mix-up between offsets in the text in the DOM tree with text in the |
| // render tree which can have a different length due to case transformation. |
| // Until we resolve that, disable this so we can run the layout tests! |
| //ASSERT(currentOffset >= renderer->caretMaxOffset()); |
| return makeDeprecatedLegacyPosition(currentNode.ptr(), renderer->caretMaxOffset()); |
| } |
| |
| unsigned textOffset = currentPosition.offsetInLeafNode(); |
| for (auto box = firstTextBox; box;) { |
| if (textOffset > box->start() && textOffset <= box->end()) |
| return currentPosition; |
| |
| auto nextBox = InlineIterator::nextTextBoxInLogicalOrder(box, orderCache); |
| if (textOffset == box->end() + 1 && nextBox && box->lineBox() != nextBox->lineBox()) |
| return currentPosition; |
| |
| box = nextBox; |
| } |
| } |
| } |
| |
| return lastVisible; |
| } |
| |
| // This function and upstream() are used for moving back and forth between visually equivalent candidates. |
| // For example, for the text node "foo bar" where whitespace is collapsible, there are two candidates |
| // that map to the VisiblePosition between 'b' and the space. This function will return the right candidate |
| // and upstream() will return the left one. |
| // Also, downstream() will return the last position in the last atomic node in boundary for all of the positions |
| // in boundary after the last candidate, where endsOfNodeAreVisuallyDistinctPositions(boundary). |
| // FIXME: This function should never be called when the line box tree is dirty. See https://bugs.webkit.org/show_bug.cgi?id=97264 |
| Position Position::downstream(EditingBoundaryCrossingRule rule) const |
| { |
| RefPtr startNode = deprecatedNode(); |
| if (!startNode) |
| return { }; |
| |
| // iterate forward from there, looking for a qualified position |
| RefPtr boundary = enclosingVisualBoundary(startNode.get()); |
| // FIXME: PositionIterator should respect Before and After positions. |
| RefPtr anchorNode = m_anchorNode; |
| PositionIterator lastVisible = m_anchorType == PositionIsAfterAnchor ? makeDeprecatedLegacyPosition(anchorNode.get(), caretMaxOffset(*anchorNode)) : *this; |
| PositionIterator currentPosition = lastVisible; |
| bool startEditable = startNode->hasEditableStyle(); |
| auto lastNode = startNode; |
| bool boundaryCrossed = false; |
| for (; !currentPosition.atEnd(); currentPosition.increment()) { |
| Ref currentNode = *currentPosition.node(); |
| |
| // Don't check for an editability change if we haven't moved to a different node, |
| // to avoid the expense of computing hasEditableStyle(). |
| if (currentNode.ptr() != lastNode.get()) { |
| // Don't change editability. |
| bool currentEditable = currentNode->hasEditableStyle(); |
| if (startEditable != currentEditable) { |
| if (rule == CannotCrossEditingBoundary) |
| break; |
| boundaryCrossed = true; |
| } |
| |
| lastNode = currentNode.ptr(); |
| } |
| |
| // stop before going above the body, up into the head |
| // return the last visible streamer position |
| if (is<HTMLBodyElement>(currentNode.get()) && currentPosition.atEndOfNode()) |
| break; |
| |
| // There is no caret position in non-text svg elements. |
| if (currentNode->isSVGElement() && !is<SVGTextElement>(currentNode.get())) |
| continue; |
| |
| // Do not move to a visually distinct position. |
| if (endsOfNodeAreVisuallyDistinctPositions(currentNode.ptr()) && currentNode.ptr() != boundary) |
| return lastVisible; |
| // Do not move past a visually disinct position. |
| // Note: The first position after the last in a node whose ends are visually distinct |
| // positions will be [boundary->parentNode(), originalBlock->computeNodeIndex() + 1]. |
| if (boundary && boundary->parentNode() == currentNode.ptr()) |
| return lastVisible; |
| |
| // skip position in unrendered or invisible node |
| CheckedPtr renderer = currentNode->renderer(); |
| if (!renderer || renderer->style().visibility() != Visibility::Visible) |
| continue; |
| |
| if (rule == CanCrossEditingBoundary && boundaryCrossed) { |
| lastVisible = currentPosition; |
| break; |
| } |
| |
| // track last visible streamer position |
| if (isStreamer(currentPosition)) |
| lastVisible = currentPosition; |
| |
| // Return position before tables and nodes which have content that can be ignored. |
| if (editingIgnoresContent(currentNode) || isRenderedTable(currentNode.ptr())) { |
| if (currentPosition.atStartOfNode()) |
| return positionBeforeNode(currentNode.ptr()); |
| continue; |
| } |
| |
| // return current position if it is in rendered text |
| if (auto* textRenderer = dynamicDowncast<RenderText>(*renderer)) { |
| auto [firstTextBox, orderCache] = InlineIterator::firstTextBoxInLogicalOrderFor(*textRenderer); |
| if (!firstTextBox) |
| continue; |
| |
| if (currentNode.ptr() != startNode) { |
| ASSERT(currentPosition.atStartOfNode()); |
| return makeContainerOffsetPosition(currentNode.ptr(), textRenderer->caretMinOffset()); |
| } |
| |
| unsigned textOffset = currentPosition.offsetInLeafNode(); |
| for (auto box = firstTextBox; box;) { |
| if (!box->length() && textOffset == box->start()) |
| return currentPosition; |
| |
| if (textOffset >= box->start() && textOffset < box->end()) |
| return currentPosition; |
| |
| auto nextBox = InlineIterator::nextTextBoxInLogicalOrder(box, orderCache); |
| if (textOffset == box->end() && nextBox && box->lineBox() != nextBox->lineBox()) |
| return currentPosition; |
| |
| box = nextBox; |
| } |
| } |
| } |
| |
| return lastVisible; |
| } |
| |
| unsigned Position::positionCountBetweenPositions(const Position& a, const Position& b) |
| { |
| if (a.isNull() || b.isNull()) |
| return UINT_MAX; |
| |
| Position endPos; |
| Position pos; |
| if (a > b) { |
| endPos = a; |
| pos = b; |
| } else if (a < b) { |
| endPos = b; |
| pos = a; |
| } else |
| return 0; |
| |
| unsigned posCount = 0; |
| while (!pos.atEndOfTree() && pos != endPos) { |
| pos = pos.next(); |
| ++posCount; |
| } |
| return posCount; |
| } |
| |
| bool Position::hasRenderedNonAnonymousDescendantsWithHeight(const RenderElement& renderer) |
| { |
| auto isHorizontal = renderer.isHorizontalWritingMode(); |
| auto* stop = renderer.nextInPreOrderAfterChildren(); |
| for (CheckedPtr descendant = renderer.firstChild(); descendant && descendant != stop; descendant = descendant->nextInPreOrder()) { |
| if (!descendant->nonPseudoNode()) |
| continue; |
| |
| auto boundingBoxLogicalHeight = [&](auto rect) { |
| return isHorizontal ? rect.height() : rect.width(); |
| }; |
| |
| if (CheckedPtr renderText = dynamicDowncast<RenderText>(*descendant)) { |
| if (boundingBoxLogicalHeight(renderText->linesBoundingBox())) |
| return true; |
| continue; |
| } |
| if (CheckedPtr renderLineBreak = dynamicDowncast<RenderLineBreak>(*descendant)) { |
| if (boundingBoxLogicalHeight(renderLineBreak->linesBoundingBox())) |
| return true; |
| continue; |
| } |
| if (CheckedPtr renderInline = dynamicDowncast<RenderInline>(*descendant)) { |
| if (isEmptyInline(*renderInline) && boundingBoxLogicalHeight(renderInline->linesBoundingBox())) |
| return true; |
| continue; |
| } |
| if (CheckedPtr renderBox = dynamicDowncast<RenderBox>(*descendant)) { |
| if (roundToInt(renderBox->logicalHeight())) |
| return true; |
| continue; |
| } |
| } |
| return false; |
| } |
| |
| bool Position::nodeIsUserSelectNone(Node* node) |
| { |
| if (!node) |
| return false; |
| return node->renderer() && (node->renderer()->style().usedUserSelect() == UserSelect::None); |
| } |
| |
| bool Position::nodeIsUserSelectAll(const Node* node) |
| { |
| if (!node) |
| return false; |
| CheckedPtr renderer = node->renderer(); |
| return renderer && renderer->style().usedUserSelect() == UserSelect::All; |
| } |
| |
| RefPtr<Node> Position::rootUserSelectAllForNode(Node* node) |
| { |
| if (!node || !nodeIsUserSelectAll(node)) |
| return nullptr; |
| RefPtr parent = node->parentNode(); |
| if (!parent) |
| return node; |
| |
| RefPtr candidateRoot = node; |
| while (parent) { |
| if (!parent->renderer()) { |
| parent = parent->parentNode(); |
| continue; |
| } |
| if (!nodeIsUserSelectAll(parent.get())) |
| break; |
| candidateRoot = WTF::move(parent); |
| parent = candidateRoot->parentNode(); |
| } |
| return candidateRoot; |
| } |
| |
| // This function should be kept in sync with PositionIterator::isCandidate(). |
| bool Position::isCandidate() const |
| { |
| if (isNull()) |
| return false; |
| |
| RefPtr node = deprecatedNode(); |
| CheckedPtr renderer = node->renderer(); |
| if (!renderer) |
| return false; |
| |
| if (renderer->style().visibility() != Visibility::Visible) |
| return false; |
| |
| if (renderer->isBR()) { |
| // FIXME: The condition should be m_anchorType == PositionIsBeforeAnchor, but for now we still need to support legacy positions. |
| return !m_offset && m_anchorType != PositionIsAfterAnchor && !nodeIsUserSelectNone(node->parentNode()); |
| } |
| |
| if (CheckedPtr renderText = dynamicDowncast<RenderText>(*renderer)) |
| return !nodeIsUserSelectNone(node.get()) && renderText->containsCaretOffset(m_offset); |
| |
| if (positionBeforeOrAfterNodeIsCandidate(*node)) { |
| return ((atFirstEditingPositionForNode() && m_anchorType == PositionIsBeforeAnchor) |
| || (atLastEditingPositionForNode() && m_anchorType == PositionIsAfterAnchor)) |
| && !nodeIsUserSelectNone(node->parentNode()); |
| } |
| |
| if (is<HTMLHtmlElement>(*m_anchorNode)) |
| return false; |
| |
| if (CheckedPtr block = dynamicDowncast<RenderBlock>(*renderer)) { |
| if (is<RenderBlockFlow>(*block) || is<RenderGrid>(*block) || is<RenderFlexibleBox>(*block)) { |
| if (block->logicalHeight() || is<HTMLBodyElement>(*m_anchorNode) || m_anchorNode->isRootEditableElement()) { |
| if (!Position::hasRenderedNonAnonymousDescendantsWithHeight(*block)) |
| return atFirstEditingPositionForNode() && !Position::nodeIsUserSelectNone(node.get()); |
| return m_anchorNode->hasEditableStyle() && !Position::nodeIsUserSelectNone(node.get()) && atEditingBoundary(); |
| } |
| return false; |
| } |
| } |
| |
| return m_anchorNode->hasEditableStyle() && !Position::nodeIsUserSelectNone(node.get()) && atEditingBoundary(); |
| } |
| |
| bool Position::isRenderedCharacter() const |
| { |
| RefPtr text = dynamicDowncast<Text>(deprecatedNode()); |
| CheckedPtr renderer = text ? text->renderer() : nullptr; |
| return renderer && renderer->containsRenderedCharacterOffset(m_offset); |
| } |
| |
| static bool inSameEnclosingBlockFlowElement(Node* a, Node* b) |
| { |
| return a && b && deprecatedEnclosingBlockFlowElement(a) == deprecatedEnclosingBlockFlowElement(b); |
| } |
| |
| bool Position::rendersInDifferentPosition(const Position& position) const |
| { |
| if (isNull() || position.isNull()) |
| return false; |
| |
| RefPtr node = deprecatedNode(); |
| CheckedPtr renderer = node->renderer(); |
| if (!renderer) |
| return false; |
| |
| RefPtr positionNode = position.deprecatedNode(); |
| CheckedPtr positionRenderer = positionNode->renderer(); |
| if (!positionRenderer) |
| return false; |
| |
| if (renderer->style().visibility() != Visibility::Visible || positionRenderer->style().visibility() != Visibility::Visible) |
| return false; |
| |
| if (node == positionNode) { |
| if (is<HTMLBRElement>(*node)) |
| return false; |
| |
| if (m_offset == static_cast<unsigned>(position.deprecatedEditingOffset())) |
| return false; |
| |
| if (!is<Text>(*node)) |
| return true; |
| } |
| |
| if (is<HTMLBRElement>(*node) && position.isCandidate()) |
| return true; |
| |
| if (is<HTMLBRElement>(*positionNode) && isCandidate()) |
| return true; |
| |
| if (!inSameEnclosingBlockFlowElement(node.get(), positionNode.get())) |
| return true; |
| |
| CheckedPtr textRenderer = dynamicDowncast<RenderText>(*renderer); |
| if (textRenderer && !textRenderer->containsCaretOffset(m_offset)) |
| return false; |
| |
| CheckedPtr textPositionRenderer = dynamicDowncast<RenderText>(*positionRenderer); |
| if (textPositionRenderer && !textPositionRenderer->containsCaretOffset(position.m_offset)) |
| return false; |
| |
| unsigned thisRenderedOffset = textRenderer ? textRenderer->countRenderedCharacterOffsetsUntil(m_offset) : m_offset; |
| unsigned positionRenderedOffset = textPositionRenderer ? textPositionRenderer->countRenderedCharacterOffsetsUntil(position.m_offset) : position.m_offset; |
| |
| if (renderer == positionRenderer && thisRenderedOffset == positionRenderedOffset) |
| return false; |
| |
| auto box1 = inlineBoxAndOffset(Affinity::Downstream).box; |
| auto box2 = position.inlineBoxAndOffset(Affinity::Downstream).box; |
| |
| LOG(Editing, "renderer: %p\n", renderer.get()); |
| LOG(Editing, "thisRenderedOffset: %d\n", thisRenderedOffset); |
| LOG(Editing, "posRenderer: %p\n", positionRenderer.get()); |
| LOG(Editing, "posRenderedOffset: %d\n", positionRenderedOffset); |
| LOG(Editing, "node min/max: %d:%d\n", caretMinOffset(*node), caretMaxOffset(*node)); |
| LOG(Editing, "pos node min/max: %d:%d\n", caretMinOffset(*positionNode), caretMaxOffset(*positionNode)); |
| LOG(Editing, "----------------------------------------------------------------------\n"); |
| |
| if (!box1 || !box2) |
| return false; |
| |
| if (box1->lineBox() != box2->lineBox()) |
| return true; |
| |
| if (nextRenderedEditable(node.get()) == positionNode && thisRenderedOffset == static_cast<unsigned>(caretMaxOffset(*positionNode)) && !positionRenderedOffset) |
| return false; |
| |
| if (previousRenderedEditable(node.get()) == positionNode && !thisRenderedOffset && positionRenderedOffset == static_cast<unsigned>(caretMaxOffset(*positionNode))) |
| return false; |
| |
| return true; |
| } |
| |
| // This assumes that it starts in editable content. |
| Position Position::leadingWhitespacePosition(Affinity affinity, bool considerNonCollapsibleWhitespace) const |
| { |
| ASSERT(isEditablePosition(*this)); |
| if (isNull()) |
| return { }; |
| |
| if (is<HTMLBRElement>(*upstream().deprecatedNode())) |
| return { }; |
| |
| Position prev = previousCharacterPosition(affinity); |
| RefPtr node = deprecatedNode(); |
| RefPtr previousNode = prev.deprecatedNode(); |
| if (prev != *this && inSameEnclosingBlockFlowElement(node.get(), previousNode.get())) { |
| if (auto* previousText = dynamicDowncast<Text>(*previousNode)) { |
| char16_t c = previousText->data()[prev.deprecatedEditingOffset()]; |
| if (considerNonCollapsibleWhitespace ? (isASCIIWhitespace(c) || c == noBreakSpace) : deprecatedIsCollapsibleWhitespace(c)) { |
| if (isEditablePosition(prev)) |
| return prev; |
| } |
| } |
| } |
| |
| return { }; |
| } |
| |
| // This assumes that it starts in editable content. |
| Position Position::trailingWhitespacePosition(Affinity, bool considerNonCollapsibleWhitespace) const |
| { |
| ASSERT(isEditablePosition(*this)); |
| if (isNull()) |
| return { }; |
| |
| VisiblePosition v(*this); |
| char16_t c = v.characterAfter(); |
| // The space must not be in another paragraph and it must be editable. |
| if (!isEndOfParagraph(v) && v.next(CannotCrossEditingBoundary).isNotNull()) |
| if (considerNonCollapsibleWhitespace ? (isASCIIWhitespace(c) || c == noBreakSpace) : deprecatedIsCollapsibleWhitespace(c)) |
| return *this; |
| |
| return { }; |
| } |
| |
| InlineBoxAndOffset Position::inlineBoxAndOffset(Affinity affinity) const |
| { |
| return inlineBoxAndOffset(affinity, primaryDirection()); |
| } |
| |
| static bool isNonTextLeafChild(RenderObject& object) |
| { |
| if (auto* renderElement = dynamicDowncast<RenderElement>(object)) |
| return !renderElement->firstChild(); |
| return false; |
| } |
| |
| static InlineIterator::TextBoxIterator searchAheadForBetterMatch(RenderText& renderer) |
| { |
| CheckedPtr container = renderer.containingBlock(); |
| CheckedPtr<RenderObject> next = &renderer; |
| while ((next = next->nextInPreOrder(container.get()))) { |
| if (is<RenderBlock>(next)) |
| return { }; |
| if (next->isBR()) |
| return { }; |
| if (isNonTextLeafChild(*next)) |
| return { }; |
| if (CheckedPtr renderText = dynamicDowncast<RenderText>(*next)) { |
| if (auto box = InlineIterator::firstTextBoxInLogicalOrderFor(*renderText).first) |
| return box; |
| } |
| } |
| return { }; |
| } |
| |
| static Position downstreamIgnoringEditingBoundaries(Position position) |
| { |
| Position lastPosition; |
| while (position != lastPosition) { |
| lastPosition = position; |
| position = position.downstream(CanCrossEditingBoundary); |
| } |
| return position; |
| } |
| |
| static Position upstreamIgnoringEditingBoundaries(Position position) |
| { |
| Position lastPosition; |
| while (position != lastPosition) { |
| lastPosition = position; |
| position = position.upstream(CanCrossEditingBoundary); |
| } |
| return position; |
| } |
| |
| InlineBoxAndOffset Position::inlineBoxAndOffset(Affinity affinity, TextDirection primaryDirection) const |
| { |
| auto caretOffset = static_cast<unsigned>(deprecatedEditingOffset()); |
| |
| RefPtr node = deprecatedNode(); |
| if (!node) |
| return { { }, caretOffset }; |
| CheckedPtr renderer = node->renderer(); |
| if (!renderer) |
| return { { }, caretOffset }; |
| |
| InlineIterator::LeafBoxIterator box; |
| |
| if (CheckedPtr lineBreakRenderer = dynamicDowncast<RenderLineBreak>(*renderer); lineBreakRenderer && lineBreakRenderer->isBR()) { |
| if (!caretOffset) |
| box = InlineIterator::boxFor(*lineBreakRenderer); |
| } else if (CheckedPtr textRenderer = dynamicDowncast<RenderText>(*renderer)) { |
| auto textBox = InlineIterator::lineLeftmostTextBoxFor(*textRenderer); |
| InlineIterator::TextBoxIterator candidate; |
| |
| for (; textBox; ++textBox) { |
| unsigned caretMinOffset = textBox->minimumCaretOffset(); |
| unsigned caretMaxOffset = textBox->maximumCaretOffset(); |
| |
| if (caretOffset < caretMinOffset || caretOffset > caretMaxOffset || (caretOffset == caretMaxOffset && textBox->isLineBreak())) |
| continue; |
| |
| if (caretOffset > caretMinOffset && caretOffset < caretMaxOffset) |
| return { textBox, caretOffset }; |
| |
| if ((caretOffset == caretMaxOffset) ^ (affinity == Affinity::Downstream)) |
| break; |
| |
| if ((caretOffset == caretMinOffset) ^ (affinity == Affinity::Upstream)) |
| break; |
| |
| if (caretOffset == caretMaxOffset) { |
| auto nextOnLine = textBox->nextLineRightwardOnLine(); |
| if (nextOnLine && nextOnLine->isLineBreak()) |
| break; |
| } |
| |
| candidate = textBox; |
| } |
| |
| if (candidate && !candidate->nextTextBox() && affinity == Affinity::Downstream) { |
| textBox = searchAheadForBetterMatch(*textRenderer); |
| if (textBox) |
| caretOffset = textBox->minimumCaretOffset(); |
| } |
| |
| box = textBox ? textBox : candidate; |
| } else { |
| RefPtr node = deprecatedNode(); |
| if (canHaveChildrenForEditing(*node)) { |
| CheckedPtr renderBlockFlow = dynamicDowncast<RenderBlockFlow>(*renderer); |
| if (renderBlockFlow && hasRenderedNonAnonymousDescendantsWithHeight(*renderBlockFlow)) { |
| // Try a visually equivalent position with possibly opposite editability. This helps in case |this| is in |
| // an editable block but surrounded by non-editable positions. It acts to negate the logic at the beginning |
| // of RenderObject::createPositionWithAffinity(). |
| Position equivalent = downstreamIgnoringEditingBoundaries(*this); |
| if (equivalent == *this) { |
| equivalent = upstreamIgnoringEditingBoundaries(*this); |
| // FIXME: Can returning nullptr really be correct here? |
| if (equivalent == *this || downstreamIgnoringEditingBoundaries(equivalent) == *this) |
| return { { }, caretOffset }; |
| } |
| |
| return equivalent.inlineBoxAndOffset(Affinity::Upstream, primaryDirection); |
| } |
| } |
| if (CheckedPtr renderBox = dynamicDowncast<RenderBox>(*renderer)) { |
| box = InlineIterator::boxFor(*renderBox); |
| if (box && caretOffset > box->minimumCaretOffset() && caretOffset < box->maximumCaretOffset()) |
| return { box, caretOffset }; |
| } |
| } |
| |
| if (!box) |
| return { { }, caretOffset }; |
| |
| unsigned char level = box->bidiLevel(); |
| |
| if (box->direction() == primaryDirection) { |
| if (caretOffset == box->rightmostCaretOffset()) { |
| auto nextBox = box->nextLineRightwardOnLine(); |
| if (!nextBox || nextBox->bidiLevel() >= level) |
| return { box, caretOffset }; |
| |
| level = nextBox->bidiLevel(); |
| |
| auto previousRun = box->nextLineLeftwardOnLine(); |
| for (; previousRun; previousRun.traverseLineLeftwardOnLine()) { |
| if (previousRun->bidiLevel() <= level) |
| break; |
| } |
| |
| if (previousRun && previousRun->bidiLevel() == level) // For example, abc FED 123 ^ CBA |
| return { box, caretOffset }; |
| |
| // For example, abc 123 ^ CBA |
| for (; nextBox; nextBox.traverseLineRightwardOnLine()) { |
| if (nextBox->bidiLevel() < level) |
| break; |
| box = nextBox; |
| } |
| caretOffset = box->rightmostCaretOffset(); |
| } else { |
| auto previousRun = box->nextLineLeftwardOnLine(); |
| if (!previousRun || previousRun->bidiLevel() >= level) |
| return { box, caretOffset }; |
| |
| level = previousRun->bidiLevel(); |
| |
| auto nextBox = box->nextLineRightwardOnLine(); |
| for (; nextBox; nextBox.traverseLineRightwardOnLine()) { |
| if (nextBox->bidiLevel() <= level) |
| break; |
| } |
| |
| if (nextBox && nextBox->bidiLevel() == level) |
| return { box, caretOffset }; |
| |
| for (; previousRun; previousRun.traverseLineLeftwardOnLine()) { |
| if (previousRun->bidiLevel() < level) |
| break; |
| box = previousRun; |
| } |
| |
| caretOffset = box->leftmostCaretOffset(); |
| } |
| return { box, caretOffset }; |
| } |
| |
| if (caretOffset == box->leftmostCaretOffset()) { |
| auto previousRun = box->nextLineLeftwardOnLineIgnoringLineBreak(); |
| if (!previousRun || previousRun->bidiLevel() < level) { |
| // Left edge of a secondary box. Set to the right edge of the entire box. |
| for (auto nextBox = box->nextLineRightwardOnLineIgnoringLineBreak(); nextBox; nextBox.traverseLineRightwardOnLineIgnoringLineBreak()) { |
| if (nextBox->bidiLevel() < level) |
| break; |
| box = nextBox; |
| } |
| caretOffset = box->rightmostCaretOffset(); |
| } else if (previousRun->bidiLevel() > level) { |
| // Right edge of a "tertiary" box. Set to the left edge of that box. |
| for (auto tertiaryRun = box->nextLineLeftwardOnLineIgnoringLineBreak(); tertiaryRun; tertiaryRun.traverseLineLeftwardOnLineIgnoringLineBreak()) { |
| if (tertiaryRun->bidiLevel() <= level) |
| break; |
| box = tertiaryRun; |
| } |
| caretOffset = box->leftmostCaretOffset(); |
| } |
| } else { |
| auto nextBox = box->nextLineRightwardOnLineIgnoringLineBreak(); |
| if (!nextBox || nextBox->bidiLevel() < level) { |
| // Right edge of a secondary box. Set to the left edge of the entire box. |
| for (auto previousRun = box->nextLineLeftwardOnLineIgnoringLineBreak(); previousRun; previousRun.traverseLineLeftwardOnLineIgnoringLineBreak()) { |
| if (previousRun->bidiLevel() < level) |
| break; |
| box = previousRun; |
| } |
| caretOffset = box->leftmostCaretOffset(); |
| } else if (nextBox->bidiLevel() > level) { |
| // Left edge of a "tertiary" box. Set to the right edge of that box. |
| for (auto tertiaryRun = box->nextLineRightwardOnLineIgnoringLineBreak(); tertiaryRun; tertiaryRun.traverseLineRightwardOnLineIgnoringLineBreak()) { |
| if (tertiaryRun->bidiLevel() <= level) |
| break; |
| box = tertiaryRun; |
| } |
| caretOffset = box->rightmostCaretOffset(); |
| } |
| } |
| |
| return { box, caretOffset }; |
| } |
| |
| TextDirection Position::primaryDirection() const |
| { |
| if (!m_anchorNode || !m_anchorNode->renderer()) |
| return TextDirection::LTR; |
| if (auto* blockFlow = lineageOfType<RenderBlockFlow>(*m_anchorNode->renderer()).first()) |
| return blockFlow->style().writingMode().bidiDirection(); |
| return TextDirection::LTR; |
| } |
| |
| #if ENABLE(TREE_DEBUGGING) |
| |
| void Position::debugPosition(ASCIILiteral msg) const |
| { |
| if (isNull()) |
| SAFE_FPRINTF(stderr, "Position [%s]: null\n", msg); |
| else |
| SAFE_FPRINTF(stderr, "Position [%s]: %s [%p] at %d\n", msg, deprecatedNode()->nodeName().utf8(), deprecatedNode(), m_offset); |
| } |
| |
| String Position::debugDescription() const |
| { |
| if (isNull()) |
| return "<null>"_s; |
| return makeString("offset "_s, m_offset, " of "_s, deprecatedNode()->debugDescription()); |
| } |
| |
| void Position::showAnchorTypeAndOffset() const |
| { |
| ASCIILiteral legacy = ""_s; |
| if (m_isLegacyEditingPosition) |
| legacy = "legacy, "_s; |
| |
| ASCIILiteral position; |
| switch (anchorType()) { |
| case PositionIsOffsetInAnchor: |
| position = "offset"_s; |
| break; |
| case PositionIsBeforeChildren: |
| position = "beforeChildren"_s; |
| break; |
| case PositionIsAfterChildren: |
| position = "afterChildren"_s; |
| break; |
| case PositionIsBeforeAnchor: |
| position = "before"_s; |
| break; |
| case PositionIsAfterAnchor: |
| position = "after"_s; |
| break; |
| } |
| SAFE_FPRINTF(stderr, "%s%s, offset:%d\n", legacy, position, m_offset); |
| } |
| |
| void Position::showTreeForThis() const |
| { |
| if (anchorNode()) { |
| anchorNode()->showTreeForThis(); |
| showAnchorTypeAndOffset(); |
| } |
| } |
| |
| #endif |
| |
| bool Position::equals(const Position& other) const |
| { |
| if (!m_anchorNode) |
| return !m_anchorNode == !other.m_anchorNode; |
| if (!other.m_anchorNode) |
| return false; |
| |
| switch (anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*m_anchorNode)); |
| switch (other.anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode; |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode && !m_anchorNode->hasChildNodes(); |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode == other.m_anchorNode && !other.m_offset; |
| case PositionIsBeforeAnchor: |
| return m_anchorNode->firstChild() == other.m_anchorNode; |
| case PositionIsAfterAnchor: |
| return false; |
| } |
| break; |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*m_anchorNode)); |
| switch (other.anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode && !m_anchorNode->hasChildNodes(); |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode; |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode == other.m_anchorNode && m_anchorNode->countChildNodes() == static_cast<unsigned>(m_offset); |
| case PositionIsBeforeAnchor: |
| return false; |
| case PositionIsAfterAnchor: |
| return m_anchorNode->lastChild() == other.m_anchorNode; |
| } |
| break; |
| case PositionIsOffsetInAnchor: |
| switch (other.anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode && !m_offset; |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode && m_offset == other.m_anchorNode->countChildNodes(); |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode == other.m_anchorNode && m_offset == other.m_offset; |
| case PositionIsBeforeAnchor: |
| return m_anchorNode->traverseToChildAt(m_offset) == other.m_anchorNode; |
| case PositionIsAfterAnchor: |
| return m_offset && m_anchorNode->traverseToChildAt(m_offset - 1) == other.m_anchorNode; |
| } |
| break; |
| case PositionIsBeforeAnchor: |
| switch (other.anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode->firstChild(); |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return false; |
| case PositionIsOffsetInAnchor: |
| return m_anchorNode == other.m_anchorNode->traverseToChildAt(other.m_offset); |
| case PositionIsBeforeAnchor: |
| return m_anchorNode == other.m_anchorNode; |
| case PositionIsAfterAnchor: |
| return m_anchorNode->previousSibling() == other.m_anchorNode; |
| } |
| break; |
| case PositionIsAfterAnchor: |
| switch (other.anchorType()) { |
| case PositionIsBeforeChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return false; |
| case PositionIsAfterChildren: |
| ASSERT(!is<Text>(*other.m_anchorNode)); |
| return m_anchorNode == other.m_anchorNode->lastChild(); |
| case PositionIsOffsetInAnchor: |
| return other.m_offset && m_anchorNode == other.m_anchorNode->traverseToChildAt(other.m_offset - 1); |
| case PositionIsBeforeAnchor: |
| return m_anchorNode->nextSibling() == other.m_anchorNode; |
| case PositionIsAfterAnchor: |
| return m_anchorNode == other.m_anchorNode; |
| } |
| break; |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| static TextStream& operator<<(TextStream& stream, Position::AnchorType anchorType) |
| { |
| switch (anchorType) { |
| case Position::PositionIsOffsetInAnchor: |
| stream << "offset in anchor"; |
| break; |
| case Position::PositionIsBeforeAnchor: |
| stream << "before anchor"; |
| break; |
| case Position::PositionIsAfterAnchor: |
| stream << "after anchor"; |
| break; |
| case Position::PositionIsBeforeChildren: |
| stream << "before children"; |
| break; |
| case Position::PositionIsAfterChildren: |
| stream << "after children"; |
| break; |
| } |
| return stream; |
| } |
| |
| TextStream& operator<<(TextStream& ts, const Position& position) |
| { |
| TextStream::GroupScope scope(ts); |
| ts << "Position "_s << &position; |
| |
| ts.dumpProperty("anchor node"_s, position.anchorNode()); |
| ts.dumpProperty("offset"_s, position.offsetInContainerNode()); |
| ts.dumpProperty("anchor type"_s, position.anchorType()); |
| |
| return ts; |
| } |
| |
| Node* commonInclusiveAncestor(const Position& a, const Position& b) |
| { |
| RefPtr nodeA = a.containerNode(); |
| RefPtr nodeB = b.containerNode(); |
| if (!nodeA || !nodeB) |
| return nullptr; |
| return commonInclusiveAncestor<ComposedTree>(*nodeA, *nodeB); |
| } |
| |
| Position positionInParentBeforeNode(Node* node) |
| { |
| RefPtr currentNode = node; |
| RefPtr ancestor = node->parentNode(); |
| while (ancestor && editingIgnoresContent(*ancestor)) { |
| currentNode = ancestor; |
| ancestor = ancestor->parentNode(); |
| } |
| ASSERT(ancestor); |
| return Position(ancestor, currentNode->computeNodeIndex(), Position::PositionIsOffsetInAnchor); |
| } |
| |
| Position positionInParentAfterNode(Node* node) |
| { |
| RefPtr currentNode = node; |
| RefPtr ancestor = node->parentNode(); |
| while (ancestor && editingIgnoresContent(*ancestor)) { |
| currentNode = ancestor; |
| ancestor = ancestor->parentNode(); |
| } |
| ASSERT(ancestor); |
| return Position(ancestor, currentNode->computeNodeIndex() + 1, Position::PositionIsOffsetInAnchor); |
| } |
| |
| Position makeContainerOffsetPosition(const BoundaryPoint& point) |
| { |
| return makeContainerOffsetPosition(point.container.copyRef(), point.offset); |
| } |
| |
| Position makeDeprecatedLegacyPosition(const BoundaryPoint& point) |
| { |
| return makeDeprecatedLegacyPosition(point.container.copyRef(), point.offset); |
| } |
| |
| std::optional<BoundaryPoint> makeBoundaryPoint(const Position& position) |
| { |
| RefPtr container { position.containerNode() }; |
| if (!container) |
| return std::nullopt; |
| return BoundaryPoint { container.releaseNonNull(), static_cast<unsigned>(position.computeOffsetInContainerNode()) }; |
| } |
| |
| template<TreeType treeType> std::partial_ordering treeOrder(const Position& a, const Position& b) |
| { |
| if (a.isNull() || b.isNull()) |
| return a.isNull() && b.isNull() ? std::partial_ordering::equivalent : std::partial_ordering::unordered; |
| |
| RefPtr aContainer = a.containerNode(); |
| RefPtr bContainer = b.containerNode(); |
| |
| if (!aContainer || !bContainer) { |
| if (!commonInclusiveAncestor<treeType>(*a.anchorNode(), *b.anchorNode())) |
| return std::partial_ordering::unordered; |
| if (!aContainer && !bContainer && a.anchorType() == b.anchorType()) |
| return std::partial_ordering::equivalent; |
| if (bContainer) |
| return a.anchorType() == Position::PositionIsBeforeAnchor ? std::partial_ordering::less : std::partial_ordering::greater; |
| return b.anchorType() == Position::PositionIsBeforeAnchor ? std::partial_ordering::greater : std::partial_ordering::less; |
| } |
| |
| // FIXME: Avoid computing node offset for cases where we don't need to. |
| |
| return treeOrder<treeType>(*makeBoundaryPoint(a), *makeBoundaryPoint(b)); |
| } |
| |
| std::partial_ordering operator<=>(const Position& a, const Position& b) |
| { |
| return treeOrder<ComposedTree>(a, b); |
| } |
| |
| template std::partial_ordering treeOrder<ComposedTree>(const Position&, const Position&); |
| template std::partial_ordering treeOrder<ShadowIncludingTree>(const Position&, const Position&); |
| |
| } // namespace WebCore |
| |
| #if ENABLE(TREE_DEBUGGING) |
| |
| void showTree(const WebCore::Position& pos) |
| { |
| pos.showTreeForThis(); |
| } |
| |
| void showTree(const WebCore::Position* pos) |
| { |
| if (pos) |
| pos->showTreeForThis(); |
| } |
| |
| #endif |