/* * Copyright (C) 2005, 2006, 2008, 2009 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE 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 "ApplyStyleCommand.h" #include "CSSComputedStyleDeclaration.h" #include "CSSParser.h" #include "CSSValuePool.h" #include "Document.h" #include "Editing.h" #include "Editor.h" #include "ElementIterator.h" #include "Frame.h" #include "HTMLFontElement.h" #include "HTMLIFrameElement.h" #include "HTMLInterchange.h" #include "HTMLNames.h" #include "HTMLSpanElement.h" #include "NodeList.h" #include "NodeTraversal.h" #include "RenderObject.h" #include "RenderText.h" #include "ScriptDisallowedScope.h" #include "StyleProperties.h" #include "StyleResolver.h" #include "Text.h" #include "TextIterator.h" #include "TextNodeTraversal.h" #include "VisibleUnits.h" #include #include #include namespace WebCore { using namespace HTMLNames; static int toIdentifier(RefPtr&& value) { return is(value) ? downcast(*value).valueID() : 0; } static String& styleSpanClassString() { static NeverDestroyed styleSpanClassString(AppleStyleSpanClass); return styleSpanClassString; } bool isLegacyAppleStyleSpan(const Node* node) { if (!is(node)) return false; return downcast(*node).attributeWithoutSynchronization(classAttr) == styleSpanClassString(); } static bool hasNoAttributeOrOnlyStyleAttribute(const StyledElement& element, ShouldStyleAttributeBeEmpty shouldStyleAttributeBeEmpty) { if (!element.hasAttributes()) return true; unsigned matchedAttributes = 0; if (element.attributeWithoutSynchronization(classAttr) == styleSpanClassString()) matchedAttributes++; if (element.hasAttribute(styleAttr) && (shouldStyleAttributeBeEmpty == AllowNonEmptyStyleAttribute || !element.inlineStyle() || element.inlineStyle()->isEmpty())) matchedAttributes++; ASSERT(matchedAttributes <= element.attributeCount()); return matchedAttributes == element.attributeCount(); } bool isStyleSpanOrSpanWithOnlyStyleAttribute(const Element& element) { if (!is(element)) return false; return hasNoAttributeOrOnlyStyleAttribute(downcast(element), AllowNonEmptyStyleAttribute); } static inline bool isSpanWithoutAttributesOrUnstyledStyleSpan(const Element& element) { if (!is(element)) return false; return hasNoAttributeOrOnlyStyleAttribute(downcast(element), StyleAttributeShouldBeEmpty); } bool isEmptyFontTag(const Element* element, ShouldStyleAttributeBeEmpty shouldStyleAttributeBeEmpty) { if (!is(element)) return false; return hasNoAttributeOrOnlyStyleAttribute(downcast(*element), shouldStyleAttributeBeEmpty); } static Ref createFontElement(Document& document) { return createHTMLElement(document, fontTag); } Ref createStyleSpanElement(Document& document) { return createHTMLElement(document, spanTag); } ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, EditAction editingAction, EPropertyLevel propertyLevel) : CompositeEditCommand(document, editingAction) , m_style(style->copy()) , m_propertyLevel(propertyLevel) , m_start(endingSelection().start().downstream()) , m_end(endingSelection().end().upstream()) , m_useEndingSelection(true) , m_removeOnly(false) { } ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction, EPropertyLevel propertyLevel) : CompositeEditCommand(document, editingAction) , m_style(style->copy()) , m_propertyLevel(propertyLevel) , m_start(start) , m_end(end) , m_useEndingSelection(false) , m_removeOnly(false) { } ApplyStyleCommand::ApplyStyleCommand(Ref&& element, bool removeOnly, EditAction editingAction) : CompositeEditCommand(element->document(), editingAction) , m_style(EditingStyle::create()) , m_propertyLevel(PropertyDefault) , m_start(endingSelection().start().downstream()) , m_end(endingSelection().end().upstream()) , m_useEndingSelection(true) , m_styledInlineElement(WTFMove(element)) , m_removeOnly(removeOnly) { } ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, IsInlineElementToRemoveFunction isInlineElementToRemoveFunction, EditAction editingAction) : CompositeEditCommand(document, editingAction) , m_style(style->copy()) , m_propertyLevel(PropertyDefault) , m_start(endingSelection().start().downstream()) , m_end(endingSelection().end().upstream()) , m_useEndingSelection(true) , m_removeOnly(true) , m_isInlineElementToRemoveFunction(isInlineElementToRemoveFunction) { } void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd) { ASSERT(!is_gt(treeOrder(newStart, newEnd))); if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end)) m_useEndingSelection = true; bool wasBaseFirst = startingSelection().isBaseFirst() || !startingSelection().isDirectional(); setEndingSelection(VisibleSelection(wasBaseFirst ? newStart : newEnd, wasBaseFirst ? newEnd : newStart, VisiblePosition::defaultAffinity, endingSelection().isDirectional())); m_start = newStart; m_end = newEnd; } Position ApplyStyleCommand::startPosition() { if (m_useEndingSelection) return endingSelection().start(); return m_start; } Position ApplyStyleCommand::endPosition() { if (m_useEndingSelection) return endingSelection().end(); return m_end; } void ApplyStyleCommand::doApply() { switch (m_propertyLevel) { case PropertyDefault: { // Apply the block-centric properties of the style. auto blockStyle = m_style->extractAndRemoveBlockProperties(); if (!blockStyle->isEmpty()) applyBlockStyle(blockStyle); // Apply any remaining styles to the inline elements. if (!m_style->isEmpty() || m_styledInlineElement || m_isInlineElementToRemoveFunction) { applyRelativeFontStyleChange(m_style.get()); applyInlineStyle(*m_style); } break; } case ForceBlockProperties: // Force all properties to be applied as block styles. applyBlockStyle(*m_style); break; } } void ApplyStyleCommand::applyBlockStyle(EditingStyle& style) { // Update document layout once before removing styles so that we avoid the expense of // updating before each and every call to check a computed style. document().updateLayoutIgnorePendingStylesheets(); auto start = startPosition(); auto end = endPosition(); if (end < start) std::swap(start, end); VisiblePosition visibleStart(start); VisiblePosition visibleEnd(end); if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan()) return; // Save and restore the selection endpoints using their indices in the editable root, since // addBlockStyleIfNeeded may moveParagraphs, which can remove these endpoints. // Calculate start and end indices from the start of the tree that they're in. auto scopeRoot = makeRefPtr(highestEditableRoot(visibleStart.deepEquivalent())); if (!scopeRoot) return; auto scope = makeRangeSelectingNodeContents(*scopeRoot); auto range = *makeSimpleRange(visibleStart, visibleEnd); auto startIndex = characterCount({ scope.start, range.start }, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions); auto endIndex = characterCount({ scope.start, range.end }, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions); VisiblePosition paragraphStart(startOfParagraph(visibleStart)); VisiblePosition nextParagraphStart(endOfParagraph(paragraphStart).next()); if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd)) visibleEnd = visibleEnd.previous(CannotCrossEditingBoundary); VisiblePosition beyondEnd(endOfParagraph(visibleEnd).next()); while (paragraphStart.isNotNull() && paragraphStart != beyondEnd) { StyleChange styleChange(&style, paragraphStart.deepEquivalent()); if (styleChange.cssStyle() || m_removeOnly) { RefPtr block = enclosingBlock(paragraphStart.deepEquivalent().deprecatedNode()); if (!m_removeOnly) { RefPtr newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent()); if (newBlock) block = newBlock; } ASSERT(!block || is(*block)); if (is(block)) { removeCSSStyle(style, downcast(*block)); if (!m_removeOnly) addBlockStyle(styleChange, downcast(*block)); } if (nextParagraphStart.isOrphan()) nextParagraphStart = endOfParagraph(paragraphStart).next(); } paragraphStart = nextParagraphStart; nextParagraphStart = endOfParagraph(paragraphStart).next(); } auto startPosition = makeDeprecatedLegacyPosition(resolveCharacterLocation(scope, startIndex, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions)); auto endPosition = makeDeprecatedLegacyPosition(resolveCharacterLocation(scope, endIndex, TextIteratorBehavior::EmitsCharactersBetweenAllVisiblePositions)); updateStartEnd(startPosition, endPosition); } static Ref copyStyleOrCreateEmpty(const StyleProperties* style) { if (!style) return MutableStyleProperties::create(); return style->mutableCopy(); } void ApplyStyleCommand::applyRelativeFontStyleChange(EditingStyle* style) { static const float MinimumFontSize = 0.1f; if (!style || !style->hasFontSizeDelta()) return; auto start = startPosition(); auto end = endPosition(); if (end < start) std::swap(start, end); // Join up any adjacent text nodes. if (is(start.deprecatedNode())) { joinChildTextNodes(start.deprecatedNode()->parentNode(), start, end); start = startPosition(); end = endPosition(); } if (start.isNull() || end.isNull()) return; if (is(*end.deprecatedNode()) && start.deprecatedNode()->parentNode() != end.deprecatedNode()->parentNode()) { joinChildTextNodes(end.deprecatedNode()->parentNode(), start, end); start = startPosition(); end = endPosition(); } if (start.isNull() || end.isNull()) return; // Split the start text nodes if needed to apply style. if (isValidCaretPositionInTextNode(start)) { splitTextAtStart(start, end); start = startPosition(); end = endPosition(); } if (start.isNull() || end.isNull()) return; if (isValidCaretPositionInTextNode(end)) { splitTextAtEnd(start, end); start = startPosition(); end = endPosition(); } if (start.isNull() || end.isNull()) return; // Calculate loop end point. // If the end node is before the start node (can only happen if the end node is // an ancestor of the start node), we gather nodes up to the next sibling of the end node RefPtr beyondEnd; ASSERT(start.deprecatedNode()); ASSERT(end.deprecatedNode()); if (end.deprecatedNode()->contains(*start.deprecatedNode())) beyondEnd = NodeTraversal::nextSkippingChildren(*end.deprecatedNode()); else beyondEnd = NodeTraversal::next(*end.deprecatedNode()); start = start.upstream(); // Move upstream to ensure we do not add redundant spans. auto startNode = makeRefPtr(start.deprecatedNode()); // Make sure we're not already at the end or the next NodeTraversal::next() will traverse past it. if (startNode == beyondEnd) return; if (is(*startNode) && start.deprecatedEditingOffset() >= caretMaxOffset(*startNode)) { // Move out of text node if range does not include its characters. startNode = NodeTraversal::next(*startNode); if (!startNode) return; } // Store away font size before making any changes to the document. // This ensures that changes to one node won't effect another. HashMap, float> startingFontSizes; for (auto node = startNode; node != beyondEnd; node = NodeTraversal::next(*node)) { ASSERT(node); startingFontSizes.set(*node, computedFontSize(node.get())); } // These spans were added by us. If empty after font size changes, they can be removed. Vector> unstyledSpans; RefPtr lastStyledNode; bool reachedEnd = false; for (auto node = startNode; node != beyondEnd && !reachedEnd; node = NodeTraversal::next(*node)) { ASSERT(node); RefPtr element; if (is(*node)) { // Only work on fully selected nodes. if (!nodeFullySelected(downcast(*node), start, end)) continue; element = &downcast(*node); } else if (is(*node) && node->renderer() && node->parentNode() != lastStyledNode) { // Last styled node was not parent node of this text node, but we wish to style this // text node. To make this possible, add a style span to surround this text node. auto span = createStyleSpanElement(document()); if (!surroundNodeRangeWithElement(*node, *node, span)) continue; reachedEnd = node->isDescendantOf(beyondEnd.get()); element = WTFMove(span); } else { // Only handle HTML elements and text nodes. continue; } lastStyledNode = node; RefPtr inlineStyle = copyStyleOrCreateEmpty(element->inlineStyle()); float currentFontSize = computedFontSize(node.get()); float desiredFontSize = std::max(MinimumFontSize, startingFontSizes.get(node.get()) + style->fontSizeDelta()); RefPtr value = inlineStyle->getPropertyCSSValue(CSSPropertyFontSize); if (value) { element->removeInlineStyleProperty(CSSPropertyFontSize); currentFontSize = computedFontSize(node.get()); } if (currentFontSize != desiredFontSize) { inlineStyle->setProperty(CSSPropertyFontSize, CSSValuePool::singleton().createValue(desiredFontSize, CSSUnitType::CSS_PX), false); setNodeAttribute(*element, styleAttr, inlineStyle->asText()); } if (inlineStyle->isEmpty()) { removeNodeAttribute(*element, styleAttr); if (isSpanWithoutAttributesOrUnstyledStyleSpan(*element)) unstyledSpans.append(element.releaseNonNull()); } } for (auto& unstyledSpan : unstyledSpans) removeNodePreservingChildren(unstyledSpan); } static ContainerNode* dummySpanAncestorForNode(Node* node) { RefPtr currentNode = node; while (currentNode && (!is(*currentNode) || !isStyleSpanOrSpanWithOnlyStyleAttribute(downcast(*currentNode)))) currentNode = currentNode->parentNode(); return currentNode ? currentNode->parentNode() : nullptr; } void ApplyStyleCommand::cleanupUnstyledAppleStyleSpans(ContainerNode* dummySpanAncestor) { if (!dummySpanAncestor) return; // Dummy spans are created when text node is split, so that style information // can be propagated, which can result in more splitting. If a dummy span gets // cloned/split, the new node is always a sibling of it. Therefore, we scan // all the children of the dummy's parent Vector> toRemove; for (auto& child : childrenOfType(*dummySpanAncestor)) { if (isSpanWithoutAttributesOrUnstyledStyleSpan(child)) toRemove.append(child); } for (auto& element : toRemove) removeNodePreservingChildren(element.get()); } RefPtr ApplyStyleCommand::splitAncestorsWithUnicodeBidi(Node* node, bool before, WritingDirection allowedDirection) { // We are allowed to leave the highest ancestor with unicode-bidi unsplit if it is unicode-bidi: embed and direction: allowedDirection. // In that case, we return the unsplit ancestor. Otherwise, we return 0. auto block = makeRefPtr(enclosingBlock(node)); if (!block || block == node) return nullptr; RefPtr highestAncestorWithUnicodeBidi; RefPtr nextHighestAncestorWithUnicodeBidi; int highestAncestorUnicodeBidi = 0; for (auto ancestor = makeRefPtr(node->parentNode()); ancestor != block; ancestor = ancestor->parentNode()) { int unicodeBidi = toIdentifier(ComputedStyleExtractor(ancestor.get()).propertyValue(CSSPropertyUnicodeBidi)); if (unicodeBidi && unicodeBidi != CSSValueNormal) { highestAncestorUnicodeBidi = unicodeBidi; nextHighestAncestorWithUnicodeBidi = highestAncestorWithUnicodeBidi; highestAncestorWithUnicodeBidi = ancestor; } } if (!highestAncestorWithUnicodeBidi) return nullptr; RefPtr unsplitAncestor; if (allowedDirection != WritingDirection::Natural && highestAncestorUnicodeBidi != CSSValueBidiOverride && is(*highestAncestorWithUnicodeBidi)) { auto highestAncestorDirection = EditingStyle::create(highestAncestorWithUnicodeBidi.get(), EditingStyle::AllProperties)->textDirection(); if (highestAncestorDirection && *highestAncestorDirection == allowedDirection) { if (!nextHighestAncestorWithUnicodeBidi) return static_pointer_cast(WTFMove(highestAncestorWithUnicodeBidi)); unsplitAncestor = static_pointer_cast(highestAncestorWithUnicodeBidi); highestAncestorWithUnicodeBidi = nextHighestAncestorWithUnicodeBidi; } } // Split every ancestor through highest ancestor with embedding. RefPtr currentNode = node; while (currentNode) { RefPtr parent = downcast(currentNode->parentNode()); if (before ? currentNode->previousSibling() : currentNode->nextSibling()) splitElement(*parent, before ? *currentNode : *currentNode->nextSibling()); if (parent == highestAncestorWithUnicodeBidi) break; currentNode = parent; } return unsplitAncestor; } void ApplyStyleCommand::removeEmbeddingUpToEnclosingBlock(Node* node, Node* unsplitAncestor) { auto block = makeRefPtr(enclosingBlock(node)); if (!block || block == node) return; for (RefPtr ancestor = node->parentNode(), parent; ancestor != block && ancestor != unsplitAncestor; ancestor = parent) { parent = ancestor->parentNode(); if (!is(*ancestor)) continue; StyledElement& element = downcast(*ancestor); int unicodeBidi = toIdentifier(ComputedStyleExtractor(&element).propertyValue(CSSPropertyUnicodeBidi)); if (!unicodeBidi || unicodeBidi == CSSValueNormal) continue; // FIXME: This code should really consider the mapped attribute 'dir', the inline style declaration, // and all matching style rules in order to determine how to best set the unicode-bidi property to 'normal'. // For now, it assumes that if the 'dir' attribute is present, then removing it will suffice, and // otherwise it sets the property in the inline style declaration. if (element.hasAttributeWithoutSynchronization(dirAttr)) { // FIXME: If this is a BDO element, we should probably just remove it if it has no // other attributes, like we (should) do with B and I elements. removeNodeAttribute(element, dirAttr); } else { auto inlineStyle = copyStyleOrCreateEmpty(element.inlineStyle()); inlineStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueNormal); inlineStyle->removeProperty(CSSPropertyDirection); setNodeAttribute(element, styleAttr, inlineStyle->asText()); if (isSpanWithoutAttributesOrUnstyledStyleSpan(element)) removeNodePreservingChildren(element); } } } static RefPtr highestEmbeddingAncestor(Node* startNode, Node* enclosingNode) { for (auto currentNode = makeRefPtr(startNode); currentNode && currentNode != enclosingNode; currentNode = currentNode->parentNode()) { if (currentNode->isHTMLElement() && toIdentifier(ComputedStyleExtractor(currentNode.get()).propertyValue(CSSPropertyUnicodeBidi)) == CSSValueEmbed) return currentNode; } return nullptr; } void ApplyStyleCommand::applyInlineStyle(EditingStyle& style) { RefPtr startDummySpanAncestor; RefPtr endDummySpanAncestor; // update document layout once before removing styles // so that we avoid the expense of updating before each and every call // to check a computed style document().updateLayoutIgnorePendingStylesheets(); // adjust to the positions we want to use for applying style auto start = startPosition(); auto end = endPosition(); if (end < start) std::swap(start, end); // split the start node and containing element if the selection starts inside of it bool splitStart = isValidCaretPositionInTextNode(start); if (splitStart) { if (shouldSplitTextElement(start.deprecatedNode()->parentElement(), style)) splitTextElementAtStart(start, end); else splitTextAtStart(start, end); start = startPosition(); end = endPosition(); startDummySpanAncestor = dummySpanAncestorForNode(start.deprecatedNode()); } // split the end node and containing element if the selection ends inside of it bool splitEnd = isValidCaretPositionInTextNode(end); if (splitEnd) { if (shouldSplitTextElement(end.deprecatedNode()->parentElement(), style)) splitTextElementAtEnd(start, end); else splitTextAtEnd(start, end); start = startPosition(); end = endPosition(); endDummySpanAncestor = dummySpanAncestorForNode(end.deprecatedNode()); } if (start.isNull() || end.isNull()) return; // Remove style from the selection. // Use the upstream position of the start for removing style. // This will ensure we remove all traces of the relevant styles from the selection // and prevent us from adding redundant ones, as described in: // Bolding and unbolding creates extraneous tags Position removeStart = start.upstream(); auto textDirection = style.textDirection(); RefPtr styleWithoutEmbedding; RefPtr embeddingStyle; if (textDirection) { // Leave alone an ancestor that provides the desired single level embedding, if there is one. auto startUnsplitAncestor = splitAncestorsWithUnicodeBidi(start.deprecatedNode(), true, *textDirection); auto endUnsplitAncestor = splitAncestorsWithUnicodeBidi(end.deprecatedNode(), false, *textDirection); removeEmbeddingUpToEnclosingBlock(start.deprecatedNode(), startUnsplitAncestor.get()); removeEmbeddingUpToEnclosingBlock(end.deprecatedNode(), endUnsplitAncestor.get()); // Avoid removing the dir attribute and the unicode-bidi and direction properties from the unsplit ancestors. Position embeddingRemoveStart = removeStart; if (startUnsplitAncestor && nodeFullySelected(*startUnsplitAncestor, removeStart, end)) embeddingRemoveStart = positionInParentAfterNode(startUnsplitAncestor.get()); Position embeddingRemoveEnd = end; if (endUnsplitAncestor && nodeFullySelected(*endUnsplitAncestor, removeStart, end)) embeddingRemoveEnd = positionInParentBeforeNode(endUnsplitAncestor.get()).downstream(); if (embeddingRemoveEnd != removeStart || embeddingRemoveEnd != end) { styleWithoutEmbedding = style.copy(); embeddingStyle = styleWithoutEmbedding->extractAndRemoveTextDirection(); if (embeddingRemoveStart <= embeddingRemoveEnd) removeInlineStyle(*embeddingStyle, embeddingRemoveStart, embeddingRemoveEnd); } } removeInlineStyle(styleWithoutEmbedding ? *styleWithoutEmbedding : style, removeStart, end); start = startPosition(); end = endPosition(); if (start.isNull() || start.isOrphan() || end.isNull() || end.isOrphan()) return; if (splitStart && mergeStartWithPreviousIfIdentical(start, end)) { start = startPosition(); end = endPosition(); } if (splitEnd) { mergeEndWithNextIfIdentical(start, end); start = startPosition(); end = endPosition(); } if (start.isNull() || end.isNull()) return; // update document layout once before running the rest of the function // so that we avoid the expense of updating before each and every call // to check a computed style document().updateLayoutIgnorePendingStylesheets(); RefPtr styleToApply = &style; if (textDirection) { // Avoid applying the unicode-bidi and direction properties beneath ancestors that already have them. auto embeddingStartNode = highestEmbeddingAncestor(start.deprecatedNode(), enclosingBlock(start.deprecatedNode())); auto embeddingEndNode = highestEmbeddingAncestor(end.deprecatedNode(), enclosingBlock(end.deprecatedNode())); if (embeddingStartNode || embeddingEndNode) { Position embeddingApplyStart = embeddingStartNode ? positionInParentAfterNode(embeddingStartNode.get()) : start; Position embeddingApplyEnd = embeddingEndNode ? positionInParentBeforeNode(embeddingEndNode.get()) : end; ASSERT(embeddingApplyStart.isNotNull() && embeddingApplyEnd.isNotNull()); if (!embeddingStyle) { styleWithoutEmbedding = style.copy(); embeddingStyle = styleWithoutEmbedding->extractAndRemoveTextDirection(); } fixRangeAndApplyInlineStyle(*embeddingStyle, embeddingApplyStart, embeddingApplyEnd); styleToApply = styleWithoutEmbedding; } } fixRangeAndApplyInlineStyle(*styleToApply, start, end); // Remove dummy style spans created by splitting text elements. cleanupUnstyledAppleStyleSpans(startDummySpanAncestor.get()); if (endDummySpanAncestor != startDummySpanAncestor) cleanupUnstyledAppleStyleSpans(endDummySpanAncestor.get()); } void ApplyStyleCommand::fixRangeAndApplyInlineStyle(EditingStyle& style, const Position& start, const Position& end) { auto startNode = makeRefPtr(start.deprecatedNode()); if (start.deprecatedEditingOffset() >= caretMaxOffset(*startNode)) { startNode = NodeTraversal::next(*startNode); if (!startNode || end < firstPositionInOrBeforeNode(startNode.get())) return; } auto pastEndNode = makeRefPtr(end.deprecatedNode()); if (end.deprecatedEditingOffset() >= caretMaxOffset(*pastEndNode)) pastEndNode = NodeTraversal::nextSkippingChildren(*pastEndNode); // FIXME: Callers should perform this operation on a Range that includes the br // if they want style applied to the empty line. // FIXME: Should this be using startNode instead of start.deprecatedNode()? if (start == end && start.deprecatedNode()->hasTagName(brTag)) pastEndNode = NodeTraversal::next(*start.deprecatedNode()); // Start from the highest fully selected ancestor so that we can modify the fully selected node. // e.g. When applying font-size: large on hello, we need to include the font element in our run // to generate hello instead of hello auto range = *makeSimpleRange(start, end); auto editableRoot = makeRefPtr(startNode->rootEditableElement()); if (startNode != editableRoot) { while (editableRoot && startNode->parentNode() != editableRoot && isNodeVisiblyContainedWithin(*startNode->parentNode(), range)) startNode = startNode->parentNode(); } applyInlineStyleToNodeRange(style, *startNode, pastEndNode.get()); } static bool containsNonEditableRegion(Node& node) { if (!node.hasEditableStyle()) return true; auto sibling = makeRefPtr(NodeTraversal::nextSkippingChildren(node)); for (auto descendant = makeRefPtr(node.firstChild()); descendant && descendant != sibling; descendant = NodeTraversal::next(*descendant)) { if (!descendant->hasEditableStyle()) return true; } return false; } struct InlineRunToApplyStyle { InlineRunToApplyStyle(Node* start, Node* end, Node* pastEndNode) : start(start) , end(end) , pastEndNode(pastEndNode) { ASSERT(start->parentNode() == end->parentNode()); } bool startAndEndAreStillInDocument() { return start && end && start->isConnected() && end->isConnected(); } RefPtr start; RefPtr end; RefPtr pastEndNode; Position positionForStyleComputation; RefPtr dummyElement; StyleChange change; }; void ApplyStyleCommand::applyInlineStyleToNodeRange(EditingStyle& style, Node& startNode, Node* pastEndNode) { if (m_removeOnly) return; document().updateLayoutIgnorePendingStylesheets(); Vector runs; RefPtr node = &startNode; for (RefPtr next; node && node != pastEndNode; node = next) { next = NodeTraversal::next(*node); if (!node->renderer() || !node->hasEditableStyle()) continue; if (!node->hasRichlyEditableStyle() && is(*node)) { // This is a plaintext-only region. Only proceed if it's fully selected. // pastEndNode is the node after the last fully selected node, so if it's inside node then // node isn't fully selected. if (pastEndNode && pastEndNode->isDescendantOf(*node)) break; // Add to this element's inline style and skip over its contents. HTMLElement& element = downcast(*node); RefPtr inlineStyle = copyStyleOrCreateEmpty(element.inlineStyle()); if (auto otherStyle = makeRefPtr(style.style())) inlineStyle->mergeAndOverrideOnConflict(*otherStyle); setNodeAttribute(element, styleAttr, inlineStyle->asText()); next = NodeTraversal::nextSkippingChildren(*node); continue; } if (isBlock(node.get())) continue; if (node->hasChildNodes()) { if (node->contains(pastEndNode) || containsNonEditableRegion(*node) || !node->parentNode()->hasEditableStyle()) continue; if (editingIgnoresContent(*node)) { next = NodeTraversal::nextSkippingChildren(*node); continue; } } auto runStart = node; auto runEnd = node; auto sibling = makeRefPtr(node->nextSibling()); while (sibling && sibling != pastEndNode && !sibling->contains(pastEndNode) && (!isBlock(sibling.get()) || sibling->hasTagName(brTag)) && !containsNonEditableRegion(*sibling)) { runEnd = sibling; sibling = runEnd->nextSibling(); } next = NodeTraversal::nextSkippingChildren(*runEnd); auto pastEndNode = makeRefPtr(NodeTraversal::nextSkippingChildren(*runEnd)); if (!shouldApplyInlineStyleToRun(style, runStart.get(), pastEndNode.get())) continue; runs.append(InlineRunToApplyStyle(runStart.get(), runEnd.get(), pastEndNode.get())); } for (auto& run : runs) { removeConflictingInlineStyleFromRun(style, run.start, run.end, run.pastEndNode.get()); if (run.startAndEndAreStillInDocument()) run.positionForStyleComputation = positionToComputeInlineStyleChange(*run.start, run.dummyElement); } document().updateLayoutIgnorePendingStylesheets(); for (auto& run : runs) run.change = StyleChange(&style, run.positionForStyleComputation); for (auto& run : runs) { if (run.dummyElement) removeNode(*run.dummyElement); if (run.startAndEndAreStillInDocument()) applyInlineStyleChange(run.start.releaseNonNull(), run.end.releaseNonNull(), run.change, AddStyledElement); } } bool ApplyStyleCommand::isStyledInlineElementToRemove(Element* element) const { return (m_styledInlineElement && element->hasTagName(m_styledInlineElement->tagQName())) || (m_isInlineElementToRemoveFunction && m_isInlineElementToRemoveFunction(element)); } bool ApplyStyleCommand::shouldApplyInlineStyleToRun(EditingStyle& style, Node* runStart, Node* pastEndNode) { ASSERT(runStart); for (auto node = makeRefPtr(runStart); node && node != pastEndNode; node = NodeTraversal::next(*node)) { if (node->hasChildNodes()) continue; // We don't consider m_isInlineElementToRemoveFunction here because we never apply style when m_isInlineElementToRemoveFunction is specified if (!style.styleIsPresentInComputedStyleOfNode(*node)) return true; if (m_styledInlineElement && !enclosingElementWithTag(positionBeforeNode(node.get()), m_styledInlineElement->tagQName())) return true; } return false; } void ApplyStyleCommand::removeConflictingInlineStyleFromRun(EditingStyle& style, RefPtr& runStart, RefPtr& runEnd, Node* pastEndNode) { ASSERT(runStart && runEnd); RefPtr next = runStart; for (RefPtr node = next; node && node->isConnected() && node != pastEndNode; node = next) { if (editingIgnoresContent(*node)) { ASSERT(!node->contains(pastEndNode)); next = NodeTraversal::nextSkippingChildren(*node); } else next = NodeTraversal::next(*node); if (!is(*node)) continue; RefPtr previousSibling = node->previousSibling(); RefPtr nextSibling = node->nextSibling(); RefPtr parent = node->parentNode(); removeInlineStyleFromElement(style, downcast(*node), RemoveAlways); if (!node->isConnected()) { // FIXME: We might need to update the start and the end of current selection here but need a test. if (runStart == node) runStart = previousSibling ? previousSibling->nextSibling() : parent->firstChild(); if (runEnd == node) runEnd = nextSibling ? nextSibling->previousSibling() : parent->lastChild(); } } } bool ApplyStyleCommand::removeInlineStyleFromElement(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle) { if (!element.parentNode() || !isEditableNode(*element.parentNode())) return false; if (isStyledInlineElementToRemove(&element)) { if (mode == RemoveNone) return true; if (extractedStyle) extractedStyle->mergeInlineStyleOfElement(element, EditingStyle::OverrideValues); removeNodePreservingChildren(element); return true; } bool removed = false; if (removeImplicitlyStyledElement(style, element, mode, extractedStyle)) removed = true; if (!element.isConnected()) return removed; // If the node was converted to a span, the span may still contain relevant // styles which must be removed (e.g. ) if (removeCSSStyle(style, element, mode, extractedStyle)) removed = true; return removed; } void ApplyStyleCommand::replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement& element) { if (hasNoAttributeOrOnlyStyleAttribute(element, StyleAttributeShouldBeEmpty)) removeNodePreservingChildren(element); else { HTMLElement* newSpanElement = replaceElementWithSpanPreservingChildrenAndAttributes(element); ASSERT_UNUSED(newSpanElement, newSpanElement && newSpanElement->isConnected()); } } bool ApplyStyleCommand::removeImplicitlyStyledElement(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle) { if (mode == RemoveNone) { ASSERT(!extractedStyle); return style.conflictsWithImplicitStyleOfElement(element) || style.conflictsWithImplicitStyleOfAttributes(element); } ASSERT(mode == RemoveIfNeeded || mode == RemoveAlways); if (style.conflictsWithImplicitStyleOfElement(element, extractedStyle, mode == RemoveAlways ? EditingStyle::ExtractMatchingStyle : EditingStyle::DoNotExtractMatchingStyle)) { replaceWithSpanOrRemoveIfWithoutAttributes(element); return true; } // unicode-bidi and direction are pushed down separately so don't push down with other styles Vector attributes; if (!style.extractConflictingImplicitStyleOfAttributes(element, extractedStyle ? EditingStyle::PreserveWritingDirection : EditingStyle::DoNotPreserveWritingDirection, extractedStyle, attributes, mode == RemoveAlways ? EditingStyle::ExtractMatchingStyle : EditingStyle::DoNotExtractMatchingStyle)) return false; for (auto& attribute : attributes) removeNodeAttribute(element, attribute); if (isEmptyFontTag(&element) || isSpanWithoutAttributesOrUnstyledStyleSpan(element)) removeNodePreservingChildren(element); return true; } bool ApplyStyleCommand::removeCSSStyle(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle) { if (mode == RemoveNone) return style.conflictsWithInlineStyleOfElement(element); RefPtr newInlineStyle; if (!style.conflictsWithInlineStyleOfElement(element, newInlineStyle, extractedStyle)) return false; if (newInlineStyle->isEmpty()) removeNodeAttribute(element, styleAttr); else setNodeAttribute(element, styleAttr, newInlineStyle->asText()); if (isSpanWithoutAttributesOrUnstyledStyleSpan(element)) removeNodePreservingChildren(element); return true; } RefPtr ApplyStyleCommand::highestAncestorWithConflictingInlineStyle(EditingStyle& style, Node* node) { if (!node) return nullptr; RefPtr result; auto unsplittableElement = makeRefPtr(unsplittableElementForPosition(firstPositionInOrBeforeNode(node))); for (auto ancestor = makeRefPtr(node); ancestor; ancestor = ancestor->parentNode()) { if (is(*ancestor) && shouldRemoveInlineStyleFromElement(style, downcast(*ancestor))) result = static_pointer_cast(ancestor); // Should stop at the editable root (cannot cross editing boundary) and // also stop at the unsplittable element to be consistent with other UAs if (ancestor == unsplittableElement) break; } return result; } void ApplyStyleCommand::applyInlineStyleToPushDown(Node& node, EditingStyle* style) { node.document().updateStyleIfNeeded(); if (!style || style->isEmpty() || !node.renderer() || is(node)) return; RefPtr newInlineStyle = style; if (is(node) && downcast(node).inlineStyle()) { newInlineStyle = style->copy(); newInlineStyle->mergeInlineStyleOfElement(downcast(node), EditingStyle::OverrideValues); } // Since addInlineStyleIfNeeded can't add styles to block-flow render objects, add style attribute instead. // FIXME: applyInlineStyleToRange should be used here instead. if ((node.renderer()->isRenderBlockFlow() || node.hasChildNodes()) && is(node)) { setNodeAttribute(downcast(node), styleAttr, newInlineStyle->style()->asText()); return; } { ScriptDisallowedScope::InMainThread scriptDisallowedScope; if (node.renderer()->isText() && static_cast(node.renderer())->isAllCollapsibleWhitespace()) return; if (node.renderer()->isBR() && !node.renderer()->style().preserveNewline()) return; } // We can't wrap node with the styled element here because new styled element will never be removed if we did. // If we modified the child pointer in pushDownInlineStyleAroundNode to point to new style element // then we fall into an infinite loop where we keep removing and adding styled element wrapping node. addInlineStyleIfNeeded(newInlineStyle.get(), node, node, DoNotAddStyledElement); } void ApplyStyleCommand::pushDownInlineStyleAroundNode(EditingStyle& style, Node* targetNode) { auto highestAncestor = highestAncestorWithConflictingInlineStyle(style, targetNode); if (!highestAncestor) return; // The outer loop is traversing the tree vertically from highestAncestor to targetNode RefPtr current = highestAncestor; // Along the way, styled elements that contain targetNode are removed and accumulated into elementsToPushDown. // Each child of the removed element, exclusing ancestors of targetNode, is then wrapped by clones of elements in elementsToPushDown. Vector> elementsToPushDown; while (current && current != targetNode && current->contains(targetNode)) { auto currentChildren = collectChildNodes(*current); RefPtr styledElement; if (is(*current) && isStyledInlineElementToRemove(downcast(current.get()))) { styledElement = downcast(current.get()); elementsToPushDown.append(*styledElement); } auto styleToPushDown = EditingStyle::create(); if (is(*current)) removeInlineStyleFromElement(style, downcast(*current), RemoveIfNeeded, styleToPushDown.ptr()); // The inner loop will go through children on each level // FIXME: we should aggregate inline child elements together so that we don't wrap each child separately. for (Ref& childRef : currentChildren) { Node& child = childRef; if (!child.parentNode()) continue; if (!child.contains(targetNode) && elementsToPushDown.size()) { for (auto& element : elementsToPushDown) { auto wrapper = element->cloneElementWithoutChildren(document()); wrapper->removeAttribute(styleAttr); surroundNodeRangeWithElement(child, child, WTFMove(wrapper)); } } // Apply style to all nodes containing targetNode and their siblings but NOT to targetNode // But if we've removed styledElement then always apply the style. if (&child != targetNode || styledElement) applyInlineStyleToPushDown(child, styleToPushDown.ptr()); // We found the next node for the outer loop (contains targetNode) // When reached targetNode, stop the outer loop upon the completion of the current inner loop if (&child == targetNode || child.contains(targetNode)) current = &child; } } } void ApplyStyleCommand::removeInlineStyle(EditingStyle& style, const Position& start, const Position& end) { ASSERT(start.isNotNull()); ASSERT(end.isNotNull()); ASSERT(start.anchorNode()->isConnected()); ASSERT(end.anchorNode()->isConnected()); ASSERT(is_lteq(treeOrder(start, end))); // FIXME: We should assert that start/end are not in the middle of a text node. Position pushDownStart = start.downstream(); // If the pushDownStart is at the end of a text node, then this node is not fully selected. // Move it to the next deep quivalent position to avoid removing the style from this node. // e.g. if pushDownStart was at Position("hello", 5) in hello
world
, we want Position("world", 0) instead. auto pushDownStartContainer = makeRefPtr(pushDownStart.containerNode()); if (is(pushDownStartContainer) && static_cast(pushDownStart.computeOffsetInContainerNode()) == downcast(*pushDownStartContainer).length()) pushDownStart = nextVisuallyDistinctCandidate(pushDownStart); // If pushDownEnd is at the start of a text node, then this node is not fully selected. // Move it to the previous deep equivalent position to avoid removing the style from this node. Position pushDownEnd = end.upstream(); auto pushDownEndContainer = makeRefPtr(pushDownEnd.containerNode()); if (is(pushDownEndContainer) && !pushDownEnd.computeOffsetInContainerNode()) pushDownEnd = previousVisuallyDistinctCandidate(pushDownEnd); pushDownInlineStyleAroundNode(style, pushDownStart.deprecatedNode()); pushDownInlineStyleAroundNode(style, pushDownEnd.deprecatedNode()); // The s and e variables store the positions used to set the ending selection after style removal // takes place. This will help callers to recognize when either the start node or the end node // are removed from the document during the work of this function. // If pushDownInlineStyleAroundNode has pruned start.deprecatedNode() or end.deprecatedNode(), // use pushDownStart or pushDownEnd instead, which pushDownInlineStyleAroundNode won't prune. Position s = start.isNull() || start.isOrphan() ? pushDownStart : start; Position e = end.isNull() || end.isOrphan() ? pushDownEnd : end; RefPtr node = start.deprecatedNode(); while (node) { RefPtr next; if (editingIgnoresContent(*node)) { ASSERT(node == end.deprecatedNode() || !node->contains(end.deprecatedNode())); next = NodeTraversal::nextSkippingChildren(*node); } else next = NodeTraversal::next(*node); if (is(*node) && nodeFullySelected(downcast(*node), start, end)) { Ref element = downcast(*node); RefPtr prev = NodeTraversal::previousPostOrder(element); RefPtr next = NodeTraversal::next(element); RefPtr styleToPushDown; RefPtr childNode; if (isStyledInlineElementToRemove(element.ptr())) { styleToPushDown = EditingStyle::create(); childNode = element->firstChild(); } removeInlineStyleFromElement(style, element, RemoveIfNeeded, styleToPushDown.get()); if (!element->isConnected()) { if (s.deprecatedNode() == element.ptr()) { // Since elem must have been fully selected, and it is at the start // of the selection, it is clear we can set the new s offset to 0. ASSERT(s.anchorType() == Position::PositionIsBeforeAnchor || s.offsetInContainerNode() <= 0); s = firstPositionInOrBeforeNode(next.get()); } if (e.deprecatedNode() == element.ptr()) { // Since elem must have been fully selected, and it is at the end // of the selection, it is clear we can set the new e offset to // the max range offset of prev. ASSERT(s.anchorType() == Position::PositionIsAfterAnchor || !offsetIsBeforeLastNodeOffset(s.offsetInContainerNode(), s.containerNode())); e = lastPositionInOrAfterNode(prev.get()); } } if (styleToPushDown) { for (; childNode; childNode = childNode->nextSibling()) applyInlineStyleToPushDown(*childNode, styleToPushDown.get()); } } if (node == end.deprecatedNode()) break; node = next.get(); } updateStartEnd(s, e); } bool ApplyStyleCommand::nodeFullySelected(Element& element, const Position& start, const Position& end) const { // The tree may have changed and Position::upstream() relies on an up-to-date layout. element.document().updateLayoutIgnorePendingStylesheets(); return firstPositionInOrBeforeNode(&element) >= start && lastPositionInOrAfterNode(&element).upstream() <= end; } bool ApplyStyleCommand::nodeFullyUnselected(Element& element, const Position& start, const Position& end) const { // The tree may have changed and Position::upstream() relies on an up-to-date layout. element.document().updateLayoutIgnorePendingStylesheets(); return lastPositionInOrAfterNode(&element).upstream() < start || firstPositionInOrBeforeNode(&element) > end; } void ApplyStyleCommand::splitTextAtStart(const Position& start, const Position& end) { ASSERT(is(start.containerNode())); Position newEnd; if (end.anchorType() == Position::PositionIsOffsetInAnchor && start.containerNode() == end.containerNode()) newEnd = Position(end.containerText(), end.offsetInContainerNode() - start.offsetInContainerNode()); else newEnd = end; RefPtr text = start.containerText(); splitTextNode(*text, start.offsetInContainerNode()); updateStartEnd(firstPositionInNode(text.get()), newEnd); } void ApplyStyleCommand::splitTextAtEnd(const Position& start, const Position& end) { ASSERT(is(end.containerNode())); bool shouldUpdateStart = start.anchorType() == Position::PositionIsOffsetInAnchor && start.containerNode() == end.containerNode(); Text& text = downcast(*end.deprecatedNode()); splitTextNode(text, end.offsetInContainerNode()); Node* prevNode = text.previousSibling(); if (!is(prevNode)) return; Position newStart = shouldUpdateStart ? Position(downcast(prevNode), start.offsetInContainerNode()) : start; updateStartEnd(newStart, lastPositionInNode(prevNode)); } void ApplyStyleCommand::splitTextElementAtStart(const Position& start, const Position& end) { ASSERT(is(start.containerNode())); Position newEnd; if (start.containerNode() == end.containerNode()) newEnd = Position(end.containerText(), end.offsetInContainerNode() - start.offsetInContainerNode()); else newEnd = end; splitTextNodeContainingElement(*start.containerText(), start.offsetInContainerNode()); updateStartEnd(positionBeforeNode(start.containerNode()), newEnd); } void ApplyStyleCommand::splitTextElementAtEnd(const Position& start, const Position& end) { ASSERT(is(end.containerNode())); bool shouldUpdateStart = start.containerNode() == end.containerNode(); splitTextNodeContainingElement(*end.containerText(), end.offsetInContainerNode()); Node* parentElement = end.containerNode()->parentNode(); if (!parentElement || !parentElement->previousSibling()) return; Node* firstTextNode = parentElement->previousSibling()->lastChild(); if (!is(firstTextNode)) return; Position newStart = shouldUpdateStart ? Position(downcast(firstTextNode), start.offsetInContainerNode()) : start; updateStartEnd(newStart, positionAfterNode(firstTextNode)); } bool ApplyStyleCommand::shouldSplitTextElement(Element* element, EditingStyle& style) { if (!is(element)) return false; return shouldRemoveInlineStyleFromElement(style, downcast(*element)); } bool ApplyStyleCommand::isValidCaretPositionInTextNode(const Position& position) { Node* node = position.containerNode(); if (position.anchorType() != Position::PositionIsOffsetInAnchor || !is(node)) return false; int offsetInText = position.offsetInContainerNode(); return offsetInText > caretMinOffset(*node) && offsetInText < caretMaxOffset(*node); } bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position& start, const Position& end) { auto startNode = makeRefPtr(start.containerNode()); if (start.computeOffsetInContainerNode()) return false; if (isAtomicNode(startNode.get())) { // note: prior siblings could be unrendered elements. it's silly to miss the // merge opportunity just for that. if (startNode->previousSibling()) return false; startNode = startNode->parentNode(); } auto previousSibling = makeRefPtr(startNode->previousSibling()); if (!previousSibling || !areIdenticalElements(*startNode, *previousSibling)) return false; auto& previousElement = downcast(*previousSibling); auto& element = downcast(*startNode); auto* startChild = element.firstChild(); ASSERT(startChild); mergeIdenticalElements(previousElement, element); // FIXME: Inconsistent that we use computeOffsetInContainerNode for start, but deprecatedEditingOffset for end. unsigned startOffset = startChild->computeNodeIndex(); unsigned endOffset = end.deprecatedEditingOffset() + (startNode == end.deprecatedNode() ? startOffset : 0); updateStartEnd({ startNode.get(), startOffset, Position::PositionIsOffsetInAnchor }, { end.deprecatedNode(), endOffset, Position::PositionIsOffsetInAnchor }); return true; } bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position& start, const Position& end) { auto endNode = makeRefPtr(end.containerNode()); if (isAtomicNode(endNode.get())) { int endOffset = end.computeOffsetInContainerNode(); if (offsetIsBeforeLastNodeOffset(endOffset, endNode.get()) || end.deprecatedNode()->nextSibling()) return false; endNode = end.deprecatedNode()->parentNode(); } if (endNode->hasTagName(brTag)) return false; auto nextSibling = makeRefPtr(endNode->nextSibling()); if (!nextSibling || !areIdenticalElements(*endNode, *nextSibling)) return false; auto& nextElement = downcast(*nextSibling); auto& element = downcast(*endNode); Node* nextChild = nextElement.firstChild(); mergeIdenticalElements(element, nextElement); bool shouldUpdateStart = start.containerNode() == endNode; unsigned endOffset = nextChild ? nextChild->computeNodeIndex() : nextElement.countChildNodes(); updateStartEnd(shouldUpdateStart ? Position(&nextElement, start.offsetInContainerNode(), Position::PositionIsOffsetInAnchor) : start, { &nextElement, endOffset, Position::PositionIsOffsetInAnchor }); return true; } bool ApplyStyleCommand::surroundNodeRangeWithElement(Node& startNode, Node& endNode, Ref&& elementToInsert) { Ref protectedStartNode = startNode; Ref element = WTFMove(elementToInsert); insertNodeBefore(element.copyRef(), startNode); if (!element->isContentRichlyEditable()) { removeNode(element); return false; } RefPtr node = &startNode; while (node) { RefPtr next = node->nextSibling(); if (isEditableNode(*node)) { removeNode(*node); appendNode(*node, element.copyRef()); } if (node == &endNode) break; node = next; } RefPtr nextSibling = element->nextSibling(); RefPtr previousSibling = element->previousSibling(); if (nextSibling && nextSibling->hasEditableStyle() && areIdenticalElements(element, *nextSibling)) mergeIdenticalElements(element, downcast(*nextSibling)); if (is(previousSibling) && previousSibling->hasEditableStyle()) { auto mergedElement = makeRefPtr(previousSibling->nextSibling()); ASSERT(mergedElement); if (mergedElement->hasEditableStyle() && areIdenticalElements(*previousSibling, *mergedElement)) mergeIdenticalElements(downcast(*previousSibling), downcast(*mergedElement)); } // FIXME: We should probably call updateStartEnd if the start or end was in the node // range so that the endingSelection() is canonicalized. See the comments at the end of // VisibleSelection::validate(). return true; } static String joinWithSpace(const String& a, const String& b) { if (a.isEmpty()) return b; if (b.isEmpty()) return a; return makeString(a, ' ', b); } void ApplyStyleCommand::addBlockStyle(const StyleChange& styleChange, HTMLElement& block) { // Do not check for legacy styles here. Those styles, like and , only apply for inline content. ASSERT(styleChange.cssStyle()); setNodeAttribute(block, styleAttr, joinWithSpace(styleChange.cssStyle()->asText(), block.getAttribute(styleAttr))); } void ApplyStyleCommand::addInlineStyleIfNeeded(EditingStyle* style, Node& start, Node& end, EAddStyledElement addStyledElement) { if (!start.isConnected() || !end.isConnected()) return; Ref protectedStart = start; RefPtr dummyElement; StyleChange styleChange(style, positionToComputeInlineStyleChange(start, dummyElement)); if (dummyElement) removeNode(*dummyElement); applyInlineStyleChange(start, end, styleChange, addStyledElement); } Position ApplyStyleCommand::positionToComputeInlineStyleChange(Node& startNode, RefPtr& dummyElement) { // It's okay to obtain the style at the startNode because we've removed all relevant styles from the current run. if (!is(startNode)) { dummyElement = createStyleSpanElement(document()); insertNodeAt(*dummyElement, positionBeforeNode(&startNode)); return firstPositionInOrBeforeNode(dummyElement.get()); } return firstPositionInOrBeforeNode(&startNode); } void ApplyStyleCommand::applyInlineStyleChange(Node& passedStart, Node& passedEnd, StyleChange& styleChange, EAddStyledElement addStyledElement) { RefPtr startNode = &passedStart; RefPtr endNode = &passedEnd; ASSERT(startNode->isConnected()); ASSERT(endNode->isConnected()); // Find appropriate font and span elements top-down. RefPtr fontContainer; RefPtr styleContainer; while (startNode == endNode) { if (is(*startNode)) { auto& container = downcast(*startNode); if (is(container)) fontContainer = &downcast(container); if (is(container) || (!is(styleContainer) && container.hasChildNodes())) styleContainer = &container; } auto* startNodeFirstChild = startNode->firstChild(); if (!startNodeFirstChild) break; endNode = startNode->lastChild(); startNode = startNodeFirstChild; } // Font tags need to go outside of CSS so that CSS font sizes override leagcy font sizes. if (styleChange.applyFontColor() || styleChange.applyFontFace() || styleChange.applyFontSize()) { if (fontContainer) { if (styleChange.applyFontColor()) setNodeAttribute(*fontContainer, colorAttr, styleChange.fontColor()); if (styleChange.applyFontFace()) setNodeAttribute(*fontContainer, faceAttr, styleChange.fontFace()); if (styleChange.applyFontSize()) setNodeAttribute(*fontContainer, sizeAttr, styleChange.fontSize()); } else { auto fontElement = createFontElement(document()); if (styleChange.applyFontColor()) fontElement->setAttributeWithoutSynchronization(colorAttr, styleChange.fontColor()); if (styleChange.applyFontFace()) fontElement->setAttributeWithoutSynchronization(faceAttr, styleChange.fontFace()); if (styleChange.applyFontSize()) fontElement->setAttributeWithoutSynchronization(sizeAttr, styleChange.fontSize()); surroundNodeRangeWithElement(*startNode, *endNode, WTFMove(fontElement)); } } if (auto styleToMerge = styleChange.cssStyle()) { if (styleContainer) { if (auto existingStyle = styleContainer->inlineStyle()) { auto inlineStyle = EditingStyle::create(existingStyle); inlineStyle->overrideWithStyle(*styleToMerge); setNodeAttribute(*styleContainer, styleAttr, inlineStyle->style()->asText()); } else setNodeAttribute(*styleContainer, styleAttr, styleToMerge->asText()); } else { auto styleElement = createStyleSpanElement(document()); styleElement->setAttribute(styleAttr, styleToMerge->asText()); surroundNodeRangeWithElement(*startNode, *endNode, WTFMove(styleElement)); } } if (styleChange.applyBold()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), bTag)); if (styleChange.applyItalic()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), iTag)); if (styleChange.applyUnderline()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), uTag)); if (styleChange.applyLineThrough()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), strikeTag)); if (styleChange.applySubscript()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), subTag)); else if (styleChange.applySuperscript()) surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), supTag)); if (m_styledInlineElement && addStyledElement == AddStyledElement) surroundNodeRangeWithElement(*startNode, *endNode, m_styledInlineElement->cloneElementWithoutChildren(document())); } float ApplyStyleCommand::computedFontSize(Node* node) { if (!node) return 0; auto value = ComputedStyleExtractor(node).propertyValue(CSSPropertyFontSize); if (!value) return 0; return downcast(*value).floatValue(CSSUnitType::CSS_PX); } void ApplyStyleCommand::joinChildTextNodes(Node* node, const Position& start, const Position& end) { if (!node) return; Position newStart = start; Position newEnd = end; Vector> textNodes; for (Text* textNode = TextNodeTraversal::firstChild(*node); textNode; textNode = TextNodeTraversal::nextSibling(*textNode)) textNodes.append(*textNode); for (auto& childText : textNodes) { auto next = makeRefPtr(childText->nextSibling()); if (!is(next)) continue; Text& nextText = downcast(*next); if (start.anchorType() == Position::PositionIsOffsetInAnchor && next == start.containerNode()) newStart = Position(childText.ptr(), childText->length() + start.offsetInContainerNode()); if (end.anchorType() == Position::PositionIsOffsetInAnchor && next == end.containerNode()) newEnd = Position(childText.ptr(), childText->length() + end.offsetInContainerNode()); String textToMove = nextText.data(); insertTextIntoNode(childText, childText->length(), textToMove); removeNode(*next); // don't move child node pointer. it may want to merge with more text nodes. } updateStartEnd(newStart, newEnd); } }