2024-10-31 16:03:06 +00:00
|
|
|
/*
|
|
|
|
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
|
|
|
|
* Copyright (c) 2020-2021, the SerenityOS developers.
|
LibWeb/CSS: Merge style declaration subclasses into CSSStyleProperties
We previously had PropertyOwningCSSStyleDeclaration and
ResolvedCSSStyleDeclaration, representing the current style properties
and resolved style respectively. Both of these were the
CSSStyleDeclaration type in the CSSOM. (We also had
ElementInlineCSSStyleDeclaration but I removed that in a previous
commit.)
In the meantime, the spec has changed so that these should now be a new
CSSStyleProperties type in the CSSOM. Also, we need to subclass
CSSStyleDeclaration for things like CSSFontFaceRule's list of
descriptors, which means it wouldn't hold style properties.
So, this commit does the fairly messy work of combining these two types
into a new CSSStyleProperties class. A lot of what previously was done
as separate methods in the two classes, now follows the spec steps of
"if the readonly flag is set, do X" instead, which is hopefully easier
to follow too.
There is still some functionality in CSSStyleDeclaration that belongs in
CSSStyleProperties, but I'll do that next. To avoid a huge diff for
"CSSStyleDeclaration-all-supported-properties-and-default-values.txt"
both here and in the following commit, we don't apply the (currently
empty) CSSStyleProperties prototype yet.
2025-03-17 17:50:49 +00:00
|
|
|
* Copyright (c) 2021-2025, Sam Atkins <sam@ladybird.org>
|
2024-10-31 16:03:06 +00:00
|
|
|
* Copyright (c) 2021, Tobias Christiansen <tobyase@serenityos.org>
|
|
|
|
* Copyright (c) 2022, MacDue <macdue@dueutil.tech>
|
|
|
|
* Copyright (c) 2024, Shannon Booth <shannon@serenityos.org>
|
|
|
|
* Copyright (c) 2024, Tommy van der Vorst <tommy@pixelspark.nl>
|
|
|
|
* Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
|
|
|
|
* Copyright (c) 2024, Glenn Skrzypczak <glenn.skrzypczak@gmail.com>
|
|
|
|
*
|
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include <LibWeb/CSS/CSSFontFaceRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSImportRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSKeyframeRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSLayerBlockRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSLayerStatementRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSMediaRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSNamespaceRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSNestedDeclarations.h>
|
|
|
|
#include <LibWeb/CSS/CSSPropertyRule.h>
|
LibWeb/CSS: Merge style declaration subclasses into CSSStyleProperties
We previously had PropertyOwningCSSStyleDeclaration and
ResolvedCSSStyleDeclaration, representing the current style properties
and resolved style respectively. Both of these were the
CSSStyleDeclaration type in the CSSOM. (We also had
ElementInlineCSSStyleDeclaration but I removed that in a previous
commit.)
In the meantime, the spec has changed so that these should now be a new
CSSStyleProperties type in the CSSOM. Also, we need to subclass
CSSStyleDeclaration for things like CSSFontFaceRule's list of
descriptors, which means it wouldn't hold style properties.
So, this commit does the fairly messy work of combining these two types
into a new CSSStyleProperties class. A lot of what previously was done
as separate methods in the two classes, now follows the spec steps of
"if the readonly flag is set, do X" instead, which is hopefully easier
to follow too.
There is still some functionality in CSSStyleDeclaration that belongs in
CSSStyleProperties, but I'll do that next. To avoid a huge diff for
"CSSStyleDeclaration-all-supported-properties-and-default-values.txt"
both here and in the following commit, we don't apply the (currently
empty) CSSStyleProperties prototype yet.
2025-03-17 17:50:49 +00:00
|
|
|
#include <LibWeb/CSS/CSSStyleProperties.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
#include <LibWeb/CSS/CSSStyleRule.h>
|
|
|
|
#include <LibWeb/CSS/CSSSupportsRule.h>
|
2025-03-28 12:42:41 +00:00
|
|
|
#include <LibWeb/CSS/FontFace.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
#include <LibWeb/CSS/Parser/Parser.h>
|
|
|
|
#include <LibWeb/CSS/PropertyName.h>
|
2025-01-27 21:09:12 +01:00
|
|
|
#include <LibWeb/CSS/StyleValues/CSSKeywordValue.h>
|
2025-03-24 17:20:08 +00:00
|
|
|
#include <LibWeb/CSS/StyleValues/CustomIdentStyleValue.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
#include <LibWeb/CSS/StyleValues/IntegerStyleValue.h>
|
|
|
|
#include <LibWeb/CSS/StyleValues/NumberStyleValue.h>
|
|
|
|
#include <LibWeb/CSS/StyleValues/OpenTypeTaggedStyleValue.h>
|
|
|
|
#include <LibWeb/CSS/StyleValues/PercentageStyleValue.h>
|
|
|
|
#include <LibWeb/CSS/StyleValues/StringStyleValue.h>
|
|
|
|
#include <LibWeb/CSS/StyleValues/StyleValueList.h>
|
|
|
|
|
|
|
|
namespace Web::CSS::Parser {
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSRule> Parser::convert_to_rule(Rule const& rule, Nested nested)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
return rule.visit(
|
2024-11-15 04:01:23 +13:00
|
|
|
[this, nested](AtRule const& at_rule) -> GC::Ptr<CSSRule> {
|
2024-10-31 16:03:06 +00:00
|
|
|
if (has_ignored_vendor_prefix(at_rule.name))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("font-face"sv))
|
|
|
|
return convert_to_font_face_rule(at_rule);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("import"sv))
|
|
|
|
return convert_to_import_rule(at_rule);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("keyframes"sv))
|
|
|
|
return convert_to_keyframes_rule(at_rule);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("layer"sv))
|
|
|
|
return convert_to_layer_rule(at_rule, nested);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("media"sv))
|
|
|
|
return convert_to_media_rule(at_rule, nested);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("namespace"sv))
|
|
|
|
return convert_to_namespace_rule(at_rule);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("supports"sv))
|
|
|
|
return convert_to_supports_rule(at_rule, nested);
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("property"sv))
|
|
|
|
return convert_to_property_rule(at_rule);
|
|
|
|
|
|
|
|
// FIXME: More at rules!
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", at_rule.name);
|
|
|
|
return {};
|
|
|
|
},
|
2024-11-15 04:01:23 +13:00
|
|
|
[this, nested](QualifiedRule const& qualified_rule) -> GC::Ptr<CSSRule> {
|
2024-10-31 16:03:06 +00:00
|
|
|
return convert_to_style_rule(qualified_rule, nested);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSStyleRule> Parser::convert_to_style_rule(QualifiedRule const& qualified_rule, Nested nested)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
TokenStream prelude_stream { qualified_rule.prelude };
|
|
|
|
|
|
|
|
auto maybe_selectors = parse_a_selector_list(prelude_stream,
|
|
|
|
nested == Nested::Yes ? SelectorType::Relative : SelectorType::Standalone);
|
|
|
|
|
|
|
|
if (maybe_selectors.is_error()) {
|
|
|
|
if (maybe_selectors.error() == ParseError::SyntaxError) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: style rule selectors invalid; discarding.");
|
|
|
|
if constexpr (CSS_PARSER_DEBUG) {
|
|
|
|
prelude_stream.dump_all_tokens();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (maybe_selectors.value().is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: empty selector; discarding.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
SelectorList selectors = maybe_selectors.release_value();
|
2024-11-08 17:50:38 +00:00
|
|
|
if (nested == Nested::Yes)
|
|
|
|
selectors = adapt_nested_relative_selector_list(selectors);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
2025-03-18 13:41:46 +00:00
|
|
|
auto declaration = convert_to_style_declaration(qualified_rule.declarations);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
GC::RootVector<CSSRule*> child_rules { realm().heap() };
|
2024-10-31 16:03:06 +00:00
|
|
|
for (auto& child : qualified_rule.child_rules) {
|
|
|
|
child.visit(
|
|
|
|
[&](Rule const& rule) {
|
|
|
|
// "In addition to nested style rules, this specification allows nested group rules inside of style rules:
|
|
|
|
// any at-rule whose body contains style rules can be nested inside of a style rule as well."
|
|
|
|
// https://drafts.csswg.org/css-nesting-1/#nested-group-rules
|
|
|
|
if (auto converted_rule = convert_to_rule(rule, Nested::Yes)) {
|
|
|
|
if (is<CSSGroupingRule>(*converted_rule)) {
|
|
|
|
child_rules.append(converted_rule);
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: nested {} is not allowed inside style rule; discarding.", converted_rule->class_name());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2025-03-18 13:41:46 +00:00
|
|
|
child_rules.append(CSSNestedDeclarations::create(realm(), *convert_to_style_declaration(declarations)));
|
2024-10-31 16:03:06 +00:00
|
|
|
});
|
|
|
|
}
|
2025-02-05 12:08:27 +00:00
|
|
|
auto nested_rules = CSSRuleList::create(realm(), move(child_rules));
|
|
|
|
return CSSStyleRule::create(realm(), move(selectors), *declaration, *nested_rules);
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSImportRule> Parser::convert_to_import_rule(AtRule const& rule)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-cascade-5/#at-import
|
|
|
|
// @import [ <url> | <string> ]
|
|
|
|
// [ layer | layer(<layer-name>) ]?
|
|
|
|
// <import-conditions> ;
|
|
|
|
//
|
|
|
|
// <import-conditions> = [ supports( [ <supports-condition> | <declaration> ] ) ]?
|
|
|
|
// <media-query-list>?
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Empty prelude.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Block is not allowed.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
TokenStream tokens { rule.prelude };
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
Optional<URL::URL> url = parse_url_function(tokens);
|
|
|
|
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
|
2025-02-05 12:08:27 +00:00
|
|
|
url = complete_url(tokens.consume_a_token().token().string());
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
if (!url.has_value()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @import rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
2025-03-19 11:23:33 +00:00
|
|
|
// FIXME: Implement layer support.
|
|
|
|
RefPtr<Supports> supports {};
|
|
|
|
if (tokens.next_token().is_function("supports"sv)) {
|
|
|
|
auto component_value = tokens.consume_a_token();
|
|
|
|
TokenStream supports_tokens { component_value.function().value };
|
|
|
|
if (supports_tokens.next_token().is_block()) {
|
|
|
|
supports = parse_a_supports(supports_tokens);
|
|
|
|
} else {
|
|
|
|
m_rule_context.append(ContextType::SupportsCondition);
|
|
|
|
auto declaration = consume_a_declaration(supports_tokens);
|
|
|
|
m_rule_context.take_last();
|
|
|
|
if (declaration.has_value()) {
|
|
|
|
auto supports_declaration = Supports::Declaration::create(declaration->to_string(), convert_to_style_property(*declaration).has_value());
|
|
|
|
supports = Supports::create(supports_declaration.release_nonnull<BooleanExpression>());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-01 14:32:11 +01:00
|
|
|
auto media_query_list = parse_a_media_query_list(tokens);
|
2025-03-19 11:23:33 +00:00
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
if (tokens.has_next_token()) {
|
|
|
|
if constexpr (CSS_PARSER_DEBUG) {
|
2025-03-19 11:23:33 +00:00
|
|
|
dbgln("Failed to parse @import rule:");
|
2024-10-31 16:03:06 +00:00
|
|
|
tokens.dump_all_tokens();
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2025-04-01 14:32:11 +01:00
|
|
|
return CSSImportRule::create(url.value(), const_cast<DOM::Document&>(*document()), supports, move(media_query_list));
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Optional<FlyString> Parser::parse_layer_name(TokenStream<ComponentValue>& tokens, AllowBlankLayerName allow_blank_layer_name)
|
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-cascade-5/#typedef-layer-name
|
|
|
|
// <layer-name> = <ident> [ '.' <ident> ]*
|
|
|
|
|
|
|
|
// "The CSS-wide keywords are reserved for future use, and cause the rule to be invalid at parse time if used as an <ident> in the <layer-name>."
|
|
|
|
auto is_valid_layer_name_part = [](auto& token) {
|
|
|
|
return token.is(Token::Type::Ident) && !is_css_wide_keyword(token.token().ident());
|
|
|
|
};
|
|
|
|
|
|
|
|
auto transaction = tokens.begin_transaction();
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
if (!tokens.has_next_token() && allow_blank_layer_name == AllowBlankLayerName::Yes) {
|
|
|
|
// No name present, just return a blank one
|
|
|
|
return FlyString();
|
|
|
|
}
|
|
|
|
|
|
|
|
auto& first_name_token = tokens.consume_a_token();
|
|
|
|
if (!is_valid_layer_name_part(first_name_token))
|
|
|
|
return {};
|
|
|
|
|
|
|
|
StringBuilder builder;
|
|
|
|
builder.append(first_name_token.token().ident());
|
|
|
|
|
|
|
|
while (tokens.has_next_token()) {
|
|
|
|
// Repeatedly parse `'.' <ident>`
|
|
|
|
if (!tokens.next_token().is_delim('.'))
|
|
|
|
break;
|
|
|
|
tokens.discard_a_token(); // '.'
|
|
|
|
|
|
|
|
auto& name_token = tokens.consume_a_token();
|
|
|
|
if (!is_valid_layer_name_part(name_token))
|
|
|
|
return {};
|
|
|
|
builder.appendff(".{}", name_token.token().ident());
|
|
|
|
}
|
|
|
|
|
|
|
|
transaction.commit();
|
|
|
|
return builder.to_fly_string_without_validation();
|
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSRule> Parser::convert_to_layer_rule(AtRule const& rule, Nested nested)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-cascade-5/#at-layer
|
|
|
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
|
|
// CSSLayerBlockRule
|
|
|
|
// @layer <layer-name>? {
|
|
|
|
// <rule-list>
|
|
|
|
// }
|
|
|
|
|
|
|
|
// First, the name
|
|
|
|
FlyString layer_name = {};
|
|
|
|
auto prelude_tokens = TokenStream { rule.prelude };
|
|
|
|
if (auto maybe_name = parse_layer_name(prelude_tokens, AllowBlankLayerName::Yes); maybe_name.has_value()) {
|
|
|
|
layer_name = maybe_name.release_value();
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (not a valid layer name) prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
prelude_tokens.discard_whitespace();
|
|
|
|
if (prelude_tokens.has_next_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer has invalid prelude, (tokens after layer name) prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then the rules
|
2025-02-05 12:08:27 +00:00
|
|
|
GC::RootVector<CSSRule*> child_rules { realm().heap() };
|
2024-11-04 17:26:19 +00:00
|
|
|
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
|
|
|
|
child.visit(
|
|
|
|
[&](Rule const& rule) {
|
|
|
|
if (auto child_rule = convert_to_rule(rule, nested))
|
|
|
|
child_rules.append(child_rule);
|
|
|
|
},
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2025-03-18 13:41:46 +00:00
|
|
|
child_rules.append(CSSNestedDeclarations::create(realm(), *convert_to_style_declaration(declarations)));
|
2024-11-04 17:26:19 +00:00
|
|
|
});
|
|
|
|
}
|
2025-02-05 12:08:27 +00:00
|
|
|
auto rule_list = CSSRuleList::create(realm(), child_rules);
|
|
|
|
return CSSLayerBlockRule::create(realm(), layer_name, rule_list);
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CSSLayerStatementRule
|
|
|
|
// @layer <layer-name>#;
|
|
|
|
auto tokens = TokenStream { rule.prelude };
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
Vector<FlyString> layer_names;
|
|
|
|
while (tokens.has_next_token()) {
|
|
|
|
// Comma
|
|
|
|
if (!layer_names.is_empty()) {
|
|
|
|
if (auto comma = tokens.consume_a_token(); !comma.is(Token::Type::Comma)) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer missing separating comma, ({}) prelude = {}; discarding.", comma.to_debug_string(), rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (auto name = parse_layer_name(tokens, AllowBlankLayerName::No); name.has_value()) {
|
|
|
|
layer_names.append(name.release_value());
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer contains invalid name, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (layer_names.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @layer statement has no layer names, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
return CSSLayerStatementRule::create(realm(), move(layer_names));
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSKeyframesRule> Parser::convert_to_keyframes_rule(AtRule const& rule)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-animations/#keyframes
|
|
|
|
// @keyframes = @keyframes <keyframes-name> { <qualified-rule-list> }
|
|
|
|
// <keyframes-name> = <custom-ident> | <string>
|
|
|
|
// <keyframe-block> = <keyframe-selector># { <declaration-list> }
|
|
|
|
// <keyframe-selector> = from | to | <percentage [0,100]>
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @keyframes rule: Empty prelude.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: Is there some way of detecting if there is a block or not?
|
|
|
|
|
|
|
|
auto prelude_stream = TokenStream { rule.prelude };
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
auto& token = prelude_stream.consume_a_token();
|
|
|
|
if (!token.is_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto name_token = token.token();
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name_token.is(Token::Type::Ident) && (is_css_wide_keyword(name_token.ident()) || name_token.ident().equals_ignoring_ascii_case("none"sv))) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.ident());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.to_debug_string());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto name = name_token.to_string();
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
GC::RootVector<CSSRule*> keyframes(realm().heap());
|
2024-10-31 16:03:06 +00:00
|
|
|
rule.for_each_as_qualified_rule_list([&](auto& qualified_rule) {
|
|
|
|
if (!qualified_rule.child_rules.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes keyframe rule contains at-rules; discarding them.");
|
|
|
|
}
|
|
|
|
|
|
|
|
auto selectors = Vector<CSS::Percentage> {};
|
|
|
|
TokenStream child_tokens { qualified_rule.prelude };
|
|
|
|
while (child_tokens.has_next_token()) {
|
|
|
|
child_tokens.discard_whitespace();
|
|
|
|
if (!child_tokens.has_next_token())
|
|
|
|
break;
|
|
|
|
auto tok = child_tokens.consume_a_token();
|
|
|
|
if (!tok.is_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid selector: {}; discarding.", tok.to_debug_string());
|
|
|
|
child_tokens.reconsume_current_input_token();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
auto token = tok.token();
|
|
|
|
auto read_a_selector = false;
|
|
|
|
if (token.is(Token::Type::Ident)) {
|
|
|
|
if (token.ident().equals_ignoring_ascii_case("from"sv)) {
|
|
|
|
selectors.append(CSS::Percentage(0));
|
|
|
|
read_a_selector = true;
|
|
|
|
}
|
|
|
|
if (token.ident().equals_ignoring_ascii_case("to"sv)) {
|
|
|
|
selectors.append(CSS::Percentage(100));
|
|
|
|
read_a_selector = true;
|
|
|
|
}
|
|
|
|
} else if (token.is(Token::Type::Percentage)) {
|
|
|
|
selectors.append(CSS::Percentage(token.percentage()));
|
|
|
|
read_a_selector = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (read_a_selector) {
|
|
|
|
child_tokens.discard_whitespace();
|
|
|
|
if (child_tokens.consume_a_token().is(Token::Type::Comma))
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
child_tokens.reconsume_current_input_token();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
PropertiesAndCustomProperties properties;
|
|
|
|
qualified_rule.for_each_as_declaration_list([&](auto const& declaration) {
|
|
|
|
extract_property(declaration, properties);
|
|
|
|
});
|
LibWeb/CSS: Merge style declaration subclasses into CSSStyleProperties
We previously had PropertyOwningCSSStyleDeclaration and
ResolvedCSSStyleDeclaration, representing the current style properties
and resolved style respectively. Both of these were the
CSSStyleDeclaration type in the CSSOM. (We also had
ElementInlineCSSStyleDeclaration but I removed that in a previous
commit.)
In the meantime, the spec has changed so that these should now be a new
CSSStyleProperties type in the CSSOM. Also, we need to subclass
CSSStyleDeclaration for things like CSSFontFaceRule's list of
descriptors, which means it wouldn't hold style properties.
So, this commit does the fairly messy work of combining these two types
into a new CSSStyleProperties class. A lot of what previously was done
as separate methods in the two classes, now follows the spec steps of
"if the readonly flag is set, do X" instead, which is hopefully easier
to follow too.
There is still some functionality in CSSStyleDeclaration that belongs in
CSSStyleProperties, but I'll do that next. To avoid a huge diff for
"CSSStyleDeclaration-all-supported-properties-and-default-values.txt"
both here and in the following commit, we don't apply the (currently
empty) CSSStyleProperties prototype yet.
2025-03-17 17:50:49 +00:00
|
|
|
auto style = CSSStyleProperties::create(realm(), move(properties.properties), move(properties.custom_properties));
|
2024-10-31 16:03:06 +00:00
|
|
|
for (auto& selector : selectors) {
|
2025-02-05 12:08:27 +00:00
|
|
|
auto keyframe_rule = CSSKeyframeRule::create(realm(), selector, *style);
|
2024-10-31 16:03:06 +00:00
|
|
|
keyframes.append(keyframe_rule);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
return CSSKeyframesRule::create(realm(), name, CSSRuleList::create(realm(), move(keyframes)));
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSNamespaceRule> Parser::convert_to_namespace_rule(AtRule const& rule)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-namespaces/#syntax
|
|
|
|
// @namespace <namespace-prefix>? [ <string> | <url> ] ;
|
|
|
|
// <namespace-prefix> = <ident>
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Empty prelude.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!rule.child_rules_and_lists_of_declarations.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Block is not allowed.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto tokens = TokenStream { rule.prelude };
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
Optional<FlyString> prefix = {};
|
|
|
|
if (tokens.next_token().is(Token::Type::Ident)) {
|
|
|
|
prefix = tokens.consume_a_token().token().ident();
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
}
|
|
|
|
|
|
|
|
FlyString namespace_uri;
|
|
|
|
if (auto url = parse_url_function(tokens); url.has_value()) {
|
2024-12-03 22:31:33 +13:00
|
|
|
namespace_uri = url.value().to_string();
|
2024-10-31 16:03:06 +00:00
|
|
|
} else if (auto& url_token = tokens.consume_a_token(); url_token.is(Token::Type::String)) {
|
|
|
|
namespace_uri = url_token.token().string();
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @namespace rule: Unable to parse `{}` as URL.", tokens.next_token().to_debug_string());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
if (tokens.has_next_token()) {
|
|
|
|
if constexpr (CSS_PARSER_DEBUG) {
|
|
|
|
dbgln("Failed to parse @namespace rule: Trailing tokens after URL.");
|
|
|
|
tokens.dump_all_tokens();
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
return CSSNamespaceRule::create(realm(), prefix, namespace_uri);
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSSupportsRule> Parser::convert_to_supports_rule(AtRule const& rule, Nested nested)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-conditional-3/#at-supports
|
|
|
|
// @supports <supports-condition> {
|
|
|
|
// <rule-list>
|
|
|
|
// }
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @supports rule: Empty prelude.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto supports_tokens = TokenStream { rule.prelude };
|
|
|
|
auto supports = parse_a_supports(supports_tokens);
|
|
|
|
if (!supports) {
|
|
|
|
if constexpr (CSS_PARSER_DEBUG) {
|
|
|
|
dbgln("Failed to parse @supports rule: supports clause invalid.");
|
|
|
|
supports_tokens.dump_all_tokens();
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
GC::RootVector<CSSRule*> child_rules { realm().heap() };
|
2024-11-04 17:26:19 +00:00
|
|
|
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
|
|
|
|
child.visit(
|
|
|
|
[&](Rule const& rule) {
|
|
|
|
if (auto child_rule = convert_to_rule(rule, nested))
|
|
|
|
child_rules.append(child_rule);
|
|
|
|
},
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2025-03-18 13:41:46 +00:00
|
|
|
child_rules.append(CSSNestedDeclarations::create(realm(), *convert_to_style_declaration(declarations)));
|
2024-11-04 17:26:19 +00:00
|
|
|
});
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
auto rule_list = CSSRuleList::create(realm(), child_rules);
|
|
|
|
return CSSSupportsRule::create(realm(), supports.release_nonnull(), rule_list);
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSPropertyRule> Parser::convert_to_property_rule(AtRule const& rule)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.css-houdini.org/css-properties-values-api-1/#at-ruledef-property
|
|
|
|
// @property <custom-property-name> {
|
|
|
|
// <declaration-list>
|
|
|
|
// }
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "Failed to parse @property rule: Empty prelude.");
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto prelude_stream = TokenStream { rule.prelude };
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
auto const& token = prelude_stream.consume_a_token();
|
|
|
|
if (!token.is_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto name_token = token.token();
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property has invalid prelude, prelude = {}; discarding.", rule.prelude);
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!name_token.is(Token::Type::Ident)) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name is invalid: {}; discarding.", name_token.to_debug_string());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_a_custom_property_name_string(name_token.ident())) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @property name doesn't start with '--': {}; discarding.", name_token.ident());
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto const& name = name_token.ident();
|
|
|
|
|
|
|
|
Optional<FlyString> syntax_maybe;
|
|
|
|
Optional<bool> inherits_maybe;
|
|
|
|
Optional<String> initial_value_maybe;
|
|
|
|
|
|
|
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
|
|
|
if (declaration.name.equals_ignoring_ascii_case("syntax"sv)) {
|
|
|
|
TokenStream token_stream { declaration.value };
|
|
|
|
token_stream.discard_whitespace();
|
|
|
|
|
|
|
|
auto const& syntax_token = token_stream.consume_a_token();
|
|
|
|
if (syntax_token.is(Token::Type::String)) {
|
|
|
|
token_stream.discard_whitespace();
|
|
|
|
if (token_stream.has_next_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in syntax");
|
|
|
|
} else {
|
|
|
|
syntax_maybe = syntax_token.token().string();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected value for @property \"syntax\": {}; discarding.", declaration.to_string());
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (declaration.name.equals_ignoring_ascii_case("inherits"sv)) {
|
|
|
|
TokenStream token_stream { declaration.value };
|
|
|
|
token_stream.discard_whitespace();
|
|
|
|
|
|
|
|
auto const& inherits_token = token_stream.consume_a_token();
|
|
|
|
if (inherits_token.is_ident("true"sv) || inherits_token.is_ident("false"sv)) {
|
|
|
|
auto const& ident = inherits_token.token().ident();
|
|
|
|
token_stream.discard_whitespace();
|
|
|
|
if (token_stream.has_next_token()) {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Unexpected trailing tokens in inherits");
|
|
|
|
} else {
|
|
|
|
inherits_maybe = (ident == "true");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dbgln_if(CSS_PARSER_DEBUG, "CSSParser: Expected true/false for @property \"inherits\" value, got: {}; discarding.", inherits_token.to_debug_string());
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (declaration.name.equals_ignoring_ascii_case("initial-value"sv)) {
|
|
|
|
// FIXME: Ensure that the initial value matches the syntax, and parse the correct CSSValue out
|
|
|
|
StringBuilder initial_value_sb;
|
|
|
|
for (auto const& component : declaration.value) {
|
|
|
|
initial_value_sb.append(component.to_string());
|
|
|
|
}
|
|
|
|
initial_value_maybe = MUST(initial_value_sb.to_string());
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (syntax_maybe.has_value() && inherits_maybe.has_value()) {
|
2025-02-05 12:08:27 +00:00
|
|
|
return CSSPropertyRule::create(realm(), name, syntax_maybe.value(), inherits_maybe.value(), std::move(initial_value_maybe));
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2024-11-15 04:01:23 +13:00
|
|
|
GC::Ptr<CSSFontFaceRule> Parser::convert_to_font_face_rule(AtRule const& rule)
|
2024-10-31 16:03:06 +00:00
|
|
|
{
|
|
|
|
// https://drafts.csswg.org/css-fonts/#font-face-rule
|
2025-04-03 12:05:49 +01:00
|
|
|
Vector<Descriptor> descriptors;
|
|
|
|
HashTable<DescriptorID> seen_descriptor_ids;
|
2024-10-31 16:03:06 +00:00
|
|
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
2025-04-03 12:05:49 +01:00
|
|
|
if (auto descriptor = convert_to_descriptor(AtRuleID::FontFace, declaration); descriptor.has_value()) {
|
|
|
|
if (seen_descriptor_ids.contains(descriptor->descriptor_id)) {
|
|
|
|
descriptors.remove_first_matching([&descriptor](Descriptor const& existing) {
|
|
|
|
return existing.descriptor_id == descriptor->descriptor_id;
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
} else {
|
2025-04-03 12:05:49 +01:00
|
|
|
seen_descriptor_ids.set(descriptor->descriptor_id);
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
2025-04-03 12:05:49 +01:00
|
|
|
descriptors.append(descriptor.release_value());
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-04-03 12:05:49 +01:00
|
|
|
return CSSFontFaceRule::create(realm(), CSSFontFaceDescriptors::create(realm(), move(descriptors)));
|
2024-10-31 16:03:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|