blob: 7fb6484451c1626f294e0ed89ca1154b7f66eea7 [file] [log] [blame]
/*
* 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 COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/editing/commands/apply_style_command.h"
#include "mojo/public/mojom/base/text_direction.mojom-blink.h"
#include "third_party/blink/renderer/core/css/css_computed_style_declaration.h"
#include "third_party/blink/renderer/core/css/css_numeric_literal_value.h"
#include "third_party/blink/renderer/core/css/css_primitive_value.h"
#include "third_party/blink/renderer/core/css/css_property_names.h"
#include "third_party/blink/renderer/core/css/css_property_value_set.h"
#include "third_party/blink/renderer/core/css_value_keywords.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node_list.h"
#include "third_party/blink/renderer/core/dom/node_traversal.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/commands/editing_commands_utilities.h"
#include "third_party/blink/renderer/core/editing/editing_style.h"
#include "third_party/blink/renderer/core/editing/editing_style_utilities.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/iterators/text_iterator.h"
#include "third_party/blink/renderer/core/editing/plain_text_range.h"
#include "third_party/blink/renderer/core/editing/relocatable_position.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/serializers/html_interchange.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_selection.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/html/html_font_element.h"
#include "third_party/blink/renderer/core/html/html_span_element.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_text.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
namespace blink {
static bool HasNoAttributeOrOnlyStyleAttribute(
const HTMLElement* element,
ShouldStyleAttributeBeEmpty should_style_attribute_be_empty) {
AttributeCollection attributes = element->Attributes();
if (attributes.IsEmpty())
return true;
unsigned matched_attributes = 0;
if (element->hasAttribute(html_names::kStyleAttr) &&
(should_style_attribute_be_empty == kAllowNonEmptyStyleAttribute ||
!element->InlineStyle() || element->InlineStyle()->IsEmpty()))
matched_attributes++;
DCHECK_LE(matched_attributes, attributes.size());
return matched_attributes == attributes.size();
}
bool IsStyleSpanOrSpanWithOnlyStyleAttribute(const Element* element) {
if (auto* span = DynamicTo<HTMLSpanElement>(element)) {
return HasNoAttributeOrOnlyStyleAttribute(span,
kAllowNonEmptyStyleAttribute);
}
return false;
}
static inline bool IsSpanWithoutAttributesOrUnstyledStyleSpan(
const Node* node) {
if (auto* span = DynamicTo<HTMLSpanElement>(node)) {
return HasNoAttributeOrOnlyStyleAttribute(span,
kStyleAttributeShouldBeEmpty);
}
return false;
}
bool IsEmptyFontTag(
const Element* element,
ShouldStyleAttributeBeEmpty should_style_attribute_be_empty) {
if (auto* font = DynamicTo<HTMLFontElement>(element)) {
return HasNoAttributeOrOnlyStyleAttribute(font,
should_style_attribute_be_empty);
}
return false;
}
static bool OffsetIsBeforeLastNodeOffset(int offset, Node* anchor_node) {
if (auto* character_data = DynamicTo<CharacterData>(anchor_node))
return offset < static_cast<int>(character_data->length());
int current_offset = 0;
for (Node* node = NodeTraversal::FirstChild(*anchor_node);
node && current_offset < offset;
node = NodeTraversal::NextSibling(*node))
current_offset++;
return offset < current_offset;
}
ApplyStyleCommand::ApplyStyleCommand(Document& document,
const EditingStyle* style,
InputEvent::InputType input_type,
PropertyLevel property_level)
: CompositeEditCommand(document),
style_(style->Copy()),
input_type_(input_type),
property_level_(property_level),
start_(MostForwardCaretPosition(EndingSelection().Start())),
end_(MostBackwardCaretPosition(EndingSelection().End())),
use_ending_selection_(true),
styled_inline_element_(nullptr),
remove_only_(false),
is_inline_element_to_remove_function_(nullptr) {}
ApplyStyleCommand::ApplyStyleCommand(Document& document,
const EditingStyle* style,
const Position& start,
const Position& end)
: CompositeEditCommand(document),
style_(style->Copy()),
input_type_(InputEvent::InputType::kNone),
property_level_(kPropertyDefault),
start_(start),
end_(end),
use_ending_selection_(false),
styled_inline_element_(nullptr),
remove_only_(false),
is_inline_element_to_remove_function_(nullptr) {}
ApplyStyleCommand::ApplyStyleCommand(Element* element, bool remove_only)
: CompositeEditCommand(element->GetDocument()),
style_(MakeGarbageCollected<EditingStyle>()),
input_type_(InputEvent::InputType::kNone),
property_level_(kPropertyDefault),
start_(MostForwardCaretPosition(EndingSelection().Start())),
end_(MostBackwardCaretPosition(EndingSelection().End())),
use_ending_selection_(true),
styled_inline_element_(element),
remove_only_(remove_only),
is_inline_element_to_remove_function_(nullptr) {}
ApplyStyleCommand::ApplyStyleCommand(
Document& document,
const EditingStyle* style,
IsInlineElementToRemoveFunction is_inline_element_to_remove_function,
InputEvent::InputType input_type)
: CompositeEditCommand(document),
style_(style->Copy()),
input_type_(input_type),
property_level_(kPropertyDefault),
start_(MostForwardCaretPosition(EndingSelection().Start())),
end_(MostBackwardCaretPosition(EndingSelection().End())),
use_ending_selection_(true),
styled_inline_element_(nullptr),
remove_only_(true),
is_inline_element_to_remove_function_(
is_inline_element_to_remove_function) {}
void ApplyStyleCommand::UpdateStartEnd(const EphemeralRange& range) {
if (!use_ending_selection_ &&
(range.StartPosition() != start_ || range.EndPosition() != end_))
use_ending_selection_ = true;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
const bool was_base_first =
StartingSelection().IsBaseFirst() || !SelectionIsDirectional();
SelectionInDOMTree::Builder builder;
if (was_base_first)
builder.SetAsForwardSelection(range);
else
builder.SetAsBackwardSelection(range);
const VisibleSelection& visible_selection =
CreateVisibleSelection(builder.Build());
SetEndingSelection(
SelectionForUndoStep::From(visible_selection.AsSelection()));
start_ = range.StartPosition();
end_ = range.EndPosition();
}
Position ApplyStyleCommand::StartPosition() {
if (use_ending_selection_)
return EndingSelection().Start();
return start_;
}
Position ApplyStyleCommand::EndPosition() {
if (use_ending_selection_)
return EndingSelection().End();
return end_;
}
void ApplyStyleCommand::DoApply(EditingState* editing_state) {
DCHECK(StartPosition().IsNotNull());
DCHECK(EndPosition().IsNotNull());
switch (property_level_) {
case kPropertyDefault: {
// Apply the block-centric properties of the style.
EditingStyle* block_style = style_->ExtractAndRemoveBlockProperties(
GetDocument().GetExecutionContext());
if (!block_style->IsEmpty()) {
ApplyBlockStyle(block_style, editing_state);
if (editing_state->IsAborted())
return;
}
// Apply any remaining styles to the inline elements.
if (!style_->IsEmpty() || styled_inline_element_ ||
is_inline_element_to_remove_function_) {
ApplyRelativeFontStyleChange(style_.Get(), editing_state);
if (editing_state->IsAborted())
return;
ApplyInlineStyle(style_.Get(), editing_state);
if (editing_state->IsAborted())
return;
}
break;
}
case kForceBlockProperties:
// Force all properties to be applied as block styles.
ApplyBlockStyle(style_.Get(), editing_state);
break;
}
}
InputEvent::InputType ApplyStyleCommand::GetInputType() const {
return input_type_;
}
void ApplyStyleCommand::ApplyBlockStyle(EditingStyle* style,
EditingState* editing_state) {
// 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
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// get positions we want to use for applying style
Position start = StartPosition();
Position end = EndPosition();
if (ComparePositions(end, start) < 0) {
Position swap = start;
start = end;
end = swap;
}
VisiblePosition visible_start = CreateVisiblePosition(start);
VisiblePosition visible_end = CreateVisiblePosition(end);
if (visible_start.IsNull() || visible_start.IsOrphan() ||
visible_end.IsNull() || visible_end.IsOrphan())
return;
// Save and restore the selection endpoints using their indices in the
// document, since addBlockStyleIfNeeded may moveParagraphs, which can remove
// these endpoints. Calculate start and end indices from the start of the tree
// that they're in.
const Node& scope = NodeTraversal::HighestAncestorOrSelf(
*visible_start.DeepEquivalent().AnchorNode());
const EphemeralRange start_range(
Position::FirstPositionInNode(scope),
visible_start.DeepEquivalent().ParentAnchoredEquivalent());
const EphemeralRange end_range(
Position::FirstPositionInNode(scope),
visible_end.DeepEquivalent().ParentAnchoredEquivalent());
const TextIteratorBehavior behavior =
TextIteratorBehavior::AllVisiblePositionsRangeLengthBehavior();
const int start_index = TextIterator::RangeLength(start_range, behavior);
const int end_index = TextIterator::RangeLength(end_range, behavior);
VisiblePosition paragraph_start(StartOfParagraph(visible_start));
Position beyond_end =
NextPositionOf(EndOfParagraph(visible_end)).DeepEquivalent();
while (
paragraph_start.IsNotNull() &&
(beyond_end.IsNull() || paragraph_start.DeepEquivalent() < beyond_end)) {
DCHECK(paragraph_start.IsValidFor(GetDocument())) << paragraph_start;
RelocatablePosition next_paragraph_start(
NextPositionOf(EndOfParagraph(paragraph_start)).DeepEquivalent());
// RelocatablePosition turns the position into ParentAnchoredEquivalent(),
// which can affect the result of CreateVisiblePosition().
// To avoid an infinite loop, reconvert into a VisiblePosition and check
// that it's after the current paragraph_start.
bool will_advance =
next_paragraph_start.GetPosition().IsNull() ||
CreateVisiblePosition(next_paragraph_start.GetPosition())
.DeepEquivalent() > paragraph_start.DeepEquivalent();
StyleChange style_change(style, paragraph_start.DeepEquivalent());
if (style_change.CssStyle().length() || remove_only_) {
Element* block =
EnclosingBlock(paragraph_start.DeepEquivalent().AnchorNode());
const Position& paragraph_start_to_move =
paragraph_start.DeepEquivalent();
if (!remove_only_ && IsEditablePosition(paragraph_start_to_move)) {
HTMLElement* new_block = MoveParagraphContentsToNewBlockIfNecessary(
paragraph_start_to_move, editing_state);
if (editing_state->IsAborted())
return;
if (new_block)
block = new_block;
}
if (auto* html_element = DynamicTo<HTMLElement>(block)) {
RemoveCSSStyle(style, html_element, editing_state);
if (editing_state->IsAborted())
return;
if (!remove_only_)
AddBlockStyle(style_change, html_element);
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
}
if (!will_advance)
break;
paragraph_start = CreateVisiblePosition(next_paragraph_start.GetPosition());
}
// Update style and layout again, since added or removed styles could have
// affected the layout. We need clean layout in order to compute
// plain-text ranges below.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
EphemeralRange start_ephemeral_range =
PlainTextRange(start_index)
.CreateRangeForSelection(To<ContainerNode>(scope));
if (start_ephemeral_range.IsNull())
return;
EphemeralRange end_ephemeral_range =
PlainTextRange(end_index).CreateRangeForSelection(
To<ContainerNode>(scope));
if (end_ephemeral_range.IsNull())
return;
UpdateStartEnd(EphemeralRange(start_ephemeral_range.StartPosition(),
end_ephemeral_range.StartPosition()));
}
static MutableCSSPropertyValueSet* CopyStyleOrCreateEmpty(
const CSSPropertyValueSet* style) {
if (!style)
return MakeGarbageCollected<MutableCSSPropertyValueSet>(kHTMLQuirksMode);
return style->MutableCopy();
}
void ApplyStyleCommand::ApplyRelativeFontStyleChange(
EditingStyle* style,
EditingState* editing_state) {
static const float kMinimumFontSize = 0.1f;
if (!style || !style->HasFontSizeDelta())
return;
Position start = StartPosition();
Position end = EndPosition();
if (ComparePositions(end, start) < 0) {
Position swap = start;
start = end;
end = swap;
}
// Join up any adjacent text nodes.
if (start.AnchorNode()->IsTextNode()) {
JoinChildTextNodes(start.AnchorNode()->parentNode(), start, end);
start = StartPosition();
end = EndPosition();
}
if (start.IsNull() || end.IsNull())
return;
if (end.AnchorNode()->IsTextNode() &&
start.AnchorNode()->parentNode() != end.AnchorNode()->parentNode()) {
JoinChildTextNodes(end.AnchorNode()->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 (IsValidCaretPositionInTextNode(end)) {
SplitTextAtEnd(start, end);
start = StartPosition();
end = EndPosition();
}
DCHECK(start.AnchorNode());
DCHECK(end.AnchorNode());
// 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
const Node* const beyond_end = end.NodeAsRangePastLastNode();
// Move upstream to ensure we do not add redundant spans.
start = MostBackwardCaretPosition(start);
Node* start_node = start.AnchorNode();
DCHECK(start_node);
// Make sure we're not already at the end or the next NodeTraversal::next()
// will traverse past it.
if (start_node == beyond_end)
return;
if (start_node->IsTextNode() &&
start.ComputeOffsetInContainerNode() >= CaretMaxOffset(start_node)) {
// Move out of text node if range does not include its characters.
start_node = NodeTraversal::Next(*start_node);
if (!start_node)
return;
}
// Store away font size before making any changes to the document.
// This ensures that changes to one node won't effect another.
HeapHashMap<Member<Node>, float> starting_font_sizes;
for (Node* node = start_node; node != beyond_end;
node = NodeTraversal::Next(*node)) {
DCHECK(node);
starting_font_sizes.Set(node, ComputedFontSize(node));
}
// These spans were added by us. If empty after font size changes, they can be
// removed.
HeapVector<Member<HTMLElement>> unstyled_spans;
Node* last_styled_node = nullptr;
Node* node = start_node;
while (node != beyond_end) {
DCHECK(node);
Node* const next_node = NodeTraversal::Next(*node);
auto* element = DynamicTo<HTMLElement>(node);
if (element) {
// Only work on fully selected nodes.
if (!ElementFullySelected(*element, start, end)) {
node = next_node;
continue;
}
} else if (node->IsTextNode() && node->GetLayoutObject() &&
node->parentNode() != last_styled_node) {
// 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 = MakeGarbageCollected<HTMLSpanElement>(GetDocument());
SurroundNodeRangeWithElement(node, node, span, editing_state);
if (editing_state->IsAborted())
return;
element = span;
} else {
node = next_node;
// Only handle HTML elements and text nodes.
continue;
}
last_styled_node = node;
MutableCSSPropertyValueSet* inline_style =
CopyStyleOrCreateEmpty(element->InlineStyle());
float current_font_size = ComputedFontSize(node);
float desired_font_size =
max(kMinimumFontSize,
starting_font_sizes.at(node) + style->FontSizeDelta());
const CSSValue* value =
inline_style->GetPropertyCSSValue(CSSPropertyID::kFontSize);
if (value) {
element->RemoveInlineStyleProperty(CSSPropertyID::kFontSize);
current_font_size = ComputedFontSize(node);
}
if (current_font_size != desired_font_size) {
inline_style->SetProperty(
CSSPropertyID::kFontSize,
*CSSNumericLiteralValue::Create(desired_font_size,
CSSPrimitiveValue::UnitType::kPixels),
false);
SetNodeAttribute(element, html_names::kStyleAttr,
AtomicString(inline_style->AsText()));
}
if (inline_style->IsEmpty()) {
RemoveElementAttribute(element, html_names::kStyleAttr);
if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element))
unstyled_spans.push_back(element);
}
node = next_node;
}
for (const auto& unstyled_span : unstyled_spans) {
RemoveNodePreservingChildren(unstyled_span, editing_state);
if (editing_state->IsAborted())
return;
}
}
static ContainerNode* DummySpanAncestorForNode(const Node* node) {
if (!node)
return nullptr;
for (Node& current : NodeTraversal::InclusiveAncestorsOf(*node)) {
if (IsStyleSpanOrSpanWithOnlyStyleAttribute(DynamicTo<Element>(current)))
return current.parentNode();
}
return nullptr;
}
void ApplyStyleCommand::CleanupUnstyledAppleStyleSpans(
ContainerNode* dummy_span_ancestor,
EditingState* editing_state) {
if (!dummy_span_ancestor)
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
Node* next;
for (Node* node = dummy_span_ancestor->firstChild(); node; node = next) {
next = node->nextSibling();
if (IsSpanWithoutAttributesOrUnstyledStyleSpan(node)) {
RemoveNodePreservingChildren(node, editing_state);
if (editing_state->IsAborted())
return;
}
}
}
HTMLElement* ApplyStyleCommand::SplitAncestorsWithUnicodeBidi(
Node* node,
bool before,
mojo_base::mojom::blink::TextDirection allowed_direction) {
// 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.
Element* block = EnclosingBlock(node);
if (!block)
return nullptr;
ContainerNode* highest_ancestor_with_unicode_bidi = nullptr;
ContainerNode* next_highest_ancestor_with_unicode_bidi = nullptr;
CSSValueID highest_ancestor_unicode_bidi = CSSValueID::kInvalid;
for (Node& runner : NodeTraversal::AncestorsOf(*node)) {
if (runner == block)
break;
CSSValueID unicode_bidi = GetIdentifierValue(
MakeGarbageCollected<CSSComputedStyleDeclaration>(&runner),
CSSPropertyID::kUnicodeBidi);
if (IsValidCSSValueID(unicode_bidi) &&
unicode_bidi != CSSValueID::kNormal) {
highest_ancestor_unicode_bidi = unicode_bidi;
next_highest_ancestor_with_unicode_bidi =
highest_ancestor_with_unicode_bidi;
highest_ancestor_with_unicode_bidi = static_cast<ContainerNode*>(&runner);
}
}
if (!highest_ancestor_with_unicode_bidi)
return nullptr;
HTMLElement* unsplit_ancestor = nullptr;
mojo_base::mojom::blink::TextDirection highest_ancestor_direction;
auto* highest_ancestor_html_element =
DynamicTo<HTMLElement>(highest_ancestor_with_unicode_bidi);
if (allowed_direction !=
mojo_base::mojom::blink::TextDirection::UNKNOWN_DIRECTION &&
highest_ancestor_unicode_bidi != CSSValueID::kBidiOverride &&
highest_ancestor_html_element &&
MakeGarbageCollected<EditingStyle>(highest_ancestor_with_unicode_bidi,
EditingStyle::kAllProperties)
->GetTextDirection(highest_ancestor_direction) &&
highest_ancestor_direction == allowed_direction) {
if (!next_highest_ancestor_with_unicode_bidi)
return highest_ancestor_html_element;
unsplit_ancestor = highest_ancestor_html_element;
highest_ancestor_with_unicode_bidi =
next_highest_ancestor_with_unicode_bidi;
}
// Split every ancestor through highest ancestor with embedding.
Node* current_node = node;
while (current_node) {
auto* parent = To<Element>(current_node->parentNode());
if (before ? current_node->previousSibling() : current_node->nextSibling())
SplitElement(parent, before ? current_node : current_node->nextSibling());
if (parent == highest_ancestor_with_unicode_bidi)
break;
current_node = parent;
}
return unsplit_ancestor;
}
void ApplyStyleCommand::RemoveEmbeddingUpToEnclosingBlock(
Node* node,
HTMLElement* unsplit_ancestor,
EditingState* editing_state) {
Element* block = EnclosingBlock(node);
if (!block)
return;
for (Node& runner : NodeTraversal::AncestorsOf(*node)) {
if (runner == block || runner == unsplit_ancestor)
break;
if (!runner.IsStyledElement())
continue;
auto* element = To<Element>(&runner);
CSSValueID unicode_bidi = GetIdentifierValue(
MakeGarbageCollected<CSSComputedStyleDeclaration>(element),
CSSPropertyID::kUnicodeBidi);
if (!IsValidCSSValueID(unicode_bidi) || unicode_bidi == CSSValueID::kNormal)
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->FastHasAttribute(html_names::kDirAttr)) {
// 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.
RemoveElementAttribute(element, html_names::kDirAttr);
} else {
MutableCSSPropertyValueSet* inline_style =
CopyStyleOrCreateEmpty(element->InlineStyle());
inline_style->SetProperty(CSSPropertyID::kUnicodeBidi,
CSSValueID::kNormal);
inline_style->RemoveProperty(CSSPropertyID::kDirection);
SetNodeAttribute(element, html_names::kStyleAttr,
AtomicString(inline_style->AsText()));
if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element)) {
RemoveNodePreservingChildren(element, editing_state);
if (editing_state->IsAborted())
return;
}
}
}
}
static HTMLElement* HighestEmbeddingAncestor(Node* start_node,
Node* enclosing_node) {
for (Node* n = start_node; n && n != enclosing_node; n = n->parentNode()) {
auto* html_element = DynamicTo<HTMLElement>(n);
if (html_element &&
EditingStyleUtilities::IsEmbedOrIsolate(GetIdentifierValue(
MakeGarbageCollected<CSSComputedStyleDeclaration>(n),
CSSPropertyID::kUnicodeBidi))) {
return html_element;
}
}
return nullptr;
}
void ApplyStyleCommand::ApplyInlineStyle(EditingStyle* style,
EditingState* editing_state) {
ContainerNode* start_dummy_span_ancestor = nullptr;
ContainerNode* end_dummy_span_ancestor = nullptr;
// 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
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
// adjust to the positions we want to use for applying style
Position start = StartPosition();
Position end = EndPosition();
if (start.IsNull() || end.IsNull())
return;
if (ComparePositions(end, start) < 0) {
Position swap = start;
start = end;
end = swap;
}
// split the start node and containing element if the selection starts inside
// of it
bool split_start = IsValidCaretPositionInTextNode(start);
if (split_start) {
if (ShouldSplitTextElement(start.AnchorNode()->parentElement(), style))
SplitTextElementAtStart(start, end);
else
SplitTextAtStart(start, end);
start = StartPosition();
end = EndPosition();
if (start.IsNull() || end.IsNull())
return;
start_dummy_span_ancestor = DummySpanAncestorForNode(start.AnchorNode());
}
// split the end node and containing element if the selection ends inside of
// it
bool split_end = IsValidCaretPositionInTextNode(end);
if (split_end) {
if (ShouldSplitTextElement(end.AnchorNode()->parentElement(), style))
SplitTextElementAtEnd(start, end);
else
SplitTextAtEnd(start, end);
start = StartPosition();
end = EndPosition();
if (start.IsNull() || end.IsNull())
return;
end_dummy_span_ancestor = DummySpanAncestorForNode(end.AnchorNode());
}
// 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:
// <rdar://problem/3724344> Bolding and unbolding creates extraneous tags
Position remove_start = MostBackwardCaretPosition(start);
mojo_base::mojom::blink::TextDirection text_direction =
mojo_base::mojom::blink::TextDirection::UNKNOWN_DIRECTION;
bool has_text_direction = style->GetTextDirection(text_direction);
EditingStyle* style_without_embedding = nullptr;
EditingStyle* embedding_style = nullptr;
if (has_text_direction) {
// Leave alone an ancestor that provides the desired single level embedding,
// if there is one.
HTMLElement* start_unsplit_ancestor =
SplitAncestorsWithUnicodeBidi(start.AnchorNode(), true, text_direction);
HTMLElement* end_unsplit_ancestor =
SplitAncestorsWithUnicodeBidi(end.AnchorNode(), false, text_direction);
RemoveEmbeddingUpToEnclosingBlock(start.AnchorNode(),
start_unsplit_ancestor, editing_state);
if (editing_state->IsAborted())
return;
RemoveEmbeddingUpToEnclosingBlock(end.AnchorNode(), end_unsplit_ancestor,
editing_state);
if (editing_state->IsAborted())
return;
// Avoid removing the dir attribute and the unicode-bidi and direction
// properties from the unsplit ancestors.
Position embedding_remove_start = remove_start;
if (start_unsplit_ancestor &&
ElementFullySelected(*start_unsplit_ancestor, remove_start, end))
embedding_remove_start =
Position::InParentAfterNode(*start_unsplit_ancestor);
Position embedding_remove_end = end;
if (end_unsplit_ancestor &&
ElementFullySelected(*end_unsplit_ancestor, remove_start, end))
embedding_remove_end = MostForwardCaretPosition(
Position::InParentBeforeNode(*end_unsplit_ancestor));
if (embedding_remove_end != remove_start || embedding_remove_end != end) {
style_without_embedding = style->Copy();
embedding_style = style_without_embedding->ExtractAndRemoveTextDirection(
GetDocument().GetExecutionContext()->GetSecureContextMode());
if (ComparePositions(embedding_remove_start, embedding_remove_end) <= 0) {
RemoveInlineStyle(
embedding_style,
EphemeralRange(embedding_remove_start, embedding_remove_end),
editing_state);
if (editing_state->IsAborted())
return;
}
}
}
RemoveInlineStyle(style_without_embedding ? style_without_embedding : style,
EphemeralRange(remove_start, end), editing_state);
if (editing_state->IsAborted())
return;
start = StartPosition();
end = EndPosition();
if (start.IsNull() || start.IsOrphan() || end.IsNull() || end.IsOrphan())
return;
if (split_start) {
bool merge_result =
MergeStartWithPreviousIfIdentical(start, end, editing_state);
if (editing_state->IsAborted())
return;
if (split_start && merge_result) {
start = StartPosition();
end = EndPosition();
}
}
if (split_end) {
MergeEndWithNextIfIdentical(start, end, editing_state);
if (editing_state->IsAborted())
return;
start = StartPosition();
end = EndPosition();
}
// 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
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
EditingStyle* style_to_apply = style;
if (has_text_direction) {
// Avoid applying the unicode-bidi and direction properties beneath
// ancestors that already have them.
HTMLElement* embedding_start_element = HighestEmbeddingAncestor(
start.AnchorNode(), EnclosingBlock(start.AnchorNode()));
HTMLElement* embedding_end_element = HighestEmbeddingAncestor(
end.AnchorNode(), EnclosingBlock(end.AnchorNode()));
if (embedding_start_element || embedding_end_element) {
Position embedding_apply_start =
embedding_start_element
? Position::InParentAfterNode(*embedding_start_element)
: start;
Position embedding_apply_end =
embedding_end_element
? Position::InParentBeforeNode(*embedding_end_element)
: end;
DCHECK(embedding_apply_start.IsNotNull());
DCHECK(embedding_apply_end.IsNotNull());
if (!embedding_style) {
style_without_embedding = style->Copy();
embedding_style =
style_without_embedding->ExtractAndRemoveTextDirection(
GetDocument().GetExecutionContext()->GetSecureContextMode());
}
FixRangeAndApplyInlineStyle(embedding_style, embedding_apply_start,
embedding_apply_end, editing_state);
if (editing_state->IsAborted())
return;
style_to_apply = style_without_embedding;
}
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
FixRangeAndApplyInlineStyle(style_to_apply, start, end, editing_state);
if (editing_state->IsAborted())
return;
// Remove dummy style spans created by splitting text elements.
CleanupUnstyledAppleStyleSpans(start_dummy_span_ancestor, editing_state);
if (editing_state->IsAborted())
return;
if (end_dummy_span_ancestor != start_dummy_span_ancestor)
CleanupUnstyledAppleStyleSpans(end_dummy_span_ancestor, editing_state);
}
void ApplyStyleCommand::FixRangeAndApplyInlineStyle(
EditingStyle* style,
const Position& start,
const Position& end,
EditingState* editing_state) {
Node* start_node = start.AnchorNode();
DCHECK(start_node);
if (start.ComputeEditingOffset() >= CaretMaxOffset(start.AnchorNode())) {
start_node = NodeTraversal::Next(*start_node);
if (!start_node ||
ComparePositions(end, FirstPositionInOrBeforeNode(*start_node)) < 0)
return;
}
Node* past_end_node = end.AnchorNode();
if (end.ComputeEditingOffset() >= CaretMaxOffset(end.AnchorNode()))
past_end_node = NodeTraversal::NextSkippingChildren(*end.AnchorNode());
// FIXME: Callers should perform this operation on a Range that includes the
// br if they want style applied to the empty line.
if (start == end && IsA<HTMLBRElement>(*start.AnchorNode()))
past_end_node = NodeTraversal::Next(*start.AnchorNode());
// Start from the highest fully selected ancestor so that we can modify the
// fully selected node. e.g. When applying font-size: large on <font
// color="blue">hello</font>, we need to include the font element in our run
// to generate <font color="blue" size="4">hello</font> instead of <font
// color="blue"><font size="4">hello</font></font>
Element* editable_root = RootEditableElement(*start_node);
if (start_node != editable_root) {
// TODO(editing-dev): Investigate why |start| can be after |end| here in
// some cases. For example, in web test
// editing/style/make-text-writing-direction-inline-{mac,win}.html
// blink::Range object will collapse to end in this case but EphemeralRange
// will trigger DCHECK, so we have to explicitly handle this.
const EphemeralRange& range =
start <= end ? EphemeralRange(start, end) : EphemeralRange(end, start);
while (editable_root && start_node->parentNode() != editable_root &&
IsNodeVisiblyContainedWithin(*start_node->parentNode(), range))
start_node = start_node->parentNode();
}
ApplyInlineStyleToNodeRange(style, start_node, past_end_node, editing_state);
}
static bool ContainsNonEditableRegion(Node& node) {
if (!HasEditableStyle(node))
return true;
Node* sibling = NodeTraversal::NextSkippingChildren(node);
for (Node* descendent = node.firstChild();
descendent && descendent != sibling;
descendent = NodeTraversal::Next(*descendent)) {
if (!HasEditableStyle(*descendent))
return true;
}
return false;
}
class InlineRunToApplyStyle {
DISALLOW_NEW();
public:
InlineRunToApplyStyle(Node* start, Node* end, Node* past_end_node)
: start(start), end(end), past_end_node(past_end_node) {
DCHECK_EQ(start->parentNode(), end->parentNode());
}
bool StartAndEndAreStillInDocument() {
return start && end && start->isConnected() && end->isConnected();
}
void Trace(Visitor* visitor) const {
visitor->Trace(start);
visitor->Trace(end);
visitor->Trace(past_end_node);
visitor->Trace(position_for_style_computation);
visitor->Trace(dummy_element);
}
Member<Node> start;
Member<Node> end;
Member<Node> past_end_node;
Position position_for_style_computation;
Member<HTMLSpanElement> dummy_element;
StyleChange change;
};
} // namespace blink
WTF_ALLOW_INIT_WITH_MEM_FUNCTIONS(blink::InlineRunToApplyStyle)
namespace blink {
void ApplyStyleCommand::ApplyInlineStyleToNodeRange(
EditingStyle* style,
Node* start_node,
Node* past_end_node,
EditingState* editing_state) {
if (remove_only_)
return;
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
HeapVector<InlineRunToApplyStyle> runs;
Node* node = start_node;
for (Node* next; node && node != past_end_node; node = next) {
next = NodeTraversal::Next(*node);
if (!node->GetLayoutObject() || !HasEditableStyle(*node))
continue;
auto* element = DynamicTo<HTMLElement>(node);
if (!HasRichlyEditableStyle(*node) && element) {
// 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 (past_end_node && past_end_node->IsDescendantOf(element))
break;
// Add to this element's inline style and skip over its contents.
next = NodeTraversal::NextSkippingChildren(*node);
if (!style->Style())
continue;
MutableCSSPropertyValueSet* inline_style =
CopyStyleOrCreateEmpty(element->InlineStyle());
inline_style->MergeAndOverrideOnConflict(style->Style());
SetNodeAttribute(element, html_names::kStyleAttr,
AtomicString(inline_style->AsText()));
continue;
}
if (IsEnclosingBlock(node))
continue;
if (node->hasChildren()) {
if (node->contains(past_end_node) || ContainsNonEditableRegion(*node) ||
!HasEditableStyle(*node->parentNode()))
continue;
if (EditingIgnoresContent(*node)) {
next = NodeTraversal::NextSkippingChildren(*node);
continue;
}
}
Node* run_start = node;
Node* run_end = node;
Node* sibling = node->nextSibling();
while (sibling && sibling != past_end_node &&
!sibling->contains(past_end_node) &&
(!IsEnclosingBlock(sibling) || IsA<HTMLBRElement>(*sibling)) &&
!ContainsNonEditableRegion(*sibling)) {
run_end = sibling;
sibling = run_end->nextSibling();
}
DCHECK(run_end);
next = NodeTraversal::NextSkippingChildren(*run_end);
Node* past_run_end_node = NodeTraversal::NextSkippingChildren(*run_end);
if (!ShouldApplyInlineStyleToRun(style, run_start, past_run_end_node))
continue;
runs.push_back(
InlineRunToApplyStyle(run_start, run_end, past_run_end_node));
}
for (auto& run : runs) {
RemoveConflictingInlineStyleFromRun(style, run.start, run.end,
run.past_end_node, editing_state);
if (editing_state->IsAborted())
return;
if (run.StartAndEndAreStillInDocument()) {
run.position_for_style_computation = PositionToComputeInlineStyleChange(
run.start, run.dummy_element, editing_state);
if (editing_state->IsAborted())
return;
}
}
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
for (auto& run : runs) {
if (run.position_for_style_computation.IsNotNull())
run.change = StyleChange(style, run.position_for_style_computation);
}
for (auto& run : runs) {
if (run.dummy_element) {
RemoveNode(run.dummy_element, editing_state);
if (editing_state->IsAborted())
return;
}
if (run.StartAndEndAreStillInDocument()) {
ApplyInlineStyleChange(run.start.Release(), run.end.Release(), run.change,
kAddStyledElement, editing_state);
if (editing_state->IsAborted())
return;
}
}
}
bool ApplyStyleCommand::IsStyledInlineElementToRemove(Element* element) const {
return (styled_inline_element_ &&
element->HasTagName(styled_inline_element_->TagQName())) ||
(is_inline_element_to_remove_function_ &&
is_inline_element_to_remove_function_(element));
}
bool ApplyStyleCommand::ShouldApplyInlineStyleToRun(EditingStyle* style,
Node* run_start,
Node* past_end_node) {
DCHECK(style);
DCHECK(run_start);
for (Node* node = run_start; node && node != past_end_node;
node = NodeTraversal::Next(*node)) {
if (node->hasChildren())
continue;
// We don't consider is_inline_element_to_remove_function_ here because we
// never apply style when is_inline_element_to_remove_function_ is specified
if (!style->StyleIsPresentInComputedStyleOfNode(node))
return true;
if (styled_inline_element_ &&
!EnclosingElementWithTag(Position::BeforeNode(*node),
styled_inline_element_->TagQName()))
return true;
}
return false;
}
void ApplyStyleCommand::RemoveConflictingInlineStyleFromRun(
EditingStyle* style,
Member<Node>& run_start,
Member<Node>& run_end,
Node* past_end_node,
EditingState* editing_state) {
DCHECK(run_start);
DCHECK(run_end);
Node* next = run_start;
for (Node* node = next; node && node->isConnected() && node != past_end_node;
node = next) {
if (EditingIgnoresContent(*node)) {
DCHECK(!node->contains(past_end_node)) << node << " " << past_end_node;
next = NodeTraversal::NextSkippingChildren(*node);
} else {
next = NodeTraversal::Next(*node);
}
auto* element = DynamicTo<HTMLElement>(*node);
if (!element)
continue;
Node* previous_sibling = element->previousSibling();
Node* next_sibling = element->nextSibling();
ContainerNode* parent = element->parentNode();
RemoveInlineStyleFromElement(style, element, editing_state, kRemoveAlways);
if (editing_state->IsAborted())
return;
if (!element->isConnected()) {
// FIXME: We might need to update the start and the end of current
// selection here but need a test.
if (run_start == *element)
run_start = previous_sibling ? previous_sibling->nextSibling()
: parent->firstChild();
if (run_end == *element)
run_end = next_sibling ? next_sibling->previousSibling()
: parent->lastChild();
}
}
}
bool ApplyStyleCommand::RemoveInlineStyleFromElement(
EditingStyle* style,
HTMLElement* element,
EditingState* editing_state,
InlineStyleRemovalMode mode,
EditingStyle* extracted_style) {
DCHECK(element);
GetDocument().UpdateStyleAndLayoutTree();
if (!element->parentNode() || !HasEditableStyle(*element->parentNode()))
return false;
if (IsStyledInlineElementToRemove(element)) {
if (mode == kRemoveNone)
return true;
if (extracted_style)
extracted_style->MergeInlineStyleOfElement(element,
EditingStyle::kOverrideValues);
RemoveNodePreservingChildren(element, editing_state);
if (editing_state->IsAborted())
return false;
return true;
}
bool removed = RemoveImplicitlyStyledElement(style, element, mode,
extracted_style, editing_state);
if (editing_state->IsAborted())
return false;
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. <b style='font-weight: bold'>)
if (RemoveCSSStyle(style, element, editing_state, mode, extracted_style))
removed = true;
if (editing_state->IsAborted())
return false;
return removed;
}
void ApplyStyleCommand::ReplaceWithSpanOrRemoveIfWithoutAttributes(
HTMLElement* elem,
EditingState* editing_state) {
if (HasNoAttributeOrOnlyStyleAttribute(elem, kStyleAttributeShouldBeEmpty))
RemoveNodePreservingChildren(elem, editing_state);
else
ReplaceElementWithSpanPreservingChildrenAndAttributes(elem);
}
bool ApplyStyleCommand::RemoveImplicitlyStyledElement(
EditingStyle* style,
HTMLElement* element,
InlineStyleRemovalMode mode,
EditingStyle* extracted_style,
EditingState* editing_state) {
DCHECK(style);
if (mode == kRemoveNone) {
DCHECK(!extracted_style);
return style->ConflictsWithImplicitStyleOfElement(element) ||
style->ConflictsWithImplicitStyleOfAttributes(element);
}
DCHECK(mode == kRemoveIfNeeded || mode == kRemoveAlways);
if (style->ConflictsWithImplicitStyleOfElement(
element, extracted_style,
mode == kRemoveAlways ? EditingStyle::kExtractMatchingStyle
: EditingStyle::kDoNotExtractMatchingStyle)) {
ReplaceWithSpanOrRemoveIfWithoutAttributes(element, editing_state);
if (editing_state->IsAborted())
return false;
return true;
}
// unicode-bidi and direction are pushed down separately so don't push down
// with other styles
Vector<QualifiedName> attributes;
if (!style->ExtractConflictingImplicitStyleOfAttributes(
element,
extracted_style ? EditingStyle::kPreserveWritingDirection
: EditingStyle::kDoNotPreserveWritingDirection,
extracted_style, attributes,
mode == kRemoveAlways ? EditingStyle::kExtractMatchingStyle
: EditingStyle::kDoNotExtractMatchingStyle))
return false;
for (const auto& attribute : attributes)
RemoveElementAttribute(element, attribute);
if (IsEmptyFontTag(element) ||
IsSpanWithoutAttributesOrUnstyledStyleSpan(element)) {
RemoveNodePreservingChildren(element, editing_state);
if (editing_state->IsAborted())
return false;
}
return true;
}
bool ApplyStyleCommand::RemoveCSSStyle(EditingStyle* style,
HTMLElement* element,
EditingState* editing_state,
InlineStyleRemovalMode mode,
EditingStyle* extracted_style) {
DCHECK(style);
DCHECK(element);
if (mode == kRemoveNone)
return style->ConflictsWithInlineStyleOfElement(element);
Vector<CSSPropertyID> properties;
if (!style->ConflictsWithInlineStyleOfElement(element, extracted_style,
properties))
return false;
// FIXME: We should use a mass-removal function here but we don't have an
// undoable one yet.
for (const auto& property : properties)
RemoveCSSProperty(element, property);
if (IsSpanWithoutAttributesOrUnstyledStyleSpan(element))
RemoveNodePreservingChildren(element, editing_state);
return true;
}
// Finds the enclosing element until which the tree can be split.
// When a user hits ENTER, they won't expect this element to be split into two.
// You may pass it as the second argument of splitTreeToNode.
static Element* UnsplittableElementForPosition(const Position& p) {
// Since enclosingNodeOfType won't search beyond the highest root editable
// node, this code works even if the closest table cell was outside of the
// root editable node.
auto* enclosing_cell = To<Element>(EnclosingNodeOfType(p, &IsTableCell));
if (enclosing_cell)
return enclosing_cell;
return RootEditableElementOf(p);
}
HTMLElement* ApplyStyleCommand::HighestAncestorWithConflictingInlineStyle(
EditingStyle* style,
Node* node) {
if (!node)
return nullptr;
Node* unsplittable_element =
UnsplittableElementForPosition(FirstPositionInOrBeforeNode(*node));
HTMLElement* result = nullptr;
for (Node* n = node; n; n = n->parentNode()) {
auto* html_element = DynamicTo<HTMLElement>(n);
if (html_element && ShouldRemoveInlineStyleFromElement(style, html_element))
result = html_element;
// Should stop at the editable root (cannot cross editing boundary) and
// also stop at the unsplittable element to be consistent with other UAs
if (n == unsplittable_element)
break;
}
return result;
}
void ApplyStyleCommand::ApplyInlineStyleToPushDown(
Node* node,
EditingStyle* style,
EditingState* editing_state) {
DCHECK(node);
node->GetDocument().UpdateStyleAndLayoutTree();
if (!style || style->IsEmpty() || !node->GetLayoutObject() ||
IsA<HTMLIFrameElement>(*node))
return;
EditingStyle* new_inline_style = style;
auto* html_element = DynamicTo<HTMLElement>(node);
if (html_element && html_element->InlineStyle()) {
new_inline_style = style->Copy();
new_inline_style->MergeInlineStyleOfElement(html_element,
EditingStyle::kOverrideValues);
}
const auto* layout_object = node->GetLayoutObject();
// Since addInlineStyleIfNeeded can't add styles to block-flow layout objects,
// add style attribute instead.
// FIXME: applyInlineStyleToRange should be used here instead.
if ((layout_object->IsLayoutBlockFlow() || node->hasChildren()) &&
html_element) {
SetNodeAttribute(html_element, html_names::kStyleAttr,
AtomicString(new_inline_style->Style()->AsText()));
return;
}
if (layout_object->IsText() &&
To<LayoutText>(layout_object)->IsAllCollapsibleWhitespace())
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(new_inline_style, node, node, editing_state);
}
void ApplyStyleCommand::PushDownInlineStyleAroundNode(
EditingStyle* style,
Node* target_node,
EditingState* editing_state) {
HTMLElement* highest_ancestor =
HighestAncestorWithConflictingInlineStyle(style, target_node);
if (!highest_ancestor)
return;
// The outer loop is traversing the tree vertically from highestAncestor to
// targetNode
Node* current = highest_ancestor;
// 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.
HeapVector<Member<Element>> elements_to_push_down;
while (current && current != target_node && current->contains(target_node)) {
NodeVector current_children;
GetChildNodes(To<ContainerNode>(*current), current_children);
Element* styled_element = nullptr;
if (current->IsStyledElement() &&
IsStyledInlineElementToRemove(To<Element>(current))) {
styled_element = To<Element>(current);
elements_to_push_down.push_back(styled_element);
}
EditingStyle* style_to_push_down = MakeGarbageCollected<EditingStyle>();
if (auto* html_element = DynamicTo<HTMLElement>(current)) {
RemoveInlineStyleFromElement(style, html_element, editing_state,
kRemoveIfNeeded, style_to_push_down);
if (editing_state->IsAborted())
return;
}
// 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 (const auto& current_child : current_children) {
Node* child = current_child;
if (!child->parentNode())
continue;
if (!child->contains(target_node) && elements_to_push_down.size()) {
for (const auto& element : elements_to_push_down) {
Element& wrapper = element->CloneWithoutChildren();
wrapper.removeAttribute(html_names::kStyleAttr);
// Delete id attribute from the second element because the same id
// cannot be used for more than one element
element->removeAttribute(html_names::kIdAttr);
if (IsA<HTMLAnchorElement>(element.Get()))
element->removeAttribute(html_names::kNameAttr);
SurroundNodeRangeWithElement(child, child, &wrapper, editing_state);
if (editing_state->IsAborted())
return;
}
}
// Apply style to all nodes containing targetNode and their siblings but
// NOT to targetNode But if we've removed styledElement then go ahead and
// always apply the style.
if (child != target_node || styled_element) {
ApplyInlineStyleToPushDown(child, style_to_push_down, editing_state);
if (editing_state->IsAborted())
return;
}
// 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 == target_node || child->contains(target_node))
current = child;
}
}
}
void ApplyStyleCommand::RemoveInlineStyle(EditingStyle* style,
const EphemeralRange& range,
EditingState* editing_state) {
const Position& start = range.StartPosition();
const Position& end = range.EndPosition();
DCHECK(Position::CommonAncestorTreeScope(start, end)) << start << " " << end;
// FIXME: We should assert that start/end are not in the middle of a text
// node.
// TODO(editing-dev): Use of UpdateStyleAndLayout
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
Position push_down_start = MostForwardCaretPosition(start);
// 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 <b>hello<div>world</div></b>, we want
// Position("world", 0) instead.
const unsigned push_down_start_offset =
push_down_start.ComputeOffsetInContainerNode();
auto* push_down_start_container =
DynamicTo<Text>(push_down_start.ComputeContainerNode());
if (push_down_start_container &&
push_down_start_offset == push_down_start_container->length())
push_down_start = NextVisuallyDistinctCandidate(push_down_start);
// TODO(editing-dev): Use of UpdateStyleAndLayout
// needs to be audited. See http://crbug.com/590369 for more details.
GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
Position push_down_end = MostBackwardCaretPosition(end);
// 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.
Node* push_down_end_container = push_down_end.ComputeContainerNode();
if (push_down_end_container && push_down_end_container->IsTextNode() &&
!push_down_end.ComputeOffsetInContainerNode())
push_down_end = PreviousVisuallyDistinctCandidate(push_down_end);
PushDownInlineStyleAroundNode(style, push_down_start.AnchorNode(),
editing_state);
if (editing_state->IsAborted())
return;
PushDownInlineStyleAroundNode(style, push_down_end.AnchorNode(),
editing_state);
if (editing_state->IsAborted())
return;
// 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.anchorNode() or
// end.anchorNode(), use pushDownStart or pushDownEnd instead, which
// pushDownInlineStyleAroundNode won't prune.
Position s = start.IsNull() || start.IsOrphan() ? push_down_start : start;
Position e = end.IsNull() || end.IsOrphan() ? push_down_end : end;
// Current ending selection resetting algorithm assumes |start| and |end|
// are in a same DOM tree even if they are not in document.
if (!Position::CommonAncestorTreeScope(start, end))
return;
Node* node = start.AnchorNode();
while (node) {
Node* next_to_process = nullptr;
if (EditingIgnoresContent(*node)) {
DCHECK(node == end.AnchorNode() || !node->contains(end.AnchorNode()))
<< node << " " << end;
next_to_process = NodeTraversal::NextSkippingChildren(*node);
} else {
next_to_process = NodeTraversal::Next(*node);
}
auto* elem = DynamicTo<HTMLElement>(node);
if (elem && ElementFullySelected(*elem, start, end)) {
Node* prev = NodeTraversal::PreviousPostOrder(*elem);
Node* next = NodeTraversal::Next(*elem);
EditingStyle* style_to_push_down = nullptr;
Node* child_node = nullptr;
if (IsStyledInlineElementToRemove(elem)) {
style_to_push_down = MakeGarbageCollected<EditingStyle>();
child_node = elem->firstChild();
}
RemoveInlineStyleFromElement(style, elem, editing_state, kRemoveIfNeeded,
style_to_push_down);
if (editing_state->IsAborted())
return;
if (!elem->isConnected()) {
if (s.AnchorNode() == elem) {
// 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.
DCHECK(s.IsBeforeAnchor() || s.IsBeforeChildren() ||
s.OffsetInContainerNode() <= 0)
<< s;
s = next ? FirstPositionInOrBeforeNode(*next) : Position();
}
if (e.AnchorNode() == elem) {
// 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.
DCHECK(s.IsAfterAnchor() ||
!OffsetIsBeforeLastNodeOffset(s.OffsetInContainerNode(),
s.ComputeContainerNode()))
<< s;
e = prev ? LastPositionInOrAfterNode(*prev) : Position();
}
}
if (style_to_push_down) {
for (; child_node; child_node = child_node->nextSibling()) {
ApplyInlineStyleToPushDown(child_node, style_to_push_down,
editing_state);
if (editing_state->IsAborted())
return;
}
}
}
if (node == end.AnchorNode())
break;
node = next_to_process;
}
UpdateStartEnd(EphemeralRange(s, e));
}
bool ApplyStyleCommand::ElementFullySelected(const HTMLElement& element,
const Position& start,
const Position& end) const {
// The tree may have changed and Position::upstream() relies on an up-to-date
// layout.
element.GetDocument().UpdateStyleAndLayout(DocumentUpdateReason::kEditing);
return ComparePositions(FirstPositionInOrBeforeNode(element), start) >= 0 &&
ComparePositions(
MostBackwardCaretPosition(LastPositionInOrAfterNode(element)),
end) <= 0;
}
void ApplyStyleCommand::SplitTextAtStart(const Position& start,
const Position& end) {
DCHECK(start.ComputeContainerNode()->IsTextNode()) << start;
Position new_end;
if (end.IsOffsetInAnchor() &&
start.ComputeContainerNode() == end.ComputeContainerNode())
new_end =
Position(end.ComputeContainerNode(),
end.OffsetInContainerNode() - start.OffsetInContainerNode());
else
new_end = end;
auto* text = To<Text>(start.ComputeContainerNode());
SplitTextNode(text, start.OffsetInContainerNode());
UpdateStartEnd(EphemeralRange(Position::FirstPositionInNode(*text), new_end));
}
void ApplyStyleCommand::SplitTextAtEnd(const Position& start,
const Position& end) {
DCHECK(end.ComputeContainerNode()->IsTextNode()) << end;
bool should_update_start =
start.IsOffsetInAnchor() &&
start.ComputeContainerNode() == end.ComputeContainerNode();
auto* text = To<Text>(end.AnchorNode());
SplitTextNode(text, end.OffsetInContainerNode());
auto* prev_text_node = DynamicTo<Text>(text->previousSibling());
if (!prev_text_node)
return;
Position new_start =
should_update_start
? Position(prev_text_node, start.OffsetInContainerNode())
: start;
UpdateStartEnd(
EphemeralRange(new_start, Position::LastPositionInNode(*prev_text_node)));
}
void ApplyStyleCommand::SplitTextElementAtStart(const Position& start,
const Position& end) {
DCHECK(start.ComputeContainerNode()->IsTextNode()) << start;
Position new_end;
if (start.ComputeContainerNode() == end.ComputeContainerNode())
new_end =
Position(end.ComputeContainerNode(),
end.OffsetInContainerNode() - start.OffsetInContainerNode());
else
new_end = end;
SplitTextNodeContainingElement(To<Text>(start.ComputeContainerNode()),
start.OffsetInContainerNode());
UpdateStartEnd(EphemeralRange(
Position::BeforeNode(*start.ComputeContainerNode()), new_end));
}
void ApplyStyleCommand::SplitTextElementAtEnd(const Position& start,
const Position& end) {
DCHECK(end.ComputeContainerNode()->IsTextNode()) << end;
bool should_update_start =
start.ComputeContainerNode() == end.ComputeContainerNode();
SplitTextNodeContainingElement(To<Text>(end.ComputeContainerNode()),
end.OffsetInContainerNode());
Node* parent_element = end.ComputeContainerNode()->parentNode();
if (!parent_element || !parent_element->previousSibling())
return;
auto* first_text_node =
DynamicTo<Text>(parent_element->previousSibling()->lastChild());
if (!first_text_node)
return;
Position new_start =
should_update_start
? Position(first_text_node, start.OffsetInContainerNode())
: start;
UpdateStartEnd(
EphemeralRange(new_start, Position::AfterNode(*first_text_node)));
}
bool ApplyStyleCommand::ShouldSplitTextElement(Element* element,
EditingStyle* style) {
auto* html_element = DynamicTo<HTMLElement>(element);
if (!html_element)
return false;
return ShouldRemoveInlineStyleFromElement(style, html_element);
}
bool ApplyStyleCommand::IsValidCaretPositionInTextNode(
const Position& position) {
DCHECK(position.IsNotNull());
Node* node = position.ComputeContainerNode();
if (!position.IsOffsetInAnchor() || !node->IsTextNode())
return false;
int offset_in_text = position.OffsetInContainerNode();
return offset_in_text > CaretMinOffset(node) &&
offset_in_text < CaretMaxOffset(node);
}
bool ApplyStyleCommand::MergeStartWithPreviousIfIdentical(
const Position& start,
const Position& end,
EditingState* editing_state) {
Node* start_node = start.ComputeContainerNode();
int start_offset = start.ComputeOffsetInContainerNode();
if (start_offset)
return false;
if (IsAtomicNode(start_node)) {
// note: prior siblings could be unrendered elements. it's silly to miss the
// merge opportunity just for that.
if (start_node->previousSibling())
return false;
start_node = start_node->parentNode();
}
if (!start_node->IsElementNode())
return false;
Node* previous_sibling = start_node->previousSibling();
if (previous_sibling &&
AreIdenticalElements(*start_node, *previous_sibling)) {
auto* previous_element = To<Element>(previous_sibling);
auto* element = To<Element>(start_node);
Node* start_child = element->firstChild();
DCHECK(start_child);
MergeIdenticalElements(previous_element, element, editing_state);
if (editing_state->IsAborted())
return false;
int start_offset_adjustment = start_child->NodeIndex();
int end_offset_adjustment =
start_node == end.AnchorNode() ? start_offset_adjustment : 0;
UpdateStartEnd(EphemeralRange(
Position(start_node, start_offset_adjustment),
Position(end.AnchorNode(),
end.ComputeEditingOffset() + end_offset_adjustment)));
return true;
}
return false;
}
bool ApplyStyleCommand::MergeEndWithNextIfIdentical(
const Position& start,
const Position& end,
EditingState* editing_state) {
Node* end_node = end.ComputeContainerNode();
if (IsAtomicNode(end_node)) {
int end_offset = end.ComputeOffsetInContainerNode();
if (OffsetIsBeforeLastNodeOffset(end_offset, end_node))
return false;
if (end.AnchorNode()->nextSibling())
return false;
end_node = end.AnchorNode()->parentNode();
}
if (!end_node->IsElementNode() || IsA<HTMLBRElement>(*end_node))
return false;
Node* next_sibling = end_node->nextSibling();
if (next_sibling && AreIdenticalElements(*end_node, *next_sibling)) {
auto* next_element = To<Element>(next_sibling);
auto* element = To<Element>(end_node);
Node* next_child = next_element->firstChild();
MergeIdenticalElements(element, next_element, editing_state);
if (editing_state->IsAborted())
return false;
bool should_update_start = start.ComputeContainerNode() == end_node;
int end_offset = next_child ? next_child->NodeIndex()
: next_element->childNodes()->length();
UpdateStartEnd(EphemeralRange(
should_update_start
? Position(next_element, start.OffsetInContainerNode())
: start,
Position(next_element, end_offset)));
return true;
}
return false;
}
void ApplyStyleCommand::SurroundNodeRangeWithElement(
Node* passed_start_node,
Node* end_node,
Element* element_to_insert,
EditingState* editing_state) {
DCHECK(passed_start_node);
DCHECK(end_node);
DCHECK(element_to_insert);
Node* node = passed_start_node;
Element* element = element_to_insert;
InsertNodeBefore(element, node, editing_state);
if (editing_state->IsAborted())
return;
GetDocument().UpdateStyleAndLayoutTree();
while (node) {
Node* next = node->nextSibling();
if (HasEditableStyle(*node)) {
RemoveNode(node, editing_state);
if (editing_state->IsAborted())
return;
AppendNode(node, element, editing_state);
if (editing_state->IsAborted())
return;
}
if (node == end_node)
break;
node = next;
}
Node* next_sibling = element->nextSibling();
Node* previous_sibling = element->previousSibling();
auto* next_sibling_element = DynamicTo<Element>(next_sibling);
if (next_sibling_element && HasEditableStyle(*next_sibling) &&
AreIdenticalElements(*element, *next_sibling_element)) {
MergeIdenticalElements(element, next_sibling_element, editing_state);
if (editing_state->IsAborted())
return;
}
auto* previous_sibling_element = DynamicTo<Element>(previous_sibling);
if (previous_sibling_element && HasEditableStyle(*previous_sibling)) {
auto* merged_element = DynamicTo<Element>(previous_sibling->nextSibling());
if (merged_element &&
HasEditableStyle(*(previous_sibling->nextSibling())) &&
AreIdenticalElements(*previous_sibling_element, *merged_element)) {
MergeIdenticalElements(previous_sibling_element, merged_element,
editing_state);
if (editing_state->IsAborted())
return;
}
}
// 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().
}
void ApplyStyleCommand::AddBlockStyle(const StyleChange& style_change,
HTMLElement* block) {
// Do not check for legacy styles here. Those styles, like <B> and <I>, only
// apply for inline content.
if (!block)
return;
String css_style = style_change.CssStyle();
StringBuilder css_text;
css_text.Append(css_style);
if (const CSSPropertyValueSet* decl = block->InlineStyle()) {
if (!css_style.IsEmpty())
css_text.Append(' ');
css_text.Append(decl->AsText());
}
SetNodeAttribute(block, html_names::kStyleAttr, css_text.ToAtomicString());
}
void ApplyStyleCommand::AddInlineStyleIfNeeded(EditingStyle* style,
Node* passed_start,
Node* passed_end,
EditingState* editing_state) {
if (!passed_start || !passed_end || !passed_start->isConnected() ||
!passed_end->isConnected())
return;
Node* start = passed_start;
Member<HTMLSpanElement> dummy_element = nullptr;
StyleChange style_change(style, PositionToComputeInlineStyleChange(
start, dummy_element, editing_state));
if (editing_state->IsAborted())
return;
if (dummy_element) {
RemoveNode(dummy_element, editing_state);
if (editing_state->IsAborted())
return;
}
ApplyInlineStyleChange(start, passed_end, style_change,
kDoNotAddStyledElement, editing_state);
}
Position ApplyStyleCommand::PositionToComputeInlineStyleChange(
Node* start_node,
Member<HTMLSpanElement>& dummy_element,
EditingState* editing_state) {
DCHECK(start_node);
// It's okay to obtain the style at the startNode because we've removed all
// relevant styles from the current run.
if (!start_node->IsElementNode()) {
dummy_element = MakeGarbageCollected<HTMLSpanElement>(GetDocument());
InsertNodeAt(dummy_element, Position::BeforeNode(*start_node),
editing_state);
if (editing_state->IsAborted())
return Position();
return Position::BeforeNode(*dummy_element);
}
return FirstPositionInOrBeforeNode(*start_node);
}
void ApplyStyleCommand::ApplyInlineStyleChange(
Node* passed_start,
Node* passed_end,
StyleChange& style_change,
AddStyledElement add_styled_element,
EditingState* editing_state) {
Node* start_node = passed_start;
Node* end_node = passed_end;
DCHECK(start_node->isConnected()) << start_node;
DCHECK(end_node->isConnected()) << end_node;
// Find appropriate font and span elements top-down.
HTMLFontElement* font_container = nullptr;
HTMLElement* style_container = nullptr;
for (Node* container = start_node; container && start_node == end_node;
container = container->firstChild()) {
if (auto* font = DynamicTo<HTMLFontElement>(container))
font_container = font;
bool style_container_is_not_span = !IsA<HTMLSpanElement>(style_container);
auto* container_element = DynamicTo<HTMLElement>(container);
if (container_element) {
if (IsA<HTMLSpanElement>(*container_element) ||
(style_container_is_not_span && container_element->HasChildren()))
style_container = container_element;
}
if (!container->hasChildren())
break;
start_node = container->firstChild();
end_node = container->lastChild();
}
// Font tags need to go outside of CSS so that CSS font sizes override leagcy
// font sizes.
if (style_change.ApplyFontColor() || style_change.ApplyFontFace() ||
style_change.ApplyFontSize()) {
if (font_container) {
if (style_change.ApplyFontColor())
SetNodeAttribute(font_container, html_names::kColorAttr,
AtomicString(style_change.FontColor()));
if (style_change.ApplyFontFace())
SetNodeAttribute(font_container, html_names::kFaceAttr,
AtomicString(style_change.FontFace()));
if (style_change.ApplyFontSize())
SetNodeAttribute(font_container, html_names::kSizeAttr,
AtomicString(style_change.FontSize()));
} else {
auto* font_element = MakeGarbageCollected<HTMLFontElement>(GetDocument());
if (style_change.ApplyFontColor())
font_element->setAttribute(html_names::kColorAttr,
AtomicString(style_change.FontColor()));
if (style_change.ApplyFontFace())
font_element->setAttribute(html_names::kFaceAttr,
AtomicString(style_change.FontFace()));
if (style_change.ApplyFontSize())
font_element->setAttribute(html_names::kSizeAttr,
AtomicString(style_change.FontSize()));
SurroundNodeRangeWithElement(start_node, end_node, font_element,
editing_state);
if (editing_state->IsAborted())
return;
}
}
if (style_change.CssStyle().length()) {
if (style_container) {
if (const CSSPropertyValueSet* existing_style =
style_container->InlineStyle()) {
String existing_text = existing_style->AsText();
StringBuilder css_text;
css_text.Append(existing_text);
if (!existing_text.IsEmpty())
css_text.Append(' ');
css_text.Append(style_change.CssStyle());
SetNodeAttribute(style_container, html_names::kStyleAttr,
css_text.ToAtomicString());
} else {
SetNodeAttribute(style_container, html_names::kStyleAttr,
AtomicString(style_change.CssStyle()));
}
} else {
auto* style_element =
MakeGarbageCollected<HTMLSpanElement>(GetDocument());
style_element->setAttribute(html_names::kStyleAttr,
AtomicString(style_change.CssStyle()));
SurroundNodeRangeWithElement(start_node, end_node, style_element,
editing_state);
if (editing_state->IsAborted())
return;
}
}
if (style_change.ApplyBold()) {
SurroundNodeRangeWithElement(
start_node, end_node,
MakeGarbageCollected<HTMLElement>(html_names::kBTag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
}
if (style_change.ApplyItalic()) {
SurroundNodeRangeWithElement(
start_node, end_node,
MakeGarbageCollected<HTMLElement>(html_names::kITag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
}
if (style_change.ApplyUnderline()) {
SurroundNodeRangeWithElement(
start_node, end_node,
MakeGarbageCollected<HTMLElement>(html_names::kUTag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
}
if (style_change.ApplyLineThrough()) {
SurroundNodeRangeWithElement(start_node, end_node,
MakeGarbageCollected<HTMLElement>(
html_names::kStrikeTag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
}
if (style_change.ApplySubscript()) {
SurroundNodeRangeWithElement(
start_node, end_node,
MakeGarbageCollected<HTMLElement>(html_names::kSubTag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
} else if (style_change.ApplySuperscript()) {
SurroundNodeRangeWithElement(
start_node, end_node,
MakeGarbageCollected<HTMLElement>(html_names::kSupTag, GetDocument()),
editing_state);
if (editing_state->IsAborted())
return;
}
if (styled_inline_element_ && add_styled_element == kAddStyledElement) {
SurroundNodeRangeWithElement(
start_node, end_node, &styled_inline_element_->CloneWithoutChildren(),
editing_state);
}
}
float ApplyStyleCommand::ComputedFontSize(Node* node) {
if (!node)
return 0;
auto* style = MakeGarbageCollected<CSSComputedStyleDeclaration>(node);
if (!style)
return 0;
const auto* value = To<CSSPrimitiveValue>(
style->GetPropertyCSSValue(CSSPropertyID::kFontSize));
if (!value)
return 0;
// TODO(yosin): We should have printer for |CSSPrimitiveValue::UnitType|.
DCHECK(value->IsPx());
return value->GetFloatValue();
}
void ApplyStyleCommand::JoinChildTextNodes(ContainerNode* node,
const Position& start,
const Position& end) {
if (!node)
return;
Position new_start = start;
Position new_end = end;
HeapVector<Member<Text>> text_nodes;
for (Node& child : NodeTraversal::ChildrenOf(*node)) {
if (auto* child_text = DynamicTo<Text>(child))
text_nodes.push_back(child_text);
}
for (const auto& text_node : text_nodes) {
Text* child_text = text_node;
Node* next = child_text->nextSibling();
auto* next_text = DynamicTo<Text>(next);
if (!next_text)
continue;
if (start.IsOffsetInAnchor() && next == start.ComputeContainerNode())
new_start = Position(
child_text, child_text->length() + start.OffsetInContainerNode());
if (end.IsOffsetInAnchor() && next == end.ComputeContainerNode())
new_end = Position(child_text,
child_text->length() + end.OffsetInContainerNode());
String text_to_move = next_text->data();
InsertTextIntoNode(child_text, child_text->length(), text_to_move);
// Removing a Text node doesn't dispatch synchronous events.
RemoveNode(next, ASSERT_NO_EDITING_ABORT);
// don't move child node pointer. it may want to merge with more text nodes.
}
UpdateStartEnd(EphemeralRange(new_start, new_end));
}
void ApplyStyleCommand::Trace(Visitor* visitor) const {
visitor->Trace(style_);
visitor->Trace(start_);
visitor->Trace(end_);
visitor->Trace(styled_inline_element_);
CompositeEditCommand::Trace(visitor);
}
} // namespace blink