| /* |
| * Copyright (C) 1999 Lars Knoll (knoll@kde.org) |
| * (C) 1999 Antti Koivisto (koivisto@kde.org) |
| * (C) 2007 David Smith (catfish.man@gmail.com) |
| * Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011 Apple Inc. |
| * All rights reserved. |
| * Copyright (C) Research In Motion Limited 2010. All rights reserved. |
| * |
| * This library is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU Library General Public |
| * License as published by the Free Software Foundation; either |
| * version 2 of the License, or (at your option) any later version. |
| * |
| * This library is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| * Library General Public License for more details. |
| * |
| * You should have received a copy of the GNU Library General Public License |
| * along with this library; see the file COPYING.LIB. If not, write to |
| * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| * Boston, MA 02110-1301, USA. |
| */ |
| |
| #include "core/dom/FirstLetterPseudoElement.h" |
| |
| #include "core/dom/Element.h" |
| #include "core/dom/StyleChangeReason.h" |
| #include "core/layout/GeneratedChildren.h" |
| #include "core/layout/LayoutObject.h" |
| #include "core/layout/LayoutObjectInlines.h" |
| #include "core/layout/LayoutText.h" |
| #include "core/layout/LayoutTextFragment.h" |
| #include "core/layout/api/LayoutTextFragmentItem.h" |
| #include "wtf/text/WTFString.h" |
| #include "wtf/text/icu/UnicodeIcu.h" |
| |
| namespace blink { |
| |
| using namespace WTF; |
| using namespace Unicode; |
| |
| // CSS 2.1 http://www.w3.org/TR/CSS21/selector.html#first-letter "Punctuation |
| // (i.e, characters defined in Unicode [UNICODE] in the "open" (Ps), "close" |
| // (Pe), "initial" (Pi). "final" (Pf) and "other" (Po) punctuation classes), |
| // that precedes or follows the first letter should be included" |
| static inline bool isPunctuationForFirstLetter(UChar c) { |
| CharCategory charCategory = category(c); |
| return charCategory == Punctuation_Open || |
| charCategory == Punctuation_Close || |
| charCategory == Punctuation_InitialQuote || |
| charCategory == Punctuation_FinalQuote || |
| charCategory == Punctuation_Other; |
| } |
| |
| static inline bool isSpaceForFirstLetter(UChar c) { |
| return isSpaceOrNewline(c) || c == noBreakSpaceCharacter; |
| } |
| |
| unsigned FirstLetterPseudoElement::firstLetterLength(const String& text) { |
| unsigned length = 0; |
| unsigned textLength = text.length(); |
| |
| if (textLength == 0) |
| return length; |
| |
| // Account for leading spaces first. |
| while (length < textLength && isSpaceForFirstLetter(text[length])) |
| length++; |
| // Now account for leading punctuation. |
| while (length < textLength && isPunctuationForFirstLetter(text[length])) |
| length++; |
| |
| // Bail if we didn't find a letter before the end of the text or before a |
| // space. |
| if (isSpaceForFirstLetter(text[length]) || length == textLength) |
| return 0; |
| |
| // Account the next character for first letter. |
| length++; |
| |
| // Keep looking for allowed punctuation for the :first-letter. |
| for (; length < textLength; ++length) { |
| UChar c = text[length]; |
| if (!isPunctuationForFirstLetter(c)) |
| break; |
| } |
| return length; |
| } |
| |
| // Once we see any of these layoutObjects we can stop looking for first-letter |
| // as they signal the end of the first line of text. |
| static bool isInvalidFirstLetterLayoutObject(const LayoutObject* obj) { |
| return (obj->isBR() || (obj->isText() && toLayoutText(obj)->isWordBreak())); |
| } |
| |
| LayoutObject* FirstLetterPseudoElement::firstLetterTextLayoutObject( |
| const Element& element) { |
| LayoutObject* parentLayoutObject = 0; |
| |
| // If we are looking at a first letter element then we need to find the |
| // first letter text layoutObject from the parent node, and not ourselves. |
| if (element.isFirstLetterPseudoElement()) |
| parentLayoutObject = element.parentOrShadowHostElement()->layoutObject(); |
| else |
| parentLayoutObject = element.layoutObject(); |
| |
| if (!parentLayoutObject || |
| !parentLayoutObject->style()->hasPseudoStyle(PseudoIdFirstLetter) || |
| !canHaveGeneratedChildren(*parentLayoutObject) || |
| !parentLayoutObject->behavesLikeBlockContainer()) |
| return nullptr; |
| |
| // Drill down into our children and look for our first text child. |
| LayoutObject* firstLetterTextLayoutObject = |
| parentLayoutObject->slowFirstChild(); |
| while (firstLetterTextLayoutObject) { |
| // This can be called when the first letter layoutObject is already in the |
| // tree. We do not want to consider that layoutObject for our text |
| // layoutObject so we go to the sibling (which is the LayoutTextFragment for |
| // the remaining text). |
| if (firstLetterTextLayoutObject->style() && |
| firstLetterTextLayoutObject->style()->styleType() == |
| PseudoIdFirstLetter) { |
| firstLetterTextLayoutObject = firstLetterTextLayoutObject->nextSibling(); |
| } else if (firstLetterTextLayoutObject->isText()) { |
| // FIXME: If there is leading punctuation in a different LayoutText than |
| // the first letter, we'll not apply the correct style to it. |
| RefPtr<StringImpl> str = |
| toLayoutText(firstLetterTextLayoutObject)->isTextFragment() |
| ? toLayoutTextFragment(firstLetterTextLayoutObject) |
| ->completeText() |
| : toLayoutText(firstLetterTextLayoutObject)->originalText(); |
| if (firstLetterLength(str.get()) || |
| isInvalidFirstLetterLayoutObject(firstLetterTextLayoutObject)) |
| break; |
| firstLetterTextLayoutObject = firstLetterTextLayoutObject->nextSibling(); |
| } else if (firstLetterTextLayoutObject->isListMarker()) { |
| firstLetterTextLayoutObject = firstLetterTextLayoutObject->nextSibling(); |
| } else if (firstLetterTextLayoutObject->isFloatingOrOutOfFlowPositioned()) { |
| if (firstLetterTextLayoutObject->style()->styleType() == |
| PseudoIdFirstLetter) { |
| firstLetterTextLayoutObject = |
| firstLetterTextLayoutObject->slowFirstChild(); |
| break; |
| } |
| firstLetterTextLayoutObject = firstLetterTextLayoutObject->nextSibling(); |
| } else if (firstLetterTextLayoutObject->isAtomicInlineLevel() || |
| firstLetterTextLayoutObject->isLayoutButton() || |
| firstLetterTextLayoutObject->isMenuList()) { |
| return nullptr; |
| } else if (firstLetterTextLayoutObject |
| ->isFlexibleBoxIncludingDeprecated() || |
| firstLetterTextLayoutObject->isLayoutGrid()) { |
| firstLetterTextLayoutObject = firstLetterTextLayoutObject->nextSibling(); |
| } else if (!firstLetterTextLayoutObject->isInline() && |
| firstLetterTextLayoutObject->style()->hasPseudoStyle( |
| PseudoIdFirstLetter) && |
| canHaveGeneratedChildren(*firstLetterTextLayoutObject)) { |
| // There is a layoutObject further down the tree which has |
| // PseudoIdFirstLetter set. When that node is attached we will handle |
| // setting up the first letter then. |
| return nullptr; |
| } else { |
| firstLetterTextLayoutObject = |
| firstLetterTextLayoutObject->slowFirstChild(); |
| } |
| } |
| |
| // No first letter text to display, we're done. |
| // FIXME: This black-list of disallowed LayoutText subclasses is fragile. |
| // crbug.com/422336. |
| // Should counter be on this list? What about LayoutTextFragment? |
| if (!firstLetterTextLayoutObject || !firstLetterTextLayoutObject->isText() || |
| isInvalidFirstLetterLayoutObject(firstLetterTextLayoutObject)) |
| return nullptr; |
| |
| return firstLetterTextLayoutObject; |
| } |
| |
| FirstLetterPseudoElement::FirstLetterPseudoElement(Element* parent) |
| : PseudoElement(parent, PseudoIdFirstLetter), |
| m_remainingTextLayoutObject(nullptr) {} |
| |
| FirstLetterPseudoElement::~FirstLetterPseudoElement() { |
| DCHECK(!m_remainingTextLayoutObject); |
| } |
| |
| void FirstLetterPseudoElement::updateTextFragments() { |
| String oldText = m_remainingTextLayoutObject->completeText(); |
| DCHECK(oldText.impl()); |
| |
| unsigned length = FirstLetterPseudoElement::firstLetterLength(oldText); |
| m_remainingTextLayoutObject->setTextFragment( |
| oldText.impl()->substring(length, oldText.length()), length, |
| oldText.length() - length); |
| m_remainingTextLayoutObject->dirtyLineBoxes(); |
| |
| for (auto child = layoutObject()->slowFirstChild(); child; |
| child = child->nextSibling()) { |
| if (!child->isText() || !toLayoutText(child)->isTextFragment()) |
| continue; |
| LayoutTextFragmentItem childFragment = |
| LayoutTextFragmentItem(toLayoutTextFragment(child)); |
| if (childFragment.firstLetterPseudoElement() != this) |
| continue; |
| |
| childFragment.setTextFragment(oldText.impl()->substring(0, length), 0, |
| length); |
| childFragment.dirtyLineBoxes(); |
| |
| // Make sure the first-letter layoutObject is set to require a layout as it |
| // needs to re-create the line boxes. The remaining text layoutObject |
| // will be marked by the LayoutText::setText. |
| childFragment.setNeedsLayoutAndPrefWidthsRecalc( |
| LayoutInvalidationReason::TextChanged); |
| break; |
| } |
| } |
| |
| void FirstLetterPseudoElement::setRemainingTextLayoutObject( |
| LayoutTextFragment* fragment) { |
| // The text fragment we get our content from is being destroyed. We need |
| // to tell our parent element to recalcStyle so we can get cleaned up |
| // as well. |
| if (!fragment) |
| setNeedsStyleRecalc(LocalStyleChange, StyleChangeReasonForTracing::create( |
| StyleChangeReason::PseudoClass)); |
| |
| m_remainingTextLayoutObject = fragment; |
| } |
| |
| void FirstLetterPseudoElement::attachLayoutTree(const AttachContext& context) { |
| PseudoElement::attachLayoutTree(context); |
| attachFirstLetterTextLayoutObjects(); |
| } |
| |
| void FirstLetterPseudoElement::detachLayoutTree(const AttachContext& context) { |
| if (m_remainingTextLayoutObject) { |
| if (m_remainingTextLayoutObject->node() && document().isActive()) { |
| Text* textNode = toText(m_remainingTextLayoutObject->node()); |
| m_remainingTextLayoutObject->setTextFragment( |
| textNode->dataImpl(), 0, textNode->dataImpl()->length()); |
| } |
| m_remainingTextLayoutObject->setFirstLetterPseudoElement(nullptr); |
| m_remainingTextLayoutObject->setIsRemainingTextLayoutObject(false); |
| } |
| m_remainingTextLayoutObject = nullptr; |
| |
| PseudoElement::detachLayoutTree(context); |
| } |
| |
| ComputedStyle* FirstLetterPseudoElement::styleForFirstLetter( |
| LayoutObject* layoutObjectContainer) { |
| DCHECK(layoutObjectContainer); |
| |
| LayoutObject* styleContainer = parentOrShadowHostElement()->layoutObject(); |
| DCHECK(styleContainer); |
| |
| // We always force the pseudo style to recompute as the first-letter style |
| // computed by the style container may not have taken the layoutObjects styles |
| // into account. |
| styleContainer->mutableStyle()->removeCachedPseudoStyle(PseudoIdFirstLetter); |
| |
| ComputedStyle* pseudoStyle = styleContainer->getCachedPseudoStyle( |
| PseudoIdFirstLetter, layoutObjectContainer->firstLineStyle()); |
| DCHECK(pseudoStyle); |
| |
| return pseudoStyle; |
| } |
| |
| void FirstLetterPseudoElement::attachFirstLetterTextLayoutObjects() { |
| LayoutObject* nextLayoutObject = |
| FirstLetterPseudoElement::firstLetterTextLayoutObject(*this); |
| DCHECK(nextLayoutObject); |
| DCHECK(nextLayoutObject->isText()); |
| |
| // The original string is going to be either a generated content string or a |
| // DOM node's string. We want the original string before it got transformed in |
| // case first-letter has no text-transform or a different text-transform |
| // applied to it. |
| String oldText = toLayoutText(nextLayoutObject)->isTextFragment() |
| ? toLayoutTextFragment(nextLayoutObject)->completeText() |
| : toLayoutText(nextLayoutObject)->originalText(); |
| DCHECK(oldText.impl()); |
| |
| ComputedStyle* pseudoStyle = styleForFirstLetter(nextLayoutObject->parent()); |
| layoutObject()->setStyle(pseudoStyle); |
| |
| // FIXME: This would already have been calculated in firstLetterLayoutObject. |
| // Can we pass the length through? |
| unsigned length = FirstLetterPseudoElement::firstLetterLength(oldText); |
| |
| // Construct a text fragment for the text after the first letter. |
| // This text fragment might be empty. |
| LayoutTextFragment* remainingText = new LayoutTextFragment( |
| nextLayoutObject->node() ? nextLayoutObject->node() |
| : &nextLayoutObject->document(), |
| oldText.impl(), length, oldText.length() - length); |
| remainingText->setFirstLetterPseudoElement(this); |
| remainingText->setIsRemainingTextLayoutObject(true); |
| remainingText->setStyle(nextLayoutObject->mutableStyle()); |
| |
| if (remainingText->node()) |
| remainingText->node()->setLayoutObject(remainingText); |
| |
| m_remainingTextLayoutObject = remainingText; |
| |
| LayoutObject* nextSibling = layoutObject()->nextSibling(); |
| layoutObject()->parent()->addChild(remainingText, nextSibling); |
| |
| // Construct text fragment for the first letter. |
| LayoutTextFragment* letter = new LayoutTextFragment( |
| &nextLayoutObject->document(), oldText.impl(), 0, length); |
| letter->setFirstLetterPseudoElement(this); |
| letter->setStyle(pseudoStyle); |
| layoutObject()->addChild(letter); |
| |
| nextLayoutObject->destroy(); |
| } |
| |
| void FirstLetterPseudoElement::didRecalcStyle(StyleRecalcChange) { |
| if (!layoutObject()) |
| return; |
| |
| // The layoutObjects inside pseudo elements are anonymous so they don't get |
| // notified of recalcStyle and must have |
| // the style propagated downward manually similar to |
| // LayoutObject::propagateStyleToAnonymousChildren. |
| LayoutObject* layoutObject = this->layoutObject(); |
| for (LayoutObject* child = layoutObject->nextInPreOrder(layoutObject); child; |
| child = child->nextInPreOrder(layoutObject)) { |
| // We need to re-calculate the correct style for the first letter element |
| // and then apply that to the container and the text fragment inside. |
| if (child->style()->styleType() == PseudoIdFirstLetter && |
| m_remainingTextLayoutObject) { |
| if (ComputedStyle* pseudoStyle = |
| styleForFirstLetter(m_remainingTextLayoutObject->parent())) |
| child->setPseudoStyle(pseudoStyle); |
| continue; |
| } |
| |
| // We only manage the style for the generated content items. |
| if (!child->isText() && !child->isQuote() && !child->isImage()) |
| continue; |
| |
| child->setPseudoStyle(layoutObject->mutableStyle()); |
| } |
| } |
| |
| } // namespace blink |