From 1453b6fd78bae93b690748fbf1d096dd19fd3807 Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Wed, 11 Mar 2026 22:01:13 -0400 Subject: [PATCH 01/21] Issue #2862 - Initial attempt at a css lowering --- layout/style/nsCSSParser.cpp | 852 ++++++++++++++++++++++++++++++++++- modules/libpref/init/all.js | 9 +- 2 files changed, 857 insertions(+), 4 deletions(-) diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 59872b0f67..02f926a5a4 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -7,6 +7,7 @@ #include "mozilla/ArrayUtils.h" #include "mozilla/DebugOnly.h" +#include "mozilla/Maybe.h" #include "mozilla/Move.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/TypedEnumBits.h" @@ -71,6 +72,7 @@ static bool sMozGradientsEnabled; static bool sControlCharVisibility; static bool sLegacyNegationPseudoClassEnabled; static bool sCascadeLayersEnabled; +static bool sNestingEnabled; const uint32_t nsCSSProps::kParserVariantTable[eCSSProperty_COUNT_no_shorthands] = { @@ -204,6 +206,843 @@ OKLabToSRGBColor(float aL, float aA, float aB, float aAlpha) nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f))); } +class CSSNestingLowerer final +{ + using SelectorList = nsTArray; + +public: + explicit CSSNestingLowerer(const nsAString& aInput) + : mInput(aInput) + , mPos(0) + , mSawNesting(false) + { + } + + bool Lower(nsAString& aOutput) + { + nsAutoString lowered; + if (!ProcessStylesheet(lowered, false)) { + return false; + } + + SkipWhitespaceAndComments(); + if (mPos != mInput.Length() || !mSawNesting) { + return false; + } + + aOutput.Assign(lowered); + return true; + } + +private: + static constexpr auto kCSSWhitespace = " \t\r\n\f"; + + static bool + IsCSSWhitespace(char16_t aChar) + { + return aChar == ' ' || aChar == '\t' || aChar == '\r' || + aChar == '\n' || aChar == '\f'; + } + + bool + AtEnd() const + { + return mPos >= mInput.Length(); + } + + char16_t + Peek() const + { + MOZ_ASSERT(!AtEnd(), "cannot peek past end"); + return mInput.CharAt(mPos); + } + + bool + StartsWithComment() const + { + return mPos + 1 < mInput.Length() && + mInput.CharAt(mPos) == '/' && + mInput.CharAt(mPos + 1) == '*'; + } + + bool + SkipComment() + { + MOZ_ASSERT(StartsWithComment(), "expected comment"); + + mPos += 2; + while (mPos + 1 < mInput.Length()) { + if (mInput.CharAt(mPos) == '*' && mInput.CharAt(mPos + 1) == '/') { + mPos += 2; + return true; + } + ++mPos; + } + + return false; + } + + void + SkipWhitespaceAndComments() + { + while (!AtEnd()) { + if (IsCSSWhitespace(Peek())) { + ++mPos; + continue; + } + if (StartsWithComment()) { + if (!SkipComment()) { + mPos = mInput.Length(); + return; + } + continue; + } + break; + } + } + + bool + SkipString(char16_t aQuote) + { + MOZ_ASSERT(!AtEnd() && Peek() == aQuote, "expected string start"); + + ++mPos; + while (!AtEnd()) { + char16_t c = Peek(); + ++mPos; + if (c == aQuote) { + return true; + } + if (c == '\\' && !AtEnd()) { + ++mPos; + continue; + } + if (c == '\n' || c == '\r' || c == '\f') { + return false; + } + } + + return false; + } + + static void + TrimWhitespace(nsAString& aText) + { + uint32_t start = 0; + uint32_t end = aText.Length(); + + while (start < end && IsCSSWhitespace(aText.CharAt(start))) { + ++start; + } + while (end > start && IsCSSWhitespace(aText.CharAt(end - 1))) { + --end; + } + + if (start == 0 && end == aText.Length()) { + return; + } + + aText.Assign(Substring(aText, start, end - start)); + } + + bool + SplitSelectorList(const nsAString& aSelectorText, SelectorList& aSelectors) + { + uint32_t itemStart = 0; + int32_t parenDepth = 0; + int32_t bracketDepth = 0; + bool inComment = false; + char16_t stringQuote = 0; + + for (uint32_t i = 0; i < aSelectorText.Length(); ++i) { + char16_t c = aSelectorText.CharAt(i); + + if (inComment) { + if (c == '*' && i + 1 < aSelectorText.Length() && + aSelectorText.CharAt(i + 1) == '/') { + inComment = false; + ++i; + } + continue; + } + + if (stringQuote) { + if (c == '\\') { + ++i; + continue; + } + if (c == stringQuote) { + stringQuote = 0; + } + continue; + } + + if (c == '/' && i + 1 < aSelectorText.Length() && + aSelectorText.CharAt(i + 1) == '*') { + inComment = true; + ++i; + continue; + } + + if (c == '"' || c == '\'') { + stringQuote = c; + continue; + } + + if (c == '(') { + ++parenDepth; + continue; + } + if (c == ')' && parenDepth > 0) { + --parenDepth; + continue; + } + if (c == '[') { + ++bracketDepth; + continue; + } + if (c == ']' && bracketDepth > 0) { + --bracketDepth; + continue; + } + + if (c == ',' && parenDepth == 0 && bracketDepth == 0) { + nsAutoString selector; + selector.Assign(Substring(aSelectorText, itemStart, i - itemStart)); + TrimWhitespace(selector); + if (!selector.IsEmpty()) { + aSelectors.AppendElement(selector); + } + itemStart = i + 1; + } + } + + nsAutoString selector; + selector.Assign(Substring(aSelectorText, itemStart)); + TrimWhitespace(selector); + if (!selector.IsEmpty()) { + aSelectors.AppendElement(selector); + } + + return !aSelectors.IsEmpty(); + } + + bool + SelectorHasAmpersand(const nsAString& aSelector) const + { + bool inComment = false; + char16_t stringQuote = 0; + + for (uint32_t i = 0; i < aSelector.Length(); ++i) { + char16_t c = aSelector.CharAt(i); + + if (inComment) { + if (c == '*' && i + 1 < aSelector.Length() && + aSelector.CharAt(i + 1) == '/') { + inComment = false; + ++i; + } + continue; + } + + if (stringQuote) { + if (c == '\\') { + ++i; + continue; + } + if (c == stringQuote) { + stringQuote = 0; + } + continue; + } + + if (c == '/' && i + 1 < aSelector.Length() && + aSelector.CharAt(i + 1) == '*') { + inComment = true; + ++i; + continue; + } + + if (c == '"' || c == '\'') { + stringQuote = c; + continue; + } + + if (c == '&') { + return true; + } + } + + return false; + } + + void + ReplaceAmpersands(const nsAString& aSelector, + const nsAString& aParent, + nsAString& aOutput) const + { + bool inComment = false; + char16_t stringQuote = 0; + + for (uint32_t i = 0; i < aSelector.Length(); ++i) { + char16_t c = aSelector.CharAt(i); + + if (inComment) { + aOutput.Append(c); + if (c == '*' && i + 1 < aSelector.Length() && + aSelector.CharAt(i + 1) == '/') { + aOutput.Append('/'); + inComment = false; + ++i; + } + continue; + } + + if (stringQuote) { + aOutput.Append(c); + if (c == '\\' && i + 1 < aSelector.Length()) { + aOutput.Append(aSelector.CharAt(i + 1)); + ++i; + continue; + } + if (c == stringQuote) { + stringQuote = 0; + } + continue; + } + + if (c == '/' && i + 1 < aSelector.Length() && + aSelector.CharAt(i + 1) == '*') { + aOutput.AppendLiteral("/*"); + inComment = true; + ++i; + continue; + } + + if (c == '"' || c == '\'') { + aOutput.Append(c); + stringQuote = c; + continue; + } + + if (c == '&') { + aOutput.Append(aParent); + continue; + } + + aOutput.Append(c); + } + } + + bool + ExpandNestedSelectors(const SelectorList& aParents, + const nsAString& aNestedSelectorText, + SelectorList& aSelectors) + { + SelectorList nestedSelectors; + if (!SplitSelectorList(aNestedSelectorText, nestedSelectors)) { + return false; + } + + for (const nsString& nestedSelector : nestedSelectors) { + bool hasAmpersand = SelectorHasAmpersand(nestedSelector); + for (const nsString& parentSelector : aParents) { + nsAutoString combined; + if (hasAmpersand) { + ReplaceAmpersands(nestedSelector, parentSelector, combined); + } else { + combined.Assign(parentSelector); + if (!combined.IsEmpty()) { + combined.Append(' '); + } + combined.Append(nestedSelector); + } + TrimWhitespace(combined); + if (!combined.IsEmpty()) { + aSelectors.AppendElement(combined); + } + } + } + + return !aSelectors.IsEmpty(); + } + + static void + AppendSelectors(const SelectorList& aSelectors, nsAString& aOutput) + { + for (uint32_t i = 0; i < aSelectors.Length(); ++i) { + if (i) { + aOutput.AppendLiteral(", "); + } + aOutput.Append(aSelectors[i]); + } + } + + static bool + StartsNestedSelector(char16_t aChar) + { + switch (aChar) { + case '.': + case '#': + case '[': + case ':': + case '&': + case '>': + case '+': + case '~': + case '*': + return true; + default: + return false; + } + } + + static bool + IsAtRuleNameChar(char16_t aChar) + { + return (aChar >= 'a' && aChar <= 'z') || + (aChar >= 'A' && aChar <= 'Z') || + (aChar >= '0' && aChar <= '9') || + aChar == '-'; + } + + static void + LowercaseASCII(nsACString& aText) + { + for (uint32_t i = 0; i < aText.Length(); ++i) { + char c = aText.CharAt(i); + if (c >= 'A' && c <= 'Z') { + aText.BeginWriting()[i] = c - 'A' + 'a'; + } + } + } + + static bool + ShouldProcessGroupRule(const nsACString& aName) + { + return aName.EqualsLiteral("media") || + aName.EqualsLiteral("supports") || + aName.EqualsLiteral("document") || + aName.EqualsLiteral("layer"); + } + + void + FlushDeclarations(const SelectorList& aSelectors, + nsAString& aDeclarations, + nsAString& aOutput) + { + nsAutoString declarations; + declarations.Assign(aDeclarations); + TrimWhitespace(declarations); + aDeclarations.Truncate(); + + if (declarations.IsEmpty()) { + return; + } + + AppendSelectors(aSelectors, aOutput); + aOutput.AppendLiteral(" { "); + aOutput.Append(declarations); + aOutput.AppendLiteral(" }\n"); + } + + bool + ReadRawBlockBody(nsAString& aBody) + { + uint32_t start = mPos; + int32_t depth = 0; + + while (!AtEnd()) { + char16_t c = Peek(); + if (c == '"' || c == '\'') { + if (!SkipString(c)) { + return false; + } + continue; + } + if (StartsWithComment()) { + if (!SkipComment()) { + return false; + } + continue; + } + if (c == '{') { + ++depth; + ++mPos; + continue; + } + if (c == '}') { + if (depth == 0) { + aBody.Assign(Substring(mInput, start, mPos - start)); + ++mPos; + return true; + } + --depth; + ++mPos; + continue; + } + ++mPos; + } + + return false; + } + + bool + ReadQualifiedRulePrelude(nsAString& aPrelude) + { + uint32_t start = mPos; + int32_t parenDepth = 0; + int32_t bracketDepth = 0; + + while (!AtEnd()) { + char16_t c = Peek(); + if (c == '"' || c == '\'') { + if (!SkipString(c)) { + return false; + } + continue; + } + if (StartsWithComment()) { + if (!SkipComment()) { + return false; + } + continue; + } + if (c == '(') { + ++parenDepth; + ++mPos; + continue; + } + if (c == ')' && parenDepth > 0) { + --parenDepth; + ++mPos; + continue; + } + if (c == '[') { + ++bracketDepth; + ++mPos; + continue; + } + if (c == ']' && bracketDepth > 0) { + --bracketDepth; + ++mPos; + continue; + } + if (c == '{' && parenDepth == 0 && bracketDepth == 0) { + aPrelude.Assign(Substring(mInput, start, mPos - start)); + TrimWhitespace(aPrelude); + ++mPos; + return !aPrelude.IsEmpty(); + } + if ((c == ';' || c == '}') && parenDepth == 0 && bracketDepth == 0) { + return false; + } + ++mPos; + } + + return false; + } + + bool + ReadAtRulePrelude(nsAString& aPrelude, nsACString& aName, bool& aHasBlock) + { + MOZ_ASSERT(!AtEnd() && Peek() == '@', "expected at-rule"); + + uint32_t start = mPos; + ++mPos; + aName.Truncate(); + while (!AtEnd() && IsAtRuleNameChar(Peek())) { + char16_t c = Peek(); + aName.Append(char(c <= 0x7f ? c : '?')); + ++mPos; + } + LowercaseASCII(aName); + + int32_t parenDepth = 0; + int32_t bracketDepth = 0; + while (!AtEnd()) { + char16_t c = Peek(); + if (c == '"' || c == '\'') { + if (!SkipString(c)) { + return false; + } + continue; + } + if (StartsWithComment()) { + if (!SkipComment()) { + return false; + } + continue; + } + if (c == '(') { + ++parenDepth; + ++mPos; + continue; + } + if (c == ')' && parenDepth > 0) { + --parenDepth; + ++mPos; + continue; + } + if (c == '[') { + ++bracketDepth; + ++mPos; + continue; + } + if (c == ']' && bracketDepth > 0) { + --bracketDepth; + ++mPos; + continue; + } + if (parenDepth == 0 && bracketDepth == 0) { + if (c == ';') { + aPrelude.Assign(Substring(mInput, start, mPos - start)); + TrimWhitespace(aPrelude); + ++mPos; + aHasBlock = false; + return true; + } + if (c == '{') { + aPrelude.Assign(Substring(mInput, start, mPos - start)); + TrimWhitespace(aPrelude); + ++mPos; + aHasBlock = true; + return true; + } + } + ++mPos; + } + + return false; + } + + bool + ConsumeDeclaration(nsAString& aDeclaration) + { + uint32_t start = mPos; + int32_t parenDepth = 0; + int32_t bracketDepth = 0; + int32_t braceDepth = 0; + + while (!AtEnd()) { + char16_t c = Peek(); + if (c == '"' || c == '\'') { + if (!SkipString(c)) { + return false; + } + continue; + } + if (StartsWithComment()) { + if (!SkipComment()) { + return false; + } + continue; + } + if (c == '(') { + ++parenDepth; + ++mPos; + continue; + } + if (c == ')' && parenDepth > 0) { + --parenDepth; + ++mPos; + continue; + } + if (c == '[') { + ++bracketDepth; + ++mPos; + continue; + } + if (c == ']' && bracketDepth > 0) { + --bracketDepth; + ++mPos; + continue; + } + if (c == '{') { + ++braceDepth; + ++mPos; + continue; + } + if (c == '}') { + if (braceDepth == 0 && parenDepth == 0 && bracketDepth == 0) { + break; + } + if (braceDepth > 0) { + --braceDepth; + } + ++mPos; + continue; + } + if (c == ';' && parenDepth == 0 && bracketDepth == 0 && + braceDepth == 0) { + ++mPos; + break; + } + ++mPos; + } + + aDeclaration.Assign(Substring(mInput, start, mPos - start)); + TrimWhitespace(aDeclaration); + if (aDeclaration.IsEmpty()) { + return false; + } + if (aDeclaration.Last() != ';') { + aDeclaration.Append(';'); + } + return true; + } + + bool + ParseAtRule(nsAString& aOutput, const SelectorList* aParents) + { + nsAutoString prelude; + nsAutoCString name; + bool hasBlock = false; + if (!ReadAtRulePrelude(prelude, name, hasBlock)) { + return false; + } + + if (!hasBlock) { + aOutput.Append(prelude); + aOutput.AppendLiteral(";\n"); + return true; + } + + if (!ShouldProcessGroupRule(name)) { + nsAutoString body; + if (!ReadRawBlockBody(body)) { + return false; + } + aOutput.Append(prelude); + aOutput.AppendLiteral(" {"); + aOutput.Append(body); + aOutput.AppendLiteral("}\n"); + return true; + } + + nsAutoString inner; + if (aParents) { + mSawNesting = true; + if (!ProcessStyleContext(*aParents, inner)) { + return false; + } + } else { + if (!ProcessStylesheet(inner, true)) { + return false; + } + } + + aOutput.Append(prelude); + aOutput.AppendLiteral(" {\n"); + aOutput.Append(inner); + aOutput.AppendLiteral("}\n"); + return true; + } + + bool + ParseQualifiedRule(nsAString& aOutput, const SelectorList* aParents) + { + nsAutoString prelude; + if (!ReadQualifiedRulePrelude(prelude)) { + return false; + } + + SelectorList selectors; + if (aParents) { + mSawNesting = true; + if (!ExpandNestedSelectors(*aParents, prelude, selectors)) { + return false; + } + } else if (!SplitSelectorList(prelude, selectors)) { + return false; + } + + return ProcessStyleContext(selectors, aOutput); + } + + bool + ProcessStyleContext(const SelectorList& aSelectors, nsAString& aOutput) + { + nsAutoString declarations; + + while (!AtEnd()) { + SkipWhitespaceAndComments(); + if (AtEnd()) { + return false; + } + + char16_t c = Peek(); + if (c == '}') { + ++mPos; + FlushDeclarations(aSelectors, declarations, aOutput); + return true; + } + + if (c == '@') { + FlushDeclarations(aSelectors, declarations, aOutput); + if (!ParseAtRule(aOutput, &aSelectors)) { + return false; + } + continue; + } + + if (StartsNestedSelector(c)) { + FlushDeclarations(aSelectors, declarations, aOutput); + if (!ParseQualifiedRule(aOutput, &aSelectors)) { + return false; + } + continue; + } + + nsAutoString declaration; + if (!ConsumeDeclaration(declaration)) { + return false; + } + if (!declarations.IsEmpty()) { + declarations.Append(' '); + } + declarations.Append(declaration); + } + + return false; + } + + bool + ProcessStylesheet(nsAString& aOutput, bool aStopAtBlockEnd) + { + while (!AtEnd()) { + SkipWhitespaceAndComments(); + if (AtEnd()) { + return !aStopAtBlockEnd; + } + + if (Peek() == '}') { + if (!aStopAtBlockEnd) { + return false; + } + ++mPos; + return true; + } + + if (Peek() == '@') { + if (!ParseAtRule(aOutput, nullptr)) { + return false; + } + } else { + if (!ParseQualifiedRule(aOutput, nullptr)) { + return false; + } + } + } + + return !aStopAtBlockEnd; + } + + const nsAString& mInput; + uint32_t mPos; + bool mSawNesting; +}; + static_assert(css::eAuthorSheetFeatures == 0 && css::eUserSheetFeatures == 1 && css::eAgentSheetFeatures == 2, @@ -1823,7 +2662,16 @@ CSSParserImpl::ParseSheet(const nsAString& aInput, "Sheet principal does not match passed principal"); #endif - nsCSSScanner scanner(aInput, aLineNumber); + nsAutoString loweredInput; + const nsAString* input = &aInput; + if (sNestingEnabled) { + CSSNestingLowerer lowerer(aInput); + if (lowerer.Lower(loweredInput)) { + input = &loweredInput; + } + } + + nsCSSScanner scanner(*input, aLineNumber); css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aSheetURI); InitScanner(scanner, reporter, aSheetURI, aBaseURI, aSheetPrincipal); @@ -18987,6 +19835,8 @@ nsCSSParser::Startup() "layout.css.legacy-negation-pseudo.enabled"); Preferences::AddBoolVarCache(&sCascadeLayersEnabled, "layout.css.cascade-layers.enabled"); + Preferences::AddBoolVarCache(&sNestingEnabled, + "layout.css.nesting.enabled"); } nsCSSParser::nsCSSParser(mozilla::css::Loader* aLoader, diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index dfdfb84b2b..6dee3b429a 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -2725,6 +2725,9 @@ pref("layout.css.resizeobserver.enabled", true); // Is support for cascade layers enabled? pref("layout.css.cascade-layers.enabled", true); +// Is support for basic CSS nesting lowering enabled? +pref("layout.css.nesting.enabled", false); + // Should rules in imported style sheets be added based on the order // of appearance of their respective @import rules in the parent // style sheet? Otherwise, they are added before rules preceding @@ -3230,7 +3233,7 @@ pref("ui.mouse.radius.inputSource.touchOnly", true); #ifdef XP_WIN -// Be as uniform as possible, use Twemoji everywhere. +// Be as uniform as possible, use Twemoji everywhere. // Optional: prefix with `Segoe UI Emoji` to use Win8+ Segoe UI font emoji where available. pref("font.name-list.emoji", "Twemoji Mozilla"); @@ -4761,7 +4764,7 @@ pref("media.ondevicechange.fakeDeviceChangeEvent.enabled", false); // those platforms we don't handle touch events anyway so it's conceptually // a no-op. pref("layout.css.touch_action.enabled", true); - + // WHATWG computed intrinsic aspect ratio for an img element // https://html.spec.whatwg.org/multipage/rendering.html#attributes-for-embedded-content-and-images // Are the width and height attributes on image-like elements mapped to the @@ -5273,7 +5276,7 @@ pref("plugins.navigator_hide_disabled_flash", false); pref("dom.mozBrowserFramesEnabled", false); // Thick caret when behind CJK characters -pref("layout.cjkthickcaret", true); +pref("layout.cjkthickcaret", true); // Is support for 'color-adjust' CSS property enabled? pref("layout.css.color-adjust.enabled", true); From 54262457d2b10826f00d752b50663f8855458cbe Mon Sep 17 00:00:00 2001 From: Basilisk-Dev Date: Wed, 11 Mar 2026 22:08:14 -0400 Subject: [PATCH 02/21] Issue #2862 - add initial nested css test file --- layout/style/test/mochitest.ini | 1 + .../test/test_basic_nesting_lowering.html | 130 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 layout/style/test/test_basic_nesting_lowering.html diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini index 8cadc7dd51..d4ad77d9a4 100644 --- a/layout/style/test/mochitest.ini +++ b/layout/style/test/mochitest.ini @@ -72,6 +72,7 @@ support-files = file_animations_with_disabled_properties.html [test_attribute_selector_eof_behavior.html] [test_aspect_ratio_property.html] [test_background_blend_mode.html] +[test_basic_nesting_lowering.html] [test_box_size_keywords.html] [test_bug73586.html] [test_css_math_functions.html] diff --git a/layout/style/test/test_basic_nesting_lowering.html b/layout/style/test/test_basic_nesting_lowering.html new file mode 100644 index 0000000000..616715ede3 --- /dev/null +++ b/layout/style/test/test_basic_nesting_lowering.html @@ -0,0 +1,130 @@ + + + + + Test for Basic CSS Nesting Lowering + + + + +
+ +
+
+

+
+
+

From c794e74cf0636119b27c96c9b0a5c661318ba6f2 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Wed, 11 Mar 2026 22:25:48 -0400
Subject: [PATCH 03/21] Issue #2862 - Modify the css lowerer so that the
 lowering pass now recognizes bare type-selector nested rules and
 combinator-led cases

---
 layout/style/nsCSSParser.cpp                  |  99 ++++++-
 .../test/test_basic_nesting_lowering.html     | 261 ++++++++++--------
 2 files changed, 242 insertions(+), 118 deletions(-)

diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp
index 02f926a5a4..10012a3f93 100644
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -597,6 +597,103 @@ private:
     }
   }
 
+  static bool
+  StartsPotentialTypeSelector(char16_t aChar)
+  {
+    return (aChar >= 'a' && aChar <= 'z') ||
+           (aChar >= 'A' && aChar <= 'Z') ||
+           aChar == '_' ||
+           aChar == '\\' ||
+           aChar >= 0x80;
+  }
+
+  bool
+  LooksLikeTypeSelectorRule() const
+  {
+    if (AtEnd() || !StartsPotentialTypeSelector(Peek())) {
+      return false;
+    }
+
+    uint32_t pos = mPos;
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+    bool inComment = false;
+    char16_t stringQuote = 0;
+
+    while (pos < mInput.Length()) {
+      char16_t c = mInput.CharAt(pos);
+
+      if (inComment) {
+        if (c == '*' && pos + 1 < mInput.Length() &&
+            mInput.CharAt(pos + 1) == '/') {
+          inComment = false;
+          ++pos;
+        }
+        ++pos;
+        continue;
+      }
+
+      if (stringQuote) {
+        if (c == '\\' && pos + 1 < mInput.Length()) {
+          pos += 2;
+          continue;
+        }
+        if (c == stringQuote) {
+          stringQuote = 0;
+        }
+        ++pos;
+        continue;
+      }
+
+      if (c == '/' && pos + 1 < mInput.Length() &&
+          mInput.CharAt(pos + 1) == '*') {
+        inComment = true;
+        pos += 2;
+        continue;
+      }
+
+      if (c == '"' || c == '\'') {
+        stringQuote = c;
+        ++pos;
+        continue;
+      }
+
+      if (c == '(') {
+        ++parenDepth;
+        ++pos;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        ++pos;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        ++pos;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        ++pos;
+        continue;
+      }
+
+      if (parenDepth == 0 && bracketDepth == 0) {
+        if (c == '{') {
+          return true;
+        }
+        if (c == ':' || c == ';' || c == '}') {
+          return false;
+        }
+      }
+
+      ++pos;
+    }
+
+    return false;
+  }
+
   static bool
   IsAtRuleNameChar(char16_t aChar)
   {
@@ -986,7 +1083,7 @@ private:
         continue;
       }
 
-      if (StartsNestedSelector(c)) {
+      if (StartsNestedSelector(c) || LooksLikeTypeSelectorRule()) {
         FlushDeclarations(aSelectors, declarations, aOutput);
         if (!ParseQualifiedRule(aOutput, &aSelectors)) {
           return false;
diff --git a/layout/style/test/test_basic_nesting_lowering.html b/layout/style/test/test_basic_nesting_lowering.html
index 616715ede3..8f3f175632 100644
--- a/layout/style/test/test_basic_nesting_lowering.html
+++ b/layout/style/test/test_basic_nesting_lowering.html
@@ -1,130 +1,157 @@
-
+
 
-
-  
-  Test for Basic CSS Nesting Lowering
-  
-  
-
-
-
- -
-
-

-
+        
+    
+    
+        
+ +
+
+

+        
-
+            if (typeof SimpleTest !== "undefined" && typeof SpecialPowers !== "undefined") {
+                runMochitest();
+            } else {
+                runStandalone();
+            }
+        
+    
 

From f2560c57b6bc8154dffe2b3ebe93bb0edc59a519 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Wed, 11 Mar 2026 22:49:25 -0400
Subject: [PATCH 04/21] Issue #2862 - Fix a few nested selectors

---
 layout/style/nsCSSParser.cpp                  |   2 +-
 .../test/test_basic_nesting_lowering.html     | 277 +++++++++---------
 2 files changed, 134 insertions(+), 145 deletions(-)

diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp
index 10012a3f93..484085b019 100644
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -683,7 +683,7 @@ private:
         if (c == '{') {
           return true;
         }
-        if (c == ':' || c == ';' || c == '}') {
+        if (c == ';' || c == '}') {
           return false;
         }
       }
diff --git a/layout/style/test/test_basic_nesting_lowering.html b/layout/style/test/test_basic_nesting_lowering.html
index 8f3f175632..6c08934a11 100644
--- a/layout/style/test/test_basic_nesting_lowering.html
+++ b/layout/style/test/test_basic_nesting_lowering.html
@@ -1,157 +1,146 @@
-
+
 
-    
-        
-        Test for Basic CSS Nesting Lowering
-        
-        
-    
-    
-        
- -
-
-

-        
+  
+
+
+
+ +
+
+

+
-    
+if (typeof SimpleTest !== "undefined" && typeof SpecialPowers !== "undefined") {
+  runMochitest();
+} else {
+  runStandalone();
+}
+
+
 

From c12883cfcff94efb65aa90b2bf092247b0ff6482 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Wed, 11 Mar 2026 22:59:52 -0400
Subject: [PATCH 05/21] Issue #2862 - Split the css lowering functionality out
 to a separate file for easier maintenance

---
 layout/style/CSSNestingLowerer.cpp | 961 +++++++++++++++++++++++++++++
 layout/style/CSSNestingLowerer.h   |  19 +
 layout/style/moz.build             |   1 +
 layout/style/nsCSSParser.cpp       | 943 +---------------------------
 4 files changed, 985 insertions(+), 939 deletions(-)
 create mode 100644 layout/style/CSSNestingLowerer.cpp
 create mode 100644 layout/style/CSSNestingLowerer.h

diff --git a/layout/style/CSSNestingLowerer.cpp b/layout/style/CSSNestingLowerer.cpp
new file mode 100644
index 0000000000..9340c4c5af
--- /dev/null
+++ b/layout/style/CSSNestingLowerer.cpp
@@ -0,0 +1,961 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CSSNestingLowerer.h"
+
+#include "mozilla/Assertions.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace css {
+
+namespace {
+
+class CSSNestingLowerer final
+{
+  using SelectorList = nsTArray;
+
+public:
+  explicit CSSNestingLowerer(const nsAString& aInput)
+    : mInput(aInput)
+    , mPos(0)
+    , mSawNesting(false)
+  {
+  }
+
+  bool Lower(nsAString& aOutput)
+  {
+    nsAutoString lowered;
+    if (!ProcessStylesheet(lowered, false)) {
+      return false;
+    }
+
+    SkipWhitespaceAndComments();
+    if (mPos != mInput.Length() || !mSawNesting) {
+      return false;
+    }
+
+    aOutput.Assign(lowered);
+    return true;
+  }
+
+private:
+  static constexpr auto kCSSWhitespace = " \t\r\n\f";
+
+  static bool
+  IsCSSWhitespace(char16_t aChar)
+  {
+    return aChar == ' ' || aChar == '\t' || aChar == '\r' ||
+           aChar == '\n' || aChar == '\f';
+  }
+
+  bool
+  AtEnd() const
+  {
+    return mPos >= mInput.Length();
+  }
+
+  char16_t
+  Peek() const
+  {
+    MOZ_ASSERT(!AtEnd(), "cannot peek past end");
+    return mInput.CharAt(mPos);
+  }
+
+  bool
+  StartsWithComment() const
+  {
+    return mPos + 1 < mInput.Length() &&
+           mInput.CharAt(mPos) == '/' &&
+           mInput.CharAt(mPos + 1) == '*';
+  }
+
+  bool
+  SkipComment()
+  {
+    MOZ_ASSERT(StartsWithComment(), "expected comment");
+
+    mPos += 2;
+    while (mPos + 1 < mInput.Length()) {
+      if (mInput.CharAt(mPos) == '*' && mInput.CharAt(mPos + 1) == '/') {
+        mPos += 2;
+        return true;
+      }
+      ++mPos;
+    }
+
+    return false;
+  }
+
+  void
+  SkipWhitespaceAndComments()
+  {
+    while (!AtEnd()) {
+      if (IsCSSWhitespace(Peek())) {
+        ++mPos;
+        continue;
+      }
+      if (StartsWithComment()) {
+        if (!SkipComment()) {
+          mPos = mInput.Length();
+          return;
+        }
+        continue;
+      }
+      break;
+    }
+  }
+
+  bool
+  SkipString(char16_t aQuote)
+  {
+    MOZ_ASSERT(!AtEnd() && Peek() == aQuote, "expected string start");
+
+    ++mPos;
+    while (!AtEnd()) {
+      char16_t c = Peek();
+      ++mPos;
+      if (c == aQuote) {
+        return true;
+      }
+      if (c == '\\' && !AtEnd()) {
+        ++mPos;
+        continue;
+      }
+      if (c == '\n' || c == '\r' || c == '\f') {
+        return false;
+      }
+    }
+
+    return false;
+  }
+
+  static void
+  TrimWhitespace(nsAString& aText)
+  {
+    uint32_t start = 0;
+    uint32_t end = aText.Length();
+
+    while (start < end && IsCSSWhitespace(aText.CharAt(start))) {
+      ++start;
+    }
+    while (end > start && IsCSSWhitespace(aText.CharAt(end - 1))) {
+      --end;
+    }
+
+    if (start == 0 && end == aText.Length()) {
+      return;
+    }
+
+    aText.Assign(Substring(aText, start, end - start));
+  }
+
+  bool
+  SplitSelectorList(const nsAString& aSelectorText, SelectorList& aSelectors)
+  {
+    uint32_t itemStart = 0;
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+    bool inComment = false;
+    char16_t stringQuote = 0;
+
+    for (uint32_t i = 0; i < aSelectorText.Length(); ++i) {
+      char16_t c = aSelectorText.CharAt(i);
+
+      if (inComment) {
+        if (c == '*' && i + 1 < aSelectorText.Length() &&
+            aSelectorText.CharAt(i + 1) == '/') {
+          inComment = false;
+          ++i;
+        }
+        continue;
+      }
+
+      if (stringQuote) {
+        if (c == '\\') {
+          ++i;
+          continue;
+        }
+        if (c == stringQuote) {
+          stringQuote = 0;
+        }
+        continue;
+      }
+
+      if (c == '/' && i + 1 < aSelectorText.Length() &&
+          aSelectorText.CharAt(i + 1) == '*') {
+        inComment = true;
+        ++i;
+        continue;
+      }
+
+      if (c == '"' || c == '\'') {
+        stringQuote = c;
+        continue;
+      }
+
+      if (c == '(') {
+        ++parenDepth;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        continue;
+      }
+
+      if (c == ',' && parenDepth == 0 && bracketDepth == 0) {
+        nsAutoString selector;
+        selector.Assign(Substring(aSelectorText, itemStart, i - itemStart));
+        TrimWhitespace(selector);
+        if (!selector.IsEmpty()) {
+          aSelectors.AppendElement(selector);
+        }
+        itemStart = i + 1;
+      }
+    }
+
+    nsAutoString selector;
+    selector.Assign(Substring(aSelectorText, itemStart));
+    TrimWhitespace(selector);
+    if (!selector.IsEmpty()) {
+      aSelectors.AppendElement(selector);
+    }
+
+    return !aSelectors.IsEmpty();
+  }
+
+  bool
+  SelectorHasAmpersand(const nsAString& aSelector) const
+  {
+    bool inComment = false;
+    char16_t stringQuote = 0;
+
+    for (uint32_t i = 0; i < aSelector.Length(); ++i) {
+      char16_t c = aSelector.CharAt(i);
+
+      if (inComment) {
+        if (c == '*' && i + 1 < aSelector.Length() &&
+            aSelector.CharAt(i + 1) == '/') {
+          inComment = false;
+          ++i;
+        }
+        continue;
+      }
+
+      if (stringQuote) {
+        if (c == '\\') {
+          ++i;
+          continue;
+        }
+        if (c == stringQuote) {
+          stringQuote = 0;
+        }
+        continue;
+      }
+
+      if (c == '/' && i + 1 < aSelector.Length() &&
+          aSelector.CharAt(i + 1) == '*') {
+        inComment = true;
+        ++i;
+        continue;
+      }
+
+      if (c == '"' || c == '\'') {
+        stringQuote = c;
+        continue;
+      }
+
+      if (c == '&') {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  void
+  ReplaceAmpersands(const nsAString& aSelector,
+                    const nsAString& aParent,
+                    nsAString& aOutput) const
+  {
+    bool inComment = false;
+    char16_t stringQuote = 0;
+
+    for (uint32_t i = 0; i < aSelector.Length(); ++i) {
+      char16_t c = aSelector.CharAt(i);
+
+      if (inComment) {
+        aOutput.Append(c);
+        if (c == '*' && i + 1 < aSelector.Length() &&
+            aSelector.CharAt(i + 1) == '/') {
+          aOutput.Append('/');
+          inComment = false;
+          ++i;
+        }
+        continue;
+      }
+
+      if (stringQuote) {
+        aOutput.Append(c);
+        if (c == '\\' && i + 1 < aSelector.Length()) {
+          aOutput.Append(aSelector.CharAt(i + 1));
+          ++i;
+          continue;
+        }
+        if (c == stringQuote) {
+          stringQuote = 0;
+        }
+        continue;
+      }
+
+      if (c == '/' && i + 1 < aSelector.Length() &&
+          aSelector.CharAt(i + 1) == '*') {
+        aOutput.AppendLiteral("/*");
+        inComment = true;
+        ++i;
+        continue;
+      }
+
+      if (c == '"' || c == '\'') {
+        aOutput.Append(c);
+        stringQuote = c;
+        continue;
+      }
+
+      if (c == '&') {
+        aOutput.Append(aParent);
+        continue;
+      }
+
+      aOutput.Append(c);
+    }
+  }
+
+  bool
+  ExpandNestedSelectors(const SelectorList& aParents,
+                        const nsAString& aNestedSelectorText,
+                        SelectorList& aSelectors)
+  {
+    SelectorList nestedSelectors;
+    if (!SplitSelectorList(aNestedSelectorText, nestedSelectors)) {
+      return false;
+    }
+
+    for (const nsString& nestedSelector : nestedSelectors) {
+      bool hasAmpersand = SelectorHasAmpersand(nestedSelector);
+      for (const nsString& parentSelector : aParents) {
+        nsAutoString combined;
+        if (hasAmpersand) {
+          ReplaceAmpersands(nestedSelector, parentSelector, combined);
+        } else {
+          combined.Assign(parentSelector);
+          if (!combined.IsEmpty()) {
+            combined.Append(' ');
+          }
+          combined.Append(nestedSelector);
+        }
+        TrimWhitespace(combined);
+        if (!combined.IsEmpty()) {
+          aSelectors.AppendElement(combined);
+        }
+      }
+    }
+
+    return !aSelectors.IsEmpty();
+  }
+
+  static void
+  AppendSelectors(const SelectorList& aSelectors, nsAString& aOutput)
+  {
+    for (uint32_t i = 0; i < aSelectors.Length(); ++i) {
+      if (i) {
+        aOutput.AppendLiteral(", ");
+      }
+      aOutput.Append(aSelectors[i]);
+    }
+  }
+
+  static bool
+  StartsNestedSelector(char16_t aChar)
+  {
+    switch (aChar) {
+      case '.':
+      case '#':
+      case '[':
+      case ':':
+      case '&':
+      case '>':
+      case '+':
+      case '~':
+      case '*':
+        return true;
+      default:
+        return false;
+    }
+  }
+
+  static bool
+  StartsPotentialTypeSelector(char16_t aChar)
+  {
+    return (aChar >= 'a' && aChar <= 'z') ||
+           (aChar >= 'A' && aChar <= 'Z') ||
+           aChar == '_' ||
+           aChar == '\\' ||
+           aChar >= 0x80;
+  }
+
+  bool
+  LooksLikeTypeSelectorRule() const
+  {
+    if (AtEnd() || !StartsPotentialTypeSelector(Peek())) {
+      return false;
+    }
+
+    uint32_t pos = mPos;
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+    bool inComment = false;
+    char16_t stringQuote = 0;
+
+    while (pos < mInput.Length()) {
+      char16_t c = mInput.CharAt(pos);
+
+      if (inComment) {
+        if (c == '*' && pos + 1 < mInput.Length() &&
+            mInput.CharAt(pos + 1) == '/') {
+          inComment = false;
+          ++pos;
+        }
+        ++pos;
+        continue;
+      }
+
+      if (stringQuote) {
+        if (c == '\\' && pos + 1 < mInput.Length()) {
+          pos += 2;
+          continue;
+        }
+        if (c == stringQuote) {
+          stringQuote = 0;
+        }
+        ++pos;
+        continue;
+      }
+
+      if (c == '/' && pos + 1 < mInput.Length() &&
+          mInput.CharAt(pos + 1) == '*') {
+        inComment = true;
+        pos += 2;
+        continue;
+      }
+
+      if (c == '"' || c == '\'') {
+        stringQuote = c;
+        ++pos;
+        continue;
+      }
+
+      if (c == '(') {
+        ++parenDepth;
+        ++pos;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        ++pos;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        ++pos;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        ++pos;
+        continue;
+      }
+
+      if (parenDepth == 0 && bracketDepth == 0) {
+        if (c == '{') {
+          return true;
+        }
+        if (c == ';' || c == '}') {
+          return false;
+        }
+      }
+
+      ++pos;
+    }
+
+    return false;
+  }
+
+  static bool
+  IsAtRuleNameChar(char16_t aChar)
+  {
+    return (aChar >= 'a' && aChar <= 'z') ||
+           (aChar >= 'A' && aChar <= 'Z') ||
+           (aChar >= '0' && aChar <= '9') ||
+           aChar == '-';
+  }
+
+  static void
+  LowercaseASCII(nsACString& aText)
+  {
+    for (uint32_t i = 0; i < aText.Length(); ++i) {
+      char c = aText.CharAt(i);
+      if (c >= 'A' && c <= 'Z') {
+        aText.BeginWriting()[i] = c - 'A' + 'a';
+      }
+    }
+  }
+
+  static bool
+  ShouldProcessGroupRule(const nsACString& aName)
+  {
+    return aName.EqualsLiteral("media") ||
+           aName.EqualsLiteral("supports") ||
+           aName.EqualsLiteral("document") ||
+           aName.EqualsLiteral("layer");
+  }
+
+  void
+  FlushDeclarations(const SelectorList& aSelectors,
+                    nsAString& aDeclarations,
+                    nsAString& aOutput)
+  {
+    nsAutoString declarations;
+    declarations.Assign(aDeclarations);
+    TrimWhitespace(declarations);
+    aDeclarations.Truncate();
+
+    if (declarations.IsEmpty()) {
+      return;
+    }
+
+    AppendSelectors(aSelectors, aOutput);
+    aOutput.AppendLiteral(" { ");
+    aOutput.Append(declarations);
+    aOutput.AppendLiteral(" }\n");
+  }
+
+  bool
+  ReadRawBlockBody(nsAString& aBody)
+  {
+    uint32_t start = mPos;
+    int32_t depth = 0;
+
+    while (!AtEnd()) {
+      char16_t c = Peek();
+      if (c == '"' || c == '\'') {
+        if (!SkipString(c)) {
+          return false;
+        }
+        continue;
+      }
+      if (StartsWithComment()) {
+        if (!SkipComment()) {
+          return false;
+        }
+        continue;
+      }
+      if (c == '{') {
+        ++depth;
+        ++mPos;
+        continue;
+      }
+      if (c == '}') {
+        if (depth == 0) {
+          aBody.Assign(Substring(mInput, start, mPos - start));
+          ++mPos;
+          return true;
+        }
+        --depth;
+        ++mPos;
+        continue;
+      }
+      ++mPos;
+    }
+
+    return false;
+  }
+
+  bool
+  ReadQualifiedRulePrelude(nsAString& aPrelude)
+  {
+    uint32_t start = mPos;
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+
+    while (!AtEnd()) {
+      char16_t c = Peek();
+      if (c == '"' || c == '\'') {
+        if (!SkipString(c)) {
+          return false;
+        }
+        continue;
+      }
+      if (StartsWithComment()) {
+        if (!SkipComment()) {
+          return false;
+        }
+        continue;
+      }
+      if (c == '(') {
+        ++parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '{' && parenDepth == 0 && bracketDepth == 0) {
+        aPrelude.Assign(Substring(mInput, start, mPos - start));
+        TrimWhitespace(aPrelude);
+        ++mPos;
+        return !aPrelude.IsEmpty();
+      }
+      if ((c == ';' || c == '}') && parenDepth == 0 && bracketDepth == 0) {
+        return false;
+      }
+      ++mPos;
+    }
+
+    return false;
+  }
+
+  bool
+  ReadAtRulePrelude(nsAString& aPrelude, nsACString& aName, bool& aHasBlock)
+  {
+    MOZ_ASSERT(!AtEnd() && Peek() == '@', "expected at-rule");
+
+    uint32_t start = mPos;
+    ++mPos;
+    aName.Truncate();
+    while (!AtEnd() && IsAtRuleNameChar(Peek())) {
+      char16_t c = Peek();
+      aName.Append(char(c <= 0x7f ? c : '?'));
+      ++mPos;
+    }
+    LowercaseASCII(aName);
+
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+    while (!AtEnd()) {
+      char16_t c = Peek();
+      if (c == '"' || c == '\'') {
+        if (!SkipString(c)) {
+          return false;
+        }
+        continue;
+      }
+      if (StartsWithComment()) {
+        if (!SkipComment()) {
+          return false;
+        }
+        continue;
+      }
+      if (c == '(') {
+        ++parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (parenDepth == 0 && bracketDepth == 0) {
+        if (c == ';') {
+          aPrelude.Assign(Substring(mInput, start, mPos - start));
+          TrimWhitespace(aPrelude);
+          ++mPos;
+          aHasBlock = false;
+          return true;
+        }
+        if (c == '{') {
+          aPrelude.Assign(Substring(mInput, start, mPos - start));
+          TrimWhitespace(aPrelude);
+          ++mPos;
+          aHasBlock = true;
+          return true;
+        }
+      }
+      ++mPos;
+    }
+
+    return false;
+  }
+
+  bool
+  ConsumeDeclaration(nsAString& aDeclaration)
+  {
+    uint32_t start = mPos;
+    int32_t parenDepth = 0;
+    int32_t bracketDepth = 0;
+    int32_t braceDepth = 0;
+
+    while (!AtEnd()) {
+      char16_t c = Peek();
+      if (c == '"' || c == '\'') {
+        if (!SkipString(c)) {
+          return false;
+        }
+        continue;
+      }
+      if (StartsWithComment()) {
+        if (!SkipComment()) {
+          return false;
+        }
+        continue;
+      }
+      if (c == '(') {
+        ++parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ')' && parenDepth > 0) {
+        --parenDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '[') {
+        ++bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == ']' && bracketDepth > 0) {
+        --bracketDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '{') {
+        ++braceDepth;
+        ++mPos;
+        continue;
+      }
+      if (c == '}') {
+        if (braceDepth == 0 && parenDepth == 0 && bracketDepth == 0) {
+          break;
+        }
+        if (braceDepth > 0) {
+          --braceDepth;
+        }
+        ++mPos;
+        continue;
+      }
+      if (c == ';' && parenDepth == 0 && bracketDepth == 0 &&
+          braceDepth == 0) {
+        ++mPos;
+        break;
+      }
+      ++mPos;
+    }
+
+    aDeclaration.Assign(Substring(mInput, start, mPos - start));
+    TrimWhitespace(aDeclaration);
+    if (aDeclaration.IsEmpty()) {
+      return false;
+    }
+    if (aDeclaration.Last() != ';') {
+      aDeclaration.Append(';');
+    }
+    return true;
+  }
+
+  bool
+  ParseAtRule(nsAString& aOutput, const SelectorList* aParents)
+  {
+    nsAutoString prelude;
+    nsAutoCString name;
+    bool hasBlock = false;
+    if (!ReadAtRulePrelude(prelude, name, hasBlock)) {
+      return false;
+    }
+
+    if (!hasBlock) {
+      aOutput.Append(prelude);
+      aOutput.AppendLiteral(";\n");
+      return true;
+    }
+
+    if (!ShouldProcessGroupRule(name)) {
+      nsAutoString body;
+      if (!ReadRawBlockBody(body)) {
+        return false;
+      }
+      aOutput.Append(prelude);
+      aOutput.AppendLiteral(" {");
+      aOutput.Append(body);
+      aOutput.AppendLiteral("}\n");
+      return true;
+    }
+
+    nsAutoString inner;
+    if (aParents) {
+      mSawNesting = true;
+      if (!ProcessStyleContext(*aParents, inner)) {
+        return false;
+      }
+    } else {
+      if (!ProcessStylesheet(inner, true)) {
+        return false;
+      }
+    }
+
+    aOutput.Append(prelude);
+    aOutput.AppendLiteral(" {\n");
+    aOutput.Append(inner);
+    aOutput.AppendLiteral("}\n");
+    return true;
+  }
+
+  bool
+  ParseQualifiedRule(nsAString& aOutput, const SelectorList* aParents)
+  {
+    nsAutoString prelude;
+    if (!ReadQualifiedRulePrelude(prelude)) {
+      return false;
+    }
+
+    SelectorList selectors;
+    if (aParents) {
+      mSawNesting = true;
+      if (!ExpandNestedSelectors(*aParents, prelude, selectors)) {
+        return false;
+      }
+    } else if (!SplitSelectorList(prelude, selectors)) {
+      return false;
+    }
+
+    return ProcessStyleContext(selectors, aOutput);
+  }
+
+  bool
+  ProcessStyleContext(const SelectorList& aSelectors, nsAString& aOutput)
+  {
+    nsAutoString declarations;
+
+    while (!AtEnd()) {
+      SkipWhitespaceAndComments();
+      if (AtEnd()) {
+        return false;
+      }
+
+      char16_t c = Peek();
+      if (c == '}') {
+        ++mPos;
+        FlushDeclarations(aSelectors, declarations, aOutput);
+        return true;
+      }
+
+      if (c == '@') {
+        FlushDeclarations(aSelectors, declarations, aOutput);
+        if (!ParseAtRule(aOutput, &aSelectors)) {
+          return false;
+        }
+        continue;
+      }
+
+      if (StartsNestedSelector(c) || LooksLikeTypeSelectorRule()) {
+        FlushDeclarations(aSelectors, declarations, aOutput);
+        if (!ParseQualifiedRule(aOutput, &aSelectors)) {
+          return false;
+        }
+        continue;
+      }
+
+      nsAutoString declaration;
+      if (!ConsumeDeclaration(declaration)) {
+        return false;
+      }
+      if (!declarations.IsEmpty()) {
+        declarations.Append(' ');
+      }
+      declarations.Append(declaration);
+    }
+
+    return false;
+  }
+
+  bool
+  ProcessStylesheet(nsAString& aOutput, bool aStopAtBlockEnd)
+  {
+    while (!AtEnd()) {
+      SkipWhitespaceAndComments();
+      if (AtEnd()) {
+        return !aStopAtBlockEnd;
+      }
+
+      if (Peek() == '}') {
+        if (!aStopAtBlockEnd) {
+          return false;
+        }
+        ++mPos;
+        return true;
+      }
+
+      if (Peek() == '@') {
+        if (!ParseAtRule(aOutput, nullptr)) {
+          return false;
+        }
+      } else {
+        if (!ParseQualifiedRule(aOutput, nullptr)) {
+          return false;
+        }
+      }
+    }
+
+    return !aStopAtBlockEnd;
+  }
+
+  const nsAString& mInput;
+  uint32_t mPos;
+  bool mSawNesting;
+};
+
+} // namespace
+
+bool
+LowerBasicCSSNesting(const nsAString& aInput, nsAString& aOutput)
+{
+  CSSNestingLowerer lowerer(aInput);
+  return lowerer.Lower(aOutput);
+}
+
+} // namespace css
+} // namespace mozilla
diff --git a/layout/style/CSSNestingLowerer.h b/layout/style/CSSNestingLowerer.h
new file mode 100644
index 0000000000..b74645186f
--- /dev/null
+++ b/layout/style/CSSNestingLowerer.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CSSNestingLowerer_h
+#define CSSNestingLowerer_h
+
+class nsAString;
+
+namespace mozilla {
+namespace css {
+
+bool LowerBasicCSSNesting(const nsAString& aInput, nsAString& aOutput);
+
+} // namespace css
+} // namespace mozilla
+
+#endif // CSSNestingLowerer_h
diff --git a/layout/style/moz.build b/layout/style/moz.build
index bc8959c1a3..e10c834daa 100644
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -127,6 +127,7 @@ UNIFIED_SOURCES += [
     'CounterStyleManager.cpp',
     'CSS.cpp',
     'CSSLexer.cpp',
+    'CSSNestingLowerer.cpp',
     'CSSRuleList.cpp',
     'CSSStyleSheet.cpp',
     'CSSVariableDeclarations.cpp',
diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp
index 484085b019..6ca421a09b 100644
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -18,6 +18,7 @@
 #include      // for std::regex and std::regex_match
 
 #include "nsCSSParser.h"
+#include "CSSNestingLowerer.h"
 #include "nsAlgorithm.h"
 #include "nsCSSProps.h"
 #include "nsCSSKeywords.h"
@@ -206,940 +207,6 @@ OKLabToSRGBColor(float aL, float aA, float aB, float aAlpha)
     nsStyleUtil::FloatToColorComponent(mozilla::clamped(aAlpha, 0.0f, 1.0f)));
 }
 
-class CSSNestingLowerer final
-{
-  using SelectorList = nsTArray;
-
-public:
-  explicit CSSNestingLowerer(const nsAString& aInput)
-    : mInput(aInput)
-    , mPos(0)
-    , mSawNesting(false)
-  {
-  }
-
-  bool Lower(nsAString& aOutput)
-  {
-    nsAutoString lowered;
-    if (!ProcessStylesheet(lowered, false)) {
-      return false;
-    }
-
-    SkipWhitespaceAndComments();
-    if (mPos != mInput.Length() || !mSawNesting) {
-      return false;
-    }
-
-    aOutput.Assign(lowered);
-    return true;
-  }
-
-private:
-  static constexpr auto kCSSWhitespace = " \t\r\n\f";
-
-  static bool
-  IsCSSWhitespace(char16_t aChar)
-  {
-    return aChar == ' ' || aChar == '\t' || aChar == '\r' ||
-           aChar == '\n' || aChar == '\f';
-  }
-
-  bool
-  AtEnd() const
-  {
-    return mPos >= mInput.Length();
-  }
-
-  char16_t
-  Peek() const
-  {
-    MOZ_ASSERT(!AtEnd(), "cannot peek past end");
-    return mInput.CharAt(mPos);
-  }
-
-  bool
-  StartsWithComment() const
-  {
-    return mPos + 1 < mInput.Length() &&
-           mInput.CharAt(mPos) == '/' &&
-           mInput.CharAt(mPos + 1) == '*';
-  }
-
-  bool
-  SkipComment()
-  {
-    MOZ_ASSERT(StartsWithComment(), "expected comment");
-
-    mPos += 2;
-    while (mPos + 1 < mInput.Length()) {
-      if (mInput.CharAt(mPos) == '*' && mInput.CharAt(mPos + 1) == '/') {
-        mPos += 2;
-        return true;
-      }
-      ++mPos;
-    }
-
-    return false;
-  }
-
-  void
-  SkipWhitespaceAndComments()
-  {
-    while (!AtEnd()) {
-      if (IsCSSWhitespace(Peek())) {
-        ++mPos;
-        continue;
-      }
-      if (StartsWithComment()) {
-        if (!SkipComment()) {
-          mPos = mInput.Length();
-          return;
-        }
-        continue;
-      }
-      break;
-    }
-  }
-
-  bool
-  SkipString(char16_t aQuote)
-  {
-    MOZ_ASSERT(!AtEnd() && Peek() == aQuote, "expected string start");
-
-    ++mPos;
-    while (!AtEnd()) {
-      char16_t c = Peek();
-      ++mPos;
-      if (c == aQuote) {
-        return true;
-      }
-      if (c == '\\' && !AtEnd()) {
-        ++mPos;
-        continue;
-      }
-      if (c == '\n' || c == '\r' || c == '\f') {
-        return false;
-      }
-    }
-
-    return false;
-  }
-
-  static void
-  TrimWhitespace(nsAString& aText)
-  {
-    uint32_t start = 0;
-    uint32_t end = aText.Length();
-
-    while (start < end && IsCSSWhitespace(aText.CharAt(start))) {
-      ++start;
-    }
-    while (end > start && IsCSSWhitespace(aText.CharAt(end - 1))) {
-      --end;
-    }
-
-    if (start == 0 && end == aText.Length()) {
-      return;
-    }
-
-    aText.Assign(Substring(aText, start, end - start));
-  }
-
-  bool
-  SplitSelectorList(const nsAString& aSelectorText, SelectorList& aSelectors)
-  {
-    uint32_t itemStart = 0;
-    int32_t parenDepth = 0;
-    int32_t bracketDepth = 0;
-    bool inComment = false;
-    char16_t stringQuote = 0;
-
-    for (uint32_t i = 0; i < aSelectorText.Length(); ++i) {
-      char16_t c = aSelectorText.CharAt(i);
-
-      if (inComment) {
-        if (c == '*' && i + 1 < aSelectorText.Length() &&
-            aSelectorText.CharAt(i + 1) == '/') {
-          inComment = false;
-          ++i;
-        }
-        continue;
-      }
-
-      if (stringQuote) {
-        if (c == '\\') {
-          ++i;
-          continue;
-        }
-        if (c == stringQuote) {
-          stringQuote = 0;
-        }
-        continue;
-      }
-
-      if (c == '/' && i + 1 < aSelectorText.Length() &&
-          aSelectorText.CharAt(i + 1) == '*') {
-        inComment = true;
-        ++i;
-        continue;
-      }
-
-      if (c == '"' || c == '\'') {
-        stringQuote = c;
-        continue;
-      }
-
-      if (c == '(') {
-        ++parenDepth;
-        continue;
-      }
-      if (c == ')' && parenDepth > 0) {
-        --parenDepth;
-        continue;
-      }
-      if (c == '[') {
-        ++bracketDepth;
-        continue;
-      }
-      if (c == ']' && bracketDepth > 0) {
-        --bracketDepth;
-        continue;
-      }
-
-      if (c == ',' && parenDepth == 0 && bracketDepth == 0) {
-        nsAutoString selector;
-        selector.Assign(Substring(aSelectorText, itemStart, i - itemStart));
-        TrimWhitespace(selector);
-        if (!selector.IsEmpty()) {
-          aSelectors.AppendElement(selector);
-        }
-        itemStart = i + 1;
-      }
-    }
-
-    nsAutoString selector;
-    selector.Assign(Substring(aSelectorText, itemStart));
-    TrimWhitespace(selector);
-    if (!selector.IsEmpty()) {
-      aSelectors.AppendElement(selector);
-    }
-
-    return !aSelectors.IsEmpty();
-  }
-
-  bool
-  SelectorHasAmpersand(const nsAString& aSelector) const
-  {
-    bool inComment = false;
-    char16_t stringQuote = 0;
-
-    for (uint32_t i = 0; i < aSelector.Length(); ++i) {
-      char16_t c = aSelector.CharAt(i);
-
-      if (inComment) {
-        if (c == '*' && i + 1 < aSelector.Length() &&
-            aSelector.CharAt(i + 1) == '/') {
-          inComment = false;
-          ++i;
-        }
-        continue;
-      }
-
-      if (stringQuote) {
-        if (c == '\\') {
-          ++i;
-          continue;
-        }
-        if (c == stringQuote) {
-          stringQuote = 0;
-        }
-        continue;
-      }
-
-      if (c == '/' && i + 1 < aSelector.Length() &&
-          aSelector.CharAt(i + 1) == '*') {
-        inComment = true;
-        ++i;
-        continue;
-      }
-
-      if (c == '"' || c == '\'') {
-        stringQuote = c;
-        continue;
-      }
-
-      if (c == '&') {
-        return true;
-      }
-    }
-
-    return false;
-  }
-
-  void
-  ReplaceAmpersands(const nsAString& aSelector,
-                    const nsAString& aParent,
-                    nsAString& aOutput) const
-  {
-    bool inComment = false;
-    char16_t stringQuote = 0;
-
-    for (uint32_t i = 0; i < aSelector.Length(); ++i) {
-      char16_t c = aSelector.CharAt(i);
-
-      if (inComment) {
-        aOutput.Append(c);
-        if (c == '*' && i + 1 < aSelector.Length() &&
-            aSelector.CharAt(i + 1) == '/') {
-          aOutput.Append('/');
-          inComment = false;
-          ++i;
-        }
-        continue;
-      }
-
-      if (stringQuote) {
-        aOutput.Append(c);
-        if (c == '\\' && i + 1 < aSelector.Length()) {
-          aOutput.Append(aSelector.CharAt(i + 1));
-          ++i;
-          continue;
-        }
-        if (c == stringQuote) {
-          stringQuote = 0;
-        }
-        continue;
-      }
-
-      if (c == '/' && i + 1 < aSelector.Length() &&
-          aSelector.CharAt(i + 1) == '*') {
-        aOutput.AppendLiteral("/*");
-        inComment = true;
-        ++i;
-        continue;
-      }
-
-      if (c == '"' || c == '\'') {
-        aOutput.Append(c);
-        stringQuote = c;
-        continue;
-      }
-
-      if (c == '&') {
-        aOutput.Append(aParent);
-        continue;
-      }
-
-      aOutput.Append(c);
-    }
-  }
-
-  bool
-  ExpandNestedSelectors(const SelectorList& aParents,
-                        const nsAString& aNestedSelectorText,
-                        SelectorList& aSelectors)
-  {
-    SelectorList nestedSelectors;
-    if (!SplitSelectorList(aNestedSelectorText, nestedSelectors)) {
-      return false;
-    }
-
-    for (const nsString& nestedSelector : nestedSelectors) {
-      bool hasAmpersand = SelectorHasAmpersand(nestedSelector);
-      for (const nsString& parentSelector : aParents) {
-        nsAutoString combined;
-        if (hasAmpersand) {
-          ReplaceAmpersands(nestedSelector, parentSelector, combined);
-        } else {
-          combined.Assign(parentSelector);
-          if (!combined.IsEmpty()) {
-            combined.Append(' ');
-          }
-          combined.Append(nestedSelector);
-        }
-        TrimWhitespace(combined);
-        if (!combined.IsEmpty()) {
-          aSelectors.AppendElement(combined);
-        }
-      }
-    }
-
-    return !aSelectors.IsEmpty();
-  }
-
-  static void
-  AppendSelectors(const SelectorList& aSelectors, nsAString& aOutput)
-  {
-    for (uint32_t i = 0; i < aSelectors.Length(); ++i) {
-      if (i) {
-        aOutput.AppendLiteral(", ");
-      }
-      aOutput.Append(aSelectors[i]);
-    }
-  }
-
-  static bool
-  StartsNestedSelector(char16_t aChar)
-  {
-    switch (aChar) {
-      case '.':
-      case '#':
-      case '[':
-      case ':':
-      case '&':
-      case '>':
-      case '+':
-      case '~':
-      case '*':
-        return true;
-      default:
-        return false;
-    }
-  }
-
-  static bool
-  StartsPotentialTypeSelector(char16_t aChar)
-  {
-    return (aChar >= 'a' && aChar <= 'z') ||
-           (aChar >= 'A' && aChar <= 'Z') ||
-           aChar == '_' ||
-           aChar == '\\' ||
-           aChar >= 0x80;
-  }
-
-  bool
-  LooksLikeTypeSelectorRule() const
-  {
-    if (AtEnd() || !StartsPotentialTypeSelector(Peek())) {
-      return false;
-    }
-
-    uint32_t pos = mPos;
-    int32_t parenDepth = 0;
-    int32_t bracketDepth = 0;
-    bool inComment = false;
-    char16_t stringQuote = 0;
-
-    while (pos < mInput.Length()) {
-      char16_t c = mInput.CharAt(pos);
-
-      if (inComment) {
-        if (c == '*' && pos + 1 < mInput.Length() &&
-            mInput.CharAt(pos + 1) == '/') {
-          inComment = false;
-          ++pos;
-        }
-        ++pos;
-        continue;
-      }
-
-      if (stringQuote) {
-        if (c == '\\' && pos + 1 < mInput.Length()) {
-          pos += 2;
-          continue;
-        }
-        if (c == stringQuote) {
-          stringQuote = 0;
-        }
-        ++pos;
-        continue;
-      }
-
-      if (c == '/' && pos + 1 < mInput.Length() &&
-          mInput.CharAt(pos + 1) == '*') {
-        inComment = true;
-        pos += 2;
-        continue;
-      }
-
-      if (c == '"' || c == '\'') {
-        stringQuote = c;
-        ++pos;
-        continue;
-      }
-
-      if (c == '(') {
-        ++parenDepth;
-        ++pos;
-        continue;
-      }
-      if (c == ')' && parenDepth > 0) {
-        --parenDepth;
-        ++pos;
-        continue;
-      }
-      if (c == '[') {
-        ++bracketDepth;
-        ++pos;
-        continue;
-      }
-      if (c == ']' && bracketDepth > 0) {
-        --bracketDepth;
-        ++pos;
-        continue;
-      }
-
-      if (parenDepth == 0 && bracketDepth == 0) {
-        if (c == '{') {
-          return true;
-        }
-        if (c == ';' || c == '}') {
-          return false;
-        }
-      }
-
-      ++pos;
-    }
-
-    return false;
-  }
-
-  static bool
-  IsAtRuleNameChar(char16_t aChar)
-  {
-    return (aChar >= 'a' && aChar <= 'z') ||
-           (aChar >= 'A' && aChar <= 'Z') ||
-           (aChar >= '0' && aChar <= '9') ||
-           aChar == '-';
-  }
-
-  static void
-  LowercaseASCII(nsACString& aText)
-  {
-    for (uint32_t i = 0; i < aText.Length(); ++i) {
-      char c = aText.CharAt(i);
-      if (c >= 'A' && c <= 'Z') {
-        aText.BeginWriting()[i] = c - 'A' + 'a';
-      }
-    }
-  }
-
-  static bool
-  ShouldProcessGroupRule(const nsACString& aName)
-  {
-    return aName.EqualsLiteral("media") ||
-           aName.EqualsLiteral("supports") ||
-           aName.EqualsLiteral("document") ||
-           aName.EqualsLiteral("layer");
-  }
-
-  void
-  FlushDeclarations(const SelectorList& aSelectors,
-                    nsAString& aDeclarations,
-                    nsAString& aOutput)
-  {
-    nsAutoString declarations;
-    declarations.Assign(aDeclarations);
-    TrimWhitespace(declarations);
-    aDeclarations.Truncate();
-
-    if (declarations.IsEmpty()) {
-      return;
-    }
-
-    AppendSelectors(aSelectors, aOutput);
-    aOutput.AppendLiteral(" { ");
-    aOutput.Append(declarations);
-    aOutput.AppendLiteral(" }\n");
-  }
-
-  bool
-  ReadRawBlockBody(nsAString& aBody)
-  {
-    uint32_t start = mPos;
-    int32_t depth = 0;
-
-    while (!AtEnd()) {
-      char16_t c = Peek();
-      if (c == '"' || c == '\'') {
-        if (!SkipString(c)) {
-          return false;
-        }
-        continue;
-      }
-      if (StartsWithComment()) {
-        if (!SkipComment()) {
-          return false;
-        }
-        continue;
-      }
-      if (c == '{') {
-        ++depth;
-        ++mPos;
-        continue;
-      }
-      if (c == '}') {
-        if (depth == 0) {
-          aBody.Assign(Substring(mInput, start, mPos - start));
-          ++mPos;
-          return true;
-        }
-        --depth;
-        ++mPos;
-        continue;
-      }
-      ++mPos;
-    }
-
-    return false;
-  }
-
-  bool
-  ReadQualifiedRulePrelude(nsAString& aPrelude)
-  {
-    uint32_t start = mPos;
-    int32_t parenDepth = 0;
-    int32_t bracketDepth = 0;
-
-    while (!AtEnd()) {
-      char16_t c = Peek();
-      if (c == '"' || c == '\'') {
-        if (!SkipString(c)) {
-          return false;
-        }
-        continue;
-      }
-      if (StartsWithComment()) {
-        if (!SkipComment()) {
-          return false;
-        }
-        continue;
-      }
-      if (c == '(') {
-        ++parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ')' && parenDepth > 0) {
-        --parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '[') {
-        ++bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ']' && bracketDepth > 0) {
-        --bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '{' && parenDepth == 0 && bracketDepth == 0) {
-        aPrelude.Assign(Substring(mInput, start, mPos - start));
-        TrimWhitespace(aPrelude);
-        ++mPos;
-        return !aPrelude.IsEmpty();
-      }
-      if ((c == ';' || c == '}') && parenDepth == 0 && bracketDepth == 0) {
-        return false;
-      }
-      ++mPos;
-    }
-
-    return false;
-  }
-
-  bool
-  ReadAtRulePrelude(nsAString& aPrelude, nsACString& aName, bool& aHasBlock)
-  {
-    MOZ_ASSERT(!AtEnd() && Peek() == '@', "expected at-rule");
-
-    uint32_t start = mPos;
-    ++mPos;
-    aName.Truncate();
-    while (!AtEnd() && IsAtRuleNameChar(Peek())) {
-      char16_t c = Peek();
-      aName.Append(char(c <= 0x7f ? c : '?'));
-      ++mPos;
-    }
-    LowercaseASCII(aName);
-
-    int32_t parenDepth = 0;
-    int32_t bracketDepth = 0;
-    while (!AtEnd()) {
-      char16_t c = Peek();
-      if (c == '"' || c == '\'') {
-        if (!SkipString(c)) {
-          return false;
-        }
-        continue;
-      }
-      if (StartsWithComment()) {
-        if (!SkipComment()) {
-          return false;
-        }
-        continue;
-      }
-      if (c == '(') {
-        ++parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ')' && parenDepth > 0) {
-        --parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '[') {
-        ++bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ']' && bracketDepth > 0) {
-        --bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (parenDepth == 0 && bracketDepth == 0) {
-        if (c == ';') {
-          aPrelude.Assign(Substring(mInput, start, mPos - start));
-          TrimWhitespace(aPrelude);
-          ++mPos;
-          aHasBlock = false;
-          return true;
-        }
-        if (c == '{') {
-          aPrelude.Assign(Substring(mInput, start, mPos - start));
-          TrimWhitespace(aPrelude);
-          ++mPos;
-          aHasBlock = true;
-          return true;
-        }
-      }
-      ++mPos;
-    }
-
-    return false;
-  }
-
-  bool
-  ConsumeDeclaration(nsAString& aDeclaration)
-  {
-    uint32_t start = mPos;
-    int32_t parenDepth = 0;
-    int32_t bracketDepth = 0;
-    int32_t braceDepth = 0;
-
-    while (!AtEnd()) {
-      char16_t c = Peek();
-      if (c == '"' || c == '\'') {
-        if (!SkipString(c)) {
-          return false;
-        }
-        continue;
-      }
-      if (StartsWithComment()) {
-        if (!SkipComment()) {
-          return false;
-        }
-        continue;
-      }
-      if (c == '(') {
-        ++parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ')' && parenDepth > 0) {
-        --parenDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '[') {
-        ++bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == ']' && bracketDepth > 0) {
-        --bracketDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '{') {
-        ++braceDepth;
-        ++mPos;
-        continue;
-      }
-      if (c == '}') {
-        if (braceDepth == 0 && parenDepth == 0 && bracketDepth == 0) {
-          break;
-        }
-        if (braceDepth > 0) {
-          --braceDepth;
-        }
-        ++mPos;
-        continue;
-      }
-      if (c == ';' && parenDepth == 0 && bracketDepth == 0 &&
-          braceDepth == 0) {
-        ++mPos;
-        break;
-      }
-      ++mPos;
-    }
-
-    aDeclaration.Assign(Substring(mInput, start, mPos - start));
-    TrimWhitespace(aDeclaration);
-    if (aDeclaration.IsEmpty()) {
-      return false;
-    }
-    if (aDeclaration.Last() != ';') {
-      aDeclaration.Append(';');
-    }
-    return true;
-  }
-
-  bool
-  ParseAtRule(nsAString& aOutput, const SelectorList* aParents)
-  {
-    nsAutoString prelude;
-    nsAutoCString name;
-    bool hasBlock = false;
-    if (!ReadAtRulePrelude(prelude, name, hasBlock)) {
-      return false;
-    }
-
-    if (!hasBlock) {
-      aOutput.Append(prelude);
-      aOutput.AppendLiteral(";\n");
-      return true;
-    }
-
-    if (!ShouldProcessGroupRule(name)) {
-      nsAutoString body;
-      if (!ReadRawBlockBody(body)) {
-        return false;
-      }
-      aOutput.Append(prelude);
-      aOutput.AppendLiteral(" {");
-      aOutput.Append(body);
-      aOutput.AppendLiteral("}\n");
-      return true;
-    }
-
-    nsAutoString inner;
-    if (aParents) {
-      mSawNesting = true;
-      if (!ProcessStyleContext(*aParents, inner)) {
-        return false;
-      }
-    } else {
-      if (!ProcessStylesheet(inner, true)) {
-        return false;
-      }
-    }
-
-    aOutput.Append(prelude);
-    aOutput.AppendLiteral(" {\n");
-    aOutput.Append(inner);
-    aOutput.AppendLiteral("}\n");
-    return true;
-  }
-
-  bool
-  ParseQualifiedRule(nsAString& aOutput, const SelectorList* aParents)
-  {
-    nsAutoString prelude;
-    if (!ReadQualifiedRulePrelude(prelude)) {
-      return false;
-    }
-
-    SelectorList selectors;
-    if (aParents) {
-      mSawNesting = true;
-      if (!ExpandNestedSelectors(*aParents, prelude, selectors)) {
-        return false;
-      }
-    } else if (!SplitSelectorList(prelude, selectors)) {
-      return false;
-    }
-
-    return ProcessStyleContext(selectors, aOutput);
-  }
-
-  bool
-  ProcessStyleContext(const SelectorList& aSelectors, nsAString& aOutput)
-  {
-    nsAutoString declarations;
-
-    while (!AtEnd()) {
-      SkipWhitespaceAndComments();
-      if (AtEnd()) {
-        return false;
-      }
-
-      char16_t c = Peek();
-      if (c == '}') {
-        ++mPos;
-        FlushDeclarations(aSelectors, declarations, aOutput);
-        return true;
-      }
-
-      if (c == '@') {
-        FlushDeclarations(aSelectors, declarations, aOutput);
-        if (!ParseAtRule(aOutput, &aSelectors)) {
-          return false;
-        }
-        continue;
-      }
-
-      if (StartsNestedSelector(c) || LooksLikeTypeSelectorRule()) {
-        FlushDeclarations(aSelectors, declarations, aOutput);
-        if (!ParseQualifiedRule(aOutput, &aSelectors)) {
-          return false;
-        }
-        continue;
-      }
-
-      nsAutoString declaration;
-      if (!ConsumeDeclaration(declaration)) {
-        return false;
-      }
-      if (!declarations.IsEmpty()) {
-        declarations.Append(' ');
-      }
-      declarations.Append(declaration);
-    }
-
-    return false;
-  }
-
-  bool
-  ProcessStylesheet(nsAString& aOutput, bool aStopAtBlockEnd)
-  {
-    while (!AtEnd()) {
-      SkipWhitespaceAndComments();
-      if (AtEnd()) {
-        return !aStopAtBlockEnd;
-      }
-
-      if (Peek() == '}') {
-        if (!aStopAtBlockEnd) {
-          return false;
-        }
-        ++mPos;
-        return true;
-      }
-
-      if (Peek() == '@') {
-        if (!ParseAtRule(aOutput, nullptr)) {
-          return false;
-        }
-      } else {
-        if (!ParseQualifiedRule(aOutput, nullptr)) {
-          return false;
-        }
-      }
-    }
-
-    return !aStopAtBlockEnd;
-  }
-
-  const nsAString& mInput;
-  uint32_t mPos;
-  bool mSawNesting;
-};
-
 static_assert(css::eAuthorSheetFeatures == 0 &&
               css::eUserSheetFeatures == 1 &&
               css::eAgentSheetFeatures == 2,
@@ -2761,11 +1828,9 @@ CSSParserImpl::ParseSheet(const nsAString& aInput,
 
   nsAutoString loweredInput;
   const nsAString* input = &aInput;
-  if (sNestingEnabled) {
-    CSSNestingLowerer lowerer(aInput);
-    if (lowerer.Lower(loweredInput)) {
-      input = &loweredInput;
-    }
+  if (sNestingEnabled &&
+      mozilla::css::LowerBasicCSSNesting(aInput, loweredInput)) {
+    input = &loweredInput;
   }
 
   nsCSSScanner scanner(*input, aLineNumber);

From 2a0179437f5f9aefb098416d1cf36ec552880630 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Wed, 11 Mar 2026 23:09:02 -0400
Subject: [PATCH 06/21] Issue #2862 - more tests files for nested css

---
 layout/style/test/mochitest.ini               |   1 +
 .../test_nesting_lowering_parser_edges.html   | 161 ++++++++++++++++++
 2 files changed, 162 insertions(+)
 create mode 100644 layout/style/test/test_nesting_lowering_parser_edges.html

diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
index d4ad77d9a4..13d1b77acc 100644
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -73,6 +73,7 @@ support-files = file_animations_with_disabled_properties.html
 [test_aspect_ratio_property.html]
 [test_background_blend_mode.html]
 [test_basic_nesting_lowering.html]
+[test_nesting_lowering_parser_edges.html]
 [test_box_size_keywords.html]
 [test_bug73586.html]
 [test_css_math_functions.html]
diff --git a/layout/style/test/test_nesting_lowering_parser_edges.html b/layout/style/test/test_nesting_lowering_parser_edges.html
new file mode 100644
index 0000000000..9ab78ac501
--- /dev/null
+++ b/layout/style/test/test_nesting_lowering_parser_edges.html
@@ -0,0 +1,161 @@
+
+
+
+  
+  Test CSS Nesting Lowering Parser Edges
+  
+  
+
+
+
+ + + + + + +
+
+
+

+
+
+

From 505600c45e695b98ed3b416730c9ecfd0b8e7072 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Thu, 12 Mar 2026 08:02:18 -0400
Subject: [PATCH 07/21] Issue #2862 - handle more edge cases in css lowering

---
 layout/style/CSSNestingLowerer.cpp            |   1 +
 layout/style/test/mochitest.ini               |   1 +
 .../test/test_nesting_lowering_recovery.html  | 149 ++++++++++++++++++
 3 files changed, 151 insertions(+)
 create mode 100644 layout/style/test/test_nesting_lowering_recovery.html

diff --git a/layout/style/CSSNestingLowerer.cpp b/layout/style/CSSNestingLowerer.cpp
index 9340c4c5af..2dd45d58a9 100644
--- a/layout/style/CSSNestingLowerer.cpp
+++ b/layout/style/CSSNestingLowerer.cpp
@@ -395,6 +395,7 @@ private:
       case '[':
       case ':':
       case '&':
+      case '|':
       case '>':
       case '+':
       case '~':
diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
index 13d1b77acc..28b3ab8122 100644
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -74,6 +74,7 @@ support-files = file_animations_with_disabled_properties.html
 [test_background_blend_mode.html]
 [test_basic_nesting_lowering.html]
 [test_nesting_lowering_parser_edges.html]
+[test_nesting_lowering_recovery.html]
 [test_box_size_keywords.html]
 [test_bug73586.html]
 [test_css_math_functions.html]
diff --git a/layout/style/test/test_nesting_lowering_recovery.html b/layout/style/test/test_nesting_lowering_recovery.html
new file mode 100644
index 0000000000..3e3184931f
--- /dev/null
+++ b/layout/style/test/test_nesting_lowering_recovery.html
@@ -0,0 +1,149 @@
+
+
+
+  
+  Test CSS Nesting Lowering Recovery Paths
+  
+  
+
+
+
+
+
+
+

+
+
+

From 64d55b0a2efeab096c82e800ba38d4453dac326b Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Fri, 13 Mar 2026 08:58:18 -0400
Subject: [PATCH 08/21] Issue #2862 - Change code references from CSS lowering
 to CSS flattening

---
 ...ingLowerer.cpp => CSSNestingFlattener.cpp} | 20 +++++++++----------
 ...NestingLowerer.h => CSSNestingFlattener.h} |  8 ++++----
 layout/style/moz.build                        |  2 +-
 layout/style/nsCSSParser.cpp                  |  8 ++++----
 4 files changed, 19 insertions(+), 19 deletions(-)
 rename layout/style/{CSSNestingLowerer.cpp => CSSNestingFlattener.cpp} (98%)
 rename layout/style/{CSSNestingLowerer.h => CSSNestingFlattener.h} (69%)

diff --git a/layout/style/CSSNestingLowerer.cpp b/layout/style/CSSNestingFlattener.cpp
similarity index 98%
rename from layout/style/CSSNestingLowerer.cpp
rename to layout/style/CSSNestingFlattener.cpp
index 2dd45d58a9..ac8fc1a2ca 100644
--- a/layout/style/CSSNestingLowerer.cpp
+++ b/layout/style/CSSNestingFlattener.cpp
@@ -3,7 +3,7 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#include "CSSNestingLowerer.h"
+#include "CSSNestingFlattener.h"
 
 #include "mozilla/Assertions.h"
 #include "nsString.h"
@@ -14,22 +14,22 @@ namespace css {
 
 namespace {
 
-class CSSNestingLowerer final
+class CSSNestingFlattener final
 {
   using SelectorList = nsTArray;
 
 public:
-  explicit CSSNestingLowerer(const nsAString& aInput)
+  explicit CSSNestingFlattener(const nsAString& aInput)
     : mInput(aInput)
     , mPos(0)
     , mSawNesting(false)
   {
   }
 
-  bool Lower(nsAString& aOutput)
+  bool Flatten(nsAString& aOutput)
   {
-    nsAutoString lowered;
-    if (!ProcessStylesheet(lowered, false)) {
+    nsAutoString flattened;
+    if (!ProcessStylesheet(flattened, false)) {
       return false;
     }
 
@@ -38,7 +38,7 @@ public:
       return false;
     }
 
-    aOutput.Assign(lowered);
+    aOutput.Assign(flattened);
     return true;
   }
 
@@ -952,10 +952,10 @@ private:
 } // namespace
 
 bool
-LowerBasicCSSNesting(const nsAString& aInput, nsAString& aOutput)
+FlattenBasicCSSNesting(const nsAString& aInput, nsAString& aOutput)
 {
-  CSSNestingLowerer lowerer(aInput);
-  return lowerer.Lower(aOutput);
+  CSSNestingFlattener flattener(aInput);
+  return flattener.Flatten(aOutput);
 }
 
 } // namespace css
diff --git a/layout/style/CSSNestingLowerer.h b/layout/style/CSSNestingFlattener.h
similarity index 69%
rename from layout/style/CSSNestingLowerer.h
rename to layout/style/CSSNestingFlattener.h
index b74645186f..41a9d4432d 100644
--- a/layout/style/CSSNestingLowerer.h
+++ b/layout/style/CSSNestingFlattener.h
@@ -3,17 +3,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#ifndef CSSNestingLowerer_h
-#define CSSNestingLowerer_h
+#ifndef CSSNestingFlattener_h
+#define CSSNestingFlattener_h
 
 class nsAString;
 
 namespace mozilla {
 namespace css {
 
-bool LowerBasicCSSNesting(const nsAString& aInput, nsAString& aOutput);
+bool FlattenBasicCSSNesting(const nsAString& aInput, nsAString& aOutput);
 
 } // namespace css
 } // namespace mozilla
 
-#endif // CSSNestingLowerer_h
+#endif // CSSNestingFlattener_h
diff --git a/layout/style/moz.build b/layout/style/moz.build
index e10c834daa..3dfbd58012 100644
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -127,7 +127,7 @@ UNIFIED_SOURCES += [
     'CounterStyleManager.cpp',
     'CSS.cpp',
     'CSSLexer.cpp',
-    'CSSNestingLowerer.cpp',
+    'CSSNestingFlattener.cpp',
     'CSSRuleList.cpp',
     'CSSStyleSheet.cpp',
     'CSSVariableDeclarations.cpp',
diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp
index 6ca421a09b..6e24a427b0 100644
--- a/layout/style/nsCSSParser.cpp
+++ b/layout/style/nsCSSParser.cpp
@@ -18,7 +18,7 @@
 #include      // for std::regex and std::regex_match
 
 #include "nsCSSParser.h"
-#include "CSSNestingLowerer.h"
+#include "CSSNestingFlattener.h"
 #include "nsAlgorithm.h"
 #include "nsCSSProps.h"
 #include "nsCSSKeywords.h"
@@ -1826,11 +1826,11 @@ CSSParserImpl::ParseSheet(const nsAString& aInput,
                "Sheet principal does not match passed principal");
 #endif
 
-  nsAutoString loweredInput;
+  nsAutoString flattenedInput;
   const nsAString* input = &aInput;
   if (sNestingEnabled &&
-      mozilla::css::LowerBasicCSSNesting(aInput, loweredInput)) {
-    input = &loweredInput;
+      mozilla::css::FlattenBasicCSSNesting(aInput, flattenedInput)) {
+    input = &flattenedInput;
   }
 
   nsCSSScanner scanner(*input, aLineNumber);

From df9550ae5ab2dd0358b175c8e1edfbdd7e01285a Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Fri, 13 Mar 2026 09:01:56 -0400
Subject: [PATCH 09/21] Issue #2862 - Change tests from lowering to flattening

---
 layout/style/test/mochitest.ini                        |  6 +++---
 ...owering.html => test_basic_nesting_flattening.html} | 10 +++++-----
 ....html => test_nesting_flattening_parser_edges.html} |  4 ++--
 ...very.html => test_nesting_flattening_recovery.html} |  2 +-
 4 files changed, 11 insertions(+), 11 deletions(-)
 rename layout/style/test/{test_basic_nesting_lowering.html => test_basic_nesting_flattening.html} (93%)
 rename layout/style/test/{test_nesting_lowering_parser_edges.html => test_nesting_flattening_parser_edges.html} (97%)
 rename layout/style/test/{test_nesting_lowering_recovery.html => test_nesting_flattening_recovery.html} (98%)

diff --git a/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
index 28b3ab8122..6e4b68e82f 100644
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -72,9 +72,9 @@ support-files = file_animations_with_disabled_properties.html
 [test_attribute_selector_eof_behavior.html]
 [test_aspect_ratio_property.html]
 [test_background_blend_mode.html]
-[test_basic_nesting_lowering.html]
-[test_nesting_lowering_parser_edges.html]
-[test_nesting_lowering_recovery.html]
+[test_basic_nesting_flattening.html]
+[test_nesting_flattening_parser_edges.html]
+[test_nesting_flattening_recovery.html]
 [test_box_size_keywords.html]
 [test_bug73586.html]
 [test_css_math_functions.html]
diff --git a/layout/style/test/test_basic_nesting_lowering.html b/layout/style/test/test_basic_nesting_flattening.html
similarity index 93%
rename from layout/style/test/test_basic_nesting_lowering.html
rename to layout/style/test/test_basic_nesting_flattening.html
index 6c08934a11..b24af42e76 100644
--- a/layout/style/test/test_basic_nesting_lowering.html
+++ b/layout/style/test/test_basic_nesting_flattening.html
@@ -2,7 +2,7 @@
 
 
   
-  Test for Basic CSS Nesting Lowering
+  Test for Basic CSS Nesting Flattening
   
   
 
@@ -68,15 +68,15 @@ function runChecks(style, report) {
          colorOf(window, scope, "color"),
          "rgb(1, 2, 3)");
   report(colorOf(window, desc, "color") === "rgb(4, 5, 6)",
-         "nested descendant rule should be lowered",
+         "nested descendant rule should be flattened",
          colorOf(window, desc, "color"),
          "rgb(4, 5, 6)");
   report(colorOf(window, desc, "border-left-color") === "rgb(19, 20, 21)",
-         "nested type selector rule should be lowered",
+         "nested type selector rule should be flattened",
          colorOf(window, desc, "border-left-color"),
          "rgb(19, 20, 21)");
   report(colorOf(window, desc, "border-bottom-color") === "rgb(22, 23, 24)",
-         "nested type selector pseudos should be lowered",
+         "nested type selector pseudos should be flattened",
          colorOf(window, desc, "border-bottom-color"),
          "rgb(22, 23, 24)");
   report(colorOf(window, scope, "background-color") === "rgb(7, 8, 9)",
@@ -96,7 +96,7 @@ function runChecks(style, report) {
          colorOf(window, scope, "border-right-color"),
          "rgb(16, 17, 18)");
   report(style.sheet.cssRules.length === 9,
-         "lowering should produce flat top-level rules",
+         "flattening should produce flat top-level rules",
          String(style.sheet.cssRules.length),
          "9");
 }
diff --git a/layout/style/test/test_nesting_lowering_parser_edges.html b/layout/style/test/test_nesting_flattening_parser_edges.html
similarity index 97%
rename from layout/style/test/test_nesting_lowering_parser_edges.html
rename to layout/style/test/test_nesting_flattening_parser_edges.html
index 9ab78ac501..100ff1bf1d 100644
--- a/layout/style/test/test_nesting_lowering_parser_edges.html
+++ b/layout/style/test/test_nesting_flattening_parser_edges.html
@@ -2,7 +2,7 @@
 
 
   
-  Test CSS Nesting Lowering Parser Edges
+  Test CSS Nesting Flattening Parser Edges
   
   
 
@@ -71,7 +71,7 @@ function runChecks(style, report) {
 
   var payload = propOf(window, scope, "--payload");
   report(payload.indexOf("alpha") !== -1 && payload.indexOf("beta") !== -1,
-         "custom property payload should survive lowering",
+         "custom property payload should survive flattening",
          payload,
          "contains alpha and beta");
 
diff --git a/layout/style/test/test_nesting_lowering_recovery.html b/layout/style/test/test_nesting_flattening_recovery.html
similarity index 98%
rename from layout/style/test/test_nesting_lowering_recovery.html
rename to layout/style/test/test_nesting_flattening_recovery.html
index 3e3184931f..63eed9091b 100644
--- a/layout/style/test/test_nesting_lowering_recovery.html
+++ b/layout/style/test/test_nesting_flattening_recovery.html
@@ -2,7 +2,7 @@
 
 
   
-  Test CSS Nesting Lowering Recovery Paths
+  Test CSS Nesting Flattening Recovery Paths
   
   
 

From 700d17fea88feef3a9cbc88b544c1b22e16ffca9 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Sat, 14 Mar 2026 13:22:59 -0400
Subject: [PATCH 10/21] Issue #2862 - Remove unused vars in CSS flattener

---
 layout/style/CSSNestingFlattener.cpp | 2 --
 1 file changed, 2 deletions(-)

diff --git a/layout/style/CSSNestingFlattener.cpp b/layout/style/CSSNestingFlattener.cpp
index ac8fc1a2ca..287e2ca5d1 100644
--- a/layout/style/CSSNestingFlattener.cpp
+++ b/layout/style/CSSNestingFlattener.cpp
@@ -43,8 +43,6 @@ public:
   }
 
 private:
-  static constexpr auto kCSSWhitespace = " \t\r\n\f";
-
   static bool
   IsCSSWhitespace(char16_t aChar)
   {

From c88750f4989f2840a1fddaff978713da3210fd6e Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Sat, 14 Mar 2026 13:23:54 -0400
Subject: [PATCH 11/21] Issue #2862 - css nesting pref true by default

---
 modules/libpref/init/all.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index 6dee3b429a..e13c9e94c9 100644
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2726,7 +2726,7 @@ pref("layout.css.resizeobserver.enabled", true);
 pref("layout.css.cascade-layers.enabled", true);
 
 // Is support for basic CSS nesting lowering enabled?
-pref("layout.css.nesting.enabled", false);
+pref("layout.css.nesting.enabled", true);
 
 // Should rules in imported style sheets be added based on the order
 // of appearance of their respective @import rules in the parent

From f7820b0ab5228c3a265ac3dfd53a0b5e48f318d0 Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Mon, 2 Feb 2026 18:43:28 +0100
Subject: [PATCH 12/21] Issue #2928 - Check mCacheQueue size before removing
 entries.

Resolves #2928
---
 image/imgLoader.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp
index 904eaaab2d..f33719c66c 100644
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -1949,7 +1949,7 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry)
     if (entry->HasNoProxies()) {
       LOG_STATIC_FUNC(gImgLog,
                       "imgLoader::RemoveFromCache removing from tracker");
-      if (mCacheTracker) {
+      if (mCacheTracker && queue.GetSize() > 0) {
         mCacheTracker->RemoveObject(entry);
       }
       queue.Remove(entry);

From 7332833ecc31d4d32a317e7560a7d0378a3b0ad7 Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Tue, 3 Feb 2026 08:51:06 +0100
Subject: [PATCH 13/21] Issue #2928 - Re-order imgLoader::RemoveFromCache

Remove from tracker first before removing from cache, and if the cache
queue is dirty, refresh it (causes a re-heap) before manipulating further.
---
 image/imgLoader.cpp | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp
index f33719c66c..a33ce46f71 100644
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -1944,17 +1944,20 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry)
                                "imgLoader::RemoveFromCache", "entry's uri",
                                key.Spec());
 
-    cache.Remove(key);
-
     if (entry->HasNoProxies()) {
       LOG_STATIC_FUNC(gImgLog,
                       "imgLoader::RemoveFromCache removing from tracker");
-      if (mCacheTracker && queue.GetSize() > 0) {
+      if (queue.IsDirty()) {
+        queue.Refresh();
+      }
+      if (mCacheTracker) {
         mCacheTracker->RemoveObject(entry);
       }
       queue.Remove(entry);
     }
 
+    cache.Remove(key);
+
     entry->SetEvicted(true);
     request->SetIsInCache(false);
     AddToUncachedImages(request);

From 252cbabfd46800bfa14310d99389b6c7d057021b Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Tue, 3 Feb 2026 10:17:19 +0100
Subject: [PATCH 14/21] Issue #2928 - Always refresh dirty queue.

---
 image/imgLoader.cpp | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp
index a33ce46f71..d57f2232d7 100644
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -1944,12 +1944,13 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry)
                                "imgLoader::RemoveFromCache", "entry's uri",
                                key.Spec());
 
+    if (queue.IsDirty()) {
+      queue.Refresh();
+    }
+
     if (entry->HasNoProxies()) {
       LOG_STATIC_FUNC(gImgLog,
                       "imgLoader::RemoveFromCache removing from tracker");
-      if (queue.IsDirty()) {
-        queue.Refresh();
-      }
       if (mCacheTracker) {
         mCacheTracker->RemoveObject(entry);
       }

From 3b799da2f8f8a70c3475277685e5f7ba27f47172 Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Wed, 4 Feb 2026 09:07:23 +0100
Subject: [PATCH 15/21] Issue #2928 - Avoid searching the image cache queue for
 an entry after we just popped it off the queue.

---
 image/imgLoader.cpp | 25 ++++++++++++++++++++-----
 image/imgLoader.h   | 11 ++++++++++-
 2 files changed, 30 insertions(+), 6 deletions(-)

diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp
index d57f2232d7..895a8924a9 100644
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -1013,6 +1013,12 @@ imgCacheQueue::GetNumElements() const
   return mQueue.size();
 }
 
+bool
+imgCacheQueue::Contains(imgCacheEntry* aEntry) const
+{
+  return mQueue.Contains(aEntry);
+}
+
 imgCacheQueue::iterator
 imgCacheQueue::begin()
 {
@@ -1631,7 +1637,9 @@ imgLoader::CheckCacheLimits(imgCacheTable& cache, imgCacheQueue& queue)
     }
 
     if (entry) {
-      RemoveFromCache(entry);
+      // We just popped this entry from the queue, so pass AlreadyRemoved
+      // to avoid searching the queue again in RemoveFromCache.
+      RemoveFromCache(entry, QueueState::AlreadyRemoved);
     }
   }
 }
@@ -1930,7 +1938,7 @@ imgLoader::RemoveFromCache(const ImageCacheKey& aKey)
 }
 
 bool
-imgLoader::RemoveFromCache(imgCacheEntry* entry)
+imgLoader::RemoveFromCache(imgCacheEntry* entry, QueueState aQueueState)
 {
   LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache entry");
 
@@ -1944,6 +1952,8 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry)
                                "imgLoader::RemoveFromCache", "entry's uri",
                                key.Spec());
 
+    cache.Remove(key);
+
     if (queue.IsDirty()) {
       queue.Refresh();
     }
@@ -1954,11 +1964,16 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry)
       if (mCacheTracker) {
         mCacheTracker->RemoveObject(entry);
       }
-      queue.Remove(entry);
+      // Only search the queue to remove the entry if its possible it might
+      // be in the queue.  If we know its not in the queue this would be
+      // wasted work.
+      MOZ_ASSERT_IF(aQueueState == QueueState::AlreadyRemoved,
+                    !queue.Contains(entry));
+      if (aQueueState == QueueState::MaybeExists) {
+        queue.Remove(entry);
+      }
     }
 
-    cache.Remove(key);
-
     entry->SetEvicted(true);
     request->SetIsInCache(false);
     AddToUncachedImages(request);
diff --git a/image/imgLoader.h b/image/imgLoader.h
index 7349a666e5..43c92faa95 100644
--- a/image/imgLoader.h
+++ b/image/imgLoader.h
@@ -316,7 +316,16 @@ public:
   nsresult InitCache();
 
   bool RemoveFromCache(const ImageCacheKey& aKey);
-  bool RemoveFromCache(imgCacheEntry* entry);
+
+  // Enumeration describing if a given entry is in the cache queue or not.
+  // There are some cases we know the entry is definitely not in the queue.
+  enum class QueueState {
+    MaybeExists,
+    AlreadyRemoved
+  };
+
+  bool RemoveFromCache(imgCacheEntry* entry,
+                       QueueState aQueueState = QueueState::MaybeExists);
 
   bool PutIntoCache(const ImageCacheKey& aKey, imgCacheEntry* aEntry);
 

From bc0dbe9abb27f277a618dc10904f208377b2de3b Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Wed, 4 Feb 2026 09:19:05 +0100
Subject: [PATCH 16/21] Issue #2928 - Improve imgLoader cache queue handling.

- Convert the imgCacheQueue to nsTArray from Vector
- Avoid marking the queue dirty when element operations are performed
that don't upset the sort order. This should improve performance as well.
---
 image/imgLoader.cpp | 66 ++++++++++++++++++++++++++++++++++-----------
 image/imgLoader.h   |  3 ++-
 2 files changed, 52 insertions(+), 17 deletions(-)

diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp
index 895a8924a9..5725fd9648 100644
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -952,12 +952,38 @@ using namespace std;
 void
 imgCacheQueue::Remove(imgCacheEntry* entry)
 {
-  queueContainer::iterator it = find(mQueue.begin(), mQueue.end(), entry);
-  if (it != mQueue.end()) {
-    mSize -= (*it)->GetDataSize();
-    mQueue.erase(it);
-    MarkDirty();
+  uint64_t index = mQueue.IndexOf(entry);
+  if (index == queueContainer::NoIndex) {
+    return;
   }
+
+  mSize -= mQueue[index]->GetDataSize();
+
+  // If the queue is clean and this is the first entry,
+  // then we can efficiently remove the entry without
+  // dirtying the sort order.
+  if (!IsDirty() && index == 0) {
+    std::pop_heap(mQueue.begin(), mQueue.end(),
+                  imgLoader::CompareCacheEntries);
+    mQueue.RemoveElementAt(mQueue.Length() - 1);
+    return;
+  }
+
+  // Remove from the middle of the list. This potentially
+  // breaks the binary heap sort order.
+  mQueue.RemoveElementAt(index);
+
+  // If we only have one entry or the queue is empty, though,
+  // then the sort order is still effectively good.
+  // Simply refresh the list to clear the dirty flag.
+  if (mQueue.Length() <= 1) {
+    Refresh();
+    return;
+  }
+
+  // Otherwise we must mark the queue dirty and potentially
+  // trigger an expensive sort later.
+  MarkDirty();
 }
 
 void
@@ -966,23 +992,26 @@ imgCacheQueue::Push(imgCacheEntry* entry)
   mSize += entry->GetDataSize();
 
   RefPtr refptr(entry);
-  mQueue.push_back(refptr);
-  MarkDirty();
+  mQueue.AppendElement(Move(refptr));
+  // If we're not dirty already, then we can efficiently add this to the binary heap immediately. 
+  if (!IsDirty()) {
+    std::push_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries);
+  }
 }
 
 already_AddRefed
 imgCacheQueue::Pop()
 {
-  if (mQueue.empty()) {
+  if (mQueue.IsEmpty()) {
     return nullptr;
   }
   if (IsDirty()) {
     Refresh();
   }
 
-  RefPtr entry = mQueue[0];
   std::pop_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries);
-  mQueue.pop_back();
+  RefPtr entry = Move(mQueue.LastElement());
+  mQueue.RemoveElementAt(mQueue.Length() - 1);
 
   mSize -= entry->GetDataSize();
   return entry.forget();
@@ -991,6 +1020,7 @@ imgCacheQueue::Pop()
 void
 imgCacheQueue::Refresh()
 {
+  // Re-heap the list. This is an O(3 * n) operation and best avoided if possible.
   std::make_heap(mQueue.begin(), mQueue.end(), imgLoader::CompareCacheEntries);
   mDirty = false;
 }
@@ -1010,7 +1040,7 @@ imgCacheQueue::IsDirty()
 uint32_t
 imgCacheQueue::GetNumElements() const
 {
-  return mQueue.size();
+  return mQueue.Length();
 }
 
 bool
@@ -1608,7 +1638,11 @@ void
 imgLoader::CacheEntriesChanged(bool aForChrome, int32_t aSizeDiff /* = 0 */)
 {
   imgCacheQueue& queue = GetCacheQueue(aForChrome);
-  queue.MarkDirty();
+  // We only need to dirty the queue if there is any sorting taking place.
+  // Empty or single-entry lists can't become dirty.
+  if (queue.GetNumElements() > 1) {
+    queue.MarkDirty();
+  }
   queue.UpdateSize(aSizeDiff);
 }
 
@@ -2016,13 +2050,13 @@ imgLoader::EvictEntries(imgCacheQueue& aQueueToClear)
   // We have to make a temporary, since RemoveFromCache removes the element
   // from the queue, invalidating iterators.
   nsTArray > entries(aQueueToClear.GetNumElements());
-  for (imgCacheQueue::const_iterator i = aQueueToClear.begin();
-       i != aQueueToClear.end(); ++i) {
+  for (auto i = aQueueToClear.begin(); i != aQueueToClear.end(); ++i) {
     entries.AppendElement(*i);
   }
 
-  for (uint32_t i = 0; i < entries.Length(); ++i) {
-    if (!RemoveFromCache(entries[i])) {
+  // Iterate in reverse order to minimize array copying.
+  for (auto& entry : entries) {
+    if (!RemoveFromCache(entry)) {
       return NS_ERROR_FAILURE;
     }
   }
diff --git a/image/imgLoader.h b/image/imgLoader.h
index 43c92faa95..8557e92bd1 100644
--- a/image/imgLoader.h
+++ b/image/imgLoader.h
@@ -178,7 +178,8 @@ public:
   uint32_t GetSize() const;
   void UpdateSize(int32_t diff);
   uint32_t GetNumElements() const;
-  typedef std::vector > queueContainer;
+  bool Contains(imgCacheEntry* aEntry) const;
+  typedef nsTArray > queueContainer;
   typedef queueContainer::iterator iterator;
   typedef queueContainer::const_iterator const_iterator;
 

From 18c04a471df26da8c412a6d52a10e5ea1f1d28b1 Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Wed, 4 Feb 2026 09:53:46 -0500
Subject: [PATCH 17/21] Issue #2551 - Initial attempt at toSorted
 implementation

---
 js/src/builtin/Array.js         | 23 +++++++++++++++++++++++
 js/src/jsarray.cpp              |  4 +++-
 js/src/vm/CommonPropertyNames.h |  1 +
 3 files changed, 27 insertions(+), 1 deletion(-)

diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js
index 54446d2578..11a4a7485f 100644
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -253,6 +253,29 @@ function ArraySort(comparefn) {
     return MergeSort(O, len, comparefn);
 }
 
+// ES2023 22.1.3.30 Array.prototype.toSorted ( comparefn )
+function ArrayToSorted(comparefn) {
+    if (comparefn !== undefined) {
+        if (!IsCallable(comparefn)) {
+            ThrowTypeError(JSMSG_NOT_FUNCTION, DecompileArg(0, comparefn));
+        }
+    }
+
+    var O = ToObject(this);
+    var len = ToLength(O.length);
+
+    var A = ArraySpeciesCreate(O, len);
+    for (var k = 0; k < len; k++) {
+        if (k in O)
+            _DefineDataProperty(A, k, O[k]);
+        else
+            delete A[k];
+    }
+
+    callFunction(std_Array_sort, A, comparefn);
+    return A;
+}
+
 /* ES5 15.4.4.18. */
 function ArrayForEach(callbackfn/*, thisArg*/) {
     /* Step 1. */
diff --git a/js/src/jsarray.cpp b/js/src/jsarray.cpp
index 6f646b3b2d..15cb23a213 100644
--- a/js/src/jsarray.cpp
+++ b/js/src/jsarray.cpp
@@ -3227,6 +3227,7 @@ static const JSFunctionSpec array_methods[] = {
     /* ES2023 proposals */
     JS_SELF_HOSTED_FN("findLast",    "ArrayFindLast",    1,0),
     JS_SELF_HOSTED_FN("findLastIndex", "ArrayFindLastIndex", 1,0),
+    JS_SELF_HOSTED_FN("toSorted",    "ArrayToSorted",    1,0),
     
     JS_FS_END
 };
@@ -3401,7 +3402,8 @@ array_proto_finish(JSContext* cx, JS::HandleObject ctor, JS::HandleObject proto)
         !DefineProperty(cx, unscopables, cx->names().flatMap, value) ||
         !DefineProperty(cx, unscopables, cx->names().includes, value) ||
         !DefineProperty(cx, unscopables, cx->names().keys, value) ||
-        !DefineProperty(cx, unscopables, cx->names().values, value))
+        !DefineProperty(cx, unscopables, cx->names().values, value) ||
+        !DefineProperty(cx, unscopables, cx->names().toSorted, value))
     {
         return false;
     }
diff --git a/js/src/vm/CommonPropertyNames.h b/js/src/vm/CommonPropertyNames.h
index 13af5c237d..6edd49c6c0 100644
--- a/js/src/vm/CommonPropertyNames.h
+++ b/js/src/vm/CommonPropertyNames.h
@@ -429,6 +429,7 @@
     macro(toJSON, toJSON, "toJSON") \
     macro(toLocaleString, toLocaleString, "toLocaleString") \
     macro(toSource, toSource, "toSource") \
+    macro(toSorted, toSorted, "toSorted") \
     macro(toString, toString, "toString") \
     macro(toUTCString, toUTCString, "toUTCString") \
     macro(true, true_, "true") \

From 4edeca8c62d975b76fd13449093f7819265a12af Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Thu, 5 Feb 2026 09:53:39 -0500
Subject: [PATCH 18/21] Issue #2551 - Ensure toSorted is 100% spec compliant
 per multiple tests

---
 js/src/builtin/Array.js | 46 ++++++++++++++++++++++++++++++++++++-----
 1 file changed, 41 insertions(+), 5 deletions(-)

diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js
index 11a4a7485f..3efe5c409b 100644
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -264,15 +264,51 @@ function ArrayToSorted(comparefn) {
     var O = ToObject(this);
     var len = ToLength(O.length);
 
-    var A = ArraySpeciesCreate(O, len);
+    var items = new List();
+    var itemsLen = 0;
     for (var k = 0; k < len; k++) {
         if (k in O)
-            _DefineDataProperty(A, k, O[k]);
-        else
-            delete A[k];
+            items[itemsLen++] = O[k];
     }
 
-    callFunction(std_Array_sort, A, comparefn);
+    var wrappedCompareFn = comparefn;
+    var sortCompare;
+    if (wrappedCompareFn === undefined) {
+        sortCompare = function(x, y) {
+            if (x === undefined)
+                return y === undefined ? 0 : 1;
+            if (y === undefined)
+                return -1;
+
+            var xString = ToString(x);
+            var yString = ToString(y);
+            if (xString < yString)
+                return -1;
+            if (xString > yString)
+                return 1;
+            return 0;
+        };
+    } else {
+        sortCompare = function(x, y) {
+            if (x === undefined)
+                return y === undefined ? 0 : 1;
+            if (y === undefined)
+                return -1;
+
+            var v = ToNumber(wrappedCompareFn(x, y));
+            return v !== v ? 0 : v;
+        };
+    }
+
+    if (itemsLen > 1)
+        MergeSort(items, itemsLen, sortCompare);
+
+    var A = ArraySpeciesCreate(O, 0);
+    A.length = len;
+
+    for (var j = 0; j < itemsLen; j++)
+        _DefineDataProperty(A, j, items[j]);
+
     return A;
 }
 

From d3764451e27e2f80afa3224c344a1d21acea6d3f Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Thu, 5 Feb 2026 10:33:42 -0500
Subject: [PATCH 19/21] Issue #2551 - Add toSorted test and do some final
 tweaks to toSorted to be compliant

---
 js/src/builtin/Array.js                 |  9 ++--
 js/src/tests/ecma_6/Array/toSorted.html | 59 +++++++++++++++++++++++++
 js/src/tests/ecma_6/Array/toSorted.js   | 57 ++++++++++++++++++++++++
 3 files changed, 119 insertions(+), 6 deletions(-)
 create mode 100644 js/src/tests/ecma_6/Array/toSorted.html
 create mode 100644 js/src/tests/ecma_6/Array/toSorted.js

diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js
index 3efe5c409b..a257a2547d 100644
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -265,10 +265,9 @@ function ArrayToSorted(comparefn) {
     var len = ToLength(O.length);
 
     var items = new List();
-    var itemsLen = 0;
+    var itemsLen = len;
     for (var k = 0; k < len; k++) {
-        if (k in O)
-            items[itemsLen++] = O[k];
+        items[k] = O[k];
     }
 
     var wrappedCompareFn = comparefn;
@@ -303,9 +302,7 @@ function ArrayToSorted(comparefn) {
     if (itemsLen > 1)
         MergeSort(items, itemsLen, sortCompare);
 
-    var A = ArraySpeciesCreate(O, 0);
-    A.length = len;
-
+    var A = ArraySpeciesCreate(O, len);
     for (var j = 0; j < itemsLen; j++)
         _DefineDataProperty(A, j, items[j]);
 
diff --git a/js/src/tests/ecma_6/Array/toSorted.html b/js/src/tests/ecma_6/Array/toSorted.html
new file mode 100644
index 0000000000..3738b05c48
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/toSorted.html
@@ -0,0 +1,59 @@
+
+
+Array.prototype.toSorted test
+
+

+
+
diff --git a/js/src/tests/ecma_6/Array/toSorted.js b/js/src/tests/ecma_6/Array/toSorted.js
new file mode 100644
index 0000000000..fbd2540deb
--- /dev/null
+++ b/js/src/tests/ecma_6/Array/toSorted.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/ */
+
+assertEq(typeof Array.prototype.toSorted, "function");
+
+// Non-mutating behavior.
+let original = [3, 1, 2];
+let sorted = original.toSorted();
+assertEq(original !== sorted, true);
+assertEq(original.join(","), "3,1,2");
+assertEq(sorted.join(","), "1,2,3");
+
+// Compare function.
+let nums = [10, 1, 5];
+let desc = nums.toSorted((a, b) => b - a);
+assertEq(desc.join(","), "10,5,1");
+
+// Stable sort.
+let stableInput = [
+    {v: 1, id: "a"},
+    {v: 1, id: "b"},
+    {v: 1, id: "c"}
+];
+let stableSorted = stableInput.toSorted((x, y) => x.v - y.v);
+assertEq(stableSorted.map(o => o.id).join(""), "abc");
+
+// Holes are treated as undefined (properties are created).
+let sparse = [3, , 1];
+let sparseSorted = sparse.toSorted();
+assertEq(sparseSorted.length, 3);
+assertEq(sparseSorted[0], 1);
+assertEq(sparseSorted[1], 3);
+assertEq(2 in sparseSorted, true);
+assertEq(sparseSorted[2], undefined);
+
+// Array-like input.
+let arrayLike = {0: 2, 1: 1, length: 2};
+let arrayLikeSorted = Array.prototype.toSorted.call(arrayLike);
+assertEq(Array.isArray(arrayLikeSorted), true);
+assertEq(arrayLikeSorted.join(","), "1,2");
+
+// Getter access order (ascending indices).
+let accessLog = [];
+let getterArr = {
+    length: 3,
+    get 0() { accessLog.push(0); return 3; },
+    get 1() { accessLog.push(1); return 1; },
+    get 2() { accessLog.push(2); return 2; }
+};
+Array.prototype.toSorted.call(getterArr);
+assertEq(accessLog.join(","), "0,1,2");
+
+// Comparator errors propagate.
+assertThrowsInstanceOf(() => [1, 2].toSorted(1), TypeError);
+
+if (typeof reportCompare === "function")
+    reportCompare(0, 0);

From d4658401f75fb74e907e7db1f46d95822fe1be3c Mon Sep 17 00:00:00 2001
From: Moonchild 
Date: Fri, 6 Feb 2026 10:44:03 +0100
Subject: [PATCH 20/21] No Issue - Remove redundant OS check for toolkit/fonts

This is already guarded by MOZ_BUNDLED_FONTS one level higher and blocks
--enable-bundled-fonts from working on other OSes
---
 toolkit/fonts/moz.build | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/toolkit/fonts/moz.build b/toolkit/fonts/moz.build
index bfdff9e075..129a7737c6 100644
--- a/toolkit/fonts/moz.build
+++ b/toolkit/fonts/moz.build
@@ -3,5 +3,4 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
-    FINAL_TARGET_FILES.fonts += ['TwemojiMozilla.ttf']
+FINAL_TARGET_FILES.fonts += ['TwemojiMozilla.ttf']

From 75db223e088f6e951000ed82e0b3aa8ce1ddc6ea Mon Sep 17 00:00:00 2001
From: Basilisk-Dev 
Date: Fri, 6 Feb 2026 06:54:54 -0500
Subject: [PATCH 21/21] Issue #2551 - comment toSorted implementation

---
 js/src/builtin/Array.js | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js
index a257a2547d..9a6022f340 100644
--- a/js/src/builtin/Array.js
+++ b/js/src/builtin/Array.js
@@ -261,15 +261,18 @@ function ArrayToSorted(comparefn) {
         }
     }
 
+    // Step 1: Let O be ? ToObject(this). Let len be ? ToLength(O.length).
     var O = ToObject(this);
     var len = ToLength(O.length);
 
+    // Step 2: Snapshot values in ascending index order into a List.
     var items = new List();
     var itemsLen = len;
     for (var k = 0; k < len; k++) {
         items[k] = O[k];
     }
 
+    // Step 3: Create SortCompare per spec.
     var wrappedCompareFn = comparefn;
     var sortCompare;
     if (wrappedCompareFn === undefined) {
@@ -299,9 +302,11 @@ function ArrayToSorted(comparefn) {
         };
     }
 
+    // Step 4: Sort the snapshot List using SortCompare.
     if (itemsLen > 1)
         MergeSort(items, itemsLen, sortCompare);
 
+    // Step 5: Create result array and write sorted values.
     var A = ArraySpeciesCreate(O, len);
     for (var j = 0; j < itemsLen; j++)
         _DefineDataProperty(A, j, items[j]);