// Copyright 2016 The Chromium Authors. All rights reserved. // Copyright (C) 2016 Apple Inc. All rights reserved. // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "config.h" #include "CSSPropertyParserHelpers.h" #include "CSSCalculationValue.h" #include "CSSCanvasValue.h" #include "CSSCrossfadeValue.h" #include "CSSFilterImageValue.h" #include "CSSGradientValue.h" #include "CSSImageSetValue.h" #include "CSSImageValue.h" #include "CSSNamedImageValue.h" #include "CSSParserIdioms.h" #include "CSSValuePool.h" #include "Pair.h" #include "StyleColor.h" namespace WebCore { namespace CSSPropertyParserHelpers { bool consumeCommaIncludingWhitespace(CSSParserTokenRange& range) { CSSParserToken value = range.peek(); if (value.type() != CommaToken) return false; range.consumeIncludingWhitespace(); return true; } bool consumeSlashIncludingWhitespace(CSSParserTokenRange& range) { CSSParserToken value = range.peek(); if (value.type() != DelimiterToken || value.delimiter() != '/') return false; range.consumeIncludingWhitespace(); return true; } CSSParserTokenRange consumeFunction(CSSParserTokenRange& range) { ASSERT(range.peek().type() == FunctionToken); CSSParserTokenRange contents = range.consumeBlock(); range.consumeWhitespace(); contents.consumeWhitespace(); return contents; } // FIXME: consider pulling in the parsing logic from CSSCalculationValue.cpp. class CalcParser { public: explicit CalcParser(CSSParserTokenRange& range, ValueRange valueRange = ValueRangeAll) : m_sourceRange(range) , m_range(range) { const CSSParserToken& token = range.peek(); if (token.functionId() == CSSValueCalc || token.functionId() == CSSValueWebkitCalc) m_calcValue = CSSCalcValue::create(consumeFunction(m_range), valueRange); } const CSSCalcValue* value() const { return m_calcValue.get(); } RefPtr consumeValue() { if (!m_calcValue) return nullptr; m_sourceRange = m_range; return CSSValuePool::singleton().createValue(m_calcValue.release()); } RefPtr consumeNumber() { if (!m_calcValue) return nullptr; m_sourceRange = m_range; return CSSValuePool::singleton().createValue(m_calcValue->doubleValue(), CSSPrimitiveValue::UnitType::CSS_NUMBER); } bool consumeNumberRaw(double& result) { if (!m_calcValue || m_calcValue->category() != CalcNumber) return false; m_sourceRange = m_range; result = m_calcValue->doubleValue(); return true; } bool consumePositiveIntegerRaw(int& result) { if (!m_calcValue || m_calcValue->category() != CalcNumber || !m_calcValue->isInt()) return false; result = static_cast(m_calcValue->doubleValue()); if (result < 1) return false; m_sourceRange = m_range; return true; } private: CSSParserTokenRange& m_sourceRange; CSSParserTokenRange m_range; RefPtr m_calcValue; }; RefPtr consumeInteger(CSSParserTokenRange& range, double minimumValue) { const CSSParserToken& token = range.peek(); if (token.type() == NumberToken) { if (token.numericValueType() == NumberValueType || token.numericValue() < minimumValue) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), CSSPrimitiveValue::UnitType::CSS_NUMBER); } CalcParser calcParser(range); if (const CSSCalcValue* calculation = calcParser.value()) { if (calculation->category() != CalcNumber || !calculation->isInt()) return nullptr; double value = calculation->doubleValue(); if (value < minimumValue) return nullptr; return calcParser.consumeNumber(); } return nullptr; } RefPtr consumePositiveInteger(CSSParserTokenRange& range) { return consumeInteger(range, 1); } bool consumePositiveIntegerRaw(CSSParserTokenRange& range, int& result) { const CSSParserToken& token = range.peek(); if (token.type() == NumberToken) { if (token.numericValueType() == NumberValueType || token.numericValue() < 1) return false; result = range.consumeIncludingWhitespace().numericValue(); return true; } CalcParser calcParser(range); return calcParser.consumePositiveIntegerRaw(result); } bool consumeNumberRaw(CSSParserTokenRange& range, double& result) { if (range.peek().type() == NumberToken) { result = range.consumeIncludingWhitespace().numericValue(); return true; } CalcParser calcParser(range, ValueRangeAll); return calcParser.consumeNumberRaw(result); } // FIXME: Work out if this can just call consumeNumberRaw RefPtr consumeNumber(CSSParserTokenRange& range, ValueRange valueRange) { const CSSParserToken& token = range.peek(); if (token.type() == NumberToken) { if (valueRange == ValueRangeNonNegative && token.numericValue() < 0) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType()); } CalcParser calcParser(range, ValueRangeAll); if (const CSSCalcValue* calculation = calcParser.value()) { // FIXME: Calcs should not be subject to parse time range checks. // spec: https://drafts.csswg.org/css-values-3/#calc-range if (calculation->category() != CalcNumber || (valueRange == ValueRangeNonNegative && calculation->isNegative())) return nullptr; return calcParser.consumeNumber(); } return nullptr; } inline bool shouldAcceptUnitlessValue(double value, CSSParserMode cssParserMode, UnitlessQuirk unitless) { // FIXME: Presentational HTML attributes shouldn't use the CSS parser for lengths return value == 0 || isUnitLessValueParsingEnabledForMode(cssParserMode) || (cssParserMode == HTMLQuirksMode && unitless == UnitlessQuirk::Allow); } RefPtr consumeLength(CSSParserTokenRange& range, CSSParserMode cssParserMode, ValueRange valueRange, UnitlessQuirk unitless) { const CSSParserToken& token = range.peek(); if (token.type() == DimensionToken) { switch (token.unitType()) { case CSSPrimitiveValue::UnitType::CSS_QUIRKY_EMS: if (cssParserMode != UASheetMode) return nullptr; FALLTHROUGH; case CSSPrimitiveValue::UnitType::CSS_EMS: case CSSPrimitiveValue::UnitType::CSS_REMS: case CSSPrimitiveValue::UnitType::CSS_CHS: case CSSPrimitiveValue::UnitType::CSS_EXS: case CSSPrimitiveValue::UnitType::CSS_PX: case CSSPrimitiveValue::UnitType::CSS_CM: case CSSPrimitiveValue::UnitType::CSS_MM: case CSSPrimitiveValue::UnitType::CSS_IN: case CSSPrimitiveValue::UnitType::CSS_PT: case CSSPrimitiveValue::UnitType::CSS_PC: case CSSPrimitiveValue::UnitType::CSS_VW: case CSSPrimitiveValue::UnitType::CSS_VH: case CSSPrimitiveValue::UnitType::CSS_VMIN: case CSSPrimitiveValue::UnitType::CSS_VMAX: break; default: return nullptr; } if ((valueRange == ValueRangeNonNegative && token.numericValue() < 0) || std::isinf(token.numericValue())) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType()); } if (token.type() == NumberToken) { if (!shouldAcceptUnitlessValue(token.numericValue(), cssParserMode, unitless) || (valueRange == ValueRangeNonNegative && token.numericValue() < 0)) return nullptr; if (std::isinf(token.numericValue())) return nullptr; CSSPrimitiveValue::UnitType unitType = CSSPrimitiveValue::UnitType::CSS_PX; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), unitType); } CalcParser calcParser(range, valueRange); if (calcParser.value() && calcParser.value()->category() == CalcLength) return calcParser.consumeValue(); return nullptr; } RefPtr consumePercent(CSSParserTokenRange& range, ValueRange valueRange) { const CSSParserToken& token = range.peek(); if (token.type() == PercentageToken) { if ((valueRange == ValueRangeNonNegative && token.numericValue() < 0) || std::isinf(token.numericValue())) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), CSSPrimitiveValue::UnitType::CSS_PERCENTAGE); } CalcParser calcParser(range, valueRange); if (const CSSCalcValue* calculation = calcParser.value()) { if (calculation->category() == CalcPercent) return calcParser.consumeValue(); } return nullptr; } static bool canConsumeCalcValue(CalculationCategory category, CSSParserMode cssParserMode) { if (category == CalcLength || category == CalcPercent || category == CalcPercentLength) return true; if (cssParserMode != SVGAttributeMode) return false; if (category == CalcNumber || category == CalcPercentNumber) return true; return false; } RefPtr consumeLengthOrPercent(CSSParserTokenRange& range, CSSParserMode cssParserMode, ValueRange valueRange, UnitlessQuirk unitless) { const CSSParserToken& token = range.peek(); if (token.type() == DimensionToken || token.type() == NumberToken) return consumeLength(range, cssParserMode, valueRange, unitless); if (token.type() == PercentageToken) return consumePercent(range, valueRange); CalcParser calcParser(range, valueRange); if (const CSSCalcValue* calculation = calcParser.value()) { if (canConsumeCalcValue(calculation->category(), cssParserMode)) return calcParser.consumeValue(); } return nullptr; } RefPtr consumeAngle(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless) { const CSSParserToken& token = range.peek(); if (token.type() == DimensionToken) { switch (token.unitType()) { case CSSPrimitiveValue::UnitType::CSS_DEG: case CSSPrimitiveValue::UnitType::CSS_RAD: case CSSPrimitiveValue::UnitType::CSS_GRAD: case CSSPrimitiveValue::UnitType::CSS_TURN: return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType()); default: return nullptr; } } if (token.type() == NumberToken && shouldAcceptUnitlessValue(token.numericValue(), cssParserMode, unitless)) { return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), CSSPrimitiveValue::UnitType::CSS_DEG); } CalcParser calcParser(range, ValueRangeAll); if (const CSSCalcValue* calculation = calcParser.value()) { if (calculation->category() == CalcAngle) return calcParser.consumeValue(); } return nullptr; } RefPtr consumeTime(CSSParserTokenRange& range, CSSParserMode cssParserMode, ValueRange valueRange, UnitlessQuirk unitless) { const CSSParserToken& token = range.peek(); CSSPrimitiveValue::UnitType unit = token.unitType(); bool acceptUnitless = token.type() == NumberToken && shouldAcceptUnitlessValue(token.numericValue(), cssParserMode, unitless); if (acceptUnitless) unit = CSSPrimitiveValue::UnitType::CSS_MS; if (token.type() == DimensionToken || acceptUnitless) { if (valueRange == ValueRangeNonNegative && token.numericValue() < 0) return nullptr; if (unit == CSSPrimitiveValue::UnitType::CSS_MS || unit == CSSPrimitiveValue::UnitType::CSS_S) return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().numericValue(), unit); return nullptr; } CalcParser calcParser(range, valueRange); if (const CSSCalcValue* calculation = calcParser.value()) { if (calculation->category() == CalcTime) return calcParser.consumeValue(); } return nullptr; } RefPtr consumeIdent(CSSParserTokenRange& range) { if (range.peek().type() != IdentToken) return nullptr; return CSSValuePool::singleton().createIdentifierValue(range.consumeIncludingWhitespace().id()); } RefPtr consumeIdentRange(CSSParserTokenRange& range, CSSValueID lower, CSSValueID upper) { if (range.peek().id() < lower || range.peek().id() > upper) return nullptr; return consumeIdent(range); } // FIXME-NEWPARSER: Eventually we'd like this to use CSSCustomIdentValue, but we need // to do other plumbing work first (like changing Pair to CSSValuePair and make it not // use only primitive values). RefPtr consumeCustomIdent(CSSParserTokenRange& range) { if (range.peek().type() != IdentToken || isCSSWideKeyword(range.peek().id())) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().value().toString(), CSSPrimitiveValue::UnitType::CSS_STRING); } RefPtr consumeString(CSSParserTokenRange& range) { if (range.peek().type() != StringToken) return nullptr; return CSSValuePool::singleton().createValue(range.consumeIncludingWhitespace().value().toString(), CSSPrimitiveValue::UnitType::CSS_STRING); } StringView consumeUrlAsStringView(CSSParserTokenRange& range) { const CSSParserToken& token = range.peek(); if (token.type() == UrlToken) { range.consumeIncludingWhitespace(); return token.value(); } if (token.functionId() == CSSValueUrl) { CSSParserTokenRange urlRange = range; CSSParserTokenRange urlArgs = urlRange.consumeBlock(); const CSSParserToken& next = urlArgs.consumeIncludingWhitespace(); if (next.type() == BadStringToken || !urlArgs.atEnd()) return StringView(); ASSERT(next.type() == StringToken); range = urlRange; range.consumeWhitespace(); return next.value(); } return StringView(); } RefPtr consumeUrl(CSSParserTokenRange& range) { StringView url = consumeUrlAsStringView(range); if (url.isNull()) return nullptr; return CSSValuePool::singleton().createValue(url.toString(), CSSPrimitiveValue::UnitType::CSS_URI); } static int clampRGBComponent(const CSSPrimitiveValue& value) { double result = value.doubleValue(); // FIXME: Multiply by 2.55 and round instead of floor. if (value.isPercentage()) result *= 2.56; return clampTo(result, 0, 255); } static Color parseRGBParameters(CSSParserTokenRange& range, bool parseAlpha) { ASSERT(range.peek().functionId() == CSSValueRgb || range.peek().functionId() == CSSValueRgba); Color result; CSSParserTokenRange args = consumeFunction(range); RefPtr colorParameter = consumeInteger(args); if (!colorParameter) colorParameter = consumePercent(args, ValueRangeAll); if (!colorParameter) return Color(); const bool isPercent = colorParameter->isPercentage(); int colorArray[3]; colorArray[0] = clampRGBComponent(*colorParameter); for (int i = 1; i < 3; i++) { if (!consumeCommaIncludingWhitespace(args)) return Color(); colorParameter = isPercent ? consumePercent(args, ValueRangeAll) : consumeInteger(args); if (!colorParameter) return Color(); colorArray[i] = clampRGBComponent(*colorParameter); } if (parseAlpha) { if (!consumeCommaIncludingWhitespace(args)) return Color(); double alpha; if (!consumeNumberRaw(args, alpha)) return Color(); // Convert the floating pointer number of alpha to an integer in the range [0, 256), // with an equal distribution across all 256 values. int alphaComponent = static_cast(clampTo(alpha, 0.0, 1.0) * nextafter(256.0, 0.0)); result = Color(makeRGBA(colorArray[0], colorArray[1], colorArray[2], alphaComponent)); } else { result = Color(makeRGB(colorArray[0], colorArray[1], colorArray[2])); } if (!args.atEnd()) return Color(); return result; } static Color parseHSLParameters(CSSParserTokenRange& range, bool parseAlpha) { ASSERT(range.peek().functionId() == CSSValueHsl || range.peek().functionId() == CSSValueHsla); CSSParserTokenRange args = consumeFunction(range); RefPtr hslValue = consumeNumber(args, ValueRangeAll); if (!hslValue) return Color(); double colorArray[3]; colorArray[0] = (((hslValue->intValue() % 360) + 360) % 360) / 360.0; for (int i = 1; i < 3; i++) { if (!consumeCommaIncludingWhitespace(args)) return Color(); hslValue = consumePercent(args, ValueRangeAll); if (!hslValue) return Color(); double doubleValue = hslValue->doubleValue(); colorArray[i] = clampTo(doubleValue, 0.0, 100.0) / 100.0; // Needs to be value between 0 and 1.0. } double alpha = 1.0; if (parseAlpha) { if (!consumeCommaIncludingWhitespace(args)) return Color(); if (!consumeNumberRaw(args, alpha)) return Color(); alpha = clampTo(alpha, 0.0, 1.0); } if (!args.atEnd()) return Color(); return Color(makeRGBAFromHSLA(colorArray[0], colorArray[1], colorArray[2], alpha)); } static Color parseColorFunctionParameters(CSSParserTokenRange& range) { ASSERT(range.peek().functionId() == CSSValueColor); CSSParserTokenRange args = consumeFunction(range); ColorSpace colorSpace; switch (args.peek().id()) { case CSSValueSRGB: colorSpace = ColorSpaceSRGB; break; case CSSValueDisplayP3: colorSpace = ColorSpaceDisplayP3; break; default: return Color(); } consumeIdent(args); double colorChannels[4] = { 0, 0, 0, 1 }; for (int i = 0; i < 3; ++i) { double value; if (consumeNumberRaw(args, value)) colorChannels[i] = std::max(0.0, std::min(1.0, value)); else break; } if (consumeSlashIncludingWhitespace(args)) { auto alphaParameter = consumePercent(args, ValueRangeAll); if (!alphaParameter) alphaParameter = consumeNumber(args, ValueRangeAll); if (!alphaParameter) return Color(); colorChannels[3] = std::max(0.0, std::min(1.0, alphaParameter->isPercentage() ? (alphaParameter->doubleValue() / 100) : alphaParameter->doubleValue())); } // FIXME: Support the comma-separated list of fallback color values. if (!args.atEnd()) return Color(); return Color(colorChannels[0], colorChannels[1], colorChannels[2], colorChannels[3], colorSpace); } static Color parseHexColor(CSSParserTokenRange& range, bool acceptQuirkyColors) { RGBA32 result; const CSSParserToken& token = range.peek(); if (token.type() == HashToken) { if (!Color::parseHexColor(token.value(), result)) return Color(); } else if (acceptQuirkyColors) { String color; if (token.type() == NumberToken || token.type() == DimensionToken) { if (token.numericValueType() != IntegerValueType || token.numericValue() < 0. || token.numericValue() >= 1000000.) return Color(); if (token.type() == NumberToken) // e.g. 112233 color = String::format("%d", static_cast(token.numericValue())); else // e.g. 0001FF color = String::number(static_cast(token.numericValue())) + token.value().toString(); while (color.length() < 6) color = "0" + color; } else if (token.type() == IdentToken) { // e.g. FF0000 color = token.value().toString(); } unsigned length = color.length(); if (length != 3 && length != 6) return Color(); if (!Color::parseHexColor(color, result)) return Color(); } else { return Color(); } range.consumeIncludingWhitespace(); return Color(result); } static Color parseColorFunction(CSSParserTokenRange& range) { CSSParserTokenRange colorRange = range; CSSValueID functionId = range.peek().functionId(); Color color; switch (functionId) { case CSSValueRgb: case CSSValueRgba: color = parseRGBParameters(colorRange, functionId == CSSValueRgba); break; case CSSValueHsl: case CSSValueHsla: color = parseHSLParameters(colorRange, functionId == CSSValueHsla); break; case CSSValueColor: color = parseColorFunctionParameters(colorRange); break; default: return Color(); } range = colorRange; return color; } RefPtr consumeColor(CSSParserTokenRange& range, CSSParserMode cssParserMode, bool acceptQuirkyColors) { CSSValueID id = range.peek().id(); if (StyleColor::isColorKeyword(id)) { if (!isValueAllowedInMode(id, cssParserMode)) return nullptr; return consumeIdent(range); } Color color = parseHexColor(range, acceptQuirkyColors); if (!color.isValid()) color = parseColorFunction(range); if (!color.isValid()) return nullptr; return CSSValuePool::singleton().createValue(color); } static RefPtr consumePositionComponent(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless) { if (range.peek().type() == IdentToken) return consumeIdent(range); return consumeLengthOrPercent(range, cssParserMode, ValueRangeAll, unitless); } static bool isHorizontalPositionKeywordOnly(const CSSPrimitiveValue& value) { return value.isValueID() && (value.valueID() == CSSValueLeft || value.valueID() == CSSValueRight); } static bool isVerticalPositionKeywordOnly(const CSSPrimitiveValue& value) { return value.isValueID() && (value.valueID() == CSSValueTop || value.valueID() == CSSValueBottom); } static void positionFromOneValue(CSSPrimitiveValue& value, RefPtr& resultX, RefPtr& resultY) { bool valueAppliesToYAxisOnly = isVerticalPositionKeywordOnly(value); resultX = &value; resultY = CSSPrimitiveValue::createIdentifier(CSSValueCenter); if (valueAppliesToYAxisOnly) std::swap(resultX, resultY); } static bool positionFromTwoValues(CSSPrimitiveValue& value1, CSSPrimitiveValue& value2, RefPtr& resultX, RefPtr& resultY) { bool mustOrderAsXY = isHorizontalPositionKeywordOnly(value1) || isVerticalPositionKeywordOnly(value2) || !value1.isValueID() || !value2.isValueID(); bool mustOrderAsYX = isVerticalPositionKeywordOnly(value1) || isHorizontalPositionKeywordOnly(value2); if (mustOrderAsXY && mustOrderAsYX) return false; resultX = &value1; resultY = &value2; if (mustOrderAsYX) std::swap(resultX, resultY); return true; } template static Ref createPrimitiveValuePair(Args&&... args) { return CSSValuePool::singleton().createValue(Pair::create(std::forward(args)...)); } static bool positionFromThreeOrFourValues(CSSPrimitiveValue** values, RefPtr& resultX, RefPtr& resultY) { CSSPrimitiveValue* center = nullptr; for (int i = 0; values[i]; i++) { CSSPrimitiveValue* currentValue = values[i]; if (!currentValue->isValueID()) return false; CSSValueID id = currentValue->valueID(); if (id == CSSValueCenter) { if (center) return false; center = currentValue; continue; } RefPtr result; if (values[i + 1] && !values[i + 1]->isValueID()) result = createPrimitiveValuePair(currentValue, values[++i]); else result = currentValue; if (id == CSSValueLeft || id == CSSValueRight) { if (resultX) return false; resultX = result; } else { ASSERT(id == CSSValueTop || id == CSSValueBottom); if (resultY) return false; resultY = result; } } if (center) { ASSERT(resultX || resultY); if (resultX && resultY) return false; if (!resultX) resultX = center; else resultY = center; } ASSERT(resultX && resultY); return true; } // FIXME: This may consume from the range upon failure. The background // shorthand works around it, but we should just fix it here. bool consumePosition(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless, RefPtr& resultX, RefPtr& resultY) { RefPtr value1 = consumePositionComponent(range, cssParserMode, unitless); if (!value1) return false; RefPtr value2 = consumePositionComponent(range, cssParserMode, unitless); if (!value2) { positionFromOneValue(*value1, resultX, resultY); return true; } RefPtr value3 = consumePositionComponent(range, cssParserMode, unitless); if (!value3) return positionFromTwoValues(*value1, *value2, resultX, resultY); RefPtr value4 = consumePositionComponent(range, cssParserMode, unitless); CSSPrimitiveValue* values[5]; values[0] = value1.get(); values[1] = value2.get(); values[2] = value3.get(); values[3] = value4.get(); values[4] = nullptr; return positionFromThreeOrFourValues(values, resultX, resultY); } RefPtr consumePosition(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless) { RefPtr resultX; RefPtr resultY; if (consumePosition(range, cssParserMode, unitless, resultX, resultY)) return createPrimitiveValuePair(resultX.releaseNonNull(), resultY.releaseNonNull()); return nullptr; } bool consumeOneOrTwoValuedPosition(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless, RefPtr& resultX, RefPtr& resultY) { RefPtr value1 = consumePositionComponent(range, cssParserMode, unitless); if (!value1) return false; RefPtr value2 = consumePositionComponent(range, cssParserMode, unitless); if (!value2) { positionFromOneValue(*value1, resultX, resultY); return true; } return positionFromTwoValues(*value1, *value2, resultX, resultY); } // This should go away once we drop support for -webkit-gradient static RefPtr consumeDeprecatedGradientPoint(CSSParserTokenRange& args, bool horizontal) { if (args.peek().type() == IdentToken) { if ((horizontal && consumeIdent(args)) || (!horizontal && consumeIdent(args))) return CSSValuePool::singleton().createValue(0., CSSPrimitiveValue::UnitType::CSS_PERCENTAGE); if ((horizontal && consumeIdent(args)) || (!horizontal && consumeIdent(args))) return CSSValuePool::singleton().createValue(100., CSSPrimitiveValue::UnitType::CSS_PERCENTAGE); if (consumeIdent(args)) return CSSValuePool::singleton().createValue(50., CSSPrimitiveValue::UnitType::CSS_PERCENTAGE); return nullptr; } RefPtr result = consumePercent(args, ValueRangeAll); if (!result) result = consumeNumber(args, ValueRangeAll); return result; } // Used to parse colors for -webkit-gradient(...). static RefPtr consumeDeprecatedGradientStopColor(CSSParserTokenRange& args, CSSParserMode cssParserMode) { if (args.peek().id() == CSSValueCurrentcolor) return nullptr; return consumeColor(args, cssParserMode); } static bool consumeDeprecatedGradientColorStop(CSSParserTokenRange& range, CSSGradientColorStop& stop, CSSParserMode cssParserMode) { CSSValueID id = range.peek().functionId(); if (id != CSSValueFrom && id != CSSValueTo && id != CSSValueColorStop) return false; CSSParserTokenRange args = consumeFunction(range); double position; if (id == CSSValueFrom || id == CSSValueTo) { position = (id == CSSValueFrom) ? 0 : 1; } else { ASSERT(id == CSSValueColorStop); const CSSParserToken& arg = args.consumeIncludingWhitespace(); if (arg.type() == PercentageToken) position = arg.numericValue() / 100.0; else if (arg.type() == NumberToken) position = arg.numericValue(); else return false; if (!consumeCommaIncludingWhitespace(args)) return false; } stop.m_position = CSSValuePool::singleton().createValue(position, CSSPrimitiveValue::UnitType::CSS_NUMBER); stop.m_color = consumeDeprecatedGradientStopColor(args, cssParserMode); return stop.m_color && args.atEnd(); } static RefPtr consumeDeprecatedGradient(CSSParserTokenRange& args, CSSParserMode cssParserMode) { RefPtr result; CSSValueID id = args.consumeIncludingWhitespace().id(); bool isDeprecatedRadialGradient = (id == CSSValueRadial); if (isDeprecatedRadialGradient) result = CSSRadialGradientValue::create(NonRepeating, CSSDeprecatedRadialGradient); else if (id == CSSValueLinear) result = CSSLinearGradientValue::create(NonRepeating, CSSDeprecatedLinearGradient); if (!result || !consumeCommaIncludingWhitespace(args)) return nullptr; RefPtr point = consumeDeprecatedGradientPoint(args, true); if (!point) return nullptr; result->setFirstX(point.copyRef()); point = consumeDeprecatedGradientPoint(args, false); if (!point) return nullptr; result->setFirstY(point.copyRef()); if (!consumeCommaIncludingWhitespace(args)) return nullptr; // For radial gradients only, we now expect a numeric radius. if (isDeprecatedRadialGradient) { RefPtr radius = consumeNumber(args, ValueRangeAll); if (!radius || !consumeCommaIncludingWhitespace(args)) return nullptr; downcast(result.get())->setFirstRadius(radius.copyRef()); } point = consumeDeprecatedGradientPoint(args, true); if (!point) return nullptr; result->setSecondX(point.copyRef()); point = consumeDeprecatedGradientPoint(args, false); if (!point) return nullptr; result->setSecondY(point.copyRef()); // For radial gradients only, we now expect the second radius. if (isDeprecatedRadialGradient) { if (!consumeCommaIncludingWhitespace(args)) return nullptr; RefPtr radius = consumeNumber(args, ValueRangeAll); if (!radius) return nullptr; downcast(result.get())->setSecondRadius(radius.copyRef()); } CSSGradientColorStop stop; while (consumeCommaIncludingWhitespace(args)) { if (!consumeDeprecatedGradientColorStop(args, stop, cssParserMode)) return nullptr; result->addStop(stop); } return result; } static bool consumeGradientColorStops(CSSParserTokenRange& range, CSSParserMode cssParserMode, CSSGradientValue* gradient) { bool supportsColorHints = gradient->gradientType() == CSSLinearGradient || gradient->gradientType() == CSSRadialGradient; // The first color stop cannot be a color hint. bool previousStopWasColorHint = true; do { CSSGradientColorStop stop; stop.m_color = consumeColor(range, cssParserMode); // Two hints in a row are not allowed. if (!stop.m_color && (!supportsColorHints || previousStopWasColorHint)) return false; previousStopWasColorHint = !stop.m_color; // FIXME-NEWPARSER: This boolean could be removed. Null checking color would be sufficient. stop.isMidpoint = !stop.m_color; stop.m_position = consumeLengthOrPercent(range, cssParserMode, ValueRangeAll); if (!stop.m_color && !stop.m_position) return false; gradient->addStop(stop); } while (consumeCommaIncludingWhitespace(range)); // The last color stop cannot be a color hint. if (previousStopWasColorHint) return false; // Must have 2 or more stops to be valid. return gradient->stopCount() >= 2; } static RefPtr consumeDeprecatedRadialGradient(CSSParserTokenRange& args, CSSParserMode cssParserMode, CSSGradientRepeat repeating) { RefPtr result = CSSRadialGradientValue::create(repeating, CSSPrefixedRadialGradient); RefPtr centerX; RefPtr centerY; consumeOneOrTwoValuedPosition(args, cssParserMode, UnitlessQuirk::Forbid, centerX, centerY); if ((centerX || centerY) && !consumeCommaIncludingWhitespace(args)) return nullptr; result->setFirstX(centerX.copyRef()); result->setFirstY(centerY.copyRef()); result->setSecondX(centerX.copyRef()); result->setSecondY(centerY.copyRef()); RefPtr shape = consumeIdent(args); RefPtr sizeKeyword = consumeIdent(args); if (!shape) shape = consumeIdent(args); result->setShape(shape.copyRef()); result->setSizingBehavior(sizeKeyword.copyRef()); // Or, two lengths or percentages if (!shape && !sizeKeyword) { RefPtr horizontalSize = consumeLengthOrPercent(args, cssParserMode, ValueRangeAll); RefPtr verticalSize; if (horizontalSize) { verticalSize = consumeLengthOrPercent(args, cssParserMode, ValueRangeAll); if (!verticalSize) return nullptr; consumeCommaIncludingWhitespace(args); result->setEndHorizontalSize(horizontalSize.copyRef()); result->setEndVerticalSize(verticalSize.copyRef()); } } else { consumeCommaIncludingWhitespace(args); } if (!consumeGradientColorStops(args, cssParserMode, result.get())) return nullptr; return result; } static RefPtr consumeRadialGradient(CSSParserTokenRange& args, CSSParserMode cssParserMode, CSSGradientRepeat repeating) { RefPtr result = CSSRadialGradientValue::create(repeating, CSSRadialGradient); RefPtr shape; RefPtr sizeKeyword; RefPtr horizontalSize; RefPtr verticalSize; // First part of grammar, the size/shape clause: // [ circle || ] | // [ ellipse || [ | ]{2} ] | // [ [ circle | ellipse] || ] for (int i = 0; i < 3; ++i) { if (args.peek().type() == IdentToken) { CSSValueID id = args.peek().id(); if (id == CSSValueCircle || id == CSSValueEllipse) { if (shape) return nullptr; shape = consumeIdent(args); } else if (id == CSSValueClosestSide || id == CSSValueClosestCorner || id == CSSValueFarthestSide || id == CSSValueFarthestCorner) { if (sizeKeyword) return nullptr; sizeKeyword = consumeIdent(args); } else { break; } } else { RefPtr center = consumeLengthOrPercent(args, cssParserMode, ValueRangeAll); if (!center) break; if (horizontalSize) return nullptr; horizontalSize = center; center = consumeLengthOrPercent(args, cssParserMode, ValueRangeAll); if (center) { verticalSize = center; ++i; } } } // You can specify size as a keyword or a length/percentage, not both. if (sizeKeyword && horizontalSize) return nullptr; // Circles must have 0 or 1 lengths. if (shape && shape->valueID() == CSSValueCircle && verticalSize) return nullptr; // Ellipses must have 0 or 2 length/percentages. if (shape && shape->valueID() == CSSValueEllipse && horizontalSize && !verticalSize) return nullptr; // If there's only one size, it must be a length. if (!verticalSize && horizontalSize && horizontalSize->isPercentage()) return nullptr; if ((horizontalSize && horizontalSize->isCalculatedPercentageWithLength()) || (verticalSize && verticalSize->isCalculatedPercentageWithLength())) return nullptr; result->setShape(shape.copyRef()); result->setSizingBehavior(sizeKeyword.copyRef()); result->setEndHorizontalSize(horizontalSize.copyRef()); result->setEndVerticalSize(verticalSize.copyRef()); RefPtr centerX; RefPtr centerY; if (args.peek().id() == CSSValueAt) { args.consumeIncludingWhitespace(); consumePosition(args, cssParserMode, UnitlessQuirk::Forbid, centerX, centerY); if (!(centerX && centerY)) return nullptr; result->setFirstX(centerX.copyRef()); result->setFirstY(centerY.copyRef()); // Right now, CSS radial gradients have the same start and end centers. result->setSecondX(centerX.copyRef()); result->setSecondY(centerY.copyRef()); } if ((shape || sizeKeyword || horizontalSize || centerX || centerY) && !consumeCommaIncludingWhitespace(args)) return nullptr; if (!consumeGradientColorStops(args, cssParserMode, result.get())) return nullptr; return result; } static RefPtr consumeLinearGradient(CSSParserTokenRange& args, CSSParserMode cssParserMode, CSSGradientRepeat repeating, CSSGradientType gradientType) { RefPtr result = CSSLinearGradientValue::create(repeating, gradientType); bool expectComma = true; RefPtr angle = consumeAngle(args, cssParserMode, UnitlessQuirk::Forbid); if (angle) result->setAngle(angle.releaseNonNull()); else if (gradientType == CSSPrefixedLinearGradient || consumeIdent(args)) { RefPtr endX = consumeIdent(args); RefPtr endY = consumeIdent(args); if (!endX && !endY) { if (gradientType == CSSLinearGradient) return nullptr; endY = CSSPrimitiveValue::createIdentifier(CSSValueTop); expectComma = false; } else if (!endX) { endX = consumeIdent(args); } result->setFirstX(endX.copyRef()); result->setFirstY(endY.copyRef()); } else { expectComma = false; } if (expectComma && !consumeCommaIncludingWhitespace(args)) return nullptr; if (!consumeGradientColorStops(args, cssParserMode, result.get())) return nullptr; return result; } RefPtr consumeImageOrNone(CSSParserTokenRange& range, CSSParserContext context) { if (range.peek().id() == CSSValueNone) return consumeIdent(range); return consumeImage(range, context); } static RefPtr consumeCrossFade(CSSParserTokenRange& args, CSSParserContext context, bool prefixed) { RefPtr fromImageValue = consumeImageOrNone(args, context); if (!fromImageValue || !consumeCommaIncludingWhitespace(args)) return nullptr; RefPtr toImageValue = consumeImageOrNone(args, context); if (!toImageValue || !consumeCommaIncludingWhitespace(args)) return nullptr; RefPtr percentage; const CSSParserToken& percentageArg = args.consumeIncludingWhitespace(); if (percentageArg.type() == PercentageToken) percentage = CSSValuePool::singleton().createValue(clampTo(percentageArg.numericValue() / 100, 0, 1), CSSPrimitiveValue::UnitType::CSS_NUMBER); else if (percentageArg.type() == NumberToken) percentage = CSSValuePool::singleton().createValue(clampTo(percentageArg.numericValue(), 0, 1), CSSPrimitiveValue::UnitType::CSS_NUMBER); if (!percentage) return nullptr; return CSSCrossfadeValue::create(fromImageValue.releaseNonNull(), toImageValue.releaseNonNull(), percentage.releaseNonNull(), prefixed); } static RefPtr consumeWebkitCanvas(CSSParserTokenRange& args) { if (args.peek().type() != IdentToken) return nullptr; auto canvasName = args.consumeIncludingWhitespace().value().toString(); if (!args.atEnd()) return nullptr; return CSSCanvasValue::create(canvasName); } static RefPtr consumeWebkitNamedImage(CSSParserTokenRange& args) { if (args.peek().type() != IdentToken) return nullptr; auto imageName = args.consumeIncludingWhitespace().value().toString(); if (!args.atEnd()) return nullptr; return CSSNamedImageValue::create(imageName); } static RefPtr consumeFilterImage(CSSParserTokenRange& args, const CSSParserContext& context) { auto imageValue = consumeImageOrNone(args, context); if (!imageValue || !consumeCommaIncludingWhitespace(args)) return nullptr; auto filterValue = consumeFilter(args, context); if (!filterValue) return nullptr; if (!args.atEnd()) return nullptr; return CSSFilterImageValue::create(imageValue.releaseNonNull(), filterValue.releaseNonNull()); } static RefPtr consumeGeneratedImage(CSSParserTokenRange& range, CSSParserContext context) { CSSValueID id = range.peek().functionId(); CSSParserTokenRange rangeCopy = range; CSSParserTokenRange args = consumeFunction(rangeCopy); RefPtr result; if (id == CSSValueRadialGradient) result = consumeRadialGradient(args, context.mode, NonRepeating); else if (id == CSSValueRepeatingRadialGradient) result = consumeRadialGradient(args, context.mode, Repeating); else if (id == CSSValueWebkitLinearGradient) result = consumeLinearGradient(args, context.mode, NonRepeating, CSSPrefixedLinearGradient); else if (id == CSSValueWebkitRepeatingLinearGradient) result = consumeLinearGradient(args, context.mode, Repeating, CSSPrefixedLinearGradient); else if (id == CSSValueRepeatingLinearGradient) result = consumeLinearGradient(args, context.mode, Repeating, CSSLinearGradient); else if (id == CSSValueLinearGradient) result = consumeLinearGradient(args, context.mode, NonRepeating, CSSLinearGradient); else if (id == CSSValueWebkitGradient) result = consumeDeprecatedGradient(args, context.mode); else if (id == CSSValueWebkitRadialGradient) result = consumeDeprecatedRadialGradient(args, context.mode, NonRepeating); else if (id == CSSValueWebkitRepeatingRadialGradient) result = consumeDeprecatedRadialGradient(args, context.mode, Repeating); else if (id == CSSValueWebkitCrossFade || id == CSSValueCrossFade) result = consumeCrossFade(args, context, id == CSSValueWebkitCrossFade); else if (id == CSSValueWebkitCanvas) result = consumeWebkitCanvas(args); else if (id == CSSValueWebkitNamedImage) result = consumeWebkitNamedImage(args); else if (id == CSSValueWebkitFilter || id == CSSValueFilter) result = consumeFilterImage(args, context); if (!result || !args.atEnd()) return nullptr; range = rangeCopy; return result; } static RefPtr consumeImageSet(CSSParserTokenRange& range, const CSSParserContext& context) { CSSParserTokenRange rangeCopy = range; CSSParserTokenRange args = consumeFunction(rangeCopy); RefPtr imageSet = CSSImageSetValue::create(); do { AtomicString urlValue = consumeUrlAsStringView(args).toAtomicString(); if (urlValue.isNull()) return nullptr; RefPtr image = CSSImageValue::create(completeURL(context, urlValue)); imageSet->append(image.releaseNonNull()); const CSSParserToken& token = args.consumeIncludingWhitespace(); if (token.type() != DimensionToken) return nullptr; if (token.value() != "x") return nullptr; ASSERT(token.unitType() == CSSPrimitiveValue::UnitType::CSS_UNKNOWN); double imageScaleFactor = token.numericValue(); if (imageScaleFactor <= 0) return nullptr; imageSet->append(CSSValuePool::singleton().createValue(imageScaleFactor, CSSPrimitiveValue::UnitType::CSS_NUMBER)); } while (consumeCommaIncludingWhitespace(args)); if (!args.atEnd()) return nullptr; range = rangeCopy; return imageSet; } static bool isGeneratedImage(CSSValueID id) { return id == CSSValueLinearGradient || id == CSSValueRadialGradient || id == CSSValueRepeatingLinearGradient || id == CSSValueRepeatingRadialGradient || id == CSSValueWebkitLinearGradient || id == CSSValueWebkitRadialGradient || id == CSSValueWebkitRepeatingLinearGradient || id == CSSValueWebkitRepeatingRadialGradient || id == CSSValueWebkitGradient || id == CSSValueWebkitCrossFade || id == CSSValueWebkitCanvas || id == CSSValueCrossFade || id == CSSValueWebkitNamedImage || id == CSSValueWebkitFilter || id == CSSValueFilter; } static bool isValidPrimitiveFilterFunction(CSSValueID filterFunction) { switch (filterFunction) { case CSSValueBlur: case CSSValueBrightness: case CSSValueContrast: case CSSValueDropShadow: case CSSValueGrayscale: case CSSValueHueRotate: case CSSValueInvert: case CSSValueOpacity: case CSSValueSaturate: case CSSValueSepia: return true; default: return false; } } RefPtr consumeFilterFunction(CSSParserTokenRange& range, const CSSParserContext& context) { CSSValueID filterType = range.peek().functionId(); if (!isValidPrimitiveFilterFunction(filterType)) return nullptr; CSSParserTokenRange args = consumeFunction(range); RefPtr filterValue = CSSFunctionValue::create(filterType); RefPtr parsedValue; if (filterType == CSSValueDropShadow) parsedValue = consumeSingleShadow(args, context.mode, false, false); else { if (args.atEnd()) return filterValue; if (filterType == CSSValueBrightness) { parsedValue = consumePercent(args, ValueRangeAll); if (!parsedValue) parsedValue = consumeNumber(args, ValueRangeAll); } else if (filterType == CSSValueHueRotate) parsedValue = consumeAngle(args, context.mode, UnitlessQuirk::Forbid); else if (filterType == CSSValueBlur) parsedValue = consumeLength(args, HTMLStandardMode, ValueRangeNonNegative); else { parsedValue = consumePercent(args, ValueRangeNonNegative); if (!parsedValue) parsedValue = consumeNumber(args, ValueRangeNonNegative); if (parsedValue && filterType != CSSValueSaturate && filterType != CSSValueContrast) { bool isPercentage = downcast(*parsedValue).isPercentage(); double maxAllowed = isPercentage ? 100.0 : 1.0; if (downcast(*parsedValue).doubleValue() > maxAllowed) parsedValue = CSSPrimitiveValue::create(maxAllowed, isPercentage ? CSSPrimitiveValue::UnitType::CSS_PERCENTAGE : CSSPrimitiveValue::UnitType::CSS_NUMBER); } } } if (!parsedValue || !args.atEnd()) return nullptr; filterValue->append(parsedValue.releaseNonNull()); return filterValue; } RefPtr consumeFilter(CSSParserTokenRange& range, const CSSParserContext& context) { if (range.peek().id() == CSSValueNone) return consumeIdent(range); auto list = CSSValueList::createSpaceSeparated(); do { RefPtr filterValue = consumeUrl(range); if (!filterValue) { filterValue = consumeFilterFunction(range, context); if (!filterValue) return nullptr; } list->append(filterValue.releaseNonNull()); } while (!range.atEnd()); return list.ptr(); } RefPtr consumeSingleShadow(CSSParserTokenRange& range, CSSParserMode cssParserMode, bool allowInset, bool allowSpread) { RefPtr style; RefPtr color; if (range.atEnd()) return nullptr; if (range.peek().id() == CSSValueInset) { if (!allowInset) return nullptr; style = consumeIdent(range); } color = consumeColor(range, cssParserMode); auto horizontalOffset = consumeLength(range, cssParserMode, ValueRangeAll); if (!horizontalOffset) return nullptr; auto verticalOffset = consumeLength(range, cssParserMode, ValueRangeAll); if (!verticalOffset) return nullptr; auto blurRadius = consumeLength(range, cssParserMode, ValueRangeAll); RefPtr spreadDistance; if (blurRadius) { // Blur radius must be non-negative. if (blurRadius->doubleValue() < 0) return nullptr; if (allowSpread) spreadDistance = consumeLength(range, cssParserMode, ValueRangeAll); } if (!range.atEnd()) { if (!color) color = consumeColor(range, cssParserMode); if (range.peek().id() == CSSValueInset) { if (!allowInset || style) return nullptr; style = consumeIdent(range); } } return CSSShadowValue::create(WTFMove(horizontalOffset), WTFMove(verticalOffset), WTFMove(blurRadius), WTFMove(spreadDistance), WTFMove(style), WTFMove(color)); } RefPtr consumeImage(CSSParserTokenRange& range, CSSParserContext context, ConsumeGeneratedImage generatedImage) { AtomicString uri = consumeUrlAsStringView(range).toAtomicString(); if (!uri.isNull()) return CSSImageValue::create(completeURL(context, uri)); if (range.peek().type() == FunctionToken) { CSSValueID id = range.peek().functionId(); if (id == CSSValueWebkitImageSet || id == CSSValueImageSet) return consumeImageSet(range, context); if (generatedImage == ConsumeGeneratedImage::Allow && isGeneratedImage(id)) return consumeGeneratedImage(range, context); } return nullptr; } } // namespace CSSPropertyParserHelpers } // namespace WebCore