blob: 5a58dc5c24cdab58293b732c1c954dcef2c7c5ce [file]
/*
* Copyright (C) 2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "ChildChangeInvalidation.h"
#include "ElementTraversal.h"
#include "NodeRenderStyle.h"
#include "PseudoClassChangeInvalidation.h"
#include "RenderElement.h"
#include "ShadowRoot.h"
#include "SlotAssignment.h"
#include "StyleComputedStyle+GettersInlines.h"
#include "StyleResolver.h"
#include "StyleScopeRuleSets.h"
#include "TypedElementDescendantIteratorInlines.h"
namespace WebCore::Style {
static bool elementIsEmptyForCSS(const Element& element)
{
for (auto* node = element.firstChild(); node; node = node->nextSibling()) {
if (is<Element>(*node))
return false;
if (auto* textNode = dynamicDowncast<Text>(*node)) {
if (!textNode->data().isEmpty())
return false;
}
}
return true;
}
bool ChildChangeInvalidation::emptyStateMayChange() const
{
// CharacterData::makeChildChange uses TextChanged only when both the old and new data are non-empty,
// so the parent's :empty state can't flip and we can skip the traversal below.
if (m_childChange.type == ContainerNode::ChildChange::Type::TextChanged)
return false;
bool wasEmpty = elementIsEmptyForCSS(*m_parentElement);
return m_childChange.isInsertion() == wasEmpty;
}
static bool isSiblingHasRelation(const MatchElement& matchElement)
{
if (!matchElement.hasRelation)
return false;
switch (*matchElement.hasRelation) {
case MatchElement::HasRelation::DirectSibling:
case MatchElement::HasRelation::IndirectSibling:
case MatchElement::HasRelation::SiblingChild:
case MatchElement::HasRelation::SiblingDescendant:
return true;
case MatchElement::HasRelation::Child:
case MatchElement::HasRelation::Descendant:
case MatchElement::HasRelation::HostDescendant:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
void ChildChangeInvalidation::invalidateForChangedElement(Element& changedElement, MatchingHasSelectors& matchingHasSelectors, ChangedElementRelation changedElementRelation)
{
Ref resolver = parentElement().styleResolver();
auto& ruleSets = resolver->ruleSets();
Invalidator::MatchElementRuleSets matchElementRuleSets;
bool isChild = changedElement.parentElement() == &parentElement();
auto canAffectElementsWithStyle = [&](const InvalidationRuleSet& ruleSet) {
if (!ruleSet.matchElement.hasRelation)
return true;
switch (*ruleSet.matchElement.hasRelation) {
case MatchElement::HasRelation::Child:
case MatchElement::HasRelation::DirectSibling:
case MatchElement::HasRelation::IndirectSibling:
return isChild;
case MatchElement::HasRelation::Descendant:
case MatchElement::HasRelation::SiblingChild:
case MatchElement::HasRelation::SiblingDescendant:
case MatchElement::HasRelation::HostDescendant:
return true;
default:
return true;
}
};
auto hasAlreadyMatchedAndMutationIsIrrelevant = [&](const InvalidationRuleSet& invalidationRuleSet) {
// Check if a pre-existing neighbor already matches the :has() argument. If so, adding/removing one
// more matching element doesn't change the :has() result, so no invalidation is needed.
// This doesn't apply inside :not() (inverted logic) or for sibling :has() arguments (direction matters).
if (!isChild)
return false;
if (invalidationRuleSet.isNegation == IsNegation::Yes)
return false;
if (isSiblingHasRelation(invalidationRuleSet.matchElement))
return false;
switch (changedElementRelation) {
case ChangedElementRelation::SelfOrDescendant:
// The changed element must be exactly at the mutation point so a sibling probe reflects pre-mutation state.
return m_childChange.previousSiblingElement == changedElement.previousElementSibling();
case ChangedElementRelation::Sibling:
// The changed element is itself a pre-existing neighbor. Only safe when the argument is a purely
// structural sibling combinator (so an element's match depends only on itself and preceding siblings)
// and the mutation is at the end of the element list, so every visited (preceding) sibling's match is
// unaffected by it — meaning "it matches now" == "it matched before the mutation".
return invalidationRuleSet.hasArgumentProperties.contains(HasArgumentProperty::StructuralSibling) && !m_childChange.nextSiblingElement;
case ChangedElementRelation::FirstOrLastChild:
return false;
}
ASSERT_NOT_REACHED();
return false;
};
auto hasMatchingInvalidationSelector = [&](auto& invalidationRuleSet) {
SelectorChecker selectorChecker(changedElement.document());
SelectorChecker::CheckingContext checkingContext(SelectorChecker::Mode::StyleInvalidation);
checkingContext.matchesAllHasScopes = true;
for (auto& selector : invalidationRuleSet.invalidationSelectors) {
if (hasAlreadyMatchedAndMutationIsIrrelevant(invalidationRuleSet)) {
// FIXME: We could cache this state across invalidations instead of just testing a single sibling.
// For sibling visits the changed element is itself the pre-existing neighbor to probe.
RefPtr<Element> sibling;
if (changedElementRelation == ChangedElementRelation::Sibling)
sibling = &changedElement;
else
sibling = m_childChange.previousSiblingElement ? m_childChange.previousSiblingElement : m_childChange.nextSiblingElement;
if (sibling && selectorChecker.match(selector, *sibling, checkingContext)) {
matchingHasSelectors.add(&selector);
continue;
}
}
if (matchingHasSelectors.contains(&selector))
continue;
if (selectorChecker.match(selector, changedElement, checkingContext)) {
matchingHasSelectors.add(&selector);
return true;
}
}
return false;
};
auto addHasInvalidation = [&](const Vector<InvalidationRuleSet>* invalidationRuleSets) {
if (!invalidationRuleSets)
return;
for (auto& invalidationRuleSet : *invalidationRuleSets) {
if (!canAffectElementsWithStyle(invalidationRuleSet))
continue;
// Order-insensitive :has() arguments can only change when the changed element's own subtree changes,
// which the SelfOrDescendant traversal already covers. Re-evaluating them per sibling is redundant
// (and re-walks the bearer subtree on every mutation), so skip them on sibling visits.
if (changedElementRelation == ChangedElementRelation::Sibling && !invalidationRuleSet.hasArgumentProperties.contains(HasArgumentProperty::OrderSensitive))
continue;
if (!hasMatchingInvalidationSelector(invalidationRuleSet))
continue;
Invalidator::addToMatchElementRuleSetsRespectingNegation(matchElementRuleSets, invalidationRuleSet);
}
};
for (auto key : makePseudoClassInvalidationKeys(CSSSelector::PseudoClass::Has, changedElement))
addHasInvalidation(ruleSets.hasPseudoClassInvalidationRuleSets(key));
Invalidator::invalidateWithMatchElementRuleSets(changedElement, matchElementRuleSets);
}
void ChildChangeInvalidation::invalidateForHasSiblings(MatchingHasSelectors& matchingHasSelectors, MutationPhase phase)
{
bool affectedByBackwardSibling = parentElement().affectedByHasWithBackwardSiblingRelationship();
bool affectedByForwardSibling = parentElement().affectedByHasWithForwardSiblingRelationship();
bool affectedByAdjacentSibling = parentElement().affectedByHasWithAdjacentSiblingRelationship();
auto invalidateSibling = [&](auto& changedElement) {
invalidateForChangedElement(changedElement, matchingHasSelectors, ChangedElementRelation::Sibling);
};
if (affectedByBackwardSibling || affectedByAdjacentSibling) {
for (RefPtr child = m_childChange.previousSiblingElement; child; child = child->previousElementSibling()) {
invalidateSibling(*child);
if (!affectedByBackwardSibling)
break;
}
}
if (affectedByForwardSibling || affectedByAdjacentSibling) {
for (RefPtr child = m_childChange.nextSiblingElement; child; child = child->nextElementSibling()) {
invalidateSibling(*child);
if (!affectedByForwardSibling)
break;
}
}
// For insertion, the pre-mutation :first/:last-child state of the neighbor will stop matching.
// For removal, the post-mutation state of the neighbor will start matching.
bool checkNow = phase == MutationPhase::Before ? m_childChange.isInsertion() : !m_childChange.isInsertion();
if (!checkNow)
return;
if (RefPtr next = m_childChange.nextSiblingElement; next && parentElement().childrenAffectedByFirstChildRules() && !next->previousElementSibling())
invalidateForChangedElement(*next, matchingHasSelectors, ChangedElementRelation::FirstOrLastChild);
if (RefPtr previous = m_childChange.previousSiblingElement; previous && parentElement().childrenAffectedByLastChildRules() && !previous->nextElementSibling())
invalidateForChangedElement(*previous, matchingHasSelectors, ChangedElementRelation::FirstOrLastChild);
}
void ChildChangeInvalidation::invalidateForHasBeforeMutation()
{
ASSERT(m_needsHasInvalidation);
MatchingHasSelectors matchingHasSelectors;
traverseRemovedElements([&](auto& changedElement) {
invalidateForChangedElement(changedElement, matchingHasSelectors, ChangedElementRelation::SelfOrDescendant);
});
invalidateForHasSiblings(matchingHasSelectors, MutationPhase::Before);
}
void ChildChangeInvalidation::invalidateForHasAfterMutation()
{
ASSERT(m_needsHasInvalidation);
MatchingHasSelectors matchingHasSelectors;
traverseAddedElements([&](auto& changedElement) {
invalidateForChangedElement(changedElement, matchingHasSelectors, ChangedElementRelation::SelfOrDescendant);
});
invalidateForHasSiblings(matchingHasSelectors, MutationPhase::After);
}
static bool NODELETE needsDescendantTraversal(const RuleFeatureSet& features)
{
// With the bundled MatchElement representation, any :has() with hasRelation=Descendant or SiblingDescendant
// needs descendant traversal. Since we don't have per-value tracking for hasRelation,
// use the usesHasPseudoClass flag as a conservative check.
return features.usesHasPseudoClass;
};
template<typename Function>
void ChildChangeInvalidation::traverseRemovedElements(Function&& function)
{
if (m_childChange.isInsertion() && m_childChange.type != ContainerNode::ChildChange::Type::AllChildrenReplaced)
return;
Ref resolver = parentElement().styleResolver();
auto& features = resolver->ruleSets().features();
bool needsDescendantTraversal = Style::needsDescendantTraversal(features);
RefPtr firstToRemove = m_childChange.previousSiblingElement ? m_childChange.previousSiblingElement->nextElementSibling() : parentElement().firstElementChild();
for (RefPtr toRemove = firstToRemove; toRemove != m_childChange.nextSiblingElement; toRemove = toRemove->nextElementSibling()) {
function(*toRemove);
if (!needsDescendantTraversal)
continue;
for (Ref descendant : descendantsOfType<Element>(*toRemove))
function(descendant);
}
}
template<typename Function>
void ChildChangeInvalidation::traverseAddedElements(Function&& function)
{
if (!m_childChange.isInsertion())
return;
auto callFunctionOnInclusiveDescendants = [&](Element& element) {
function(element);
Ref resolver = parentElement().styleResolver();
auto& features = resolver->ruleSets().features();
if (!needsDescendantTraversal(features))
return;
for (Ref descendant : descendantsOfType<Element>(element))
function(descendant);
};
if (RefPtr newElement = m_childChange.siblingChanged)
callFunctionOnInclusiveDescendants(*newElement);
else if (auto* children = m_childChange.insertedChildren) {
for (Ref node : *children) {
if (auto* element = dynamicDowncast<Element>(node.get()))
callFunctionOnInclusiveDescendants(*element);
}
}
}
static void invalidateForSiblingCombinators(Element* sibling)
{
for (RefPtr element = sibling; element; element = element->nextElementSibling()) {
if (element->styleIsAffectedByPreviousSibling())
element->invalidateStyle();
if (element->descendantsAffectedByPreviousSibling()) {
for (RefPtr siblingChild = element->firstElementChild(); siblingChild; siblingChild = siblingChild->nextElementSibling())
siblingChild->invalidateStyleForSubtree();
}
if (!element->affectsNextSiblingElementStyle())
return;
}
}
static void invalidateForForwardPositionalRules(Element& parent, Element* elementAfterChange)
{
bool childrenAffected = parent.childrenAffectedByForwardPositionalRules();
bool descendantsAffected = parent.descendantsAffectedByForwardPositionalRules();
if (!childrenAffected && !descendantsAffected)
return;
for (RefPtr sibling = elementAfterChange; sibling; sibling = sibling->nextElementSibling()) {
if (childrenAffected)
sibling->invalidateStyle();
if (descendantsAffected) {
for (RefPtr siblingChild = sibling->firstElementChild(); siblingChild; siblingChild = siblingChild->nextElementSibling())
siblingChild->invalidateStyleForSubtree();
}
}
}
static void invalidateForBackwardPositionalRules(Element& parent, Element* elementBeforeChange)
{
bool childrenAffected = parent.childrenAffectedByBackwardPositionalRules();
bool descendantsAffected = parent.descendantsAffectedByBackwardPositionalRules();
if (!childrenAffected && !descendantsAffected)
return;
for (RefPtr sibling = elementBeforeChange; sibling; sibling = sibling->previousElementSibling()) {
if (childrenAffected)
sibling->invalidateStyle();
if (descendantsAffected) {
for (RefPtr siblingChild = sibling->firstElementChild(); siblingChild; siblingChild = siblingChild->nextElementSibling())
siblingChild->invalidateStyleForSubtree();
}
}
}
static void invalidateForFirstChildState(Element& child, bool state)
{
auto* style = child.renderStyle();
if (!style || style->firstChildState() == state)
child.invalidateStyleForSubtree();
}
static void invalidateForLastChildState(Element& child, bool state)
{
auto* style = child.renderStyle();
if (!style || style->lastChildState() == state)
child.invalidateStyleForSubtree();
}
void ChildChangeInvalidation::invalidateAfterChange()
{
if (m_childChange.source == ContainerNode::ChildChange::Source::Parser)
return;
checkForSiblingStyleChanges();
}
void ChildChangeInvalidation::invalidateAfterFinishedParsingChildren(Element& parent)
{
if (!parent.needsStyleInvalidation())
return;
RefPtr lastChildElement = ElementTraversal::lastChild(parent);
if (!lastChildElement)
return;
if (parent.childrenAffectedByLastChildRules())
invalidateForLastChildState(*lastChildElement, false);
invalidateForBackwardPositionalRules(parent, lastChildElement.get());
}
void ChildChangeInvalidation::checkForSiblingStyleChanges()
{
Ref parent = parentElement();
RefPtr elementBeforeChange = m_childChange.previousSiblingElement;
RefPtr elementAfterChange = m_childChange.nextSiblingElement;
// :first-child. In the parser callback case, we don't have to check anything, since we were right the first time.
// In the DOM case, we only need to do something if |afterChange| is not 0.
// |afterChange| is 0 in the parser case, so it works out that we'll skip this block.
if (parent->childrenAffectedByFirstChildRules() && elementAfterChange) {
// Find our new first child.
RefPtr<Element> newFirstElement = ElementTraversal::firstChild(parent.get());
// This is the insert/append case.
if (newFirstElement != elementAfterChange)
invalidateForFirstChildState(*elementAfterChange, true);
// We also have to handle node removal.
if (m_childChange.type == ContainerNode::ChildChange::Type::ElementRemoved && newFirstElement == elementAfterChange)
invalidateForFirstChildState(*newFirstElement, false);
}
// :last-child. In the parser callback case, we don't have to check anything, since we were right the first time.
// In the DOM case, we only need to do something if |afterChange| is not 0.
if (parent->childrenAffectedByLastChildRules() && elementBeforeChange) {
// Find our new last child.
RefPtr<Element> newLastElement = ElementTraversal::lastChild(parent.get());
if (newLastElement != elementBeforeChange)
invalidateForLastChildState(*elementBeforeChange, true);
// We also have to handle node removal.
if (m_childChange.type == ContainerNode::ChildChange::Type::ElementRemoved && newLastElement == elementBeforeChange)
invalidateForLastChildState(*newLastElement, false);
}
invalidateForSiblingCombinators(elementAfterChange.get());
invalidateForForwardPositionalRules(parent, elementAfterChange.get());
invalidateForBackwardPositionalRules(parent, elementBeforeChange.get());
}
}