| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights |
| * reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "third_party/blink/renderer/core/dom/text.h" |
| |
| #include <utility> |
| |
| #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" |
| #include "third_party/blink/renderer/core/dom/events/scoped_event_queue.h" |
| #include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h" |
| #include "third_party/blink/renderer/core/dom/layout_tree_builder.h" |
| #include "third_party/blink/renderer/core/dom/layout_tree_builder_traversal.h" |
| #include "third_party/blink/renderer/core/dom/node_computed_style.h" |
| #include "third_party/blink/renderer/core/dom/node_traversal.h" |
| #include "third_party/blink/renderer/core/dom/shadow_root.h" |
| #include "third_party/blink/renderer/core/dom/whitespace_attacher.h" |
| #include "third_party/blink/renderer/core/layout/layout_object_factory.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/layout_text_combine.h" |
| #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" |
| #include "third_party/blink/renderer/core/layout/svg/layout_svg_inline_text.h" |
| #include "third_party/blink/renderer/core/svg/svg_foreign_object_element.h" |
| #include "third_party/blink/renderer/core/svg_names.h" |
| #include "third_party/blink/renderer/platform/bindings/dom_data_store.h" |
| #include "third_party/blink/renderer/platform/bindings/exception_state.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" |
| |
| namespace blink { |
| |
| Text* Text::Create(Document& document, const String& data) { |
| return MakeGarbageCollected<Text>(document, data, kCreateText); |
| } |
| |
| Text* Text::CreateEditingText(Document& document, const String& data) { |
| return MakeGarbageCollected<Text>(document, data, kCreateEditingText); |
| } |
| |
| Node* Text::MergeNextSiblingNodesIfPossible() { |
| // Remove empty text nodes. |
| if (!length()) { |
| // Care must be taken to get the next node before removing the current node. |
| Node* next_node = NodeTraversal::NextPostOrder(*this); |
| remove(IGNORE_EXCEPTION_FOR_TESTING); |
| return next_node; |
| } |
| |
| // Merge text nodes. |
| while (Node* next_sibling = nextSibling()) { |
| if (next_sibling->getNodeType() != kTextNode) |
| break; |
| |
| auto* next_text = To<Text>(next_sibling); |
| |
| // Remove empty text nodes. |
| if (!next_text->length()) { |
| next_text->remove(IGNORE_EXCEPTION_FOR_TESTING); |
| continue; |
| } |
| |
| // Both non-empty text nodes. Merge them. |
| unsigned offset = length(); |
| String next_text_data = next_text->data(); |
| String old_text_data = data(); |
| SetDataWithoutUpdate(data() + next_text_data); |
| UpdateTextLayoutObject(old_text_data.length(), 0); |
| |
| GetDocument().DidMergeTextNodes(*this, *next_text, offset); |
| |
| // Empty nextText for layout update. |
| next_text->SetDataWithoutUpdate(g_empty_string); |
| next_text->UpdateTextLayoutObject(0, next_text_data.length()); |
| |
| // Restore nextText for mutation event. |
| next_text->SetDataWithoutUpdate(next_text_data); |
| next_text->UpdateTextLayoutObject(0, 0); |
| |
| GetDocument().IncDOMTreeVersion(); |
| DidModifyData(old_text_data, CharacterData::kUpdateFromNonParser); |
| next_text->remove(IGNORE_EXCEPTION_FOR_TESTING); |
| } |
| |
| return NodeTraversal::NextPostOrder(*this); |
| } |
| |
| Text* Text::splitText(unsigned offset, ExceptionState& exception_state) { |
| // IndexSizeError: Raised if the specified offset is negative or greater than |
| // the number of 16-bit units in data. |
| if (offset > length()) { |
| exception_state.ThrowDOMException( |
| DOMExceptionCode::kIndexSizeError, |
| "The offset " + String::Number(offset) + |
| " is larger than the Text node's length."); |
| return nullptr; |
| } |
| |
| EventQueueScope scope; |
| String old_str = data(); |
| Text* new_text = CloneWithData(GetDocument(), old_str.Substring(offset)); |
| SetDataWithoutUpdate(old_str.Substring(0, offset)); |
| |
| DidModifyData(old_str, CharacterData::kUpdateFromNonParser); |
| |
| if (parentNode()) |
| parentNode()->InsertBefore(new_text, nextSibling(), exception_state); |
| if (exception_state.HadException()) |
| return nullptr; |
| |
| if (GetLayoutObject()) { |
| GetLayoutObject()->SetTextWithOffset(DataImpl(), 0, old_str.length()); |
| if (data().IsEmpty()) { |
| // To avoid |LayoutText| has empty text, we rebuild layout tree. |
| SetForceReattachLayoutTree(); |
| } |
| } |
| |
| if (parentNode()) |
| GetDocument().DidSplitTextNode(*this); |
| else |
| GetDocument().DidRemoveText(*this, offset, old_str.length() - offset); |
| |
| // [NewObject] must always create a new wrapper. Check that a wrapper |
| // does not exist yet. |
| DCHECK( |
| DOMDataStore::GetWrapper(new_text, v8::Isolate::GetCurrent()).IsEmpty()); |
| |
| return new_text; |
| } |
| |
| static const Text* EarliestLogicallyAdjacentTextNode(const Text* t) { |
| for (const Node* n = t->previousSibling(); n; n = n->previousSibling()) { |
| if (auto* text_node = DynamicTo<Text>(n)) { |
| t = text_node; |
| continue; |
| } |
| |
| break; |
| } |
| return t; |
| } |
| |
| static const Text* LatestLogicallyAdjacentTextNode(const Text* t) { |
| for (const Node* n = t->nextSibling(); n; n = n->nextSibling()) { |
| if (auto* text_node = DynamicTo<Text>(n)) { |
| t = text_node; |
| continue; |
| } |
| |
| break; |
| } |
| return t; |
| } |
| |
| String Text::wholeText() const { |
| const Text* start_text = EarliestLogicallyAdjacentTextNode(this); |
| const Text* end_text = LatestLogicallyAdjacentTextNode(this); |
| |
| Node* one_past_end_text = end_text->nextSibling(); |
| unsigned result_length = 0; |
| for (const Node* n = start_text; n != one_past_end_text; |
| n = n->nextSibling()) { |
| auto* text_node = DynamicTo<Text>(n); |
| if (!text_node) |
| continue; |
| const String& data = text_node->data(); |
| CHECK_GE(std::numeric_limits<unsigned>::max() - data.length(), |
| result_length); |
| result_length += data.length(); |
| } |
| StringBuilder result; |
| result.ReserveCapacity(result_length); |
| for (const Node* n = start_text; n != one_past_end_text; |
| n = n->nextSibling()) { |
| auto* text_node = DynamicTo<Text>(n); |
| if (!text_node) |
| continue; |
| result.Append(text_node->data()); |
| } |
| DCHECK_EQ(result.length(), result_length); |
| |
| return result.ToString(); |
| } |
| |
| Text* Text::ReplaceWholeText(const String& new_text) { |
| // Remove all adjacent text nodes, and replace the contents of this one. |
| |
| // Protect startText and endText against mutation event handlers removing the |
| // last ref |
| Text* start_text = const_cast<Text*>(EarliestLogicallyAdjacentTextNode(this)); |
| Text* end_text = const_cast<Text*>(LatestLogicallyAdjacentTextNode(this)); |
| |
| ContainerNode* parent = parentNode(); // Protect against mutation handlers |
| // moving this node during traversal |
| for (Node* n = start_text; |
| n && n != this && n->IsTextNode() && n->parentNode() == parent;) { |
| Node* node_to_remove = n; |
| n = node_to_remove->nextSibling(); |
| parent->RemoveChild(node_to_remove, IGNORE_EXCEPTION_FOR_TESTING); |
| } |
| |
| if (this != end_text) { |
| Node* one_past_end_text = end_text->nextSibling(); |
| for (Node* n = nextSibling(); n && n != one_past_end_text && |
| n->IsTextNode() && |
| n->parentNode() == parent;) { |
| Node* node_to_remove = n; |
| n = node_to_remove->nextSibling(); |
| parent->RemoveChild(node_to_remove, IGNORE_EXCEPTION_FOR_TESTING); |
| } |
| } |
| |
| if (new_text.IsEmpty()) { |
| if (parent && parentNode() == parent) |
| parent->RemoveChild(this, IGNORE_EXCEPTION_FOR_TESTING); |
| return nullptr; |
| } |
| |
| setData(new_text); |
| return this; |
| } |
| |
| String Text::nodeName() const { |
| return "#text"; |
| } |
| |
| Node::NodeType Text::getNodeType() const { |
| return kTextNode; |
| } |
| |
| Node* Text::Clone(Document& factory, CloneChildrenFlag) const { |
| return CloneWithData(factory, data()); |
| } |
| |
| static inline bool EndsWithWhitespace(const String& text) { |
| return text.length() && IsASCIISpace(text[text.length() - 1]); |
| } |
| |
| static inline bool CanHaveWhitespaceChildren( |
| const ComputedStyle& style, |
| const Text::AttachContext& context) { |
| const LayoutObject& parent = *context.parent; |
| // <button> and <fieldset> should allow whitespace even though |
| // LayoutFlexibleBox doesn't. |
| if (parent.IsButtonIncludingNG() || parent.IsFieldset()) |
| return true; |
| |
| if (parent.IsTable() || parent.IsTableRow() || parent.IsTableSection() || |
| parent.IsLayoutTableCol() || parent.IsFrameSet() || |
| parent.IsFlexibleBoxIncludingNG() || parent.IsLayoutGridIncludingNG() || |
| parent.IsSVGRoot() || parent.IsSVGContainer() || parent.IsSVGImage() || |
| parent.IsSVGShape()) { |
| if (!context.use_previous_in_flow || !context.previous_in_flow || |
| !context.previous_in_flow->IsText()) |
| return false; |
| |
| return style.PreserveNewline() || |
| !EndsWithWhitespace( |
| To<LayoutText>(context.previous_in_flow)->GetText()); |
| } |
| return true; |
| } |
| |
| bool Text::TextLayoutObjectIsNeeded(const AttachContext& context, |
| const ComputedStyle& style) const { |
| const LayoutObject& parent = *context.parent; |
| if (!parent.CanHaveChildren()) |
| return false; |
| |
| if (IsEditingText()) |
| return true; |
| |
| if (!length()) |
| return false; |
| |
| if (style.Display() == EDisplay::kNone) |
| return false; |
| |
| if (!ContainsOnlyWhitespaceOrEmpty()) |
| return true; |
| |
| if (!CanHaveWhitespaceChildren(style, context)) |
| return false; |
| |
| // pre-wrap in SVG never makes layoutObject. |
| if (style.WhiteSpace() == EWhiteSpace::kPreWrap && parent.IsSVG()) |
| return false; |
| |
| // pre/pre-wrap/pre-line always make layoutObjects. |
| if (style.PreserveNewline()) |
| return true; |
| |
| if (!context.use_previous_in_flow) |
| return false; |
| |
| if (!context.previous_in_flow) |
| return parent.IsLayoutInline(); |
| |
| if (context.previous_in_flow->IsText()) { |
| return !EndsWithWhitespace( |
| To<LayoutText>(context.previous_in_flow)->GetText()); |
| } |
| |
| return context.previous_in_flow->IsInline() && |
| !context.previous_in_flow->IsBR(); |
| } |
| |
| static bool IsSVGText(Text* text) { |
| Node* parent_or_shadow_host_node = text->ParentOrShadowHostNode(); |
| DCHECK(parent_or_shadow_host_node); |
| return parent_or_shadow_host_node->IsSVGElement() && |
| !IsA<SVGForeignObjectElement>(*parent_or_shadow_host_node); |
| } |
| |
| LayoutText* Text::CreateTextLayoutObject(const ComputedStyle& style, |
| LegacyLayout legacy) { |
| if (IsSVGText(this)) |
| return new LayoutSVGInlineText(this, DataImpl()); |
| |
| if (style.HasTextCombine()) |
| return new LayoutTextCombine(this, DataImpl()); |
| |
| return LayoutObjectFactory::CreateText(this, DataImpl(), legacy); |
| } |
| |
| void Text::AttachLayoutTree(AttachContext& context) { |
| if (context.parent) { |
| ContainerNode* style_parent = LayoutTreeBuilderTraversal::Parent(*this); |
| if (style_parent) { |
| const ComputedStyle* style = style_parent->GetComputedStyle(); |
| DCHECK(style); |
| if (TextLayoutObjectIsNeeded(context, *style)) { |
| LayoutTreeBuilderForText(*this, context, style).CreateLayoutObject(); |
| context.previous_in_flow = GetLayoutObject(); |
| } |
| } |
| } |
| CharacterData::AttachLayoutTree(context); |
| } |
| |
| void Text::ReattachLayoutTreeIfNeeded(AttachContext& context) { |
| bool layout_object_is_needed = false; |
| ContainerNode* style_parent = LayoutTreeBuilderTraversal::Parent(*this); |
| if (style_parent && context.parent) { |
| DCHECK(style_parent->GetComputedStyle()); |
| layout_object_is_needed = |
| TextLayoutObjectIsNeeded(context, *style_parent->GetComputedStyle()); |
| } |
| |
| if (layout_object_is_needed == !!GetLayoutObject()) |
| return; |
| |
| AttachContext reattach_context(context); |
| reattach_context.performing_reattach = true; |
| |
| if (layout_object_is_needed) { |
| DCHECK(!GetLayoutObject()); |
| LayoutTreeBuilderForText(*this, context, style_parent->GetComputedStyle()) |
| .CreateLayoutObject(); |
| } else { |
| DetachLayoutTree(true /* performing_reattach*/); |
| } |
| CharacterData::AttachLayoutTree(reattach_context); |
| } |
| |
| namespace { |
| |
| bool NeedsWhitespaceLayoutObject(const ComputedStyle& style) { |
| return style.PreserveNewline(); |
| } |
| |
| } // namespace |
| |
| void Text::RecalcTextStyle(const StyleRecalcChange change) { |
| scoped_refptr<const ComputedStyle> new_style = |
| GetDocument().GetStyleResolver().StyleForText(this); |
| if (LayoutText* layout_text = GetLayoutObject()) { |
| const ComputedStyle* layout_parent_style = |
| GetLayoutObject()->Parent()->Style(); |
| if (!new_style || GetForceReattachLayoutTree() || |
| (new_style != layout_parent_style && |
| !new_style->InheritedEqual(*layout_parent_style))) { |
| // The computed style or the need for an anonymous inline wrapper for a |
| // display:contents text child changed. |
| SetNeedsReattachLayoutTree(); |
| } else { |
| layout_text->SetStyle(std::move(new_style)); |
| if (NeedsStyleRecalc()) |
| layout_text->SetTextIfNeeded(DataImpl()); |
| } |
| } else if (new_style && (NeedsStyleRecalc() || change.ReattachLayoutTree() || |
| GetForceReattachLayoutTree() || |
| NeedsWhitespaceLayoutObject(*new_style))) { |
| SetNeedsReattachLayoutTree(); |
| } |
| ClearNeedsStyleRecalc(); |
| } |
| |
| void Text::RebuildTextLayoutTree(WhitespaceAttacher& whitespace_attacher) { |
| DCHECK(!ChildNeedsStyleRecalc()); |
| DCHECK(NeedsReattachLayoutTree()); |
| DCHECK(parentNode()); |
| |
| AttachContext context; |
| context.parent = LayoutTreeBuilderTraversal::ParentLayoutObject(*this); |
| ReattachLayoutTree(context); |
| whitespace_attacher.DidReattachText(this); |
| ClearNeedsReattachLayoutTree(); |
| } |
| |
| // Passing both |text_node| and its layout object because repeated calls to |
| // |Node::GetLayoutObject()| are discouraged. |
| static bool ShouldUpdateLayoutByReattaching(const Text& text_node, |
| LayoutText* text_layout_object) { |
| DCHECK_EQ(text_node.GetLayoutObject(), text_layout_object); |
| if (!text_layout_object) |
| return true; |
| Node::AttachContext context; |
| context.parent = text_layout_object->Parent(); |
| if (!text_node.TextLayoutObjectIsNeeded(context, |
| *text_layout_object->Style())) { |
| return true; |
| } |
| if (text_layout_object->IsTextFragment()) { |
| // Changes of |textNode| may change first letter part, so we should |
| // reattach. Note: When |textNode| is empty or holds collapsed white spaces |
| // |text_fragment_layout_object| represents first-letter part but it isn't |
| // inside first-letter-pseudo element. See http://crbug.com/978947 |
| const auto& text_fragment_layout_object = |
| *To<LayoutTextFragment>(text_layout_object); |
| return text_fragment_layout_object.GetFirstLetterPseudoElement() || |
| !text_fragment_layout_object.IsRemainingTextLayoutObject(); |
| } |
| return false; |
| } |
| |
| void Text::UpdateTextLayoutObject(unsigned offset_of_replaced_data, |
| unsigned length_of_replaced_data) { |
| if (!InActiveDocument()) |
| return; |
| LayoutText* text_layout_object = GetLayoutObject(); |
| if (ShouldUpdateLayoutByReattaching(*this, text_layout_object)) { |
| SetForceReattachLayoutTree(); |
| return; |
| } |
| |
| text_layout_object->SetTextWithOffset(DataImpl(), offset_of_replaced_data, |
| length_of_replaced_data); |
| } |
| |
| Text* Text::CloneWithData(Document& factory, const String& data) const { |
| return Create(factory, data); |
| } |
| |
| void Text::Trace(Visitor* visitor) const { |
| CharacterData::Trace(visitor); |
| } |
| |
| } // namespace blink |