| 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 | |