diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index 6b6a65d72e..fba3fd9721 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] = { @@ -217,6 +219,843 @@ OKLabToSRGBColor(float aL, float aA, float aB, float aAlpha) alpha); } +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, @@ -1838,7 +2677,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); @@ -19024,6 +19872,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 6ae7223600..c6523891ff 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -2711,6 +2711,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 @@ -3216,7 +3219,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"); @@ -4731,7 +4734,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 @@ -5245,7 +5248,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);