1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 04:47:35 +00:00

LibWeb: Move UnresolvedStyleValue resolution into the CSS Parser

Resolving typed `attr()` functions is going to involve using more
internal Parser methods, so this is the simplest solution for that.
Also... resolving these is basically parsing them, so it makes more
sense for that process to live here.

This is just moving code, with minimal changes so it still works.
This commit is contained in:
Sam Atkins 2023-09-04 11:16:49 +01:00 committed by Andreas Kling
parent cab8b3e180
commit 7d10484660
4 changed files with 287 additions and 308 deletions

View file

@ -12,6 +12,7 @@
#include <AK/Debug.h>
#include <AK/GenericLexer.h>
#include <AK/SourceLocation.h>
#include <AK/TemporaryChange.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSKeyframeRule.h>
@ -6483,23 +6484,285 @@ bool Parser::has_ignored_vendor_prefix(StringView string)
return true;
}
RefPtr<CalculatedStyleValue> Parser::parse_calculated_value(Badge<StyleComputer>, ParsingContext const& context, ComponentValue const& token)
NonnullRefPtr<StyleValue> Parser::resolve_unresolved_style_value(Badge<StyleComputer>, ParsingContext const& context, DOM::Element& element, Optional<Selector::PseudoElement> pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved)
{
auto parser = MUST(Parser::create(context, ""sv));
return parser.parse_calculated_value(token);
}
// 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 StyleValue from it.
VERIFY(unresolved.contains_var_or_attr());
RefPtr<StyleValue> Parser::parse_css_value(Badge<StyleComputer>, ParsingContext const& context, PropertyID property_id, Vector<ComponentValue> const& tokens)
{
if (tokens.is_empty() || property_id == CSS::PropertyID::Invalid || property_id == CSS::PropertyID::Custom)
return nullptr;
// 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 = MUST(Parser::create(context, ""sv));
TokenStream<ComponentValue> token_stream { tokens };
auto result = parser.parse_css_value(property_id, token_stream);
if (result.is_error())
return nullptr;
return result.release_value();
return parser.resolve_unresolved_style_value(element, pseudo_element, property_id, unresolved);
}
class PropertyDependencyNode : public RefCounted<PropertyDependencyNode> {
public:
static NonnullRefPtr<PropertyDependencyNode> create(String name)
{
return adopt_ref(*new PropertyDependencyNode(move(name)));
}
void add_child(NonnullRefPtr<PropertyDependencyNode> 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(String name)
: m_name(move(name))
{
}
String m_name;
Vector<NonnullRefPtr<PropertyDependencyNode>> m_children;
bool m_marked { false };
};
NonnullRefPtr<StyleValue> Parser::resolve_unresolved_style_value(DOM::Element& element, Optional<Selector::PseudoElement> pseudo_element, PropertyID property_id, UnresolvedStyleValue const& unresolved)
{
TokenStream unresolved_values_without_variables_expanded { unresolved.values() };
Vector<ComponentValue> values_with_variables_expanded;
HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>> dependencies;
if (!expand_variables(element, pseudo_element, string_from_property_id(property_id), dependencies, unresolved_values_without_variables_expanded, values_with_variables_expanded))
return UnsetStyleValue::the();
TokenStream unresolved_values_with_variables_expanded { values_with_variables_expanded };
Vector<ComponentValue> expanded_values;
if (!expand_unresolved_values(element, string_from_property_id(property_id), unresolved_values_with_variables_expanded, expanded_values))
return UnsetStyleValue::the();
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 UnsetStyleValue::the();
}
static RefPtr<StyleValue const> get_custom_property(DOM::Element const& element, Optional<CSS::Selector::PseudoElement> pseudo_element, FlyString const& custom_property_name)
{
if (pseudo_element.has_value()) {
if (auto it = element.custom_properties(pseudo_element).find(custom_property_name.to_string().to_deprecated_string()); it != element.custom_properties(pseudo_element).end())
return it->value.value;
}
for (auto const* current_element = &element; current_element; current_element = current_element->parent_element()) {
if (auto it = current_element->custom_properties({}).find(custom_property_name.to_string().to_deprecated_string()); it != current_element->custom_properties({}).end())
return it->value.value;
}
return nullptr;
}
bool Parser::expand_variables(DOM::Element& element, Optional<Selector::PseudoElement> pseudo_element, StringView property_name, HashMap<FlyString, NonnullRefPtr<PropertyDependencyNode>>& dependencies, TokenStream<ComponentValue>& source, Vector<ComponentValue>& 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 name) -> NonnullRefPtr<PropertyDependencyNode> {
if (auto existing = dependencies.get(name); existing.has_value())
return *existing.value();
auto new_node = PropertyDependencyNode::create(name.to_string());
dependencies.set(name, new_node);
return new_node;
};
while (source.has_next_token()) {
auto const& value = source.next_token();
if (value.is_block()) {
auto const& source_block = value.block();
Vector<ComponentValue> block_values;
TokenStream source_block_contents { source_block.values() };
if (!expand_variables(element, pseudo_element, property_name, dependencies, source_block_contents, block_values))
return false;
NonnullRefPtr<Block> block = Block::create(source_block.token(), move(block_values));
dest.empend(block);
continue;
}
if (!value.is_function()) {
dest.empend(value);
continue;
}
if (!value.function().name().equals_ignoring_ascii_case("var"sv)) {
auto const& source_function = value.function();
Vector<ComponentValue> function_values;
TokenStream source_function_contents { source_function.values() };
if (!expand_variables(element, pseudo_element, property_name, dependencies, source_function_contents, function_values))
return false;
NonnullRefPtr<Function> function = Function::create(FlyString::from_utf8(source_function.name()).release_value_but_fixme_should_propagate_errors(), move(function_values));
dest.empend(function);
continue;
}
TokenStream var_contents { value.function().values() };
var_contents.skip_whitespace();
if (!var_contents.has_next_token())
return false;
auto const& custom_property_name_token = var_contents.next_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.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(FlyString::from_utf8(property_name).release_value_but_fixme_should_propagate_errors());
auto child = get_dependency_node(FlyString::from_utf8(custom_property_name).release_value_but_fixme_should_propagate_errors());
parent->add_child(child);
if (parent->has_cycles())
return false;
if (auto custom_property_value = get_custom_property(element, pseudo_element, FlyString::from_utf8(custom_property_name).release_value_but_fixme_should_propagate_errors())) {
VERIFY(custom_property_value->is_unresolved());
TokenStream custom_property_tokens { custom_property_value->as_unresolved().values() };
if (!expand_variables(element, pseudo_element, custom_property_name, dependencies, custom_property_tokens, dest))
return false;
continue;
}
// Use the provided fallback value, if any.
var_contents.skip_whitespace();
if (var_contents.has_next_token()) {
auto const& comma_token = var_contents.next_token();
if (!comma_token.is(Token::Type::Comma))
return false;
var_contents.skip_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, StringView property_name, TokenStream<ComponentValue>& source, Vector<ComponentValue>& dest)
{
while (source.has_next_token()) {
auto const& value = source.next_token();
if (value.is_function()) {
if (value.function().name().equals_ignoring_ascii_case("attr"sv)) {
// https://drafts.csswg.org/css-values-5/#attr-substitution
TokenStream attr_contents { value.function().values() };
attr_contents.skip_whitespace();
if (!attr_contents.has_next_token())
return false;
auto const& attr_name_token = attr_contents.next_token();
if (!attr_name_token.is(Token::Type::Ident))
return false;
auto attr_name = attr_name_token.token().ident();
auto attr_value = element.get_attribute(attr_name);
// 1. If the attr() function has a substitution value, replace the attr() function by the substitution value.
if (!attr_value.is_null()) {
// FIXME: attr() should also accept an optional type argument, not just strings.
dest.empend(Token::of_string(FlyString::from_deprecated_fly_string(attr_value).release_value_but_fixme_should_propagate_errors()));
continue;
}
// 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.
attr_contents.skip_whitespace();
if (attr_contents.has_next_token()) {
auto const& comma_token = attr_contents.next_token();
if (!comma_token.is(Token::Type::Comma))
return false;
attr_contents.skip_whitespace();
if (!expand_unresolved_values(element, property_name, attr_contents, dest))
return false;
continue;
}
// 3. Otherwise, the property containing the attr() function is invalid at computed-value time.
return false;
}
if (auto maybe_calc_value = parse_calculated_value(value); maybe_calc_value && maybe_calc_value->is_calculated()) {
auto& calc_value = maybe_calc_value->as_calculated();
if (calc_value.resolves_to_angle()) {
auto resolved_value = calc_value.resolve_angle();
dest.empend(Token::create_dimension(resolved_value->to_degrees(), "deg"_fly_string));
continue;
}
if (calc_value.resolves_to_frequency()) {
auto resolved_value = calc_value.resolve_frequency();
dest.empend(Token::create_dimension(resolved_value->to_hertz(), "hz"_fly_string));
continue;
}
if (calc_value.resolves_to_length()) {
// FIXME: In order to resolve lengths, we need to know the font metrics in case a font-relative unit
// is used. So... we can't do that until style is computed?
// This might be easier once we have calc-simplification implemented.
}
if (calc_value.resolves_to_percentage()) {
auto resolved_value = calc_value.resolve_percentage();
dest.empend(Token::create_percentage(resolved_value.value().value()));
continue;
}
if (calc_value.resolves_to_time()) {
auto resolved_value = calc_value.resolve_time();
dest.empend(Token::create_dimension(resolved_value->to_seconds(), "s"_fly_string));
continue;
}
if (calc_value.resolves_to_number()) {
auto resolved_value = calc_value.resolve_number();
dest.empend(Token::create_number(resolved_value.value()));
continue;
}
}
auto const& source_function = value.function();
Vector<ComponentValue> function_values;
TokenStream source_function_contents { source_function.values() };
if (!expand_unresolved_values(element, property_name, source_function_contents, function_values))
return false;
NonnullRefPtr<Function> function = Function::create(source_function.name(), move(function_values));
dest.empend(function);
continue;
}
if (value.is_block()) {
auto const& source_block = value.block();
TokenStream source_block_values { source_block.values() };
Vector<ComponentValue> block_values;
if (!expand_unresolved_values(element, property_name, source_block_values, block_values))
return false;
NonnullRefPtr<Block> block = Block::create(source_block.token(), move(block_values));
dest.empend(move(block));
continue;
}
dest.empend(value.token());
}
return true;
}
}