diff --git a/image/imgLoader.cpp b/image/imgLoader.cpp index 904eaaab2d..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,13 @@ imgCacheQueue::IsDirty() uint32_t imgCacheQueue::GetNumElements() const { - return mQueue.size(); + return mQueue.Length(); +} + +bool +imgCacheQueue::Contains(imgCacheEntry* aEntry) const +{ + return mQueue.Contains(aEntry); } imgCacheQueue::iterator @@ -1602,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); } @@ -1631,7 +1671,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 +1972,7 @@ imgLoader::RemoveFromCache(const ImageCacheKey& aKey) } bool -imgLoader::RemoveFromCache(imgCacheEntry* entry) +imgLoader::RemoveFromCache(imgCacheEntry* entry, QueueState aQueueState) { LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache entry"); @@ -1946,13 +1988,24 @@ imgLoader::RemoveFromCache(imgCacheEntry* entry) cache.Remove(key); + if (queue.IsDirty()) { + queue.Refresh(); + } + if (entry->HasNoProxies()) { LOG_STATIC_FUNC(gImgLog, "imgLoader::RemoveFromCache removing from tracker"); 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); + } } entry->SetEvicted(true); @@ -1997,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 7349a666e5..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; @@ -316,7 +317,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); diff --git a/js/src/builtin/Array.js b/js/src/builtin/Array.js index 54446d2578..9a6022f340 100644 --- a/js/src/builtin/Array.js +++ b/js/src/builtin/Array.js @@ -253,6 +253,67 @@ 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)); + } + } + + // 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) { + 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; + }; + } + + // 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]); + + 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/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);
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") \
diff --git a/layout/style/CSSNestingFlattener.cpp b/layout/style/CSSNestingFlattener.cpp
new file mode 100644
index 0000000000..287e2ca5d1
--- /dev/null
+++ b/layout/style/CSSNestingFlattener.cpp
@@ -0,0 +1,960 @@
+/* -*- 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 "CSSNestingFlattener.h"
+
+#include "mozilla/Assertions.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace css {
+
+namespace {
+
+class CSSNestingFlattener final
+{
+  using SelectorList = nsTArray;
+
+public:
+  explicit CSSNestingFlattener(const nsAString& aInput)
+    : mInput(aInput)
+    , mPos(0)
+    , mSawNesting(false)
+  {
+  }
+
+  bool Flatten(nsAString& aOutput)
+  {
+    nsAutoString flattened;
+    if (!ProcessStylesheet(flattened, false)) {
+      return false;
+    }
+
+    SkipWhitespaceAndComments();
+    if (mPos != mInput.Length() || !mSawNesting) {
+      return false;
+    }
+
+    aOutput.Assign(flattened);
+    return true;
+  }
+
+private:
+  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 '~':
+      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
+FlattenBasicCSSNesting(const nsAString& aInput, nsAString& aOutput)
+{
+  CSSNestingFlattener flattener(aInput);
+  return flattener.Flatten(aOutput);
+}
+
+} // namespace css
+} // namespace mozilla
diff --git a/layout/style/CSSNestingFlattener.h b/layout/style/CSSNestingFlattener.h
new file mode 100644
index 0000000000..41a9d4432d
--- /dev/null
+++ b/layout/style/CSSNestingFlattener.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 CSSNestingFlattener_h
+#define CSSNestingFlattener_h
+
+class nsAString;
+
+namespace mozilla {
+namespace css {
+
+bool FlattenBasicCSSNesting(const nsAString& aInput, nsAString& aOutput);
+
+} // namespace css
+} // namespace mozilla
+
+#endif // CSSNestingFlattener_h
diff --git a/layout/style/moz.build b/layout/style/moz.build
index bc8959c1a3..3dfbd58012 100644
--- a/layout/style/moz.build
+++ b/layout/style/moz.build
@@ -127,6 +127,7 @@ UNIFIED_SOURCES += [
     'CounterStyleManager.cpp',
     'CSS.cpp',
     'CSSLexer.cpp',
+    'CSSNestingFlattener.cpp',
     'CSSRuleList.cpp',
     'CSSStyleSheet.cpp',
     'CSSVariableDeclarations.cpp',
diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp
index 59872b0f67..6e24a427b0 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"
@@ -17,6 +18,7 @@
 #include      // for std::regex and std::regex_match
 
 #include "nsCSSParser.h"
+#include "CSSNestingFlattener.h"
 #include "nsAlgorithm.h"
 #include "nsCSSProps.h"
 #include "nsCSSKeywords.h"
@@ -71,6 +73,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] = {
@@ -1823,7 +1826,14 @@ CSSParserImpl::ParseSheet(const nsAString& aInput,
                "Sheet principal does not match passed principal");
 #endif
 
-  nsCSSScanner scanner(aInput, aLineNumber);
+  nsAutoString flattenedInput;
+  const nsAString* input = &aInput;
+  if (sNestingEnabled &&
+      mozilla::css::FlattenBasicCSSNesting(aInput, flattenedInput)) {
+    input = &flattenedInput;
+  }
+
+  nsCSSScanner scanner(*input, aLineNumber);
   css::ErrorReporter reporter(scanner, mSheet, mChildLoader, aSheetURI);
   InitScanner(scanner, reporter, aSheetURI, aBaseURI, aSheetPrincipal);
 
@@ -18987,6 +18997,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/layout/style/test/mochitest.ini b/layout/style/test/mochitest.ini
index 8cadc7dd51..6e4b68e82f 100644
--- a/layout/style/test/mochitest.ini
+++ b/layout/style/test/mochitest.ini
@@ -72,6 +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_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_flattening.html b/layout/style/test/test_basic_nesting_flattening.html
new file mode 100644
index 0000000000..b24af42e76
--- /dev/null
+++ b/layout/style/test/test_basic_nesting_flattening.html
@@ -0,0 +1,146 @@
+
+
+
+  
+  Test for Basic CSS Nesting Flattening
+  
+  
+
+
+
+ +
+
+

+
+
+
diff --git a/layout/style/test/test_nesting_flattening_parser_edges.html b/layout/style/test/test_nesting_flattening_parser_edges.html
new file mode 100644
index 0000000000..100ff1bf1d
--- /dev/null
+++ b/layout/style/test/test_nesting_flattening_parser_edges.html
@@ -0,0 +1,161 @@
+
+
+
+  
+  Test CSS Nesting Flattening Parser Edges
+  
+  
+
+
+
+ + + + + + +
+
+
+

+
+
+
diff --git a/layout/style/test/test_nesting_flattening_recovery.html b/layout/style/test/test_nesting_flattening_recovery.html
new file mode 100644
index 0000000000..63eed9091b
--- /dev/null
+++ b/layout/style/test/test_nesting_flattening_recovery.html
@@ -0,0 +1,149 @@
+
+
+
+  
+  Test CSS Nesting Flattening Recovery Paths
+  
+  
+
+
+
+
+
+
+

+
+
+
diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js
index dfdfb84b2b..e13c9e94c9 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", true);
+
 // 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);
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']