From 0b52ce469d045c1789ce4729f747c59806d7876b Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Mon, 30 Dec 2024 17:24:39 +0000 Subject: [PATCH] WIP: Replace Parser "current property" with a stack of contexts TODO: Add a helper for temporarily adding a context! TODO: Add contexts when parsing a longhand from a shorthand? This lets us disallow quirks inside functions, like we're supposed to. This also lays the groundwork for being able to more easily determine what type a percentage inside a calculation should become, as this is based on the same stack of contexts. --- Libraries/LibWeb/CSS/Parser/Parser.cpp | 230 ++++++++++++++----- Libraries/LibWeb/CSS/Parser/Parser.h | 6 + Libraries/LibWeb/CSS/Parser/ParsingContext.h | 6 +- 3 files changed, 175 insertions(+), 67 deletions(-) diff --git a/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Libraries/LibWeb/CSS/Parser/Parser.cpp index 4cb2deac739c9..b1b769d71df91 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -1933,6 +1933,9 @@ RefPtr Parser::parse_calculated_value(ComponentValue const OwnPtr Parser::parse_a_calc_function_node(Function const& function) { + m_value_context.append(FunctionContext { function.name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + if (function.name.equals_ignoring_ascii_case("calc"sv)) return parse_a_calculation(function.value); @@ -1974,12 +1977,33 @@ Optional Parser::parse_dimension(ComponentValue const& component_valu auto numeric_value = component_value.token().number_value(); if (numeric_value == 0) return Length::make_px(0); - if (m_context.in_quirks_mode() && property_has_quirk(m_context.current_property_id(), Quirk::UnitlessLength)) { - // https://quirks.spec.whatwg.org/#quirky-length-value - // FIXME: Disallow quirk when inside a CSS sub-expression (like `calc()`) - // "The value must not be supported in arguments to CSS expressions other than the rect() - // expression, and must not be supported in the supports() static method of the CSS interface." - return Length::make_px(CSSPixels::nearest_value_for(numeric_value)); + + if (m_context.in_quirks_mode()) { + // https://drafts.csswg.org/css-values-4/#deprecated-quirky-length + // "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 calc(), except that they must be allowed in rect() in the clip property." + + // So, it must be allowed in the top-level ValueParsingContext, and then not disallowed by any child contexts. + + Optional top_level_property; + if (!m_value_context.is_empty()) { + top_level_property = m_value_context.first().visit( + [](PropertyID const& property_id) -> Optional { return property_id; }, + [](auto const&) -> Optional { return OptionalNone {}; }); + } + + bool unitless_length_allowed = top_level_property.has_value() && property_has_quirk(top_level_property.value(), Quirk::UnitlessLength); + for (auto i = 1u; i < m_value_context.size() && unitless_length_allowed; i++) { + unitless_length_allowed = m_value_context[i].visit( + [](PropertyID const& property_id) { return property_has_quirk(property_id, Quirk::UnitlessLength); }, + [top_level_property](FunctionContext const& function_context) { + return function_context.name == "rect"sv && top_level_property == PropertyID::Clip; + }); + } + if (unitless_length_allowed) + return Length::make_px(CSSPixels::nearest_value_for(numeric_value)); } } @@ -2844,6 +2868,9 @@ RefPtr Parser::parse_rect_value(TokenStream& toke if (!function_token.is_function("rect"sv)) return nullptr; + m_value_context.append(FunctionContext { "rect"sv }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + Vector params; auto argument_tokens = TokenStream { function_token.function().value }; @@ -2991,6 +3018,9 @@ RefPtr Parser::parse_rgb_color_value(TokenStream& if (!function_token.is_function("rgb"sv) && !function_token.is_function("rgba"sv)) return {}; + m_value_context.append(FunctionContext { function_token.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + RefPtr red; RefPtr green; RefPtr blue; @@ -3113,6 +3143,9 @@ RefPtr Parser::parse_hsl_color_value(TokenStream& if (!function_token.is_function("hsl"sv) && !function_token.is_function("hsla"sv)) return {}; + m_value_context.append(FunctionContext { function_token.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + RefPtr h; RefPtr s; RefPtr l; @@ -3214,6 +3247,9 @@ RefPtr Parser::parse_hwb_color_value(TokenStream& if (!function_token.is_function("hwb"sv)) return {}; + m_value_context.append(FunctionContext { function_token.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + RefPtr h; RefPtr w; RefPtr b; @@ -3447,6 +3483,9 @@ RefPtr Parser::parse_color_function(TokenStream& if (!function_token.is_function("color"sv)) return {}; + m_value_context.append(FunctionContext { function_token.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + auto inner_tokens = TokenStream { function_token.function().value }; inner_tokens.discard_whitespace(); @@ -3546,66 +3585,89 @@ RefPtr Parser::parse_color_value(TokenStream& tok return {}; } - // https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk - if (m_context.in_quirks_mode() && property_has_quirk(m_context.current_property_id(), Quirk::HashlessHexColor)) { - // 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 {}; + // https://drafts.csswg.org/css-color-4/#quirky-color + if (m_context.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()" - // 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()); - } + 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; }); } - // 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(); + 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 {}; - // 4. If serialization does not consist of three or six characters, return an error. - if (serialization.bytes().size() != 3 && serialization.bytes().size() != 6) - 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 {}; - // 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'))) + // 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 {}; - } - // 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()); + // 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()); + } } } @@ -3661,6 +3723,10 @@ RefPtr Parser::parse_counter_value(TokenStream& t if (token.is_function("counter"sv)) { // counter() = counter( , ? ) auto& function = token.function(); + + m_value_context.append(FunctionContext { function.name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + 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) @@ -3689,6 +3755,10 @@ RefPtr Parser::parse_counter_value(TokenStream& t if (token.is_function("counters"sv)) { // counters() = counters( , , ? ) auto& function = token.function(); + + m_value_context.append(FunctionContext { function.name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + 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) @@ -5565,6 +5635,10 @@ RefPtr Parser::parse_filter_value_list_value(TokenStream Parser::parse_font_face_src(TokenStream& compo auto const& function = maybe_function.function(); if (function.name.equals_ignoring_ascii_case("format"sv)) { + m_value_context.append(FunctionContext { function.name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + TokenStream format_tokens { function.value }; format_tokens.discard_whitespace(); auto const& format_name_token = format_tokens.consume_a_token(); @@ -6731,6 +6808,9 @@ RefPtr Parser::parse_math_depth_value(TokenStream // add() if (token.is_function("add"sv)) { + m_value_context.append(FunctionContext { token.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + auto add_tokens = TokenStream { token.function().value }; add_tokens.discard_whitespace(); auto const& integer_token = add_tokens.consume_a_token(); @@ -7021,6 +7101,10 @@ RefPtr Parser::parse_easing_value(TokenStream& to argument.remove_all_matching([](auto& value) { return value.is(Token::Type::Whitespace); }); auto name = part.function().name; + + m_value_context.append(FunctionContext { name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + if (name.equals_ignoring_ascii_case("linear"sv)) { // linear() = linear( [ && {0,2} ]# ) Vector stops; @@ -7171,6 +7255,10 @@ RefPtr Parser::parse_transform_value(TokenStream& auto maybe_function = transform_function_from_string(part.function().name); if (!maybe_function.has_value()) return nullptr; + + m_value_context.append(FunctionContext { part.function().name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + auto function = maybe_function.release_value(); auto function_metadata = transform_function_metadata(function); @@ -7739,6 +7827,10 @@ Optional Parser::parse_track_sizing_function(ComponentVa { if (token.is_function()) { auto const& function_token = token.function(); + + m_value_context.append(FunctionContext { function_token.name }); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + if (function_token.name.equals_ignoring_ascii_case("repeat"sv)) { auto maybe_repeat = parse_repeat(function_token.value); if (maybe_repeat.has_value()) @@ -8354,7 +8446,9 @@ bool block_contains_var_or_attr(SimpleBlock const& block) Parser::ParseErrorOr> Parser::parse_css_value(PropertyID property_id, TokenStream& unprocessed_tokens, Optional original_source_text) { - m_context.set_current_property_id(property_id); + m_value_context.append(property_id); + ScopeGuard leave_context { [&] { m_value_context.take_last(); } }; + Vector component_values; bool contains_var_or_attr = false; bool const property_accepts_custom_ident = property_accepts_type(property_id, ValueType::CustomIdent); @@ -9180,8 +9274,20 @@ OwnPtr Parser::convert_to_calculation_node(CalcParsing::Node co if (dimension.is_length()) return NumericCalculationNode::create(dimension.length()); if (dimension.is_percentage()) { - // FIXME: Figure this out in non-property contexts - auto percentage_resolved_type = property_resolves_percentages_relative_to(m_context.current_property_id()); + // Determine what type the percentage should be resolved as, if anything. + Optional percentage_resolved_type; + for (auto const& value_context : m_value_context.in_reverse()) { + percentage_resolved_type = value_context.visit( + [](PropertyID property_id) -> Optional { + return property_resolves_percentages_relative_to(property_id); + }, + [](FunctionContext const&) -> Optional { + // FIXME: Some functions provide this. The spec mentions `media-progress()` as an example. + return {}; + }); + if (percentage_resolved_type.has_value()) + break; + } return NumericCalculationNode::create(dimension.percentage(), percentage_resolved_type); } if (dimension.is_resolution()) diff --git a/Libraries/LibWeb/CSS/Parser/Parser.h b/Libraries/LibWeb/CSS/Parser/Parser.h index c1204dbe63b63..8c6e3760d6180 100644 --- a/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Libraries/LibWeb/CSS/Parser/Parser.h @@ -442,6 +442,12 @@ class Parser { Vector m_tokens; TokenStream m_token_stream; + struct FunctionContext { + StringView name; + }; + using ValueParsingContext = Variant; + Vector m_value_context; + enum class ContextType { Unknown, Style, diff --git a/Libraries/LibWeb/CSS/Parser/ParsingContext.h b/Libraries/LibWeb/CSS/Parser/ParsingContext.h index 6783db1247d84..131f37d86fe85 100644 --- a/Libraries/LibWeb/CSS/Parser/ParsingContext.h +++ b/Libraries/LibWeb/CSS/Parser/ParsingContext.h @@ -1,6 +1,6 @@ /* * Copyright (c) 2020-2021, the SerenityOS developers. - * Copyright (c) 2021-2023, Sam Atkins + * Copyright (c) 2021-2024, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -34,9 +34,6 @@ class ParsingContext { HTML::Window const* window() const; URL::URL complete_url(StringView) const; - PropertyID current_property_id() const { return m_current_property_id; } - void set_current_property_id(PropertyID property_id) { m_current_property_id = property_id; } - JS::Realm& realm() const { VERIFY(m_realm); @@ -46,7 +43,6 @@ class ParsingContext { private: GC::Ptr m_realm; GC::Ptr m_document; - PropertyID m_current_property_id { PropertyID::Invalid }; URL::URL m_url; Mode m_mode { Mode::Normal }; };