| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * (C) 2007 David Smith (catfish.man@gmail.com) |
| * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. |
| * All rights reserved. |
| * Copyright (C) Research In Motion Limited 2010. 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/first_letter_pseudo_element.h" |
| |
| #include "third_party/blink/renderer/core/accessibility/ax_object_cache.h" |
| #include "third_party/blink/renderer/core/css/resolver/style_resolver.h" |
| #include "third_party/blink/renderer/core/css/style_change_reason.h" |
| #include "third_party/blink/renderer/core/css/style_request.h" |
| #include "third_party/blink/renderer/core/dom/element.h" |
| #include "third_party/blink/renderer/core/dom/node_computed_style.h" |
| #include "third_party/blink/renderer/core/layout/generated_children.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_object_inlines.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" |
| #include "third_party/blink/renderer/core/layout/list/layout_list_item.h" |
| #include "third_party/blink/renderer/platform/text/text_break_iterator.h" |
| #include "third_party/blink/renderer/platform/wtf/text/code_point_iterator.h" |
| #include "third_party/blink/renderer/platform/wtf/text/unicode.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // CSS 2.1 http://www.w3.org/TR/CSS21/selector.html#first-letter "Punctuation |
| // (i.e, characters defined in Unicode [UNICODE] in the "open" (Ps), "close" |
| // (Pe), "initial" (Pi). "final" (Pf) and "other" (Po) punctuation classes), |
| // that precedes or follows the first letter should be included" |
| inline bool IsPunctuationForFirstLetter(UChar32 c) { |
| WTF::unicode::CharCategory char_category = WTF::unicode::Category(c); |
| return char_category == WTF::unicode::kPunctuation_Open || |
| char_category == WTF::unicode::kPunctuation_Close || |
| char_category == WTF::unicode::kPunctuation_InitialQuote || |
| char_category == WTF::unicode::kPunctuation_FinalQuote || |
| char_category == WTF::unicode::kPunctuation_Other; |
| } |
| |
| bool IsPunctuationForFirstLetter(const String& string, unsigned offset) { |
| return IsPunctuationForFirstLetter(*StringView(string, offset).begin()); |
| } |
| |
| inline bool IsNewLine(UChar c) { |
| if (c == 0xA || c == 0xD) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| inline bool IsSpace(UChar c) { |
| if (IsNewLine(c)) { |
| return false; |
| } |
| |
| return IsSpaceOrNewline(c); |
| } |
| |
| inline bool IsSpaceForFirstLetter(UChar c, bool preserve_breaks) { |
| return (preserve_breaks ? IsSpace(c) : IsSpaceOrNewline(c)) || |
| c == WTF::unicode::kNoBreakSpaceCharacter; |
| } |
| |
| // Once we see any of these layoutObjects we can stop looking for first-letter |
| // as they signal the end of the first line of text. |
| bool IsInvalidFirstLetterLayoutObject(const LayoutObject* obj) { |
| return (obj->IsBR() || (obj->IsText() && To<LayoutText>(obj)->IsWordBreak())); |
| } |
| |
| bool IsParentInlineLayoutObject(const LayoutObject* layout_object) { |
| return layout_object && IsA<LayoutInline>(layout_object->Parent()); |
| } |
| |
| unsigned FirstLetterLengthLegacy(const String& text, bool preserve_breaks) { |
| unsigned length = 0; |
| unsigned text_length = text.length(); |
| |
| if (text_length == 0) |
| return length; |
| |
| // Account for leading spaces first. |
| while (length < text_length && |
| IsSpaceForFirstLetter(text[length], preserve_breaks)) { |
| length++; |
| } |
| |
| // Now account for leading punctuation. |
| while (length < text_length && |
| IsPunctuationForFirstLetter(text.CharacterStartingAt(length))) { |
| length += LengthOfGraphemeCluster(text, length); |
| } |
| |
| // Bail if we didn't find a letter before the end of the text or before a |
| // space. |
| if (IsSpaceForFirstLetter(text[length], preserve_breaks) || |
| IsNewLine(text[length]) || length == text_length) { |
| return 0; |
| } |
| |
| // Account the next character for first letter. |
| length += LengthOfGraphemeCluster(text, length); |
| |
| // Keep looking for allowed punctuation for the :first-letter. |
| unsigned num_code_units = 0; |
| for (; length < text_length; length += num_code_units) { |
| UChar32 c = text.CharacterStartingAt(length); |
| if (!IsPunctuationForFirstLetter(c)) |
| break; |
| num_code_units = LengthOfGraphemeCluster(text, length); |
| } |
| return length; |
| } |
| |
| LayoutText* FirstLetterTextLayoutObjectLegacy(const Element& element) { |
| LayoutObject* parent_layout_object = nullptr; |
| |
| // If we are looking at a first letter element then we need to find the |
| // first letter text LayoutObject from the parent node, and not ourselves. |
| if (element.IsFirstLetterPseudoElement()) { |
| parent_layout_object = |
| element.ParentOrShadowHostElement()->GetLayoutObject(); |
| } else { |
| parent_layout_object = element.GetLayoutObject(); |
| } |
| |
| if (!parent_layout_object || |
| !parent_layout_object->Style()->HasPseudoElementStyle( |
| kPseudoIdFirstLetter) || |
| !CanHaveGeneratedChildren(*parent_layout_object) || |
| !parent_layout_object->BehavesLikeBlockContainer()) |
| return nullptr; |
| |
| // Drill down into our children and look for our first text child. |
| LayoutObject* first_letter_text_layout_object = |
| parent_layout_object->SlowFirstChild(); |
| while (first_letter_text_layout_object) { |
| // This can be called when the first letter layoutObject is already in the |
| // tree. We do not want to consider that layoutObject for our text |
| // layoutObject so we go to the sibling (which is the LayoutTextFragment for |
| // the remaining text). |
| if (first_letter_text_layout_object->Style() && |
| first_letter_text_layout_object->Style()->StyleType() == |
| kPseudoIdFirstLetter) { |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->NextSibling(); |
| } else if (auto* layout_text = |
| DynamicTo<LayoutText>(first_letter_text_layout_object)) { |
| // Don't apply first letter styling to passwords and other elements |
| // obfuscated by -webkit-text-security. Also, see |
| // ShouldUpdateLayoutByReattaching() in text.cc. |
| if (layout_text->IsSecure()) |
| return nullptr; |
| // FIXME: If there is leading punctuation in a different LayoutText than |
| // the first letter, we'll not apply the correct style to it. |
| String str = layout_text->IsTextFragment() |
| ? To<LayoutTextFragment>(first_letter_text_layout_object) |
| ->CompleteText() |
| : layout_text->OriginalText(); |
| bool preserve_breaks = ShouldPreserveBreaks( |
| first_letter_text_layout_object->StyleRef().GetWhiteSpaceCollapse()); |
| if (FirstLetterLengthLegacy(str.Impl(), preserve_breaks) || |
| IsInvalidFirstLetterLayoutObject(first_letter_text_layout_object)) { |
| break; |
| } |
| |
| // In case of inline level content made of punctuation and there is no |
| // sibling, we'll apply style to it. |
| if (IsParentInlineLayoutObject(first_letter_text_layout_object) && |
| str.length() && !first_letter_text_layout_object->NextSibling()) { |
| break; |
| } |
| |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->NextSibling(); |
| } else if (first_letter_text_layout_object->IsListMarker()) { |
| // The list item marker may have out-of-flow siblings inside an anonymous |
| // block. Skip them to make sure we leave the anonymous block before |
| // continuing looking for the first letter text. |
| do { |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->NextInPreOrderAfterChildren( |
| parent_layout_object); |
| } while ( |
| first_letter_text_layout_object && |
| first_letter_text_layout_object->IsFloatingOrOutOfFlowPositioned()); |
| } else if (first_letter_text_layout_object |
| ->IsFloatingOrOutOfFlowPositioned()) { |
| if (first_letter_text_layout_object->Style()->StyleType() == |
| kPseudoIdFirstLetter) { |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->SlowFirstChild(); |
| break; |
| } |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->NextSibling(); |
| } else if (first_letter_text_layout_object->IsAtomicInlineLevel() || |
| first_letter_text_layout_object->IsButton() || |
| first_letter_text_layout_object->IsMenuList()) { |
| return nullptr; |
| } else if (first_letter_text_layout_object->IsFlexibleBox() || |
| first_letter_text_layout_object->IsLayoutGrid() || |
| first_letter_text_layout_object->IsMathML()) { |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->NextSibling(); |
| } else if (!first_letter_text_layout_object->IsInline() && |
| first_letter_text_layout_object->Style()->HasPseudoElementStyle( |
| kPseudoIdFirstLetter) && |
| CanHaveGeneratedChildren(*first_letter_text_layout_object)) { |
| // There is a layoutObject further down the tree which has |
| // PseudoIdFirstLetter set. When that node is attached we will handle |
| // setting up the first letter then. |
| return nullptr; |
| } else if ((first_letter_text_layout_object->IsInline() || |
| first_letter_text_layout_object->IsAnonymousBlock()) && |
| !first_letter_text_layout_object->SlowFirstChild()) { |
| if (LayoutObject* next_sibling = |
| first_letter_text_layout_object->NextSibling()) { |
| first_letter_text_layout_object = next_sibling; |
| continue; |
| } |
| LayoutObject* parent = first_letter_text_layout_object->Parent(); |
| if (parent && parent != parent_layout_object) { |
| first_letter_text_layout_object = parent->NextSibling(); |
| continue; |
| } |
| return nullptr; |
| } else { |
| first_letter_text_layout_object = |
| first_letter_text_layout_object->SlowFirstChild(); |
| } |
| } |
| |
| // No first letter text to display, we're done. |
| // FIXME: This list of disallowed LayoutText subclasses is fragile. |
| // crbug.com/422336. |
| // Should counter be on this list? What about LayoutTextFragment? |
| if (!first_letter_text_layout_object || |
| !first_letter_text_layout_object->IsText() || |
| IsInvalidFirstLetterLayoutObject(first_letter_text_layout_object)) |
| return nullptr; |
| |
| return To<LayoutText>(first_letter_text_layout_object); |
| } |
| |
| } // namespace |
| |
| unsigned FirstLetterPseudoElement::FirstLetterLength(const String& text, |
| bool preserve_breaks, |
| Punctuation& punctuation) { |
| if (!RuntimeEnabledFeatures::CSSFirstLetterTextRewriteEnabled()) { |
| return FirstLetterLengthLegacy(text, preserve_breaks); |
| } |
| DCHECK_NE(punctuation, Punctuation::kDisallow); |
| |
| unsigned length = 0; |
| unsigned text_length = text.length(); |
| |
| if (text_length == 0) { |
| return length; |
| } |
| |
| // Account for leading spaces first. If there is leading punctuation from a |
| // different text node, spaces can not appear in between to form valid |
| // ::first-letter text. |
| if (punctuation == Punctuation::kNotSeen) { |
| while (length < text_length && |
| IsSpaceForFirstLetter(text[length], preserve_breaks)) { |
| length++; |
| } |
| if (length == text_length) { |
| // Only contains spaces. |
| return 0; |
| } |
| } |
| |
| unsigned punctuation_start = length; |
| // Now account for leading punctuation. |
| while (length < text_length && IsPunctuationForFirstLetter(text, length)) { |
| length += LengthOfGraphemeCluster(text, length); |
| } |
| |
| if (length == text_length) { |
| if (length > punctuation_start) { |
| // Text ends at allowed leading punctuation. Signal that we may continue |
| // looking for ::first-letter text in the next text node, including more |
| // punctuation. |
| punctuation = Punctuation::kSeen; |
| return length; |
| } |
| } |
| |
| // Stop allowing leading punctuation. |
| punctuation = Punctuation::kDisallow; |
| |
| DCHECK_LT(length, text_length); |
| if (IsSpaceForFirstLetter(text[length], preserve_breaks) || |
| IsNewLine(text[length])) { |
| return 0; |
| } |
| |
| // Account the next character for first letter. |
| length += LengthOfGraphemeCluster(text, length); |
| |
| // Keep looking for allowed punctuation for the ::first-letter within the same |
| // text node. We are allowed to ignore trailing punctuation in following text |
| // nodes per spec. |
| unsigned num_code_units = 0; |
| for (; length < text_length; length += num_code_units) { |
| if (!IsPunctuationForFirstLetter(text, length)) { |
| break; |
| } |
| num_code_units = LengthOfGraphemeCluster(text, length); |
| } |
| return length; |
| } |
| |
| void FirstLetterPseudoElement::Trace(Visitor* visitor) const { |
| visitor->Trace(remaining_text_layout_object_); |
| PseudoElement::Trace(visitor); |
| } |
| |
| namespace { |
| |
| LayoutObject* FirstInFlowInlineDescendantForFirstLetter(LayoutObject& parent) { |
| // https://drafts.csswg.org/css-pseudo/#first-text-line: |
| // |
| // - The first formatted line of a block container that establishes an inline |
| // formatting context represents the inline-level content of its first line |
| // box. |
| // - The first formatted line of a block container or multi-column container |
| // that contains block-level content (and is not a table wrapper box) is the |
| // first formatted line of its first in-flow block-level child. If no such |
| // line exists, it has no first formatted line. |
| LayoutObject* first_inline = parent.SlowFirstChild(); |
| |
| while (first_inline) { |
| if (first_inline->IsFloatingOrOutOfFlowPositioned()) { |
| first_inline = first_inline->NextSibling(); |
| continue; |
| } |
| if (first_inline->IsListMarker()) { |
| LayoutObject* list_item = first_inline; |
| while (list_item && !list_item->IsLayoutListItem()) { |
| DCHECK_NE(list_item, &parent); |
| list_item = list_item->Parent(); |
| } |
| // Skip the marker contents, but don't escape the list item. |
| first_inline = first_inline->NextInPreOrderAfterChildren(list_item); |
| continue; |
| } |
| if (first_inline->IsInline()) { |
| return first_inline; |
| } |
| if (!first_inline->BehavesLikeBlockContainer()) { |
| // Block level in-flow displays like flex, grid, and table do not have a |
| // first formatted line. |
| return nullptr; |
| } |
| if (first_inline->StyleRef().HasPseudoElementStyle(kPseudoIdFirstLetter)) { |
| // Applying ::first-letter styles from multiple nested containers is not |
| // supported. ::first-letter styles from the inner-most container is |
| // applied - bail out. |
| return nullptr; |
| } |
| first_inline = first_inline->SlowFirstChild(); |
| } |
| return nullptr; |
| } |
| |
| } // namespace |
| |
| LayoutText* FirstLetterPseudoElement::FirstLetterTextLayoutObject( |
| const Element& element) { |
| if (!RuntimeEnabledFeatures::CSSFirstLetterTextRewriteEnabled()) { |
| return FirstLetterTextLayoutObjectLegacy(element); |
| } |
| LayoutObject* parent_layout_object = nullptr; |
| |
| if (element.IsFirstLetterPseudoElement()) { |
| // If the passed-in element is a ::first-letter pseudo element we need to |
| // start from the originating element. |
| parent_layout_object = |
| element.ParentOrShadowHostElement()->GetLayoutObject(); |
| } else { |
| parent_layout_object = element.GetLayoutObject(); |
| } |
| |
| if (!parent_layout_object || |
| !parent_layout_object->StyleRef().HasPseudoElementStyle( |
| kPseudoIdFirstLetter) || |
| !CanHaveGeneratedChildren(*parent_layout_object) || |
| !parent_layout_object->BehavesLikeBlockContainer()) { |
| // This element can not have a styleable ::first-letter. |
| return nullptr; |
| } |
| |
| LayoutObject* inline_child = |
| FirstInFlowInlineDescendantForFirstLetter(*parent_layout_object); |
| if (!inline_child) { |
| return nullptr; |
| } |
| |
| LayoutObject* stay_inside = inline_child->Parent(); |
| LayoutText* punctuation_text = nullptr; |
| Punctuation punctuation = Punctuation::kNotSeen; |
| |
| while (inline_child) { |
| if (inline_child->StyleRef().StyleType() == kPseudoIdFirstLetter) { |
| // This can be called when the ::first-letter LayoutObject is already in |
| // the tree. We do not want to consider that LayoutObject for our text |
| // LayoutObject so we go to the sibling (which is the LayoutTextFragment |
| // for the remaining text). |
| inline_child = inline_child->NextSibling(); |
| } else if (inline_child->IsListMarker()) { |
| inline_child = inline_child->NextInPreOrderAfterChildren(stay_inside); |
| } else if (inline_child->IsInline()) { |
| if (auto* layout_text = DynamicTo<LayoutText>(inline_child)) { |
| // Don't apply first letter styling to passwords and other elements |
| // obfuscated by -webkit-text-security. Also, see |
| // ShouldUpdateLayoutByReattaching() in text.cc. |
| if (layout_text->IsSecure()) { |
| return nullptr; |
| } |
| if (layout_text->IsBR() || layout_text->IsWordBreak()) { |
| return nullptr; |
| } |
| String str = layout_text->IsTextFragment() |
| ? To<LayoutTextFragment>(inline_child)->CompleteText() |
| : layout_text->OriginalText(); |
| bool preserve_breaks = ShouldPreserveBreaks( |
| inline_child->StyleRef().GetWhiteSpaceCollapse()); |
| |
| if (FirstLetterLength(str, preserve_breaks, punctuation)) { |
| // A prefix, or the whole text for the current layout_text is |
| // included in the valid ::first-letter text. |
| |
| if (punctuation == Punctuation::kSeen) { |
| // So far, we have only seen punctuation. Need to continue looking |
| // for a typographic character unit to go along with the |
| // punctuation. |
| if (!punctuation_text) { |
| punctuation_text = layout_text; |
| } |
| } else { |
| // We have found valid ::first-letter text. When the ::first-letter |
| // text spans multiple elements, the UA is free to style only one of |
| // the elements, all of the elements, or none of the elements. Here |
| // we choose to return the first, which matches the Firefox |
| // behavior. |
| if (punctuation_text) { |
| return punctuation_text; |
| } else { |
| return layout_text; |
| } |
| } |
| } else if (punctuation == Punctuation::kDisallow) { |
| // No ::first-letter text seen in this text node. Non-null |
| // punctuation_text means we have seen punctuation in a previous text |
| // node, but leading_punctuation was reset to false as we encountered |
| // spaces or other content that is neither punctuation nor a valid |
| // typographic character unit for ::first-letter. |
| return nullptr; |
| } |
| } else if (inline_child->IsAtomicInlineLevel() || |
| inline_child->IsButton() || inline_child->IsMenuList()) { |
| return nullptr; |
| } |
| inline_child = inline_child->NextInPreOrder(stay_inside); |
| } else if (inline_child->IsFloatingOrOutOfFlowPositioned()) { |
| if (inline_child->StyleRef().StyleType() == kPseudoIdFirstLetter) { |
| inline_child = inline_child->SlowFirstChild(); |
| } else { |
| inline_child = inline_child->NextInPreOrderAfterChildren(stay_inside); |
| } |
| } else { |
| return nullptr; |
| } |
| } |
| return nullptr; |
| } |
| |
| FirstLetterPseudoElement::FirstLetterPseudoElement(Element* parent) |
| : PseudoElement(parent, kPseudoIdFirstLetter), |
| remaining_text_layout_object_(nullptr) {} |
| |
| FirstLetterPseudoElement::~FirstLetterPseudoElement() { |
| DCHECK(!remaining_text_layout_object_); |
| } |
| |
| void FirstLetterPseudoElement::UpdateTextFragments() { |
| String old_text(remaining_text_layout_object_->CompleteText()); |
| DCHECK(old_text.Impl()); |
| |
| bool preserve_breaks = ShouldPreserveBreaks( |
| remaining_text_layout_object_->StyleRef().GetWhiteSpaceCollapse()); |
| FirstLetterPseudoElement::Punctuation punctuation = |
| FirstLetterPseudoElement::Punctuation::kNotSeen; |
| unsigned length = FirstLetterPseudoElement::FirstLetterLength( |
| old_text, preserve_breaks, punctuation); |
| remaining_text_layout_object_->SetTextFragment( |
| old_text.Impl()->Substring(length, old_text.length()), length, |
| old_text.length() - length); |
| remaining_text_layout_object_->InvalidateInlineItems(); |
| |
| for (auto* child = GetLayoutObject()->SlowFirstChild(); child; |
| child = child->NextSibling()) { |
| if (!child->IsText() || !To<LayoutText>(child)->IsTextFragment()) |
| continue; |
| auto* child_fragment = To<LayoutTextFragment>(child); |
| if (child_fragment->GetFirstLetterPseudoElement() != this) |
| continue; |
| |
| child_fragment->SetTextFragment(old_text.Impl()->Substring(0, length), 0, |
| length); |
| child_fragment->InvalidateInlineItems(); |
| |
| // Make sure the first-letter layoutObject is set to require a layout as it |
| // needs to re-create the line boxes. The remaining text layoutObject |
| // will be marked by the LayoutText::setText. |
| child_fragment->SetNeedsLayoutAndIntrinsicWidthsRecalc( |
| layout_invalidation_reason::kTextChanged); |
| break; |
| } |
| } |
| |
| void FirstLetterPseudoElement::ClearRemainingTextLayoutObject() { |
| DCHECK(remaining_text_layout_object_); |
| remaining_text_layout_object_ = nullptr; |
| |
| if (GetDocument().InStyleRecalc()) { |
| // UpdateFirstLetterPseudoElement will handle remaining_text_layout_object_ |
| // changes during style recalc and layout tree rebuild. |
| return; |
| } |
| |
| // When we remove nodes from the tree, we do not mark ancestry for |
| // ChildNeedsStyleRecalc(). When removing the text node which contains the |
| // first letter, we need to UpdateFirstLetter to render the new first letter |
| // or remove the ::first-letter pseudo if there is no text left. Do that as |
| // part of a style recalc for this ::first-letter. |
| SetNeedsStyleRecalc( |
| kLocalStyleChange, |
| StyleChangeReasonForTracing::Create(style_change_reason::kPseudoClass)); |
| } |
| |
| void FirstLetterPseudoElement::AttachLayoutTree(AttachContext& context) { |
| LayoutText* first_letter_text = |
| FirstLetterPseudoElement::FirstLetterTextLayoutObject(*this); |
| // The FirstLetterPseudoElement should have been removed in |
| // Element::UpdateFirstLetterPseudoElement(). However if there existed a first |
| // letter before updating it, the layout tree will be different after |
| // DetachLayoutTree() called right before this method. |
| // If there is a bug in FirstLetterTextLayoutObject(), we might end up with |
| // null here. DCHECKing here, but handling the null pointer below to avoid |
| // crashes. |
| DCHECK(first_letter_text); |
| |
| AttachContext first_letter_context(context); |
| first_letter_context.next_sibling = first_letter_text; |
| first_letter_context.next_sibling_valid = true; |
| if (first_letter_text) { |
| first_letter_context.parent = first_letter_text->Parent(); |
| } |
| PseudoElement::AttachLayoutTree(first_letter_context); |
| if (first_letter_text) |
| AttachFirstLetterTextLayoutObjects(first_letter_text); |
| } |
| |
| void FirstLetterPseudoElement::DetachLayoutTree(bool performing_reattach) { |
| if (remaining_text_layout_object_) { |
| if (remaining_text_layout_object_->GetNode() && GetDocument().IsActive()) { |
| auto* text_node = To<Text>(remaining_text_layout_object_->GetNode()); |
| remaining_text_layout_object_->SetTextFragment( |
| text_node->data(), 0, text_node->data().length()); |
| } |
| remaining_text_layout_object_->SetFirstLetterPseudoElement(nullptr); |
| remaining_text_layout_object_->SetIsRemainingTextLayoutObject(false); |
| } |
| remaining_text_layout_object_ = nullptr; |
| |
| PseudoElement::DetachLayoutTree(performing_reattach); |
| } |
| |
| LayoutObject* FirstLetterPseudoElement::CreateLayoutObject( |
| const ComputedStyle& style) { |
| if (UNLIKELY(!style.InitialLetter().IsNormal())) { |
| return LayoutObject::CreateBlockFlowOrListItem(this, style); |
| } |
| |
| return PseudoElement::CreateLayoutObject(style); |
| } |
| |
| const ComputedStyle* FirstLetterPseudoElement::CustomStyleForLayoutObject( |
| const StyleRecalcContext& style_recalc_context) { |
| LayoutObject* first_letter_text = |
| FirstLetterPseudoElement::FirstLetterTextLayoutObject(*this); |
| if (!first_letter_text) |
| return nullptr; |
| DCHECK(first_letter_text->Parent()); |
| return ParentOrShadowHostElement()->StyleForPseudoElement( |
| style_recalc_context, |
| StyleRequest(GetPseudoId(), |
| first_letter_text->Parent()->FirstLineStyle())); |
| } |
| |
| void FirstLetterPseudoElement::AttachFirstLetterTextLayoutObjects( |
| LayoutText* first_letter_text) { |
| DCHECK(first_letter_text); |
| |
| // The original string is going to be either a generated content string or a |
| // DOM node's string. We want the original string before it got transformed in |
| // case first-letter has no text-transform or a different text-transform |
| // applied to it. |
| String old_text = |
| first_letter_text->IsTextFragment() |
| ? To<LayoutTextFragment>(first_letter_text)->CompleteText() |
| : first_letter_text->OriginalText(); |
| DCHECK(old_text.Impl()); |
| |
| // FIXME: This would already have been calculated in firstLetterLayoutObject. |
| // Can we pass the length through? |
| bool preserve_breaks = ShouldPreserveBreaks( |
| first_letter_text->StyleRef().GetWhiteSpaceCollapse()); |
| FirstLetterPseudoElement::Punctuation punctuation = |
| FirstLetterPseudoElement::Punctuation::kNotSeen; |
| unsigned length = FirstLetterPseudoElement::FirstLetterLength( |
| old_text, preserve_breaks, punctuation); |
| |
| // In case of inline level content made of punctuation, we use |
| // the whole text length instead of FirstLetterLength. |
| if (IsParentInlineLayoutObject(first_letter_text) && length == 0 && |
| old_text.length()) { |
| length = old_text.length(); |
| } |
| |
| unsigned remaining_length = old_text.length() - length; |
| |
| // Construct a text fragment for the text after the first letter. |
| // This text fragment might be empty. |
| LayoutTextFragment* remaining_text; |
| |
| if (first_letter_text->GetNode()) { |
| remaining_text = |
| LayoutTextFragment::Create(first_letter_text->GetNode(), |
| old_text.Impl(), length, remaining_length); |
| } else { |
| remaining_text = LayoutTextFragment::CreateAnonymous( |
| *this, old_text.Impl(), length, remaining_length); |
| } |
| |
| remaining_text->SetFirstLetterPseudoElement(this); |
| remaining_text->SetIsRemainingTextLayoutObject(true); |
| remaining_text->SetStyle(first_letter_text->Style()); |
| |
| if (remaining_text->GetNode()) |
| remaining_text->GetNode()->SetLayoutObject(remaining_text); |
| |
| remaining_text_layout_object_ = remaining_text; |
| |
| LayoutObject* next_sibling = GetLayoutObject()->NextSibling(); |
| GetLayoutObject()->Parent()->AddChild(remaining_text, next_sibling); |
| |
| // Construct text fragment for the first letter. |
| const ComputedStyle* const letter_style = GetComputedStyle(); |
| LayoutTextFragment* letter = |
| LayoutTextFragment::CreateAnonymous(*this, old_text.Impl(), 0, length); |
| letter->SetFirstLetterPseudoElement(this); |
| if (UNLIKELY(GetLayoutObject()->IsInitialLetterBox())) { |
| const LayoutBlock& paragraph = *GetLayoutObject()->ContainingBlock(); |
| // TODO(crbug.com/1393280): Once we can store used font somewhere, we should |
| // compute initial-letter font during layout to take proper effective style. |
| const ComputedStyle& paragraph_style = |
| paragraph.EffectiveStyle(StyleVariant::kFirstLine); |
| const ComputedStyle* initial_letter_text_style = |
| GetDocument().GetStyleResolver().StyleForInitialLetterText( |
| *letter_style, paragraph_style); |
| letter->SetStyle(std::move(initial_letter_text_style)); |
| } else { |
| letter->SetStyle(letter_style); |
| } |
| GetLayoutObject()->AddChild(letter); |
| |
| // AXObjects are normally removed from destroyed layout objects in |
| // Node::DetachLayoutTree(), but as the ::first-letter implementation manually |
| // destroys the layout object for the first letter text, it must manually |
| // remove the accessibility object for it as well. |
| if (auto* cache = GetDocument().ExistingAXObjectCache()) { |
| cache->RemoveAXObjectsInLayoutSubtree(first_letter_text); |
| } |
| first_letter_text->Destroy(); |
| } |
| |
| Node* FirstLetterPseudoElement::InnerNodeForHitTesting() const { |
| // When we hit a first letter during hit testing, hover state and events |
| // should be triggered on the parent of the real text node where the first |
| // letter is taken from. The first letter may not come from a real node - for |
| // quotes and generated text in ::before/::after. In that case walk up the |
| // layout tree to find the closest ancestor which is not anonymous. Note that |
| // display:contents will not be skipped since we generate anonymous |
| // LayoutInline boxes for ::before/::after with display:contents. |
| DCHECK(remaining_text_layout_object_); |
| LayoutObject* layout_object = remaining_text_layout_object_; |
| while (layout_object->IsAnonymous()) { |
| layout_object = layout_object->Parent(); |
| DCHECK(layout_object); |
| } |
| Node* node = layout_object->GetNode(); |
| DCHECK(node); |
| if (layout_object == remaining_text_layout_object_) { |
| // The text containing the first-letter is a real node, return its flat tree |
| // parent. If we used the layout tree parent, we would have incorrectly |
| // skipped display:contents ancestors. |
| return FlatTreeTraversal::Parent(*node); |
| } |
| if (node->IsPseudoElement()) { |
| // ::first-letter in generated content for ::before/::after. Use pseudo |
| // element parent. |
| return node->ParentOrShadowHostNode(); |
| } |
| return node; |
| } |
| |
| } // namespace blink |