1 /*
2  * Copyright (C) 1999 Lars Knoll (knoll@kde.org)
3  *           (C) 1999 Antti Koivisto (koivisto@kde.org)
4  *           (C) 2007 David Smith (catfish.man@gmail.com)
5  * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc.
6  * All rights reserved.
7  * Copyright (C) Research In Motion Limited 2010. All rights reserved.
8  *
9  * This library is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Library General Public
11  * License as published by the Free Software Foundation; either
12  * version 2 of the License, or (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17  * Library General Public License for more details.
18  *
19  * You should have received a copy of the GNU Library General Public License
20  * along with this library; see the file COPYING.LIB.  If not, write to
21  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22  * Boston, MA 02110-1301, USA.
23  */
24 
25 #include "third_party/blink/renderer/core/dom/first_letter_pseudo_element.h"
26 
27 #include "third_party/blink/renderer/core/css/style_change_reason.h"
28 #include "third_party/blink/renderer/core/dom/element.h"
29 #include "third_party/blink/renderer/core/dom/node_computed_style.h"
30 #include "third_party/blink/renderer/core/layout/generated_children.h"
31 #include "third_party/blink/renderer/core/layout/layout_object.h"
32 #include "third_party/blink/renderer/core/layout/layout_object_inlines.h"
33 #include "third_party/blink/renderer/core/layout/layout_text.h"
34 #include "third_party/blink/renderer/core/layout/layout_text_fragment.h"
35 #include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h"
36 #include "third_party/blink/renderer/platform/text/text_break_iterator.h"
37 #include "third_party/blink/renderer/platform/wtf/text/unicode.h"
38 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
39 
40 namespace blink {
41 
42 // CSS 2.1 http://www.w3.org/TR/CSS21/selector.html#first-letter "Punctuation
43 // (i.e, characters defined in Unicode [UNICODE] in the "open" (Ps), "close"
44 // (Pe), "initial" (Pi). "final" (Pf) and "other" (Po) punctuation classes),
45 // that precedes or follows the first letter should be included"
IsPunctuationForFirstLetter(UChar32 c)46 static inline bool IsPunctuationForFirstLetter(UChar32 c) {
47   WTF::unicode::CharCategory char_category = WTF::unicode::Category(c);
48   return char_category == WTF::unicode::kPunctuation_Open ||
49          char_category == WTF::unicode::kPunctuation_Close ||
50          char_category == WTF::unicode::kPunctuation_InitialQuote ||
51          char_category == WTF::unicode::kPunctuation_FinalQuote ||
52          char_category == WTF::unicode::kPunctuation_Other;
53 }
54 
IsSpaceForFirstLetter(UChar c)55 static inline bool IsSpaceForFirstLetter(UChar c) {
56   return IsSpaceOrNewline(c) || c == WTF::unicode::kNoBreakSpaceCharacter;
57 }
58 
FirstLetterLength(const String & text)59 unsigned FirstLetterPseudoElement::FirstLetterLength(const String& text) {
60   unsigned length = 0;
61   unsigned text_length = text.length();
62 
63   if (text_length == 0)
64     return length;
65 
66   // Account for leading spaces first.
67   while (length < text_length && IsSpaceForFirstLetter(text[length]))
68     length++;
69   // Now account for leading punctuation.
70   while (length < text_length &&
71          IsPunctuationForFirstLetter(text.CharacterStartingAt(length)))
72     length += LengthOfGraphemeCluster(text, length);
73 
74   // Bail if we didn't find a letter before the end of the text or before a
75   // space.
76   if (IsSpaceForFirstLetter(text[length]) || length == text_length)
77     return 0;
78 
79   // Account the next character for first letter.
80   length += LengthOfGraphemeCluster(text, length);
81 
82   // Keep looking for allowed punctuation for the :first-letter.
83   unsigned num_code_units = 0;
84   for (; length < text_length; length += num_code_units) {
85     UChar32 c = text.CharacterStartingAt(length);
86     if (!IsPunctuationForFirstLetter(c))
87       break;
88     num_code_units = LengthOfGraphemeCluster(text, length);
89   }
90   return length;
91 }
92 
93 // Once we see any of these layoutObjects we can stop looking for first-letter
94 // as they signal the end of the first line of text.
IsInvalidFirstLetterLayoutObject(const LayoutObject * obj)95 static bool IsInvalidFirstLetterLayoutObject(const LayoutObject* obj) {
96   return (obj->IsBR() || (obj->IsText() && ToLayoutText(obj)->IsWordBreak()));
97 }
98 
FirstLetterTextLayoutObject(const Element & element)99 LayoutText* FirstLetterPseudoElement::FirstLetterTextLayoutObject(
100     const Element& element) {
101   LayoutObject* parent_layout_object = nullptr;
102 
103   // If we are looking at a first letter element then we need to find the
104   // first letter text LayoutObject from the parent node, and not ourselves.
105   if (element.IsFirstLetterPseudoElement()) {
106     parent_layout_object =
107         element.ParentOrShadowHostElement()->GetLayoutObject();
108   } else {
109     parent_layout_object = element.GetLayoutObject();
110   }
111 
112   if (!parent_layout_object ||
113       !parent_layout_object->Style()->HasPseudoElementStyle(
114           kPseudoIdFirstLetter) ||
115       !CanHaveGeneratedChildren(*parent_layout_object) ||
116       !parent_layout_object->BehavesLikeBlockContainer())
117     return nullptr;
118 
119   // Drill down into our children and look for our first text child.
120   LayoutObject* first_letter_text_layout_object =
121       parent_layout_object->SlowFirstChild();
122   while (first_letter_text_layout_object) {
123     // This can be called when the first letter layoutObject is already in the
124     // tree. We do not want to consider that layoutObject for our text
125     // layoutObject so we go to the sibling (which is the LayoutTextFragment for
126     // the remaining text).
127     if (first_letter_text_layout_object->Style() &&
128         first_letter_text_layout_object->Style()->StyleType() ==
129             kPseudoIdFirstLetter) {
130       first_letter_text_layout_object =
131           first_letter_text_layout_object->NextSibling();
132     } else if (first_letter_text_layout_object->IsText()) {
133       // FIXME: If there is leading punctuation in a different LayoutText than
134       // the first letter, we'll not apply the correct style to it.
135       scoped_refptr<StringImpl> str =
136           ToLayoutText(first_letter_text_layout_object)->IsTextFragment()
137               ? ToLayoutTextFragment(first_letter_text_layout_object)
138                     ->CompleteText()
139               : ToLayoutText(first_letter_text_layout_object)->OriginalText();
140       if (FirstLetterLength(str.get()) ||
141           IsInvalidFirstLetterLayoutObject(first_letter_text_layout_object))
142         break;
143       first_letter_text_layout_object =
144           first_letter_text_layout_object->NextSibling();
145     } else if (first_letter_text_layout_object
146                    ->IsListMarkerIncludingNGOutsideAndInside()) {
147       // The list item marker may have out-of-flow siblings inside an anonymous
148       // block. Skip them to make sure we leave the anonymous block before
149       // continuing looking for the first letter text.
150       do {
151         first_letter_text_layout_object =
152             first_letter_text_layout_object->NextInPreOrderAfterChildren(
153                 parent_layout_object);
154       } while (
155           first_letter_text_layout_object &&
156           first_letter_text_layout_object->IsFloatingOrOutOfFlowPositioned());
157     } else if (first_letter_text_layout_object
158                    ->IsFloatingOrOutOfFlowPositioned()) {
159       if (first_letter_text_layout_object->Style()->StyleType() ==
160           kPseudoIdFirstLetter) {
161         first_letter_text_layout_object =
162             first_letter_text_layout_object->SlowFirstChild();
163         break;
164       }
165       first_letter_text_layout_object =
166           first_letter_text_layout_object->NextSibling();
167     } else if (first_letter_text_layout_object->IsAtomicInlineLevel() ||
168                first_letter_text_layout_object->IsLayoutButton() ||
169                IsMenuList(first_letter_text_layout_object)) {
170       return nullptr;
171     } else if (first_letter_text_layout_object
172                    ->IsFlexibleBoxIncludingDeprecatedAndNG() ||
173                first_letter_text_layout_object->IsLayoutGrid()) {
174       first_letter_text_layout_object =
175           first_letter_text_layout_object->NextSibling();
176     } else if (!first_letter_text_layout_object->IsInline() &&
177                first_letter_text_layout_object->Style()->HasPseudoElementStyle(
178                    kPseudoIdFirstLetter) &&
179                CanHaveGeneratedChildren(*first_letter_text_layout_object)) {
180       // There is a layoutObject further down the tree which has
181       // PseudoIdFirstLetter set. When that node is attached we will handle
182       // setting up the first letter then.
183       return nullptr;
184     } else {
185       first_letter_text_layout_object =
186           first_letter_text_layout_object->SlowFirstChild();
187     }
188   }
189 
190   // No first letter text to display, we're done.
191   // FIXME: This black-list of disallowed LayoutText subclasses is fragile.
192   // crbug.com/422336.
193   // Should counter be on this list? What about LayoutTextFragment?
194   if (!first_letter_text_layout_object ||
195       !first_letter_text_layout_object->IsText() ||
196       IsInvalidFirstLetterLayoutObject(first_letter_text_layout_object))
197     return nullptr;
198 
199   return ToLayoutText(first_letter_text_layout_object);
200 }
201 
FirstLetterPseudoElement(Element * parent)202 FirstLetterPseudoElement::FirstLetterPseudoElement(Element* parent)
203     : PseudoElement(parent, kPseudoIdFirstLetter),
204       remaining_text_layout_object_(nullptr) {}
205 
~FirstLetterPseudoElement()206 FirstLetterPseudoElement::~FirstLetterPseudoElement() {
207   DCHECK(!remaining_text_layout_object_);
208 }
209 
UpdateTextFragments()210 void FirstLetterPseudoElement::UpdateTextFragments() {
211   String old_text(remaining_text_layout_object_->CompleteText());
212   DCHECK(old_text.Impl());
213 
214   unsigned length = FirstLetterPseudoElement::FirstLetterLength(old_text);
215   remaining_text_layout_object_->SetTextFragment(
216       old_text.Impl()->Substring(length, old_text.length()), length,
217       old_text.length() - length);
218   remaining_text_layout_object_->DirtyLineBoxes();
219 
220   for (auto* child = GetLayoutObject()->SlowFirstChild(); child;
221        child = child->NextSibling()) {
222     if (!child->IsText() || !ToLayoutText(child)->IsTextFragment())
223       continue;
224     LayoutTextFragment* child_fragment = ToLayoutTextFragment(child);
225     if (child_fragment->GetFirstLetterPseudoElement() != this)
226       continue;
227 
228     child_fragment->SetTextFragment(old_text.Impl()->Substring(0, length), 0,
229                                     length);
230     child_fragment->DirtyLineBoxes();
231 
232     // Make sure the first-letter layoutObject is set to require a layout as it
233     // needs to re-create the line boxes. The remaining text layoutObject
234     // will be marked by the LayoutText::setText.
235     child_fragment->SetNeedsLayoutAndIntrinsicWidthsRecalc(
236         layout_invalidation_reason::kTextChanged);
237     break;
238   }
239 }
240 
ClearRemainingTextLayoutObject()241 void FirstLetterPseudoElement::ClearRemainingTextLayoutObject() {
242   DCHECK(remaining_text_layout_object_);
243   remaining_text_layout_object_ = nullptr;
244 
245   if (GetDocument().GetStyleEngine().InRebuildLayoutTree()) {
246     // We are in the layout tree rebuild phase. We will do UpdateFirstLetter()
247     // as part of RebuildFirstLetterLayoutTree() or AttachLayoutTree(). Marking
248     // us style-dirty during layout tree rebuild is not allowed.
249     return;
250   }
251 
252   // When we remove nodes from the tree, we do not mark ancestry for
253   // ChildNeedsStyleRecalc(). When removing the text node which contains the
254   // first letter, we need to UpdateFirstLetter to render the new first letter
255   // or remove the ::first-letter pseudo if there is no text left. Do that as
256   // part of a style recalc for this ::first-letter.
257   SetNeedsStyleRecalc(
258       kLocalStyleChange,
259       StyleChangeReasonForTracing::Create(style_change_reason::kPseudoClass));
260 }
261 
AttachLayoutTree(AttachContext & context)262 void FirstLetterPseudoElement::AttachLayoutTree(AttachContext& context) {
263   LayoutText* first_letter_text =
264       FirstLetterPseudoElement::FirstLetterTextLayoutObject(*this);
265   // The FirstLetterPseudoElement should have been removed in
266   // Element::UpdateFirstLetterPseudoElement(). However if there existed a first
267   // letter before updating it, the layout tree will be different after
268   // DetachLayoutTree() called right before this method.
269   // If there is a bug in FirstLetterTextLayoutObject(), we might end up with
270   // null here. DCHECKing here, but handling the null pointer below to avoid
271   // crashes.
272   DCHECK(first_letter_text);
273 
274   AttachContext first_letter_context(context);
275   first_letter_context.next_sibling = first_letter_text;
276   first_letter_context.next_sibling_valid = true;
277   if (first_letter_text) {
278     first_letter_context.parent = first_letter_text->Parent();
279     if (first_letter_context.parent->ForceLegacyLayout())
280       first_letter_context.force_legacy_layout = true;
281   }
282   PseudoElement::AttachLayoutTree(first_letter_context);
283   if (first_letter_text)
284     AttachFirstLetterTextLayoutObjects(first_letter_text);
285 }
286 
DetachLayoutTree(bool performing_reattach)287 void FirstLetterPseudoElement::DetachLayoutTree(bool performing_reattach) {
288   if (remaining_text_layout_object_) {
289     if (remaining_text_layout_object_->GetNode() && GetDocument().IsActive()) {
290       auto* text_node = To<Text>(remaining_text_layout_object_->GetNode());
291       remaining_text_layout_object_->SetTextFragment(
292           text_node->DataImpl(), 0, text_node->DataImpl()->length());
293     }
294     remaining_text_layout_object_->SetFirstLetterPseudoElement(nullptr);
295     remaining_text_layout_object_->SetIsRemainingTextLayoutObject(false);
296   }
297   remaining_text_layout_object_ = nullptr;
298 
299   PseudoElement::DetachLayoutTree(performing_reattach);
300 }
301 
302 scoped_refptr<ComputedStyle>
CustomStyleForLayoutObject()303 FirstLetterPseudoElement::CustomStyleForLayoutObject() {
304   LayoutObject* first_letter_text =
305       FirstLetterPseudoElement::FirstLetterTextLayoutObject(*this);
306   if (!first_letter_text)
307     return nullptr;
308   DCHECK(first_letter_text->Parent());
309   return ParentOrShadowHostElement()->StyleForPseudoElement(
310       PseudoElementStyleRequest(GetPseudoId()),
311       first_letter_text->Parent()->FirstLineStyle());
312 }
313 
AttachFirstLetterTextLayoutObjects(LayoutText * first_letter_text)314 void FirstLetterPseudoElement::AttachFirstLetterTextLayoutObjects(LayoutText* first_letter_text) {
315   DCHECK(first_letter_text);
316 
317   // The original string is going to be either a generated content string or a
318   // DOM node's string. We want the original string before it got transformed in
319   // case first-letter has no text-transform or a different text-transform
320   // applied to it.
321   String old_text = first_letter_text->IsTextFragment() ? ToLayoutTextFragment(first_letter_text)->CompleteText() : first_letter_text->OriginalText();
322   DCHECK(old_text.Impl());
323 
324   // FIXME: This would already have been calculated in firstLetterLayoutObject.
325   // Can we pass the length through?
326   unsigned length = FirstLetterPseudoElement::FirstLetterLength(old_text);
327   unsigned remaining_length = old_text.length() - length;
328 
329   // Construct a text fragment for the text after the first letter.
330   // This text fragment might be empty.
331   LayoutTextFragment* remaining_text;
332 
333   LegacyLayout legacy_layout = first_letter_text->ForceLegacyLayout()
334                                    ? LegacyLayout::kForce
335                                    : LegacyLayout::kAuto;
336 
337   if (first_letter_text->GetNode()) {
338     remaining_text = LayoutTextFragment::Create(
339         first_letter_text->GetNode(), old_text.Impl(), length, remaining_length,
340         legacy_layout);
341   } else {
342     remaining_text = LayoutTextFragment::CreateAnonymous(
343         *this, old_text.Impl(), length, remaining_length, legacy_layout);
344   }
345 
346   remaining_text->SetFirstLetterPseudoElement(this);
347   remaining_text->SetIsRemainingTextLayoutObject(true);
348   remaining_text->SetStyle(first_letter_text->Style());
349 
350   if (remaining_text->GetNode())
351     remaining_text->GetNode()->SetLayoutObject(remaining_text);
352 
353   remaining_text_layout_object_ = remaining_text;
354 
355   LayoutObject* next_sibling = GetLayoutObject()->NextSibling();
356   GetLayoutObject()->Parent()->AddChild(remaining_text, next_sibling);
357 
358   // Construct text fragment for the first letter.
359   const ComputedStyle* const letter_style = GetComputedStyle();
360   LayoutTextFragment* letter = LayoutTextFragment::CreateAnonymous(
361       *this, old_text.Impl(), 0, length, legacy_layout);
362   letter->SetFirstLetterPseudoElement(this);
363   letter->SetStyle(letter_style);
364   GetLayoutObject()->AddChild(letter);
365 
366   first_letter_text->Destroy();
367 }
368 
InnerNodeForHitTesting() const369 Node* FirstLetterPseudoElement::InnerNodeForHitTesting() const {
370   // When we hit a first letter during hit testing, hover state and events
371   // should be triggered on the parent of the real text node where the first
372   // letter is taken from. The first letter may not come from a real node - for
373   // quotes and generated text in ::before/::after. In that case walk up the
374   // layout tree to find the closest ancestor which is not anonymous. Note that
375   // display:contents will not be skipped since we generate anonymous
376   // LayoutInline boxes for ::before/::after with display:contents.
377   DCHECK(remaining_text_layout_object_);
378   LayoutObject* layout_object = remaining_text_layout_object_;
379   while (layout_object->IsAnonymous()) {
380     layout_object = layout_object->Parent();
381     DCHECK(layout_object);
382   }
383   Node* node = layout_object->GetNode();
384   DCHECK(node);
385   if (layout_object == remaining_text_layout_object_) {
386     // The text containing the first-letter is a real node, return its flat tree
387     // parent. If we used the layout tree parent, we would have incorrectly
388     // skipped display:contents ancestors.
389     return FlatTreeTraversal::Parent(*node);
390   }
391   if (node->IsPseudoElement()) {
392     // ::first-letter in generated content for ::before/::after. Use pseudo
393     // element parent.
394     return node->ParentOrShadowHostNode();
395   }
396   return node;
397 }
398 
399 }  // namespace blink
400