blob: dab06b15ca8af258f55039d504859feb5c1f8885 [file] [log] [blame]
/*
* Copyright (C) 2004-2025 Apple Inc. All rights reserved.
* Copyright (C) 2015 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 "Editing.h"
#include "AXObjectCache.h"
#include "CachedImage.h"
#include "ContainerNodeInlines.h"
#include "EditingInlines.h"
#include "Editor.h"
#include "ElementChildIteratorInlines.h"
#include "ElementInlines.h"
#include "ElementRareData.h"
#include "FrameDestructionObserverInlines.h"
#include "GraphicsLayer.h"
#include "HTMLBodyElement.h"
#include "HTMLDListElement.h"
#include "HTMLDivElement.h"
#include "HTMLElementFactory.h"
#include "HTMLImageElement.h"
#include "HTMLInterchange.h"
#include "HTMLLIElement.h"
#include "HTMLNames.h"
#include "HTMLOListElement.h"
#include "HTMLParagraphElement.h"
#include "HTMLPictureElement.h"
#include "HTMLSpanElement.h"
#include "HTMLTableElement.h"
#include "HTMLTextFormControlElement.h"
#include "HTMLUListElement.h"
#include "HitTestSource.h"
#include "ImageOverlay.h"
#include "LocalFrame.h"
#include "NodeTraversal.h"
#include "PositionIterator.h"
#include "Range.h"
#include "RenderBlock.h"
#include "RenderElement.h"
#include "RenderLayer.h"
#include "RenderLayerBacking.h"
#include "RenderObjectInlines.h"
#include "RenderStyle+GettersInlines.h"
#include "RenderTableCell.h"
#include "RenderTextControlSingleLine.h"
#include "RenderedPosition.h"
#include "ShadowRoot.h"
#include "Text.h"
#include "TextControlInnerElements.h"
#include "TextIterator.h"
#include "VisibleUnits.h"
#include "WritingMode.h"
#include <wtf/Assertions.h>
#include <wtf/IterationStatus.h>
#include <wtf/StdLibExtras.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/unicode/CharacterNames.h>
namespace WebCore {
using namespace HTMLNames;
static bool isVisiblyAdjacent(const Position&, const Position&);
bool canHaveChildrenForEditing(const Node& node)
{
return !is<Text>(node) && !is<HTMLPictureElement>(node) && node.canContainRangeEndPoint();
}
// Atomic means that the node has no children, or has children which are ignored for the purposes of editing.
bool isAtomicNode(const Node* node)
{
return node && (!node->hasChildNodes() || editingIgnoresContent(*node));
}
RefPtr<ContainerNode> highestEditableRoot(const Position& position, EditableType editableType)
{
RefPtr<ContainerNode> highestEditableRoot = editableRootForPosition(position, editableType);
if (!highestEditableRoot)
return nullptr;
for (RefPtr<ContainerNode> node = highestEditableRoot; !is<HTMLBodyElement>(*node); ) {
node = node->parentNode();
if (!node)
break;
// FIXME: Can this ever be a Document or DocumentFragment? If not, this should return Element* instead.
if (hasEditableStyle(*node, editableType))
highestEditableRoot = node;
}
return highestEditableRoot;
}
Element* lowestEditableAncestor(Node* node)
{
for (RefPtr currentNode = node; currentNode; currentNode = currentNode->parentNode()) {
if (currentNode->hasEditableStyle())
return currentNode->rootEditableElement();
if (is<HTMLBodyElement>(*currentNode))
break;
}
return nullptr;
}
static bool isEditableToAccessibility(const Node& node)
{
ASSERT(AXObjectCache::accessibilityEnabled());
ASSERT(node.document().existingAXObjectCache());
if (CheckedPtr cache = node.document().existingAXObjectCache())
return cache->rootAXEditableElement(&node);
return false;
}
static bool computeEditability(const Node& node, EditableType editableType, Node::ShouldUpdateStyle shouldUpdateStyle)
{
if (node.computeEditability(Node::UserSelectAllTreatment::NotEditable, shouldUpdateStyle) != Node::Editability::ReadOnly)
return true;
switch (editableType) {
case ContentIsEditable:
return false;
case HasEditableAXRole:
return isEditableToAccessibility(node);
}
ASSERT_NOT_REACHED();
return false;
}
bool hasEditableStyle(const Node& node, EditableType editableType)
{
return computeEditability(node, editableType, Node::ShouldUpdateStyle::DoNotUpdate);
}
bool isEditableNode(const Node& node)
{
return computeEditability(node, ContentIsEditable, Node::ShouldUpdateStyle::Update);
}
bool isEditablePosition(const Position& position, EditableType editableType)
{
RefPtr node = position.containerNode();
return node && computeEditability(*node, editableType, Node::ShouldUpdateStyle::Update);
}
bool isAtUnsplittableElement(const Position& position)
{
RefPtr node = position.containerNode();
return node == editableRootForPosition(position) || node == enclosingNodeOfType(position, isTableCell);
}
bool isRichlyEditablePosition(const Position& position)
{
RefPtr node = position.containerNode();
return node && node->hasRichlyEditableStyle();
}
Element* editableRootForPosition(const Position& position, EditableType editableType)
{
RefPtr node = position.containerNode();
if (!node)
return nullptr;
switch (editableType) {
case HasEditableAXRole:
if (CheckedPtr cache = node->document().existingAXObjectCache())
return const_cast<Element*>(cache->rootAXEditableElement(node.get()));
[[fallthrough]];
case ContentIsEditable:
return node->rootEditableElement();
}
return nullptr;
}
// Finds the enclosing element until which the tree can be split.
// When a user hits ENTER, he/she won't expect this element to be split into two.
// You may pass it as the second argument of splitTreeToNode.
RefPtr<Element> unsplittableElementForPosition(const Position& position)
{
// Since enclosingNodeOfType won't search beyond the highest root editable node,
// this code works even if the closest table cell was outside of the root editable node.
if (RefPtr enclosingCell = downcast<Element>(enclosingNodeOfType(position, &isTableCell)))
return enclosingCell;
return editableRootForPosition(position);
}
Position nextCandidate(const Position& position)
{
for (PositionIterator nextPosition = position; !nextPosition.atEnd(); ) {
nextPosition.increment();
if (nextPosition.isCandidate())
return nextPosition;
}
return { };
}
Position nextVisuallyDistinctCandidate(const Position& position, SkipDisplayContents skipDisplayContents)
{
// FIXME: Use PositionIterator instead.
Position nextPosition = position;
Position downstreamStart = nextPosition.downstream();
while (!nextPosition.atEndOfTree()) {
nextPosition = nextPosition.next(Character);
if (nextPosition.isCandidate() && nextPosition.downstream() != downstreamStart)
return nextPosition;
if (RefPtr node = nextPosition.containerNode()) {
if (!node->renderer()) {
if (skipDisplayContents == SkipDisplayContents::No) {
if (auto element = dynamicDowncast<Element>(node); element && element->hasDisplayContents())
continue;
}
nextPosition = lastPositionInOrAfterNode(node.get());
}
}
}
return { };
}
Position previousCandidate(const Position& position)
{
PositionIterator previousPosition = position;
while (!previousPosition.atStart()) {
previousPosition.decrement();
if (previousPosition.isCandidate())
return previousPosition;
}
return { };
}
Position previousVisuallyDistinctCandidate(const Position& position)
{
// FIXME: Use PositionIterator instead.
Position previousPosition = position;
Position downstreamStart = previousPosition.downstream();
while (!previousPosition.atStartOfTree()) {
previousPosition = previousPosition.previous(Character);
if (previousPosition.isCandidate() && previousPosition.downstream() != downstreamStart)
return previousPosition;
if (RefPtr node = previousPosition.containerNode()) {
if (!node->renderer())
previousPosition = firstPositionInOrBeforeNode(node.get());
}
}
return { };
}
Position firstEditablePositionAfterPositionInRoot(const Position& position, ContainerNode* highestRoot)
{
if (!highestRoot)
return { };
// position falls before highestRoot.
if (position < firstPositionInNode(highestRoot) && highestRoot->hasEditableStyle())
return firstPositionInNode(highestRoot);
Position candidate = position;
if (&position.deprecatedNode()->treeScope() != &highestRoot->treeScope()) {
RefPtr shadowAncestor = highestRoot->treeScope().ancestorNodeInThisScope(position.protectedDeprecatedNode().get());
if (!shadowAncestor)
return { };
candidate = positionAfterNode(shadowAncestor.get());
}
while (candidate.deprecatedNode() && !isEditablePosition(candidate) && candidate.protectedDeprecatedNode()->isDescendantOf(*highestRoot))
candidate = isAtomicNode(candidate.deprecatedNode()) ? positionInParentAfterNode(candidate.protectedDeprecatedNode().get()) : nextVisuallyDistinctCandidate(candidate);
if (candidate.deprecatedNode() && !candidate.protectedDeprecatedNode()->isInclusiveDescendantOf(*highestRoot))
return { };
return candidate;
}
Position lastEditablePositionBeforePositionInRoot(const Position& position, ContainerNode* highestRoot)
{
if (!highestRoot)
return { };
// When position falls after highestRoot, the result is easy to compute.
if (position > lastPositionInNode(highestRoot))
return lastPositionInNode(highestRoot);
Position candidate = position;
if (&position.deprecatedNode()->treeScope() != &highestRoot->treeScope()) {
RefPtr shadowAncestor = highestRoot->treeScope().ancestorNodeInThisScope(position.protectedDeprecatedNode().get());
if (!shadowAncestor)
return { };
candidate = firstPositionInOrBeforeNode(shadowAncestor.get());
}
while (candidate.deprecatedNode() && !isEditablePosition(candidate) && candidate.protectedDeprecatedNode()->isDescendantOf(*highestRoot))
candidate = isAtomicNode(candidate.deprecatedNode()) ? positionInParentBeforeNode(candidate.protectedDeprecatedNode().get()) : previousVisuallyDistinctCandidate(candidate);
if (candidate.deprecatedNode() && !candidate.protectedDeprecatedNode()->isInclusiveDescendantOf(*highestRoot))
return { };
return candidate;
}
// FIXME: The function name, comment, and code say three different things here!
// Whether or not content before and after this node will collapse onto the same line as it.
bool isBlock(const Node& node)
{
return node.renderer() && !node.renderer()->isInline();
}
bool isInline(const Node& node)
{
return node.renderer() && node.renderer()->isInline();
}
// FIXME: Deploy this in all of the places where enclosingBlockFlow/enclosingBlockFlowOrTableElement are used.
// FIXME: Pass a position to this function. The enclosing block of [table, x] for example, should be the
// block that contains the table and not the table, and this function should be the only one responsible for
// knowing about these kinds of special cases.
RefPtr<Element> enclosingBlock(RefPtr<Node> node, EditingBoundaryCrossingRule rule)
{
return dynamicDowncast<Element>(enclosingNodeOfType(firstPositionInOrBeforeNode(node.get()), isBlock, rule));
}
TextDirection directionOfEnclosingBlock(const Position& position)
{
auto block = enclosingBlock(position.protectedContainerNode());
if (!block)
return TextDirection::LTR;
auto renderer = block->renderer();
if (!renderer)
return TextDirection::LTR;
return renderer->writingMode().bidiDirection();
}
// This method is used to create positions in the DOM. It returns the maximum valid offset
// in a node. It returns 1 for some elements even though they do not have children, which
// creates technically invalid DOM Positions. Be sure to call parentAnchoredEquivalent
// on a Position before using it to create a DOM Range, or an exception will be thrown.
int lastOffsetForEditing(const Node& node)
{
if (node.isCharacterDataNode() || node.hasChildNodes())
return node.length();
// FIXME: Might be more helpful to return 1 for any node where editingIgnoresContent is true, even one that happens to have child nodes, like a select element with option node children.
return editingIgnoresContent(node) ? 1 : 0;
}
bool isAmbiguousBoundaryCharacter(char16_t character)
{
// These are characters that can behave as word boundaries, but can appear within words.
// If they are just typed, i.e. if they are immediately followed by a caret, we want to delay text checking until the next character has been typed.
// FIXME: this is required until <rdar://problem/6853027> is fixed and text checking can do this for us.
return character == '\'' || character == '@' || character == rightSingleQuotationMark || character == hebrewPunctuationGershayim;
}
String stringWithRebalancedWhitespace(const String& string, bool startIsStartOfParagraph, bool shouldEmitNBSPbeforeEnd)
{
StringBuilder rebalancedString;
bool previousCharacterWasSpace = false;
unsigned length = string.length();
for (unsigned i = 0; i < length; ++i) {
auto character = string[i];
if (!deprecatedIsEditingWhitespace(character)) {
previousCharacterWasSpace = false;
continue;
}
Latin1Character selectedWhitespaceCharacter;
// We need to ensure there is no next sibling text node. See https://bugs.webkit.org/show_bug.cgi?id=123163
if (previousCharacterWasSpace || (!i && startIsStartOfParagraph) || (i == length - 1 && shouldEmitNBSPbeforeEnd)) {
selectedWhitespaceCharacter = noBreakSpace;
previousCharacterWasSpace = false;
} else {
selectedWhitespaceCharacter = ' ';
previousCharacterWasSpace = true;
}
if (character == selectedWhitespaceCharacter)
continue;
rebalancedString.reserveCapacity(length);
rebalancedString.appendSubstring(string, rebalancedString.length(), i - rebalancedString.length());
rebalancedString.append(selectedWhitespaceCharacter);
}
if (rebalancedString.isEmpty())
return string;
rebalancedString.reserveCapacity(length);
rebalancedString.appendSubstring(string, rebalancedString.length(), length - rebalancedString.length());
return rebalancedString.toString();
}
bool isTableStructureNode(const Node& node)
{
auto* renderer = node.renderer();
return renderer && (renderer->isRenderTableCell() || renderer->isRenderTableRow() || renderer->isRenderTableSection() || renderer->isRenderTableCol());
}
const String& nonBreakingSpaceString()
{
static NeverDestroyed<String> nonBreakingSpaceString(span(noBreakSpace));
return nonBreakingSpaceString;
}
RefPtr<Element> isFirstPositionAfterTable(const VisiblePosition& position)
{
Position upstream(position.deepEquivalent().upstream());
RefPtr node = upstream.deprecatedNode();
if (!node)
return nullptr;
auto* renderer = node->renderer();
if (!renderer || !renderer->isRenderTable() || !upstream.atLastEditingPositionForNode())
return nullptr;
return downcast<Element>(node.releaseNonNull());
}
RefPtr<Element> isLastPositionBeforeTable(const VisiblePosition& position)
{
Position downstream(position.deepEquivalent().downstream());
RefPtr node = downstream.deprecatedNode();
if (!node)
return nullptr;
auto* renderer = node->renderer();
if (!renderer || !renderer->isRenderTable() || !downstream.atFirstEditingPositionForNode())
return nullptr;
return downcast<Element>(node.releaseNonNull());
}
// Returns the visible position at the beginning of a node
VisiblePosition visiblePositionBeforeNode(Node& node)
{
if (node.hasChildNodes())
return VisiblePosition(firstPositionInOrBeforeNode(&node));
ASSERT(node.parentNode());
ASSERT(!node.parentNode()->isShadowRoot());
return positionInParentBeforeNode(&node);
}
// Returns the visible position at the ending of a node
VisiblePosition visiblePositionAfterNode(Node& node)
{
if (node.hasChildNodes())
return VisiblePosition(lastPositionInOrAfterNode(&node));
ASSERT(node.parentNode());
ASSERT(!node.parentNode()->isShadowRoot());
return positionInParentAfterNode(&node);
}
VisiblePosition closestEditablePositionInElementForAbsolutePoint(const Element& element, const IntPoint& point)
{
if (!element.isConnected() || !element.document().frame())
return { };
Ref<const Element> protectedElement { element };
element.protectedDocument()->updateLayoutIgnorePendingStylesheets();
CheckedPtr renderer = element.renderer();
// Look at the inner element of a form control, not the control itself, as it is the editable part.
if (RefPtr formControlElement = dynamicDowncast<HTMLTextFormControlElement>(element)) {
if (!formControlElement->isInnerTextElementEditable())
return { };
if (RefPtr innerTextElement = formControlElement->innerTextElement())
renderer = innerTextElement->renderer();
}
if (!renderer)
return { };
auto absoluteBoundingBox = renderer->absoluteBoundingBoxRect();
auto constrainedAbsolutePoint = point.constrainedBetween(absoluteBoundingBox.minXMinYCorner(), absoluteBoundingBox.maxXMaxYCorner());
auto localPoint = renderer->absoluteToLocal(constrainedAbsolutePoint, UseTransforms);
auto visiblePosition = renderer->visiblePositionForPoint(flooredLayoutPoint(localPoint), HitTestSource::User);
return isEditablePosition(visiblePosition.deepEquivalent()) ? visiblePosition : VisiblePosition { };
}
bool isListHTMLElement(Node* node)
{
return node && (is<HTMLUListElement>(*node) || is<HTMLOListElement>(*node) || is<HTMLDListElement>(*node));
}
bool isListItem(const Node& node)
{
return isListHTMLElement(node.parentNode()) || (node.renderer() && node.renderer()->isRenderListItem());
}
Element* enclosingElementWithTag(const Position& position, const QualifiedName& tagName)
{
auto root = highestEditableRoot(position);
for (RefPtr node = position.deprecatedNode(); node; node = node->parentNode()) {
if (root && !node->hasEditableStyle())
continue;
auto* element = dynamicDowncast<Element>(*node);
if (!element)
continue;
if (element->hasTagName(tagName))
return element;
if (node == root)
return nullptr;
}
return nullptr;
}
RefPtr<Node> enclosingNodeOfType(const Position& position, bool (*nodeIsOfType)(const Node&), EditingBoundaryCrossingRule rule)
{
// FIXME: support CanSkipCrossEditingBoundary
ASSERT(rule == CanCrossEditingBoundary || rule == CannotCrossEditingBoundary);
auto root = rule == CannotCrossEditingBoundary ? highestEditableRoot(position) : nullptr;
for (RefPtr n = position.deprecatedNode(); n; n = n->parentNode()) {
// Don't return a non-editable node if the input position was editable, since
// the callers from editing will no doubt want to perform editing inside the returned node.
if (root && !n->hasEditableStyle())
continue;
if (nodeIsOfType(*n))
return n;
if (n == root)
return nullptr;
}
return nullptr;
}
RefPtr<Node> highestEnclosingNodeOfType(const Position& position, bool (*nodeIsOfType)(const Node&), EditingBoundaryCrossingRule rule, Node* stayWithin)
{
RefPtr<Node> highest;
RefPtr root = rule == CannotCrossEditingBoundary ? highestEditableRoot(position) : nullptr;
for (RefPtr<Node> n = position.containerNode(); n && n != stayWithin; n = n->parentNode()) {
if (root && !n->hasEditableStyle())
continue;
if (nodeIsOfType(*n))
highest = n.get();
if (n == root)
break;
}
return highest;
}
static bool hasARenderedDescendant(Node* node, Node* excludedNode)
{
for (RefPtr n = node->firstChild(); n;) {
if (n == excludedNode) {
n = NodeTraversal::nextSkippingChildren(*n, node);
continue;
}
if (n->renderer())
return true;
n = NodeTraversal::next(*n, node);
}
return false;
}
RefPtr<Node> highestNodeToRemoveInPruning(Node* node)
{
RefPtr<Node> previousNode;
RefPtr rootEditableElement = node ? node->rootEditableElement() : nullptr;
for (RefPtr currentNode = node; currentNode; currentNode = currentNode->parentNode()) {
if (auto* renderer = currentNode->renderer()) {
if (!renderer->canHaveChildren() || hasARenderedDescendant(currentNode.get(), previousNode.get()) || rootEditableElement == currentNode.get())
return previousNode;
}
previousNode = currentNode.get();
}
return nullptr;
}
RefPtr<Element> enclosingTableCell(const Position& position)
{
return downcast<Element>(enclosingNodeOfType(position, isTableCell));
}
RefPtr<Element> enclosingAnchorElement(const Position& p)
{
for (RefPtr node = p.deprecatedNode(); node; node = node->parentNode()) {
if (RefPtr element = dynamicDowncast<Element>(*node); element && element->isLink())
return element;
}
return nullptr;
}
RefPtr<HTMLElement> enclosingList(Node* node)
{
if (!node)
return nullptr;
RefPtr root = highestEditableRoot(firstPositionInOrBeforeNode(node));
for (RefPtr ancestor = node->parentNode(); ancestor; ancestor = ancestor->parentNode()) {
auto* htmlElement = dynamicDowncast<HTMLElement>(*ancestor);
if (htmlElement && (is<HTMLUListElement>(*htmlElement) || is<HTMLOListElement>(*htmlElement)))
return htmlElement;
if (ancestor == root)
return nullptr;
}
return nullptr;
}
RefPtr<Node> enclosingListChild(Node* node)
{
if (!node)
return nullptr;
// Check for a list item element, or for a node whose parent is a list element. Such a node
// will appear visually as a list item (but without a list marker)
RefPtr root = highestEditableRoot(firstPositionInOrBeforeNode(node));
// FIXME: This function is inappropriately named since it starts with node instead of node->parentNode()
for (RefPtr n = node; n && n->parentNode(); n = n->parentNode()) {
if (is<HTMLLIElement>(*n) || (isListHTMLElement(n->parentNode()) && n != root))
return n;
if (n == root || isTableCell(*n))
return nullptr;
}
return nullptr;
}
// FIXME: This function should not need to call isStartOfParagraph/isEndOfParagraph.
RefPtr<Node> enclosingEmptyListItem(const VisiblePosition& position)
{
// Check that position is on a line by itself inside a list item
RefPtr listChildNode = enclosingListChild(position.deepEquivalent().protectedDeprecatedNode().get());
if (!listChildNode || !isStartOfParagraph(position) || !isEndOfParagraph(position))
return nullptr;
VisiblePosition firstInListChild(firstPositionInOrBeforeNode(listChildNode.get()));
VisiblePosition lastInListChild(lastPositionInOrAfterNode(listChildNode.get()));
if (firstInListChild != position || lastInListChild != position)
return nullptr;
return listChildNode;
}
RefPtr<HTMLElement> outermostEnclosingList(Node* node, Node* rootList)
{
RefPtr list = enclosingList(node);
if (!list)
return nullptr;
while (RefPtr nextList = enclosingList(list.get())) {
if (nextList == rootList)
break;
list = WTF::move(nextList);
}
return list;
}
bool canMergeLists(Element* firstList, Element* secondList)
{
auto* first = dynamicDowncast<HTMLElement>(firstList);
auto* second = dynamicDowncast<HTMLElement>(secondList);
if (!first || !second)
return false;
return first->localName() == second->localName() // make sure the list types match (ol vs. ul)
&& first->hasEditableStyle() && second->hasEditableStyle() // both lists are editable
&& first->rootEditableElement() == second->rootEditableElement() // don't cross editing boundaries
// Make sure there is no visible content between this li and the previous list.
&& isVisiblyAdjacent(positionInParentAfterNode(first), positionInParentBeforeNode(second));
}
static Node* previousNodeConsideringAtomicNodes(const Node* node)
{
if (node->previousSibling()) {
SUPPRESS_UNCOUNTED_LOCAL auto* n = node->previousSibling();
while (!isAtomicNode(n) && n->lastChild())
n = n->lastChild();
return n;
}
return node->parentNode();
}
static Node* nextNodeConsideringAtomicNodes(const Node* node)
{
if (!isAtomicNode(node) && node->firstChild())
return node->firstChild();
if (node->nextSibling())
return node->nextSibling();
RefPtr<const Node> n = node;
while (n && !n->nextSibling())
n = n->parentNode();
if (n)
return n->nextSibling();
return nullptr;
}
Node* previousLeafNode(const Node* nodeArg)
{
SUPPRESS_UNCOUNTED_LOCAL auto* node = nodeArg;
while ((node = previousNodeConsideringAtomicNodes(node))) {
if (isAtomicNode(node))
return const_cast<Node*>(node);
}
return nullptr;
}
Node* nextLeafNode(const Node* nodeArg)
{
SUPPRESS_UNCOUNTED_LOCAL auto* node = nodeArg;
while ((node = nextNodeConsideringAtomicNodes(node))) {
if (isAtomicNode(node))
return const_cast<Node*>(node);
}
return nullptr;
}
// FIXME: Do not require renderer, so that this can be used within fragments.
bool isRenderedTable(const Node* node)
{
RefPtr element = dynamicDowncast<HTMLElement>(node);
if (!element)
return false;
auto* renderer = element->renderer();
return renderer && renderer->isRenderTable();
}
bool isTableCell(const Node& node)
{
auto* renderer = node.renderer();
if (!renderer)
return node.hasTagName(tdTag) || node.hasTagName(thTag);
return renderer->isRenderTableCell();
}
bool isEmptyTableCell(const Node* node)
{
// Returns true IFF the passed in node is one of:
// .) a table cell with no children,
// .) a table cell with a single BR child, and which has no other child renderers, including :before and :after renderers
// .) the BR child of such a table cell
// Find rendered node
while (node && !node->renderer())
node = node->parentNode();
if (!node)
return false;
// Make sure the rendered node is a table cell or <br>.
// If it's a <br>, then the parent node has to be a table cell.
CheckedPtr renderer = node->renderer();
if (renderer->isBR()) {
renderer = renderer->parent();
if (!renderer)
return false;
}
auto* renderTableCell = dynamicDowncast<RenderTableCell>(*renderer);
if (!renderTableCell)
return false;
// Check that the table cell contains no child renderers except for perhaps a single <br>.
CheckedPtr childRenderer = renderTableCell->firstChild();
if (!childRenderer)
return true;
if (!childRenderer->isBR())
return false;
return !childRenderer->nextSibling();
}
Ref<HTMLElement> createDefaultParagraphElement(Document& document)
{
switch (document.editor().defaultParagraphSeparator()) {
case EditorParagraphSeparator::div:
return HTMLDivElement::create(document);
case EditorParagraphSeparator::p:
break;
}
return HTMLParagraphElement::create(document);
}
Ref<HTMLElement> createHTMLElement(Document& document, const QualifiedName& name)
{
return HTMLElementFactory::createElement(name, document);
}
Ref<HTMLElement> createHTMLElement(Document& document, const AtomString& tagName)
{
return createHTMLElement(document, QualifiedName(nullAtom(), tagName, xhtmlNamespaceURI));
}
HTMLSpanElement* tabSpanNode(Node* node)
{
if (auto* span = dynamicDowncast<HTMLSpanElement>(node); span && span->attributeWithoutSynchronization(classAttr) == AppleTabSpanClass)
return span;
return nullptr;
}
HTMLSpanElement* parentTabSpanNode(Node* node)
{
return is<Text>(node) ? tabSpanNode(node->parentNode()) : nullptr;
}
static Ref<Element> createTabSpanElement(Document& document, Text& tabTextNode)
{
auto spanElement = HTMLSpanElement::create(document);
spanElement->setAttributeWithoutSynchronization(classAttr, AppleTabSpanClass);
spanElement->setAttribute(styleAttr, "white-space:pre"_s);
spanElement->appendChild(tabTextNode);
return spanElement;
}
Ref<Element> createTabSpanElement(Document& document, String&& tabText)
{
return createTabSpanElement(document, document.createTextNode(WTF::move(tabText)));
}
Ref<Element> createTabSpanElement(Document& document)
{
return createTabSpanElement(document, document.createEditingTextNode("\t"_s));
}
unsigned numEnclosingMailBlockquotes(const Position& position)
{
unsigned count = 0;
for (RefPtr node = position.deprecatedNode(); node; node = node->parentNode()) {
if (isMailBlockquote(*node))
++count;
}
return count;
}
void updatePositionForNodeRemoval(Position& position, Node& node)
{
if (position.isNull())
return;
switch (position.anchorType()) {
case Position::PositionIsBeforeChildren:
if (node.isShadowIncludingInclusiveAncestorOf(position.containerNode()))
position = positionInParentBeforeNode(&node);
break;
case Position::PositionIsAfterChildren:
if (node.isShadowIncludingInclusiveAncestorOf(position.containerNode()))
position = positionInParentBeforeNode(&node);
break;
case Position::PositionIsOffsetInAnchor:
if (position.containerNode() == node.parentNode() && static_cast<unsigned>(position.offsetInContainerNode()) > node.computeNodeIndex())
position.moveToOffset(position.offsetInContainerNode() - 1);
else if (node.isShadowIncludingInclusiveAncestorOf(position.containerNode()))
position = positionInParentBeforeNode(&node);
break;
case Position::PositionIsAfterAnchor:
if (node.isShadowIncludingInclusiveAncestorOf(position.anchorNode()))
position = positionInParentAfterNode(&node);
break;
case Position::PositionIsBeforeAnchor:
if (node.isShadowIncludingInclusiveAncestorOf(position.anchorNode()))
position = positionInParentBeforeNode(&node);
break;
}
}
bool isMailBlockquote(const Node& node)
{
auto* htmlElement = dynamicDowncast<HTMLElement>(node);
if (!htmlElement || !htmlElement->hasTagName(blockquoteTag))
return false;
return htmlElement->attributeWithoutSynchronization(typeAttr) == "cite"_s;
}
int caretMinOffset(const Node& node)
{
auto* renderer = node.renderer();
ASSERT(!node.isCharacterDataNode() || !renderer || renderer->isRenderText());
if (renderer && renderer->isRenderText())
return renderer->caretMinOffset();
if (RefPtr pictureElement = dynamicDowncast<HTMLPictureElement>(node)) {
if (RefPtr firstImage = childrenOfType<HTMLImageElement>(*pictureElement).first())
return firstImage->computeNodeIndex();
}
return 0;
}
// If a node can contain candidates for VisiblePositions, return the offset of the last candidate, otherwise
// return the number of children for container nodes and the length for unrendered text nodes.
int caretMaxOffset(const Node& node)
{
// For rendered text nodes, return the last position that a caret could occupy.
if (auto* text = dynamicDowncast<Text>(node)) {
if (auto* renderer = text->renderer())
return renderer->caretMaxOffset();
}
return lastOffsetForEditing(node);
}
bool lineBreakExistsAtVisiblePosition(const VisiblePosition& position)
{
return lineBreakExistsAtPosition(position.deepEquivalent().downstream());
}
bool lineBreakExistsAtPosition(const Position& position)
{
if (position.isNull())
return false;
if (position.anchorNode()->hasTagName(brTag) && position.atFirstEditingPositionForNode())
return true;
if (!position.anchorNode()->renderer())
return false;
RefPtr textNode = dynamicDowncast<Text>(*position.anchorNode());
if (!textNode || !textNode->renderer()->style().preserveNewline())
return false;
unsigned offset = position.offsetInContainerNode();
return offset < textNode->length() && textNode->data()[offset] == '\n';
}
// Modifies selections that have an end point at the edge of a table
// that contains the other endpoint so that they don't confuse
// code that iterates over selected paragraphs.
VisibleSelection selectionForParagraphIteration(const VisibleSelection& original)
{
VisibleSelection newSelection(original);
VisiblePosition startOfSelection(newSelection.visibleStart());
VisiblePosition endOfSelection(newSelection.visibleEnd());
// If the end of the selection to modify is just after a table, and
// if the start of the selection is inside that table, then the last paragraph
// that we'll want modify is the last one inside the table, not the table itself
// (a table is itself a paragraph).
if (RefPtr table = isFirstPositionAfterTable(endOfSelection)) {
if (startOfSelection.deepEquivalent().deprecatedNode()->isDescendantOf(*table))
newSelection = VisibleSelection(startOfSelection, endOfSelection.previous(CannotCrossEditingBoundary));
}
// If the start of the selection to modify is just before a table,
// and if the end of the selection is inside that table, then the first paragraph
// we'll want to modify is the first one inside the table, not the paragraph
// containing the table itself.
if (RefPtr table = isLastPositionBeforeTable(startOfSelection)) {
if (endOfSelection.deepEquivalent().deprecatedNode()->isDescendantOf(*table))
newSelection = VisibleSelection(startOfSelection.next(CannotCrossEditingBoundary), endOfSelection);
}
return newSelection;
}
// FIXME: indexForVisiblePosition and visiblePositionForIndex use TextIterators to convert between
// VisiblePositions and indices. But TextIterator iteration using TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions
// does not exactly match VisiblePosition iteration, so using them to preserve a selection during an editing
// opertion is unreliable. TextIterator's TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions mode needs to be fixed,
// or these functions need to be changed to iterate using actual VisiblePositions.
// FIXME: Deploy these functions everywhere that TextIterators are used to convert between VisiblePositions and indices.
int indexForVisiblePosition(const VisiblePosition& visiblePosition, RefPtr<ContainerNode>& scope)
{
if (visiblePosition.isNull())
return 0;
auto position = visiblePosition.deepEquivalent();
Ref document = *position.document();
auto editableRoot = highestEditableRoot(position, AXObjectCache::accessibilityEnabled() ? HasEditableAXRole : ContentIsEditable);
if (editableRoot && !document->inDesignMode())
scope = editableRoot;
else {
if (position.containerNode()->isInShadowTree())
scope = position.containerNode()->containingShadowRoot();
else
scope = WTF::move(document);
}
auto range = *makeSimpleRange(makeBoundaryPointBeforeNodeContents(*scope), position);
return characterCount(range, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions);
}
// FIXME: Merge this function with the one above.
int indexForVisiblePosition(Node& node, const VisiblePosition& visiblePosition, TextIteratorBehaviors behaviors)
{
if (visiblePosition.isNull())
return 0;
auto range = makeSimpleRange(makeBoundaryPointBeforeNodeContents(node), visiblePosition);
return range ? characterCount(*range, behaviors) : 0;
}
VisiblePosition visiblePositionForPositionWithOffset(const VisiblePosition& position, int offset)
{
RefPtr<ContainerNode> root;
unsigned startIndex = indexForVisiblePosition(position, root);
if (!root)
return { };
return visiblePositionForIndex(startIndex + offset, root.get());
}
VisiblePosition visiblePositionForIndex(int index, Node* scope, TextIteratorBehaviors behaviors)
{
if (!scope)
return { };
return { makeDeprecatedLegacyPosition(resolveCharacterLocation(makeRangeSelectingNodeContents(*scope), index, behaviors)) };
}
VisiblePosition visiblePositionForIndexUsingCharacterIterator(Node& node, int index)
{
if (index <= 0)
return { firstPositionInOrBeforeNode(&node) };
auto range = makeRangeSelectingNodeContents(node);
CharacterIterator it(range);
if (!it.atEnd())
it.advance(index - 1);
if (!it.atEnd() && it.text().length() == 1 && it.text()[0] == '\n') {
// FIXME: workaround for collapsed range (where only start position is correct) emitted for some emitted newlines.
it.advance(1);
if (!it.atEnd())
return { makeDeprecatedLegacyPosition(it.range().start) };
}
return { makeDeprecatedLegacyPosition((it.atEnd() ? range : it.range()).end), Affinity::Upstream };
}
// Determines whether two positions are visibly next to each other (first then second)
// while ignoring whitespaces and unrendered nodes
static bool isVisiblyAdjacent(const Position& first, const Position& second)
{
return VisiblePosition(first) == VisiblePosition(second.upstream());
}
// Determines whether a node is inside a range or visibly starts and ends at the boundaries of the range.
// Call this function to determine whether a node is visibly fit inside selectedRange
bool isNodeVisiblyContainedWithin(Node& node, const SimpleRange& range)
{
if (contains<ComposedTree>(range, node))
return true;
auto startPosition = makeDeprecatedLegacyPosition(range.start);
auto endPosition = makeDeprecatedLegacyPosition(range.end);
bool startIsVisuallySame = visiblePositionBeforeNode(node) == startPosition;
if (startIsVisuallySame && positionInParentAfterNode(&node) < endPosition)
return true;
bool endIsVisuallySame = visiblePositionAfterNode(node) == endPosition;
if (endIsVisuallySame && startPosition < positionInParentBeforeNode(&node))
return true;
return startIsVisuallySame && endIsVisuallySame;
}
bool isRenderedAsNonInlineTableImageOrHR(const Node* node)
{
if (!node)
return false;
RenderObject* renderer = node->renderer();
return renderer && !renderer->isInline() && (renderer->isRenderTable() || renderer->isImage() || renderer->isHR());
}
Element* elementIfEquivalent(const Element& first, Node& second)
{
auto* secondElement = dynamicDowncast<Element>(second);
if (secondElement && first.hasTagName(secondElement->tagQName()) && first.hasEquivalentAttributes(*secondElement))
return secondElement;
return nullptr;
}
bool isNonTableCellHTMLBlockElement(const Node* node)
{
return node->hasTagName(listingTag)
|| node->hasTagName(olTag)
|| node->hasTagName(preTag)
|| is<HTMLTableElement>(*node)
|| node->hasTagName(ulTag)
|| node->hasTagName(xmpTag)
|| node->hasTagName(h1Tag)
|| node->hasTagName(h2Tag)
|| node->hasTagName(h3Tag)
|| node->hasTagName(h4Tag)
|| node->hasTagName(h5Tag);
}
Position adjustedSelectionStartForStyleComputation(const VisibleSelection& selection)
{
// This function is used by range style computations to avoid bugs like:
// <rdar://problem/4017641> REGRESSION (Mail): you can only bold/unbold a selection starting from end of line once
// It is important to skip certain irrelevant content at the start of the selection, so we do not wind up
// with a spurious "mixed" style.
auto visiblePosition = selection.visibleStart();
if (visiblePosition.isNull())
return { };
// if the selection is a caret, just return the position, since the style
// behind us is relevant
if (selection.isCaret())
return visiblePosition.deepEquivalent();
// if the selection starts just before a paragraph break, skip over it
if (isEndOfParagraph(visiblePosition))
return visiblePosition.next().deepEquivalent().downstream();
// otherwise, make sure to be at the start of the first selected node,
// instead of possibly at the end of the last node before the selection
return visiblePosition.deepEquivalent().downstream();
}
// FIXME: Should this be deprecated like deprecatedEnclosingBlockFlowElement is?
static Element* elementIfBlockFlow(Node& node)
{
auto* element = dynamicDowncast<Element>(node);
if (!element)
return nullptr;
auto* renderer = element->renderer();
return renderer && renderer->isRenderBlockFlow() ? element : nullptr;
}
bool isBlockFlowElement(const Node& node)
{
return elementIfBlockFlow(const_cast<Node&>(node));
}
Element* deprecatedEnclosingBlockFlowElement(Node* node)
{
if (!node)
return nullptr;
if (auto* element = elementIfBlockFlow(*node))
return element;
while ((node = node->parentNode())) {
if (auto* element = elementIfBlockFlow(*node))
return element;
if (auto* body = dynamicDowncast<HTMLBodyElement>(*node))
return body;
}
return nullptr;
}
static inline bool caretRendersInsideNode(const Node& node)
{
return !isRenderedTable(&node) && !editingIgnoresContent(node);
}
RenderBlock* rendererForCaretPainting(const Node* node)
{
if (!node)
return nullptr;
auto* renderer = node->renderer();
if (!renderer)
return nullptr;
// If caretNode is a block and caret is inside it, then caret should be painted by that block.
if (auto* blockFlow = dynamicDowncast<RenderBlockFlow>(*renderer)) {
if (caretRendersInsideNode(*node))
return blockFlow;
}
return renderer->containingBlock();
}
LayoutRect localCaretRectInRendererForCaretPainting(const VisiblePosition& caretPosition, RenderBlock*& caretPainter)
{
if (caretPosition.isNull())
return LayoutRect();
ASSERT(caretPosition.deepEquivalent().deprecatedNode()->renderer());
auto [localRect, renderer] = caretPosition.localCaretRect();
return localCaretRectInRendererForRect(localRect, caretPosition.deepEquivalent().deprecatedNode(), renderer.get(), caretPainter);
}
LayoutRect localCaretRectInRendererForRect(LayoutRect& localRect, Node* node, RenderObject* renderer, RenderBlock*& caretPainter)
{
// Get the renderer that will be responsible for painting the caret
// (which is either the renderer we just found, or one of its containers).
caretPainter = rendererForCaretPainting(node);
// Compute an offset between the renderer and the caretPainter.
while (renderer != caretPainter) {
CheckedPtr containerObject = renderer->container();
if (!containerObject)
return LayoutRect();
localRect.move(renderer->offsetFromContainer(*containerObject, localRect.location()));
renderer = containerObject.get();
}
return localRect;
}
IntRect absoluteBoundsForLocalCaretRect(RenderBlock* rendererForCaretPainting, const LayoutRect& rect, bool* insideFixed)
{
if (insideFixed)
*insideFixed = false;
if (!rendererForCaretPainting || rect.isEmpty())
return IntRect();
LayoutRect localRect(rect);
rendererForCaretPainting->flipForWritingMode(localRect);
return rendererForCaretPainting->localToAbsoluteQuad(FloatRect(localRect), UseTransforms, insideFixed).enclosingBoundingBox();
}
HashSet<Ref<HTMLImageElement>> visibleImageElementsInRangeWithNonLoadedImages(const SimpleRange& range)
{
HashSet<Ref<HTMLImageElement>> result;
for (TextIterator iterator(range); !iterator.atEnd(); iterator.advance()) {
RefPtr imageElement = dynamicDowncast<HTMLImageElement>(iterator.node());
if (!imageElement)
continue;
auto* cachedImage = imageElement->cachedImage();
if (cachedImage && cachedImage->isLoading())
result.add(imageElement.releaseNonNull());
}
return result;
}
enum class RangeEndpointsToAdjust : uint8_t {
Start = 1 << 0,
End = 1 << 1,
};
static std::optional<unsigned> visualDistanceOnSameLine(const RenderedPosition& first, const RenderedPosition& second)
{
if (first.isNull() || second.isNull())
return std::nullopt;
if (first.box() == second.box())
return std::max(first.offset(), second.offset()) - std::min(first.offset(), second.offset());
enum class VisualBoundary : bool { Left, Right };
auto distanceFromOffsetToVisualBoundary = [](const RenderedPosition& position, VisualBoundary boundary) {
auto box = position.box();
auto offset = position.offset();
return (boundary == VisualBoundary::Left) == (box->direction() == TextDirection::LTR)
? std::max(box->minimumCaretOffset(), offset) - box->minimumCaretOffset()
: std::max(box->maximumCaretOffset(), offset) - offset;
};
unsigned distance = 0;
bool foundFirst = false;
bool foundSecond = false;
for (auto box = first.lineBox()->lineLeftmostLeafBox(); box; box = box->nextLineRightwardOnLine()) {
if (box == first.box()) {
distance += distanceFromOffsetToVisualBoundary(first, foundSecond ? VisualBoundary::Left : VisualBoundary::Right);
foundFirst = true;
} else if (box == second.box()) {
distance += distanceFromOffsetToVisualBoundary(second, foundFirst ? VisualBoundary::Left : VisualBoundary::Right);
foundSecond = true;
} else if (foundFirst || foundSecond)
distance += box->maximumCaretOffset() - box->minimumCaretOffset();
if (foundFirst && foundSecond)
return distance;
}
return std::nullopt;
}
static std::optional<BoundaryPoint> findBidiBoundary(const RenderedPosition& position, unsigned bidiLevel, SelectionExtentMovement movement, TextDirection selectionDirection)
{
auto leftBoundary = position.leftBoundaryOfBidiRun(bidiLevel);
auto rightBoundary = position.rightBoundaryOfBidiRun(bidiLevel);
bool moveLeft = [&] {
switch (movement) {
case SelectionExtentMovement::Left:
return true;
case SelectionExtentMovement::Right:
return false;
case SelectionExtentMovement::Closest: {
auto distanceToLeft = visualDistanceOnSameLine(position, leftBoundary);
if (!distanceToLeft)
return false;
auto distanceToRight = visualDistanceOnSameLine(position, rightBoundary);
if (!distanceToRight)
return true;
return *distanceToLeft < *distanceToRight;
}
}
ASSERT_NOT_REACHED();
return false;
}();
// This looks unintuitive, but is necessary to ensure that the boundary is moved
// (visually) to the left or right, respectively, in both LTR and RTL paragraphs.
return (position.box()->direction() == selectionDirection) == moveLeft ? leftBoundary.boundaryPoint() : rightBoundary.boundaryPoint();
}
enum class BoxIterationDirection : bool { SameAsLine, OppositeOfLine };
static InlineIterator::LeafBoxIterator advanceInDirection(InlineIterator::LeafBoxIterator box, TextDirection direction, BoxIterationDirection iterationDirection)
{
bool shouldMoveRight = (iterationDirection == BoxIterationDirection::SameAsLine) == (direction == TextDirection::LTR);
return shouldMoveRight ? box->nextLineRightwardOnLine() : box->nextLineLeftwardOnLine();
}
static void forEachRenderedBoxBetween(const RenderedPosition& first, const RenderedPosition& second, NOESCAPE const Function<IterationStatus(InlineIterator::LeafBoxIterator)>& callback)
{
if (first.isNull()) {
ASSERT_NOT_REACHED();
return;
}
if (second.isNull()) {
ASSERT_NOT_REACHED();
return;
}
if (first.lineBox().atEnd()) {
ASSERT_NOT_REACHED();
return;
}
if (first.box() == second.box()) {
callback(first.box());
return;
}
bool foundOneEndpoint = false;
for (auto box = first.lineBox()->lineLeftmostLeafBox(); box; box = box->nextLineRightwardOnLine()) {
bool atFirstEndpoint = box == first.box();
bool atSecondEndpoint = box == second.box();
bool atEndpoint = atFirstEndpoint || atSecondEndpoint;
bool atLastEndpoint = atEndpoint && foundOneEndpoint;
if (!atEndpoint && !foundOneEndpoint)
continue;
foundOneEndpoint = true;
bool shouldSkipBox = [&] {
if (!atEndpoint)
return false;
auto& position = (atFirstEndpoint ? first : second);
return atLastEndpoint ? position.atLeftmostOffsetInBox() : position.atRightmostOffsetInBox();
}();
if (shouldSkipBox)
continue;
if (callback(box) == IterationStatus::Done)
return;
if (atLastEndpoint)
return;
}
}
PositionRange positionsForRange(const SimpleRange& range)
{
return {
makeContainerOffsetPosition(range.start).downstream(),
makeContainerOffsetPosition(range.end).upstream()
};
}
static InlineIterator::LeafBoxIterator boxWithMinimumBidiLevelBetween(const RenderedPosition& start, const RenderedPosition& end)
{
InlineIterator::LeafBoxIterator result;
forEachRenderedBoxBetween(start, end, [&](auto box) {
if (!result || box->bidiLevel() < result->bidiLevel())
result = box;
return IterationStatus::Continue;
});
return result;
}
TextDirection primaryDirectionForSingleLineRange(const Position& start, const Position& end)
{
ASSERT(inSameLine(start, end));
RenderedPosition renderedStart { start, Affinity::Downstream };
RenderedPosition renderedEnd { end, Affinity::Upstream };
auto direction = start.primaryDirection();
if (renderedStart.isNull() || renderedEnd.isNull())
return direction;
if (auto box = boxWithMinimumBidiLevelBetween(renderedStart, renderedEnd))
return box->direction();
return direction;
}
static std::optional<SimpleRange> makeVisuallyContiguousIfNeeded(const SimpleRange& range, OptionSet<RangeEndpointsToAdjust> endpoints, SelectionExtentMovement movement)
{
if (range.collapsed())
return std::nullopt;
auto [start, end] = positionsForRange(range);
auto firstLineDirection = TextDirection::LTR;
RenderedPosition renderedStart { start };
if (renderedStart.isNull() || renderedStart.lineBox().atEnd())
return std::nullopt;
auto lastLineDirection = TextDirection::LTR;
RenderedPosition renderedEnd { end };
if (renderedEnd.isNull() || renderedEnd.lineBox().atEnd())
return std::nullopt;
if (renderedStart.box() == renderedEnd.box())
return std::nullopt;
auto bidiLevelAtStart = renderedStart.box()->bidiLevel();
auto bidiLevelAtEnd = renderedEnd.box()->bidiLevel();
auto targetBidiLevelAtStart = bidiLevelAtStart;
auto targetBidiLevelAtEnd = bidiLevelAtEnd;
std::optional<BoundaryPoint> adjustedStart;
std::optional<BoundaryPoint> adjustedEnd;
if (inSameLine(start, end)) {
if (auto box = boxWithMinimumBidiLevelBetween(renderedStart, renderedEnd)) {
targetBidiLevelAtStart = box->bidiLevel();
targetBidiLevelAtEnd = targetBidiLevelAtStart;
firstLineDirection = box->direction();
lastLineDirection = firstLineDirection;
}
} else {
bool firstLineOnlyContainsSelectedTextInOppositeDirection = true;
firstLineDirection = start.primaryDirection();
std::optional<BoundaryPoint> firstPositionForSelectedTextInOppositeDirectionOnFirstLine;
for (auto box = renderedStart.box(); box; box = advanceInDirection(box, firstLineDirection, BoxIterationDirection::SameAsLine)) {
targetBidiLevelAtStart = std::min(targetBidiLevelAtStart, box->bidiLevel());
if (box->direction() == firstLineDirection)
firstLineOnlyContainsSelectedTextInOppositeDirection = false;
if (!firstLineOnlyContainsSelectedTextInOppositeDirection)
continue;
if (RefPtr node = box->renderer().node()) {
if (box->isText())
firstPositionForSelectedTextInOppositeDirectionOnFirstLine.emplace(node.releaseNonNull(), box->minimumCaretOffset());
else
firstPositionForSelectedTextInOppositeDirectionOnFirstLine = makeBoundaryPointBeforeNode(node.releaseNonNull());
}
}
if (firstLineOnlyContainsSelectedTextInOppositeDirection)
adjustedStart = WTF::move(firstPositionForSelectedTextInOppositeDirectionOnFirstLine);
bool lastLineOnlyContainsSelectedTextInOppositeDirection = true;
lastLineDirection = end.primaryDirection();
std::optional<BoundaryPoint> lastPositionForSelectedTextInOppositeDirectionOnLastLine;
for (auto box = renderedEnd.box(); box; box = advanceInDirection(box, lastLineDirection, BoxIterationDirection::OppositeOfLine)) {
targetBidiLevelAtEnd = std::min(targetBidiLevelAtEnd, box->bidiLevel());
if (box->direction() == lastLineDirection)
lastLineOnlyContainsSelectedTextInOppositeDirection = false;
if (!lastLineOnlyContainsSelectedTextInOppositeDirection)
continue;
if (RefPtr node = box->renderer().node()) {
if (box->isText())
lastPositionForSelectedTextInOppositeDirectionOnLastLine.emplace(node.releaseNonNull(), box->maximumCaretOffset());
else
lastPositionForSelectedTextInOppositeDirectionOnLastLine = makeBoundaryPointAfterNode(node.releaseNonNull());
}
}
if (lastLineOnlyContainsSelectedTextInOppositeDirection)
adjustedEnd = WTF::move(lastPositionForSelectedTextInOppositeDirectionOnLastLine);
}
if (!adjustedStart && bidiLevelAtStart > targetBidiLevelAtStart && start != logicalStartOfLine(start) && endpoints.contains(RangeEndpointsToAdjust::Start))
adjustedStart = findBidiBoundary(renderedStart, targetBidiLevelAtStart + 1, movement, firstLineDirection);
if (!adjustedEnd && bidiLevelAtEnd > targetBidiLevelAtEnd && end != logicalEndOfLine(end) && endpoints.contains(RangeEndpointsToAdjust::End))
adjustedEnd = findBidiBoundary(renderedEnd, targetBidiLevelAtEnd + 1, movement, lastLineDirection);
if (!adjustedStart && !adjustedEnd)
return std::nullopt;
auto adjustedRange = range;
if (adjustedStart)
adjustedRange.start = WTF::move(*adjustedStart);
if (adjustedEnd)
adjustedRange.end = WTF::move(*adjustedEnd);
if (!is_lt(treeOrder(adjustedRange.start, adjustedRange.end)))
return std::nullopt;
return adjustedRange;
}
SimpleRange adjustToVisuallyContiguousRange(const SimpleRange& range)
{
return makeVisuallyContiguousIfNeeded(range, {
RangeEndpointsToAdjust::Start,
RangeEndpointsToAdjust::End
}, SelectionExtentMovement::Closest).value_or(range);
}
EnclosingLayerInfomation computeEnclosingLayer(const SimpleRange& range)
{
auto [start, end] = positionsForRange(range);
if (start.isOrphan() || end.isOrphan())
return { };
if (!isEditablePosition(start) && range.collapsed())
return { };
auto findEnclosingLayer = [](const Position& position) -> RenderLayer* {
RefPtr container = position.containerNode();
if (!container)
return nullptr;
CheckedPtr renderer = container->renderer();
if (!renderer)
return nullptr;
return renderer->enclosingLayer();
};
auto [startLayer, endLayer] = [&] -> std::pair<CheckedPtr<RenderLayer>, CheckedPtr<RenderLayer>> {
if (RefPtr container = start.containerNode(); container && ImageOverlay::isInsideOverlay(*container)) {
RefPtr host = container->shadowHost();
if (!host) {
ASSERT_NOT_REACHED();
return { };
}
CheckedPtr renderer = host->renderer();
if (!renderer)
return { };
CheckedPtr enclosingLayer = renderer->enclosingLayer();
return { enclosingLayer, enclosingLayer };
}
return { findEnclosingLayer(start), findEnclosingLayer(end) };
}();
if (!startLayer)
return { };
if (!endLayer)
return { };
for (CheckedPtr layer = startLayer->commonAncestorWithLayer(*endLayer); layer; layer = layer->enclosingContainingBlockLayer(CrossFrameBoundaries::Yes)) {
if (!layer->isComposited())
continue;
RefPtr graphicsLayer = [layer] -> RefPtr<GraphicsLayer> {
auto* backing = layer->backing();
if (RefPtr scrolledContentsLayer = backing->scrolledContentsLayer())
return scrolledContentsLayer;
if (RefPtr foregroundLayer = backing->foregroundLayer())
return foregroundLayer;
if (backing->isFrameLayerWithTiledBacking())
return backing->parentForSublayers();
return backing->graphicsLayer();
}();
if (!graphicsLayer)
continue;
auto identifier = graphicsLayer->layerIDIgnoringStructuralLayer();
if (!identifier)
continue;
return { WTF::move(startLayer), WTF::move(endLayer), WTF::move(layer), WTF::move(graphicsLayer), WTF::move(identifier) };
}
return { };
}
void adjustVisibleExtentPreservingVisualContiguity(const VisiblePosition& base, VisiblePosition& extent, SelectionExtentMovement movement)
{
auto start = makeBoundaryPoint(base.deepEquivalent());
auto end = makeBoundaryPoint(extent.deepEquivalent());
if (!start || !end)
return;
OptionSet<RangeEndpointsToAdjust> endpoints;
auto baseExtentOrder = treeOrder<ComposedTree>(*start, *end);
bool startIsMoving = is_gt(baseExtentOrder);
bool endIsMoving = is_lt(baseExtentOrder);
if (startIsMoving) {
std::swap(start, end);
endpoints.add(RangeEndpointsToAdjust::Start);
} else if (endIsMoving)
endpoints.add(RangeEndpointsToAdjust::End);
else
return;
auto adjustedRange = makeVisuallyContiguousIfNeeded({ WTF::move(*start), WTF::move(*end) }, endpoints, movement);
if (!adjustedRange)
return;
extent = { makeContainerOffsetPosition(startIsMoving ? adjustedRange->start : adjustedRange->end) };
}
bool crossesBidiTextBoundaryInSameLine(const VisiblePosition& position, const VisiblePosition& other)
{
if (!inSameLine(position, other))
return false;
std::optional<unsigned char> currentLevel;
bool foundDifferentBidiLevel = false;
forEachRenderedBoxBetween(RenderedPosition { position }, RenderedPosition { other }, [&](auto box) {
auto bidiLevel = box->bidiLevel();
if (!currentLevel) {
currentLevel = bidiLevel;
return IterationStatus::Continue;
}
if (currentLevel == bidiLevel)
return IterationStatus::Continue;
foundDifferentBidiLevel = true;
return IterationStatus::Done;
});
return foundDifferentBidiLevel;
}
} // namespace WebCore