/* * Copyright (c) 2018-2024, Andreas Kling * Copyright (c) 2020-2021, the SerenityOS developers. * Copyright (c) 2021-2025, Sam Atkins * Copyright (c) 2021, Tobias Christiansen * Copyright (c) 2022, MacDue * Copyright (c) 2024, Shannon Booth * Copyright (c) 2024, Tommy van der Vorst * Copyright (c) 2024, Matthew Olsson * Copyright (c) 2024, Glenn Skrzypczak * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::CSS::Parser { Optional Parser::parse_dimension(ComponentValue const& component_value) { if (component_value.is(Token::Type::Dimension)) { auto numeric_value = component_value.token().dimension_value(); auto unit_string = component_value.token().dimension_unit(); if (auto length_type = Length::unit_from_name(unit_string); length_type.has_value()) return Length { numeric_value, length_type.release_value() }; if (auto angle_type = Angle::unit_from_name(unit_string); angle_type.has_value()) return Angle { numeric_value, angle_type.release_value() }; if (auto flex_type = Flex::unit_from_name(unit_string); flex_type.has_value()) return Flex { numeric_value, flex_type.release_value() }; if (auto frequency_type = Frequency::unit_from_name(unit_string); frequency_type.has_value()) return Frequency { numeric_value, frequency_type.release_value() }; if (auto resolution_type = Resolution::unit_from_name(unit_string); resolution_type.has_value()) return Resolution { numeric_value, resolution_type.release_value() }; if (auto time_type = Time::unit_from_name(unit_string); time_type.has_value()) return Time { numeric_value, time_type.release_value() }; } if (component_value.is(Token::Type::Percentage)) return Percentage { component_value.token().percentage() }; if (component_value.is(Token::Type::Number)) { auto numeric_value = component_value.token().number_value(); if (numeric_value == 0) return Length::make_px(0); if (context_allows_quirky_length()) return Length::make_px(CSSPixels::nearest_value_for(numeric_value)); } return {}; } Optional Parser::parse_angle(TokenStream& tokens) { if (auto value = parse_angle_value(tokens)) { if (value->is_angle()) return value->as_angle().angle(); if (value->is_calculated()) return AngleOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_angle_percentage(TokenStream& tokens) { if (auto value = parse_angle_percentage_value(tokens)) { if (value->is_angle()) return value->as_angle().angle(); if (value->is_percentage()) return value->as_percentage().percentage(); if (value->is_calculated()) return AnglePercentage { value->as_calculated() }; } return {}; } Optional Parser::parse_flex(TokenStream& tokens) { if (auto value = parse_flex_value(tokens)) { if (value->is_flex()) return value->as_flex().flex(); if (value->is_calculated()) return FlexOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_frequency(TokenStream& tokens) { if (auto value = parse_frequency_value(tokens)) { if (value->is_frequency()) return value->as_frequency().frequency(); if (value->is_calculated()) return FrequencyOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_frequency_percentage(TokenStream& tokens) { if (auto value = parse_frequency_percentage_value(tokens)) { if (value->is_frequency()) return value->as_frequency().frequency(); if (value->is_percentage()) return value->as_percentage().percentage(); if (value->is_calculated()) return FrequencyPercentage { value->as_calculated() }; } return {}; } Optional Parser::parse_integer(TokenStream& tokens) { if (auto value = parse_integer_value(tokens)) { if (value->is_integer()) return value->as_integer().integer(); if (value->is_calculated()) return IntegerOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_length(TokenStream& tokens) { if (auto value = parse_length_value(tokens)) { if (value->is_length()) return value->as_length().length(); if (value->is_calculated()) return LengthOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_length_percentage(TokenStream& tokens) { if (auto value = parse_length_percentage_value(tokens)) { if (value->is_length()) return value->as_length().length(); if (value->is_percentage()) return value->as_percentage().percentage(); if (value->is_calculated()) return LengthPercentage { value->as_calculated() }; } return {}; } Optional Parser::parse_number(TokenStream& tokens) { if (auto value = parse_number_value(tokens)) { if (value->is_number()) return value->as_number().number(); if (value->is_calculated()) return NumberOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_number_percentage(TokenStream& tokens) { if (auto value = parse_number_percentage_value(tokens)) { if (value->is_number()) return Number { Number::Type::Number, value->as_number().number() }; if (value->is_percentage()) return value->as_percentage().percentage(); if (value->is_calculated()) return NumberPercentage { value->as_calculated() }; } return {}; } Optional Parser::parse_resolution(TokenStream& tokens) { if (auto value = parse_resolution_value(tokens)) { if (value->is_resolution()) return value->as_resolution().resolution(); if (value->is_calculated()) return ResolutionOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_time(TokenStream& tokens) { if (auto value = parse_time_value(tokens)) { if (value->is_time()) return value->as_time().time(); if (value->is_calculated()) return TimeOrCalculated { value->as_calculated() }; } return {}; } Optional Parser::parse_time_percentage(TokenStream& tokens) { if (auto value = parse_time_percentage_value(tokens)) { if (value->is_time()) return value->as_time().time(); if (value->is_percentage()) return value->as_percentage().percentage(); if (value->is_calculated()) return TimePercentage { value->as_calculated() }; } return {}; } Optional Parser::parse_ratio(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); // FIXME: It seems like `calc(...) / calc(...)` is a valid , but this case is neither mentioned in a spec, // nor tested in WPT, as far as I can tell. // Still, we should probably support it. That means not assuming we can resolve the calculation immediately. auto read_number_value = [this](ComponentValue const& component_value) -> Optional { if (component_value.is(Token::Type::Number)) return component_value.token().number_value(); if (component_value.is_function()) { auto maybe_calc = parse_calculated_value(component_value); if (!maybe_calc) return {}; if (maybe_calc->is_number()) return maybe_calc->as_number().value(); if (!maybe_calc->is_calculated() || !maybe_calc->as_calculated().resolves_to_number()) return {}; if (auto resolved_number = maybe_calc->as_calculated().resolve_number({}); resolved_number.has_value() && resolved_number.value() >= 0) { return resolved_number.value(); } } return {}; }; // ` = [ / ]?` auto maybe_numerator = read_number_value(tokens.consume_a_token()); if (!maybe_numerator.has_value() || maybe_numerator.value() < 0) return {}; auto numerator = maybe_numerator.value(); { auto two_value_transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto const& solidus = tokens.consume_a_token(); tokens.discard_whitespace(); auto maybe_denominator = read_number_value(tokens.consume_a_token()); if (solidus.is_delim('/') && maybe_denominator.has_value() && maybe_denominator.value() >= 0) { auto denominator = maybe_denominator.value(); // Two-value ratio two_value_transaction.commit(); transaction.commit(); return Ratio { numerator, denominator }; } } // Single-value ratio transaction.commit(); return Ratio { numerator }; } // https://www.w3.org/TR/css-syntax-3/#urange-syntax Optional Parser::parse_unicode_range(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); // = // u '+' '?'* | // u '?'* | // u '?'* | // u | // u | // u '+' '?'+ // (All with no whitespace in between tokens.) // NOTE: Parsing this is different from usual. We take these steps: // 1. Match the grammar above against the tokens, concatenating them into a string using their original representation. // 2. Then, parse that string according to the spec algorithm. // Step 2 is performed by calling the other parse_unicode_range() overload. auto is_ending_token = [](ComponentValue const& component_value) { return component_value.is(Token::Type::EndOfFile) || component_value.is(Token::Type::Comma) || component_value.is(Token::Type::Semicolon) || component_value.is(Token::Type::Whitespace); }; auto create_unicode_range = [&](StringView text, auto& local_transaction) -> Optional { auto maybe_unicode_range = parse_unicode_range(text); if (maybe_unicode_range.has_value()) { local_transaction.commit(); transaction.commit(); } return maybe_unicode_range; }; // All options start with 'u'/'U'. auto const& u = tokens.consume_a_token(); if (!u.is_ident("u"sv)) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: does not start with 'u'"); return {}; } auto const& second_token = tokens.consume_a_token(); // u '+' '?'* | // u '+' '?'+ if (second_token.is_delim('+')) { auto local_transaction = tokens.begin_transaction(); StringBuilder string_builder; string_builder.append(second_token.token().original_source_text()); auto const& third_token = tokens.consume_a_token(); if (third_token.is(Token::Type::Ident) || third_token.is_delim('?')) { string_builder.append(third_token.token().original_source_text()); while (tokens.next_token().is_delim('?')) string_builder.append(tokens.consume_a_token().token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); } } // u '?'* if (second_token.is(Token::Type::Dimension)) { auto local_transaction = tokens.begin_transaction(); StringBuilder string_builder; string_builder.append(second_token.token().original_source_text()); while (tokens.next_token().is_delim('?')) string_builder.append(tokens.consume_a_token().token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); } // u '?'* | // u | // u if (second_token.is(Token::Type::Number)) { auto local_transaction = tokens.begin_transaction(); StringBuilder string_builder; string_builder.append(second_token.token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); auto const& third_token = tokens.consume_a_token(); if (third_token.is_delim('?')) { string_builder.append(third_token.token().original_source_text()); while (tokens.next_token().is_delim('?')) string_builder.append(tokens.consume_a_token().token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); } else if (third_token.is(Token::Type::Dimension)) { string_builder.append(third_token.token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); } else if (third_token.is(Token::Type::Number)) { string_builder.append(third_token.token().original_source_text()); if (is_ending_token(tokens.next_token())) return create_unicode_range(string_builder.string_view(), local_transaction); } } if constexpr (CSS_PARSER_DEBUG) { dbgln("CSSParser: Tokens did not match grammar."); tokens.dump_all_tokens(); } return {}; } Optional Parser::parse_unicode_range(StringView text) { auto make_valid_unicode_range = [&](u32 start_value, u32 end_value) -> Optional { // https://www.w3.org/TR/css-syntax-3/#maximum-allowed-code-point constexpr u32 maximum_allowed_code_point = 0x10FFFF; // To determine what codepoints the represents: // 1. If end value is greater than the maximum allowed code point, // the is invalid and a syntax error. if (end_value > maximum_allowed_code_point) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Invalid : end_value ({}) > maximum ({})", end_value, maximum_allowed_code_point); return {}; } // 2. If start value is greater than end value, the is invalid and a syntax error. if (start_value > end_value) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Invalid : start_value ({}) > end_value ({})", start_value, end_value); return {}; } // 3. Otherwise, the represents a contiguous range of codepoints from start value to end value, inclusive. return Gfx::UnicodeRange { start_value, end_value }; }; // 1. Skipping the first u token, concatenate the representations of all the tokens in the production together. // Let this be text. // NOTE: The concatenation is already done by the caller. GenericLexer lexer { text }; // 2. If the first character of text is U+002B PLUS SIGN, consume it. // Otherwise, this is an invalid , and this algorithm must exit. if (lexer.next_is('+')) { lexer.consume(); } else { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Second character of was not '+'; got: '{}'", lexer.consume()); return {}; } // 3. Consume as many hex digits from text as possible. // then consume as many U+003F QUESTION MARK (?) code points as possible. auto start_position = lexer.tell(); auto hex_digits = lexer.consume_while(is_ascii_hex_digit); auto question_marks = lexer.consume_while([](auto it) { return it == '?'; }); // If zero code points were consumed, or more than six code points were consumed, // this is an invalid , and this algorithm must exit. size_t consumed_code_points = hex_digits.length() + question_marks.length(); if (consumed_code_points == 0 || consumed_code_points > 6) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: start value had {} digits/?s, expected between 1 and 6.", consumed_code_points); return {}; } StringView start_value_code_points = text.substring_view(start_position, consumed_code_points); // If any U+003F QUESTION MARK (?) code points were consumed, then: if (question_marks.length() > 0) { // 1. If there are any code points left in text, this is an invalid , // and this algorithm must exit. if (lexer.tell_remaining() != 0) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: invalid; had {} code points left over.", lexer.tell_remaining()); return {}; } // 2. Interpret the consumed code points as a hexadecimal number, // with the U+003F QUESTION MARK (?) code points replaced by U+0030 DIGIT ZERO (0) code points. // This is the start value. auto start_value_string = start_value_code_points.replace("?"sv, "0"sv, ReplaceMode::All); auto maybe_start_value = AK::StringUtils::convert_to_uint_from_hex(start_value_string); if (!maybe_start_value.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: ?-converted start value did not parse as hex number."); return {}; } u32 start_value = maybe_start_value.release_value(); // 3. Interpret the consumed code points as a hexadecimal number again, // with the U+003F QUESTION MARK (?) code points replaced by U+0046 LATIN CAPITAL LETTER F (F) code points. // This is the end value. auto end_value_string = start_value_code_points.replace("?"sv, "F"sv, ReplaceMode::All); auto maybe_end_value = AK::StringUtils::convert_to_uint_from_hex(end_value_string); if (!maybe_end_value.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: ?-converted end value did not parse as hex number."); return {}; } u32 end_value = maybe_end_value.release_value(); // 4. Exit this algorithm. return make_valid_unicode_range(start_value, end_value); } // Otherwise, interpret the consumed code points as a hexadecimal number. This is the start value. auto maybe_start_value = AK::StringUtils::convert_to_uint_from_hex(start_value_code_points); if (!maybe_start_value.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: start value did not parse as hex number."); return {}; } u32 start_value = maybe_start_value.release_value(); // 4. If there are no code points left in text, The end value is the same as the start value. // Exit this algorithm. if (lexer.tell_remaining() == 0) return make_valid_unicode_range(start_value, start_value); // 5. If the next code point in text is U+002D HYPHEN-MINUS (-), consume it. if (lexer.next_is('-')) { lexer.consume(); } // Otherwise, this is an invalid , and this algorithm must exit. else { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: start and end values not separated by '-'."); return {}; } // 6. Consume as many hex digits as possible from text. auto end_hex_digits = lexer.consume_while(is_ascii_hex_digit); // If zero hex digits were consumed, or more than 6 hex digits were consumed, // this is an invalid , and this algorithm must exit. if (end_hex_digits.length() == 0 || end_hex_digits.length() > 6) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: end value had {} digits, expected between 1 and 6.", end_hex_digits.length()); return {}; } // If there are any code points left in text, this is an invalid , and this algorithm must exit. if (lexer.tell_remaining() != 0) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: invalid; had {} code points left over.", lexer.tell_remaining()); return {}; } // 7. Interpret the consumed code points as a hexadecimal number. This is the end value. auto maybe_end_value = AK::StringUtils::convert_to_uint_from_hex(end_hex_digits); if (!maybe_end_value.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: end value did not parse as hex number."); return {}; } u32 end_value = maybe_end_value.release_value(); return make_valid_unicode_range(start_value, end_value); } Vector Parser::parse_unicode_ranges(TokenStream& tokens) { Vector unicode_ranges; auto range_token_lists = parse_a_comma_separated_list_of_component_values(tokens); for (auto& range_tokens : range_token_lists) { TokenStream range_token_stream { range_tokens }; auto maybe_unicode_range = parse_unicode_range(range_token_stream); if (!maybe_unicode_range.has_value()) { dbgln_if(CSS_PARSER_DEBUG, "CSSParser: unicode-range format invalid; discarding."); return {}; } unicode_ranges.append(maybe_unicode_range.release_value()); } return unicode_ranges; } RefPtr Parser::parse_integer_value(TokenStream& tokens) { auto const& peek_token = tokens.next_token(); if (peek_token.is(Token::Type::Number) && peek_token.token().number().is_integer()) { tokens.discard_a_token(); // integer return IntegerStyleValue::create(peek_token.token().number().integer_value()); } if (auto calc = parse_calculated_value(peek_token); calc && (calc->is_integer() || (calc->is_calculated() && calc->as_calculated().resolves_to_number()))) { tokens.discard_a_token(); // calc return calc; } return nullptr; } RefPtr Parser::parse_number_value(TokenStream& tokens) { auto const& peek_token = tokens.next_token(); if (peek_token.is(Token::Type::Number)) { tokens.discard_a_token(); // number return NumberStyleValue::create(peek_token.token().number().value()); } if (auto calc = parse_calculated_value(peek_token); calc && (calc->is_number() || (calc->is_calculated() && calc->as_calculated().resolves_to_number()))) { tokens.discard_a_token(); // calc return calc; } return nullptr; } RefPtr Parser::parse_number_percentage_value(TokenStream& tokens) { // Parses [ | ] (which is equivalent to []) if (auto value = parse_number_value(tokens)) return value; if (auto value = parse_percentage_value(tokens)) return value; return nullptr; } RefPtr Parser::parse_number_percentage_none_value(TokenStream& tokens) { // Parses [ | | none] (which is equivalent to [ | none]) if (auto value = parse_number_value(tokens)) return value; if (auto value = parse_percentage_value(tokens)) return value; if (tokens.next_token().is_ident("none"sv)) { tokens.discard_a_token(); // keyword none return CSSKeywordValue::create(Keyword::None); } return nullptr; } RefPtr Parser::parse_percentage_value(TokenStream& tokens) { auto const& peek_token = tokens.next_token(); if (peek_token.is(Token::Type::Percentage)) { tokens.discard_a_token(); // percentage return PercentageStyleValue::create(Percentage(peek_token.token().percentage())); } if (auto calc = parse_calculated_value(peek_token); calc && (calc->is_percentage() || (calc->is_calculated() && calc->as_calculated().resolves_to_percentage()))) { tokens.discard_a_token(); // calc return calc; } return nullptr; } RefPtr Parser::parse_angle_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto angle_type = Angle::unit_from_name(dimension_token.dimension_unit()); angle_type.has_value()) { transaction.commit(); return AngleStyleValue::create(Angle { (dimension_token.dimension_value()), angle_type.release_value() }); } return nullptr; } // https://svgwg.org/svg2-draft/types.html#presentation-attribute-css-value // When parsing an SVG attribute, an angle is allowed without a unit. // FIXME: How should these numbers be interpreted? https://github.com/w3c/svgwg/issues/792 // For now: Convert to an angle in degrees. if (tokens.next_token().is(Token::Type::Number) && is_parsing_svg_presentation_attribute()) { auto numeric_value = tokens.consume_a_token().token().number_value(); return AngleStyleValue::create(Angle::make_degrees(numeric_value)); } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_angle() || (calc->is_calculated() && calc->as_calculated().resolves_to_angle()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_angle_percentage_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto angle_type = Angle::unit_from_name(dimension_token.dimension_unit()); angle_type.has_value()) { transaction.commit(); return AngleStyleValue::create(Angle { (dimension_token.dimension_value()), angle_type.release_value() }); } return nullptr; } if (tokens.next_token().is(Token::Type::Percentage)) return PercentageStyleValue::create(Percentage { tokens.consume_a_token().token().percentage() }); // https://svgwg.org/svg2-draft/types.html#presentation-attribute-css-value // When parsing an SVG attribute, an angle is allowed without a unit. // FIXME: How should these numbers be interpreted? https://github.com/w3c/svgwg/issues/792 // For now: Convert to an angle in degrees. if (tokens.next_token().is(Token::Type::Number) && is_parsing_svg_presentation_attribute()) { auto numeric_value = tokens.consume_a_token().token().number_value(); return AngleStyleValue::create(Angle::make_degrees(numeric_value)); } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_angle() || calc->is_percentage() || (calc->is_calculated() && calc->as_calculated().resolves_to_angle_percentage()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_flex_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto flex_type = Flex::unit_from_name(dimension_token.dimension_unit()); flex_type.has_value()) { transaction.commit(); return FlexStyleValue::create(Flex { (dimension_token.dimension_value()), flex_type.release_value() }); } return nullptr; } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_flex() || (calc->is_calculated() && calc->as_calculated().resolves_to_flex()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_frequency_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto frequency_type = Frequency::unit_from_name(dimension_token.dimension_unit()); frequency_type.has_value()) { transaction.commit(); return FrequencyStyleValue::create(Frequency { (dimension_token.dimension_value()), frequency_type.release_value() }); } return nullptr; } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_frequency() || (calc->is_calculated() && calc->as_calculated().resolves_to_frequency()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_frequency_percentage_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto frequency_type = Frequency::unit_from_name(dimension_token.dimension_unit()); frequency_type.has_value()) { transaction.commit(); return FrequencyStyleValue::create(Frequency { (dimension_token.dimension_value()), frequency_type.release_value() }); } return nullptr; } if (tokens.next_token().is(Token::Type::Percentage)) return PercentageStyleValue::create(Percentage { tokens.consume_a_token().token().percentage() }); auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_frequency() || calc->is_percentage() || (calc->is_calculated() && calc->as_calculated().resolves_to_frequency_percentage()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_length_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto length_type = Length::unit_from_name(dimension_token.dimension_unit()); length_type.has_value()) { transaction.commit(); return LengthStyleValue::create(Length { (dimension_token.dimension_value()), length_type.release_value() }); } return nullptr; } if (tokens.next_token().is(Token::Type::Number)) { auto transaction = tokens.begin_transaction(); auto numeric_value = tokens.consume_a_token().token().number_value(); if (numeric_value == 0) { transaction.commit(); return LengthStyleValue::create(Length::make_px(0)); } if (context_allows_quirky_length()) { transaction.commit(); return LengthStyleValue::create(Length::make_px(CSSPixels::nearest_value_for(numeric_value))); } // https://svgwg.org/svg2-draft/types.html#presentation-attribute-css-value // When parsing an SVG attribute, a length is allowed without a unit. // FIXME: How should these numbers be interpreted? https://github.com/w3c/svgwg/issues/792 // For now: Convert to a length in pixels. if (is_parsing_svg_presentation_attribute()) { transaction.commit(); return LengthStyleValue::create(Length::make_px(CSSPixels::nearest_value_for(numeric_value))); } } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_length() || (calc->is_calculated() && calc->as_calculated().resolves_to_length()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_length_percentage_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto length_type = Length::unit_from_name(dimension_token.dimension_unit()); length_type.has_value()) { transaction.commit(); return LengthStyleValue::create(Length { (dimension_token.dimension_value()), length_type.release_value() }); } return nullptr; } if (tokens.next_token().is(Token::Type::Percentage)) return PercentageStyleValue::create(Percentage { tokens.consume_a_token().token().percentage() }); if (tokens.next_token().is(Token::Type::Number)) { auto transaction = tokens.begin_transaction(); auto numeric_value = tokens.consume_a_token().token().number_value(); if (numeric_value == 0) { transaction.commit(); return LengthStyleValue::create(Length::make_px(0)); } if (context_allows_quirky_length()) { transaction.commit(); return LengthStyleValue::create(Length::make_px(CSSPixels::nearest_value_for(numeric_value))); } // https://svgwg.org/svg2-draft/types.html#presentation-attribute-css-value // When parsing an SVG attribute, a length is allowed without a unit. // FIXME: How should these numbers be interpreted? https://github.com/w3c/svgwg/issues/792 // For now: Convert to a length in pixels. if (is_parsing_svg_presentation_attribute()) { transaction.commit(); return LengthStyleValue::create(Length::make_px(CSSPixels::nearest_value_for(numeric_value))); } } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_length() || calc->is_percentage() || (calc->is_calculated() && calc->as_calculated().resolves_to_length_percentage()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_resolution_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto resolution_type = Resolution::unit_from_name(dimension_token.dimension_unit()); resolution_type.has_value()) { transaction.commit(); return ResolutionStyleValue::create(Resolution { (dimension_token.dimension_value()), resolution_type.release_value() }); } return nullptr; } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_resolution() || (calc->is_calculated() && calc->as_calculated().resolves_to_resolution()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_time_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto time_type = Time::unit_from_name(dimension_token.dimension_unit()); time_type.has_value()) { transaction.commit(); return TimeStyleValue::create(Time { (dimension_token.dimension_value()), time_type.release_value() }); } return nullptr; } auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_time() || (calc->is_calculated() && calc->as_calculated().resolves_to_time()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_time_percentage_value(TokenStream& tokens) { if (tokens.next_token().is(Token::Type::Dimension)) { auto transaction = tokens.begin_transaction(); auto& dimension_token = tokens.consume_a_token().token(); if (auto time_type = Time::unit_from_name(dimension_token.dimension_unit()); time_type.has_value()) { transaction.commit(); return TimeStyleValue::create(Time { (dimension_token.dimension_value()), time_type.release_value() }); } return nullptr; } if (tokens.next_token().is(Token::Type::Percentage)) return PercentageStyleValue::create(Percentage { tokens.consume_a_token().token().percentage() }); auto transaction = tokens.begin_transaction(); if (auto calc = parse_calculated_value(tokens.consume_a_token()); calc && (calc->is_time() || calc->is_percentage() || (calc->is_calculated() && calc->as_calculated().resolves_to_time_percentage()))) { transaction.commit(); return calc; } return nullptr; } RefPtr Parser::parse_keyword_value(TokenStream& tokens) { auto const& peek_token = tokens.next_token(); if (peek_token.is(Token::Type::Ident)) { auto keyword = keyword_from_string(peek_token.token().ident()); if (keyword.has_value()) { tokens.discard_a_token(); // ident return CSSKeywordValue::create(keyword.value()); } } return nullptr; } // https://www.w3.org/TR/CSS2/visufx.html#value-def-shape RefPtr Parser::parse_rect_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto const& function_token = tokens.consume_a_token(); if (!function_token.is_function("rect"sv)) return nullptr; auto context_guard = push_temporary_value_parsing_context(FunctionContext { "rect"sv }); Vector params; auto argument_tokens = TokenStream { function_token.function().value }; enum class CommaRequirement { Unknown, RequiresCommas, RequiresNoCommas }; enum class Side { Top = 0, Right = 1, Bottom = 2, Left = 3 }; auto comma_requirement = CommaRequirement::Unknown; // In CSS 2.1, the only valid value is: rect(, , , ) where // and specify offsets from the top border edge of the box, and , and // specify offsets from the left border edge of the box. for (size_t side = 0; side < 4; side++) { argument_tokens.discard_whitespace(); // , , , and may either have a value or 'auto'. // Negative lengths are permitted. if (argument_tokens.next_token().is_ident("auto"sv)) { (void)argument_tokens.consume_a_token(); // `auto` params.append(Length::make_auto()); } else { auto maybe_length = parse_length(argument_tokens); if (!maybe_length.has_value()) return nullptr; if (maybe_length.value().is_calculated()) { dbgln("FIXME: Support calculated lengths in rect(): {}", maybe_length.value().calculated()->to_string(CSS::CSSStyleValue::SerializationMode::Normal)); return nullptr; } params.append(maybe_length.value().value()); } argument_tokens.discard_whitespace(); // The last side, should be no more tokens following it. if (static_cast(side) == Side::Left) { if (argument_tokens.has_next_token()) return nullptr; break; } bool next_is_comma = argument_tokens.next_token().is(Token::Type::Comma); // Authors should separate offset values with commas. User agents must support separation // with commas, but may also support separation without commas (but not a combination), // because a previous revision of this specification was ambiguous in this respect. if (comma_requirement == CommaRequirement::Unknown) comma_requirement = next_is_comma ? CommaRequirement::RequiresCommas : CommaRequirement::RequiresNoCommas; if (comma_requirement == CommaRequirement::RequiresCommas) { if (next_is_comma) argument_tokens.discard_a_token(); else return nullptr; } else if (comma_requirement == CommaRequirement::RequiresNoCommas) { if (next_is_comma) return nullptr; } else { VERIFY_NOT_REACHED(); } } transaction.commit(); return RectStyleValue::create(EdgeRect { params[0], params[1], params[2], params[3] }); } // https://www.w3.org/TR/css-color-4/#typedef-hue RefPtr Parser::parse_hue_none_value(TokenStream& tokens) { // Parses [ | none] // = | if (auto angle = parse_angle_value(tokens)) return angle; if (auto number = parse_number_value(tokens)) return number; if (tokens.next_token().is_ident("none"sv)) { tokens.discard_a_token(); // keyword none return CSSKeywordValue::create(Keyword::None); } return nullptr; } // https://www.w3.org/TR/css-color-4/#typedef-color-alpha-value RefPtr Parser::parse_solidus_and_alpha_value(TokenStream& tokens) { // [ / [ | none] ]? // = | // Common to the modern-syntax color functions. auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); if (!tokens.consume_a_token().is_delim('/')) return {}; tokens.discard_whitespace(); auto alpha = parse_number_percentage_none_value(tokens); if (!alpha) return {}; tokens.discard_whitespace(); transaction.commit(); return alpha; } // https://www.w3.org/TR/css-color-4/#funcdef-rgb RefPtr Parser::parse_rgb_color_value(TokenStream& outer_tokens) { // rgb() = [ | ] // rgba() = [ | ] // = rgb( #{3} , ? ) | // rgb( #{3} , ? ) // = rgba( #{3} , ? ) | // rgba( #{3} , ? ) // = rgb( // [ | | none]{3} // [ / [ | none] ]? ) // = rgba( // [ | | none]{3} // [ / [ | none] ]? ) auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function("rgb"sv) && !function_token.is_function("rgba"sv)) return {}; auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.function().name }); RefPtr red; RefPtr green; RefPtr blue; RefPtr alpha; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); red = parse_number_percentage_none_value(inner_tokens); if (!red) return {}; inner_tokens.discard_whitespace(); bool legacy_syntax = inner_tokens.next_token().is(Token::Type::Comma); if (legacy_syntax) { // Legacy syntax // #{3} , ? // | #{3} , ? // So, r/g/b can be numbers or percentages, as long as they're all the same type. // We accepted the 'none' keyword when parsing the red value, but it's not allowed in the legacy syntax. if (red->is_keyword()) return {}; inner_tokens.discard_a_token(); // comma inner_tokens.discard_whitespace(); green = parse_number_percentage_value(inner_tokens); if (!green) return {}; inner_tokens.discard_whitespace(); if (!inner_tokens.consume_a_token().is(Token::Type::Comma)) return {}; inner_tokens.discard_whitespace(); blue = parse_number_percentage_value(inner_tokens); if (!blue) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { // Try and read comma and alpha if (!inner_tokens.consume_a_token().is(Token::Type::Comma)) return {}; inner_tokens.discard_whitespace(); alpha = parse_number_percentage_value(inner_tokens); if (!alpha) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) return {}; } // Verify we're all percentages or all numbers auto is_percentage = [](CSSStyleValue const& style_value) { return style_value.is_percentage() || (style_value.is_calculated() && style_value.as_calculated().resolves_to_percentage()); }; bool red_is_percentage = is_percentage(*red); bool green_is_percentage = is_percentage(*green); bool blue_is_percentage = is_percentage(*blue); if (red_is_percentage != green_is_percentage || red_is_percentage != blue_is_percentage) return {}; } else { // Modern syntax // [ | | none]{3} [ / [ | none] ]? green = parse_number_percentage_none_value(inner_tokens); if (!green) return {}; inner_tokens.discard_whitespace(); blue = parse_number_percentage_none_value(inner_tokens); if (!blue) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return {}; } } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return CSSRGB::create(red.release_nonnull(), green.release_nonnull(), blue.release_nonnull(), alpha.release_nonnull(), legacy_syntax ? ColorSyntax::Legacy : ColorSyntax::Modern); } // https://www.w3.org/TR/css-color-4/#funcdef-hsl RefPtr Parser::parse_hsl_color_value(TokenStream& outer_tokens) { // hsl() = [ | ] // hsla() = [ | ] // = hsl( // [ | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) // = hsla( // [ | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) // = hsl( , , , ? ) // = hsla( , , , ? ) auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function("hsl"sv) && !function_token.is_function("hsla"sv)) return {}; auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.function().name }); RefPtr h; RefPtr s; RefPtr l; RefPtr alpha; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); h = parse_hue_none_value(inner_tokens); if (!h) return {}; inner_tokens.discard_whitespace(); bool legacy_syntax = inner_tokens.next_token().is(Token::Type::Comma); if (legacy_syntax) { // Legacy syntax // , , , ? // We accepted the 'none' keyword when parsing the h value, but it's not allowed in the legacy syntax. if (h->is_keyword()) return {}; (void)inner_tokens.consume_a_token(); // comma inner_tokens.discard_whitespace(); s = parse_percentage_value(inner_tokens); if (!s) return {}; inner_tokens.discard_whitespace(); if (!inner_tokens.consume_a_token().is(Token::Type::Comma)) return {}; inner_tokens.discard_whitespace(); l = parse_percentage_value(inner_tokens); if (!l) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { // Try and read comma and alpha if (!inner_tokens.consume_a_token().is(Token::Type::Comma)) return {}; inner_tokens.discard_whitespace(); alpha = parse_number_percentage_value(inner_tokens); // The parser has consumed a comma, so the alpha value is now required if (!alpha) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) return {}; } } else { // Modern syntax // [ | none] // [ | | none] // [ | | none] // [ / [ | none] ]? s = parse_number_percentage_none_value(inner_tokens); if (!s) return {}; inner_tokens.discard_whitespace(); l = parse_number_percentage_none_value(inner_tokens); if (!l) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return {}; } } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return CSSHSL::create(h.release_nonnull(), s.release_nonnull(), l.release_nonnull(), alpha.release_nonnull(), legacy_syntax ? ColorSyntax::Legacy : ColorSyntax::Modern); } // https://www.w3.org/TR/css-color-4/#funcdef-hwb RefPtr Parser::parse_hwb_color_value(TokenStream& outer_tokens) { // hwb() = hwb( // [ | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function("hwb"sv)) return {}; auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.function().name }); RefPtr h; RefPtr w; RefPtr b; RefPtr alpha; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); h = parse_hue_none_value(inner_tokens); if (!h) return {}; inner_tokens.discard_whitespace(); w = parse_number_percentage_none_value(inner_tokens); if (!w) return {}; inner_tokens.discard_whitespace(); b = parse_number_percentage_none_value(inner_tokens); if (!b) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return {}; } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return CSSHWB::create(h.release_nonnull(), w.release_nonnull(), b.release_nonnull(), alpha.release_nonnull()); } Optional, 4>> Parser::parse_lab_like_color_value(TokenStream& outer_tokens, StringView function_name) { // This helper is designed to be compatible with lab and oklab and parses a function with a form like: // f() = f( [ | | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function(function_name)) return OptionalNone {}; RefPtr l; RefPtr a; RefPtr b; RefPtr alpha; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); l = parse_number_percentage_none_value(inner_tokens); if (!l) return OptionalNone {}; inner_tokens.discard_whitespace(); a = parse_number_percentage_none_value(inner_tokens); if (!a) return OptionalNone {}; inner_tokens.discard_whitespace(); b = parse_number_percentage_none_value(inner_tokens); if (!b) return OptionalNone {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return OptionalNone {}; } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return Array { move(l), move(a), move(b), move(alpha) }; } // https://www.w3.org/TR/css-color-4/#funcdef-lab RefPtr Parser::parse_lab_color_value(TokenStream& outer_tokens) { // lab() = lab( [ | | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) auto maybe_color_values = parse_lab_like_color_value(outer_tokens, "lab"sv); if (!maybe_color_values.has_value()) return {}; auto& color_values = *maybe_color_values; return CSSLabLike::create(color_values[0].release_nonnull(), color_values[1].release_nonnull(), color_values[2].release_nonnull(), color_values[3].release_nonnull()); } // https://www.w3.org/TR/css-color-4/#funcdef-oklab RefPtr Parser::parse_oklab_color_value(TokenStream& outer_tokens) { // oklab() = oklab( [ | | none] // [ | | none] // [ | | none] // [ / [ | none] ]? ) auto maybe_color_values = parse_lab_like_color_value(outer_tokens, "oklab"sv); if (!maybe_color_values.has_value()) return {}; auto& color_values = *maybe_color_values; return CSSLabLike::create(color_values[0].release_nonnull(), color_values[1].release_nonnull(), color_values[2].release_nonnull(), color_values[3].release_nonnull()); } Optional, 4>> Parser::parse_lch_like_color_value(TokenStream& outer_tokens, StringView function_name) { // This helper is designed to be compatible with lch and oklch and parses a function with a form like: // f() = f( [ | | none] // [ | | none] // [ | none] // [ / [ | none] ]? ) auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto const& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function(function_name)) return OptionalNone {}; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); auto l = parse_number_percentage_none_value(inner_tokens); if (!l) return OptionalNone {}; inner_tokens.discard_whitespace(); auto c = parse_number_percentage_none_value(inner_tokens); if (!c) return OptionalNone {}; inner_tokens.discard_whitespace(); auto h = parse_hue_none_value(inner_tokens); if (!h) return OptionalNone {}; inner_tokens.discard_whitespace(); RefPtr alpha; if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return OptionalNone {}; } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return Array { move(l), move(c), move(h), move(alpha) }; } // https://www.w3.org/TR/css-color-4/#funcdef-lch RefPtr Parser::parse_lch_color_value(TokenStream& outer_tokens) { // lch() = lch( [ | | none] // [ | | none] // [ | none] // [ / [ | none] ]? ) auto maybe_color_values = parse_lch_like_color_value(outer_tokens, "lch"sv); if (!maybe_color_values.has_value()) return {}; auto& color_values = *maybe_color_values; return CSSLCHLike::create(color_values[0].release_nonnull(), color_values[1].release_nonnull(), color_values[2].release_nonnull(), color_values[3].release_nonnull()); } // https://www.w3.org/TR/css-color-4/#funcdef-oklch RefPtr Parser::parse_oklch_color_value(TokenStream& outer_tokens) { // oklch() = oklch( [ | | none] // [ | | none] // [ | none] // [ / [ | none] ]? ) auto maybe_color_values = parse_lch_like_color_value(outer_tokens, "oklch"sv); if (!maybe_color_values.has_value()) return {}; auto& color_values = *maybe_color_values; return CSSLCHLike::create(color_values[0].release_nonnull(), color_values[1].release_nonnull(), color_values[2].release_nonnull(), color_values[3].release_nonnull()); } // https://www.w3.org/TR/css-color-4/#funcdef-color RefPtr Parser::parse_color_function(TokenStream& outer_tokens) { // color() = color( [ / [ | none ] ]? ) // = [ | ] // = [ | | none ]{3} // = srgb | srgb-linear | display-p3 | a98-rgb | prophoto-rgb | rec2020 // = [ | | none ]{3} // = xyz | xyz-d50 | xyz-d65 auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto const& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function("color"sv)) return {}; auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.function().name }); auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); auto const& maybe_color_space = inner_tokens.consume_a_token(); inner_tokens.discard_whitespace(); if (!any_of(CSSColor::s_supported_color_space, [&](auto supported) { return maybe_color_space.is_ident(supported); })) return {}; auto const& color_space = maybe_color_space.token().ident(); auto c1 = parse_number_percentage_none_value(inner_tokens); if (!c1) return {}; inner_tokens.discard_whitespace(); auto c2 = parse_number_percentage_none_value(inner_tokens); if (!c2) return {}; inner_tokens.discard_whitespace(); auto c3 = parse_number_percentage_none_value(inner_tokens); if (!c3) return {}; inner_tokens.discard_whitespace(); RefPtr alpha; if (inner_tokens.has_next_token()) { alpha = parse_solidus_and_alpha_value(inner_tokens); if (!alpha || inner_tokens.has_next_token()) return {}; } if (!alpha) alpha = NumberStyleValue::create(1); transaction.commit(); return CSSColor::create(color_space.to_ascii_lowercase(), c1.release_nonnull(), c2.release_nonnull(), c3.release_nonnull(), alpha.release_nonnull()); } // https://drafts.csswg.org/css-color-5/#funcdef-light-dark RefPtr Parser::parse_light_dark_color_value(TokenStream& outer_tokens) { auto transaction = outer_tokens.begin_transaction(); outer_tokens.discard_whitespace(); auto const& function_token = outer_tokens.consume_a_token(); if (!function_token.is_function("light-dark"sv)) return {}; auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); auto light = parse_color_value(inner_tokens); if (!light) return {}; inner_tokens.discard_whitespace(); if (!inner_tokens.consume_a_token().is(Token::Type::Comma)) return {}; inner_tokens.discard_whitespace(); auto dark = parse_color_value(inner_tokens); if (!dark) return {}; inner_tokens.discard_whitespace(); if (inner_tokens.has_next_token()) return {}; transaction.commit(); return CSSLightDark::create(light.release_nonnull(), dark.release_nonnull()); } // https://www.w3.org/TR/css-color-4/#color-syntax RefPtr Parser::parse_color_value(TokenStream& tokens) { // Keywords: | | currentColor { auto transaction = tokens.begin_transaction(); if (auto keyword = parse_keyword_value(tokens); keyword && keyword->has_color()) { transaction.commit(); return keyword; } } // Functions if (auto color = parse_color_function(tokens)) return color; if (auto rgb = parse_rgb_color_value(tokens)) return rgb; if (auto hsl = parse_hsl_color_value(tokens)) return hsl; if (auto hwb = parse_hwb_color_value(tokens)) return hwb; if (auto lab = parse_lab_color_value(tokens)) return lab; if (auto lch = parse_lch_color_value(tokens)) return lch; if (auto oklab = parse_oklab_color_value(tokens)) return oklab; if (auto oklch = parse_oklch_color_value(tokens)) return oklch; if (auto light_dark = parse_light_dark_color_value(tokens)) return light_dark; auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto const& component_value = tokens.consume_a_token(); if (component_value.is(Token::Type::Ident)) { auto ident = component_value.token().ident(); auto color = Color::from_string(ident); if (color.has_value()) { transaction.commit(); return CSSColorValue::create_from_color(color.release_value(), ColorSyntax::Legacy, ident); } // Otherwise, fall through to the hashless-hex-color case } if (component_value.is(Token::Type::Hash)) { auto color = Color::from_string(MUST(String::formatted("#{}", component_value.token().hash_value()))); if (color.has_value()) { transaction.commit(); return CSSColorValue::create_from_color(color.release_value(), ColorSyntax::Legacy); } return {}; } // https://drafts.csswg.org/css-color-4/#quirky-color if (in_quirks_mode()) { // "When CSS is being parsed in quirks mode, is a type of that is only valid in certain properties:" // (NOTE: List skipped for brevity; quirks data is assigned in Properties.json) // "It is not valid in properties that include or reference these properties, such as the background shorthand, // or inside functional notations such as color-mix()" bool quirky_color_allowed = false; if (!m_value_context.is_empty()) { quirky_color_allowed = m_value_context.first().visit( [](PropertyID const& property_id) { return property_has_quirk(property_id, Quirk::HashlessHexColor); }, [](FunctionContext const&) { return false; }); } for (auto i = 1u; i < m_value_context.size() && quirky_color_allowed; i++) { quirky_color_allowed = m_value_context[i].visit( [](PropertyID const& property_id) { return property_has_quirk(property_id, Quirk::UnitlessLength); }, [](FunctionContext const&) { return false; }); } if (quirky_color_allowed) { // NOTE: This algorithm is no longer in the spec, since the concept got moved and renamed. However, it works, // and so we might as well keep using it. // The value of a quirky color is obtained from the possible component values using the following algorithm, // aborting on the first step that returns a value: // 1. Let cv be the component value. auto const& cv = component_value; String serialization; // 2. If cv is a or a , follow these substeps: if (cv.is(Token::Type::Number) || cv.is(Token::Type::Dimension)) { // 1. If cv’s type flag is not "integer", return an error. // This means that values that happen to use scientific notation, e.g., 5e5e5e, will fail to parse. if (!cv.token().number().is_integer()) return {}; // 2. If cv’s value is less than zero, return an error. auto value = cv.is(Token::Type::Number) ? cv.token().to_integer() : cv.token().dimension_value_int(); if (value < 0) return {}; // 3. Let serialization be the serialization of cv’s value, as a base-ten integer using digits 0-9 (U+0030 to U+0039) in the shortest form possible. StringBuilder serialization_builder; serialization_builder.appendff("{}", value); // 4. If cv is a , append the unit to serialization. if (cv.is(Token::Type::Dimension)) serialization_builder.append(cv.token().dimension_unit()); // 5. If serialization consists of fewer than six characters, prepend zeros (U+0030) so that it becomes six characters. serialization = MUST(serialization_builder.to_string()); if (serialization_builder.length() < 6) { StringBuilder builder; for (size_t i = 0; i < (6 - serialization_builder.length()); i++) builder.append('0'); builder.append(serialization_builder.string_view()); serialization = MUST(builder.to_string()); } } // 3. Otherwise, cv is an ; let serialization be cv’s value. else { if (!cv.is(Token::Type::Ident)) return {}; serialization = cv.token().ident().to_string(); } // 4. If serialization does not consist of three or six characters, return an error. if (serialization.bytes().size() != 3 && serialization.bytes().size() != 6) return {}; // 5. If serialization contains any characters not in the range [0-9A-Fa-f] (U+0030 to U+0039, U+0041 to U+0046, U+0061 to U+0066), return an error. for (auto c : serialization.bytes_as_string_view()) { if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) return {}; } // 6. Return the concatenation of "#" (U+0023) and serialization. auto color = Color::from_string(MUST(String::formatted("#{}", serialization))); if (color.has_value()) { transaction.commit(); return CSSColorValue::create_from_color(color.release_value(), ColorSyntax::Legacy); } } } return {}; } // https://drafts.csswg.org/css-lists-3/#counter-functions RefPtr Parser::parse_counter_value(TokenStream& tokens) { auto parse_counter_name = [this](TokenStream& tokens) -> Optional { // https://drafts.csswg.org/css-lists-3/#typedef-counter-name // Counters are referred to in CSS syntax using the type, which represents // their name as a . A name cannot match the keyword none; // such an identifier is invalid as a . auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto counter_name = parse_custom_ident_value(tokens, { { "none"sv } }); if (!counter_name) return {}; tokens.discard_whitespace(); if (tokens.has_next_token()) return {}; transaction.commit(); return counter_name->custom_ident(); }; auto parse_counter_style = [this](TokenStream& tokens) -> RefPtr { // https://drafts.csswg.org/css-counter-styles-3/#typedef-counter-style // = | // For now we just support , found here: // https://drafts.csswg.org/css-counter-styles-3/#typedef-counter-style-name // is a that is not an ASCII case-insensitive match for none. auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto counter_style_name = parse_custom_ident_value(tokens, { { "none"sv } }); if (!counter_style_name) return {}; tokens.discard_whitespace(); if (tokens.has_next_token()) return {}; transaction.commit(); return counter_style_name.release_nonnull(); }; auto transaction = tokens.begin_transaction(); auto const& token = tokens.consume_a_token(); if (token.is_function("counter"sv)) { // counter() = counter( , ? ) auto& function = token.function(); auto context_guard = push_temporary_value_parsing_context(FunctionContext { function.name }); TokenStream function_tokens { function.value }; auto function_values = parse_a_comma_separated_list_of_component_values(function_tokens); if (function_values.is_empty() || function_values.size() > 2) return nullptr; TokenStream name_tokens { function_values[0] }; auto counter_name = parse_counter_name(name_tokens); if (!counter_name.has_value()) return nullptr; RefPtr counter_style; if (function_values.size() > 1) { TokenStream counter_style_tokens { function_values[1] }; counter_style = parse_counter_style(counter_style_tokens); if (!counter_style) return nullptr; } else { // In both cases, if the argument is omitted it defaults to `decimal`. counter_style = CustomIdentStyleValue::create("decimal"_fly_string); } transaction.commit(); return CounterStyleValue::create_counter(counter_name.release_value(), counter_style.release_nonnull()); } if (token.is_function("counters"sv)) { // counters() = counters( , , ? ) auto& function = token.function(); auto context_guard = push_temporary_value_parsing_context(FunctionContext { function.name }); TokenStream function_tokens { function.value }; auto function_values = parse_a_comma_separated_list_of_component_values(function_tokens); if (function_values.size() < 2 || function_values.size() > 3) return nullptr; TokenStream name_tokens { function_values[0] }; auto counter_name = parse_counter_name(name_tokens); if (!counter_name.has_value()) return nullptr; TokenStream string_tokens { function_values[1] }; string_tokens.discard_whitespace(); auto join_string = parse_string_value(string_tokens); string_tokens.discard_whitespace(); if (!join_string || string_tokens.has_next_token()) return nullptr; RefPtr counter_style; if (function_values.size() > 2) { TokenStream counter_style_tokens { function_values[2] }; counter_style = parse_counter_style(counter_style_tokens); if (!counter_style) return nullptr; } else { // In both cases, if the argument is omitted it defaults to `decimal`. counter_style = CustomIdentStyleValue::create("decimal"_fly_string); } transaction.commit(); return CounterStyleValue::create_counters(counter_name.release_value(), join_string->string_value(), counter_style.release_nonnull()); } return nullptr; } RefPtr Parser::parse_ratio_value(TokenStream& tokens) { if (auto ratio = parse_ratio(tokens); ratio.has_value()) return RatioStyleValue::create(ratio.release_value()); return nullptr; } RefPtr Parser::parse_string_value(TokenStream& tokens) { auto const& peek = tokens.next_token(); if (peek.is(Token::Type::String)) { tokens.discard_a_token(); return StringStyleValue::create(peek.token().string()); } return nullptr; } RefPtr Parser::parse_image_value(TokenStream& tokens) { tokens.mark(); auto url = parse_url_function(tokens); if (url.has_value()) { // If the value is a 'url(..)' parse as image, but if it is just a reference 'url(#xx)', leave it alone, // so we can parse as URL further on. These URLs are used as references inside SVG documents for masks. if (!url.value().equals(m_url, URL::ExcludeFragment::Yes)) { tokens.discard_a_mark(); return ImageStyleValue::create(url.value()); } tokens.restore_a_mark(); return nullptr; } tokens.discard_a_mark(); if (auto linear_gradient = parse_linear_gradient_function(tokens)) return linear_gradient; if (auto conic_gradient = parse_conic_gradient_function(tokens)) return conic_gradient; if (auto radial_gradient = parse_radial_gradient_function(tokens)) return radial_gradient; return nullptr; } // https://svgwg.org/svg2-draft/painting.html#SpecifyingPaint RefPtr Parser::parse_paint_value(TokenStream& tokens) { // ` = none | | [none | ]? | context-fill | context-stroke` auto parse_color_or_none = [&]() -> Optional> { if (auto color = parse_color_value(tokens)) return color; // NOTE: also accepts identifiers, so we do this identifier check last. if (tokens.next_token().is(Token::Type::Ident)) { auto maybe_keyword = keyword_from_string(tokens.next_token().token().ident()); if (maybe_keyword.has_value()) { // FIXME: Accept `context-fill` and `context-stroke` switch (*maybe_keyword) { case Keyword::None: tokens.discard_a_token(); return CSSKeywordValue::create(*maybe_keyword); default: return nullptr; } } } return OptionalNone {}; }; // FIMXE: Allow context-fill/context-stroke here if (auto color_or_none = parse_color_or_none(); color_or_none.has_value()) return *color_or_none; if (auto url = parse_url_value(tokens)) { tokens.discard_whitespace(); if (auto color_or_none = parse_color_or_none(); color_or_none == nullptr) { // Fail to parse if the fallback is invalid, but otherwise ignore it. // FIXME: Use fallback color return nullptr; } return url; } return nullptr; } // https://www.w3.org/TR/css-values-4/#position RefPtr Parser::parse_position_value(TokenStream& tokens, PositionParsingMode position_parsing_mode) { auto parse_position_edge = [](TokenStream& tokens) -> Optional { auto transaction = tokens.begin_transaction(); auto& token = tokens.consume_a_token(); if (!token.is(Token::Type::Ident)) return {}; auto keyword = keyword_from_string(token.token().ident()); if (!keyword.has_value()) return {}; transaction.commit(); return keyword_to_position_edge(*keyword); }; auto is_horizontal = [](PositionEdge edge, bool accept_center) -> bool { switch (edge) { case PositionEdge::Left: case PositionEdge::Right: return true; case PositionEdge::Center: return accept_center; default: return false; } }; auto is_vertical = [](PositionEdge edge, bool accept_center) -> bool { switch (edge) { case PositionEdge::Top: case PositionEdge::Bottom: return true; case PositionEdge::Center: return accept_center; default: return false; } }; // = [ // [ left | center | right | top | bottom | ] // | // [ left | center | right ] && [ top | center | bottom ] // | // [ left | center | right | ] // [ top | center | bottom | ] // | // [ [ left | right ] ] && // [ [ top | bottom ] ] // ] // [ left | center | right | top | bottom | ] auto alternative_1 = [&]() -> RefPtr { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); // [ left | center | right | top | bottom ] if (auto maybe_edge = parse_position_edge(tokens); maybe_edge.has_value()) { auto edge = maybe_edge.release_value(); transaction.commit(); // [ left | right ] if (is_horizontal(edge, false)) return PositionStyleValue::create(EdgeStyleValue::create(edge, {}), EdgeStyleValue::create(PositionEdge::Center, {})); // [ top | bottom ] if (is_vertical(edge, false)) return PositionStyleValue::create(EdgeStyleValue::create(PositionEdge::Center, {}), EdgeStyleValue::create(edge, {})); // [ center ] VERIFY(edge == PositionEdge::Center); return PositionStyleValue::create(EdgeStyleValue::create(PositionEdge::Center, {}), EdgeStyleValue::create(PositionEdge::Center, {})); } // [ ] if (auto maybe_percentage = parse_length_percentage(tokens); maybe_percentage.has_value()) { transaction.commit(); return PositionStyleValue::create(EdgeStyleValue::create({}, *maybe_percentage), EdgeStyleValue::create(PositionEdge::Center, {})); } return nullptr; }; // [ left | center | right ] && [ top | center | bottom ] auto alternative_2 = [&]() -> RefPtr { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); // Parse out two position edges auto maybe_first_edge = parse_position_edge(tokens); if (!maybe_first_edge.has_value()) return nullptr; auto first_edge = maybe_first_edge.release_value(); tokens.discard_whitespace(); auto maybe_second_edge = parse_position_edge(tokens); if (!maybe_second_edge.has_value()) return nullptr; auto second_edge = maybe_second_edge.release_value(); // If 'left' or 'right' is given, that position is X and the other is Y. // Conversely - // If 'top' or 'bottom' is given, that position is Y and the other is X. if (is_vertical(first_edge, false) || is_horizontal(second_edge, false)) swap(first_edge, second_edge); // [ left | center | right ] [ top | bottom | center ] if (is_horizontal(first_edge, true) && is_vertical(second_edge, true)) { transaction.commit(); return PositionStyleValue::create(EdgeStyleValue::create(first_edge, {}), EdgeStyleValue::create(second_edge, {})); } return nullptr; }; // [ left | center | right | ] // [ top | center | bottom | ] auto alternative_3 = [&]() -> RefPtr { auto transaction = tokens.begin_transaction(); auto parse_position_or_length = [&](bool as_horizontal) -> RefPtr { tokens.discard_whitespace(); if (auto maybe_position = parse_position_edge(tokens); maybe_position.has_value()) { auto position = maybe_position.release_value(); bool valid = as_horizontal ? is_horizontal(position, true) : is_vertical(position, true); if (!valid) return nullptr; return EdgeStyleValue::create(position, {}); } auto maybe_length = parse_length_percentage(tokens); if (!maybe_length.has_value()) return nullptr; return EdgeStyleValue::create({}, maybe_length); }; // [ left | center | right | ] auto horizontal_edge = parse_position_or_length(true); if (!horizontal_edge) return nullptr; // [ top | center | bottom | ] auto vertical_edge = parse_position_or_length(false); if (!vertical_edge) return nullptr; transaction.commit(); return PositionStyleValue::create(horizontal_edge.release_nonnull(), vertical_edge.release_nonnull()); }; // [ [ left | right ] ] && // [ [ top | bottom ] ] auto alternative_4 = [&]() -> RefPtr { struct PositionAndLength { PositionEdge position; LengthPercentage length; }; auto parse_position_and_length = [&]() -> Optional { tokens.discard_whitespace(); auto maybe_position = parse_position_edge(tokens); if (!maybe_position.has_value()) return {}; tokens.discard_whitespace(); auto maybe_length = parse_length_percentage(tokens); if (!maybe_length.has_value()) return {}; return PositionAndLength { .position = maybe_position.release_value(), .length = maybe_length.release_value(), }; }; auto transaction = tokens.begin_transaction(); auto maybe_group1 = parse_position_and_length(); if (!maybe_group1.has_value()) return nullptr; auto maybe_group2 = parse_position_and_length(); if (!maybe_group2.has_value()) return nullptr; auto group1 = maybe_group1.release_value(); auto group2 = maybe_group2.release_value(); // [ [ left | right ] ] [ [ top | bottom ] ] if (is_horizontal(group1.position, false) && is_vertical(group2.position, false)) { transaction.commit(); return PositionStyleValue::create(EdgeStyleValue::create(group1.position, group1.length), EdgeStyleValue::create(group2.position, group2.length)); } // [ [ top | bottom ] ] [ [ left | right ] ] if (is_vertical(group1.position, false) && is_horizontal(group2.position, false)) { transaction.commit(); return PositionStyleValue::create(EdgeStyleValue::create(group2.position, group2.length), EdgeStyleValue::create(group1.position, group1.length)); } return nullptr; }; // The extra 3-value syntax that's allowed for background-position: // [ center | [ left | right ] ? ] && // [ center | [ top | bottom ] ? ] auto alternative_5_for_background_position = [&]() -> RefPtr { auto transaction = tokens.begin_transaction(); struct PositionAndMaybeLength { PositionEdge position; Optional length; }; // [ ? ] auto parse_position_and_maybe_length = [&]() -> Optional { auto inner_transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto maybe_position = parse_position_edge(tokens); if (!maybe_position.has_value()) return {}; tokens.discard_whitespace(); auto maybe_length = parse_length_percentage(tokens); if (maybe_length.has_value()) { // 'center' cannot be followed by a if (maybe_position.value() == PositionEdge::Center && maybe_length.has_value()) return {}; } inner_transaction.commit(); return PositionAndMaybeLength { .position = maybe_position.release_value(), .length = move(maybe_length), }; }; auto maybe_group1 = parse_position_and_maybe_length(); if (!maybe_group1.has_value()) return nullptr; auto maybe_group2 = parse_position_and_maybe_length(); if (!maybe_group2.has_value()) return nullptr; auto group1 = maybe_group1.release_value(); auto group2 = maybe_group2.release_value(); // 2-value or 4-value if both s are present or missing. if (group1.length.has_value() == group2.length.has_value()) return nullptr; // If 'left' or 'right' is given, that position is X and the other is Y. // Conversely - // If 'top' or 'bottom' is given, that position is Y and the other is X. if (is_vertical(group1.position, false) || is_horizontal(group2.position, false)) swap(group1, group2); // [ center | [ left | right ] ] if (!is_horizontal(group1.position, true)) return nullptr; // [ center | [ top | bottom ] ] if (!is_vertical(group2.position, true)) return nullptr; auto to_style_value = [&](PositionAndMaybeLength const& group) -> NonnullRefPtr { if (group.position == PositionEdge::Center) return EdgeStyleValue::create(PositionEdge::Center, {}); return EdgeStyleValue::create(group.position, group.length); }; transaction.commit(); return PositionStyleValue::create(to_style_value(group1), to_style_value(group2)); }; // Note: The alternatives must be attempted in this order since shorter alternatives can match a prefix of longer ones. if (auto position = alternative_4()) return position; if (position_parsing_mode == PositionParsingMode::BackgroundPosition) { if (auto position = alternative_5_for_background_position()) return position; } if (auto position = alternative_3()) return position; if (auto position = alternative_2()) return position; if (auto position = alternative_1()) return position; return nullptr; } RefPtr Parser::parse_easing_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto const& part = tokens.consume_a_token(); if (part.is(Token::Type::Ident)) { auto name = part.token().ident(); auto maybe_simple_easing = [&] -> RefPtr { if (name.equals_ignoring_ascii_case("linear"sv)) return EasingStyleValue::create(EasingStyleValue::Linear::identity()); if (name.equals_ignoring_ascii_case("ease"sv)) return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease()); if (name.equals_ignoring_ascii_case("ease-in"sv)) return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_in()); if (name.equals_ignoring_ascii_case("ease-out"sv)) return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_out()); if (name.equals_ignoring_ascii_case("ease-in-out"sv)) return EasingStyleValue::create(EasingStyleValue::CubicBezier::ease_in_out()); if (name.equals_ignoring_ascii_case("step-start"sv)) return EasingStyleValue::create(EasingStyleValue::Steps::step_start()); if (name.equals_ignoring_ascii_case("step-end"sv)) return EasingStyleValue::create(EasingStyleValue::Steps::step_end()); return {}; }(); if (!maybe_simple_easing) return nullptr; transaction.commit(); return maybe_simple_easing; } if (!part.is_function()) return nullptr; TokenStream argument_tokens { part.function().value }; auto comma_separated_arguments = parse_a_comma_separated_list_of_component_values(argument_tokens); // Remove whitespace for (auto& argument : comma_separated_arguments) argument.remove_all_matching([](auto& value) { return value.is(Token::Type::Whitespace); }); auto name = part.function().name; auto context_guard = push_temporary_value_parsing_context(FunctionContext { name }); if (name.equals_ignoring_ascii_case("linear"sv)) { // linear() = linear( [ && {0,2} ]# ) Vector stops; for (auto const& argument : comma_separated_arguments) { TokenStream argument_tokens { argument }; Optional output; Optional first_input; Optional second_input; if (argument_tokens.next_token().is(Token::Type::Number)) output = argument_tokens.consume_a_token().token().number_value(); if (argument_tokens.next_token().is(Token::Type::Percentage)) { first_input = argument_tokens.consume_a_token().token().percentage() / 100; if (argument_tokens.next_token().is(Token::Type::Percentage)) { second_input = argument_tokens.consume_a_token().token().percentage() / 100; } } if (argument_tokens.next_token().is(Token::Type::Number)) { if (output.has_value()) return nullptr; output = argument_tokens.consume_a_token().token().number_value(); } if (argument_tokens.has_next_token() || !output.has_value()) return nullptr; stops.append({ output.value(), first_input, first_input.has_value() }); if (second_input.has_value()) stops.append({ output.value(), second_input, true }); } if (stops.is_empty()) return nullptr; transaction.commit(); return EasingStyleValue::create(EasingStyleValue::Linear { move(stops) }); } if (name.equals_ignoring_ascii_case("cubic-bezier"sv)) { if (comma_separated_arguments.size() != 4) return nullptr; for (auto const& argument : comma_separated_arguments) { if (argument.size() != 1) return nullptr; if (!argument[0].is(Token::Type::Number)) return nullptr; } EasingStyleValue::CubicBezier bezier { comma_separated_arguments[0][0].token().number_value(), comma_separated_arguments[1][0].token().number_value(), comma_separated_arguments[2][0].token().number_value(), comma_separated_arguments[3][0].token().number_value(), }; if (bezier.x1 < 0.0 || bezier.x1 > 1.0 || bezier.x2 < 0.0 || bezier.x2 > 1.0) return nullptr; transaction.commit(); return EasingStyleValue::create(bezier); } if (name.equals_ignoring_ascii_case("steps"sv)) { if (comma_separated_arguments.is_empty() || comma_separated_arguments.size() > 2) return nullptr; for (auto const& argument : comma_separated_arguments) { if (argument.size() != 1) return nullptr; } EasingStyleValue::Steps steps; auto const& intervals_argument = comma_separated_arguments[0][0]; if (!intervals_argument.is(Token::Type::Number)) return nullptr; if (!intervals_argument.token().number().is_integer()) return nullptr; auto intervals = intervals_argument.token().to_integer(); if (comma_separated_arguments.size() == 2) { TokenStream identifier_stream { comma_separated_arguments[1] }; auto keyword_value = parse_keyword_value(identifier_stream); if (!keyword_value) return nullptr; switch (keyword_value->to_keyword()) { case Keyword::JumpStart: steps.position = EasingStyleValue::Steps::Position::JumpStart; break; case Keyword::JumpEnd: steps.position = EasingStyleValue::Steps::Position::JumpEnd; break; case Keyword::JumpBoth: steps.position = EasingStyleValue::Steps::Position::JumpBoth; break; case Keyword::JumpNone: steps.position = EasingStyleValue::Steps::Position::JumpNone; break; case Keyword::Start: steps.position = EasingStyleValue::Steps::Position::Start; break; case Keyword::End: steps.position = EasingStyleValue::Steps::Position::End; break; default: return nullptr; } } // Perform extra validation // https://drafts.csswg.org/css-easing/#step-easing-functions // If the is jump-none, the must be at least 2, or the function is invalid. // Otherwise, the must be at least 1, or the function is invalid. if (steps.position == EasingStyleValue::Steps::Position::JumpNone) { if (intervals <= 1) return nullptr; } else if (intervals <= 0) { return nullptr; } steps.number_of_intervals = intervals; transaction.commit(); return EasingStyleValue::create(steps); } return nullptr; } Optional Parser::parse_url_function(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto& component_value = tokens.consume_a_token(); auto convert_string_to_url = [&](StringView url_string) -> Optional { auto url = complete_url(url_string); if (url.has_value()) { transaction.commit(); return url; } return {}; }; if (component_value.is(Token::Type::Url)) { auto url_string = component_value.token().url(); return convert_string_to_url(url_string); } if (component_value.is_function("url"sv)) { auto const& function_values = component_value.function().value; // FIXME: Handle url-modifiers. https://www.w3.org/TR/css-values-4/#url-modifiers for (size_t i = 0; i < function_values.size(); ++i) { auto const& value = function_values[i]; if (value.is(Token::Type::Whitespace)) continue; if (value.is(Token::Type::String)) { auto url_string = value.token().string(); return convert_string_to_url(url_string); } break; } } return {}; } RefPtr Parser::parse_url_value(TokenStream& tokens) { auto url = parse_url_function(tokens); if (!url.has_value()) return nullptr; return URLStyleValue::create(*url); } // https://www.w3.org/TR/css-shapes-1/#typedef-shape-radius Optional Parser::parse_shape_radius(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto maybe_radius = parse_length_percentage(tokens); if (maybe_radius.has_value()) { // Negative radius is invalid. auto radius = maybe_radius.value(); if ((radius.is_length() && radius.length().raw_value() < 0) || (radius.is_percentage() && radius.percentage().value() < 0)) return {}; transaction.commit(); return radius; } if (tokens.next_token().is_ident("closest-side"sv)) { tokens.discard_a_token(); transaction.commit(); return FitSide::ClosestSide; } if (tokens.next_token().is_ident("farthest-side"sv)) { tokens.discard_a_token(); transaction.commit(); return FitSide::FarthestSide; } return {}; } RefPtr Parser::parse_fit_content_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto& component_value = tokens.consume_a_token(); if (component_value.is_ident("fit-content"sv)) { transaction.commit(); return FitContentStyleValue::create(); return nullptr; } if (!component_value.is_function()) return nullptr; auto const& function = component_value.function(); if (function.name != "fit-content"sv) return nullptr; TokenStream argument_tokens { function.value }; argument_tokens.discard_whitespace(); auto maybe_length = parse_length_percentage(argument_tokens); if (!maybe_length.has_value()) return nullptr; argument_tokens.discard_whitespace(); if (argument_tokens.has_next_token()) return nullptr; transaction.commit(); return FitContentStyleValue::create(maybe_length.release_value()); } RefPtr Parser::parse_basic_shape_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto& component_value = tokens.consume_a_token(); if (!component_value.is_function()) return nullptr; auto function_name = component_value.function().name.bytes_as_string_view(); // FIXME: Implement path(). See: https://www.w3.org/TR/css-shapes-1/#basic-shape-functions if (function_name.equals_ignoring_ascii_case("inset"sv)) { // inset() = inset( {1,4} [ round <'border-radius'> ]? ) // FIXME: Parse the border-radius. auto arguments_tokens = TokenStream { component_value.function().value }; // If less than four values are provided, // the omitted values default in the same way as the margin shorthand: // an omitted second or third value defaults to the first, and an omitted fourth value defaults to the second. // The four s define the position of the top, right, bottom, and left edges of a rectangle. arguments_tokens.discard_whitespace(); auto top = parse_length_percentage(arguments_tokens); if (!top.has_value()) return nullptr; arguments_tokens.discard_whitespace(); auto right = parse_length_percentage(arguments_tokens); if (!right.has_value()) right = top; arguments_tokens.discard_whitespace(); auto bottom = parse_length_percentage(arguments_tokens); if (!bottom.has_value()) bottom = top; arguments_tokens.discard_whitespace(); auto left = parse_length_percentage(arguments_tokens); if (!left.has_value()) left = right; arguments_tokens.discard_whitespace(); if (arguments_tokens.has_next_token()) return nullptr; transaction.commit(); return BasicShapeStyleValue::create(Inset { LengthBox(top.value(), right.value(), bottom.value(), left.value()) }); } if (function_name.equals_ignoring_ascii_case("xywh"sv)) { // xywh() = xywh( {2} {2} [ round <'border-radius'> ]? ) // FIXME: Parse the border-radius. auto arguments_tokens = TokenStream { component_value.function().value }; arguments_tokens.discard_whitespace(); auto x = parse_length_percentage(arguments_tokens); if (!x.has_value()) return nullptr; arguments_tokens.discard_whitespace(); auto y = parse_length_percentage(arguments_tokens); if (!y.has_value()) return nullptr; arguments_tokens.discard_whitespace(); auto width = parse_length_percentage(arguments_tokens); if (!width.has_value()) return nullptr; arguments_tokens.discard_whitespace(); auto height = parse_length_percentage(arguments_tokens); if (!height.has_value()) return nullptr; arguments_tokens.discard_whitespace(); if (arguments_tokens.has_next_token()) return nullptr; // Negative width or height is invalid. if ((width->is_length() && width->length().raw_value() < 0) || (width->is_percentage() && width->percentage().value() < 0) || (height->is_length() && height->length().raw_value() < 0) || (height->is_percentage() && height->percentage().value() < 0)) return nullptr; transaction.commit(); return BasicShapeStyleValue::create(Xywh { x.value(), y.value(), width.value(), height.value() }); } if (function_name.equals_ignoring_ascii_case("rect"sv)) { // rect() = rect( [ | auto ]{4} [ round <'border-radius'> ]? ) // FIXME: Parse the border-radius. auto arguments_tokens = TokenStream { component_value.function().value }; auto parse_length_percentage_or_auto = [this](TokenStream& tokens) -> Optional { tokens.discard_whitespace(); auto value = parse_length_percentage(tokens); if (!value.has_value()) { if (tokens.consume_a_token().is_ident("auto"sv)) { value = Length::make_auto(); } } return value; }; auto top = parse_length_percentage_or_auto(arguments_tokens); auto right = parse_length_percentage_or_auto(arguments_tokens); auto bottom = parse_length_percentage_or_auto(arguments_tokens); auto left = parse_length_percentage_or_auto(arguments_tokens); if (!top.has_value() || !right.has_value() || !bottom.has_value() || !left.has_value()) return nullptr; arguments_tokens.discard_whitespace(); if (arguments_tokens.has_next_token()) return nullptr; transaction.commit(); return BasicShapeStyleValue::create(Rect { LengthBox(top.value(), right.value(), bottom.value(), left.value()) }); } if (function_name.equals_ignoring_ascii_case("circle"sv)) { // circle() = circle( ? [ at ]? ) auto arguments_tokens = TokenStream { component_value.function().value }; auto radius = parse_shape_radius(arguments_tokens).value_or(FitSide::ClosestSide); auto position = PositionStyleValue::create_center(); arguments_tokens.discard_whitespace(); if (arguments_tokens.next_token().is_ident("at"sv)) { arguments_tokens.discard_a_token(); arguments_tokens.discard_whitespace(); auto maybe_position = parse_position_value(arguments_tokens); if (maybe_position.is_null()) return nullptr; position = maybe_position.release_nonnull(); } arguments_tokens.discard_whitespace(); if (arguments_tokens.has_next_token()) return nullptr; transaction.commit(); return BasicShapeStyleValue::create(Circle { radius, position }); } if (function_name.equals_ignoring_ascii_case("ellipse"sv)) { // ellipse() = ellipse( [ {2} ]? [ at ]? ) auto arguments_tokens = TokenStream { component_value.function().value }; Optional radius_x = parse_shape_radius(arguments_tokens); Optional radius_y = parse_shape_radius(arguments_tokens); if (radius_x.has_value() && !radius_y.has_value()) return nullptr; if (!radius_x.has_value()) { radius_x = FitSide::ClosestSide; radius_y = FitSide::ClosestSide; } auto position = PositionStyleValue::create_center(); arguments_tokens.discard_whitespace(); if (arguments_tokens.next_token().is_ident("at"sv)) { arguments_tokens.discard_a_token(); arguments_tokens.discard_whitespace(); auto maybe_position = parse_position_value(arguments_tokens); if (maybe_position.is_null()) return nullptr; position = maybe_position.release_nonnull(); } arguments_tokens.discard_whitespace(); if (arguments_tokens.has_next_token()) return nullptr; transaction.commit(); return BasicShapeStyleValue::create(Ellipse { radius_x.value(), radius_y.value(), position }); } if (function_name.equals_ignoring_ascii_case("polygon"sv)) { // polygon() = polygon( <'fill-rule'>? , [ ]# ) auto arguments_tokens = TokenStream { component_value.function().value }; auto arguments = parse_a_comma_separated_list_of_component_values(arguments_tokens); if (arguments.size() < 1) return nullptr; Optional fill_rule; auto const& first_argument = arguments[0]; TokenStream first_argument_tokens { first_argument }; first_argument_tokens.discard_whitespace(); if (first_argument_tokens.next_token().is_ident("nonzero"sv)) { fill_rule = Gfx::WindingRule::Nonzero; } else if (first_argument_tokens.next_token().is_ident("evenodd"sv)) { fill_rule = Gfx::WindingRule::EvenOdd; } if (fill_rule.has_value()) { first_argument_tokens.discard_a_token(); if (first_argument_tokens.has_next_token()) return nullptr; arguments.remove(0); } else { fill_rule = Gfx::WindingRule::Nonzero; } if (arguments.size() < 1) return nullptr; Vector points; for (auto& argument : arguments) { TokenStream argument_tokens { argument }; argument_tokens.discard_whitespace(); auto x_pos = parse_length_percentage(argument_tokens); if (!x_pos.has_value()) return nullptr; argument_tokens.discard_whitespace(); auto y_pos = parse_length_percentage(argument_tokens); if (!y_pos.has_value()) return nullptr; argument_tokens.discard_whitespace(); if (argument_tokens.has_next_token()) return nullptr; points.append(Polygon::Point { *x_pos, *y_pos }); } transaction.commit(); return BasicShapeStyleValue::create(Polygon { fill_rule.value(), move(points) }); } return nullptr; } RefPtr Parser::parse_builtin_value(TokenStream& tokens) { auto transaction = tokens.begin_transaction(); auto& component_value = tokens.consume_a_token(); if (component_value.is(Token::Type::Ident)) { auto ident = component_value.token().ident(); if (ident.equals_ignoring_ascii_case("inherit"sv)) { transaction.commit(); return CSSKeywordValue::create(Keyword::Inherit); } if (ident.equals_ignoring_ascii_case("initial"sv)) { transaction.commit(); return CSSKeywordValue::create(Keyword::Initial); } if (ident.equals_ignoring_ascii_case("unset"sv)) { transaction.commit(); return CSSKeywordValue::create(Keyword::Unset); } if (ident.equals_ignoring_ascii_case("revert"sv)) { transaction.commit(); return CSSKeywordValue::create(Keyword::Revert); } if (ident.equals_ignoring_ascii_case("revert-layer"sv)) { transaction.commit(); return CSSKeywordValue::create(Keyword::RevertLayer); } } return nullptr; } // https://www.w3.org/TR/css-values-4/#custom-idents RefPtr Parser::parse_custom_ident_value(TokenStream& tokens, ReadonlySpan blacklist) { auto transaction = tokens.begin_transaction(); tokens.discard_whitespace(); auto const& token = tokens.consume_a_token(); if (!token.is(Token::Type::Ident)) return nullptr; auto custom_ident = token.token().ident(); // The CSS-wide keywords are not valid s. if (is_css_wide_keyword(custom_ident)) return nullptr; // The default keyword is reserved and is also not a valid . if (custom_ident.equals_ignoring_ascii_case("default"sv)) return nullptr; // Specifications using must specify clearly what other keywords are excluded from , // if any—for example by saying that any pre-defined keywords in that property’s value definition are excluded. // Excluded keywords are excluded in all ASCII case permutations. for (auto& value : blacklist) { if (custom_ident.equals_ignoring_ascii_case(value)) return nullptr; } transaction.commit(); return CustomIdentStyleValue::create(custom_ident); } Optional Parser::parse_grid_size(ComponentValue const& component_value) { if (component_value.is_function()) { if (auto maybe_calculated = parse_calculated_value(component_value)) { if (maybe_calculated->is_length()) return GridSize(maybe_calculated->as_length().length()); if (maybe_calculated->is_percentage()) return GridSize(maybe_calculated->as_percentage().percentage()); if (maybe_calculated->is_calculated() && maybe_calculated->as_calculated().resolves_to_length_percentage()) return GridSize(LengthPercentage(maybe_calculated->as_calculated())); // FIXME: Support calculated } return {}; } if (component_value.is_ident("auto"sv)) return GridSize::make_auto(); if (component_value.is_ident("max-content"sv)) return GridSize(GridSize::Type::MaxContent); if (component_value.is_ident("min-content"sv)) return GridSize(GridSize::Type::MinContent); auto dimension = parse_dimension(component_value); if (!dimension.has_value()) return {}; if (dimension->is_length()) return GridSize(dimension->length()); else if (dimension->is_percentage()) return GridSize(dimension->percentage()); else if (dimension->is_flex()) return GridSize(dimension->flex()); return {}; } Optional Parser::parse_grid_fit_content(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-fit-content // 'fit-content( )' // Represents the formula max(minimum, min(limit, max-content)), where minimum represents an auto minimum (which is often, but not always, // equal to a min-content minimum), and limit is the track sizing function passed as an argument to fit-content(). // This is essentially calculated as the smaller of minmax(auto, max-content) and minmax(auto, limit). auto function_tokens = TokenStream(component_values); function_tokens.discard_whitespace(); auto maybe_length_percentage = parse_length_percentage(function_tokens); if (maybe_length_percentage.has_value()) return CSS::GridFitContent(CSS::GridSize(CSS::GridSize::Type::FitContent, maybe_length_percentage.value())); return {}; } Optional Parser::parse_min_max(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-minmax // 'minmax(min, max)' // Defines a size range greater than or equal to min and less than or equal to max. If the max is // less than the min, then the max will be floored by the min (essentially yielding minmax(min, // min)). As a maximum, a value sets the track’s flex factor; it is invalid as a minimum. auto function_tokens = TokenStream(component_values); auto comma_separated_list = parse_a_comma_separated_list_of_component_values(function_tokens); if (comma_separated_list.size() != 2) return {}; TokenStream part_one_tokens { comma_separated_list[0] }; part_one_tokens.discard_whitespace(); if (!part_one_tokens.has_next_token()) return {}; NonnullRawPtr current_token = part_one_tokens.consume_a_token(); auto min_grid_size = parse_grid_size(current_token); TokenStream part_two_tokens { comma_separated_list[1] }; part_two_tokens.discard_whitespace(); if (!part_two_tokens.has_next_token()) return {}; current_token = part_two_tokens.consume_a_token(); auto max_grid_size = parse_grid_size(current_token); if (min_grid_size.has_value() && max_grid_size.has_value()) { // https://www.w3.org/TR/css-grid-2/#valdef-grid-template-columns-minmax // As a maximum, a value sets the track’s flex factor; it is invalid as a minimum. if (min_grid_size.value().is_flexible_length()) return {}; return CSS::GridMinMax(min_grid_size.value(), max_grid_size.value()); } return {}; } Optional Parser::parse_repeat(Vector const& component_values) { // https://www.w3.org/TR/css-grid-2/#repeat-syntax // 7.2.3.1. Syntax of repeat() // The generic form of the repeat() syntax is, approximately, // repeat( [ | auto-fill | auto-fit ] , ) auto is_auto_fill = false; auto is_auto_fit = false; auto function_tokens = TokenStream(component_values); auto comma_separated_list = parse_a_comma_separated_list_of_component_values(function_tokens); if (comma_separated_list.size() != 2) return {}; // The first argument specifies the number of repetitions. TokenStream part_one_tokens { comma_separated_list[0] }; part_one_tokens.discard_whitespace(); if (!part_one_tokens.has_next_token()) return {}; auto& current_token = part_one_tokens.consume_a_token(); auto repeat_count = 0; if (current_token.is(Token::Type::Number) && current_token.token().number().is_integer() && current_token.token().number_value() > 0) repeat_count = current_token.token().number_value(); else if (current_token.is_ident("auto-fill"sv)) is_auto_fill = true; else if (current_token.is_ident("auto-fit"sv)) is_auto_fit = true; // The second argument is a track list, which is repeated that number of times. TokenStream part_two_tokens { comma_separated_list[1] }; part_two_tokens.discard_whitespace(); if (!part_two_tokens.has_next_token()) return {}; Vector> repeat_params; auto last_object_was_line_names = false; while (part_two_tokens.has_next_token()) { auto const& token = part_two_tokens.consume_a_token(); Vector line_names; if (token.is_block()) { if (last_object_was_line_names) return {}; last_object_was_line_names = true; if (!token.block().is_square()) return {}; TokenStream block_tokens { token.block().value }; while (block_tokens.has_next_token()) { auto const& current_block_token = block_tokens.consume_a_token(); line_names.append(current_block_token.token().ident().to_string()); block_tokens.discard_whitespace(); } repeat_params.append(GridLineNames { move(line_names) }); part_two_tokens.discard_whitespace(); } else { last_object_was_line_names = false; auto track_sizing_function = parse_track_sizing_function(token); if (!track_sizing_function.has_value()) return {}; // However, there are some restrictions: // The repeat() notation can’t be nested. if (track_sizing_function.value().is_repeat()) return {}; // Automatic repetitions (auto-fill or auto-fit) cannot be combined with intrinsic or flexible sizes. // Note that 'auto' is also an intrinsic size (and thus not permitted) but we can't use // track_sizing_function.is_auto(..) to check for it, as it requires AvailableSize, which is why there is // a separate check for it below. // https://www.w3.org/TR/css-grid-2/#repeat-syntax // https://www.w3.org/TR/css-grid-2/#intrinsic-sizing-function if (track_sizing_function.value().is_default() && (track_sizing_function.value().grid_size().is_flexible_length() || token.is_ident("auto"sv)) && (is_auto_fill || is_auto_fit)) return {}; repeat_params.append(track_sizing_function.value()); part_two_tokens.discard_whitespace(); } } // Thus the precise syntax of the repeat() notation has several forms: // = repeat( [ ] , [ ? ]+ ? ) // = repeat( [ auto-fill | auto-fit ] , [ ? ]+ ? ) // = repeat( [ ] , [ ? ]+ ? ) // = repeat( [ | auto-fill ], +) // The variant can represent the repetition of any , but is limited to a // fixed number of repetitions. // The variant can repeat automatically to fill a space, but requires definite track // sizes so that the number of repetitions can be calculated. It can only appear once in the track // list, but the same track list can also contain s. // The variant is for adding line names to subgrids. It can only be used with the // subgrid keyword and cannot specify track sizes, only line names. // If a repeat() function that is not a ends up placing two adjacent to // each other, the name lists are merged. For example, repeat(2, [a] 1fr [b]) is equivalent to [a] // 1fr [b a] 1fr [b]. if (is_auto_fill) return GridRepeat(GridTrackSizeList(move(repeat_params)), GridRepeat::Type::AutoFill); else if (is_auto_fit) return GridRepeat(GridTrackSizeList(move(repeat_params)), GridRepeat::Type::AutoFit); else return GridRepeat(GridTrackSizeList(move(repeat_params)), repeat_count); } Optional Parser::parse_track_sizing_function(ComponentValue const& token) { if (token.is_function()) { auto const& function_token = token.function(); auto context_guard = push_temporary_value_parsing_context(FunctionContext { function_token.name }); if (function_token.name.equals_ignoring_ascii_case("repeat"sv)) { auto maybe_repeat = parse_repeat(function_token.value); if (maybe_repeat.has_value()) return CSS::ExplicitGridTrack(maybe_repeat.value()); else return {}; } else if (function_token.name.equals_ignoring_ascii_case("minmax"sv)) { auto maybe_min_max_value = parse_min_max(function_token.value); if (maybe_min_max_value.has_value()) return CSS::ExplicitGridTrack(maybe_min_max_value.value()); else return {}; } else if (function_token.name.equals_ignoring_ascii_case("fit-content"sv)) { auto maybe_fit_content_value = parse_grid_fit_content(function_token.value); if (maybe_fit_content_value.has_value()) return CSS::ExplicitGridTrack(maybe_fit_content_value.value()); return {}; } else if (auto maybe_calculated = parse_calculated_value(token)) { if (maybe_calculated->is_length()) return ExplicitGridTrack(GridSize(maybe_calculated->as_length().length())); if (maybe_calculated->is_percentage()) return ExplicitGridTrack(GridSize(maybe_calculated->as_percentage().percentage())); if (maybe_calculated->is_calculated() && maybe_calculated->as_calculated().resolves_to_length_percentage()) return ExplicitGridTrack(GridSize(LengthPercentage(maybe_calculated->as_calculated()))); } return {}; } else if (token.is_ident("auto"sv)) { return CSS::ExplicitGridTrack(GridSize(Length::make_auto())); } else if (token.is_block()) { return {}; } else { auto grid_size = parse_grid_size(token); if (!grid_size.has_value()) return {}; return CSS::ExplicitGridTrack(grid_size.value()); } } RefPtr Parser::parse_grid_track_placement(TokenStream& tokens) { // FIXME: This shouldn't be needed. Right now, the below code returns a CSSStyleValue even if no tokens are consumed! if (!tokens.has_next_token()) return nullptr; if (tokens.remaining_token_count() > 3) return nullptr; // https://www.w3.org/TR/css-grid-2/#line-placement // Line-based Placement: the grid-row-start, grid-column-start, grid-row-end, and grid-column-end properties // = // auto | // | // [ && ? ] | // [ span && [ || ] ] auto is_valid_integer = [](auto& token) -> bool { // An value of zero makes the declaration invalid. if (token.is(Token::Type::Number) && token.token().number().is_integer() && token.token().number_value() != 0) return true; return false; }; auto parse_custom_ident = [this](auto& tokens) { // The additionally excludes the keywords span and auto. return parse_custom_ident_value(tokens, { { "span"sv, "auto"sv } }); }; auto transaction = tokens.begin_transaction(); // FIXME: Handle the single-token case inside the loop instead, so that we can more easily call this from // `parse_grid_area_shorthand_value()` using a single TokenStream. if (tokens.remaining_token_count() == 1) { if (auto custom_ident = parse_custom_ident(tokens)) { transaction.commit(); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_line({}, custom_ident->custom_ident().to_string())); } auto const& token = tokens.consume_a_token(); if (auto maybe_calculated = parse_calculated_value(token)) { if (maybe_calculated->is_number()) { transaction.commit(); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_line(static_cast(maybe_calculated->as_number().number()), {})); } if (maybe_calculated->is_calculated() && maybe_calculated->as_calculated().resolves_to_number()) { transaction.commit(); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_line(static_cast(maybe_calculated->as_calculated().resolve_integer({}).value()), {})); } } if (token.is_ident("auto"sv)) { transaction.commit(); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_auto()); } if (is_valid_integer(token)) { transaction.commit(); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_line(static_cast(token.token().number_value()), {})); } return nullptr; } auto span_value = false; auto span_or_position_value = 0; String identifier_value; while (tokens.has_next_token()) { auto const& token = tokens.next_token(); if (token.is_ident("auto"sv)) return nullptr; if (token.is_ident("span"sv)) { if (span_value) return nullptr; tokens.discard_a_token(); // span if (tokens.has_next_token() && ((span_or_position_value != 0 && identifier_value.is_empty()) || (span_or_position_value == 0 && !identifier_value.is_empty()))) return nullptr; span_value = true; continue; } if (is_valid_integer(token)) { if (span_or_position_value != 0) return nullptr; span_or_position_value = static_cast(tokens.consume_a_token().token().to_integer()); continue; } if (auto custom_ident = parse_custom_ident(tokens)) { if (!identifier_value.is_empty()) return nullptr; identifier_value = custom_ident->custom_ident().to_string(); continue; } break; } if (tokens.has_next_token()) return nullptr; // Negative integers or zero are invalid. if (span_value && span_or_position_value < 1) return nullptr; // If the is omitted, it defaults to 1. if (span_or_position_value == 0) span_or_position_value = 1; transaction.commit(); if (!identifier_value.is_empty()) return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_line(span_or_position_value, identifier_value)); return GridTrackPlacementStyleValue::create(GridTrackPlacement::make_span(span_or_position_value)); } RefPtr Parser::parse_calculated_value(ComponentValue const& component_value) { if (!component_value.is_function()) return nullptr; auto const& function = component_value.function(); CalculationContext context {}; for (auto const& value_context : m_value_context.in_reverse()) { auto maybe_context = value_context.visit( [](PropertyID property_id) -> Optional { return CalculationContext { .percentages_resolve_as = property_resolves_percentages_relative_to(property_id), .resolve_numbers_as_integers = property_accepts_type(property_id, ValueType::Integer), }; }, [](FunctionContext const& function) -> Optional { // Gradients resolve percentages as lengths relative to the gradient-box. if (function.name.is_one_of_ignoring_ascii_case( "linear-gradient"sv, "repeating-linear-gradient"sv, "radial-gradient"sv, "repeating-radial-gradient"sv, "conic-gradient"sv, "repeating-conic-gradient"sv)) { return CalculationContext { .percentages_resolve_as = ValueType::Length }; } // FIXME: Add other functions that provide a context for resolving values return {}; }); if (maybe_context.has_value()) { context = maybe_context.release_value(); break; } } auto function_node = parse_a_calc_function_node(function, context); if (!function_node) return nullptr; auto function_type = function_node->numeric_type(); if (!function_type.has_value()) return nullptr; return CalculatedStyleValue::create(function_node.release_nonnull(), function_type.release_value(), context); } RefPtr Parser::parse_a_calc_function_node(Function const& function, CalculationContext const& context) { auto context_guard = push_temporary_value_parsing_context(FunctionContext { function.name }); if (function.name.equals_ignoring_ascii_case("calc"sv)) return parse_a_calculation(function.value, context); if (auto maybe_function = parse_math_function(function, context)) return maybe_function; return nullptr; } RefPtr Parser::convert_to_calculation_node(CalcParsing::Node const& node, CalculationContext const& context) { return node.visit( [this, &context](NonnullOwnPtr const& product_node) -> RefPtr { Vector> children; children.ensure_capacity(product_node->children.size()); for (auto const& child : product_node->children) { if (auto child_as_node = convert_to_calculation_node(child, context)) { children.append(child_as_node.release_nonnull()); } else { return nullptr; } } return ProductCalculationNode::create(move(children)); }, [this, &context](NonnullOwnPtr const& sum_node) -> RefPtr { Vector> children; children.ensure_capacity(sum_node->children.size()); for (auto const& child : sum_node->children) { if (auto child_as_node = convert_to_calculation_node(child, context)) { children.append(child_as_node.release_nonnull()); } else { return nullptr; } } return SumCalculationNode::create(move(children)); }, [this, &context](NonnullOwnPtr const& invert_node) -> RefPtr { if (auto child_as_node = convert_to_calculation_node(invert_node->child, context)) return InvertCalculationNode::create(child_as_node.release_nonnull()); return nullptr; }, [this, &context](NonnullOwnPtr const& negate_node) -> RefPtr { if (auto child_as_node = convert_to_calculation_node(negate_node->child, context)) return NegateCalculationNode::create(child_as_node.release_nonnull()); return nullptr; }, [this, &context](NonnullRawPtr const& component_value) -> RefPtr { // NOTE: This is the "process the leaf nodes" part of step 5 of https://drafts.csswg.org/css-values-4/#parse-a-calculation // We divert a little from the spec: Rather than modify an existing tree of values, we construct a new one from that source tree. // This lets us make CalculationNodes immutable. // 1. If leaf is a parenthesized simple block, replace leaf with the result of parsing a calculation from leaf’s contents. if (component_value->is_block() && component_value->block().is_paren()) { auto leaf_calculation = parse_a_calculation(component_value->block().value, context); if (!leaf_calculation) return nullptr; return leaf_calculation.release_nonnull(); } // 2. If leaf is a math function, replace leaf with the internal representation of that math function. // NOTE: All function tokens at this point should be math functions. if (component_value->is_function()) { auto const& function = component_value->function(); auto leaf_calculation = parse_a_calc_function_node(function, context); if (!leaf_calculation) return nullptr; return leaf_calculation.release_nonnull(); } // AD-HOC: We also need to convert tokens into their numeric types. if (component_value->is(Token::Type::Ident)) { auto maybe_keyword = keyword_from_string(component_value->token().ident()); if (!maybe_keyword.has_value()) return nullptr; return NumericCalculationNode::from_keyword(*maybe_keyword, context); } if (component_value->is(Token::Type::Number)) return NumericCalculationNode::create(component_value->token().number(), context); if (component_value->is(Token::Type::Dimension)) { auto numeric_value = component_value->token().dimension_value(); auto unit_string = component_value->token().dimension_unit(); if (auto length_type = Length::unit_from_name(unit_string); length_type.has_value()) return NumericCalculationNode::create(Length { numeric_value, length_type.release_value() }, context); if (auto angle_type = Angle::unit_from_name(unit_string); angle_type.has_value()) return NumericCalculationNode::create(Angle { numeric_value, angle_type.release_value() }, context); if (auto flex_type = Flex::unit_from_name(unit_string); flex_type.has_value()) { // https://www.w3.org/TR/css3-grid-layout/#fr-unit // NOTE: values are not s (nor are they compatible with s, like some values), // so they cannot be represented in or combined with other unit types in calc() expressions. // FIXME: Flex is allowed in calc(), so figure out what this spec text means and how to implement it. dbgln_if(CSS_PARSER_DEBUG, "Rejecting in calc()"); return nullptr; } if (auto frequency_type = Frequency::unit_from_name(unit_string); frequency_type.has_value()) return NumericCalculationNode::create(Frequency { numeric_value, frequency_type.release_value() }, context); if (auto resolution_type = Resolution::unit_from_name(unit_string); resolution_type.has_value()) return NumericCalculationNode::create(Resolution { numeric_value, resolution_type.release_value() }, context); if (auto time_type = Time::unit_from_name(unit_string); time_type.has_value()) return NumericCalculationNode::create(Time { numeric_value, time_type.release_value() }, context); dbgln_if(CSS_PARSER_DEBUG, "Unrecognized dimension type in calc() expression: {}", component_value->to_string()); return nullptr; } if (component_value->is(Token::Type::Percentage)) return NumericCalculationNode::create(Percentage { component_value->token().percentage() }, context); // NOTE: If we get here, then we have a ComponentValue that didn't get replaced with something else, // so the calc() is invalid. dbgln_if(CSS_PARSER_DEBUG, "Leftover ComponentValue in calc tree! That probably means the syntax is invalid, but maybe we just didn't implement `{}` yet.", component_value->to_debug_string()); return nullptr; }, [](CalcParsing::Operator const& op) -> RefPtr { dbgln_if(CSS_PARSER_DEBUG, "Leftover Operator {} in calc tree!", op.delim); return nullptr; }); } // https://drafts.csswg.org/css-values-4/#parse-a-calculation RefPtr Parser::parse_a_calculation(Vector const& original_values, CalculationContext const& context) { // 1. Discard any s from values. // 2. An item in values is an “operator” if it’s a with the value "+", "-", "*", or "/". Otherwise, it’s a “value”. Vector values; for (auto const& value : original_values) { if (value.is(Token::Type::Whitespace)) continue; if (value.is(Token::Type::Delim)) { if (first_is_one_of(value.token().delim(), static_cast('+'), static_cast('-'), static_cast('*'), static_cast('/'))) { // NOTE: Sequential operators are invalid syntax. if (!values.is_empty() && values.last().has()) return nullptr; values.append(CalcParsing::Operator { static_cast(value.token().delim()) }); continue; } } values.append(NonnullRawPtr { value }); } // If we have no values, the syntax is invalid. if (values.is_empty()) return nullptr; // NOTE: If the first or last value is an operator, the syntax is invalid. if (values.first().has() || values.last().has()) return nullptr; // 3. Collect children into Product and Invert nodes. // For every consecutive run of value items in values separated by "*" or "/" operators: while (true) { Optional first_product_operator = values.find_first_index_if([](auto const& item) { return item.template has() && first_is_one_of(item.template get().delim, '*', '/'); }); if (!first_product_operator.has_value()) break; auto start_of_run = first_product_operator.value() - 1; auto end_of_run = first_product_operator.value() + 1; for (auto i = start_of_run + 1; i < values.size(); i += 2) { auto& item = values[i]; if (!item.has()) { end_of_run = i - 1; break; } auto delim = item.get().delim; if (!first_is_one_of(delim, '*', '/')) { end_of_run = i - 1; break; } } // 1. For each "/" operator in the run, replace its right-hand value item rhs with an Invert node containing rhs as its child. Vector run_values; run_values.append(move(values[start_of_run])); for (auto i = start_of_run + 1; i <= end_of_run; i += 2) { auto& operator_ = values[i].get().delim; auto& rhs = values[i + 1]; if (operator_ == '/') { run_values.append(make(move(rhs))); continue; } VERIFY(operator_ == '*'); run_values.append(move(rhs)); } // 2. Replace the entire run with a Product node containing the value items of the run as its children. values.remove(start_of_run, end_of_run - start_of_run + 1); values.insert(start_of_run, make(move(run_values))); } // 4. Collect children into Sum and Negate nodes. Optional single_value; { // 1. For each "-" operator item in values, replace its right-hand value item rhs with a Negate node containing rhs as its child. for (auto i = 0u; i < values.size(); ++i) { auto& maybe_minus_operator = values[i]; if (!maybe_minus_operator.has() || maybe_minus_operator.get().delim != '-') continue; auto rhs_index = ++i; auto negate_node = make(move(values[rhs_index])); values.remove(rhs_index); values.insert(rhs_index, move(negate_node)); } // 2. If values has only one item, and it is a Product node or a parenthesized simple block, replace values with that item. if (values.size() == 1) { values.first().visit( [&](ComponentValue const& component_value) { if (component_value.is_block() && component_value.block().is_paren()) single_value = NonnullRawPtr { component_value }; }, [&](NonnullOwnPtr& node) { single_value = move(node); }, [](auto&) {}); } // Otherwise, replace values with a Sum node containing the value items of values as its children. if (!single_value.has_value()) { values.remove_all_matching([](CalcParsing::Node& value) { return value.has(); }); single_value = make(move(values)); } } VERIFY(single_value.has_value()); // 5. At this point values is a tree of Sum, Product, Negate, and Invert nodes, with other types of values at the leaf nodes. Process the leaf nodes. // NOTE: We process leaf nodes as part of this conversion. auto calculation_tree = convert_to_calculation_node(*single_value, context); if (!calculation_tree) return nullptr; // 6. Return the result of simplifying a calculation tree from values. return simplify_a_calculation_tree(*calculation_tree, context, CalculationResolutionContext {}); } // https://drafts.csswg.org/css-fonts/#typedef-opentype-tag RefPtr Parser::parse_opentype_tag_value(TokenStream& tokens) { // = // The is a case-sensitive OpenType feature tag. // As specified in the OpenType specification [OPENTYPE], feature tags contain four ASCII characters. // Tag strings longer or shorter than four characters, or containing characters outside the U+20–7E codepoint range are invalid. auto transaction = tokens.begin_transaction(); auto string_value = parse_string_value(tokens); if (string_value == nullptr) return nullptr; auto string = string_value->string_value().bytes_as_string_view(); if (string.length() != 4) return nullptr; for (char c : string) { if (c < 0x20 || c > 0x7E) return nullptr; } transaction.commit(); return string_value; } NonnullRefPtr Parser::resolve_unresolved_style_value(ParsingParams const& context, DOM::Element& element, Optional pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) { // Unresolved always contains a var() or attr(), unless it is a custom property's value, in which case we shouldn't be trying // to produce a different CSSStyleValue from it. VERIFY(unresolved.contains_var_or_attr()); // If the value is invalid, we fall back to `unset`: https://www.w3.org/TR/css-variables-1/#invalid-at-computed-value-time auto parser = Parser::create(context, ""sv); return parser.resolve_unresolved_style_value(element, pseudo_element, property_id, unresolved); } class PropertyDependencyNode : public RefCounted { public: static NonnullRefPtr create(FlyString name) { return adopt_ref(*new PropertyDependencyNode(move(name))); } void add_child(NonnullRefPtr new_child) { for (auto const& child : m_children) { if (child->m_name == new_child->m_name) return; } // We detect self-reference already. VERIFY(new_child->m_name != m_name); m_children.append(move(new_child)); } bool has_cycles() { if (m_marked) return true; TemporaryChange change { m_marked, true }; for (auto& child : m_children) { if (child->has_cycles()) return true; } return false; } private: explicit PropertyDependencyNode(FlyString name) : m_name(move(name)) { } FlyString m_name; Vector> m_children; bool m_marked { false }; }; NonnullRefPtr Parser::resolve_unresolved_style_value(DOM::Element& element, Optional pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved) { TokenStream unresolved_values_without_variables_expanded { unresolved.values() }; Vector values_with_variables_expanded; HashMap> dependencies; ScopeGuard mark_element_if_uses_custom_properties = [&] { for (auto const& name : dependencies.keys()) { if (is_a_custom_property_name_string(name)) { element.set_style_uses_css_custom_properties(true); return; } } }; if (!expand_variables(element, pseudo_element, string_from_property_id(property_id), dependencies, unresolved_values_without_variables_expanded, values_with_variables_expanded)) return CSSKeywordValue::create(Keyword::Unset); TokenStream unresolved_values_with_variables_expanded { values_with_variables_expanded }; Vector expanded_values; if (!expand_unresolved_values(element, string_from_property_id(property_id), unresolved_values_with_variables_expanded, expanded_values)) return CSSKeywordValue::create(Keyword::Unset); auto expanded_value_tokens = TokenStream { expanded_values }; if (auto parsed_value = parse_css_value(property_id, expanded_value_tokens); !parsed_value.is_error()) return parsed_value.release_value(); return CSSKeywordValue::create(Keyword::Unset); } static RefPtr get_custom_property(DOM::Element const& element, Optional pseudo_element, FlyString const& custom_property_name) { if (pseudo_element.has_value()) { if (auto it = element.custom_properties(pseudo_element).find(custom_property_name); it != element.custom_properties(pseudo_element).end()) return it->value.value; } for (auto const* current_element = &element; current_element; current_element = current_element->parent_or_shadow_host_element()) { if (auto it = current_element->custom_properties({}).find(custom_property_name); it != current_element->custom_properties({}).end()) return it->value.value; } return nullptr; } bool Parser::expand_variables(DOM::Element& element, Optional pseudo_element, FlyString const& property_name, HashMap>& dependencies, TokenStream& source, Vector& dest) { // Arbitrary large value chosen to avoid the billion-laughs attack. // https://www.w3.org/TR/css-variables-1/#long-variables size_t const MAX_VALUE_COUNT = 16384; if (source.remaining_token_count() + dest.size() > MAX_VALUE_COUNT) { dbgln("Stopped expanding CSS variables: maximum length reached."); return false; } auto get_dependency_node = [&](FlyString const& name) -> NonnullRefPtr { if (auto existing = dependencies.get(name); existing.has_value()) return *existing.value(); auto new_node = PropertyDependencyNode::create(name); dependencies.set(name, new_node); return new_node; }; while (source.has_next_token()) { auto const& value = source.consume_a_token(); if (value.is_block()) { auto const& source_block = value.block(); Vector block_values; TokenStream source_block_contents { source_block.value }; if (!expand_variables(element, pseudo_element, property_name, dependencies, source_block_contents, block_values)) return false; dest.empend(SimpleBlock { source_block.token, move(block_values) }); continue; } if (!value.is_function()) { dest.empend(value.token()); continue; } if (!value.function().name.equals_ignoring_ascii_case("var"sv)) { auto const& source_function = value.function(); Vector function_values; TokenStream source_function_contents { source_function.value }; if (!expand_variables(element, pseudo_element, property_name, dependencies, source_function_contents, function_values)) return false; dest.empend(Function { source_function.name, move(function_values) }); continue; } TokenStream var_contents { value.function().value }; var_contents.discard_whitespace(); if (!var_contents.has_next_token()) return false; auto const& custom_property_name_token = var_contents.consume_a_token(); if (!custom_property_name_token.is(Token::Type::Ident)) return false; auto custom_property_name = custom_property_name_token.token().ident(); if (!custom_property_name.bytes_as_string_view().starts_with("--"sv)) return false; // Detect dependency cycles. https://www.w3.org/TR/css-variables-1/#cycles // We do not do this by the spec, since we are not keeping a graph of var dependencies around, // but rebuilding it every time. if (custom_property_name == property_name) return false; auto parent = get_dependency_node(property_name); auto child = get_dependency_node(custom_property_name); parent->add_child(child); if (parent->has_cycles()) return false; if (auto custom_property_value = get_custom_property(element, pseudo_element, custom_property_name)) { VERIFY(custom_property_value->is_unresolved()); TokenStream custom_property_tokens { custom_property_value->as_unresolved().values() }; auto dest_size_before = dest.size(); if (!expand_variables(element, pseudo_element, custom_property_name, dependencies, custom_property_tokens, dest)) return false; // If the size of dest has increased, then the custom property is not the initial guaranteed-invalid value. // If it hasn't increased, then it is the initial guaranteed-invalid value, and thus we should move on to the fallback value. if (dest_size_before < dest.size()) continue; dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expanding custom property '{}' did not return any tokens, treating it as invalid and moving on to the fallback value.", custom_property_name); } // Use the provided fallback value, if any. var_contents.discard_whitespace(); if (var_contents.has_next_token()) { auto const& comma_token = var_contents.consume_a_token(); if (!comma_token.is(Token::Type::Comma)) return false; var_contents.discard_whitespace(); if (!expand_variables(element, pseudo_element, property_name, dependencies, var_contents, dest)) return false; } } return true; } bool Parser::expand_unresolved_values(DOM::Element& element, FlyString const& property_name, TokenStream& source, Vector& dest) { while (source.has_next_token()) { auto const& value = source.consume_a_token(); if (value.is_function()) { if (value.function().name.equals_ignoring_ascii_case("attr"sv)) { if (!substitute_attr_function(element, property_name, value.function(), dest)) return false; continue; } auto const& source_function = value.function(); Vector function_values; TokenStream source_function_contents { source_function.value }; if (!expand_unresolved_values(element, property_name, source_function_contents, function_values)) return false; dest.empend(Function { source_function.name, move(function_values) }); continue; } if (value.is_block()) { auto const& source_block = value.block(); TokenStream source_block_values { source_block.value }; Vector block_values; if (!expand_unresolved_values(element, property_name, source_block_values, block_values)) return false; dest.empend(SimpleBlock { source_block.token, move(block_values) }); continue; } dest.empend(value.token()); } return true; } // https://drafts.csswg.org/css-values-5/#attr-substitution bool Parser::substitute_attr_function(DOM::Element& element, FlyString const& property_name, Function const& attr_function, Vector& dest) { // First, parse the arguments to attr(): // attr() = attr( ? , ?) // = string | url | ident | color | number | percentage | length | angle | time | frequency | flex | TokenStream attr_contents { attr_function.value }; attr_contents.discard_whitespace(); if (!attr_contents.has_next_token()) return false; // - Attribute name // FIXME: Support optional attribute namespace if (!attr_contents.next_token().is(Token::Type::Ident)) return false; auto attribute_name = attr_contents.consume_a_token().token().ident(); attr_contents.discard_whitespace(); // - Attribute type (optional) auto attribute_type = "string"_fly_string; if (attr_contents.next_token().is(Token::Type::Ident)) { attribute_type = attr_contents.consume_a_token().token().ident(); attr_contents.discard_whitespace(); } // - Comma, then fallback values (optional) bool has_fallback_values = false; if (attr_contents.has_next_token()) { if (!attr_contents.next_token().is(Token::Type::Comma)) return false; (void)attr_contents.consume_a_token(); // Comma has_fallback_values = true; } // Then, run the substitution algorithm: // 1. If the attr() function has a substitution value, replace the attr() function by the substitution value. // https://drafts.csswg.org/css-values-5/#attr-types if (element.has_attribute(attribute_name)) { auto parse_string_as_component_value = [this](String const& string) { auto tokens = Tokenizer::tokenize(string, "utf-8"sv); TokenStream stream { tokens }; return parse_a_component_value(stream); }; auto attribute_value = element.get_attribute_value(attribute_name); if (attribute_type.equals_ignoring_ascii_case("angle"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a whose unit matches the given type, the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Dimension)) { if (Angle::unit_from_name(component_value->token().dimension_unit()).has_value()) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("color"_fly_string)) { // Parse a component value from the attribute’s value. // If the result is a or a named color ident, the substitution value is that result as a . // Otherwise there is no substitution value. auto component_value = parse_string_as_component_value(attribute_value); if (component_value.has_value()) { if ((component_value->is(Token::Type::Hash) && Color::from_string(MUST(String::formatted("#{}", component_value->token().hash_value()))).has_value()) || (component_value->is(Token::Type::Ident) && Color::from_string(component_value->token().ident()).has_value())) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("flex"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a whose unit matches the given type, the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Dimension)) { if (Flex::unit_from_name(component_value->token().dimension_unit()).has_value()) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("frequency"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a whose unit matches the given type, the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Dimension)) { if (Frequency::unit_from_name(component_value->token().dimension_unit()).has_value()) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("ident"_fly_string)) { // The substitution value is a CSS , whose value is the literal value of the attribute, // with leading and trailing ASCII whitespace stripped. (No CSS parsing of the value is performed.) // If the attribute value, after trimming, is the empty string, there is instead no substitution value. // If the ’s value is a CSS-wide keyword or `default`, there is instead no substitution value. auto substitution_value = MUST(attribute_value.trim(Infra::ASCII_WHITESPACE)); if (!substitution_value.is_empty() && !substitution_value.equals_ignoring_ascii_case("default"sv) && !is_css_wide_keyword(substitution_value)) { dest.empend(Token::create_ident(substitution_value)); return true; } } else if (attribute_type.equals_ignoring_ascii_case("length"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a whose unit matches the given type, the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Dimension)) { if (Length::unit_from_name(component_value->token().dimension_unit()).has_value()) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("number"_fly_string)) { // Parse a component value from the attribute’s value. // If the result is a , the result is the substitution value. // Otherwise, there is no substitution value. auto component_value = parse_string_as_component_value(attribute_value); if (component_value.has_value() && component_value->is(Token::Type::Number)) { dest.append(component_value.release_value()); return true; } } else if (attribute_type.equals_ignoring_ascii_case("percentage"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a , the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Percentage)) { dest.append(component_value.release_value()); return true; } } else if (attribute_type.equals_ignoring_ascii_case("string"_fly_string)) { // The substitution value is a CSS string, whose value is the literal value of the attribute. // (No CSS parsing or "cleanup" of the value is performed.) // No value triggers fallback. dest.empend(Token::create_string(attribute_value)); return true; } else if (attribute_type.equals_ignoring_ascii_case("time"_fly_string)) { // Parse a component value from the attribute’s value. auto component_value = parse_string_as_component_value(attribute_value); // If the result is a whose unit matches the given type, the result is the substitution value. // Otherwise, there is no substitution value. if (component_value.has_value() && component_value->is(Token::Type::Dimension)) { if (Time::unit_from_name(component_value->token().dimension_unit()).has_value()) { dest.append(component_value.release_value()); return true; } } } else if (attribute_type.equals_ignoring_ascii_case("url"_fly_string)) { // The substitution value is a CSS value, whose url is the literal value of the attribute. // (No CSS parsing or "cleanup" of the value is performed.) // No value triggers fallback. dest.empend(Token::create_url(attribute_value)); return true; } else { // Dimension units // Parse a component value from the attribute’s value. // If the result is a , the substitution value is a dimension with the result’s value, and the given unit. // Otherwise, there is no substitution value. auto component_value = parse_string_as_component_value(attribute_value); if (component_value.has_value() && component_value->is(Token::Type::Number)) { if (attribute_value == "%"sv) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else if (auto angle_unit = Angle::unit_from_name(attribute_type); angle_unit.has_value()) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else if (auto flex_unit = Flex::unit_from_name(attribute_type); flex_unit.has_value()) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else if (auto frequency_unit = Frequency::unit_from_name(attribute_type); frequency_unit.has_value()) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else if (auto length_unit = Length::unit_from_name(attribute_type); length_unit.has_value()) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else if (auto time_unit = Time::unit_from_name(attribute_type); time_unit.has_value()) { dest.empend(Token::create_dimension(component_value->token().number_value(), attribute_type)); return true; } else { // Not a dimension unit. return false; } } } } // 2. Otherwise, if the attr() function has a fallback value as its last argument, replace the attr() function by the fallback value. // If there are any var() or attr() references in the fallback, substitute them as well. if (has_fallback_values) return expand_unresolved_values(element, property_name, attr_contents, dest); if (attribute_type.equals_ignoring_ascii_case("string"_fly_string)) { // If the argument is string, defaults to the empty string if omitted dest.empend(Token::create_string({})); return true; } // 3. Otherwise, the property containing the attr() function is invalid at computed-value time. return false; } }