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