476f4d7b1ca5eae1433187fa4385210439743ef1
[WebKit-https.git] / Source / WebCore / style / RenderTreeUpdaterFirstLetter.cpp
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-2011, 2017 Apple Inc. All rights reserved.
6  * Copyright (C) Research In Motion Limited 2010. All rights reserved.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23
24 #include "config.h"
25 #include "RenderTreeUpdaterFirstLetter.h"
26
27 #include "FontCascade.h"
28 #include "RenderBlock.h"
29 #include "RenderButton.h"
30 #include "RenderInline.h"
31 #include "RenderRubyRun.h"
32 #include "RenderSVGText.h"
33 #include "RenderStyle.h"
34 #include "RenderTable.h"
35 #include "RenderTextFragment.h"
36
37 namespace WebCore {
38
39 static RenderStyle styleForFirstLetter(const RenderBlock& firstLetterBlock, const RenderObject& firstLetterContainer)
40 {
41     auto* containerFirstLetterStyle = firstLetterBlock.getCachedPseudoStyle(FIRST_LETTER, &firstLetterContainer.firstLineStyle());
42     // FIXME: There appears to be some path where we have a first letter renderer without first letter style.
43     ASSERT(containerFirstLetterStyle);
44     auto firstLetterStyle = RenderStyle::clone(containerFirstLetterStyle ? *containerFirstLetterStyle : firstLetterContainer.firstLineStyle());
45
46     // If we have an initial letter drop that is >= 1, then we need to force floating to be on.
47     if (firstLetterStyle.initialLetterDrop() >= 1 && !firstLetterStyle.isFloating())
48         firstLetterStyle.setFloating(firstLetterStyle.isLeftToRightDirection() ? LeftFloat : RightFloat);
49
50     // We have to compute the correct font-size for the first-letter if it has an initial letter height set.
51     auto* paragraph = firstLetterContainer.isRenderBlockFlow() ? &firstLetterContainer : firstLetterContainer.containingBlock();
52     if (firstLetterStyle.initialLetterHeight() >= 1 && firstLetterStyle.fontMetrics().hasCapHeight() && paragraph->style().fontMetrics().hasCapHeight()) {
53         // FIXME: For ideographic baselines, we want to go from line edge to line edge. This is equivalent to (N-1)*line-height + the font height.
54         // We don't yet support ideographic baselines.
55         // For an N-line first-letter and for alphabetic baselines, the cap-height of the first letter needs to equal (N-1)*line-height of paragraph lines + cap-height of the paragraph
56         // Mathematically we can't rely on font-size, since font().height() doesn't necessarily match. For reliability, the best approach is simply to
57         // compare the final measured cap-heights of the two fonts in order to get to the closest possible value.
58         firstLetterStyle.setLineBoxContain(LineBoxContainInitialLetter);
59         int lineHeight = paragraph->style().computedLineHeight();
60
61         // Set the font to be one line too big and then ratchet back to get to a precise fit. We can't just set the desired font size based off font height metrics
62         // because many fonts bake ascent into the font metrics. Therefore we have to look at actual measured cap height values in order to know when we have a good fit.
63         auto newFontDescription = firstLetterStyle.fontDescription();
64         float capRatio = firstLetterStyle.fontMetrics().floatCapHeight() / firstLetterStyle.computedFontPixelSize();
65         float startingFontSize = ((firstLetterStyle.initialLetterHeight() - 1) * lineHeight + paragraph->style().fontMetrics().capHeight()) / capRatio;
66         newFontDescription.setSpecifiedSize(startingFontSize);
67         newFontDescription.setComputedSize(startingFontSize);
68         firstLetterStyle.setFontDescription(newFontDescription);
69         firstLetterStyle.fontCascade().update(firstLetterStyle.fontCascade().fontSelector());
70
71         int desiredCapHeight = (firstLetterStyle.initialLetterHeight() - 1) * lineHeight + paragraph->style().fontMetrics().capHeight();
72         int actualCapHeight = firstLetterStyle.fontMetrics().capHeight();
73         while (actualCapHeight > desiredCapHeight) {
74             auto newFontDescription = firstLetterStyle.fontDescription();
75             newFontDescription.setSpecifiedSize(newFontDescription.specifiedSize() - 1);
76             newFontDescription.setComputedSize(newFontDescription.computedSize() -1);
77             firstLetterStyle.setFontDescription(newFontDescription);
78             firstLetterStyle.fontCascade().update(firstLetterStyle.fontCascade().fontSelector());
79             actualCapHeight = firstLetterStyle.fontMetrics().capHeight();
80         }
81     }
82
83     // Force inline display (except for floating first-letters).
84     firstLetterStyle.setDisplay(firstLetterStyle.isFloating() ? BLOCK : INLINE);
85     // CSS2 says first-letter can't be positioned.
86     firstLetterStyle.setPosition(StaticPosition);
87     return firstLetterStyle;
88 }
89
90 // CSS 2.1 http://www.w3.org/TR/CSS21/selector.html#first-letter
91 // "Punctuation (i.e, characters defined in Unicode [UNICODE] in the "open" (Ps), "close" (Pe),
92 // "initial" (Pi). "final" (Pf) and "other" (Po) punctuation classes), that precedes or follows the first letter should be included"
93 static inline bool isPunctuationForFirstLetter(UChar c)
94 {
95     return U_GET_GC_MASK(c) & (U_GC_PS_MASK | U_GC_PE_MASK | U_GC_PI_MASK | U_GC_PF_MASK | U_GC_PO_MASK);
96 }
97
98 static inline bool shouldSkipForFirstLetter(UChar c)
99 {
100     return isSpaceOrNewline(c) || c == noBreakSpace || isPunctuationForFirstLetter(c);
101 }
102
103 static void updateFirstLetterStyle(RenderBlock& firstLetterBlock, RenderObject& currentChild)
104 {
105     RenderElement* firstLetter = currentChild.parent();
106     ASSERT(firstLetter->isFirstLetter());
107
108     RenderElement* firstLetterContainer = firstLetter->parent();
109     auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
110     ASSERT(firstLetter->isFloating() || firstLetter->isInline());
111
112     if (Style::determineChange(firstLetter->style(), pseudoStyle) == Style::Detach) {
113         // The first-letter renderer needs to be replaced. Create a new renderer of the right type.
114         RenderPtr<RenderBoxModelObject> newFirstLetter;
115         if (pseudoStyle.display() == INLINE)
116             newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
117         else
118             newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
119         newFirstLetter->initializeStyle();
120         newFirstLetter->setIsFirstLetter();
121
122         // Move the first letter into the new renderer.
123         while (RenderObject* child = firstLetter->firstChild()) {
124             if (is<RenderText>(*child))
125                 downcast<RenderText>(*child).removeAndDestroyTextBoxes();
126             auto toMove = firstLetter->takeChild(*child);
127             newFirstLetter->addChild(WTFMove(toMove));
128         }
129
130         RenderObject* nextSibling = firstLetter->nextSibling();
131         if (RenderTextFragment* remainingText = downcast<RenderBoxModelObject>(*firstLetter).firstLetterRemainingText()) {
132             ASSERT(remainingText->isAnonymous() || remainingText->textNode()->renderer() == remainingText);
133             // Replace the old renderer with the new one.
134             remainingText->setFirstLetter(*newFirstLetter);
135             newFirstLetter->setFirstLetterRemainingText(*remainingText);
136         }
137         firstLetterContainer->removeAndDestroyChild(*firstLetter);
138         firstLetterContainer->addChild(WTFMove(newFirstLetter), nextSibling);
139     } else
140         firstLetter->setStyle(WTFMove(pseudoStyle));
141 }
142
143 static void createFirstLetterRenderer(RenderBlock& firstLetterBlock, RenderText& currentTextChild)
144 {
145     RenderElement* firstLetterContainer = currentTextChild.parent();
146     auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
147     RenderPtr<RenderBoxModelObject> newFirstLetter;
148     if (pseudoStyle.display() == INLINE)
149         newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
150     else
151         newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
152     newFirstLetter->initializeStyle();
153     newFirstLetter->setIsFirstLetter();
154
155     auto& firstLetter = *newFirstLetter;
156     firstLetterContainer->addChild(WTFMove(newFirstLetter), &currentTextChild);
157
158     // The original string is going to be either a generated content string or a DOM node's
159     // string. We want the original string before it got transformed in case first-letter has
160     // no text-transform or a different text-transform applied to it.
161     String oldText = currentTextChild.originalText();
162     ASSERT(!oldText.isNull());
163
164     if (!oldText.isEmpty()) {
165         unsigned length = 0;
166
167         // Account for leading spaces and punctuation.
168         while (length < oldText.length() && shouldSkipForFirstLetter(oldText[length]))
169             length++;
170
171         // Account for first grapheme cluster.
172         length += numCharactersInGraphemeClusters(StringView(oldText).substring(length), 1);
173
174         // Keep looking for whitespace and allowed punctuation, but avoid
175         // accumulating just whitespace into the :first-letter.
176         for (unsigned scanLength = length; scanLength < oldText.length(); ++scanLength) {
177             UChar c = oldText[scanLength];
178
179             if (!shouldSkipForFirstLetter(c))
180                 break;
181
182             if (isPunctuationForFirstLetter(c))
183                 length = scanLength + 1;
184         }
185
186         auto* textNode = currentTextChild.textNode();
187         auto* beforeChild = currentTextChild.nextSibling();
188         firstLetterContainer->removeAndDestroyChild(currentTextChild);
189
190         // Construct a text fragment for the text after the first letter.
191         // This text fragment might be empty.
192         RenderPtr<RenderTextFragment> newRemainingText;
193         if (textNode) {
194             newRemainingText = createRenderer<RenderTextFragment>(*textNode, oldText, length, oldText.length() - length);
195             textNode->setRenderer(newRemainingText.get());
196         } else
197             newRemainingText = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, length, oldText.length() - length);
198
199         RenderTextFragment& remainingText = *newRemainingText;
200         firstLetterContainer->addChild(WTFMove(newRemainingText), beforeChild);
201         remainingText.setFirstLetter(firstLetter);
202         firstLetter.setFirstLetterRemainingText(remainingText);
203
204         // construct text fragment for the first letter
205         auto letter = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, 0, length);
206
207         firstLetter.addChild(WTFMove(letter));
208     }
209 }
210
211 static bool supportsFirstLetter(RenderBlock& block)
212 {
213     if (is<RenderButton>(block))
214         return true;
215     if (!is<RenderBlockFlow>(block))
216         return false;
217     if (is<RenderSVGText>(block))
218         return false;
219     if (is<RenderRubyRun>(block))
220         return false;
221     return block.canHaveGeneratedChildren();
222 }
223
224 void RenderTreeUpdater::FirstLetter::update(RenderBlock& block)
225 {
226     if (!block.style().hasPseudoStyle(FIRST_LETTER))
227         return;
228     if (!supportsFirstLetter(block))
229         return;
230
231     // FIXME: This should be refactored, firstLetterContainer is not needed.
232     RenderObject* firstLetterRenderer;
233     RenderElement* firstLetterContainer;
234     block.getFirstLetter(firstLetterRenderer, firstLetterContainer);
235
236     if (!firstLetterRenderer)
237         return;
238
239     // Other containers are handled when updating their renderers.
240     if (&block != firstLetterContainer)
241         return;
242
243     // If the child already has style, then it has already been created, so we just want
244     // to update it.
245     if (firstLetterRenderer->parent()->style().styleType() == FIRST_LETTER) {
246         updateFirstLetterStyle(block, *firstLetterRenderer);
247         return;
248     }
249
250     if (!is<RenderText>(firstLetterRenderer))
251         return;
252
253     createFirstLetterRenderer(block, downcast<RenderText>(*firstLetterRenderer));
254 }
255
256 };