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(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" |
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(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 | |
154 | void RenderTreeBuilder::FirstLetter::cleanupOnDestroy(RenderTextFragment& textFragment) |
155 | { |
156 | if (!textFragment.firstLetter()) |
157 | return; |
158 | m_builder.destroy(*textFragment.firstLetter()); |
159 | } |
160 | |
161 | void 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 | |
203 | void 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 | |