blob: 49b1abb42138a4ef1369528973876cc8971d16e8 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "core/css/parser/CSSPropertyParserHelpers.h"
#include "core/css/CSSCalculationValue.h"
#include "core/css/CSSStringValue.h"
#include "core/css/CSSValuePair.h"
// TODO(timloh): Remove this dependency
#include "core/css/parser/CSSPropertyParser.h"
namespace blink {
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;
}
// TODO(rwlbuis): consider pulling in the parsing logic from CSSCalculationValue.cpp.
class CalcParser {
STACK_ALLOCATED();
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(); }
CSSPrimitiveValue* consumeValue()
{
if (!m_calcValue)
return nullptr;
m_sourceRange = m_range;
return CSSPrimitiveValue::create(m_calcValue.release());
}
CSSPrimitiveValue* consumeNumber()
{
if (!m_calcValue)
return nullptr;
m_sourceRange = m_range;
CSSPrimitiveValue::UnitType unitType = m_calcValue->isInt() ? CSSPrimitiveValue::UnitType::Integer : CSSPrimitiveValue::UnitType::Number;
return cssValuePool().createValue(m_calcValue->doubleValue(), unitType);
}
bool consumeNumberRaw(double& result)
{
if (!m_calcValue || m_calcValue->category() != CalcNumber)
return false;
m_sourceRange = m_range;
result = m_calcValue->doubleValue();
return true;
}
private:
CSSParserTokenRange& m_sourceRange;
CSSParserTokenRange m_range;
Member<CSSCalcValue> m_calcValue;
};
CSSPrimitiveValue* 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().createValue(range.consumeIncludingWhitespace().numericValue(), CSSPrimitiveValue::UnitType::Integer);
}
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;
}
CSSPrimitiveValue* consumePositiveInteger(CSSParserTokenRange& range)
{
return consumeInteger(range, 1);
}
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);
}
// TODO(timloh): Work out if this can just call consumeNumberRaw
CSSPrimitiveValue* consumeNumber(CSSParserTokenRange& range, ValueRange valueRange)
{
const CSSParserToken& token = range.peek();
if (token.type() == NumberToken) {
if (valueRange == ValueRangeNonNegative && token.numericValue() < 0)
return nullptr;
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType());
}
CalcParser calcParser(range, ValueRangeAll);
if (const CSSCalcValue* calculation = calcParser.value()) {
// TODO(rwlbuis) 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 shouldAcceptUnitlessLength(double value, CSSParserMode cssParserMode, UnitlessQuirk unitless)
{
// TODO(timloh): Presentational HTML attributes shouldn't use the CSS parser for lengths
return value == 0
|| isUnitLessLengthParsingEnabledForMode(cssParserMode)
|| (cssParserMode == HTMLQuirksMode && unitless == UnitlessQuirk::Allow);
}
CSSPrimitiveValue* consumeLength(CSSParserTokenRange& range, CSSParserMode cssParserMode, ValueRange valueRange, UnitlessQuirk unitless)
{
const CSSParserToken& token = range.peek();
if (token.type() == DimensionToken) {
switch (token.unitType()) {
case CSSPrimitiveValue::UnitType::QuirkyEms:
if (cssParserMode != UASheetMode)
return nullptr;
/* fallthrough intentional */
case CSSPrimitiveValue::UnitType::Ems:
case CSSPrimitiveValue::UnitType::Rems:
case CSSPrimitiveValue::UnitType::Chs:
case CSSPrimitiveValue::UnitType::Exs:
case CSSPrimitiveValue::UnitType::Pixels:
case CSSPrimitiveValue::UnitType::Centimeters:
case CSSPrimitiveValue::UnitType::Millimeters:
case CSSPrimitiveValue::UnitType::Inches:
case CSSPrimitiveValue::UnitType::Points:
case CSSPrimitiveValue::UnitType::Picas:
case CSSPrimitiveValue::UnitType::UserUnits:
case CSSPrimitiveValue::UnitType::ViewportWidth:
case CSSPrimitiveValue::UnitType::ViewportHeight:
case CSSPrimitiveValue::UnitType::ViewportMin:
case CSSPrimitiveValue::UnitType::ViewportMax:
break;
default:
return nullptr;
}
if (valueRange == ValueRangeNonNegative && token.numericValue() < 0)
return nullptr;
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType());
}
if (token.type() == NumberToken) {
if (!shouldAcceptUnitlessLength(token.numericValue(), cssParserMode, unitless)
|| (valueRange == ValueRangeNonNegative && token.numericValue() < 0))
return nullptr;
CSSPrimitiveValue::UnitType unitType = CSSPrimitiveValue::UnitType::Pixels;
if (cssParserMode == SVGAttributeMode)
unitType = CSSPrimitiveValue::UnitType::UserUnits;
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), unitType);
}
if (cssParserMode == SVGAttributeMode)
return nullptr;
CalcParser calcParser(range, valueRange);
if (calcParser.value() && calcParser.value()->category() == CalcLength)
return calcParser.consumeValue();
return nullptr;
}
CSSPrimitiveValue* consumePercent(CSSParserTokenRange& range, ValueRange valueRange)
{
const CSSParserToken& token = range.peek();
if (token.type() == PercentageToken) {
if (valueRange == ValueRangeNonNegative && token.numericValue() < 0)
return nullptr;
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), CSSPrimitiveValue::UnitType::Percentage);
}
CalcParser calcParser(range, valueRange);
if (const CSSCalcValue* calculation = calcParser.value()) {
if (calculation->category() == CalcPercent)
return calcParser.consumeValue();
}
return nullptr;
}
CSSPrimitiveValue* 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 (calculation->category() == CalcLength || calculation->category() == CalcPercent || calculation->category() == CalcPercentLength)
return calcParser.consumeValue();
}
return nullptr;
}
CSSPrimitiveValue* consumeAngle(CSSParserTokenRange& range)
{
const CSSParserToken& token = range.peek();
if (token.type() == DimensionToken) {
switch (token.unitType()) {
case CSSPrimitiveValue::UnitType::Degrees:
case CSSPrimitiveValue::UnitType::Radians:
case CSSPrimitiveValue::UnitType::Gradians:
case CSSPrimitiveValue::UnitType::Turns:
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType());
default:
return nullptr;
}
}
if (token.type() == NumberToken && token.numericValue() == 0) {
range.consumeIncludingWhitespace();
return cssValuePool().createValue(0, CSSPrimitiveValue::UnitType::Degrees);
}
CalcParser calcParser(range, ValueRangeAll);
if (const CSSCalcValue* calculation = calcParser.value()) {
if (calculation->category() == CalcAngle)
return calcParser.consumeValue();
}
return nullptr;
}
CSSPrimitiveValue* consumeTime(CSSParserTokenRange& range, ValueRange valueRange)
{
const CSSParserToken& token = range.peek();
if (token.type() == DimensionToken) {
if (valueRange == ValueRangeNonNegative && token.numericValue() < 0)
return nullptr;
CSSPrimitiveValue::UnitType unit = token.unitType();
if (unit == CSSPrimitiveValue::UnitType::Milliseconds || unit == CSSPrimitiveValue::UnitType::Seconds)
return cssValuePool().createValue(range.consumeIncludingWhitespace().numericValue(), token.unitType());
return nullptr;
}
CalcParser calcParser(range, valueRange);
if (const CSSCalcValue* calculation = calcParser.value()) {
if (calculation->category() == CalcTime)
return calcParser.consumeValue();
}
return nullptr;
}
CSSPrimitiveValue* consumeIdent(CSSParserTokenRange& range)
{
if (range.peek().type() != IdentToken)
return nullptr;
return cssValuePool().createIdentifierValue(range.consumeIncludingWhitespace().id());
}
CSSPrimitiveValue* consumeIdentRange(CSSParserTokenRange& range, CSSValueID lower, CSSValueID upper)
{
if (range.peek().id() < lower || range.peek().id() > upper)
return nullptr;
return consumeIdent(range);
}
CSSCustomIdentValue* consumeCustomIdent(CSSParserTokenRange& range)
{
if (range.peek().type() != IdentToken || isCSSWideKeyword(range.peek().id()))
return nullptr;
return CSSCustomIdentValue::create(range.consumeIncludingWhitespace().value());
}
CSSStringValue* consumeString(CSSParserTokenRange& range)
{
if (range.peek().type() != StringToken)
return nullptr;
return CSSStringValue::create(range.consumeIncludingWhitespace().value());
}
String consumeUrl(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 String();
ASSERT(next.type() == StringToken);
range = urlRange;
range.consumeWhitespace();
return next.value();
}
return String();
}
static int clampRGBComponent(const CSSPrimitiveValue& value)
{
double result = value.getDoubleValue();
// TODO(timloh): Multiply by 2.55 and round instead of floor.
if (value.isPercentage())
result *= 2.56;
return clampTo<int>(result, 0, 255);
}
static bool parseRGBParameters(CSSParserTokenRange& range, RGBA32& result, bool parseAlpha)
{
ASSERT(range.peek().functionId() == CSSValueRgb || range.peek().functionId() == CSSValueRgba);
CSSParserTokenRange args = consumeFunction(range);
CSSPrimitiveValue* colorParameter = consumeInteger(args);
if (!colorParameter)
colorParameter = consumePercent(args, ValueRangeAll);
if (!colorParameter)
return false;
const bool isPercent = colorParameter->isPercentage();
int colorArray[3];
colorArray[0] = clampRGBComponent(*colorParameter);
for (int i = 1; i < 3; i++) {
if (!consumeCommaIncludingWhitespace(args))
return false;
colorParameter = isPercent ? consumePercent(args, ValueRangeAll) : consumeInteger(args);
if (!colorParameter)
return false;
colorArray[i] = clampRGBComponent(*colorParameter);
}
if (parseAlpha) {
if (!consumeCommaIncludingWhitespace(args))
return false;
double alpha;
if (!consumeNumberRaw(args, alpha))
return false;
// 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<int>(clampTo<double>(alpha, 0.0, 1.0) * nextafter(256.0, 0.0));
result = makeRGBA(colorArray[0], colorArray[1], colorArray[2], alphaComponent);
} else {
result = makeRGB(colorArray[0], colorArray[1], colorArray[2]);
}
return args.atEnd();
}
static bool parseHSLParameters(CSSParserTokenRange& range, RGBA32& result, bool parseAlpha)
{
ASSERT(range.peek().functionId() == CSSValueHsl || range.peek().functionId() == CSSValueHsla);
CSSParserTokenRange args = consumeFunction(range);
CSSPrimitiveValue* hslValue = consumeNumber(args, ValueRangeAll);
if (!hslValue)
return false;
double colorArray[3];
colorArray[0] = (((hslValue->getIntValue() % 360) + 360) % 360) / 360.0;
for (int i = 1; i < 3; i++) {
if (!consumeCommaIncludingWhitespace(args))
return false;
hslValue = consumePercent(args, ValueRangeAll);
if (!hslValue)
return false;
double doubleValue = hslValue->getDoubleValue();
colorArray[i] = clampTo<double>(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 false;
if (!consumeNumberRaw(args, alpha))
return false;
alpha = clampTo<double>(alpha, 0.0, 1.0);
}
result = makeRGBAFromHSLA(colorArray[0], colorArray[1], colorArray[2], alpha);
return args.atEnd();
}
static bool parseHexColor(CSSParserTokenRange& range, RGBA32& result, bool acceptQuirkyColors)
{
const CSSParserToken& token = range.peek();
String color;
if (acceptQuirkyColors) {
if (token.type() == NumberToken && token.numericValueType() == IntegerValueType
&& token.numericValue() >= 0. && token.numericValue() < 1000000.) { // e.g. 112233
color = String::format("%06d", static_cast<int>(token.numericValue()));
} else if (token.type() == DimensionToken) { // e.g. 0001FF
// TODO(timloh): This should check the numericValueType flag
color = String::number(static_cast<int>(token.numericValue())) + String(token.value());
if (color.length() > 6)
return false;
while (color.length() < 6)
color = "0" + color;
} else if (token.type() == IdentToken) { // e.g. FF0000
unsigned length = token.value().length();
if (length != 3 && length != 6)
return false;
color = token.value();
}
}
if (token.type() == HashToken)
color = token.value();
if (!Color::parseHexColor(color, result))
return false;
range.consumeIncludingWhitespace();
return true;
}
static bool parseColorFunction(CSSParserTokenRange& range, RGBA32& result)
{
CSSValueID functionId = range.peek().functionId();
if (functionId < CSSValueRgb || functionId > CSSValueHsla)
return false;
CSSParserTokenRange colorRange = range;
if ((functionId <= CSSValueRgba && !parseRGBParameters(colorRange, result, functionId == CSSValueRgba))
|| (functionId >= CSSValueHsl && !parseHSLParameters(colorRange, result, functionId == CSSValueHsla)))
return false;
range = colorRange;
return true;
}
CSSValue* consumeColor(CSSParserTokenRange& range, CSSParserMode cssParserMode, bool acceptQuirkyColors)
{
CSSValueID id = range.peek().id();
if (CSSPropertyParser::isColorKeyword(id)) {
if (!isValueAllowedInMode(id, cssParserMode))
return nullptr;
return consumeIdent(range);
}
RGBA32 color = Color::transparent;
if (!parseHexColor(range, color, acceptQuirkyColors) && !parseColorFunction(range, color))
return nullptr;
return cssValuePool().createColorValue(color);
}
static CSSPrimitiveValue* consumePositionComponent(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless)
{
if (range.peek().type() == IdentToken)
return consumeIdent<CSSValueLeft, CSSValueTop, CSSValueBottom, CSSValueRight, CSSValueCenter>(range);
return consumeLengthOrPercent(range, cssParserMode, ValueRangeAll, unitless);
}
static bool isHorizontalPositionKeywordOnly(const CSSPrimitiveValue& value)
{
return value.isValueID() && (value.getValueID() == CSSValueLeft || value.getValueID() == CSSValueRight);
}
static bool isVerticalPositionKeywordOnly(const CSSPrimitiveValue& value)
{
return value.isValueID() && (value.getValueID() == CSSValueTop || value.getValueID() == CSSValueBottom);
}
static void positionFromOneValue(CSSPrimitiveValue* value, CSSValue*& resultX, CSSValue*& resultY)
{
bool valueAppliesToYAxisOnly = isVerticalPositionKeywordOnly(*value);
resultX = value;
resultY = cssValuePool().createIdentifierValue(CSSValueCenter);
if (valueAppliesToYAxisOnly)
std::swap(resultX, resultY);
}
static bool positionFromTwoValues(CSSPrimitiveValue* value1, CSSPrimitiveValue* value2,
CSSValue*& resultX, CSSValue*& 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;
}
static bool positionFromThreeOrFourValues(CSSPrimitiveValue** values, CSSValue*& resultX, CSSValue*& resultY)
{
CSSPrimitiveValue* center = nullptr;
for (int i = 0; values[i]; i++) {
CSSPrimitiveValue* currentValue = values[i];
if (!currentValue->isValueID())
return false;
CSSValueID id = currentValue->getValueID();
if (id == CSSValueCenter) {
if (center)
return false;
center = currentValue;
continue;
}
CSSValue* result = nullptr;
if (values[i + 1] && !values[i + 1]->isValueID()) {
result = CSSValuePair::create(currentValue, values[++i], CSSValuePair::KeepIdenticalValues);
} 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;
}
// TODO(timloh): 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, CSSValue*& resultX, CSSValue*& resultY)
{
CSSPrimitiveValue* value1 = consumePositionComponent(range, cssParserMode, unitless);
if (!value1)
return false;
CSSPrimitiveValue* value2 = consumePositionComponent(range, cssParserMode, unitless);
if (!value2) {
positionFromOneValue(value1, resultX, resultY);
return true;
}
CSSPrimitiveValue* value3 = consumePositionComponent(range, cssParserMode, unitless);
if (!value3)
return positionFromTwoValues(value1, value2, resultX, resultY);
CSSPrimitiveValue* value4 = consumePositionComponent(range, cssParserMode, unitless);
CSSPrimitiveValue* values[5];
values[0] = value1;
values[1] = value2;
values[2] = value3;
values[3] = value4;
values[4] = nullptr;
return positionFromThreeOrFourValues(values, resultX, resultY);
}
CSSValuePair* consumePosition(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless)
{
CSSValue* resultX = nullptr;
CSSValue* resultY = nullptr;
if (consumePosition(range, cssParserMode, unitless, resultX, resultY))
return CSSValuePair::create(resultX, resultY, CSSValuePair::KeepIdenticalValues);
return nullptr;
}
bool consumeOneOrTwoValuedPosition(CSSParserTokenRange& range, CSSParserMode cssParserMode, UnitlessQuirk unitless, CSSValue*& resultX, CSSValue*& resultY)
{
CSSPrimitiveValue* value1 = consumePositionComponent(range, cssParserMode, unitless);
if (!value1)
return false;
CSSPrimitiveValue* value2 = consumePositionComponent(range, cssParserMode, unitless);
if (!value2) {
positionFromOneValue(value1, resultX, resultY);
return true;
}
return positionFromTwoValues(value1, value2, resultX, resultY);
}
} // namespace CSSPropertyParserHelpers
} // namespace blink