1
0
mirror of https://github.com/roytam1/UXP.git synced 2026-05-26 13:58:49 +00:00

Issue #2862 - Initial attempt at a css lowering

This commit is contained in:
Basilisk-Dev
2026-03-11 22:01:13 -04:00
committed by roytam1
parent c31f629aee
commit ce4b9975db
2 changed files with 857 additions and 4 deletions
+851 -1
View File
@@ -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<nsString>;
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,
+6 -3
View File
@@ -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);