| /* |
| * Copyright (C) 2004, 2005, 2006, 2009 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "core/dom/Position.h" |
| |
| #include "core/HTMLNames.h" |
| #include "core/css/CSSComputedStyleDeclaration.h" |
| #include "core/dom/PositionIterator.h" |
| #include "core/dom/Text.h" |
| #include "core/dom/shadow/ElementShadow.h" |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/VisiblePosition.h" |
| #include "core/editing/VisibleUnits.h" |
| #include "core/editing/iterators/TextIterator.h" |
| #include "core/frame/LocalFrame.h" |
| #include "core/frame/Settings.h" |
| #include "core/html/HTMLTableElement.h" |
| #include "core/layout/LayoutBlock.h" |
| #include "core/layout/LayoutInline.h" |
| #include "core/layout/LayoutText.h" |
| #include "core/layout/line/InlineIterator.h" |
| #include "core/layout/line/InlineTextBox.h" |
| #include "platform/Logging.h" |
| #include "wtf/text/CString.h" |
| #include "wtf/text/CharacterNames.h" |
| #include <stdio.h> |
| |
| namespace blink { |
| |
| using namespace HTMLNames; |
| |
| #if ENABLE(ASSERT) |
| static bool canBeAnchorNode(Node* node) |
| { |
| if (!node || node->isFirstLetterPseudoElement()) |
| return true; |
| return !node->isPseudoElement(); |
| } |
| #endif |
| |
| // TODO(yosin) We should move |nextRenderedEditable()| to "EditingUtilities.cpp" with |
| // |rendersInDifferentPosition()|. |
| static Node* nextRenderedEditable(Node* node) |
| { |
| for (node = nextAtomicLeafNode(*node); node; node = nextAtomicLeafNode(*node)) { |
| LayoutObject* layoutObject = node->layoutObject(); |
| if (!layoutObject) |
| continue; |
| if (!node->hasEditableStyle()) |
| continue; |
| if ((layoutObject->isBox() && toLayoutBox(layoutObject)->inlineBoxWrapper()) || (layoutObject->isText() && toLayoutText(layoutObject)->firstTextBox())) |
| return node; |
| } |
| return 0; |
| } |
| |
| // TODO(yosin) We should move |previousRenderedEditable()| to "EditingUtilities.cpp" |
| // with |rendersInDifferentPosition()|. |
| static Node* previousRenderedEditable(Node* node) |
| { |
| for (node = previousAtomicLeafNode(*node); node; node = previousAtomicLeafNode(*node)) { |
| LayoutObject* layoutObject = node->layoutObject(); |
| if (!layoutObject) |
| continue; |
| if (!node->hasEditableStyle()) |
| continue; |
| if ((layoutObject->isBox() && toLayoutBox(layoutObject)->inlineBoxWrapper()) || (layoutObject->isText() && toLayoutText(layoutObject)->firstTextBox())) |
| return node; |
| } |
| return 0; |
| } |
| |
| template <typename Strategy> |
| const TreeScope* PositionAlgorithm<Strategy>::commonAncestorTreeScope(const PositionAlgorithm<Strategy>& a, const PositionAlgorithm<Strategy>& b) |
| { |
| if (!a.computeContainerNode() || !b.computeContainerNode()) |
| return nullptr; |
| return a.computeContainerNode()->treeScope().commonAncestorTreeScope(b.computeContainerNode()->treeScope()); |
| } |
| |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::createLegacyEditingPosition(PassRefPtrWillBeRawPtr<Node> anchorNode, int offset) |
| { |
| if (!anchorNode || anchorNode->isTextNode()) |
| return PositionAlgorithm<Strategy>(anchorNode, offset); |
| |
| if (!Strategy::editingIgnoresContent(anchorNode.get())) |
| return PositionAlgorithm<Strategy>(anchorNode, offset); |
| |
| if (offset == 0) |
| return PositionAlgorithm<Strategy>(anchorNode, PositionAnchorType::BeforeAnchor); |
| |
| ASSERT(offset == Strategy::lastOffsetForEditing(anchorNode.get())); |
| return PositionAlgorithm<Strategy>(anchorNode, PositionAnchorType::AfterAnchor); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy>::PositionAlgorithm(PassRefPtrWillBeRawPtr<Node> anchorNode, PositionAnchorType anchorType) |
| : m_anchorNode(anchorNode) |
| , m_offset(0) |
| , m_anchorType(anchorType) |
| { |
| if (!m_anchorNode) { |
| m_anchorType = PositionAnchorType::OffsetInAnchor; |
| return; |
| } |
| if (m_anchorNode->isTextNode()) { |
| ASSERT(m_anchorType == PositionAnchorType::BeforeAnchor || m_anchorType == PositionAnchorType::AfterAnchor); |
| return; |
| } |
| ASSERT(canBeAnchorNode(m_anchorNode.get())); |
| ASSERT(m_anchorType != PositionAnchorType::OffsetInAnchor); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy>::PositionAlgorithm(PassRefPtrWillBeRawPtr<Node> anchorNode, int offset) |
| : m_anchorNode(anchorNode) |
| , m_offset(offset) |
| , m_anchorType(PositionAnchorType::OffsetInAnchor) |
| { |
| ASSERT(offset >= 0); |
| ASSERT(canBeAnchorNode(m_anchorNode.get())); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy>::PositionAlgorithm(const PositionAlgorithm& other) |
| : m_anchorNode(other.m_anchorNode) |
| , m_offset(other.m_offset) |
| , m_anchorType(other.m_anchorType) |
| { |
| } |
| |
| // -- |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::computeContainerNode() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| |
| switch (anchorType()) { |
| case PositionAnchorType::BeforeChildren: |
| case PositionAnchorType::AfterChildren: |
| case PositionAnchorType::OffsetInAnchor: |
| return m_anchorNode.get(); |
| case PositionAnchorType::BeforeAnchor: |
| case PositionAnchorType::AfterAnchor: |
| return Strategy::parent(*m_anchorNode); |
| } |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::computeOffsetInContainerNode() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| |
| switch (anchorType()) { |
| case PositionAnchorType::BeforeChildren: |
| return 0; |
| case PositionAnchorType::AfterChildren: |
| return lastOffsetInNode(m_anchorNode.get()); |
| case PositionAnchorType::OffsetInAnchor: |
| return minOffsetForNode(m_anchorNode.get(), m_offset); |
| case PositionAnchorType::BeforeAnchor: |
| return Strategy::index(*m_anchorNode); |
| case PositionAnchorType::AfterAnchor: |
| return Strategy::index(*m_anchorNode) + 1; |
| } |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| // Neighbor-anchored positions are invalid DOM positions, so they need to be |
| // fixed up before handing them off to the Range object. |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::parentAnchoredEquivalent() const |
| { |
| if (!m_anchorNode) |
| return PositionAlgorithm<Strategy>(); |
| |
| // FIXME: This should only be necessary for legacy positions, but is also needed for positions before and after Tables |
| if (m_offset == 0 && !isAfterAnchorOrAfterChildren()) { |
| if (Strategy::parent(*m_anchorNode) && (Strategy::editingIgnoresContent(m_anchorNode.get()) || isRenderedHTMLTableElement(m_anchorNode.get()))) |
| return inParentBeforeNode(*m_anchorNode); |
| return PositionAlgorithm<Strategy>(m_anchorNode.get(), 0); |
| } |
| if (!m_anchorNode->offsetInCharacters() |
| && (isAfterAnchorOrAfterChildren() || static_cast<unsigned>(m_offset) == m_anchorNode->countChildren()) |
| && (Strategy::editingIgnoresContent(m_anchorNode.get()) || isRenderedHTMLTableElement(m_anchorNode.get())) |
| && computeContainerNode()) { |
| return inParentAfterNode(*m_anchorNode); |
| } |
| |
| return PositionAlgorithm<Strategy>(computeContainerNode(), computeOffsetInContainerNode()); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::toOffsetInAnchor() const |
| { |
| if (isNull()) |
| return PositionAlgorithm<Strategy>(); |
| |
| return PositionAlgorithm<Strategy>(computeContainerNode(), computeOffsetInContainerNode()); |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::computeEditingOffset() const |
| { |
| if (isAfterAnchorOrAfterChildren()) |
| return Strategy::lastOffsetForEditing(m_anchorNode.get()); |
| return m_offset; |
| } |
| |
| // TODO(yosin) We should replace all usage of |deprecatedEditingOffset()| by |
| // |computeEditingOffset()|. |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::deprecatedEditingOffset() const |
| { |
| return computeEditingOffset(); |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::computeNodeBeforePosition() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| switch (anchorType()) { |
| case PositionAnchorType::BeforeChildren: |
| return 0; |
| case PositionAnchorType::AfterChildren: |
| return Strategy::lastChild(*m_anchorNode); |
| case PositionAnchorType::OffsetInAnchor: |
| return m_offset ? Strategy::childAt(*m_anchorNode, m_offset - 1) : 0; |
| case PositionAnchorType::BeforeAnchor: |
| return Strategy::previousSibling(*m_anchorNode); |
| case PositionAnchorType::AfterAnchor: |
| return m_anchorNode.get(); |
| } |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::computeNodeAfterPosition() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| |
| switch (anchorType()) { |
| case PositionAnchorType::BeforeChildren: |
| return Strategy::firstChild(*m_anchorNode); |
| case PositionAnchorType::AfterChildren: |
| return 0; |
| case PositionAnchorType::OffsetInAnchor: |
| return Strategy::childAt(*m_anchorNode, m_offset); |
| case PositionAnchorType::BeforeAnchor: |
| return m_anchorNode.get(); |
| case PositionAnchorType::AfterAnchor: |
| return Strategy::nextSibling(*m_anchorNode); |
| } |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| // An implementation of |Range::firstNode()|. |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::nodeAsRangeFirstNode() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| if (!isOffsetInAnchor()) |
| return toOffsetInAnchor().nodeAsRangeFirstNode(); |
| if (m_anchorNode->offsetInCharacters()) |
| return m_anchorNode.get(); |
| if (Node* child = Strategy::childAt(*m_anchorNode, m_offset)) |
| return child; |
| if (!m_offset) |
| return m_anchorNode.get(); |
| return Strategy::nextSkippingChildren(*m_anchorNode); |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::nodeAsRangeLastNode() const |
| { |
| if (isNull()) |
| return nullptr; |
| if (Node* pastLastNode = nodeAsRangePastLastNode()) |
| return Strategy::previous(*pastLastNode); |
| return &Strategy::lastWithinOrSelf(*computeContainerNode()); |
| } |
| |
| // An implementation of |Range::pastLastNode()|. |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::nodeAsRangePastLastNode() const |
| { |
| if (!m_anchorNode) |
| return nullptr; |
| if (!isOffsetInAnchor()) |
| return toOffsetInAnchor().nodeAsRangePastLastNode(); |
| if (m_anchorNode->offsetInCharacters()) |
| return Strategy::nextSkippingChildren(*m_anchorNode); |
| if (Node* child = Strategy::childAt(*m_anchorNode, m_offset)) |
| return child; |
| return Strategy::nextSkippingChildren(*m_anchorNode); |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::commonAncestorContainer(const PositionAlgorithm<Strategy>& other) const |
| { |
| return Strategy::commonAncestor(*computeContainerNode(), *other.computeContainerNode()); |
| } |
| |
| Element* associatedElementOf(const Position& position) |
| { |
| Node* node = position.anchorNode(); |
| if (!node || node->isElementNode()) |
| return toElement(node); |
| ContainerNode* parent = NodeTraversal::parent(*node); |
| return parent && parent->isElementNode() ? toElement(parent) : nullptr; |
| } |
| |
| int comparePositions(const PositionInComposedTree& positionA, const PositionInComposedTree& positionB) |
| { |
| ASSERT(positionA.isNotNull()); |
| ASSERT(positionB.isNotNull()); |
| |
| Node* containerA = positionA.computeContainerNode(); |
| Node* containerB = positionB.computeContainerNode(); |
| int offsetA = positionA.computeOffsetInContainerNode(); |
| int offsetB = positionB.computeOffsetInContainerNode(); |
| return comparePositionsInComposedTree(containerA, offsetA, containerB, offsetB); |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::compareTo(const PositionAlgorithm<Strategy>& other) const |
| { |
| return comparePositions(*this, other); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::previous(PositionMoveType moveType) const |
| { |
| Node* node = anchorNode(); |
| if (!node) |
| return PositionAlgorithm<Strategy>(*this); |
| |
| int offset = deprecatedEditingOffset(); |
| |
| if (offset > 0) { |
| if (editingIgnoresContent(node)) |
| return beforeNode(node); |
| if (Node* child = Strategy::childAt(*node, offset - 1)) |
| return lastPositionInOrAfterNode(child); |
| |
| // 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 PositionAlgorithm<Strategy>(node, offset - 1); |
| case Character: |
| return PositionAlgorithm<Strategy>(node, uncheckedPreviousOffset(node, offset)); |
| case BackwardDeletion: |
| return PositionAlgorithm<Strategy>(node, uncheckedPreviousOffsetForBackwardDeletion(node, offset)); |
| } |
| } |
| |
| if (ContainerNode* parent = Strategy::parent(*node)) { |
| if (editingIgnoresContent(parent)) |
| return beforeNode(parent); |
| // TODO(yosin) We should use |Strategy::index(Node&)| instead of |
| // |Node::nodeIndex()|. |
| return PositionAlgorithm<Strategy>(parent, node->nodeIndex()); |
| } |
| return PositionAlgorithm<Strategy>(*this); |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::next(PositionMoveType moveType) const |
| { |
| ASSERT(moveType != BackwardDeletion); |
| |
| Node* node = anchorNode(); |
| if (!node) |
| return PositionAlgorithm<Strategy>(*this); |
| |
| int offset = deprecatedEditingOffset(); |
| |
| if (Node* child = Strategy::childAt(*node, offset)) |
| return firstPositionInOrBeforeNode(child); |
| |
| // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of |
| // DOM tree version. |
| if (!Strategy::hasChildren(*node) && offset < EditingStrategy::lastOffsetForEditing(node)) { |
| // 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. |
| return createLegacyEditingPosition(node, (moveType == Character) ? uncheckedNextOffset(node, offset) : offset + 1); |
| } |
| |
| if (ContainerNode* parent = Strategy::parent(*node)) |
| return createLegacyEditingPosition(parent, Strategy::index(*node) + 1); |
| return PositionAlgorithm<Strategy>(*this); |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::uncheckedPreviousOffset(const Node* n, int current) |
| { |
| return n->layoutObject() ? n->layoutObject()->previousOffset(current) : current - 1; |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::uncheckedPreviousOffsetForBackwardDeletion(const Node* n, int current) |
| { |
| return n->layoutObject() ? n->layoutObject()->previousOffsetForBackwardDeletion(current) : current - 1; |
| } |
| |
| template <typename Strategy> |
| int PositionAlgorithm<Strategy>::uncheckedNextOffset(const Node* n, int current) |
| { |
| return n->layoutObject() ? n->layoutObject()->nextOffset(current) : current + 1; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::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 PositionAnchorType::OffsetInAnchor: |
| return m_offset == 0; |
| case PositionAnchorType::BeforeChildren: |
| case PositionAnchorType::BeforeAnchor: |
| return true; |
| case PositionAnchorType::AfterChildren: |
| case PositionAnchorType::AfterAnchor: |
| // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead |
| // of DOM tree version. |
| return !EditingStrategy::lastOffsetForEditing(anchorNode()); |
| } |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::atLastEditingPositionForNode() const |
| { |
| if (isNull()) |
| return true; |
| // TODO(yosin): Position after anchor shouldn't be considered as at the |
| // first editing position for node since that position resides outside of |
| // the node. |
| // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of |
| // DOM tree version. |
| return isAfterAnchorOrAfterChildren() || m_offset >= EditingStrategy::lastOffsetForEditing(anchorNode()); |
| } |
| |
| // 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. |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::atEditingBoundary() const |
| { |
| PositionAlgorithm<Strategy> nextPosition = downstream(CanCrossEditingBoundary); |
| if (atFirstEditingPositionForNode() && nextPosition.isNotNull() && !nextPosition.anchorNode()->hasEditableStyle()) |
| return true; |
| |
| PositionAlgorithm<Strategy> prevPosition = upstream(CanCrossEditingBoundary); |
| if (atLastEditingPositionForNode() && prevPosition.isNotNull() && !prevPosition.anchorNode()->hasEditableStyle()) |
| return true; |
| |
| return nextPosition.isNotNull() && !nextPosition.anchorNode()->hasEditableStyle() |
| && prevPosition.isNotNull() && !prevPosition.anchorNode()->hasEditableStyle(); |
| } |
| |
| template <typename Strategy> |
| static ContainerNode* nonShadowBoundaryParentNode(Node* node) |
| { |
| ContainerNode* parent = Strategy::parent(*node); |
| return parent && !parent->isShadowRoot() ? parent : nullptr; |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::parentEditingBoundary() const |
| { |
| if (!m_anchorNode) |
| return 0; |
| |
| Node* documentElement = m_anchorNode->document().documentElement(); |
| if (!documentElement) |
| return 0; |
| |
| Node* boundary = computeContainerNode(); |
| while (boundary != documentElement && nonShadowBoundaryParentNode<Strategy>(boundary) && m_anchorNode->hasEditableStyle() == Strategy::parent(*boundary)->hasEditableStyle()) |
| boundary = nonShadowBoundaryParentNode<Strategy>(boundary); |
| |
| return boundary; |
| } |
| |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::atStartOfTree() const |
| { |
| if (isNull()) |
| return true; |
| return !Strategy::parent(*anchorNode()) && m_offset == 0; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::atEndOfTree() const |
| { |
| if (isNull()) |
| return true; |
| // TODO(yosin) We should use |Strategy::lastOffsetForEditing()| instead of |
| // DOM tree version. |
| return !Strategy::parent(*anchorNode()) && m_offset >= EditingStrategy::lastOffsetForEditing(anchorNode()); |
| } |
| |
| // TODO(yosin) We should move |renderedOffsetOf()| to "EditingUtilities.cpp" with |
| // |rendersInDifferentPosition()|. |
| static int renderedOffsetOf(const Position& position) |
| { |
| const int offset = position.computeEditingOffset(); |
| Node* const anchorNode = position.anchorNode(); |
| if (!anchorNode->isTextNode() || !anchorNode->layoutObject()) |
| return offset; |
| |
| int result = 0; |
| LayoutText* textLayoutObject = toLayoutText(anchorNode->layoutObject()); |
| for (InlineTextBox *box = textLayoutObject->firstTextBox(); box; box = box->nextTextBox()) { |
| int start = box->start(); |
| int end = box->start() + box->len(); |
| if (offset < start) |
| return result; |
| if (offset <= end) { |
| result += offset - start; |
| return result; |
| } |
| result += box->len(); |
| } |
| return result; |
| } |
| |
| // Whether or not [node, 0] and [node, lastOffsetForEditing(node)] are their own VisiblePositions. |
| // If true, adjacent candidates are visually distinct. |
| // FIXME: Disregard nodes with layoutObjects that have no height, as we do in isCandidate. |
| // FIXME: Share code with isCandidate, if possible. |
| static bool endsOfNodeAreVisuallyDistinctPositions(Node* node) |
| { |
| if (!node || !node->layoutObject()) |
| return false; |
| |
| if (!node->layoutObject()->isInline()) |
| return true; |
| |
| // Don't include inline tables. |
| if (isHTMLTableElement(*node)) |
| return false; |
| |
| // A Marquee elements are moving so we should assume their ends are always |
| // visibily distinct. |
| if (isHTMLMarqueeElement(*node)) |
| return true; |
| |
| // There is a VisiblePosition inside an empty inline-block container. |
| return node->layoutObject()->isReplaced() && canHaveChildrenForEditing(node) && toLayoutBox(node->layoutObject())->size().height() != 0 && !node->hasChildren(); |
| } |
| |
| template <typename Strategy> |
| static Node* enclosingVisualBoundary(Node* node) |
| { |
| while (node && !endsOfNodeAreVisuallyDistinctPositions(node)) |
| node = Strategy::parent(*node); |
| |
| 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. |
| template <typename Strategy> |
| static bool isStreamer(const PositionIteratorAlgorithm<Strategy>& pos) |
| { |
| if (!pos.node()) |
| return true; |
| |
| if (isAtomicNode(pos.node())) |
| 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. |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::upstream(EditingBoundaryCrossingRule rule) const |
| { |
| TRACE_EVENT0("blink", "Position::upstream"); |
| |
| Node* startNode = anchorNode(); |
| if (!startNode) |
| return PositionAlgorithm<Strategy>(); |
| |
| // iterate backward from there, looking for a qualified position |
| Node* boundary = enclosingVisualBoundary<Strategy>(startNode); |
| // FIXME: PositionIterator should respect Before and After positions. |
| PositionIteratorAlgorithm<Strategy> lastVisible(isAfterAnchor() ? createLegacyEditingPosition(m_anchorNode.get(), Strategy::caretMaxOffset(*m_anchorNode)) : PositionAlgorithm<Strategy>(*this)); |
| PositionIteratorAlgorithm<Strategy> currentPos = lastVisible; |
| bool startEditable = startNode->hasEditableStyle(); |
| Node* lastNode = startNode; |
| bool boundaryCrossed = false; |
| for (; !currentPos.atStart(); currentPos.decrement()) { |
| Node* currentNode = currentPos.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 != lastNode) { |
| // Don't change editability. |
| bool currentEditable = currentNode->hasEditableStyle(); |
| if (startEditable != currentEditable) { |
| if (rule == CannotCrossEditingBoundary) |
| break; |
| boundaryCrossed = true; |
| } |
| lastNode = currentNode; |
| } |
| |
| // 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) && currentNode != boundary) |
| return lastVisible.deprecatedComputePosition(); |
| |
| // skip position in non-laid out or invisible node |
| LayoutObject* layoutObject = currentNode->layoutObject(); |
| if (!layoutObject || layoutObject->style()->visibility() != VISIBLE) |
| continue; |
| |
| if (rule == CanCrossEditingBoundary && boundaryCrossed) { |
| lastVisible = currentPos; |
| break; |
| } |
| |
| // track last visible streamer position |
| if (isStreamer<Strategy>(currentPos)) |
| lastVisible = currentPos; |
| |
| // 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 nodeIndex() call. |
| if (endsOfNodeAreVisuallyDistinctPositions(currentNode) && currentPos.atStartOfNode()) |
| return lastVisible.deprecatedComputePosition(); |
| |
| // Return position after tables and nodes which have content that can be ignored. |
| if (Strategy::editingIgnoresContent(currentNode) || isRenderedHTMLTableElement(currentNode)) { |
| if (currentPos.atEndOfNode()) |
| return afterNode(currentNode); |
| continue; |
| } |
| |
| // return current position if it is in laid out text |
| if (layoutObject->isText() && toLayoutText(layoutObject)->firstTextBox()) { |
| if (currentNode != 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 |
| // layout 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 >= layoutObject->caretMaxOffset()); |
| return PositionAlgorithm<Strategy>(currentNode, layoutObject->caretMaxOffset()); |
| } |
| |
| unsigned textOffset = currentPos.offsetInLeafNode(); |
| LayoutText* textLayoutObject = toLayoutText(layoutObject); |
| InlineTextBox* lastTextBox = textLayoutObject->lastTextBox(); |
| for (InlineTextBox* box = textLayoutObject->firstTextBox(); box; box = box->nextTextBox()) { |
| if (textOffset <= box->start() + box->len()) { |
| if (textOffset > box->start()) |
| return currentPos.computePosition(); |
| continue; |
| } |
| |
| if (box == lastTextBox || textOffset != box->start() + box->len() + 1) |
| continue; |
| |
| // The text continues on the next line only if the last text box is not on this line and |
| // none of the boxes on this line have a larger start offset. |
| |
| bool continuesOnNextLine = true; |
| InlineBox* otherBox = box; |
| while (continuesOnNextLine) { |
| otherBox = otherBox->nextLeafChild(); |
| if (!otherBox) |
| break; |
| if (otherBox == lastTextBox || (otherBox->layoutObject() == textLayoutObject && toInlineTextBox(otherBox)->start() > textOffset)) |
| continuesOnNextLine = false; |
| } |
| |
| otherBox = box; |
| while (continuesOnNextLine) { |
| otherBox = otherBox->prevLeafChild(); |
| if (!otherBox) |
| break; |
| if (otherBox == lastTextBox || (otherBox->layoutObject() == textLayoutObject && toInlineTextBox(otherBox)->start() > textOffset)) |
| continuesOnNextLine = false; |
| } |
| |
| if (continuesOnNextLine) |
| return currentPos.computePosition(); |
| } |
| } |
| } |
| return lastVisible.deprecatedComputePosition(); |
| } |
| |
| // 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 |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> PositionAlgorithm<Strategy>::downstream(EditingBoundaryCrossingRule rule) const |
| { |
| TRACE_EVENT0("blink", "Position::downstream"); |
| |
| Node* startNode = anchorNode(); |
| if (!startNode) |
| return PositionAlgorithm<Strategy>(); |
| |
| // iterate forward from there, looking for a qualified position |
| Node* boundary = enclosingVisualBoundary<Strategy>(startNode); |
| // FIXME: PositionIterator should respect Before and After positions. |
| PositionIteratorAlgorithm<Strategy> lastVisible(isAfterAnchor() ? createLegacyEditingPosition(m_anchorNode.get(), Strategy::caretMaxOffset(*m_anchorNode)) : PositionAlgorithm<Strategy>(*this)); |
| PositionIteratorAlgorithm<Strategy> currentPos = lastVisible; |
| bool startEditable = startNode->hasEditableStyle(); |
| Node* lastNode = startNode; |
| bool boundaryCrossed = false; |
| for (; !currentPos.atEnd(); currentPos.increment()) { |
| Node* currentNode = currentPos.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 != lastNode) { |
| // Don't change editability. |
| bool currentEditable = currentNode->hasEditableStyle(); |
| if (startEditable != currentEditable) { |
| if (rule == CannotCrossEditingBoundary) |
| break; |
| boundaryCrossed = true; |
| } |
| |
| lastNode = currentNode; |
| } |
| |
| // stop before going above the body, up into the head |
| // return the last visible streamer position |
| if (isHTMLBodyElement(*currentNode) && currentPos.atEndOfNode()) |
| break; |
| |
| // Do not move to a visually distinct position. |
| if (endsOfNodeAreVisuallyDistinctPositions(currentNode) && currentNode != boundary) |
| return lastVisible.deprecatedComputePosition(); |
| // 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->nodeIndex() + 1]. |
| if (boundary && Strategy::parent(*boundary) == currentNode) |
| return lastVisible.deprecatedComputePosition(); |
| |
| // skip position in non-laid out or invisible node |
| LayoutObject* layoutObject = currentNode->layoutObject(); |
| if (!layoutObject || layoutObject->style()->visibility() != VISIBLE) |
| continue; |
| |
| if (rule == CanCrossEditingBoundary && boundaryCrossed) { |
| lastVisible = currentPos; |
| break; |
| } |
| |
| // track last visible streamer position |
| if (isStreamer<Strategy>(currentPos)) |
| lastVisible = currentPos; |
| |
| // Return position before tables and nodes which have content that can be ignored. |
| if (Strategy::editingIgnoresContent(currentNode) || isRenderedHTMLTableElement(currentNode)) { |
| if (currentPos.offsetInLeafNode() <= layoutObject->caretMinOffset()) |
| return createLegacyEditingPosition(currentNode, layoutObject->caretMinOffset()); |
| continue; |
| } |
| |
| // return current position if it is in laid out text |
| if (layoutObject->isText() && toLayoutText(layoutObject)->firstTextBox()) { |
| if (currentNode != startNode) { |
| ASSERT(currentPos.atStartOfNode()); |
| return PositionAlgorithm<Strategy>(currentNode, layoutObject->caretMinOffset()); |
| } |
| |
| unsigned textOffset = currentPos.offsetInLeafNode(); |
| LayoutText* textLayoutObject = toLayoutText(layoutObject); |
| InlineTextBox* lastTextBox = textLayoutObject->lastTextBox(); |
| for (InlineTextBox* box = textLayoutObject->firstTextBox(); box; box = box->nextTextBox()) { |
| if (textOffset <= box->end()) { |
| if (textOffset >= box->start()) |
| return currentPos.computePosition(); |
| continue; |
| } |
| |
| if (box == lastTextBox || textOffset != box->start() + box->len()) |
| continue; |
| |
| // The text continues on the next line only if the last text box is not on this line and |
| // none of the boxes on this line have a larger start offset. |
| |
| bool continuesOnNextLine = true; |
| InlineBox* otherBox = box; |
| while (continuesOnNextLine) { |
| otherBox = otherBox->nextLeafChild(); |
| if (!otherBox) |
| break; |
| if (otherBox == lastTextBox || (otherBox->layoutObject() == textLayoutObject && toInlineTextBox(otherBox)->start() >= textOffset)) |
| continuesOnNextLine = false; |
| } |
| |
| otherBox = box; |
| while (continuesOnNextLine) { |
| otherBox = otherBox->prevLeafChild(); |
| if (!otherBox) |
| break; |
| if (otherBox == lastTextBox || (otherBox->layoutObject() == textLayoutObject && toInlineTextBox(otherBox)->start() >= textOffset)) |
| continuesOnNextLine = false; |
| } |
| |
| if (continuesOnNextLine) |
| return currentPos.computePosition(); |
| } |
| } |
| } |
| |
| return lastVisible.deprecatedComputePosition(); |
| } |
| |
| static int boundingBoxLogicalHeight(LayoutObject *o, const IntRect &rect) |
| { |
| return o->style()->isHorizontalWritingMode() ? rect.height() : rect.width(); |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::hasRenderedNonAnonymousDescendantsWithHeight(LayoutObject* layoutObject) |
| { |
| LayoutObject* stop = layoutObject->nextInPreOrderAfterChildren(); |
| for (LayoutObject *o = layoutObject->slowFirstChild(); o && o != stop; o = o->nextInPreOrder()) { |
| if (o->nonPseudoNode()) { |
| if ((o->isText() && boundingBoxLogicalHeight(o, toLayoutText(o)->linesBoundingBox())) |
| || (o->isBox() && toLayoutBox(o)->pixelSnappedLogicalHeight()) |
| || (o->isLayoutInline() && isEmptyInline(LineLayoutItem(o)) && boundingBoxLogicalHeight(o, toLayoutInline(o)->linesBoundingBox()))) |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::nodeIsUserSelectNone(Node* node) |
| { |
| return node && node->layoutObject() && !node->layoutObject()->isSelectable(); |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::nodeIsUserSelectAll(const Node* node) |
| { |
| return RuntimeEnabledFeatures::userSelectAllEnabled() && node && node->layoutObject() && node->layoutObject()->style()->userSelect() == SELECT_ALL; |
| } |
| |
| template <typename Strategy> |
| Node* PositionAlgorithm<Strategy>::rootUserSelectAllForNode(Node* node) |
| { |
| if (!node || !nodeIsUserSelectAll(node)) |
| return 0; |
| Node* parent = Strategy::parent(*node); |
| if (!parent) |
| return node; |
| |
| Node* candidateRoot = node; |
| while (parent) { |
| if (!parent->layoutObject()) { |
| parent = Strategy::parent(*parent); |
| continue; |
| } |
| if (!nodeIsUserSelectAll(parent)) |
| break; |
| candidateRoot = parent; |
| parent = Strategy::parent(*candidateRoot); |
| } |
| return candidateRoot; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::isCandidate() const |
| { |
| if (isNull()) |
| return false; |
| |
| LayoutObject* layoutObject = anchorNode()->layoutObject(); |
| if (!layoutObject) |
| return false; |
| |
| if (layoutObject->style()->visibility() != VISIBLE) |
| return false; |
| |
| if (layoutObject->isBR()) { |
| // TODO(leviw) The condition should be |
| // m_anchorType == PositionAnchorType::BeforeAnchor, but for now we |
| // still need to support legacy positions. |
| return !m_offset && !isAfterAnchor() && !nodeIsUserSelectNone(Strategy::parent(*anchorNode())); |
| } |
| |
| if (layoutObject->isText()) |
| return !nodeIsUserSelectNone(anchorNode()) && inRenderedText(); |
| |
| if (layoutObject->isSVG()) { |
| // We don't consider SVG elements are contenteditable except for |
| // associated layoutObject returns isText() true, e.g. LayoutSVGInlineText. |
| return false; |
| } |
| |
| if (isRenderedHTMLTableElement(anchorNode()) || Strategy::editingIgnoresContent(anchorNode())) |
| return (atFirstEditingPositionForNode() || atLastEditingPositionForNode()) && !nodeIsUserSelectNone(Strategy::parent(*anchorNode())); |
| |
| if (isHTMLHtmlElement(*m_anchorNode)) |
| return false; |
| |
| if (layoutObject->isLayoutBlockFlow() || layoutObject->isFlexibleBox() || layoutObject->isLayoutGrid()) { |
| if (toLayoutBlock(layoutObject)->logicalHeight() || isHTMLBodyElement(*m_anchorNode)) { |
| if (!hasRenderedNonAnonymousDescendantsWithHeight(layoutObject)) |
| return atFirstEditingPositionForNode() && !nodeIsUserSelectNone(anchorNode()); |
| return m_anchorNode->hasEditableStyle() && !nodeIsUserSelectNone(anchorNode()) && atEditingBoundary(); |
| } |
| } else { |
| LocalFrame* frame = m_anchorNode->document().frame(); |
| bool caretBrowsing = frame->settings() && frame->settings()->caretBrowsingEnabled(); |
| return (caretBrowsing || m_anchorNode->hasEditableStyle()) && !nodeIsUserSelectNone(anchorNode()) && atEditingBoundary(); |
| } |
| |
| return false; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::inRenderedText() const |
| { |
| if (isNull() || !anchorNode()->isTextNode()) |
| return false; |
| |
| LayoutObject* layoutObject = anchorNode()->layoutObject(); |
| if (!layoutObject) |
| return false; |
| |
| LayoutText* textLayoutObject = toLayoutText(layoutObject); |
| for (InlineTextBox *box = textLayoutObject->firstTextBox(); box; box = box->nextTextBox()) { |
| if (m_offset < static_cast<int>(box->start()) && !textLayoutObject->containsReversedText()) { |
| // The offset we're looking for is before this node |
| // this means the offset must be in content that is |
| // not laid out. Return false. |
| return false; |
| } |
| if (box->containsCaretOffset(m_offset)) { |
| // Return false for offsets inside composed characters. |
| return m_offset == 0 || m_offset == textLayoutObject->nextOffset(textLayoutObject->previousOffset(m_offset)); |
| } |
| } |
| |
| return false; |
| } |
| |
| template <typename Strategy> |
| bool PositionAlgorithm<Strategy>::isRenderedCharacter() const |
| { |
| if (isNull()) |
| return false; |
| ASSERT(isOffsetInAnchor()); |
| if (!anchorNode()->isTextNode()) |
| return false; |
| |
| LayoutObject* layoutObject = anchorNode()->layoutObject(); |
| if (!layoutObject) |
| return false; |
| |
| return toLayoutText(layoutObject)->isRenderedCharacter(offsetInContainerNode()); |
| } |
| |
| // TODO(yosin) We should move |rendersInDifferentPosition()to "EditingUtilities.cpp" |
| // with |renderedOffsetOf()|. |
| bool rendersInDifferentPosition(const Position& position1, const Position& position2) |
| { |
| if (position1.isNull() || position2.isNull()) |
| return false; |
| |
| LayoutObject* layoutObject = position1.anchorNode()->layoutObject(); |
| if (!layoutObject) |
| return false; |
| |
| LayoutObject* posLayoutObject = position2.anchorNode()->layoutObject(); |
| if (!posLayoutObject) |
| return false; |
| |
| if (layoutObject->style()->visibility() != VISIBLE |
| || posLayoutObject->style()->visibility() != VISIBLE) |
| return false; |
| |
| if (position1.anchorNode() == position2.anchorNode()) { |
| if (isHTMLBRElement(*position1.anchorNode())) |
| return false; |
| |
| if (position1.deprecatedEditingOffset() == position2.deprecatedEditingOffset()) |
| return false; |
| |
| if (!position1.anchorNode()->isTextNode() && !position2.anchorNode()->isTextNode()) |
| return true; |
| } |
| |
| if (isHTMLBRElement(*position1.anchorNode()) && position2.isCandidate()) |
| return true; |
| |
| if (isHTMLBRElement(*position2.anchorNode()) && position1.isCandidate()) |
| return true; |
| |
| if (!inSameContainingBlockFlowElement(position1.anchorNode(), position2.anchorNode())) |
| return true; |
| |
| if (position1.anchorNode()->isTextNode() && !position1.inRenderedText()) |
| return false; |
| |
| if (position2.anchorNode()->isTextNode() && !position2.inRenderedText()) |
| return false; |
| |
| const int renderedOffset1 = renderedOffsetOf(position1); |
| const int renderedOffset2 = renderedOffsetOf(position2); |
| |
| if (layoutObject == posLayoutObject && renderedOffset1 == renderedOffset2) |
| return false; |
| |
| InlineBoxPosition boxPosition1 = position1.computeInlineBoxPosition(DOWNSTREAM); |
| InlineBoxPosition boxPosition2 = position2.computeInlineBoxPosition(DOWNSTREAM); |
| |
| WTF_LOG(Editing, "layoutObject1: %p [%p]\n", layoutObject, boxPosition1.inlineBox); |
| WTF_LOG(Editing, "renderedOffset1: %d\n", renderedOffset1); |
| WTF_LOG(Editing, "layoutObject2: %p [%p]\n", posLayoutObject, boxPosition2.inlineBox); |
| WTF_LOG(Editing, "renderedOffset2: %d\n", renderedOffset2); |
| WTF_LOG(Editing, "node1 min/max: %d:%d\n", caretMinOffset(position1.anchorNode()), caretMaxOffset(position1.anchorNode())); |
| WTF_LOG(Editing, "node2 min/max: %d:%d\n", caretMinOffset(position2.anchorNode()), caretMaxOffset(position2.anchorNode())); |
| WTF_LOG(Editing, "----------------------------------------------------------------------\n"); |
| |
| if (!boxPosition1.inlineBox || !boxPosition2.inlineBox) { |
| return false; |
| } |
| |
| if (boxPosition1.inlineBox->root() != boxPosition2.inlineBox->root()) { |
| return true; |
| } |
| |
| if (nextRenderedEditable(position1.anchorNode()) == position2.anchorNode() |
| && renderedOffset1 == caretMaxOffset(position1.anchorNode()) && !renderedOffset2) { |
| return false; |
| } |
| |
| if (previousRenderedEditable(position1.anchorNode()) == position2.anchorNode() |
| && !renderedOffset1 && renderedOffset2 == caretMaxOffset(position2.anchorNode())) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| template <typename Strategy> |
| InlineBoxPosition PositionAlgorithm<Strategy>::computeInlineBoxPosition(EAffinity affinity) const |
| { |
| return computeInlineBoxPosition(affinity, primaryDirectionOf(*anchorNode())); |
| } |
| |
| static bool isNonTextLeafChild(LayoutObject* object) |
| { |
| if (object->slowFirstChild()) |
| return false; |
| if (object->isText()) |
| return false; |
| return true; |
| } |
| |
| static InlineTextBox* searchAheadForBetterMatch(LayoutObject* layoutObject) |
| { |
| LayoutBlock* container = layoutObject->containingBlock(); |
| for (LayoutObject* next = layoutObject->nextInPreOrder(container); next; next = next->nextInPreOrder(container)) { |
| if (next->isLayoutBlock()) |
| return 0; |
| if (next->isBR()) |
| return 0; |
| if (isNonTextLeafChild(next)) |
| return 0; |
| if (next->isText()) { |
| InlineTextBox* match = 0; |
| int minOffset = INT_MAX; |
| for (InlineTextBox* box = toLayoutText(next)->firstTextBox(); box; box = box->nextTextBox()) { |
| int caretMinOffset = box->caretMinOffset(); |
| if (caretMinOffset < minOffset) { |
| match = box; |
| minOffset = caretMinOffset; |
| } |
| } |
| if (match) |
| return match; |
| } |
| } |
| return 0; |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> downstreamIgnoringEditingBoundaries(PositionAlgorithm<Strategy> position) |
| { |
| PositionAlgorithm<Strategy> lastPosition; |
| while (position != lastPosition) { |
| lastPosition = position; |
| position = position.downstream(CanCrossEditingBoundary); |
| } |
| return position; |
| } |
| |
| template <typename Strategy> |
| PositionAlgorithm<Strategy> upstreamIgnoringEditingBoundaries(PositionAlgorithm<Strategy> position) |
| { |
| PositionAlgorithm<Strategy> lastPosition; |
| while (position != lastPosition) { |
| lastPosition = position; |
| position = position.upstream(CanCrossEditingBoundary); |
| } |
| return position; |
| } |
| |
| template <typename Strategy> |
| InlineBoxPosition PositionAlgorithm<Strategy>::computeInlineBoxPosition(EAffinity affinity, TextDirection primaryDirection) const |
| { |
| InlineBox* inlineBox = nullptr; |
| int caretOffset = deprecatedEditingOffset(); |
| LayoutObject* layoutObject = m_anchorNode->isShadowRoot() ? toShadowRoot(m_anchorNode)->host()->layoutObject() : m_anchorNode->layoutObject(); |
| |
| if (!layoutObject->isText()) { |
| inlineBox = 0; |
| if (canHaveChildrenForEditing(anchorNode()) && layoutObject->isLayoutBlockFlow() && hasRenderedNonAnonymousDescendantsWithHeight(layoutObject)) { |
| // 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 LayoutObject::createVisiblePosition(). |
| PositionAlgorithm<Strategy> thisPosition = PositionAlgorithm<Strategy>(*this); |
| PositionAlgorithm<Strategy> equivalent = downstreamIgnoringEditingBoundaries(thisPosition); |
| if (equivalent == thisPosition) { |
| equivalent = upstreamIgnoringEditingBoundaries(thisPosition); |
| if (equivalent == thisPosition || downstreamIgnoringEditingBoundaries(equivalent) == thisPosition) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| } |
| |
| return equivalent.computeInlineBoxPosition(UPSTREAM, primaryDirection); |
| } |
| if (layoutObject->isBox()) { |
| inlineBox = toLayoutBox(layoutObject)->inlineBoxWrapper(); |
| if (!inlineBox || (caretOffset > inlineBox->caretMinOffset() && caretOffset < inlineBox->caretMaxOffset())) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| } |
| } else { |
| LayoutText* textLayoutObject = toLayoutText(layoutObject); |
| |
| InlineTextBox* box; |
| InlineTextBox* candidate = 0; |
| |
| for (box = textLayoutObject->firstTextBox(); box; box = box->nextTextBox()) { |
| int caretMinOffset = box->caretMinOffset(); |
| int caretMaxOffset = box->caretMaxOffset(); |
| |
| if (caretOffset < caretMinOffset || caretOffset > caretMaxOffset || (caretOffset == caretMaxOffset && box->isLineBreak())) |
| continue; |
| |
| if (caretOffset > caretMinOffset && caretOffset < caretMaxOffset) |
| return InlineBoxPosition(box, caretOffset); |
| |
| if (((caretOffset == caretMaxOffset) ^ (affinity == DOWNSTREAM)) |
| || ((caretOffset == caretMinOffset) ^ (affinity == UPSTREAM)) |
| || (caretOffset == caretMaxOffset && box->nextLeafChild() && box->nextLeafChild()->isLineBreak())) |
| break; |
| |
| candidate = box; |
| } |
| if (candidate && candidate == textLayoutObject->lastTextBox() && affinity == DOWNSTREAM) { |
| box = searchAheadForBetterMatch(textLayoutObject); |
| if (box) |
| caretOffset = box->caretMinOffset(); |
| } |
| inlineBox = box ? box : candidate; |
| } |
| |
| if (!inlineBox) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| unsigned char level = inlineBox->bidiLevel(); |
| |
| if (inlineBox->direction() == primaryDirection) { |
| if (caretOffset == inlineBox->caretRightmostOffset()) { |
| InlineBox* nextBox = inlineBox->nextLeafChild(); |
| if (!nextBox || nextBox->bidiLevel() >= level) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| level = nextBox->bidiLevel(); |
| InlineBox* prevBox = inlineBox; |
| do { |
| prevBox = prevBox->prevLeafChild(); |
| } while (prevBox && prevBox->bidiLevel() > level); |
| |
| // For example, abc FED 123 ^ CBA |
| if (prevBox && prevBox->bidiLevel() == level) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| // For example, abc 123 ^ CBA |
| while (InlineBox* nextBox = inlineBox->nextLeafChild()) { |
| if (nextBox->bidiLevel() < level) |
| break; |
| inlineBox = nextBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretRightmostOffset()); |
| } |
| |
| InlineBox* prevBox = inlineBox->prevLeafChild(); |
| if (!prevBox || prevBox->bidiLevel() >= level) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| level = prevBox->bidiLevel(); |
| InlineBox* nextBox = inlineBox; |
| do { |
| nextBox = nextBox->nextLeafChild(); |
| } while (nextBox && nextBox->bidiLevel() > level); |
| |
| if (nextBox && nextBox->bidiLevel() == level) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| while (InlineBox* prevBox = inlineBox->prevLeafChild()) { |
| if (prevBox->bidiLevel() < level) |
| break; |
| inlineBox = prevBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretLeftmostOffset()); |
| } |
| |
| if (caretOffset == inlineBox->caretLeftmostOffset()) { |
| InlineBox* prevBox = inlineBox->prevLeafChildIgnoringLineBreak(); |
| if (!prevBox || prevBox->bidiLevel() < level) { |
| // Left edge of a secondary run. Set to the right edge of the entire |
| // run. |
| while (InlineBox* nextBox = inlineBox->nextLeafChildIgnoringLineBreak()) { |
| if (nextBox->bidiLevel() < level) |
| break; |
| inlineBox = nextBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretRightmostOffset()); |
| } |
| |
| if (prevBox->bidiLevel() > level) { |
| // Right edge of a "tertiary" run. Set to the left edge of that run. |
| while (InlineBox* tertiaryBox = inlineBox->prevLeafChildIgnoringLineBreak()) { |
| if (tertiaryBox->bidiLevel() <= level) |
| break; |
| inlineBox = tertiaryBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretLeftmostOffset()); |
| } |
| return InlineBoxPosition(inlineBox, caretOffset); |
| } |
| |
| if (layoutObject && layoutObject->style()->unicodeBidi() == Plaintext) { |
| if (inlineBox->bidiLevel() < level) |
| return InlineBoxPosition(inlineBox, inlineBox->caretLeftmostOffset()); |
| return InlineBoxPosition(inlineBox, inlineBox->caretRightmostOffset()); |
| } |
| |
| InlineBox* nextBox = inlineBox->nextLeafChildIgnoringLineBreak(); |
| if (!nextBox || nextBox->bidiLevel() < level) { |
| // Right edge of a secondary run. Set to the left edge of the entire |
| // run. |
| while (InlineBox* prevBox = inlineBox->prevLeafChildIgnoringLineBreak()) { |
| if (prevBox->bidiLevel() < level) |
| break; |
| inlineBox = prevBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretLeftmostOffset()); |
| } |
| |
| if (nextBox->bidiLevel() <= level) |
| return InlineBoxPosition(inlineBox, caretOffset); |
| |
| // Left edge of a "tertiary" run. Set to the right edge of that run. |
| while (InlineBox* tertiaryBox = inlineBox->nextLeafChildIgnoringLineBreak()) { |
| if (tertiaryBox->bidiLevel() <= level) |
| break; |
| inlineBox = tertiaryBox; |
| } |
| return InlineBoxPosition(inlineBox, inlineBox->caretRightmostOffset()); |
| } |
| |
| template <typename Strategy> |
| void PositionAlgorithm<Strategy>::debugPosition(const char* msg) const |
| { |
| static const char* const anchorTypes[] = { |
| "OffsetInAnchor", |
| "BeforeAnchor", |
| "AfterAnchor", |
| "BeforeChildren", |
| "AfterChildren", |
| "Invalid", |
| }; |
| |
| if (isNull()) { |
| fprintf(stderr, "Position [%s]: null\n", msg); |
| return; |
| } |
| |
| const char* anchorType = anchorTypes[std::min(static_cast<size_t>(m_anchorType), WTF_ARRAY_LENGTH(anchorTypes) - 1)]; |
| if (m_anchorNode->isTextNode()) { |
| fprintf(stderr, "Position [%s]: %s [%p] %s, (%s) at %d\n", msg, anchorNode()->nodeName().utf8().data(), anchorNode(), anchorType, m_anchorNode->nodeValue().utf8().data(), m_offset); |
| return; |
| } |
| |
| fprintf(stderr, "Position [%s]: %s [%p] %s at %d\n", msg, anchorNode()->nodeName().utf8().data(), anchorNode(), anchorType, m_offset); |
| } |
| |
| PositionInComposedTree toPositionInComposedTree(const Position& pos) |
| { |
| if (pos.isNull()) |
| return PositionInComposedTree(); |
| |
| PositionInComposedTree position; |
| if (pos.isOffsetInAnchor()) { |
| Node* anchor = pos.anchorNode(); |
| if (anchor->offsetInCharacters()) |
| return PositionInComposedTree(anchor, pos.computeOffsetInContainerNode()); |
| ASSERT(!isActiveInsertionPoint(*anchor)); |
| int offset = pos.computeOffsetInContainerNode(); |
| Node* child = NodeTraversal::childAt(*anchor, offset); |
| if (!child) { |
| if (anchor->isShadowRoot()) |
| return PositionInComposedTree(anchor->shadowHost(), PositionAnchorType::AfterChildren); |
| return PositionInComposedTree(anchor, PositionAnchorType::AfterChildren); |
| } |
| child->updateDistribution(); |
| if (isActiveInsertionPoint(*child)) { |
| if (anchor->isShadowRoot()) |
| return PositionInComposedTree(anchor->shadowHost(), offset); |
| return PositionInComposedTree(anchor, offset); |
| } |
| return PositionInComposedTree(ComposedTreeTraversal::parent(*child), ComposedTreeTraversal::index(*child)); |
| } |
| |
| return PositionInComposedTree(pos.anchorNode(), pos.anchorType()); |
| } |
| |
| Position toPositionInDOMTree(const Position& position) |
| { |
| return position; |
| } |
| |
| Position toPositionInDOMTree(const PositionInComposedTree& position) |
| { |
| if (position.isNull()) |
| return Position(); |
| |
| Node* anchorNode = position.anchorNode(); |
| |
| switch (position.anchorType()) { |
| case PositionAnchorType::AfterChildren: |
| // FIXME: When anchorNode is <img>, assertion fails in the constructor. |
| return Position(anchorNode, PositionAnchorType::AfterChildren); |
| case PositionAnchorType::AfterAnchor: |
| return positionAfterNode(anchorNode); |
| case PositionAnchorType::BeforeChildren: |
| return Position(anchorNode, PositionAnchorType::BeforeChildren); |
| case PositionAnchorType::BeforeAnchor: |
| return positionBeforeNode(anchorNode); |
| case PositionAnchorType::OffsetInAnchor: { |
| int offset = position.offsetInContainerNode(); |
| if (anchorNode->offsetInCharacters()) |
| return Position(anchorNode, offset); |
| Node* child = ComposedTreeTraversal::childAt(*anchorNode, offset); |
| if (child) |
| return Position(child->parentNode(), child->nodeIndex()); |
| if (!position.offsetInContainerNode()) |
| return Position(anchorNode, PositionAnchorType::BeforeChildren); |
| |
| // |child| is null when the position is at the end of the children. |
| // <div>foo|</div> |
| return Position(anchorNode, PositionAnchorType::AfterChildren); |
| } |
| default: |
| ASSERT_NOT_REACHED(); |
| return Position(); |
| } |
| } |
| |
| #ifndef NDEBUG |
| |
| template <typename Strategy> |
| void PositionAlgorithm<Strategy>::formatForDebugger(char* buffer, unsigned length) const |
| { |
| StringBuilder result; |
| |
| if (isNull()) { |
| result.appendLiteral("<null>"); |
| } else { |
| char s[1024]; |
| result.appendLiteral("offset "); |
| result.appendNumber(m_offset); |
| result.appendLiteral(" of "); |
| anchorNode()->formatForDebugger(s, sizeof(s)); |
| result.append(s); |
| } |
| |
| strncpy(buffer, result.toString().utf8().data(), length - 1); |
| } |
| |
| template <typename Strategy> |
| void PositionAlgorithm<Strategy>::showAnchorTypeAndOffset() const |
| { |
| switch (anchorType()) { |
| case PositionAnchorType::OffsetInAnchor: |
| fputs("offset", stderr); |
| break; |
| case PositionAnchorType::BeforeChildren: |
| fputs("beforeChildren", stderr); |
| break; |
| case PositionAnchorType::AfterChildren: |
| fputs("afterChildren", stderr); |
| break; |
| case PositionAnchorType::BeforeAnchor: |
| fputs("before", stderr); |
| break; |
| case PositionAnchorType::AfterAnchor: |
| fputs("after", stderr); |
| break; |
| } |
| fprintf(stderr, ", offset:%d\n", m_offset); |
| } |
| |
| template <typename Strategy> |
| void PositionAlgorithm<Strategy>::showTreeForThis() const |
| { |
| if (!anchorNode()) |
| return; |
| anchorNode()->showTreeForThis(); |
| showAnchorTypeAndOffset(); |
| } |
| |
| template <typename Strategy> |
| void PositionAlgorithm<Strategy>::showTreeForThisInComposedTree() const |
| { |
| if (!anchorNode()) |
| return; |
| anchorNode()->showTreeForThisInComposedTree(); |
| showAnchorTypeAndOffset(); |
| } |
| |
| #endif |
| |
| template class CORE_TEMPLATE_EXPORT PositionAlgorithm<EditingStrategy>; |
| template class CORE_TEMPLATE_EXPORT PositionAlgorithm<EditingInComposedTreeStrategy>; |
| |
| } // namespace blink |
| |
| #ifndef NDEBUG |
| |
| void showTree(const blink::Position& pos) |
| { |
| pos.showTreeForThis(); |
| } |
| |
| void showTree(const blink::Position* pos) |
| { |
| if (pos) |
| pos->showTreeForThis(); |
| else |
| fprintf(stderr, "Cannot showTree for (nil)\n"); |
| } |
| |
| #endif |