Skip to content

Commit

Permalink
WIP: Replace Parser "current property" with a stack of contexts
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
AtkinsSJ committed Jan 3, 2025
1 parent a24718c commit 0b52ce4
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 67 deletions.
230 changes: 168 additions & 62 deletions Libraries/LibWeb/CSS/Parser/Parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1933,6 +1933,9 @@ RefPtr<CalculatedStyleValue> Parser::parse_calculated_value(ComponentValue const

OwnPtr<CalculationNode> 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);

Expand Down Expand Up @@ -1974,12 +1977,33 @@ Optional<Dimension> 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 <quirky-length> 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, <quirky-length> is a type of <length> 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<PropertyID> top_level_property;
if (!m_value_context.is_empty()) {
top_level_property = m_value_context.first().visit(
[](PropertyID const& property_id) -> Optional<PropertyID> { return property_id; },
[](auto const&) -> Optional<PropertyID> { 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));
}
}

Expand Down Expand Up @@ -2844,6 +2868,9 @@ RefPtr<CSSStyleValue> Parser::parse_rect_value(TokenStream<ComponentValue>& 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<Length, 4> params;
auto argument_tokens = TokenStream { function_token.function().value };

Expand Down Expand Up @@ -2991,6 +3018,9 @@ RefPtr<CSSStyleValue> Parser::parse_rgb_color_value(TokenStream<ComponentValue>&
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<CSSStyleValue> red;
RefPtr<CSSStyleValue> green;
RefPtr<CSSStyleValue> blue;
Expand Down Expand Up @@ -3113,6 +3143,9 @@ RefPtr<CSSStyleValue> Parser::parse_hsl_color_value(TokenStream<ComponentValue>&
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<CSSStyleValue> h;
RefPtr<CSSStyleValue> s;
RefPtr<CSSStyleValue> l;
Expand Down Expand Up @@ -3214,6 +3247,9 @@ RefPtr<CSSStyleValue> Parser::parse_hwb_color_value(TokenStream<ComponentValue>&
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<CSSStyleValue> h;
RefPtr<CSSStyleValue> w;
RefPtr<CSSStyleValue> b;
Expand Down Expand Up @@ -3447,6 +3483,9 @@ RefPtr<CSSStyleValue> Parser::parse_color_function(TokenStream<ComponentValue>&
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();

Expand Down Expand Up @@ -3546,66 +3585,89 @@ RefPtr<CSSStyleValue> Parser::parse_color_value(TokenStream<ComponentValue>& 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 <number-token> or a <dimension-token>, 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, <quirky-color> is a type of <color> 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 <dimension-token>, 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 <ident-token>; 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 <number-token> or a <dimension-token>, 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 <dimension-token>, 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 <ident-token>; 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());
}
}
}

Expand Down Expand Up @@ -3661,6 +3723,10 @@ RefPtr<CSSStyleValue> Parser::parse_counter_value(TokenStream<ComponentValue>& t
if (token.is_function("counter"sv)) {
// counter() = counter( <counter-name>, <counter-style>? )
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)
Expand Down Expand Up @@ -3689,6 +3755,10 @@ RefPtr<CSSStyleValue> Parser::parse_counter_value(TokenStream<ComponentValue>& t
if (token.is_function("counters"sv)) {
// counters() = counters( <counter-name>, <string>, <counter-style>? )
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)
Expand Down Expand Up @@ -5565,6 +5635,10 @@ RefPtr<CSSStyleValue> Parser::parse_filter_value_list_value(TokenStream<Componen
auto filter_token = parse_filter_function_name(token.function().name);
if (!filter_token.has_value())
return nullptr;

m_value_context.append(FunctionContext { token.function().name });
ScopeGuard leave_context { [&] { m_value_context.take_last(); } };

auto filter_function = parse_filter_function(*filter_token, token.function().value);
if (!filter_function.has_value())
return nullptr;
Expand Down Expand Up @@ -6570,6 +6644,9 @@ Vector<ParsedFontFace::Source> Parser::parse_font_face_src(TokenStream<T>& 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();
Expand Down Expand Up @@ -6731,6 +6808,9 @@ RefPtr<CSSStyleValue> Parser::parse_math_depth_value(TokenStream<ComponentValue>

// add(<integer>)
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();
Expand Down Expand Up @@ -7021,6 +7101,10 @@ RefPtr<CSSStyleValue> Parser::parse_easing_value(TokenStream<ComponentValue>& 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( [ <number> && <percentage>{0,2} ]# )
Vector<EasingStyleValue::Linear::Stop> stops;
Expand Down Expand Up @@ -7171,6 +7255,10 @@ RefPtr<CSSStyleValue> Parser::parse_transform_value(TokenStream<ComponentValue>&
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);

Expand Down Expand Up @@ -7739,6 +7827,10 @@ Optional<CSS::ExplicitGridTrack> 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())
Expand Down Expand Up @@ -8354,7 +8446,9 @@ bool block_contains_var_or_attr(SimpleBlock const& block)

Parser::ParseErrorOr<NonnullRefPtr<CSSStyleValue>> Parser::parse_css_value(PropertyID property_id, TokenStream<ComponentValue>& unprocessed_tokens, Optional<String> 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<ComponentValue> component_values;
bool contains_var_or_attr = false;
bool const property_accepts_custom_ident = property_accepts_type(property_id, ValueType::CustomIdent);
Expand Down Expand Up @@ -9180,8 +9274,20 @@ OwnPtr<CalculationNode> 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<ValueType> percentage_resolved_type;
for (auto const& value_context : m_value_context.in_reverse()) {
percentage_resolved_type = value_context.visit(
[](PropertyID property_id) -> Optional<ValueType> {
return property_resolves_percentages_relative_to(property_id);
},
[](FunctionContext const&) -> Optional<ValueType> {
// 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())
Expand Down
Loading

0 comments on commit 0b52ce4

Please sign in to comment.