199fb178670e23beb88b8aa23708e5d0f99a1003
[WebKit-https.git] / Source / WebCore / rendering / updating / RenderTreeBuilderFirstLetter.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 "RenderTreeBuilderFirstLetter.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 #include "RenderTreeBuilder.h"
37
38 namespace WebCore {
39
40 static RenderStyle styleForFirstLetter(const RenderBlock& firstLetterBlock, const RenderObject& firstLetterContainer)
41 {
42     auto* containerFirstLetterStyle = firstLetterBlock.getCachedPseudoStyle(FIRST_LETTER, &firstLetterContainer.firstLineStyle());
43     // FIXME: There appears to be some path where we have a first letter renderer without first letter style.
44     ASSERT(containerFirstLetterStyle);
45     auto firstLetterStyle = RenderStyle::clone(containerFirstLetterStyle ? *containerFirstLetterStyle : firstLetterContainer.firstLineStyle());
46
47     // If we have an initial letter drop that is >= 1, then we need to force floating to be on.
48     if (firstLetterStyle.initialLetterDrop() >= 1 && !firstLetterStyle.isFloating())
49         firstLetterStyle.setFloating(firstLetterStyle.isLeftToRightDirection() ? LeftFloat : RightFloat);
50
51     // We have to compute the correct font-size for the first-letter if it has an initial letter height set.
52     auto* paragraph = firstLetterContainer.isRenderBlockFlow() ? &firstLetterContainer : firstLetterContainer.containingBlock();
53     if (firstLetterStyle.initialLetterHeight() >= 1 && firstLetterStyle.fontMetrics().hasCapHeight() && paragraph->style().fontMetrics().hasCapHeight()) {
54         // 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.
55         // We don't yet support ideographic baselines.
56         // 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
57         // Mathematically we can't rely on font-size, since font().height() doesn't necessarily match. For reliability, the best approach is simply to
58         // compare the final measured cap-heights of the two fonts in order to get to the closest possible value.
59         firstLetterStyle.setLineBoxContain(LineBoxContainInitialLetter);
60         int lineHeight = paragraph->style().computedLineHeight();
61
62         // 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
63         // 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.
64         auto newFontDescription = firstLetterStyle.fontDescription();
65         float capRatio = firstLetterStyle.fontMetrics().floatCapHeight() / firstLetterStyle.computedFontPixelSize();
66         float startingFontSize = ((firstLetterStyle.initialLetterHeight() - 1) * lineHeight + paragraph->style().fontMetrics().capHeight()) / capRatio;
67         newFontDescription.setSpecifiedSize(startingFontSize);
68         newFontDescription.setComputedSize(startingFontSize);
69         firstLetterStyle.setFontDescription(newFontDescription);
70         firstLetterStyle.fontCascade().update(firstLetterStyle.fontCascade().fontSelector());
71
72         int desiredCapHeight = (firstLetterStyle.initialLetterHeight() - 1) * lineHeight + paragraph->style().fontMetrics().capHeight();
73         int actualCapHeight = firstLetterStyle.fontMetrics().capHeight();
74         while (actualCapHeight > desiredCapHeight) {
75             auto newFontDescription = firstLetterStyle.fontDescription();
76             newFontDescription.setSpecifiedSize(newFontDescription.specifiedSize() - 1);
77             newFontDescription.setComputedSize(newFontDescription.computedSize() -1);
78             firstLetterStyle.setFontDescription(newFontDescription);
79             firstLetterStyle.fontCascade().update(firstLetterStyle.fontCascade().fontSelector());
80             actualCapHeight = firstLetterStyle.fontMetrics().capHeight();
81         }
82     }
83
84     // Force inline display (except for floating first-letters).
85     firstLetterStyle.setDisplay(firstLetterStyle.isFloating() ? BLOCK : INLINE);
86     // CSS2 says first-letter can't be positioned.
87     firstLetterStyle.setPosition(StaticPosition);
88     return firstLetterStyle;
89 }
90
91 // CSS 2.1 http://www.w3.org/TR/CSS21/selector.html#first-letter
92 // "Punctuation (i.e, characters defined in Unicode [UNICODE] in the "open" (Ps), "close" (Pe),
93 // "initial" (Pi). "final" (Pf) and "other" (Po) punctuation classes), that precedes or follows the first letter should be included"
94 static inline bool isPunctuationForFirstLetter(UChar32 c)
95 {
96     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);
97 }
98
99 static inline bool shouldSkipForFirstLetter(UChar32 c)
100 {
101     return isSpaceOrNewline(c) || c == noBreakSpace || isPunctuationForFirstLetter(c);
102 }
103
104 static bool supportsFirstLetter(RenderBlock& block)
105 {
106     if (is<RenderButton>(block))
107         return true;
108     if (!is<RenderBlockFlow>(block))
109         return false;
110     if (is<RenderSVGText>(block))
111         return false;
112     if (is<RenderRubyRun>(block))
113         return false;
114     return block.canHaveGeneratedChildren();
115 }
116
117 RenderTreeBuilder::FirstLetter::FirstLetter(RenderTreeBuilder& builder)
118     : m_builder(builder)
119 {
120 }
121
122 void RenderTreeBuilder::FirstLetter::updateAfterDescendants(RenderBlock& block)
123 {
124     if (!block.style().hasPseudoStyle(FIRST_LETTER))
125         return;
126     if (!supportsFirstLetter(block))
127         return;
128
129     // FIXME: This should be refactored, firstLetterContainer is not needed.
130     RenderObject* firstLetterRenderer;
131     RenderElement* firstLetterContainer;
132     block.getFirstLetter(firstLetterRenderer, firstLetterContainer);
133
134     if (!firstLetterRenderer)
135         return;
136
137     // Other containers are handled when updating their renderers.
138     if (&block != firstLetterContainer)
139         return;
140
141     // If the child already has style, then it has already been created, so we just want
142     // to update it.
143     if (firstLetterRenderer->parent()->style().styleType() == FIRST_LETTER) {
144         updateStyle(block, *firstLetterRenderer);
145         return;
146     }
147
148     if (!is<RenderText>(firstLetterRenderer))
149         return;
150
151     createRenderers(block, downcast<RenderText>(*firstLetterRenderer));
152 }
153
154 void RenderTreeBuilder::FirstLetter::updateStyle(RenderBlock& firstLetterBlock, RenderObject& currentChild)
155 {
156     RenderElement* firstLetter = currentChild.parent();
157     ASSERT(firstLetter->isFirstLetter());
158
159     RenderElement* firstLetterContainer = firstLetter->parent();
160     auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
161     ASSERT(firstLetter->isFloating() || firstLetter->isInline());
162
163     if (Style::determineChange(firstLetter->style(), pseudoStyle) == Style::Detach) {
164         // The first-letter renderer needs to be replaced. Create a new renderer of the right type.
165         RenderPtr<RenderBoxModelObject> newFirstLetter;
166         if (pseudoStyle.display() == INLINE)
167             newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
168         else
169             newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
170         newFirstLetter->initializeStyle();
171         newFirstLetter->setIsFirstLetter();
172
173         // Move the first letter into the new renderer.
174         while (RenderObject* child = firstLetter->firstChild()) {
175             if (is<RenderText>(*child))
176                 downcast<RenderText>(*child).removeAndDestroyTextBoxes();
177             auto toMove = firstLetter->takeChild(m_builder, *child);
178             m_builder.insertChild(*newFirstLetter, WTFMove(toMove));
179         }
180
181         RenderObject* nextSibling = firstLetter->nextSibling();
182         if (RenderTextFragment* remainingText = downcast<RenderBoxModelObject>(*firstLetter).firstLetterRemainingText()) {
183             ASSERT(remainingText->isAnonymous() || remainingText->textNode()->renderer() == remainingText);
184             // Replace the old renderer with the new one.
185             remainingText->setFirstLetter(*newFirstLetter);
186             newFirstLetter->setFirstLetterRemainingText(*remainingText);
187         }
188         firstLetterContainer->removeAndDestroyChild(m_builder, *firstLetter);
189         m_builder.insertChild(*firstLetterContainer, WTFMove(newFirstLetter), nextSibling);
190         return;
191     }
192
193     firstLetter->setStyle(WTFMove(pseudoStyle));
194 }
195
196 void RenderTreeBuilder::FirstLetter::createRenderers(RenderBlock& firstLetterBlock, RenderText& currentTextChild)
197 {
198     RenderElement* firstLetterContainer = currentTextChild.parent();
199     auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
200     RenderPtr<RenderBoxModelObject> newFirstLetter;
201     if (pseudoStyle.display() == INLINE)
202         newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
203     else
204         newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
205     newFirstLetter->initializeStyle();
206     newFirstLetter->setIsFirstLetter();
207
208     auto& firstLetter = *newFirstLetter;
209     m_builder.insertChild(*firstLetterContainer, WTFMove(newFirstLetter), &currentTextChild);
210
211     // The original string is going to be either a generated content string or a DOM node's
212     // string. We want the original string before it got transformed in case first-letter has
213     // no text-transform or a different text-transform applied to it.
214     String oldText = currentTextChild.originalText();
215     ASSERT(!oldText.isNull());
216
217     if (!oldText.isEmpty()) {
218         unsigned length = 0;
219
220         // Account for leading spaces and punctuation.
221         while (length < oldText.length() && shouldSkipForFirstLetter(oldText.characterStartingAt(length)))
222             length += numCodeUnitsInGraphemeClusters(StringView(oldText).substring(length), 1);
223
224         // Account for first grapheme cluster.
225         length += numCodeUnitsInGraphemeClusters(StringView(oldText).substring(length), 1);
226
227         // Keep looking for whitespace and allowed punctuation, but avoid
228         // accumulating just whitespace into the :first-letter.
229         unsigned numCodeUnits = 0;
230         for (unsigned scanLength = length; scanLength < oldText.length(); scanLength += numCodeUnits) {
231             UChar32 c = oldText.characterStartingAt(scanLength);
232
233             if (!shouldSkipForFirstLetter(c))
234                 break;
235
236             numCodeUnits = numCodeUnitsInGraphemeClusters(StringView(oldText).substring(scanLength), 1);
237
238             if (isPunctuationForFirstLetter(c))
239                 length = scanLength + numCodeUnits;
240         }
241
242         auto* textNode = currentTextChild.textNode();
243         auto* beforeChild = currentTextChild.nextSibling();
244         firstLetterContainer->removeAndDestroyChild(m_builder, currentTextChild);
245
246         // Construct a text fragment for the text after the first letter.
247         // This text fragment might be empty.
248         RenderPtr<RenderTextFragment> newRemainingText;
249         if (textNode) {
250             newRemainingText = createRenderer<RenderTextFragment>(*textNode, oldText, length, oldText.length() - length);
251             textNode->setRenderer(newRemainingText.get());
252         } else
253             newRemainingText = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, length, oldText.length() - length);
254
255         RenderTextFragment& remainingText = *newRemainingText;
256         m_builder.insertChild(*firstLetterContainer, WTFMove(newRemainingText), beforeChild);
257         remainingText.setFirstLetter(firstLetter);
258         firstLetter.setFirstLetterRemainingText(remainingText);
259
260         // construct text fragment for the first letter
261         auto letter = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, 0, length);
262
263         m_builder.insertChild(firstLetter, WTFMove(letter));
264     }
265 }
266
267 };