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
38namespace WebCore {
39
40static RenderStyle styleForFirstLetter(const RenderBlock& firstLetterBlock, const RenderObject& firstLetterContainer)
41{
42 auto* containerFirstLetterStyle = firstLetterBlock.getCachedPseudoStyle(PseudoId::FirstLetter, &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() ? Float::Left : Float::Right);
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(WTFMove(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(WTFMove(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() ? DisplayType::Block : DisplayType::Inline);
86 // CSS2 says first-letter can't be positioned.
87 firstLetterStyle.setPosition(PositionType::Static);
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"
94static 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
99static inline bool shouldSkipForFirstLetter(UChar32 c)
100{
101 return isSpaceOrNewline(c) || c == noBreakSpace || isPunctuationForFirstLetter(c);
102}
103
104static 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
117RenderTreeBuilder::FirstLetter::FirstLetter(RenderTreeBuilder& builder)
118 : m_builder(builder)
119{
120}
121
122void RenderTreeBuilder::FirstLetter::updateAfterDescendants(RenderBlock& block)
123{
124 if (!block.style().hasPseudoStyle(PseudoId::FirstLetter))
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() == PseudoId::FirstLetter) {
144 updateStyle(block, *firstLetterRenderer);
145 return;
146 }
147
148 if (!is<RenderText>(firstLetterRenderer))
149 return;
150
151 createRenderers(block, downcast<RenderText>(*firstLetterRenderer));
152}
153
154void RenderTreeBuilder::FirstLetter::cleanupOnDestroy(RenderTextFragment& textFragment)
155{
156 if (!textFragment.firstLetter())
157 return;
158 m_builder.destroy(*textFragment.firstLetter());
159}
160
161void RenderTreeBuilder::FirstLetter::updateStyle(RenderBlock& firstLetterBlock, RenderObject& currentChild)
162{
163 RenderElement* firstLetter = currentChild.parent();
164 ASSERT(firstLetter->isFirstLetter());
165
166 RenderElement* firstLetterContainer = firstLetter->parent();
167 auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
168 ASSERT(firstLetter->isFloating() || firstLetter->isInline());
169
170 if (Style::determineChange(firstLetter->style(), pseudoStyle) == Style::Detach) {
171 // The first-letter renderer needs to be replaced. Create a new renderer of the right type.
172 RenderPtr<RenderBoxModelObject> newFirstLetter;
173 if (pseudoStyle.display() == DisplayType::Inline)
174 newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
175 else
176 newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
177 newFirstLetter->initializeStyle();
178 newFirstLetter->setIsFirstLetter();
179
180 // Move the first letter into the new renderer.
181 while (RenderObject* child = firstLetter->firstChild()) {
182 if (is<RenderText>(*child))
183 downcast<RenderText>(*child).removeAndDestroyTextBoxes();
184 auto toMove = m_builder.detach(*firstLetter, *child);
185 m_builder.attach(*newFirstLetter, WTFMove(toMove));
186 }
187
188 RenderObject* nextSibling = firstLetter->nextSibling();
189 if (RenderTextFragment* remainingText = downcast<RenderBoxModelObject>(*firstLetter).firstLetterRemainingText()) {
190 ASSERT(remainingText->isAnonymous() || remainingText->textNode()->renderer() == remainingText);
191 // Replace the old renderer with the new one.
192 remainingText->setFirstLetter(*newFirstLetter);
193 newFirstLetter->setFirstLetterRemainingText(*remainingText);
194 }
195 m_builder.destroy(*firstLetter);
196 m_builder.attach(*firstLetterContainer, WTFMove(newFirstLetter), nextSibling);
197 return;
198 }
199
200 firstLetter->setStyle(WTFMove(pseudoStyle));
201}
202
203void RenderTreeBuilder::FirstLetter::createRenderers(RenderBlock& firstLetterBlock, RenderText& currentTextChild)
204{
205 RenderElement* textContentParent = currentTextChild.parent();
206 RenderElement* firstLetterContainer = nullptr;
207 if (auto* wrapperInlineForDisplayContents = currentTextChild.inlineWrapperForDisplayContents())
208 firstLetterContainer = wrapperInlineForDisplayContents->parent();
209 else
210 firstLetterContainer = textContentParent;
211 auto pseudoStyle = styleForFirstLetter(firstLetterBlock, *firstLetterContainer);
212 RenderPtr<RenderBoxModelObject> newFirstLetter;
213 if (pseudoStyle.display() == DisplayType::Inline)
214 newFirstLetter = createRenderer<RenderInline>(firstLetterBlock.document(), WTFMove(pseudoStyle));
215 else
216 newFirstLetter = createRenderer<RenderBlockFlow>(firstLetterBlock.document(), WTFMove(pseudoStyle));
217 newFirstLetter->initializeStyle();
218 newFirstLetter->setIsFirstLetter();
219
220 // The original string is going to be either a generated content string or a DOM node's
221 // string. We want the original string before it got transformed in case first-letter has
222 // no text-transform or a different text-transform applied to it.
223 String oldText = currentTextChild.originalText();
224 ASSERT(!oldText.isNull());
225
226 if (!oldText.isEmpty()) {
227 unsigned length = 0;
228
229 // Account for leading spaces and punctuation.
230 while (length < oldText.length() && shouldSkipForFirstLetter(oldText.characterStartingAt(length)))
231 length += numCodeUnitsInGraphemeClusters(StringView(oldText).substring(length), 1);
232
233 // Account for first grapheme cluster.
234 length += numCodeUnitsInGraphemeClusters(StringView(oldText).substring(length), 1);
235
236 // Keep looking for whitespace and allowed punctuation, but avoid
237 // accumulating just whitespace into the :first-letter.
238 unsigned numCodeUnits = 0;
239 for (unsigned scanLength = length; scanLength < oldText.length(); scanLength += numCodeUnits) {
240 UChar32 c = oldText.characterStartingAt(scanLength);
241
242 if (!shouldSkipForFirstLetter(c))
243 break;
244
245 numCodeUnits = numCodeUnitsInGraphemeClusters(StringView(oldText).substring(scanLength), 1);
246
247 if (isPunctuationForFirstLetter(c))
248 length = scanLength + numCodeUnits;
249 }
250
251 auto* textNode = currentTextChild.textNode();
252 auto* beforeChild = currentTextChild.nextSibling();
253 auto inlineWrapperForDisplayContents = makeWeakPtr(currentTextChild.inlineWrapperForDisplayContents());
254 auto hasInlineWrapperForDisplayContents = inlineWrapperForDisplayContents.get();
255 m_builder.destroy(currentTextChild);
256
257 // Construct a text fragment for the text after the first letter.
258 // This text fragment might be empty.
259 RenderPtr<RenderTextFragment> newRemainingText;
260 if (textNode) {
261 newRemainingText = createRenderer<RenderTextFragment>(*textNode, oldText, length, oldText.length() - length);
262 textNode->setRenderer(newRemainingText.get());
263 } else
264 newRemainingText = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, length, oldText.length() - length);
265
266 RenderTextFragment& remainingText = *newRemainingText;
267 ASSERT_UNUSED(hasInlineWrapperForDisplayContents, hasInlineWrapperForDisplayContents == inlineWrapperForDisplayContents.get());
268 remainingText.setInlineWrapperForDisplayContents(inlineWrapperForDisplayContents.get());
269 m_builder.attach(*textContentParent, WTFMove(newRemainingText), beforeChild);
270
271 // FIXME: Make attach the final step so that we don't need to keep firstLetter around.
272 auto& firstLetter = *newFirstLetter;
273 remainingText.setFirstLetter(firstLetter);
274 firstLetter.setFirstLetterRemainingText(remainingText);
275 m_builder.attach(*firstLetterContainer, WTFMove(newFirstLetter), &remainingText);
276
277 // Construct text fragment for the first letter.
278 auto letter = createRenderer<RenderTextFragment>(firstLetterBlock.document(), oldText, 0, length);
279 m_builder.attach(firstLetter, WTFMove(letter));
280 }
281}
282
283};
284