1/*
2 * Copyright (C) 2005, 2006, 2008, 2009 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 * notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 * notice, this list of conditions and the following disclaimer in the
11 * documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "ApplyStyleCommand.h"
28
29#include "CSSComputedStyleDeclaration.h"
30#include "CSSParser.h"
31#include "CSSValuePool.h"
32#include "Document.h"
33#include "Editing.h"
34#include "Editor.h"
35#include "ElementIterator.h"
36#include "Frame.h"
37#include "HTMLFontElement.h"
38#include "HTMLIFrameElement.h"
39#include "HTMLInterchange.h"
40#include "HTMLNames.h"
41#include "HTMLSpanElement.h"
42#include "NodeList.h"
43#include "NodeTraversal.h"
44#include "RenderObject.h"
45#include "RenderText.h"
46#include "StyleProperties.h"
47#include "StyleResolver.h"
48#include "Text.h"
49#include "TextIterator.h"
50#include "TextNodeTraversal.h"
51#include "VisibleUnits.h"
52#include <wtf/NeverDestroyed.h>
53#include <wtf/StdLibExtras.h>
54#include <wtf/text/StringBuilder.h>
55
56namespace WebCore {
57
58using namespace HTMLNames;
59
60static int toIdentifier(RefPtr<CSSValue>&& value)
61{
62 return is<CSSPrimitiveValue>(value) ? downcast<CSSPrimitiveValue>(*value).valueID() : 0;
63}
64
65static String& styleSpanClassString()
66{
67 static NeverDestroyed<String> styleSpanClassString(AppleStyleSpanClass);
68 return styleSpanClassString;
69}
70
71bool isLegacyAppleStyleSpan(const Node* node)
72{
73 if (!is<HTMLSpanElement>(node))
74 return false;
75
76 return downcast<HTMLSpanElement>(*node).attributeWithoutSynchronization(classAttr) == styleSpanClassString();
77}
78
79static bool hasNoAttributeOrOnlyStyleAttribute(const StyledElement& element, ShouldStyleAttributeBeEmpty shouldStyleAttributeBeEmpty)
80{
81 if (!element.hasAttributes())
82 return true;
83
84 unsigned matchedAttributes = 0;
85 if (element.attributeWithoutSynchronization(classAttr) == styleSpanClassString())
86 matchedAttributes++;
87 if (element.hasAttribute(styleAttr) && (shouldStyleAttributeBeEmpty == AllowNonEmptyStyleAttribute
88 || !element.inlineStyle() || element.inlineStyle()->isEmpty()))
89 matchedAttributes++;
90
91 ASSERT(matchedAttributes <= element.attributeCount());
92 return matchedAttributes == element.attributeCount();
93}
94
95bool isStyleSpanOrSpanWithOnlyStyleAttribute(const Element& element)
96{
97 if (!is<HTMLSpanElement>(element))
98 return false;
99 return hasNoAttributeOrOnlyStyleAttribute(downcast<HTMLSpanElement>(element), AllowNonEmptyStyleAttribute);
100}
101
102static inline bool isSpanWithoutAttributesOrUnstyledStyleSpan(const Element& element)
103{
104 if (!is<HTMLSpanElement>(element))
105 return false;
106 return hasNoAttributeOrOnlyStyleAttribute(downcast<HTMLSpanElement>(element), StyleAttributeShouldBeEmpty);
107}
108
109bool isEmptyFontTag(const Element* element, ShouldStyleAttributeBeEmpty shouldStyleAttributeBeEmpty)
110{
111 if (!is<HTMLFontElement>(element))
112 return false;
113
114 return hasNoAttributeOrOnlyStyleAttribute(downcast<HTMLFontElement>(*element), shouldStyleAttributeBeEmpty);
115}
116
117static Ref<HTMLElement> createFontElement(Document& document)
118{
119 return createHTMLElement(document, fontTag);
120}
121
122Ref<HTMLElement> createStyleSpanElement(Document& document)
123{
124 return createHTMLElement(document, spanTag);
125}
126
127ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, EditAction editingAction, EPropertyLevel propertyLevel)
128 : CompositeEditCommand(document, editingAction)
129 , m_style(style->copy())
130 , m_propertyLevel(propertyLevel)
131 , m_start(endingSelection().start().downstream())
132 , m_end(endingSelection().end().upstream())
133 , m_useEndingSelection(true)
134 , m_removeOnly(false)
135{
136}
137
138ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, const Position& start, const Position& end, EditAction editingAction, EPropertyLevel propertyLevel)
139 : CompositeEditCommand(document, editingAction)
140 , m_style(style->copy())
141 , m_propertyLevel(propertyLevel)
142 , m_start(start)
143 , m_end(end)
144 , m_useEndingSelection(false)
145 , m_removeOnly(false)
146{
147}
148
149ApplyStyleCommand::ApplyStyleCommand(Ref<Element>&& element, bool removeOnly, EditAction editingAction)
150 : CompositeEditCommand(element->document(), editingAction)
151 , m_style(EditingStyle::create())
152 , m_propertyLevel(PropertyDefault)
153 , m_start(endingSelection().start().downstream())
154 , m_end(endingSelection().end().upstream())
155 , m_useEndingSelection(true)
156 , m_styledInlineElement(WTFMove(element))
157 , m_removeOnly(removeOnly)
158{
159}
160
161ApplyStyleCommand::ApplyStyleCommand(Document& document, const EditingStyle* style, IsInlineElementToRemoveFunction isInlineElementToRemoveFunction, EditAction editingAction)
162 : CompositeEditCommand(document, editingAction)
163 , m_style(style->copy())
164 , m_propertyLevel(PropertyDefault)
165 , m_start(endingSelection().start().downstream())
166 , m_end(endingSelection().end().upstream())
167 , m_useEndingSelection(true)
168 , m_removeOnly(true)
169 , m_isInlineElementToRemoveFunction(isInlineElementToRemoveFunction)
170{
171}
172
173void ApplyStyleCommand::updateStartEnd(const Position& newStart, const Position& newEnd)
174{
175 ASSERT(comparePositions(newEnd, newStart) >= 0);
176
177 if (!m_useEndingSelection && (newStart != m_start || newEnd != m_end))
178 m_useEndingSelection = true;
179
180 bool wasBaseFirst = startingSelection().isBaseFirst() || !startingSelection().isDirectional();
181 setEndingSelection(VisibleSelection(wasBaseFirst ? newStart : newEnd, wasBaseFirst ? newEnd : newStart, VP_DEFAULT_AFFINITY, endingSelection().isDirectional()));
182 m_start = newStart;
183 m_end = newEnd;
184}
185
186Position ApplyStyleCommand::startPosition()
187{
188 if (m_useEndingSelection)
189 return endingSelection().start();
190
191 return m_start;
192}
193
194Position ApplyStyleCommand::endPosition()
195{
196 if (m_useEndingSelection)
197 return endingSelection().end();
198
199 return m_end;
200}
201
202void ApplyStyleCommand::doApply()
203{
204 switch (m_propertyLevel) {
205 case PropertyDefault: {
206 // Apply the block-centric properties of the style.
207 auto blockStyle = m_style->extractAndRemoveBlockProperties();
208 if (!blockStyle->isEmpty())
209 applyBlockStyle(blockStyle);
210 // Apply any remaining styles to the inline elements.
211 if (!m_style->isEmpty() || m_styledInlineElement || m_isInlineElementToRemoveFunction) {
212 applyRelativeFontStyleChange(m_style.get());
213 applyInlineStyle(*m_style);
214 }
215 break;
216 }
217 case ForceBlockProperties:
218 // Force all properties to be applied as block styles.
219 applyBlockStyle(*m_style);
220 break;
221 }
222}
223
224void ApplyStyleCommand::applyBlockStyle(EditingStyle& style)
225{
226 // update document layout once before removing styles
227 // so that we avoid the expense of updating before each and every call
228 // to check a computed style
229 document().updateLayoutIgnorePendingStylesheets();
230
231 // get positions we want to use for applying style
232 Position start = startPosition();
233 Position end = endPosition();
234 if (comparePositions(end, start) < 0) {
235 Position swap = start;
236 start = end;
237 end = swap;
238 }
239
240 VisiblePosition visibleStart(start);
241 VisiblePosition visibleEnd(end);
242
243 if (visibleStart.isNull() || visibleStart.isOrphan() || visibleEnd.isNull() || visibleEnd.isOrphan())
244 return;
245
246 // Save and restore the selection endpoints using their indices in the editable root, since
247 // addBlockStyleIfNeeded may moveParagraphs, which can remove these endpoints.
248 // Calculate start and end indices from the start of the tree that they're in.
249 auto* scope = highestEditableRoot(visibleStart.deepEquivalent());
250 if (!scope)
251 return;
252
253 auto startRange = Range::create(document(), firstPositionInNode(scope), visibleStart.deepEquivalent().parentAnchoredEquivalent());
254 auto endRange = Range::create(document(), firstPositionInNode(scope), visibleEnd.deepEquivalent().parentAnchoredEquivalent());
255 int startIndex = TextIterator::rangeLength(startRange.ptr(), true);
256 int endIndex = TextIterator::rangeLength(endRange.ptr(), true);
257
258 VisiblePosition paragraphStart(startOfParagraph(visibleStart));
259 VisiblePosition nextParagraphStart(endOfParagraph(paragraphStart).next());
260 if (visibleEnd != visibleStart && isStartOfParagraph(visibleEnd))
261 visibleEnd = visibleEnd.previous(CannotCrossEditingBoundary);
262 VisiblePosition beyondEnd(endOfParagraph(visibleEnd).next());
263 while (paragraphStart.isNotNull() && paragraphStart != beyondEnd) {
264 StyleChange styleChange(&style, paragraphStart.deepEquivalent());
265 if (styleChange.cssStyle() || m_removeOnly) {
266 RefPtr<Node> block = enclosingBlock(paragraphStart.deepEquivalent().deprecatedNode());
267 if (!m_removeOnly) {
268 RefPtr<Node> newBlock = moveParagraphContentsToNewBlockIfNecessary(paragraphStart.deepEquivalent());
269 if (newBlock)
270 block = newBlock;
271 }
272 ASSERT(!block || is<HTMLElement>(*block));
273 if (is<HTMLElement>(block)) {
274 removeCSSStyle(style, downcast<HTMLElement>(*block));
275 if (!m_removeOnly)
276 addBlockStyle(styleChange, downcast<HTMLElement>(*block));
277 }
278
279 if (nextParagraphStart.isOrphan())
280 nextParagraphStart = endOfParagraph(paragraphStart).next();
281 }
282
283 paragraphStart = nextParagraphStart;
284 nextParagraphStart = endOfParagraph(paragraphStart).next();
285 }
286
287 {
288 auto startRange = TextIterator::rangeFromLocationAndLength(scope, startIndex, 0, true);
289 auto endRange = TextIterator::rangeFromLocationAndLength(scope, endIndex, 0, true);
290 if (startRange && endRange)
291 updateStartEnd(startRange->startPosition(), endRange->startPosition());
292 }
293}
294
295static Ref<MutableStyleProperties> copyStyleOrCreateEmpty(const StyleProperties* style)
296{
297 if (!style)
298 return MutableStyleProperties::create();
299 return style->mutableCopy();
300}
301
302void ApplyStyleCommand::applyRelativeFontStyleChange(EditingStyle* style)
303{
304 static const float MinimumFontSize = 0.1f;
305
306 if (!style || !style->hasFontSizeDelta())
307 return;
308
309 Position start = startPosition();
310 Position end = endPosition();
311 if (comparePositions(end, start) < 0) {
312 Position swap = start;
313 start = end;
314 end = swap;
315 }
316
317 // Join up any adjacent text nodes.
318 if (is<Text>(*start.deprecatedNode())) {
319 joinChildTextNodes(start.deprecatedNode()->parentNode(), start, end);
320 start = startPosition();
321 end = endPosition();
322 }
323
324 if (start.isNull() || end.isNull())
325 return;
326
327 if (is<Text>(*end.deprecatedNode()) && start.deprecatedNode()->parentNode() != end.deprecatedNode()->parentNode()) {
328 joinChildTextNodes(end.deprecatedNode()->parentNode(), start, end);
329 start = startPosition();
330 end = endPosition();
331 }
332
333 if (start.isNull() || end.isNull())
334 return;
335
336 // Split the start text nodes if needed to apply style.
337 if (isValidCaretPositionInTextNode(start)) {
338 splitTextAtStart(start, end);
339 start = startPosition();
340 end = endPosition();
341 }
342
343 if (start.isNull() || end.isNull())
344 return;
345
346 if (isValidCaretPositionInTextNode(end)) {
347 splitTextAtEnd(start, end);
348 start = startPosition();
349 end = endPosition();
350 }
351
352 if (start.isNull() || end.isNull())
353 return;
354
355 // Calculate loop end point.
356 // If the end node is before the start node (can only happen if the end node is
357 // an ancestor of the start node), we gather nodes up to the next sibling of the end node
358 Node* beyondEnd;
359 ASSERT(start.deprecatedNode());
360 ASSERT(end.deprecatedNode());
361 if (start.deprecatedNode()->isDescendantOf(*end.deprecatedNode()))
362 beyondEnd = NodeTraversal::nextSkippingChildren(*end.deprecatedNode());
363 else
364 beyondEnd = NodeTraversal::next(*end.deprecatedNode());
365
366 start = start.upstream(); // Move upstream to ensure we do not add redundant spans.
367 Node* startNode = start.deprecatedNode();
368
369 // Make sure we're not already at the end or the next NodeTraversal::next() will traverse past it.
370 if (startNode == beyondEnd)
371 return;
372
373 if (is<Text>(*startNode) && start.deprecatedEditingOffset() >= caretMaxOffset(*startNode)) {
374 // Move out of text node if range does not include its characters.
375 startNode = NodeTraversal::next(*startNode);
376 if (!startNode)
377 return;
378 }
379
380 // Store away font size before making any changes to the document.
381 // This ensures that changes to one node won't effect another.
382 HashMap<Node*, float> startingFontSizes;
383 for (Node* node = startNode; node != beyondEnd; node = NodeTraversal::next(*node)) {
384 ASSERT(node);
385 startingFontSizes.set(node, computedFontSize(node));
386 }
387
388 // These spans were added by us. If empty after font size changes, they can be removed.
389 Vector<Ref<HTMLElement>> unstyledSpans;
390
391 Node* lastStyledNode = nullptr;
392 for (Node* node = startNode; node != beyondEnd; node = NodeTraversal::next(*node)) {
393 ASSERT(node);
394 RefPtr<HTMLElement> element;
395 if (is<HTMLElement>(*node)) {
396 // Only work on fully selected nodes.
397 if (!nodeFullySelected(downcast<HTMLElement>(*node), start, end))
398 continue;
399 element = &downcast<HTMLElement>(*node);
400 } else if (is<Text>(*node) && node->renderer() && node->parentNode() != lastStyledNode) {
401 // Last styled node was not parent node of this text node, but we wish to style this
402 // text node. To make this possible, add a style span to surround this text node.
403 auto span = createStyleSpanElement(document());
404 surroundNodeRangeWithElement(*node, *node, span.copyRef());
405 element = WTFMove(span);
406 } else {
407 // Only handle HTML elements and text nodes.
408 continue;
409 }
410 lastStyledNode = node;
411
412 RefPtr<MutableStyleProperties> inlineStyle = copyStyleOrCreateEmpty(element->inlineStyle());
413 float currentFontSize = computedFontSize(node);
414 float desiredFontSize = std::max(MinimumFontSize, startingFontSizes.get(node) + style->fontSizeDelta());
415 RefPtr<CSSValue> value = inlineStyle->getPropertyCSSValue(CSSPropertyFontSize);
416 if (value) {
417 element->removeInlineStyleProperty(CSSPropertyFontSize);
418 currentFontSize = computedFontSize(node);
419 }
420 if (currentFontSize != desiredFontSize) {
421 inlineStyle->setProperty(CSSPropertyFontSize, CSSValuePool::singleton().createValue(desiredFontSize, CSSPrimitiveValue::CSS_PX), false);
422 setNodeAttribute(*element, styleAttr, inlineStyle->asText());
423 }
424 if (inlineStyle->isEmpty()) {
425 removeNodeAttribute(*element, styleAttr);
426 if (isSpanWithoutAttributesOrUnstyledStyleSpan(*element))
427 unstyledSpans.append(element.releaseNonNull());
428 }
429 }
430
431 for (auto& unstyledSpan : unstyledSpans)
432 removeNodePreservingChildren(unstyledSpan);
433}
434
435static ContainerNode* dummySpanAncestorForNode(const Node* node)
436{
437 while (node && (!is<Element>(*node) || !isStyleSpanOrSpanWithOnlyStyleAttribute(downcast<Element>(*node))))
438 node = node->parentNode();
439
440 return node ? node->parentNode() : nullptr;
441}
442
443void ApplyStyleCommand::cleanupUnstyledAppleStyleSpans(ContainerNode* dummySpanAncestor)
444{
445 if (!dummySpanAncestor)
446 return;
447
448 // Dummy spans are created when text node is split, so that style information
449 // can be propagated, which can result in more splitting. If a dummy span gets
450 // cloned/split, the new node is always a sibling of it. Therefore, we scan
451 // all the children of the dummy's parent
452
453 Vector<Element*> toRemove;
454 for (auto& child : childrenOfType<Element>(*dummySpanAncestor)) {
455 if (isSpanWithoutAttributesOrUnstyledStyleSpan(child))
456 toRemove.append(&child);
457 }
458
459 for (auto& element : toRemove)
460 removeNodePreservingChildren(*element);
461}
462
463HTMLElement* ApplyStyleCommand::splitAncestorsWithUnicodeBidi(Node* node, bool before, WritingDirection allowedDirection)
464{
465 // We are allowed to leave the highest ancestor with unicode-bidi unsplit if it is unicode-bidi: embed and direction: allowedDirection.
466 // In that case, we return the unsplit ancestor. Otherwise, we return 0.
467 Element* block = enclosingBlock(node);
468 if (!block || block == node)
469 return 0;
470
471 Node* highestAncestorWithUnicodeBidi = nullptr;
472 Node* nextHighestAncestorWithUnicodeBidi = nullptr;
473 int highestAncestorUnicodeBidi = 0;
474 for (Node* n = node->parentNode(); n != block; n = n->parentNode()) {
475 int unicodeBidi = toIdentifier(ComputedStyleExtractor(n).propertyValue(CSSPropertyUnicodeBidi));
476 if (unicodeBidi && unicodeBidi != CSSValueNormal) {
477 highestAncestorUnicodeBidi = unicodeBidi;
478 nextHighestAncestorWithUnicodeBidi = highestAncestorWithUnicodeBidi;
479 highestAncestorWithUnicodeBidi = n;
480 }
481 }
482
483 if (!highestAncestorWithUnicodeBidi)
484 return 0;
485
486 HTMLElement* unsplitAncestor = nullptr;
487
488 if (allowedDirection != WritingDirection::Natural && highestAncestorUnicodeBidi != CSSValueBidiOverride && is<HTMLElement>(*highestAncestorWithUnicodeBidi)) {
489 auto highestAncestorDirection = EditingStyle::create(highestAncestorWithUnicodeBidi, EditingStyle::AllProperties)->textDirection();
490 if (highestAncestorDirection && *highestAncestorDirection == allowedDirection) {
491 if (!nextHighestAncestorWithUnicodeBidi)
492 return downcast<HTMLElement>(highestAncestorWithUnicodeBidi);
493
494 unsplitAncestor = downcast<HTMLElement>(highestAncestorWithUnicodeBidi);
495 highestAncestorWithUnicodeBidi = nextHighestAncestorWithUnicodeBidi;
496 }
497 }
498
499 // Split every ancestor through highest ancestor with embedding.
500 RefPtr<Node> currentNode = node;
501 while (currentNode) {
502 RefPtr<Element> parent = downcast<Element>(currentNode->parentNode());
503 if (before ? currentNode->previousSibling() : currentNode->nextSibling())
504 splitElement(*parent, before ? *currentNode : *currentNode->nextSibling());
505 if (parent == highestAncestorWithUnicodeBidi)
506 break;
507 currentNode = parent;
508 }
509 return unsplitAncestor;
510}
511
512void ApplyStyleCommand::removeEmbeddingUpToEnclosingBlock(Node* node, Node* unsplitAncestor)
513{
514 Element* block = enclosingBlock(node);
515 if (!block || block == node)
516 return;
517
518 Node* parent = nullptr;
519 for (Node* ancestor = node->parentNode(); ancestor != block && ancestor != unsplitAncestor; ancestor = parent) {
520 parent = ancestor->parentNode();
521 if (!is<StyledElement>(*ancestor))
522 continue;
523
524 StyledElement& element = downcast<StyledElement>(*ancestor);
525 int unicodeBidi = toIdentifier(ComputedStyleExtractor(&element).propertyValue(CSSPropertyUnicodeBidi));
526 if (!unicodeBidi || unicodeBidi == CSSValueNormal)
527 continue;
528
529 // FIXME: This code should really consider the mapped attribute 'dir', the inline style declaration,
530 // and all matching style rules in order to determine how to best set the unicode-bidi property to 'normal'.
531 // For now, it assumes that if the 'dir' attribute is present, then removing it will suffice, and
532 // otherwise it sets the property in the inline style declaration.
533 if (element.hasAttributeWithoutSynchronization(dirAttr)) {
534 // FIXME: If this is a BDO element, we should probably just remove it if it has no
535 // other attributes, like we (should) do with B and I elements.
536 removeNodeAttribute(element, dirAttr);
537 } else {
538 RefPtr<MutableStyleProperties> inlineStyle = copyStyleOrCreateEmpty(element.inlineStyle());
539 inlineStyle->setProperty(CSSPropertyUnicodeBidi, CSSValueNormal);
540 inlineStyle->removeProperty(CSSPropertyDirection);
541 setNodeAttribute(element, styleAttr, inlineStyle->asText());
542 if (isSpanWithoutAttributesOrUnstyledStyleSpan(element))
543 removeNodePreservingChildren(element);
544 }
545 }
546}
547
548static Node* highestEmbeddingAncestor(Node* startNode, Node* enclosingNode)
549{
550 for (Node* n = startNode; n && n != enclosingNode; n = n->parentNode()) {
551 if (n->isHTMLElement() && toIdentifier(ComputedStyleExtractor(n).propertyValue(CSSPropertyUnicodeBidi)) == CSSValueEmbed)
552 return n;
553 }
554
555 return 0;
556}
557
558void ApplyStyleCommand::applyInlineStyle(EditingStyle& style)
559{
560 RefPtr<ContainerNode> startDummySpanAncestor;
561 RefPtr<ContainerNode> endDummySpanAncestor;
562
563 // update document layout once before removing styles
564 // so that we avoid the expense of updating before each and every call
565 // to check a computed style
566 document().updateLayoutIgnorePendingStylesheets();
567
568 // adjust to the positions we want to use for applying style
569 Position start = startPosition();
570 Position end = endPosition();
571
572 if (start.isNull() || end.isNull())
573 return;
574
575 if (comparePositions(end, start) < 0) {
576 Position swap = start;
577 start = end;
578 end = swap;
579 }
580
581 // split the start node and containing element if the selection starts inside of it
582 bool splitStart = isValidCaretPositionInTextNode(start);
583 if (splitStart) {
584 if (shouldSplitTextElement(start.deprecatedNode()->parentElement(), style))
585 splitTextElementAtStart(start, end);
586 else
587 splitTextAtStart(start, end);
588 start = startPosition();
589 end = endPosition();
590 startDummySpanAncestor = dummySpanAncestorForNode(start.deprecatedNode());
591 }
592
593 if (start.isNull() || end.isNull())
594 return;
595
596 // split the end node and containing element if the selection ends inside of it
597 bool splitEnd = isValidCaretPositionInTextNode(end);
598 if (splitEnd) {
599 if (shouldSplitTextElement(end.deprecatedNode()->parentElement(), style))
600 splitTextElementAtEnd(start, end);
601 else
602 splitTextAtEnd(start, end);
603 start = startPosition();
604 end = endPosition();
605 endDummySpanAncestor = dummySpanAncestorForNode(end.deprecatedNode());
606 }
607
608 if (start.isNull() || end.isNull())
609 return;
610
611 // Remove style from the selection.
612 // Use the upstream position of the start for removing style.
613 // This will ensure we remove all traces of the relevant styles from the selection
614 // and prevent us from adding redundant ones, as described in:
615 // <rdar://problem/3724344> Bolding and unbolding creates extraneous tags
616 Position removeStart = start.upstream();
617 auto textDirection = style.textDirection();
618 RefPtr<EditingStyle> styleWithoutEmbedding;
619 RefPtr<EditingStyle> embeddingStyle;
620 if (textDirection.hasValue()) {
621 // Leave alone an ancestor that provides the desired single level embedding, if there is one.
622 auto* startUnsplitAncestor = splitAncestorsWithUnicodeBidi(start.deprecatedNode(), true, *textDirection);
623 auto* endUnsplitAncestor = splitAncestorsWithUnicodeBidi(end.deprecatedNode(), false, *textDirection);
624 removeEmbeddingUpToEnclosingBlock(start.deprecatedNode(), startUnsplitAncestor);
625 removeEmbeddingUpToEnclosingBlock(end.deprecatedNode(), endUnsplitAncestor);
626
627 // Avoid removing the dir attribute and the unicode-bidi and direction properties from the unsplit ancestors.
628 Position embeddingRemoveStart = removeStart;
629 if (startUnsplitAncestor && nodeFullySelected(*startUnsplitAncestor, removeStart, end))
630 embeddingRemoveStart = positionInParentAfterNode(startUnsplitAncestor);
631
632 Position embeddingRemoveEnd = end;
633 if (endUnsplitAncestor && nodeFullySelected(*endUnsplitAncestor, removeStart, end))
634 embeddingRemoveEnd = positionInParentBeforeNode(endUnsplitAncestor).downstream();
635
636 if (embeddingRemoveEnd != removeStart || embeddingRemoveEnd != end) {
637 styleWithoutEmbedding = style.copy();
638 embeddingStyle = styleWithoutEmbedding->extractAndRemoveTextDirection();
639
640 if (comparePositions(embeddingRemoveStart, embeddingRemoveEnd) <= 0)
641 removeInlineStyle(*embeddingStyle, embeddingRemoveStart, embeddingRemoveEnd);
642 }
643 }
644
645 removeInlineStyle(styleWithoutEmbedding ? *styleWithoutEmbedding : style, removeStart, end);
646 start = startPosition();
647 end = endPosition();
648 if (start.isNull() || start.isOrphan() || end.isNull() || end.isOrphan())
649 return;
650
651 if (splitStart && mergeStartWithPreviousIfIdentical(start, end)) {
652 start = startPosition();
653 end = endPosition();
654 }
655
656 if (splitEnd) {
657 mergeEndWithNextIfIdentical(start, end);
658 start = startPosition();
659 end = endPosition();
660 }
661
662 if (start.isNull() || end.isNull())
663 return;
664
665 // update document layout once before running the rest of the function
666 // so that we avoid the expense of updating before each and every call
667 // to check a computed style
668 document().updateLayoutIgnorePendingStylesheets();
669
670 RefPtr<EditingStyle> styleToApply = &style;
671 if (textDirection.hasValue()) {
672 // Avoid applying the unicode-bidi and direction properties beneath ancestors that already have them.
673 Node* embeddingStartNode = highestEmbeddingAncestor(start.deprecatedNode(), enclosingBlock(start.deprecatedNode()));
674 Node* embeddingEndNode = highestEmbeddingAncestor(end.deprecatedNode(), enclosingBlock(end.deprecatedNode()));
675
676 if (embeddingStartNode || embeddingEndNode) {
677 Position embeddingApplyStart = embeddingStartNode ? positionInParentAfterNode(embeddingStartNode) : start;
678 Position embeddingApplyEnd = embeddingEndNode ? positionInParentBeforeNode(embeddingEndNode) : end;
679 ASSERT(embeddingApplyStart.isNotNull() && embeddingApplyEnd.isNotNull());
680
681 if (!embeddingStyle) {
682 styleWithoutEmbedding = style.copy();
683 embeddingStyle = styleWithoutEmbedding->extractAndRemoveTextDirection();
684 }
685 fixRangeAndApplyInlineStyle(*embeddingStyle, embeddingApplyStart, embeddingApplyEnd);
686
687 styleToApply = styleWithoutEmbedding;
688 }
689 }
690
691 fixRangeAndApplyInlineStyle(*styleToApply, start, end);
692
693 // Remove dummy style spans created by splitting text elements.
694 cleanupUnstyledAppleStyleSpans(startDummySpanAncestor.get());
695 if (endDummySpanAncestor != startDummySpanAncestor)
696 cleanupUnstyledAppleStyleSpans(endDummySpanAncestor.get());
697}
698
699void ApplyStyleCommand::fixRangeAndApplyInlineStyle(EditingStyle& style, const Position& start, const Position& end)
700{
701 Node* startNode = start.deprecatedNode();
702
703 if (start.deprecatedEditingOffset() >= caretMaxOffset(*startNode)) {
704 startNode = NodeTraversal::next(*startNode);
705 if (!startNode || comparePositions(end, firstPositionInOrBeforeNode(startNode)) < 0)
706 return;
707 }
708
709 Node* pastEndNode = end.deprecatedNode();
710 if (end.deprecatedEditingOffset() >= caretMaxOffset(*pastEndNode))
711 pastEndNode = NodeTraversal::nextSkippingChildren(*pastEndNode);
712
713 // FIXME: Callers should perform this operation on a Range that includes the br
714 // if they want style applied to the empty line.
715 // FIXME: Should this be using startNode instead of start.deprecatedNode()?
716 if (start == end && start.deprecatedNode()->hasTagName(brTag))
717 pastEndNode = NodeTraversal::next(*start.deprecatedNode());
718
719 // Start from the highest fully selected ancestor so that we can modify the fully selected node.
720 // e.g. When applying font-size: large on <font color="blue">hello</font>, we need to include the font element in our run
721 // to generate <font color="blue" size="4">hello</font> instead of <font color="blue"><font size="4">hello</font></font>
722 auto range = Range::create(startNode->document(), start, end);
723 auto* editableRoot = startNode->rootEditableElement();
724 if (startNode != editableRoot) {
725 while (editableRoot && startNode->parentNode() != editableRoot && isNodeVisiblyContainedWithin(*startNode->parentNode(), range))
726 startNode = startNode->parentNode();
727 }
728
729 applyInlineStyleToNodeRange(style, *startNode, pastEndNode);
730}
731
732static bool containsNonEditableRegion(Node& node)
733{
734 if (!node.hasEditableStyle())
735 return true;
736
737 Node* sibling = NodeTraversal::nextSkippingChildren(node);
738 for (Node* descendant = node.firstChild(); descendant && descendant != sibling; descendant = NodeTraversal::next(*descendant)) {
739 if (!descendant->hasEditableStyle())
740 return true;
741 }
742
743 return false;
744}
745
746struct InlineRunToApplyStyle {
747 InlineRunToApplyStyle(Node* start, Node* end, Node* pastEndNode)
748 : start(start)
749 , end(end)
750 , pastEndNode(pastEndNode)
751 {
752 ASSERT(start->parentNode() == end->parentNode());
753 }
754
755 bool startAndEndAreStillInDocument()
756 {
757 return start && end && start->isConnected() && end->isConnected();
758 }
759
760 RefPtr<Node> start;
761 RefPtr<Node> end;
762 RefPtr<Node> pastEndNode;
763 Position positionForStyleComputation;
764 RefPtr<Node> dummyElement;
765 StyleChange change;
766};
767
768void ApplyStyleCommand::applyInlineStyleToNodeRange(EditingStyle& style, Node& startNode, Node* pastEndNode)
769{
770 if (m_removeOnly)
771 return;
772
773 document().updateLayoutIgnorePendingStylesheets();
774
775 Vector<InlineRunToApplyStyle> runs;
776 RefPtr<Node> node = &startNode;
777 for (RefPtr<Node> next; node && node != pastEndNode; node = next) {
778 next = NodeTraversal::next(*node);
779
780 if (!node->renderer() || !node->hasEditableStyle())
781 continue;
782
783 if (!node->hasRichlyEditableStyle() && is<HTMLElement>(*node)) {
784 // This is a plaintext-only region. Only proceed if it's fully selected.
785 // pastEndNode is the node after the last fully selected node, so if it's inside node then
786 // node isn't fully selected.
787 if (pastEndNode && pastEndNode->isDescendantOf(*node))
788 break;
789 // Add to this element's inline style and skip over its contents.
790 HTMLElement& element = downcast<HTMLElement>(*node);
791 RefPtr<MutableStyleProperties> inlineStyle = copyStyleOrCreateEmpty(element.inlineStyle());
792 if (MutableStyleProperties* otherStyle = style.style())
793 inlineStyle->mergeAndOverrideOnConflict(*otherStyle);
794 setNodeAttribute(element, styleAttr, inlineStyle->asText());
795 next = NodeTraversal::nextSkippingChildren(*node);
796 continue;
797 }
798
799 if (isBlock(node.get()))
800 continue;
801
802 if (node->hasChildNodes()) {
803 if (node->contains(pastEndNode) || containsNonEditableRegion(*node) || !node->parentNode()->hasEditableStyle())
804 continue;
805 if (editingIgnoresContent(*node)) {
806 next = NodeTraversal::nextSkippingChildren(*node);
807 continue;
808 }
809 }
810
811 Node* runStart = node.get();
812 Node* runEnd = node.get();
813 Node* sibling = node->nextSibling();
814 while (sibling && sibling != pastEndNode && !sibling->contains(pastEndNode) && (!isBlock(sibling) || sibling->hasTagName(brTag)) && !containsNonEditableRegion(*sibling)) {
815 runEnd = sibling;
816 sibling = runEnd->nextSibling();
817 }
818 next = NodeTraversal::nextSkippingChildren(*runEnd);
819
820 Node* pastEndNode = NodeTraversal::nextSkippingChildren(*runEnd);
821 if (!shouldApplyInlineStyleToRun(style, runStart, pastEndNode))
822 continue;
823
824 runs.append(InlineRunToApplyStyle(runStart, runEnd, pastEndNode));
825 }
826
827 for (auto& run : runs) {
828 removeConflictingInlineStyleFromRun(style, run.start, run.end, run.pastEndNode.get());
829 if (run.startAndEndAreStillInDocument())
830 run.positionForStyleComputation = positionToComputeInlineStyleChange(*run.start, run.dummyElement);
831 }
832
833 document().updateLayoutIgnorePendingStylesheets();
834
835 for (auto& run : runs)
836 run.change = StyleChange(&style, run.positionForStyleComputation);
837
838 for (auto& run : runs) {
839 if (run.dummyElement)
840 removeNode(*run.dummyElement);
841 if (run.startAndEndAreStillInDocument())
842 applyInlineStyleChange(run.start.releaseNonNull(), run.end.releaseNonNull(), run.change, AddStyledElement);
843 }
844}
845
846bool ApplyStyleCommand::isStyledInlineElementToRemove(Element* element) const
847{
848 return (m_styledInlineElement && element->hasTagName(m_styledInlineElement->tagQName()))
849 || (m_isInlineElementToRemoveFunction && m_isInlineElementToRemoveFunction(element));
850}
851
852bool ApplyStyleCommand::shouldApplyInlineStyleToRun(EditingStyle& style, Node* runStart, Node* pastEndNode)
853{
854 ASSERT(runStart);
855
856 for (Node* node = runStart; node && node != pastEndNode; node = NodeTraversal::next(*node)) {
857 if (node->hasChildNodes())
858 continue;
859 // We don't consider m_isInlineElementToRemoveFunction here because we never apply style when m_isInlineElementToRemoveFunction is specified
860 if (!style.styleIsPresentInComputedStyleOfNode(*node))
861 return true;
862 if (m_styledInlineElement && !enclosingElementWithTag(positionBeforeNode(node), m_styledInlineElement->tagQName()))
863 return true;
864 }
865 return false;
866}
867
868void ApplyStyleCommand::removeConflictingInlineStyleFromRun(EditingStyle& style, RefPtr<Node>& runStart, RefPtr<Node>& runEnd, Node* pastEndNode)
869{
870 ASSERT(runStart && runEnd);
871 RefPtr<Node> next = runStart;
872 for (RefPtr<Node> node = next; node && node->isConnected() && node != pastEndNode; node = next) {
873 if (editingIgnoresContent(*node)) {
874 ASSERT(!node->contains(pastEndNode));
875 next = NodeTraversal::nextSkippingChildren(*node);
876 } else
877 next = NodeTraversal::next(*node);
878
879 if (!is<HTMLElement>(*node))
880 continue;
881
882 RefPtr<Node> previousSibling = node->previousSibling();
883 RefPtr<Node> nextSibling = node->nextSibling();
884 RefPtr<ContainerNode> parent = node->parentNode();
885 removeInlineStyleFromElement(style, downcast<HTMLElement>(*node), RemoveAlways);
886 if (!node->isConnected()) {
887 // FIXME: We might need to update the start and the end of current selection here but need a test.
888 if (runStart == node)
889 runStart = previousSibling ? previousSibling->nextSibling() : parent->firstChild();
890 if (runEnd == node)
891 runEnd = nextSibling ? nextSibling->previousSibling() : parent->lastChild();
892 }
893 }
894}
895
896bool ApplyStyleCommand::removeInlineStyleFromElement(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle)
897{
898 if (!element.parentNode() || !isEditableNode(*element.parentNode()))
899 return false;
900
901 if (isStyledInlineElementToRemove(&element)) {
902 if (mode == RemoveNone)
903 return true;
904 if (extractedStyle)
905 extractedStyle->mergeInlineStyleOfElement(element, EditingStyle::OverrideValues);
906 removeNodePreservingChildren(element);
907 return true;
908 }
909
910 bool removed = false;
911 if (removeImplicitlyStyledElement(style, element, mode, extractedStyle))
912 removed = true;
913
914 if (!element.isConnected())
915 return removed;
916
917 // If the node was converted to a span, the span may still contain relevant
918 // styles which must be removed (e.g. <b style='font-weight: bold'>)
919 if (removeCSSStyle(style, element, mode, extractedStyle))
920 removed = true;
921
922 return removed;
923}
924
925void ApplyStyleCommand::replaceWithSpanOrRemoveIfWithoutAttributes(HTMLElement& element)
926{
927 if (hasNoAttributeOrOnlyStyleAttribute(element, StyleAttributeShouldBeEmpty))
928 removeNodePreservingChildren(element);
929 else {
930 HTMLElement* newSpanElement = replaceElementWithSpanPreservingChildrenAndAttributes(element);
931 ASSERT_UNUSED(newSpanElement, newSpanElement && newSpanElement->isConnected());
932 }
933}
934
935bool ApplyStyleCommand::removeImplicitlyStyledElement(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle)
936{
937 if (mode == RemoveNone) {
938 ASSERT(!extractedStyle);
939 return style.conflictsWithImplicitStyleOfElement(element) || style.conflictsWithImplicitStyleOfAttributes(element);
940 }
941
942 ASSERT(mode == RemoveIfNeeded || mode == RemoveAlways);
943 if (style.conflictsWithImplicitStyleOfElement(element, extractedStyle, mode == RemoveAlways ? EditingStyle::ExtractMatchingStyle : EditingStyle::DoNotExtractMatchingStyle)) {
944 replaceWithSpanOrRemoveIfWithoutAttributes(element);
945 return true;
946 }
947
948 // unicode-bidi and direction are pushed down separately so don't push down with other styles
949 Vector<QualifiedName> attributes;
950 if (!style.extractConflictingImplicitStyleOfAttributes(element, extractedStyle ? EditingStyle::PreserveWritingDirection : EditingStyle::DoNotPreserveWritingDirection, extractedStyle, attributes, mode == RemoveAlways ? EditingStyle::ExtractMatchingStyle : EditingStyle::DoNotExtractMatchingStyle))
951 return false;
952
953 for (auto& attribute : attributes)
954 removeNodeAttribute(element, attribute);
955
956 if (isEmptyFontTag(&element) || isSpanWithoutAttributesOrUnstyledStyleSpan(element))
957 removeNodePreservingChildren(element);
958
959 return true;
960}
961
962bool ApplyStyleCommand::removeCSSStyle(EditingStyle& style, HTMLElement& element, InlineStyleRemovalMode mode, EditingStyle* extractedStyle)
963{
964 if (mode == RemoveNone)
965 return style.conflictsWithInlineStyleOfElement(element);
966
967 RefPtr<MutableStyleProperties> newInlineStyle;
968 if (!style.conflictsWithInlineStyleOfElement(element, newInlineStyle, extractedStyle))
969 return false;
970
971 if (newInlineStyle->isEmpty())
972 removeNodeAttribute(element, styleAttr);
973 else
974 setNodeAttribute(element, styleAttr, newInlineStyle->asText());
975
976 if (isSpanWithoutAttributesOrUnstyledStyleSpan(element))
977 removeNodePreservingChildren(element);
978
979 return true;
980}
981
982HTMLElement* ApplyStyleCommand::highestAncestorWithConflictingInlineStyle(EditingStyle& style, Node* node)
983{
984 if (!node)
985 return nullptr;
986
987 HTMLElement* result = nullptr;
988 Node* unsplittableElement = unsplittableElementForPosition(firstPositionInOrBeforeNode(node));
989
990 for (Node* ancestor = node; ancestor; ancestor = ancestor->parentNode()) {
991 if (is<HTMLElement>(*ancestor) && shouldRemoveInlineStyleFromElement(style, downcast<HTMLElement>(*ancestor)))
992 result = downcast<HTMLElement>(ancestor);
993 // Should stop at the editable root (cannot cross editing boundary) and
994 // also stop at the unsplittable element to be consistent with other UAs
995 if (ancestor == unsplittableElement)
996 break;
997 }
998
999 return result;
1000}
1001
1002void ApplyStyleCommand::applyInlineStyleToPushDown(Node& node, EditingStyle* style)
1003{
1004 node.document().updateStyleIfNeeded();
1005
1006 if (!style || style->isEmpty() || !node.renderer() || is<HTMLIFrameElement>(node))
1007 return;
1008
1009 RefPtr<EditingStyle> newInlineStyle = style;
1010 if (is<HTMLElement>(node) && downcast<HTMLElement>(node).inlineStyle()) {
1011 newInlineStyle = style->copy();
1012 newInlineStyle->mergeInlineStyleOfElement(downcast<HTMLElement>(node), EditingStyle::OverrideValues);
1013 }
1014
1015 // Since addInlineStyleIfNeeded can't add styles to block-flow render objects, add style attribute instead.
1016 // FIXME: applyInlineStyleToRange should be used here instead.
1017 if ((node.renderer()->isRenderBlockFlow() || node.hasChildNodes()) && is<HTMLElement>(node)) {
1018 setNodeAttribute(downcast<HTMLElement>(node), styleAttr, newInlineStyle->style()->asText());
1019 return;
1020 }
1021
1022 if (node.renderer()->isText() && static_cast<RenderText*>(node.renderer())->isAllCollapsibleWhitespace())
1023 return;
1024 if (node.renderer()->isBR() && !node.renderer()->style().preserveNewline())
1025 return;
1026
1027 // We can't wrap node with the styled element here because new styled element will never be removed if we did.
1028 // If we modified the child pointer in pushDownInlineStyleAroundNode to point to new style element
1029 // then we fall into an infinite loop where we keep removing and adding styled element wrapping node.
1030 addInlineStyleIfNeeded(newInlineStyle.get(), node, node, DoNotAddStyledElement);
1031}
1032
1033void ApplyStyleCommand::pushDownInlineStyleAroundNode(EditingStyle& style, Node* targetNode)
1034{
1035 HTMLElement* highestAncestor = highestAncestorWithConflictingInlineStyle(style, targetNode);
1036 if (!highestAncestor)
1037 return;
1038
1039 // The outer loop is traversing the tree vertically from highestAncestor to targetNode
1040 RefPtr<Node> current = highestAncestor;
1041 // Along the way, styled elements that contain targetNode are removed and accumulated into elementsToPushDown.
1042 // Each child of the removed element, exclusing ancestors of targetNode, is then wrapped by clones of elements in elementsToPushDown.
1043 Vector<Ref<Element>> elementsToPushDown;
1044 while (current && current != targetNode && current->contains(targetNode)) {
1045 auto currentChildren = collectChildNodes(*current);
1046
1047 RefPtr<StyledElement> styledElement;
1048 if (is<StyledElement>(*current) && isStyledInlineElementToRemove(downcast<Element>(current.get()))) {
1049 styledElement = downcast<StyledElement>(current.get());
1050 elementsToPushDown.append(*styledElement);
1051 }
1052
1053 auto styleToPushDown = EditingStyle::create();
1054 if (is<HTMLElement>(*current))
1055 removeInlineStyleFromElement(style, downcast<HTMLElement>(*current), RemoveIfNeeded, styleToPushDown.ptr());
1056
1057 // The inner loop will go through children on each level
1058 // FIXME: we should aggregate inline child elements together so that we don't wrap each child separately.
1059 for (Ref<Node>& childRef : currentChildren) {
1060 Node& child = childRef;
1061 if (!child.parentNode())
1062 continue;
1063 if (!child.contains(targetNode) && elementsToPushDown.size()) {
1064 for (auto& element : elementsToPushDown) {
1065 auto wrapper = element->cloneElementWithoutChildren(document());
1066 wrapper->removeAttribute(styleAttr);
1067 surroundNodeRangeWithElement(child, child, WTFMove(wrapper));
1068 }
1069 }
1070
1071 // Apply style to all nodes containing targetNode and their siblings but NOT to targetNode
1072 // But if we've removed styledElement then always apply the style.
1073 if (&child != targetNode || styledElement)
1074 applyInlineStyleToPushDown(child, styleToPushDown.ptr());
1075
1076 // We found the next node for the outer loop (contains targetNode)
1077 // When reached targetNode, stop the outer loop upon the completion of the current inner loop
1078 if (&child == targetNode || child.contains(targetNode))
1079 current = &child;
1080 }
1081 }
1082}
1083
1084void ApplyStyleCommand::removeInlineStyle(EditingStyle& style, const Position& start, const Position& end)
1085{
1086 ASSERT(start.isNotNull());
1087 ASSERT(end.isNotNull());
1088 ASSERT(start.anchorNode()->isConnected());
1089 ASSERT(end.anchorNode()->isConnected());
1090 ASSERT(comparePositions(start, end) <= 0);
1091 // FIXME: We should assert that start/end are not in the middle of a text node.
1092
1093 Position pushDownStart = start.downstream();
1094 // If the pushDownStart is at the end of a text node, then this node is not fully selected.
1095 // Move it to the next deep quivalent position to avoid removing the style from this node.
1096 // e.g. if pushDownStart was at Position("hello", 5) in <b>hello<div>world</div></b>, we want Position("world", 0) instead.
1097 auto* pushDownStartContainer = pushDownStart.containerNode();
1098 if (is<Text>(pushDownStartContainer) && pushDownStart.computeOffsetInContainerNode() == pushDownStartContainer->maxCharacterOffset())
1099 pushDownStart = nextVisuallyDistinctCandidate(pushDownStart);
1100 // If pushDownEnd is at the start of a text node, then this node is not fully selected.
1101 // Move it to the previous deep equivalent position to avoid removing the style from this node.
1102 Position pushDownEnd = end.upstream();
1103 auto* pushDownEndContainer = pushDownEnd.containerNode();
1104 if (is<Text>(pushDownEndContainer) && !pushDownEnd.computeOffsetInContainerNode())
1105 pushDownEnd = previousVisuallyDistinctCandidate(pushDownEnd);
1106
1107 pushDownInlineStyleAroundNode(style, pushDownStart.deprecatedNode());
1108 pushDownInlineStyleAroundNode(style, pushDownEnd.deprecatedNode());
1109
1110 // The s and e variables store the positions used to set the ending selection after style removal
1111 // takes place. This will help callers to recognize when either the start node or the end node
1112 // are removed from the document during the work of this function.
1113 // If pushDownInlineStyleAroundNode has pruned start.deprecatedNode() or end.deprecatedNode(),
1114 // use pushDownStart or pushDownEnd instead, which pushDownInlineStyleAroundNode won't prune.
1115 Position s = start.isNull() || start.isOrphan() ? pushDownStart : start;
1116 Position e = end.isNull() || end.isOrphan() ? pushDownEnd : end;
1117
1118 RefPtr<Node> node = start.deprecatedNode();
1119 while (node) {
1120 RefPtr<Node> next;
1121 if (editingIgnoresContent(*node)) {
1122 ASSERT(node == end.deprecatedNode() || !node->contains(end.deprecatedNode()));
1123 next = NodeTraversal::nextSkippingChildren(*node);
1124 } else
1125 next = NodeTraversal::next(*node);
1126
1127 if (is<HTMLElement>(*node) && nodeFullySelected(downcast<HTMLElement>(*node), start, end)) {
1128 Ref<HTMLElement> element = downcast<HTMLElement>(*node);
1129 RefPtr<Node> prev = NodeTraversal::previousPostOrder(element);
1130 RefPtr<Node> next = NodeTraversal::next(element);
1131 RefPtr<EditingStyle> styleToPushDown;
1132 RefPtr<Node> childNode;
1133 if (isStyledInlineElementToRemove(element.ptr())) {
1134 styleToPushDown = EditingStyle::create();
1135 childNode = element->firstChild();
1136 }
1137
1138 removeInlineStyleFromElement(style, element, RemoveIfNeeded, styleToPushDown.get());
1139 if (!element->isConnected()) {
1140 if (s.deprecatedNode() == element.ptr()) {
1141 // Since elem must have been fully selected, and it is at the start
1142 // of the selection, it is clear we can set the new s offset to 0.
1143 ASSERT(s.anchorType() == Position::PositionIsBeforeAnchor || s.offsetInContainerNode() <= 0);
1144 s = firstPositionInOrBeforeNode(next.get());
1145 }
1146 if (e.deprecatedNode() == element.ptr()) {
1147 // Since elem must have been fully selected, and it is at the end
1148 // of the selection, it is clear we can set the new e offset to
1149 // the max range offset of prev.
1150 ASSERT(s.anchorType() == Position::PositionIsAfterAnchor || !offsetIsBeforeLastNodeOffset(s.offsetInContainerNode(), s.containerNode()));
1151 e = lastPositionInOrAfterNode(prev.get());
1152 }
1153 }
1154
1155 if (styleToPushDown) {
1156 for (; childNode; childNode = childNode->nextSibling())
1157 applyInlineStyleToPushDown(*childNode, styleToPushDown.get());
1158 }
1159 }
1160 if (node == end.deprecatedNode())
1161 break;
1162 node = next.get();
1163 }
1164
1165 updateStartEnd(s, e);
1166}
1167
1168bool ApplyStyleCommand::nodeFullySelected(Element& element, const Position& start, const Position& end) const
1169{
1170 // The tree may have changed and Position::upstream() relies on an up-to-date layout.
1171 element.document().updateLayoutIgnorePendingStylesheets();
1172
1173 return comparePositions(firstPositionInOrBeforeNode(&element), start) >= 0
1174 && comparePositions(lastPositionInOrAfterNode(&element).upstream(), end) <= 0;
1175}
1176
1177bool ApplyStyleCommand::nodeFullyUnselected(Element& element, const Position& start, const Position& end) const
1178{
1179 // The tree may have changed and Position::upstream() relies on an up-to-date layout.
1180 element.document().updateLayoutIgnorePendingStylesheets();
1181
1182 return comparePositions(lastPositionInOrAfterNode(&element).upstream(), start) < 0
1183 || comparePositions(firstPositionInOrBeforeNode(&element), end) > 0;
1184}
1185
1186void ApplyStyleCommand::splitTextAtStart(const Position& start, const Position& end)
1187{
1188 ASSERT(is<Text>(start.containerNode()));
1189
1190 Position newEnd;
1191 if (end.anchorType() == Position::PositionIsOffsetInAnchor && start.containerNode() == end.containerNode())
1192 newEnd = Position(end.containerText(), end.offsetInContainerNode() - start.offsetInContainerNode());
1193 else
1194 newEnd = end;
1195
1196 RefPtr<Text> text = start.containerText();
1197 splitTextNode(*text, start.offsetInContainerNode());
1198 updateStartEnd(firstPositionInNode(text.get()), newEnd);
1199}
1200
1201void ApplyStyleCommand::splitTextAtEnd(const Position& start, const Position& end)
1202{
1203 ASSERT(is<Text>(end.containerNode()));
1204
1205 bool shouldUpdateStart = start.anchorType() == Position::PositionIsOffsetInAnchor && start.containerNode() == end.containerNode();
1206 Text& text = downcast<Text>(*end.deprecatedNode());
1207 splitTextNode(text, end.offsetInContainerNode());
1208
1209 Node* prevNode = text.previousSibling();
1210 if (!is<Text>(prevNode))
1211 return;
1212
1213 Position newStart = shouldUpdateStart ? Position(downcast<Text>(prevNode), start.offsetInContainerNode()) : start;
1214 updateStartEnd(newStart, lastPositionInNode(prevNode));
1215}
1216
1217void ApplyStyleCommand::splitTextElementAtStart(const Position& start, const Position& end)
1218{
1219 ASSERT(is<Text>(start.containerNode()));
1220
1221 Position newEnd;
1222 if (start.containerNode() == end.containerNode())
1223 newEnd = Position(end.containerText(), end.offsetInContainerNode() - start.offsetInContainerNode());
1224 else
1225 newEnd = end;
1226
1227 splitTextNodeContainingElement(*start.containerText(), start.offsetInContainerNode());
1228 updateStartEnd(positionBeforeNode(start.containerNode()), newEnd);
1229}
1230
1231void ApplyStyleCommand::splitTextElementAtEnd(const Position& start, const Position& end)
1232{
1233 ASSERT(is<Text>(end.containerNode()));
1234
1235 bool shouldUpdateStart = start.containerNode() == end.containerNode();
1236 splitTextNodeContainingElement(*end.containerText(), end.offsetInContainerNode());
1237
1238 Node* parentElement = end.containerNode()->parentNode();
1239 if (!parentElement || !parentElement->previousSibling())
1240 return;
1241 Node* firstTextNode = parentElement->previousSibling()->lastChild();
1242 if (!is<Text>(firstTextNode))
1243 return;
1244
1245 Position newStart = shouldUpdateStart ? Position(downcast<Text>(firstTextNode), start.offsetInContainerNode()) : start;
1246 updateStartEnd(newStart, positionAfterNode(firstTextNode));
1247}
1248
1249bool ApplyStyleCommand::shouldSplitTextElement(Element* element, EditingStyle& style)
1250{
1251 if (!is<HTMLElement>(element))
1252 return false;
1253
1254 return shouldRemoveInlineStyleFromElement(style, downcast<HTMLElement>(*element));
1255}
1256
1257bool ApplyStyleCommand::isValidCaretPositionInTextNode(const Position& position)
1258{
1259 Node* node = position.containerNode();
1260 if (position.anchorType() != Position::PositionIsOffsetInAnchor || !is<Text>(node))
1261 return false;
1262 int offsetInText = position.offsetInContainerNode();
1263 return offsetInText > caretMinOffset(*node) && offsetInText < caretMaxOffset(*node);
1264}
1265
1266bool ApplyStyleCommand::mergeStartWithPreviousIfIdentical(const Position& start, const Position& end)
1267{
1268 auto* startNode = start.containerNode();
1269 int startOffset = start.computeOffsetInContainerNode();
1270 if (startOffset)
1271 return false;
1272
1273 if (isAtomicNode(startNode)) {
1274 // note: prior siblings could be unrendered elements. it's silly to miss the
1275 // merge opportunity just for that.
1276 if (startNode->previousSibling())
1277 return false;
1278
1279 startNode = startNode->parentNode();
1280 }
1281
1282 auto* previousSibling = startNode->previousSibling();
1283 if (!previousSibling || !areIdenticalElements(*startNode, *previousSibling))
1284 return false;
1285
1286 auto& previousElement = downcast<Element>(*previousSibling);
1287 auto& element = downcast<Element>(*startNode);
1288 auto* startChild = element.firstChild();
1289 ASSERT(startChild);
1290 mergeIdenticalElements(previousElement, element);
1291
1292 int startOffsetAdjustment = startChild->computeNodeIndex();
1293 int endOffsetAdjustment = startNode == end.deprecatedNode() ? startOffsetAdjustment : 0;
1294 updateStartEnd({ startNode, startOffsetAdjustment, Position::PositionIsOffsetInAnchor},
1295 { end.deprecatedNode(), end.deprecatedEditingOffset() + endOffsetAdjustment, Position::PositionIsOffsetInAnchor });
1296 return true;
1297}
1298
1299bool ApplyStyleCommand::mergeEndWithNextIfIdentical(const Position& start, const Position& end)
1300{
1301 Node* endNode = end.containerNode();
1302
1303 if (isAtomicNode(endNode)) {
1304 int endOffset = end.computeOffsetInContainerNode();
1305 if (offsetIsBeforeLastNodeOffset(endOffset, endNode) || end.deprecatedNode()->nextSibling())
1306 return false;
1307
1308 endNode = end.deprecatedNode()->parentNode();
1309 }
1310
1311 if (endNode->hasTagName(brTag))
1312 return false;
1313
1314 Node* nextSibling = endNode->nextSibling();
1315 if (!nextSibling || !areIdenticalElements(*endNode, *nextSibling))
1316 return false;
1317
1318 auto& nextElement = downcast<Element>(*nextSibling);
1319 auto& element = downcast<Element>(*endNode);
1320 Node* nextChild = nextElement.firstChild();
1321
1322 mergeIdenticalElements(element, nextElement);
1323
1324 bool shouldUpdateStart = start.containerNode() == endNode;
1325 int endOffset = nextChild ? nextChild->computeNodeIndex() : nextElement.countChildNodes();
1326 updateStartEnd(shouldUpdateStart ? Position(&nextElement, start.offsetInContainerNode(), Position::PositionIsOffsetInAnchor) : start,
1327 { &nextElement, endOffset, Position::PositionIsOffsetInAnchor });
1328 return true;
1329}
1330
1331void ApplyStyleCommand::surroundNodeRangeWithElement(Node& startNode, Node& endNode, Ref<Element>&& elementToInsert)
1332{
1333 Ref<Node> protectedStartNode = startNode;
1334 Ref<Element> element = WTFMove(elementToInsert);
1335
1336 insertNodeBefore(element.copyRef(), startNode);
1337
1338 RefPtr<Node> node = &startNode;
1339 while (node) {
1340 RefPtr<Node> next = node->nextSibling();
1341 if (isEditableNode(*node)) {
1342 removeNode(*node);
1343 appendNode(*node, element.copyRef());
1344 }
1345 if (node == &endNode)
1346 break;
1347 node = next;
1348 }
1349
1350 RefPtr<Node> nextSibling = element->nextSibling();
1351 RefPtr<Node> previousSibling = element->previousSibling();
1352
1353 if (nextSibling && nextSibling->hasEditableStyle() && areIdenticalElements(element, *nextSibling))
1354 mergeIdenticalElements(element, downcast<Element>(*nextSibling));
1355
1356 if (is<Element>(previousSibling) && previousSibling->hasEditableStyle()) {
1357 auto* mergedElement = previousSibling->nextSibling();
1358 ASSERT(mergedElement);
1359 if (mergedElement->hasEditableStyle() && areIdenticalElements(*previousSibling, *mergedElement))
1360 mergeIdenticalElements(downcast<Element>(*previousSibling), downcast<Element>(*mergedElement));
1361 }
1362
1363 // FIXME: We should probably call updateStartEnd if the start or end was in the node
1364 // range so that the endingSelection() is canonicalized. See the comments at the end of
1365 // VisibleSelection::validate().
1366}
1367
1368void ApplyStyleCommand::addBlockStyle(const StyleChange& styleChange, HTMLElement& block)
1369{
1370 ASSERT(styleChange.cssStyle());
1371 // Do not check for legacy styles here. Those styles, like <B> and <I>, only apply for
1372 // inline content.
1373
1374 String cssStyle = styleChange.cssStyle()->asText();
1375 StringBuilder cssText;
1376 cssText.append(cssStyle);
1377 if (const StyleProperties* decl = block.inlineStyle()) {
1378 if (!cssStyle.isEmpty())
1379 cssText.append(' ');
1380 cssText.append(decl->asText());
1381 }
1382 setNodeAttribute(block, styleAttr, cssText.toString());
1383}
1384
1385void ApplyStyleCommand::addInlineStyleIfNeeded(EditingStyle* style, Node& start, Node& end, EAddStyledElement addStyledElement)
1386{
1387 if (!start.isConnected() || !end.isConnected())
1388 return;
1389
1390 Ref<Node> protectedStart = start;
1391 RefPtr<Node> dummyElement;
1392 StyleChange styleChange(style, positionToComputeInlineStyleChange(start, dummyElement));
1393
1394 if (dummyElement)
1395 removeNode(*dummyElement);
1396
1397 applyInlineStyleChange(start, end, styleChange, addStyledElement);
1398}
1399
1400Position ApplyStyleCommand::positionToComputeInlineStyleChange(Node& startNode, RefPtr<Node>& dummyElement)
1401{
1402 // It's okay to obtain the style at the startNode because we've removed all relevant styles from the current run.
1403 if (!is<Element>(startNode)) {
1404 dummyElement = createStyleSpanElement(document());
1405 insertNodeAt(*dummyElement, positionBeforeNode(&startNode));
1406 return firstPositionInOrBeforeNode(dummyElement.get());
1407 }
1408
1409 return firstPositionInOrBeforeNode(&startNode);
1410}
1411
1412void ApplyStyleCommand::applyInlineStyleChange(Node& passedStart, Node& passedEnd, StyleChange& styleChange, EAddStyledElement addStyledElement)
1413{
1414 RefPtr<Node> startNode = &passedStart;
1415 RefPtr<Node> endNode = &passedEnd;
1416 ASSERT(startNode->isConnected());
1417 ASSERT(endNode->isConnected());
1418
1419 // Find appropriate font and span elements top-down.
1420 HTMLFontElement* fontContainer = nullptr;
1421 HTMLElement* styleContainer = nullptr;
1422 while (startNode == endNode) {
1423 if (is<HTMLElement>(*startNode)) {
1424 auto& container = downcast<HTMLElement>(*startNode);
1425 if (is<HTMLFontElement>(container))
1426 fontContainer = &downcast<HTMLFontElement>(container);
1427 if (is<HTMLSpanElement>(container) || (!is<HTMLSpanElement>(styleContainer) && container.hasChildNodes()))
1428 styleContainer = &container;
1429 }
1430 auto* startNodeFirstChild = startNode->firstChild();
1431 if (!startNodeFirstChild)
1432 break;
1433 endNode = startNode->lastChild();
1434 startNode = startNodeFirstChild;
1435 }
1436
1437 // Font tags need to go outside of CSS so that CSS font sizes override leagcy font sizes.
1438 if (styleChange.applyFontColor() || styleChange.applyFontFace() || styleChange.applyFontSize()) {
1439 if (fontContainer) {
1440 if (styleChange.applyFontColor())
1441 setNodeAttribute(*fontContainer, colorAttr, styleChange.fontColor());
1442 if (styleChange.applyFontFace())
1443 setNodeAttribute(*fontContainer, faceAttr, styleChange.fontFace());
1444 if (styleChange.applyFontSize())
1445 setNodeAttribute(*fontContainer, sizeAttr, styleChange.fontSize());
1446 } else {
1447 auto fontElement = createFontElement(document());
1448 if (styleChange.applyFontColor())
1449 fontElement->setAttributeWithoutSynchronization(colorAttr, styleChange.fontColor());
1450 if (styleChange.applyFontFace())
1451 fontElement->setAttributeWithoutSynchronization(faceAttr, styleChange.fontFace());
1452 if (styleChange.applyFontSize())
1453 fontElement->setAttributeWithoutSynchronization(sizeAttr, styleChange.fontSize());
1454 surroundNodeRangeWithElement(*startNode, *endNode, WTFMove(fontElement));
1455 }
1456 }
1457
1458 if (auto styleToMerge = styleChange.cssStyle()) {
1459 if (styleContainer) {
1460 if (auto existingStyle = styleContainer->inlineStyle()) {
1461 auto inlineStyle = EditingStyle::create(existingStyle);
1462 inlineStyle->overrideWithStyle(*styleToMerge);
1463 setNodeAttribute(*styleContainer, styleAttr, inlineStyle->style()->asText());
1464 } else
1465 setNodeAttribute(*styleContainer, styleAttr, styleToMerge->asText());
1466 } else {
1467 auto styleElement = createStyleSpanElement(document());
1468 styleElement->setAttribute(styleAttr, styleToMerge->asText());
1469 surroundNodeRangeWithElement(*startNode, *endNode, WTFMove(styleElement));
1470 }
1471 }
1472
1473 if (styleChange.applyBold())
1474 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), bTag));
1475
1476 if (styleChange.applyItalic())
1477 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), iTag));
1478
1479 if (styleChange.applyUnderline())
1480 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), uTag));
1481
1482 if (styleChange.applyLineThrough())
1483 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), strikeTag));
1484
1485 if (styleChange.applySubscript())
1486 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), subTag));
1487 else if (styleChange.applySuperscript())
1488 surroundNodeRangeWithElement(*startNode, *endNode, createHTMLElement(document(), supTag));
1489
1490 if (m_styledInlineElement && addStyledElement == AddStyledElement)
1491 surroundNodeRangeWithElement(*startNode, *endNode, m_styledInlineElement->cloneElementWithoutChildren(document()));
1492}
1493
1494float ApplyStyleCommand::computedFontSize(Node* node)
1495{
1496 if (!node)
1497 return 0;
1498
1499 auto value = ComputedStyleExtractor(node).propertyValue(CSSPropertyFontSize);
1500 return downcast<CSSPrimitiveValue>(*value).floatValue(CSSPrimitiveValue::CSS_PX);
1501}
1502
1503void ApplyStyleCommand::joinChildTextNodes(Node* node, const Position& start, const Position& end)
1504{
1505 if (!node)
1506 return;
1507
1508 Position newStart = start;
1509 Position newEnd = end;
1510
1511 Vector<Ref<Text>> textNodes;
1512 for (Text* textNode = TextNodeTraversal::firstChild(*node); textNode; textNode = TextNodeTraversal::nextSibling(*textNode))
1513 textNodes.append(*textNode);
1514
1515 for (auto& childText : textNodes) {
1516 Node* next = childText->nextSibling();
1517 if (!is<Text>(next))
1518 continue;
1519
1520 Text& nextText = downcast<Text>(*next);
1521 if (start.anchorType() == Position::PositionIsOffsetInAnchor && next == start.containerNode())
1522 newStart = Position(childText.ptr(), childText->length() + start.offsetInContainerNode());
1523 if (end.anchorType() == Position::PositionIsOffsetInAnchor && next == end.containerNode())
1524 newEnd = Position(childText.ptr(), childText->length() + end.offsetInContainerNode());
1525 String textToMove = nextText.data();
1526 insertTextIntoNode(childText, childText->length(), textToMove);
1527 removeNode(*next);
1528 // don't move child node pointer. it may want to merge with more text nodes.
1529 }
1530
1531 updateStartEnd(newStart, newEnd);
1532}
1533
1534}
1535