From 328cba210e82e8ebe827de5b4f384e62395ce7ea Mon Sep 17 00:00:00 2001 From: "L. David Baron" Date: Tue, 11 May 2010 08:49:43 -0700 Subject: [PATCH] Implement parsing of calc(). (Bug 363249) r=bzbarsky --- layout/style/nsCSSParser.cpp | 390 ++++++++++++++++++++++++++++++++++- 1 file changed, 389 insertions(+), 1 deletion(-) diff --git a/layout/style/nsCSSParser.cpp b/layout/style/nsCSSParser.cpp index e084b579d908..d6e98b89c077 100644 --- a/layout/style/nsCSSParser.cpp +++ b/layout/style/nsCSSParser.cpp @@ -87,6 +87,7 @@ #include "nsAutoPtr.h" #include "nsTArray.h" #include "prlog.h" +#include "CSSCalc.h" // Flags for ParseVariant method #define VARIANT_KEYWORD 0x000001 // K @@ -114,6 +115,7 @@ #define VARIANT_IMAGE_RECT 0x01000000 // eCSSUnit_Function // This is an extra bit that says that a VARIANT_ANGLE allows unitless zero: #define VARIANT_ZERO_ANGLE 0x02000000 // unitless zero for angles +#define VARIANT_CALC 0x04000000 // eCSSUnit_Calc // Common combinations of variants #define VARIANT_AL (VARIANT_AUTO | VARIANT_LENGTH) @@ -479,6 +481,18 @@ protected: PRInt32 aSourceType); PRBool ParseBorderStyle(); PRBool ParseBorderWidth(); + + PRBool ParseCalc(nsCSSValue &aValue, PRInt32 aVariantMask); + PRBool ParseCalcAdditiveExpression(nsCSSValue& aValue, + PRInt32& aVariantMask); + PRBool ParseCalcMultiplicativeExpression(nsCSSValue& aValue, + PRInt32& aVariantMask, + PRBool *aHadFinalWS); + PRBool ParseCalcTerm(nsCSSValue& aValue, PRInt32& aVariantMask); + PRBool ParseCalcMinMax(nsCSSValue& aValue, nsCSSUnit aUnit, + PRInt32& aVariantMask); + PRBool RequireWhitespace(); + // for 'clip' and '-moz-image-region' PRBool ParseRect(nsCSSRect& aRect, nsCSSProperty aPropID); @@ -4388,6 +4402,10 @@ CSSParserImpl::TranslateDimension(nsCSSValue& aValue, return PR_FALSE; } +// Note that this does include VARIANT_CALC, which is numeric. This is +// because calc() parsing, as proposed, drops range restrictions inside +// the calc() expression and clamps the result of the calculation to the +// range. #define VARIANT_ALL_NONNUMERIC \ VARIANT_KEYWORD | \ VARIANT_COLOR | \ @@ -4403,8 +4421,13 @@ CSSParserImpl::TranslateDimension(nsCSSValue& aValue, VARIANT_SYSFONT | \ VARIANT_GRADIENT | \ VARIANT_CUBIC_BEZIER | \ - VARIANT_ALL + VARIANT_ALL | \ + VARIANT_CALC +// Note that callers passing VARIANT_CALC in aVariantMask will get +// full-range parsing inside the calc() expression, and the code that +// computes the calc will be required to clamp the resulting value to an +// appropriate range. PRBool CSSParserImpl::ParseNonNegativeVariant(nsCSSValue& aValue, PRInt32 aVariantMask, @@ -4443,6 +4466,10 @@ CSSParserImpl::ParseNonNegativeVariant(nsCSSValue& aValue, return PR_FALSE; } +// Note that callers passing VARIANT_CALC in aVariantMask will get +// full-range parsing inside the calc() expression, and the code that +// computes the calc will be required to clamp the resulting value to an +// appropriate range. PRBool CSSParserImpl::ParsePositiveNonZeroVariant(nsCSSValue& aValue, PRInt32 aVariantMask, @@ -4663,6 +4690,14 @@ CSSParserImpl::ParseVariant(nsCSSValue& aValue, return ParseTransitionTimingFunctionValues(aValue); } } + if ((aVariantMask & VARIANT_CALC) && + (eCSSToken_Function == tk->mType) && + (tk->mIdent.LowerCaseEqualsLiteral("-moz-calc") || + tk->mIdent.LowerCaseEqualsLiteral("-moz-min") || + tk->mIdent.LowerCaseEqualsLiteral("-moz-max"))) { + // calc() currently allows only lengths and percents inside it. + return ParseCalc(aValue, aVariantMask & VARIANT_LP); + } UngetToken(); return PR_FALSE; @@ -7152,6 +7187,359 @@ CSSParserImpl::ParseBorderColors(nsCSSValueList** aResult, return PR_FALSE; } +// Parse the top level of a calc() expression, which can be calc(), +// min(), or max(). +PRBool +CSSParserImpl::ParseCalc(nsCSSValue &aValue, PRInt32 aVariantMask) +{ + // Parsing calc expressions requires, in a number of cases, looking + // for a token that is *either* a value of the property or a number. + // This can be done without lookahead when we assume that the property + // values cannot themselves be numbers. + NS_ASSERTION(!(aVariantMask & VARIANT_NUMBER), "unexpected variant mask"); + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + + nsCSSUnit unit; + if (mToken.mIdent.LowerCaseEqualsLiteral("-moz-min")) { + unit = eCSSUnit_Calc_Minimum; + } else if (mToken.mIdent.LowerCaseEqualsLiteral("-moz-max")) { + unit = eCSSUnit_Calc_Maximum; + } else { + NS_ASSERTION(mToken.mIdent.LowerCaseEqualsLiteral("-moz-calc"), + "unexpected function"); + unit = eCSSUnit_Calc; + } + + if (unit != eCSSUnit_Calc) { + return ParseCalcMinMax(aValue, unit, aVariantMask); + } + + // One-iteration loop so we can break to the error-handling case. + do { + // The toplevel of a calc() is always an nsCSSValue::Array of length 1. + nsRefPtr arr = nsCSSValue::Array::Create(1); + if (!arr) { + mScanner.SetLowLevelError(NS_ERROR_OUT_OF_MEMORY); + break; + } + + if (!ParseCalcAdditiveExpression(arr->Item(0), aVariantMask)) + break; + + if (!ExpectSymbol(')', PR_TRUE)) + break; + + aValue.SetArrayValue(arr, eCSSUnit_Calc); + return PR_TRUE; + } while (PR_FALSE); + + SkipUntil(')'); + return PR_FALSE; +} + +// We optimize away the production given that +// ParseVariant consumes initial whitespace and we call +// ExpectSymbol(')') with PR_TRUE for aSkipWS. +// * If aVariantMask is VARIANT_NUMBER, this function parses the +// production. +// * If aVariantMask does not contain VARIANT_NUMBER, this function +// parses the production. +// * Otherwise (VARIANT_NUMBER and other bits) this function parses +// whichever one of the productions matches ***and modifies +// aVariantMask*** to reflect which one it has parsed by either +// removing VARIANT_NUMBER or removing all other bits. +// It does so iteratively, but builds the correct recursive +// data structure. +PRBool +CSSParserImpl::ParseCalcAdditiveExpression(nsCSSValue& aValue, + PRInt32& aVariantMask) +{ + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + nsCSSValue *storage = &aValue; + for (;;) { + PRBool haveWS; + if (!ParseCalcMultiplicativeExpression(*storage, aVariantMask, &haveWS)) + return PR_FALSE; + + if (!haveWS || !GetToken(PR_FALSE)) + return PR_TRUE; + nsCSSUnit unit; + if (mToken.IsSymbol('+')) { + unit = eCSSUnit_Calc_Plus; + } else if (mToken.IsSymbol('-')) { + unit = eCSSUnit_Calc_Minus; + } else { + UngetToken(); + return PR_TRUE; + } + if (!RequireWhitespace()) + return PR_FALSE; + + nsRefPtr arr = nsCSSValue::Array::Create(2); + if (!arr) { + mScanner.SetLowLevelError(NS_ERROR_OUT_OF_MEMORY); + return PR_FALSE; + } + arr->Item(0) = aValue; + storage = &arr->Item(1); + aValue.SetArrayValue(arr, unit); + } +} + +struct ReduceNumberCalcOps : public mozilla::css::BasicFloatCalcOps +{ + struct ComputeData {}; + + static result_type ComputeLeafValue(const nsCSSValue& aValue, + const ComputeData& aClosure) + { + NS_ABORT_IF_FALSE(aValue.GetUnit() == eCSSUnit_Number, "unexpected unit"); + return aValue.GetFloatValue(); + } + + static float ComputeNumber(const nsCSSValue& aValue) + { + return mozilla::css::ComputeCalc( + aValue, ReduceNumberCalcOps::ComputeData()); + } +}; + +// * If aVariantMask is VARIANT_NUMBER, this function parses the +// production. +// * If aVariantMask does not contain VARIANT_NUMBER, this function +// parses the production. +// * Otherwise (VARIANT_NUMBER and other bits) this function parses +// whichever one of the productions matches ***and modifies +// aVariantMask*** to reflect which one it has parsed by either +// removing VARIANT_NUMBER or removing all other bits. +// It does so iteratively, but builds the correct recursive data +// structure. +// This function always consumes *trailing* whitespace when it returns +// true; whether there was any such whitespace is returned in the +// aHadFinalWS parameter. +PRBool +CSSParserImpl::ParseCalcMultiplicativeExpression(nsCSSValue& aValue, + PRInt32& aVariantMask, + PRBool *aHadFinalWS) +{ + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + PRBool gotValue = PR_FALSE; // already got the part with the unit + PRBool afterDivision = PR_FALSE; + + nsCSSValue *storage = &aValue; + for (;;) { + PRInt32 variantMask; + if (afterDivision || gotValue) { + variantMask = VARIANT_NUMBER; + } else { + variantMask = aVariantMask | VARIANT_NUMBER; + } + if (!ParseCalcTerm(*storage, variantMask)) + return PR_FALSE; + NS_ABORT_IF_FALSE(variantMask != 0, + "ParseCalcTerm did not set variantMask appropriately"); + NS_ABORT_IF_FALSE(!(variantMask & VARIANT_NUMBER) || + !(variantMask & ~PRInt32(VARIANT_NUMBER)), + "ParseCalcTerm did not set variantMask appropriately"); + + if (variantMask & VARIANT_NUMBER) { + // Simplify the value immediately so we can check for division by + // zero. + float number = mozilla::css::ComputeCalc( + *storage, ReduceNumberCalcOps::ComputeData()); + if (number == 0.0 && afterDivision) + return PR_FALSE; + storage->SetFloatValue(number, eCSSUnit_Number); + } else { + gotValue = PR_TRUE; + + if (storage != &aValue) { + // Simplify any numbers in the Times_L position (which are + // not simplified by the check above). + NS_ABORT_IF_FALSE(storage == &aValue.GetArrayValue()->Item(1), + "unexpected relationship to current storage"); + nsCSSValue &leftValue = aValue.GetArrayValue()->Item(0); + float number = mozilla::css::ComputeCalc( + leftValue, ReduceNumberCalcOps::ComputeData()); + leftValue.SetFloatValue(number, eCSSUnit_Number); + } + } + + PRBool hadWS = RequireWhitespace(); + if (!GetToken(PR_FALSE)) { + *aHadFinalWS = hadWS; + break; + } + nsCSSUnit unit; + if (mToken.IsSymbol('*')) { + unit = gotValue ? eCSSUnit_Calc_Times_R : eCSSUnit_Calc_Times_L; + afterDivision = PR_FALSE; + } else if (mToken.IsSymbol('/')) { + unit = eCSSUnit_Calc_Divided; + afterDivision = PR_TRUE; + } else { + UngetToken(); + *aHadFinalWS = hadWS; + break; + } + + nsRefPtr arr = nsCSSValue::Array::Create(2); + if (!arr) { + mScanner.SetLowLevelError(NS_ERROR_OUT_OF_MEMORY); + return PR_FALSE; + } + arr->Item(0) = aValue; + storage = &arr->Item(1); + aValue.SetArrayValue(arr, unit); + } + + // Adjust aVariantMask (see comments above function) to reflect which + // option we took. + if (aVariantMask & VARIANT_NUMBER) { + if (gotValue) { + aVariantMask &= ~PRInt32(VARIANT_NUMBER); + } else { + aVariantMask = VARIANT_NUMBER; + } + } else { + if (!gotValue) { + // We had to find a value, but we didn't. + return PR_FALSE; + } + } + + return PR_TRUE; +} + +// * If aVariantMask is VARIANT_NUMBER, this function parses the +// production. +// * If aVariantMask does not contain VARIANT_NUMBER, this function +// parses the production. +// * Otherwise (VARIANT_NUMBER and other bits) this function parses +// whichever one of the productions matches ***and modifies +// aVariantMask*** to reflect which one it has parsed by either +// removing VARIANT_NUMBER or removing all other bits. +PRBool +CSSParserImpl::ParseCalcTerm(nsCSSValue& aValue, PRInt32& aVariantMask) +{ + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + if (!GetToken(PR_TRUE)) + return PR_FALSE; + // Either an additive expression in parentheses... + if (mToken.IsSymbol('(')) { + if (!ParseCalcAdditiveExpression(aValue, aVariantMask) || + !ExpectSymbol(')', PR_TRUE)) { + SkipUntil(')'); + return PR_FALSE; + } + return PR_TRUE; + } + // ... or a min() or max() expression + if (mToken.mType == eCSSToken_Function && + (mToken.mIdent.LowerCaseEqualsLiteral("min") || + mToken.mIdent.LowerCaseEqualsLiteral("max"))) { + nsCSSUnit unit = mToken.mIdent.LowerCaseEqualsLiteral("min") + ? eCSSUnit_Calc_Minimum : eCSSUnit_Calc_Maximum; + return ParseCalcMinMax(aValue, unit, aVariantMask); + } + // ... or just a value + UngetToken(); + if (!ParseVariant(aValue, aVariantMask, nsnull)) { + return PR_FALSE; + } + // If we did the value parsing, we need to adjust aVariantMask to + // reflect which option we took (see above). + if (aVariantMask & VARIANT_NUMBER) { + if (aValue.GetUnit() == eCSSUnit_Number) { + aVariantMask = VARIANT_NUMBER; + } else { + aVariantMask &= ~PRInt32(VARIANT_NUMBER); + } + } + return PR_TRUE; +} + +// This function handles and modifies aVariantMask exactly as +// described for ParcCalcTerm above. +PRBool +CSSParserImpl::ParseCalcMinMax(nsCSSValue& aValue, nsCSSUnit aUnit, + PRInt32& aVariantMask) +{ + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + NS_ASSERTION(aUnit == eCSSUnit_Calc_Minimum || + aUnit == eCSSUnit_Calc_Maximum, + "unexpected unit"); + NS_ASSERTION(mToken.mType == eCSSToken_Function, "unexpected current token"); + NS_ASSERTION(aUnit != eCSSUnit_Calc_Minimum || + mToken.mIdent.LowerCaseEqualsLiteral("min") || + mToken.mIdent.LowerCaseEqualsLiteral("-moz-min"), + "unexpected current token"); + NS_ASSERTION(aUnit != eCSSUnit_Calc_Maximum || + mToken.mIdent.LowerCaseEqualsLiteral("max") || + mToken.mIdent.LowerCaseEqualsLiteral("-moz-max"), + "unexpected current token"); + + nsAutoTArray values; + for (;;) { + nsCSSValue *v = values.AppendElement(); + if (!v) { + mScanner.SetLowLevelError(NS_ERROR_OUT_OF_MEMORY); + return PR_FALSE; + } + + if (!ParseCalcAdditiveExpression(*v, aVariantMask)) + return PR_FALSE; + + NS_ABORT_IF_FALSE(!(aVariantMask & VARIANT_NUMBER) || + !(aVariantMask & ~PRInt32(VARIANT_NUMBER)), + "parsing additive expr did not adjust variant mask"); + NS_ABORT_IF_FALSE(aVariantMask != 0, "unexpected variant mask"); + + if (ExpectSymbol(',', PR_TRUE)) + continue; + + if (ExpectSymbol(')', PR_TRUE)) + break; + + SkipUntil(')'); + return PR_FALSE; + } + + // We allow min() and max() to take 1 or more arguments; the code + // above already ensures that. + NS_ABORT_IF_FALSE(values.Length() > 0, "unexpected length"); + + nsRefPtr arr = nsCSSValue::Array::Create(values.Length()); + if (!arr) { + mScanner.SetLowLevelError(NS_ERROR_OUT_OF_MEMORY); + return PR_FALSE; + } + for (PRUint32 i = 0, i_end = values.Length(); i < i_end; ++i) { + arr->Item(i) = values[i]; + } + + aValue.SetArrayValue(arr, aUnit); + return PR_TRUE; +} + +// This function consumes all consecutive whitespace and returns whether +// there was any. +PRBool +CSSParserImpl::RequireWhitespace() +{ + if (!GetToken(PR_FALSE)) + return PR_FALSE; + if (mToken.mType != eCSSToken_WhiteSpace) { + UngetToken(); + return PR_FALSE; + } + // Skip any additional whitespace tokens. + if (GetToken(PR_TRUE)) { + UngetToken(); + } + return PR_TRUE; +} + PRBool CSSParserImpl::ParseRect(nsCSSRect& aRect, nsCSSProperty aPropID) {