tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit f8ea0f226284bb76ac8fb52806c4cc3a05eb7803
parent f0e0295fbc6abf83ab26b2e781922c51abc8d8bb
Author: Alexandru Marc <amarc@mozilla.com>
Date:   Wed,  3 Dec 2025 17:42:13 +0200

Revert "Bug 1997380: Add test to exercise querySelector bloom filter. r=emilio" for causing multiple failures

This reverts commit 2bf971952649f68eb066e07fc283f96d1c881a51.

Revert "Bug 1997380: Use bloom filter to optimize querySelector attribute matching. r=emilio,firefox-style-system-reviewers"

This reverts commit 91a80a1d9781ee3f3ca84ab833db52c3d813d864.

Revert "Bug 1997380: Implement bloom filter for element attributes. r=smaug,emilio,firefox-style-system-reviewers"

This reverts commit c043d7c313746e7a546b4d43732a0a82681ef376.

Diffstat:
Mdom/base/AttrArray.cpp | 105++++++++++++++++++++++++++++++++-----------------------------------------------
Mdom/base/AttrArray.h | 176+++++++++----------------------------------------------------------------------
Mdom/base/Element.cpp | 180+------------------------------------------------------------------------------
Mdom/base/Element.h | 36------------------------------------
Mdom/base/nsStyledElement.cpp | 3++-
Mdom/svg/SVGElement.cpp | 5-----
Mdom/xul/nsXULElement.cpp | 6++++--
Mlayout/style/GeckoBindings.cpp | 4----
Mlayout/style/GeckoBindings.h | 1-
Mservo/components/style/dom.rs | 48------------------------------------------------
Mservo/components/style/dom_apis.rs | 169++++++++++++++++---------------------------------------------------------------
Mservo/components/style/gecko/wrapper.rs | 17+----------------
Dtesting/web-platform/tests/dom/nodes/querySelector-mixed-case.html | 431-------------------------------------------------------------------------------
13 files changed, 105 insertions(+), 1076 deletions(-)

diff --git a/dom/base/AttrArray.cpp b/dom/base/AttrArray.cpp @@ -35,12 +35,12 @@ AttrArray::Impl::~Impl() { void AttrArray::SetMappedDeclarationBlock( already_AddRefed<mozilla::StyleLockedDeclarationBlock> aBlock) { MOZ_ASSERT(NS_IsMainThread()); - MOZ_ASSERT(HasImpl()); + MOZ_ASSERT(mImpl); MOZ_ASSERT(IsPendingMappedAttributeEvaluation()); if (auto* decl = GetMappedDeclarationBlock()) { Servo_DeclarationBlock_Release(decl); } - GetImpl()->mMappedAttributeBits = reinterpret_cast<uintptr_t>(aBlock.take()); + mImpl->mMappedAttributeBits = reinterpret_cast<uintptr_t>(aBlock.take()); MOZ_ASSERT(!IsPendingMappedAttributeEvaluation()); } @@ -103,13 +103,13 @@ const nsAttrValue* AttrArray::GetAttr(const nsAString& aName, const nsAttrValue* AttrArray::AttrAt(uint32_t aPos) const { NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray"); - return &GetImpl()->Attrs()[aPos].mValue; + return &mImpl->Attrs()[aPos].mValue; } template <typename Name> inline nsresult AttrArray::AddNewAttribute(Name* aName, nsAttrValue& aValue) { - MOZ_ASSERT(!HasImpl() || GetImpl()->mCapacity >= GetImpl()->mAttrCount); - if (!HasImpl() || GetImpl()->mCapacity == GetImpl()->mAttrCount) { + MOZ_ASSERT(!mImpl || mImpl->mCapacity >= mImpl->mAttrCount); + if (!mImpl || mImpl->mCapacity == mImpl->mAttrCount) { if (!GrowBy(1)) { return NS_ERROR_OUT_OF_MEMORY; } @@ -161,29 +161,27 @@ nsresult AttrArray::SetAndSwapAttr(mozilla::dom::NodeInfo* aName, nsresult AttrArray::RemoveAttrAt(uint32_t aPos, nsAttrValue& aValue) { NS_ASSERTION(aPos < AttrCount(), "out-of-bounds"); - Impl* impl = GetImpl(); - impl->mBuffer[aPos].mValue.SwapValueWith(aValue); - impl->mBuffer[aPos].~InternalAttr(); + mImpl->mBuffer[aPos].mValue.SwapValueWith(aValue); + mImpl->mBuffer[aPos].~InternalAttr(); // InternalAttr are not trivially copyable *but* we manually called the // destructor so the memmove should be ok. - memmove((void*)(impl->mBuffer + aPos), impl->mBuffer + aPos + 1, - (impl->mAttrCount - aPos - 1) * sizeof(InternalAttr)); + memmove((void*)(mImpl->mBuffer + aPos), mImpl->mBuffer + aPos + 1, + (mImpl->mAttrCount - aPos - 1) * sizeof(InternalAttr)); - --impl->mAttrCount; + --mImpl->mAttrCount; return NS_OK; } mozilla::dom::BorrowedAttrInfo AttrArray::AttrInfoAt(uint32_t aPos) const { NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray"); - const Impl* impl = GetImpl(); - return BorrowedAttrInfo(&impl->mBuffer[aPos].mName, - &impl->mBuffer[aPos].mValue); + InternalAttr& attr = mImpl->mBuffer[aPos]; + return BorrowedAttrInfo(&attr.mName, &attr.mValue); } const nsAttrName* AttrArray::AttrNameAt(uint32_t aPos) const { NS_ASSERTION(aPos < AttrCount(), "out-of-bounds access in AttrArray"); - return &GetImpl()->mBuffer[aPos].mName; + return &mImpl->mBuffer[aPos].mName; } [[nodiscard]] bool AttrArray::GetSafeAttrNameAt( @@ -191,7 +189,7 @@ const nsAttrName* AttrArray::AttrNameAt(uint32_t aPos) const { if (aPos >= AttrCount()) { return false; } - *aResult = &GetImpl()->mBuffer[aPos].mName; + *aResult = &mImpl->mBuffer[aPos].mName; return true; } @@ -241,36 +239,33 @@ int32_t AttrArray::IndexOfAttr(const nsAtom* aLocalName, } void AttrArray::Compact() { - if (!HasImpl()) { + if (!mImpl) { return; } - Impl* impl = GetImpl(); - if (!impl->mAttrCount && !impl->mMappedAttributeBits) { - Clear(); + if (!mImpl->mAttrCount && !mImpl->mMappedAttributeBits) { + mImpl.reset(); return; } // Nothing to do. - if (impl->mAttrCount == impl->mCapacity) { + if (mImpl->mAttrCount == mImpl->mCapacity) { return; } - // Extract the real pointer for realloc Impl* oldImpl = mImpl.release(); - - Impl* newImpl = static_cast<Impl*>( + Impl* impl = static_cast<Impl*>( realloc(oldImpl, Impl::AllocationSizeForAttributes(oldImpl->mAttrCount))); - if (!newImpl) { - SetImpl(oldImpl); + if (!impl) { + mImpl.reset(oldImpl); return; } - newImpl->mCapacity = newImpl->mAttrCount; - SetImpl(newImpl); + impl->mCapacity = impl->mAttrCount; + mImpl.reset(impl); } nsresult AttrArray::EnsureCapacityToClone(const AttrArray& aOther) { - MOZ_ASSERT(!HasImpl(), + MOZ_ASSERT(!mImpl, "AttrArray::EnsureCapacityToClone requires the array be empty " "when called"); @@ -281,15 +276,13 @@ nsresult AttrArray::EnsureCapacityToClone(const AttrArray& aOther) { // No need to use a CheckedUint32 because we are cloning. We know that we // have already allocated an AttrArray of this size. - Impl* impl = - static_cast<Impl*>(malloc(Impl::AllocationSizeForAttributes(attrCount))); - NS_ENSURE_TRUE(impl, NS_ERROR_OUT_OF_MEMORY); + mImpl.reset( + static_cast<Impl*>(malloc(Impl::AllocationSizeForAttributes(attrCount)))); + NS_ENSURE_TRUE(mImpl, NS_ERROR_OUT_OF_MEMORY); - impl->mMappedAttributeBits = 0; - impl->mCapacity = attrCount; - impl->mAttrCount = 0; - impl->mSubtreeBloomFilter = aOther.GetSubtreeBloomFilter(); - SetImpl(impl); + mImpl->mMappedAttributeBits = 0; + mImpl->mCapacity = attrCount; + mImpl->mAttrCount = 0; return NS_OK; } @@ -298,7 +291,7 @@ bool AttrArray::GrowBy(uint32_t aGrowSize) { const uint32_t kLinearThreshold = 16; const uint32_t kLinearGrowSize = 4; - CheckedUint32 capacity = HasImpl() ? GetImpl()->mCapacity : 0; + CheckedUint32 capacity = mImpl ? mImpl->mCapacity : 0; CheckedUint32 minCapacity = capacity; minCapacity += aGrowSize; if (!minCapacity.isValid()) { @@ -324,7 +317,7 @@ bool AttrArray::GrowBy(uint32_t aGrowSize) { } bool AttrArray::GrowTo(uint32_t aCapacity) { - uint32_t oldCapacity = HasImpl() ? GetImpl()->mCapacity : 0; + uint32_t oldCapacity = mImpl ? mImpl->mCapacity : 0; if (aCapacity <= oldCapacity) { return true; } @@ -343,46 +336,32 @@ bool AttrArray::GrowTo(uint32_t aCapacity) { MOZ_ASSERT(sizeInBytes.value() == Impl::AllocationSizeForAttributes(aCapacity)); - const bool needToInitialize = !HasImpl(); - uint64_t oldBloom = 0xFFFFFFFFFFFFFFFFULL; - Impl* oldImpl = nullptr; - - if (HasImpl()) { - // We have a real Impl pointer, extract it for realloc - oldImpl = mImpl.release(); - } else if (HasTaggedBloom()) { - // Preserve bloom filter from the tagged value - oldBloom = GetTaggedBloom(); - } - + const bool needToInitialize = !mImpl; + Impl* oldImpl = mImpl.release(); Impl* newImpl = static_cast<Impl*>(realloc(oldImpl, sizeInBytes.value())); if (!newImpl) { - if (oldImpl) { - SetImpl(oldImpl); - } else if (HasTaggedBloom()) { - SetTaggedBloom(oldBloom); - } + mImpl.reset(oldImpl); return false; } + mImpl.reset(newImpl); + // Set initial counts if we didn't have a buffer before if (needToInitialize) { - newImpl->mMappedAttributeBits = 0; - newImpl->mAttrCount = 0; - newImpl->mSubtreeBloomFilter = oldBloom; + mImpl->mMappedAttributeBits = 0; + mImpl->mAttrCount = 0; } - newImpl->mCapacity = aCapacity; - SetImpl(newImpl); + mImpl->mCapacity = aCapacity; return true; } size_t AttrArray::SizeOfExcludingThis( mozilla::MallocSizeOf aMallocSizeOf) const { - if (!HasImpl()) { + if (!mImpl) { return 0; } - size_t n = aMallocSizeOf(GetImpl()); + size_t n = aMallocSizeOf(mImpl.get()); for (const InternalAttr& attr : Attrs()) { n += attr.mValue.SizeOfExcludingThis(aMallocSizeOf); } diff --git a/dom/base/AttrArray.h b/dom/base/AttrArray.h @@ -23,36 +23,18 @@ namespace mozilla { class AttributeStyles; struct StyleLockedDeclarationBlock; - -namespace dom { -class Element; -class ElementInternals; -} // namespace dom } // namespace mozilla class AttrArray { using BorrowedAttrInfo = mozilla::dom::BorrowedAttrInfo; - // Declare as friend to grant access to call SetAndSwapAttr. - friend class mozilla::dom::Element; - friend class mozilla::dom::ElementInternals; - public: - AttrArray() { - // Initialize bloom filter. - SetTaggedBloom(0x1ULL); - } - ~AttrArray() { - // If mImpl contains a tagged bloom filter (bit 0 = 1), we must clear it - // before the BindgenUniquePtr destructor tries to free it as a pointer. - if (HasTaggedBloom()) { - mImpl.release(); - } - } + AttrArray() = default; + ~AttrArray() = default; bool HasAttrs() const { return !!AttrCount(); } - uint32_t AttrCount() const { return HasImpl() ? GetImpl()->mAttrCount : 0; } + uint32_t AttrCount() const { return mImpl ? mImpl->mAttrCount : 0; } const nsAttrValue* GetAttr(const nsAtom* aLocalName) const; @@ -66,6 +48,14 @@ class AttrArray { const nsAttrValue* GetAttr(const nsAString& aName, nsCaseTreatment aCaseSensitive) const; const nsAttrValue* AttrAt(uint32_t aPos) const; + // SetAndSwapAttr swaps the current attribute value with aValue. + // If the attribute was unset, an empty value will be swapped into aValue + // and aHadValue will be set to false. Otherwise, aHadValue will be set to + // true. + nsresult SetAndSwapAttr(nsAtom* aLocalName, nsAttrValue& aValue, + bool* aHadValue); + nsresult SetAndSwapAttr(mozilla::dom::NodeInfo* aName, nsAttrValue& aValue, + bool* aHadValue); // This stores the argument and clears the pending mapped attribute evaluation // bit, so after calling this IsPendingMappedAttributeEvaluation() is @@ -74,11 +64,11 @@ class AttrArray { already_AddRefed<mozilla::StyleLockedDeclarationBlock>); bool IsPendingMappedAttributeEvaluation() const { - return HasImpl() && GetImpl()->mMappedAttributeBits & 1; + return mImpl && mImpl->mMappedAttributeBits & 1; } mozilla::StyleLockedDeclarationBlock* GetMappedDeclarationBlock() const { - return HasImpl() ? GetImpl()->GetMappedDeclarationBlock() : nullptr; + return mImpl ? mImpl->GetMappedDeclarationBlock() : nullptr; } // Remove the attr at position aPos. The value of the attr is placed in @@ -111,9 +101,9 @@ class AttrArray { // Mark the element as pending mapped attribute evaluation. This should be // called when a mapped attribute is changed (regardless of connectedness). bool MarkAsPendingPresAttributeEvaluation() { - // It'd be great to be able to assert that HasImpl() is true or we're the + // It'd be great to be able to assert that mImpl is non-null or we're the // <body> or <svg> elements. - if (MOZ_UNLIKELY(!HasImpl()) && !GrowBy(1)) { + if (MOZ_UNLIKELY(!mImpl) && !GrowBy(1)) { return false; } InfallibleMarkAsPendingPresAttributeEvaluation(); @@ -122,8 +112,8 @@ class AttrArray { // See above. void InfallibleMarkAsPendingPresAttributeEvaluation() { - MOZ_ASSERT(HasImpl()); - GetImpl()->mMappedAttributeBits |= 1; + MOZ_ASSERT(mImpl); + mImpl->mMappedAttributeBits |= 1; } // Clear the servo declaration block on the mapped attributes, if any @@ -198,17 +188,7 @@ class AttrArray { bool GrowBy(uint32_t aGrowSize); bool GrowTo(uint32_t aCapacity); - void Clear() { - // If mImpl contains a tagged bloom filter, release it first to prevent - // unique_ptr from trying to delete it as a pointer - if (HasTaggedBloom()) { - mImpl.release(); - } else { - mImpl.reset(); - } - // Reinitialize to default tagged bloom filter - SetTaggedBloom(0x1ULL); - } + void Clear() { mImpl.reset(); } private: // Tries to create an attribute, growing the buffer if needed, with the given @@ -254,134 +234,18 @@ class AttrArray { // have), and are pending an update to recompute our declaration. uintptr_t mMappedAttributeBits = 0; - // Combined bloom filter (63 bits) for both classes and attributes - // Bit 0: Always 1 to match tagged pointer implementation. - // Bits 1-63: Combined bloom filter (63 bits) - uint64_t mSubtreeBloomFilter; - - public: - Impl() : mSubtreeBloomFilter(0xFFFFFFFFFFFFFFFFULL) {} - // Allocated in the same buffer as `Impl`. InternalAttr mBuffer[0]; }; mozilla::Span<InternalAttr> Attrs() { - return HasImpl() ? GetImpl()->Attrs() : mozilla::Span<InternalAttr>(); + return mImpl ? mImpl->Attrs() : mozilla::Span<InternalAttr>(); } mozilla::Span<const InternalAttr> Attrs() const { - return HasImpl() ? GetImpl()->Attrs() : mozilla::Span<const InternalAttr>(); - } - - bool HasTaggedBloom() const { - return (reinterpret_cast<uintptr_t>(mImpl.get()) & 1) != 0; - } - - bool HasImpl() const { - MOZ_ASSERT(mImpl.get() != nullptr); - return !HasTaggedBloom(); - } - - Impl* GetImpl() { - MOZ_ASSERT(HasImpl()); - return mImpl.get(); + return mImpl ? mImpl->Attrs() : mozilla::Span<const InternalAttr>(); } - const Impl* GetImpl() const { - MOZ_ASSERT(HasImpl()); - return mImpl.get(); - } - - uint64_t GetTaggedBloom() const { - MOZ_ASSERT(HasTaggedBloom()); - return reinterpret_cast<uint64_t>(mImpl.get()); - } - - void SetTaggedBloom(uint64_t aBloom) { - // If we already have a tagged bloom, release it first - if (HasTaggedBloom()) { - mImpl.release(); - } - // Ensure bit 0 is set (tag bit) and upper bit is clear (valid pointer - // range) - MOZ_ASSERT((aBloom & 1) != 0); - mImpl.reset(reinterpret_cast<Impl*>(static_cast<uintptr_t>(aBloom))); - } - - void SetImpl(Impl* aImpl) { - MOZ_ASSERT(aImpl != nullptr && - (reinterpret_cast<uintptr_t>(aImpl) & 1) == 0); - // If we currently have a tagged bloom filter, release it first to prevent - // unique_ptr from trying to delete it as a pointer - if (HasTaggedBloom()) { - mImpl.release(); - } - mImpl.reset(aImpl); - } - - public: - // Set bloom filter directly from 64-bit value - void SetSubtreeBloomFilter(uint64_t aBloom) { - if (HasImpl()) { - GetImpl()->mSubtreeBloomFilter = aBloom; - } else { - SetTaggedBloom(aBloom); - } - } - - // Get bloom filter, used for fast querySelector - uint64_t GetSubtreeBloomFilter() const { - if (HasImpl()) { - return GetImpl()->mSubtreeBloomFilter; - } else if (HasTaggedBloom()) { - return GetTaggedBloom(); - } - MOZ_ASSERT_UNREACHABLE("Bloom filter should never be nullptr"); - return 0xFFFFFFFFFFFFFFFFULL; - } - - // Update bloom filter with new 64-bit hash - void UpdateSubtreeBloomFilter(uint64_t aHash) { - if (HasImpl()) { - GetImpl()->mSubtreeBloomFilter |= aHash; - } else { - uint64_t current = GetSubtreeBloomFilter(); - SetTaggedBloom(current | aHash); - } - } - - // Check if bloom may contain the given hash - bool BloomMayHave(uint64_t aHash) const { - uint64_t bloom = GetSubtreeBloomFilter(); - return (bloom & aHash) == aHash; - } - - private: - // Internal method that swaps the current attribute value with aValue. - // Does NOT update bloom filters - external code should always use - // Element::SetAndSwapAttr instead, which calls this and updates bloom - // filters. If the attribute was unset, an empty value will be swapped into - // aValue and aHadValue will be set to false. Otherwise, aHadValue will be set - // to true. - nsresult SetAndSwapAttr(nsAtom* aLocalName, nsAttrValue& aValue, - bool* aHadValue); - nsresult SetAndSwapAttr(mozilla::dom::NodeInfo* aName, nsAttrValue& aValue, - bool* aHadValue); - - // mImpl serves dual purposes using pointer tagging: - // - // 1. When the element has no attributes: - // - Stores a "tagged bloom filter" (bit 0 is 1). The remaining - // bits are used as a bloomfilter to identify the existence of - // attribute & class names in this subtree for querySelector. - // - // 2. When the element has attributes: - // - Points to an Impl struct (bit 0 is 0). - // - Contains actual attribute storage and the bloom filter - // is now copied as a member of Impl. - // - // Use HasTaggedBloom() vs HasImpl() to distinguish. mozilla::BindgenUniquePtr<Impl> mImpl; }; diff --git a/dom/base/Element.cpp b/dom/base/Element.cpp @@ -2201,144 +2201,6 @@ bool Element::HasVisibleScrollbars() { return scrollFrame && !scrollFrame->GetScrollbarVisibility().isEmpty(); } -// Hash function for 63-bit bloom filter (k=2) -// Returns 64-bit value with bit 0 set to 1 and 2 bits set in range 1-63 -static uint64_t HashForBloomFilter63(const nsAtom* aAtom) { - if (!aAtom) { - return 1ULL; // Just the tag bit - } - uint32_t hash = aAtom->hash(); - uint64_t filter = 1ULL; - // Set 2 bits in the 63-bit range (bits 1-63) - filter |= 1ULL << (1 + (hash % 63)); // First hash function - filter |= 1ULL << (1 + ((hash >> 6) % 63)); // Second hash function - return filter; -} - -// Propagates this element's bloom filter up the tree by OR-ing it with -// all ancestor element bloom filters, stopping early if no new bits are added. -void Element::PropagateBloomFilterToParents() { - Element* toUpdate = this; - Element* parent = GetParentElement(); - - while (parent) { - uint64_t childBloom = toUpdate->mAttrs.GetSubtreeBloomFilter(); - uint64_t parentBloom = parent->mAttrs.GetSubtreeBloomFilter(); - uint64_t newBloom = parentBloom | childBloom; - - if (newBloom == parentBloom) { - break; - } - parent->mAttrs.SetSubtreeBloomFilter(newBloom); - toUpdate = parent; - parent = toUpdate->GetParentElement(); - } -} - -// Hashes all class names in a class attribute value for the bloom filter. -// Handles both single class (eAtom) and multiple classes (eAtomArray). -static uint64_t HashClassesForBloom(const nsAttrValue* aValue) { - uint64_t filter = 1ULL; // Start with tag bit - if (!aValue) { - return filter; - } - - if (aValue->Type() == nsAttrValue::eAtomArray) { - const mozilla::AttrAtomArray* array = aValue->GetAtomArrayValue(); - if (array) { - for (const RefPtr<nsAtom>& className : array->mArray) { - filter |= HashForBloomFilter63(className); - } - } - } else if (aValue->Type() == nsAttrValue::eAtom) { - filter |= HashForBloomFilter63(aValue->GetAtomValue()); - } -#ifdef DEBUG - else { - // Assert that only empty strings make it here. - nsAutoString value; - aValue->ToString(value); - bool isOnlyWhitespace = true; - for (uint32_t i = 0; i < value.Length(); i++) { - if (!nsContentUtils::IsHTMLWhitespace(value[i])) { - isOnlyWhitespace = false; - break; - } - } - MOZ_ASSERT(isOnlyWhitespace, "Expecting only empty strings here."); - } -#endif - - return filter; -} - -#ifdef DEBUG -// Asserts that the bloom filter contains all expected bits from -// current attributes, classes, and descendant bloom filters. -void Element::VerifySubtreeBloomFilter() const { - uint64_t expectedBloom = 1ULL; - - // Hash all attribute names in kNameSpaceID_None namespace - uint32_t attrCount = GetAttrCount(); - for (uint32_t i = 0; i < attrCount; i++) { - const nsAttrName* attrName = GetAttrNameAt(i); - MOZ_ASSERT(attrName, "Attribute name should not be null"); - if (attrName->NamespaceEquals(kNameSpaceID_None)) { - nsAtom* localName = attrName->LocalName(); - expectedBloom |= HashForBloomFilter63(localName); - - if (!localName->IsAsciiLowercase()) { - Document* doc = OwnerDoc(); - if (!IsHTMLElement() && doc->IsHTMLDocument()) { - RefPtr<nsAtom> lowercaseAttr(localName); - ToLowerCaseASCII(lowercaseAttr); - expectedBloom |= HashForBloomFilter63(lowercaseAttr); - } - } - } - } - - // Hash class names - const nsAttrValue* classAttr = GetClasses(); - expectedBloom |= HashClassesForBloom(classAttr); - - // Include children's bloom filters - for (Element* child = GetFirstElementChild(); child; - child = child->GetNextElementSibling()) { - expectedBloom |= child->mAttrs.GetSubtreeBloomFilter(); - } - - uint64_t actualBloom = mAttrs.GetSubtreeBloomFilter(); - // Bloom filters are append-only: bits can be set but never cleared. - // So actualBloom may contain extra bits from removed attributes. - // We only check that all expected bits are present. - MOZ_ASSERT((actualBloom & expectedBloom) == expectedBloom, - "Bloom filter missing required bits"); -} -#endif - -void Element::UpdateSubtreeBloomFilterForClass(const nsAttrValue* aClassValue) { - if (!aClassValue) { - return; - } - mAttrs.UpdateSubtreeBloomFilter(HashClassesForBloom(aClassValue)); -} - -void Element::UpdateSubtreeBloomFilterForAttribute(nsAtom* aAttribute) { - MOZ_ASSERT(aAttribute, "Attribute should not be null"); - mAttrs.UpdateSubtreeBloomFilter(HashForBloomFilter63(aAttribute)); - - // For non-HTML elements in HTML documents, also add the lowercase hash. - // This ensures querySelector on HTML ancestors can find these attributes, - // since HTML elements use case-insensitive attribute matching. - if (!aAttribute->IsAsciiLowercase() && !IsHTMLElement() && - OwnerDoc()->IsHTMLDocument()) { - RefPtr<nsAtom> lowercaseAttr(aAttribute); - ToLowerCaseASCII(lowercaseAttr); - mAttrs.UpdateSubtreeBloomFilter(HashForBloomFilter63(lowercaseAttr)); - } -} - nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) { MOZ_ASSERT(aParent.IsContent() || aParent.IsDocument(), "Must have content or document parent!"); @@ -2477,13 +2339,6 @@ nsresult Element::BindToTree(BindContext& aContext, nsINode& aParent) { MOZ_ASSERT(aParent.IsInComposedDoc() == IsInComposedDoc()); MOZ_ASSERT(aParent.IsInShadowTree() == IsInShadowTree()); MOZ_ASSERT(aParent.SubtreeRoot() == SubtreeRoot()); - -#ifdef DEBUG - VerifySubtreeBloomFilter(); -#endif - - // When binding to tree, propagate this element's bloom to parents. - PropagateBloomFilterToParents(); return NS_OK; } @@ -3035,37 +2890,6 @@ nsresult Element::SetAttr(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix, }); } -nsresult Element::SetAndSwapAttr(nsAtom* aLocalName, nsAttrValue& aValue, - bool* aHadValue) { - MOZ_TRY(mAttrs.SetAndSwapAttr(aLocalName, aValue, aHadValue)); - - if (aLocalName == nsGkAtoms::_class) { - UpdateSubtreeBloomFilterForClass(GetClasses()); - } - UpdateSubtreeBloomFilterForAttribute(aLocalName); - PropagateBloomFilterToParents(); - - return NS_OK; -} - -nsresult Element::SetAndSwapAttr(mozilla::dom::NodeInfo* aName, - nsAttrValue& aValue, bool* aHadValue) { - MOZ_TRY(mAttrs.SetAndSwapAttr(aName, aValue, aHadValue)); - - // Only update bloom filter for null-namespace attributes, since the - // querySelector bloom filter optimization only applies to those. - if (aName->NamespaceEquals(kNameSpaceID_None)) { - nsAtom* localName = aName->NameAtom(); - if (localName == nsGkAtoms::_class) { - UpdateSubtreeBloomFilterForClass(GetClasses()); - } - UpdateSubtreeBloomFilterForAttribute(localName); - PropagateBloomFilterToParents(); - } - - return NS_OK; -} - nsresult Element::SetAttr(int32_t aNamespaceID, nsAtom* aName, nsAtom* aPrefix, nsAtom* aValue, nsIPrincipal* aSubjectPrincipal, bool aNotify) { @@ -3191,7 +3015,7 @@ nsresult Element::SetAttrAndNotify( hadDirAuto = HasDirAuto(); // already takes bdi into account } - MOZ_TRY(SetAndSwapAttr(aName, aParsedValue, &oldValueSet)); + MOZ_TRY(mAttrs.SetAndSwapAttr(aName, aParsedValue, &oldValueSet)); if (IsAttributeMapped(aName) && !IsPendingMappedAttributeEvaluation()) { mAttrs.InfallibleMarkAsPendingPresAttributeEvaluation(); if (Document* doc = GetComposedDoc()) { @@ -3202,7 +3026,7 @@ nsresult Element::SetAttrAndNotify( RefPtr<mozilla::dom::NodeInfo> ni = mNodeInfo->NodeInfoManager()->GetNodeInfo(aName, aPrefix, aNamespaceID, ATTRIBUTE_NODE); - MOZ_TRY(SetAndSwapAttr(ni, aParsedValue, &oldValueSet)); + MOZ_TRY(mAttrs.SetAndSwapAttr(ni, aParsedValue, &oldValueSet)); } PostIdMaybeChange(aNamespaceID, aName, &valueForAfterSetAttr); diff --git a/dom/base/Element.h b/dom/base/Element.h @@ -1059,32 +1059,6 @@ class Element : public FragmentOrElement { nsresult UnsetAttr(int32_t aNameSpaceID, nsAtom* aName, bool aNotify); /** - * Swap an attribute value. This is a public wrapper that ensures bloom - * filter updates are performed correctly. For kNameSpaceID_None attributes, - * this will automatically update the bloom filter and propagate changes to - * parent elements. - * - * @param aLocalName the local name of the attribute - * @param aValue the value to swap (will contain old value on return) - * @param aHadValue set to true if attribute existed, false otherwise - */ - nsresult SetAndSwapAttr(nsAtom* aLocalName, nsAttrValue& aValue, - bool* aHadValue); - - /** - * Swap an attribute value. This is a public wrapper that ensures bloom - * filter updates are performed correctly. For kNameSpaceID_None attributes, - * this will automatically update the bloom filter and propagate changes to - * parent elements. - * - * @param aName the node info of the attribute - * @param aValue the value to swap (will contain old value on return) - * @param aHadValue set to true if attribute existed, false otherwise - */ - nsresult SetAndSwapAttr(mozilla::dom::NodeInfo* aName, nsAttrValue& aValue, - bool* aHadValue); - - /** * Get the namespace / name / prefix of a given attribute. * * @param aIndex the index of the attribute name @@ -2347,16 +2321,6 @@ class Element : public FragmentOrElement { MOZ_CAN_RUN_SCRIPT void FireBeforematchEvent(ErrorResult& aRv); - void PropagateBloomFilterToParents(); - void UpdateSubtreeBloomFilterForClass(const nsAttrValue* aClassValue); - void UpdateSubtreeBloomFilterForAttribute(nsAtom* aAttribute); - uint64_t GetSubtreeBloomFilter() const { - return mAttrs.GetSubtreeBloomFilter(); - } -#ifdef DEBUG - void VerifySubtreeBloomFilter() const; -#endif - protected: enum class ReparseAttributes { No, Yes }; /** diff --git a/dom/base/nsStyledElement.cpp b/dom/base/nsStyledElement.cpp @@ -154,7 +154,8 @@ nsresult nsStyledElement::ReparseStyleAttribute(bool aForceInDataDoc) { // Don't bother going through SetInlineStyleDeclaration; we don't // want to fire off mutation events or document notifications anyway bool oldValueSet; - nsresult rv = SetAndSwapAttr(nsGkAtoms::style, attrValue, &oldValueSet); + nsresult rv = + mAttrs.SetAndSwapAttr(nsGkAtoms::style, attrValue, &oldValueSet); NS_ENSURE_SUCCESS(rv, rv); } diff --git a/dom/svg/SVGElement.cpp b/dom/svg/SVGElement.cpp @@ -283,11 +283,6 @@ void SVGElement::DidAnimateClass() { } mClassAnimAttr->ParseAtomArray(src); - // Update bloom filter after mutating mClassAnimAttr. - UpdateSubtreeBloomFilterForClass(mClassAnimAttr.get()); - UpdateSubtreeBloomFilterForAttribute(nsGkAtoms::_class); - PropagateBloomFilterToParents(); - // FIXME(emilio): This re-selector-matches, but we do the snapshot stuff right // above... Is this needed anymore? if (presShell) { diff --git a/dom/xul/nsXULElement.cpp b/dom/xul/nsXULElement.cpp @@ -1047,9 +1047,11 @@ nsresult nsXULElement::MakeHeavyweight(nsXULPrototypeElement* aPrototype) { bool oldValueSet; // XXX we might wanna have a SetAndTakeAttr that takes an nsAttrName if (protoattr->mName.IsAtom()) { - rv = SetAndSwapAttr(protoattr->mName.Atom(), attrValue, &oldValueSet); + rv = mAttrs.SetAndSwapAttr(protoattr->mName.Atom(), attrValue, + &oldValueSet); } else { - rv = SetAndSwapAttr(protoattr->mName.NodeInfo(), attrValue, &oldValueSet); + rv = mAttrs.SetAndSwapAttr(protoattr->mName.NodeInfo(), attrValue, + &oldValueSet); } NS_ENSURE_SUCCESS(rv, rv); } diff --git a/layout/style/GeckoBindings.cpp b/layout/style/GeckoBindings.cpp @@ -1787,10 +1787,6 @@ nsAtom** Gecko_Element_ExportedParts(const nsAttrValue* aValue, return reinterpret_cast<nsAtom**>(parts->Elements()); } -uint64_t Gecko_Element_GetSubtreeBloomFilter(const Element* aElement) { - return aElement->GetSubtreeBloomFilter(); -} - bool StyleSingleFontFamily::IsNamedFamily(const nsAString& aFamilyName) const { if (!IsFamilyName()) { return false; diff --git a/layout/style/GeckoBindings.h b/layout/style/GeckoBindings.h @@ -109,7 +109,6 @@ const nsINode* Gecko_GetNextStyleChild(mozilla::dom::StyleChildrenIterator*); nsAtom* Gecko_Element_ImportedPart(const nsAttrValue*, nsAtom*); nsAtom** Gecko_Element_ExportedParts(const nsAttrValue*, nsAtom*, size_t* aOutLength); -uint64_t Gecko_Element_GetSubtreeBloomFilter(const mozilla::dom::Element*); NS_DECL_THREADSAFE_FFI_REFCOUNTING(mozilla::css::SheetLoadDataHolder, SheetLoadDataHolder); diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs @@ -87,19 +87,6 @@ pub struct DomDescendants<N> { scope: N, } -impl<N> DomDescendants<N> -where - N: TNode, -{ - /// Returns the next element ignoring all of our subtree. - #[inline] - pub fn next_skipping_children(&mut self) -> Option<N> { - let prev = self.previous.take()?; - self.previous = prev.next_in_preorder_skipping_children(self.scope); - self.previous - } -} - impl<N> Iterator for DomDescendants<N> where N: TNode, @@ -202,15 +189,7 @@ pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { if let Some(c) = self.first_child() { return Some(c); } - self.next_in_preorder_skipping_children(scoped_to) - } - /// Returns the next node in tree order, skipping the children of the current node. - /// - /// This is useful when we know that a subtree cannot contain matches, allowing us - /// to skip entire subtrees during traversal. - #[inline] - fn next_in_preorder_skipping_children(&self, scoped_to: Self) -> Option<Self> { let mut current = *self; loop { if current == scoped_to { @@ -485,33 +464,6 @@ pub trait TElement: false } - /// Returns the bloom filter for this element's subtree, used for fast - /// querySelector optimization by allowing subtrees to be skipped. - /// Each element's filter includes hashes for all of it's class names and - /// attribute names (not values), along with the names for all descendent - /// elements. - /// - /// The default implementation returns all bits set, meaning the bloom filter - /// never filters anything. - fn subtree_bloom_filter(&self) -> u64 { - u64::MAX - } - - /// Check if this element's subtree may contain elements with the given bloom hash. - fn bloom_may_have_hash(&self, bloom_hash: u64) -> bool { - let bloom = self.subtree_bloom_filter(); - (bloom & bloom_hash) == bloom_hash - } - - /// Convert a 32-bit atom hash to a bloom filter value using k=2 hash functions. - /// This must match the C++ implementation in dom::base::Element::HashForBloomFilter63 - fn hash_for_bloom_filter(hash: u32) -> u64 { - let mut filter = 1u64; - filter |= 1u64 << (1 + (hash % 63)); // First hash function - filter |= 1u64 << (1 + ((hash >> 6) % 63)); // Second hash function - filter - } - /// Return the list of slotted nodes of this node. fn slotted_nodes(&self) -> &[Self::ConcreteNode] { &[] diff --git a/servo/components/style/dom_apis.rs b/servo/components/style/dom_apis.rs @@ -237,56 +237,26 @@ where fn invalidated_descendants(&mut self, _e: E, _child: E) {} } -enum Operation { - Reject, - Accept, - RejectSkippingChildren, -} - -impl From<bool> for Operation { - #[inline(always)] - fn from(matches: bool) -> Self { - if matches { - Operation::Accept - } else { - Operation::Reject - } - } -} - fn collect_all_elements<E, Q, F>(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F) where E: TElement, Q: SelectorQuery<E>, - F: FnMut(E) -> Operation, + F: FnMut(E) -> bool, { - let mut iter = root.dom_descendants(); - let mut cur = iter.next(); - while let Some(node) = cur { + for node in root.dom_descendants() { let element = match node.as_element() { Some(e) => e, - None => { - cur = iter.next(); - continue; - }, + None => continue, }; - match filter(element) { - // Element matches - add to results and continue traversing its children. - Operation::Accept => { - Q::append_element(results, element); - if Q::should_stop_after_first_match() { - return; - } - }, - // Element doesn't match - skip it but continue traversing its children. - Operation::Reject => {}, - // Element doesn't match and skip entire subtree. - Operation::RejectSkippingChildren => { - cur = iter.next_skipping_children(); - continue; - }, + + if !filter(element) { + continue; + } + + Q::append_element(results, element); + if Q::should_stop_after_first_match() { + return; } - cur = iter.next(); } } @@ -374,7 +344,7 @@ fn collect_elements_with_id<E, Q, F>( Ok(elements) => elements, Err(()) => { collect_all_elements::<E, Q, _>(root, results, |e| { - Operation::from(e.has_id(id, class_and_id_case_sensitivity) && filter(e)) + e.has_id(id, class_and_id_case_sensitivity) && filter(e) }); return; @@ -479,68 +449,26 @@ where { match *component { Component::ExplicitUniversalType => { - collect_all_elements::<E, Q, _>(root, results, |_| Operation::Accept) - }, - Component::Class(ref class) => { - // Bloom filter can only be used when case sensitive. - let bloom_hash = if class_and_id_case_sensitivity == CaseSensitivity::CaseSensitive { - Some(E::hash_for_bloom_filter(class.0.get_hash())) - } else { - None - }; - - collect_all_elements::<E, Q, _>(root, results, |element| { - if bloom_hash.is_some_and(|hash| !element.bloom_may_have_hash(hash)) { - return Operation::RejectSkippingChildren; - } - Operation::from(element.has_class(class, class_and_id_case_sensitivity)) - }); + collect_all_elements::<E, Q, _>(root, results, |_| true) }, + Component::Class(ref class) => collect_all_elements::<E, Q, _>(root, results, |element| { + element.has_class(class, class_and_id_case_sensitivity) + }), Component::LocalName(ref local_name) => { collect_all_elements::<E, Q, _>(root, results, |element| { - Operation::from(local_name_matches(element, local_name)) + local_name_matches(element, local_name) }) }, Component::AttributeInNoNamespaceExists { ref local_name, ref local_name_lower, - } => { - // For HTML elements: C++ hashes lowercase - // For XUL/SVG/MathML elements: C++ hashes original case - let hash_original = E::hash_for_bloom_filter(local_name.0.get_hash()); - let hash_lower = if local_name.0 == local_name_lower.0 { - hash_original - } else { - E::hash_for_bloom_filter(local_name_lower.0.get_hash()) - }; - - collect_all_elements::<E, Q, _>(root, results, |element| { - // Check bloom filter first - let bloom_found_hash = if hash_original == hash_lower - || !element.as_node().owner_doc().is_html_document() - { - element.bloom_may_have_hash(hash_original) - } else if element.is_html_element_in_html_document() { - // HTML elements store lowercase hashes - element.bloom_may_have_hash(hash_lower) - } else { - // Non-HTML elements in HTML documents might have HTML descendants - // with lowercase-only hashes, so check both - element.bloom_may_have_hash(hash_original) - || element.bloom_may_have_hash(hash_lower) - }; - - if !bloom_found_hash { - return Operation::RejectSkippingChildren; - } - - Operation::from(element.has_attr_in_no_namespace(matching::select_name( - &element, - local_name, - local_name_lower, - ))) - }); - }, + } => collect_all_elements::<E, Q, _>(root, results, |element| { + element.has_attr_in_no_namespace(matching::select_name( + &element, + local_name, + local_name_lower, + )) + }), Component::AttributeInNoNamespace { ref local_name, ref value, @@ -549,15 +477,8 @@ where } => { let empty_namespace = selectors::parser::namespace_empty_string::<E::Impl>(); let namespace_constraint = NamespaceConstraint::Specific(&empty_namespace); - - // Only use bloom filter to check for attribute name existence. - let bloom_hash = E::hash_for_bloom_filter(local_name.0.get_hash()); - collect_all_elements::<E, Q, _>(root, results, |element| { - if !element.bloom_may_have_hash(bloom_hash) { - return Operation::RejectSkippingChildren; - } - Operation::from(element.attr_matches( + element.attr_matches( &namespace_constraint, local_name, &AttrSelectorOperation::WithValue { @@ -568,8 +489,8 @@ where ), value, }, - )) - }); + ) + }) }, ref other => { let id = match get_id(other) { @@ -772,38 +693,20 @@ where match simple_filter { SimpleFilter::Class(ref class) => { collect_all_elements::<E, Q, _>(root, results, |element| { - Operation::from( - element.has_class(class, class_and_id_case_sensitivity) - && matching::matches_selector_list( - selector_list, - &element, - matching_context, - ), - ) + element.has_class(class, class_and_id_case_sensitivity) + && matching::matches_selector_list(selector_list, &element, matching_context) }); }, SimpleFilter::LocalName(ref local_name) => { collect_all_elements::<E, Q, _>(root, results, |element| { - Operation::from( - local_name_matches(element, local_name) - && matching::matches_selector_list( - selector_list, - &element, - matching_context, - ), - ) + local_name_matches(element, local_name) + && matching::matches_selector_list(selector_list, &element, matching_context) }); }, SimpleFilter::Attr(ref local_name) => { collect_all_elements::<E, Q, _>(root, results, |element| { - Operation::from( - has_attr(element, local_name) - && matching::matches_selector_list( - selector_list, - &element, - matching_context, - ), - ) + has_attr(element, local_name) + && matching::matches_selector_list(selector_list, &element, matching_context) }); }, } @@ -822,11 +725,7 @@ fn query_selector_slow<E, Q>( Q: SelectorQuery<E>, { collect_all_elements::<E, Q, _>(root, results, |element| { - Operation::from(matching::matches_selector_list( - selector_list, - &element, - matching_context, - )) + matching::matches_selector_list(selector_list, &element, matching_context) }); } diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs @@ -660,22 +660,12 @@ impl<'le> GeckoElement<'le> { self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) } } - /// Check if mImpl contains a real pointer (not a bloom filter). - #[inline(always)] - fn has_attr_impl(&self) -> bool { - let ptr = self.0.mAttrs.mImpl.mPtr as usize; - ptr != 0 && (ptr & 1) == 0 - } - #[inline(always)] fn attrs(&self) -> &[structs::AttrArray_InternalAttr] { unsafe { - if !self.has_attr_impl() { - return &[]; - } match self.0.mAttrs.mImpl.mPtr.as_ref() { Some(attrs) => attrs.mBuffer.as_slice(attrs.mAttrCount as usize), - None => &[], + None => return &[], } } } @@ -1103,11 +1093,6 @@ impl<'le> TElement for GeckoElement<'le> { } #[inline] - fn subtree_bloom_filter(&self) -> u64 { - unsafe { bindings::Gecko_Element_GetSubtreeBloomFilter(self.0) } - } - - #[inline] fn local_name(&self) -> &WeakAtom { unsafe { WeakAtom::new(self.as_node().node_info().mInner.mName) } } diff --git a/testing/web-platform/tests/dom/nodes/querySelector-mixed-case.html b/testing/web-platform/tests/dom/nodes/querySelector-mixed-case.html @@ -1,431 +0,0 @@ -<!DOCTYPE html> -<meta charset="utf-8"> -<title>querySelector with mixed-case attributes</title> -<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1997380"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> - -<body> -<div id="test-container"></div> - -<script> -"use strict"; - -const container = document.getElementById("test-container"); - -function buildTestTree() { - // Build entire DOM tree structure first (without attributes) - const html1 = document.createElement("div"); - html1.id = "html1"; - - const svg1 = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg1.id = "svg1"; - - const svg2 = document.createElementNS("http://www.w3.org/2000/svg", "g"); - svg2.id = "svg2"; - - const svg3 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - svg3.id = "svg3"; - - const html2 = document.createElement("div"); - html2.id = "html2"; - - const math1 = document.createElementNS("http://www.w3.org/1998/Math/MathML", "math"); - math1.id = "math1"; - - const math2 = document.createElementNS("http://www.w3.org/1998/Math/MathML", "mi"); - math2.id = "math2"; - - const svg4 = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg4.id = "svg4"; - - const svg5 = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - svg5.id = "svg5"; - - const html3 = document.createElement("div"); - html3.id = "html3"; - - // Create foreignObject with HTML inside SVG (will go in svg1) - const foreign1 = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); - foreign1.id = "foreign1"; - - const html4 = document.createElement("div"); - html4.id = "html4"; - - const html5 = document.createElement("span"); - html5.id = "html5"; - - // Create nested: HTML > SVG inside html2 - const svg6 = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg6.id = "svg6"; - - const foreign2 = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); - foreign2.id = "foreign2"; - - const html6 = document.createElement("div"); - html6.id = "html6"; - - const svg7 = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg7.id = "svg7"; - - const svg8 = document.createElementNS("http://www.w3.org/2000/svg", "circle"); - svg8.id = "svg8"; - - // Build tree structure - svg2.appendChild(svg3); - svg1.appendChild(svg2); - - // Add foreignObject with HTML to svg1 - html4.appendChild(html5); - foreign1.appendChild(html4); - svg1.appendChild(foreign1); - - html1.appendChild(svg1); - - math1.appendChild(math2); - html2.appendChild(math1); - - // Add deeply nested SVG > foreignObject > HTML > SVG to html2 - svg7.appendChild(svg8); - html6.appendChild(svg7); - foreign2.appendChild(html6); - svg6.appendChild(foreign2); - html2.appendChild(svg6); - - html1.appendChild(html2); - - svg4.appendChild(svg5); - - // Create wrapper container for all trees - const wrapper = document.createElement("div"); - wrapper.appendChild(html1); - wrapper.appendChild(svg4); - wrapper.appendChild(html3); - - // NOW set all attributes - html1.setAttribute("viewBox", "html-val"); - html1.setAttribute("dataIndex", "0"); - html1.setAttribute("testAttr", "alpha"); - - svg1.setAttribute("viewBox", "svg-val"); - svg1.setAttribute("dataIndex", "1"); - svg1.setAttribute("testAttr", "beta"); - - svg2.setAttribute("mixedCase", "foo"); - svg2.setAttribute("dataValue", "first"); - - svg3.setAttribute("innerAttr", "found"); - - html2.setAttribute("viewBox", "nested"); - html2.setAttribute("mathVariant", "bold"); - - math1.setAttribute("mathVariant", "italic"); - math1.setAttribute("displayStyle", "true"); - - math2.setAttribute("testIndex", "5"); - math2.setAttribute("dataValue", "second"); - - svg5.setAttribute("viewBox", "rect-val"); - svg5.setAttribute("testIndex", "10"); - - html3.setAttribute("MixedCase", "bar"); - html3.setAttribute("TestIndex", "20"); - - // Set attributes on foreignObject tree in svg1 - foreign1.setAttribute("foreignAttr", "foreign-val"); - - html4.setAttribute("ForeignHTML", "inside-foreign"); - html4.setAttribute("DataType", "html-in-svg"); - - html5.setAttribute("NestedCase", "span-val"); - - // Set attributes on deeply nested tree in html2 - svg6.setAttribute("DeepSVG", "middle-svg"); - svg6.setAttribute("CaseSensitive", "nested"); - - foreign2.setAttribute("SecondForeign", "deep-foreign"); - - html6.setAttribute("InnerHTML", "deep-html"); - html6.setAttribute("DataType", "nested"); - - svg7.setAttribute("ReEnteredSVG", "back-to-svg"); - - svg8.setAttribute("DeepestAttr", "circle-val"); - svg8.setAttribute("CaseSensitive", "deepest"); - - return wrapper; -} - -function runTests(root) { - // DOM tree structure with attributes: - // <div id="test-container"> - // <div id="html1" viewBox="html-val" dataIndex="0" testAttr="alpha"> - // <svg id="svg1" viewBox="svg-val" dataIndex="1" testAttr="beta"> - // <g id="svg2" mixedCase="foo" dataValue="first"> - // <circle id="svg3" innerAttr="found"> - // </g> - // <foreignObject id="foreign1" foreignAttr="foreign-val"> - // <div id="html4" ForeignHTML="inside-foreign" DataType="html-in-svg"> - // <span id="html5" NestedCase="span-val"> - // </div> - // </foreignObject> - // </svg> - // <div id="html2" viewBox="nested" mathVariant="bold"> - // <math id="math1" mathVariant="italic" displayStyle="true"> - // <mi id="math2" testIndex="5" dataValue="second"> - // </math> - // <svg id="svg6" DeepSVG="middle-svg" CaseSensitive="nested"> - // <foreignObject id="foreign2" SecondForeign="deep-foreign"> - // <div id="html6" InnerHTML="deep-html" DataType="nested"> - // <svg id="svg7" ReEnteredSVG="back-to-svg"> - // <circle id="svg8" DeepestAttr="circle-val" CaseSensitive="deepest"> - // </svg> - // </div> - // </foreignObject> - // </svg> - // </div> - // </div> - // <svg id="svg4"> - // <rect id="svg5" viewBox="rect-val" testIndex="10"> - // </svg> - // <div id="html3" MixedCase="bar" TestIndex="20"> - // </div> - - // Test 1: viewBox with mixed case - HTML is case-insensitive, SVG is case-sensitive - let results = root.querySelectorAll("[viewBox]"); - assert_equals(results.length, 4, "[viewBox] should match 2 HTML elements + 2 SVG elements"); - let ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html1", "html2", "svg1", "svg5"], "[viewBox] should match correct elements"); - - // Test 2: viewbox lowercase - should only match HTML elements - results = root.querySelectorAll("[viewbox]"); - assert_equals(results.length, 2, "[viewbox] should only match HTML elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html1", "html2"], "[viewbox] should only match HTML divs"); - - // Test 3: VIEWBOX uppercase - should only match HTML elements - results = root.querySelectorAll("[VIEWBOX]"); - assert_equals(results.length, 2, "[VIEWBOX] should only match HTML elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html1", "html2"], "[VIEWBOX] should only match HTML divs"); - - // Test 4: mathVariant - HTML is case-insensitive, MathML is case-sensitive - results = root.querySelectorAll("[mathVariant]"); - assert_equals(results.length, 2, "[mathVariant] should match HTML + MathML"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html2", "math1"], "[mathVariant] should match correct elements"); - - // Test 5: mathvariant lowercase - should only match HTML - results = root.querySelectorAll("[mathvariant]"); - assert_equals(results.length, 1, "[mathvariant] should only match HTML"); - assert_equals(results[0].id, "html2", "[mathvariant] should match html2"); - - // Test 6: dataIndex exact case - results = root.querySelectorAll("[dataIndex]"); - assert_equals(results.length, 2, "[dataIndex] should match HTML and SVG"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html1", "svg1"], "[dataIndex] should match html1 and svg1"); - - // Test 7: dataindex lowercase - should only match HTML - results = root.querySelectorAll("[dataindex]"); - assert_equals(results.length, 1, "[dataindex] should only match HTML"); - assert_equals(results[0].id, "html1", "[dataindex] should match html1"); - - // Test 8: testIndex mixed case - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [testIndex] → matches math2, svg5 (case-sensitive) - results = root.querySelectorAll("[testIndex]"); - assert_equals(results.length, 3, "[testIndex] should match 3 elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html3", "math2", "svg5"], "[testIndex] should match html3, math2, and svg5"); - - // Test 9: TestIndex with capital T - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [TestIndex] → no match (testIndex != TestIndex) - results = root.querySelectorAll("[TestIndex]"); - assert_equals(results.length, 1, "[TestIndex] should only match HTML element"); - assert_equals(results[0].id, "html3", "[TestIndex] should match html3"); - - // Test 10: testindex all lowercase - // HTML elements: selector lowercased → [testindex] → matches html3 - // SVG/MathML: selector used as-is → [testindex] → no match (testIndex != testindex) - results = root.querySelectorAll("[testindex]"); - assert_equals(results.length, 1, "[testindex] should only match HTML element"); - assert_equals(results[0].id, "html3", "[testindex] should match html3"); - - // Test 11: mixedCase exact - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [mixedCase] → matches svg2 (case-sensitive) - results = root.querySelectorAll("[mixedCase]"); - assert_equals(results.length, 2, "[mixedCase] should match 2 elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html3", "svg2"], "[mixedCase] should match html3 and svg2"); - - // Test 12: MixedCase capital M - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [MixedCase] → no match (mixedCase != MixedCase) - results = root.querySelectorAll("[MixedCase]"); - assert_equals(results.length, 1, "[MixedCase] should only match HTML element"); - assert_equals(results[0].id, "html3", "[MixedCase] should match html3"); - - // Test 13: mixedcase all lowercase - // HTML elements: selector lowercased → [mixedcase] → matches html3 - // SVG: selector used as-is → [mixedcase] → no match (mixedCase != mixedcase) - results = root.querySelectorAll("[mixedcase]"); - assert_equals(results.length, 1, "[mixedcase] should only match HTML element"); - assert_equals(results[0].id, "html3", "[mixedcase] should match html3"); - - // Test 14: testAttr exact case - results = root.querySelectorAll("[testAttr]"); - assert_equals(results.length, 2, "[testAttr] should match HTML and SVG"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html1", "svg1"], "[testAttr] should match html1 and svg1"); - - // Test 15: Attribute value matching with prefix - results = root.querySelectorAll('[viewBox^="svg"]'); - assert_equals(results.length, 1, '[viewBox^="svg"] should match 1 SVG element'); - assert_equals(results[0].id, "svg1", '[viewBox^="svg"] should match svg1'); - - // Test 16: Attribute value matching with exact match - results = root.querySelectorAll('[dataIndex="0"]'); - assert_equals(results.length, 1, '[dataIndex="0"] should match 1 element'); - assert_equals(results[0].id, "html1", '[dataIndex="0"] should match html1'); - - // Test 17: Nested element query - results = root.querySelectorAll("[innerAttr]"); - assert_equals(results.length, 1, "[innerAttr] should match nested circle"); - assert_equals(results[0].id, "svg3", "[innerAttr] should match svg3"); - - // Test 18: displayStyle on MathML - results = root.querySelectorAll("[displayStyle]"); - assert_equals(results.length, 1, "[displayStyle] should match MathML"); - assert_equals(results[0].id, "math1", "[displayStyle] should match math1"); - - // Test 19: displaystyle lowercase - should not match anything (no HTML element has it) - results = root.querySelectorAll("[displaystyle]"); - assert_equals(results.length, 0, "[displaystyle] should not match anything"); - - // Test 20: Case-sensitive value matching with 's' flag - results = root.querySelectorAll('[testAttr="alpha" s]'); - assert_equals(results.length, 1, '[testAttr="alpha" s] should match 1 element'); - assert_equals(results[0].id, "html1", '[testAttr="alpha" s] should match html1'); - - // Test 21: Case-insensitive value matching with 'i' flag - results = root.querySelectorAll('[testAttr="ALPHA" i]'); - assert_equals(results.length, 1, '[testAttr="ALPHA" i] should match 1 element'); - assert_equals(results[0].id, "html1", '[testAttr="ALPHA" i] should match html1'); - - // Test 22: ForeignHTML - HTML inside foreignObject should be case-insensitive - results = root.querySelectorAll("[ForeignHTML]"); - assert_equals(results.length, 1, "[ForeignHTML] should match HTML in foreignObject"); - assert_equals(results[0].id, "html4", "[ForeignHTML] should match html4"); - - // Test 23: foreignhtml lowercase - should match HTML element in foreignObject - results = root.querySelectorAll("[foreignhtml]"); - assert_equals(results.length, 1, "[foreignhtml] should match HTML in foreignObject"); - assert_equals(results[0].id, "html4", "[foreignhtml] should match html4"); - - - // Test 24: DataType - should match both HTML elements (case-insensitive) - results = root.querySelectorAll("[DataType]"); - assert_equals(results.length, 2, "[DataType] should match 2 HTML elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html4", "html6"], "[DataType] should match html4 and html6"); - - // Test 25: datatype lowercase - should match both HTML elements - results = root.querySelectorAll("[datatype]"); - assert_equals(results.length, 2, "[datatype] should match 2 HTML elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["html4", "html6"], "[datatype] should match html4 and html6"); - - // Test 26: CaseSensitive on SVG elements - case-sensitive - results = root.querySelectorAll("[CaseSensitive]"); - assert_equals(results.length, 2, "[CaseSensitive] should match 2 SVG elements"); - ids = Array.from(results).map(el => el.id).sort(); - assert_array_equals(ids, ["svg6", "svg8"], "[CaseSensitive] should match svg6, svg8"); - - // Test 27: casesensitive lowercase - should NOT match SVG elements - results = root.querySelectorAll("[casesensitive]"); - assert_equals(results.length, 0, "[casesensitive] should not match SVG elements"); - - // Test 28: NestedCase - HTML span inside foreignObject should be case-insensitive - results = root.querySelectorAll("[NestedCase]"); - assert_equals(results.length, 1, "[NestedCase] should match HTML span in foreignObject"); - assert_equals(results[0].id, "html5", "[NestedCase] should match html5"); - - // Test 29: nestedcase lowercase - should match HTML span - results = root.querySelectorAll("[nestedcase]"); - assert_equals(results.length, 1, "[nestedcase] should match HTML span in foreignObject"); - assert_equals(results[0].id, "html5", "[nestedcase] should match html5"); - - // Test 30: ReEnteredSVG - SVG inside HTML inside foreignObject should be case-sensitive - results = root.querySelectorAll("[ReEnteredSVG]"); - assert_equals(results.length, 1, "[ReEnteredSVG] should match SVG nested in HTML in foreignObject"); - assert_equals(results[0].id, "svg7", "[ReEnteredSVG] should match svg7"); - - // Test 31: reenteredsvg lowercase - should NOT match (SVG is case-sensitive) - results = root.querySelectorAll("[reenteredsvg]"); - assert_equals(results.length, 0, "[reenteredsvg] should not match SVG element"); - - // Test 32: DeepestAttr - deeply nested SVG circle should be case-sensitive - results = root.querySelectorAll("[DeepestAttr]"); - assert_equals(results.length, 1, "[DeepestAttr] should match deeply nested circle"); - assert_equals(results[0].id, "svg8", "[DeepestAttr] should match svg8"); - - // Test 33: deepestattr lowercase - should NOT match - results = root.querySelectorAll("[deepestattr]"); - assert_equals(results.length, 0, "[deepestattr] should not match SVG circle"); - - // Test 34: foreignAttr on foreignObject element - case-sensitive - results = root.querySelectorAll("[foreignAttr]"); - assert_equals(results.length, 1, "[foreignAttr] should match foreignObject"); - assert_equals(results[0].id, "foreign1", "[foreignAttr] should match foreign1"); - - // Test 35: foreignattr lowercase - should NOT match foreignObject (SVG namespace) - results = root.querySelectorAll("[foreignattr]"); - assert_equals(results.length, 0, "[foreignattr] should not match foreignObject"); - - // Test 36: InnerHTML on HTML element inside foreignObject - case-insensitive - results = root.querySelectorAll("[InnerHTML]"); - assert_equals(results.length, 1, "[InnerHTML] should match HTML in foreignObject"); - assert_equals(results[0].id, "html6", "[InnerHTML] should match html6"); - - // Test 37: innerhtml lowercase - should match HTML in foreignObject - results = root.querySelectorAll("[innerhtml]"); - assert_equals(results.length, 1, "[innerhtml] should match HTML in foreignObject"); - assert_equals(results[0].id, "html6", "[innerhtml] should match html6"); - - // Test 38: DeepSVG on SVG element - case-sensitive - results = root.querySelectorAll("[DeepSVG]"); - assert_equals(results.length, 1, "[DeepSVG] should match SVG element"); - assert_equals(results[0].id, "svg6", "[DeepSVG] should match svg6"); - - // Test 39: deepsvg lowercase - should NOT match SVG - results = root.querySelectorAll("[deepsvg]"); - assert_equals(results.length, 0, "[deepsvg] should not match SVG element"); - - // Test 40: SecondForeign on foreignObject element - case-sensitive - results = root.querySelectorAll("[SecondForeign]"); - assert_equals(results.length, 1, "[SecondForeign] should match foreignObject"); - assert_equals(results[0].id, "foreign2", "[SecondForeign] should match foreign2"); - - // Test 41: secondforeign lowercase - should NOT match foreignObject - results = root.querySelectorAll("[secondforeign]"); - assert_equals(results.length, 0, "[secondforeign] should not match foreignObject"); -} - -test(() => { - const tree = buildTestTree(); - - // Test on disconnected tree first - runTests(tree); - - // Now append to document and test again - container.appendChild(tree); - runTests(container); - - container.innerHTML = ""; -}, "Mixed HTML/SVG/MathML tree with various mixed-case attributes"); - -</script> -</body>