2024-10-31 16:03:06 +00:00
|
|
|
|
/*
|
|
|
|
|
|
* Copyright (c) 2018-2022, Andreas Kling <andreas@ladybird.org>
|
|
|
|
|
|
* Copyright (c) 2020-2021, the SerenityOS developers.
|
2026-03-27 11:57:30 +00:00
|
|
|
|
* Copyright (c) 2021-2026, 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
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-07 02:07:49 +13:00
|
|
|
|
#include <LibGC/HeapVector.h>
|
2026-03-27 11:57:30 +00:00
|
|
|
|
#include <LibWeb/CSS/CSSContainerRule.h>
|
2026-01-31 01:03:29 +13:00
|
|
|
|
#include <LibWeb/CSS/CSSCounterStyleRule.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#include <LibWeb/CSS/CSSFontFaceRule.h>
|
2026-01-14 17:06:59 +13:00
|
|
|
|
#include <LibWeb/CSS/CSSFontFeatureValuesRule.h>
|
2026-03-04 22:40:51 +13:00
|
|
|
|
#include <LibWeb/CSS/CSSFunctionDeclarations.h>
|
2026-03-07 02:07:49 +13:00
|
|
|
|
#include <LibWeb/CSS/CSSFunctionRule.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#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>
|
2025-05-15 11:48:56 +01:00
|
|
|
|
#include <LibWeb/CSS/CSSMarginRule.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#include <LibWeb/CSS/CSSMediaRule.h>
|
|
|
|
|
|
#include <LibWeb/CSS/CSSNamespaceRule.h>
|
|
|
|
|
|
#include <LibWeb/CSS/CSSNestedDeclarations.h>
|
2025-05-13 12:17:41 +01:00
|
|
|
|
#include <LibWeb/CSS/CSSPageRule.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#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>
|
2026-03-27 11:57:30 +00:00
|
|
|
|
#include <LibWeb/CSS/ContainerQuery.h>
|
2025-03-28 12:42:41 +00:00
|
|
|
|
#include <LibWeb/CSS/FontFace.h>
|
2025-07-23 10:27:53 +01:00
|
|
|
|
#include <LibWeb/CSS/Parser/ErrorReporter.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#include <LibWeb/CSS/Parser/Parser.h>
|
2025-07-23 12:20:01 +02:00
|
|
|
|
#include <LibWeb/CSS/Parser/Syntax.h>
|
|
|
|
|
|
#include <LibWeb/CSS/Parser/SyntaxParsing.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#include <LibWeb/CSS/PropertyName.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>
|
2025-08-08 10:28:41 +01:00
|
|
|
|
#include <LibWeb/CSS/StyleValues/KeywordStyleValue.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
#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>
|
2026-03-07 02:07:49 +13:00
|
|
|
|
#include <LibWeb/CSS/StyleValues/UnresolvedStyleValue.h>
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
namespace Web::CSS::Parser {
|
|
|
|
|
|
|
2025-05-13 13:54:43 +01:00
|
|
|
|
// A helper that ensures only the last instance of each descriptor is included, while also handling shorthands.
|
|
|
|
|
|
class DescriptorList {
|
|
|
|
|
|
public:
|
|
|
|
|
|
DescriptorList(AtRuleID at_rule)
|
|
|
|
|
|
: m_at_rule(at_rule)
|
|
|
|
|
|
{
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void append(Descriptor&& descriptor)
|
|
|
|
|
|
{
|
2026-03-05 00:03:57 +13:00
|
|
|
|
if (is_shorthand(m_at_rule, descriptor.descriptor_name_and_id)) {
|
|
|
|
|
|
for_each_expanded_longhand(m_at_rule, descriptor.descriptor_name_and_id, descriptor.value, [this](auto longhand_id, auto longhand_value) {
|
2025-05-13 13:54:43 +01:00
|
|
|
|
append_internal(Descriptor { longhand_id, longhand_value.release_nonnull() });
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
append_internal(move(descriptor));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vector<Descriptor> release_descriptors()
|
|
|
|
|
|
{
|
|
|
|
|
|
return move(m_descriptors);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
void append_internal(Descriptor&& descriptor)
|
|
|
|
|
|
{
|
2026-03-05 00:03:57 +13:00
|
|
|
|
if (m_seen_descriptor_ids.contains(descriptor.descriptor_name_and_id)) {
|
2025-05-13 13:54:43 +01:00
|
|
|
|
m_descriptors.remove_first_matching([&descriptor](Descriptor const& existing) {
|
2026-03-05 00:03:57 +13:00
|
|
|
|
return existing.descriptor_name_and_id == descriptor.descriptor_name_and_id;
|
2025-05-13 13:54:43 +01:00
|
|
|
|
});
|
|
|
|
|
|
} else {
|
2026-03-05 00:03:57 +13:00
|
|
|
|
m_seen_descriptor_ids.set(descriptor.descriptor_name_and_id);
|
2025-05-13 13:54:43 +01:00
|
|
|
|
}
|
|
|
|
|
|
m_descriptors.append(move(descriptor));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
AtRuleID m_at_rule;
|
|
|
|
|
|
Vector<Descriptor> m_descriptors;
|
2026-03-05 00:03:57 +13:00
|
|
|
|
HashTable<DescriptorNameAndID> m_seen_descriptor_ids;
|
2025-05-13 13:54:43 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-05 16:00:11 +13:00
|
|
|
|
template<typename NestedDeclarationsRule>
|
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> {
|
2026-01-25 04:49:04 +00:00
|
|
|
|
// https://compat.spec.whatwg.org/#css-at-rules
|
|
|
|
|
|
// @-webkit-keyframes must be supported as an alias of @keyframes.
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("keyframes"sv) || at_rule.name.equals_ignoring_ascii_case("-webkit-keyframes"sv))
|
|
|
|
|
|
return convert_to_keyframes_rule(at_rule);
|
|
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
if (has_ignored_vendor_prefix(at_rule.name))
|
|
|
|
|
|
return {};
|
|
|
|
|
|
|
2026-03-27 11:57:30 +00:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("container"sv))
|
|
|
|
|
|
return convert_to_container_rule<NestedDeclarationsRule>(at_rule, nested);
|
|
|
|
|
|
|
2026-01-31 01:03:29 +13:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("counter-style"sv))
|
|
|
|
|
|
return convert_to_counter_style_rule(at_rule);
|
|
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("font-face"sv))
|
|
|
|
|
|
return convert_to_font_face_rule(at_rule);
|
|
|
|
|
|
|
2026-01-14 17:06:59 +13:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("font-feature-values"sv))
|
|
|
|
|
|
return convert_to_font_feature_values_rule(at_rule);
|
|
|
|
|
|
|
2026-03-07 02:07:49 +13:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("function"sv))
|
|
|
|
|
|
return convert_to_function_rule(at_rule);
|
|
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
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("layer"sv))
|
2026-03-05 16:00:11 +13:00
|
|
|
|
return convert_to_layer_rule<NestedDeclarationsRule>(at_rule, nested);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
2025-05-15 11:48:56 +01:00
|
|
|
|
if (is_margin_rule_name(at_rule.name))
|
|
|
|
|
|
return convert_to_margin_rule(at_rule);
|
|
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("media"sv))
|
2026-03-05 16:00:11 +13:00
|
|
|
|
return convert_to_media_rule<NestedDeclarationsRule>(at_rule, nested);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("namespace"sv))
|
|
|
|
|
|
return convert_to_namespace_rule(at_rule);
|
|
|
|
|
|
|
2025-05-13 12:17:41 +01:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("page"sv))
|
|
|
|
|
|
return convert_to_page_rule(at_rule);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("property"sv))
|
|
|
|
|
|
return convert_to_property_rule(at_rule);
|
|
|
|
|
|
|
2025-05-13 12:17:41 +01:00
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("supports"sv))
|
2026-03-05 16:00:11 +13:00
|
|
|
|
return convert_to_supports_rule<NestedDeclarationsRule>(at_rule, nested);
|
2025-05-13 12:17:41 +01:00
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
// FIXME: More at rules!
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(UnknownRuleError { .rule_name = MUST(String::formatted("@{}", at_rule.name)) });
|
2024-10-31 16:03:06 +00:00
|
|
|
|
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) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Selectors invalid."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (maybe_selectors.value().is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty selector."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
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-04-14 16:02:29 +01:00
|
|
|
|
GC::RootVector<GC::Ref<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
|
2026-03-05 16:00:11 +13:00
|
|
|
|
if (auto converted_rule = convert_to_rule<CSSNestedDeclarations>(rule, Nested::Yes)) {
|
2024-10-31 16:03:06 +00:00
|
|
|
|
if (is<CSSGroupingRule>(*converted_rule)) {
|
2025-04-14 16:02:29 +01:00
|
|
|
|
child_rules.append(*converted_rule);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
} else {
|
2025-07-23 10:27:53 +01:00
|
|
|
|
ErrorReporter::the().report(InvalidRuleLocationError {
|
|
|
|
|
|
.outer_rule_name = "style"_fly_string,
|
|
|
|
|
|
.inner_rule_name = MUST(FlyString::from_utf8(converted_rule->class_name())),
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
child_rules.append(CSSNestedDeclarations::create(realm(), *this, declarations));
|
2024-10-31 16:03:06 +00:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2025-04-14 16:02:29 +01:00
|
|
|
|
auto nested_rules = CSSRuleList::create(realm(), child_rules);
|
2025-02-05 12:08:27 +00:00
|
|
|
|
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>?
|
2025-07-23 10:34:28 +01:00
|
|
|
|
TokenStream tokens { rule.prelude };
|
|
|
|
|
|
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@import"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Must be a statement, not a block."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (rule.prelude.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@import"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
2025-04-08 14:33:01 +01:00
|
|
|
|
Optional<URL> url = parse_url_function(tokens);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
if (!url.has_value() && tokens.next_token().is(Token::Type::String))
|
2025-04-08 14:33:01 +01:00
|
|
|
|
url = URL { tokens.consume_a_token().token().string().to_string() };
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (!url.has_value()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@import"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = MUST(String::formatted("Unable to parse `{}` as URL.", tokens.next_token().to_debug_string())),
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
2025-12-04 14:15:07 +00:00
|
|
|
|
Optional<FlyString> layer;
|
|
|
|
|
|
// [ layer | layer(<layer-name>) ]?
|
|
|
|
|
|
if (tokens.next_token().is_ident("layer"sv)) {
|
|
|
|
|
|
tokens.discard_a_token(); // layer
|
|
|
|
|
|
layer = FlyString {};
|
|
|
|
|
|
} else if (tokens.next_token().is_function("layer"sv)) {
|
|
|
|
|
|
auto layer_transaction = tokens.begin_transaction();
|
|
|
|
|
|
auto& layer_function = tokens.consume_a_token().function();
|
|
|
|
|
|
TokenStream layer_tokens { layer_function.value };
|
|
|
|
|
|
auto name = parse_layer_name(layer_tokens, AllowBlankLayerName::No);
|
|
|
|
|
|
layer_tokens.discard_whitespace();
|
|
|
|
|
|
if (!name.has_value() || layer_tokens.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@import"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = MUST(String::formatted("Unable to parse `{}` as a valid layer.", layer_function.original_source_text())),
|
|
|
|
|
|
});
|
|
|
|
|
|
} else {
|
|
|
|
|
|
layer_transaction.commit();
|
|
|
|
|
|
layer = name.release_value();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// <import-conditions> = [ supports( [ <supports-condition> | <declaration> ] ) ]?
|
|
|
|
|
|
// <media-query-list>?
|
|
|
|
|
|
tokens.discard_whitespace();
|
2025-03-19 11:23:33 +00:00
|
|
|
|
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 };
|
2025-12-01 12:39:33 +00:00
|
|
|
|
supports = parse_a_supports(supports_tokens);
|
|
|
|
|
|
if (!supports) {
|
2025-04-14 17:10:20 +01:00
|
|
|
|
m_rule_context.append(RuleContext::SupportsCondition);
|
2025-12-01 12:54:40 +00:00
|
|
|
|
auto supports_declaration = parse_supports_declaration(supports_tokens);
|
2025-03-19 11:23:33 +00:00
|
|
|
|
m_rule_context.take_last();
|
2025-12-01 12:54:40 +00:00
|
|
|
|
if (supports_declaration)
|
2025-03-19 11:23:33 +00:00
|
|
|
|
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()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@import"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens in prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-04 16:48:19 +00:00
|
|
|
|
return CSSImportRule::create(realm(), url.release_value(), const_cast<DOM::Document*>(m_document.ptr()), move(layer), move(supports), MediaList::create(realm(), 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 16:00:11 +13:00
|
|
|
|
template<typename NestedDeclarationsRule>
|
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
|
2025-06-17 08:01:49 +01:00
|
|
|
|
if (rule.is_block_rule) {
|
2024-10-31 16:03:06 +00:00
|
|
|
|
// 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 {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@layer"_fly_string,
|
|
|
|
|
|
.prelude = prelude_tokens.dump_string(),
|
|
|
|
|
|
.description = "Not a valid layer name."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prelude_tokens.discard_whitespace();
|
|
|
|
|
|
if (prelude_tokens.has_next_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@layer"_fly_string,
|
|
|
|
|
|
.prelude = prelude_tokens.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after name in prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Then the rules
|
2025-04-14 16:02:29 +01:00
|
|
|
|
GC::RootVector<GC::Ref<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) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
if (auto child_rule = convert_to_rule<NestedDeclarationsRule>(rule, nested))
|
2025-04-14 16:02:29 +01:00
|
|
|
|
child_rules.append(*child_rule);
|
2024-11-04 17:26:19 +00:00
|
|
|
|
},
|
|
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
child_rules.append(NestedDeclarationsRule::create(realm(), *this, 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>#;
|
2025-07-23 10:34:28 +01:00
|
|
|
|
auto prelude_tokens = TokenStream { rule.prelude };
|
|
|
|
|
|
prelude_tokens.discard_whitespace();
|
2024-10-31 16:03:06 +00:00
|
|
|
|
Vector<FlyString> layer_names;
|
2025-07-23 10:34:28 +01:00
|
|
|
|
while (prelude_tokens.has_next_token()) {
|
2024-10-31 16:03:06 +00:00
|
|
|
|
// Comma
|
|
|
|
|
|
if (!layer_names.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
if (auto comma = prelude_tokens.consume_a_token(); !comma.is(Token::Type::Comma)) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@layer"_fly_string,
|
|
|
|
|
|
.prelude = prelude_tokens.dump_string(),
|
|
|
|
|
|
.description = "Missing comma between layer names."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2025-07-23 10:34:28 +01:00
|
|
|
|
prelude_tokens.discard_whitespace();
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 10:34:28 +01:00
|
|
|
|
if (auto name = parse_layer_name(prelude_tokens, AllowBlankLayerName::No); name.has_value()) {
|
2024-10-31 16:03:06 +00:00
|
|
|
|
layer_names.append(name.release_value());
|
|
|
|
|
|
} else {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@layer"_fly_string,
|
|
|
|
|
|
.prelude = prelude_tokens.dump_string(),
|
|
|
|
|
|
.description = "Contains invalid layer name."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2025-07-23 10:34:28 +01:00
|
|
|
|
prelude_tokens.discard_whitespace();
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (layer_names.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@layer"_fly_string,
|
|
|
|
|
|
.prelude = prelude_tokens.dump_string(),
|
|
|
|
|
|
.description = "No layer names provided."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
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]>
|
2025-07-23 10:34:28 +01:00
|
|
|
|
auto prelude_stream = TokenStream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
auto& token = prelude_stream.consume_a_token();
|
|
|
|
|
|
if (!token.is_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Name must be a <string> or <ident>."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto name_token = token.token();
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after name in prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 18:05:35 +13:00
|
|
|
|
if (name_token.is(Token::Type::Ident) && !is_valid_custom_ident(name_token.ident(), { { "none"sv } })) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Invalid name."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Name must be a <string> or <ident>."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto name = name_token.to_string();
|
|
|
|
|
|
|
2025-04-14 16:02:29 +01:00
|
|
|
|
GC::RootVector<GC::Ref<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()) {
|
2025-07-23 10:27:53 +01:00
|
|
|
|
for (auto const& child_rule : qualified_rule.child_rules) {
|
|
|
|
|
|
ErrorReporter::the().report(InvalidRuleLocationError {
|
|
|
|
|
|
.outer_rule_name = "@keyframes"_fly_string,
|
|
|
|
|
|
.inner_rule_name = child_rule.visit(
|
|
|
|
|
|
[](Rule const& rule) {
|
|
|
|
|
|
return rule.visit(
|
|
|
|
|
|
[](AtRule const& at_rule) { return MUST(String::formatted("@{}", at_rule.name)); },
|
|
|
|
|
|
[](QualifiedRule const&) { return "qualified-rule"_string; });
|
|
|
|
|
|
},
|
|
|
|
|
|
[](auto&) {
|
|
|
|
|
|
return "list-of-declarations"_string;
|
|
|
|
|
|
}),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 17:16:10 +01:00
|
|
|
|
auto selectors = Vector<Percentage> {};
|
2024-10-31 16:03:06 +00:00
|
|
|
|
TokenStream child_tokens { qualified_rule.prelude };
|
|
|
|
|
|
while (child_tokens.has_next_token()) {
|
|
|
|
|
|
child_tokens.discard_whitespace();
|
|
|
|
|
|
if (!child_tokens.has_next_token())
|
|
|
|
|
|
break;
|
2026-04-15 17:16:10 +01:00
|
|
|
|
auto& next_token = child_tokens.next_token();
|
|
|
|
|
|
if (!next_token.is_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "keyframe"_fly_string,
|
|
|
|
|
|
.prelude = child_tokens.dump_string(),
|
|
|
|
|
|
.description = "Invalid selector."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
auto read_a_selector = false;
|
2026-04-15 17:16:10 +01:00
|
|
|
|
if (next_token.is_ident("from"sv)) {
|
|
|
|
|
|
child_tokens.discard_a_token(); // from
|
|
|
|
|
|
selectors.append(Percentage(0));
|
|
|
|
|
|
read_a_selector = true;
|
|
|
|
|
|
} else if (next_token.is_ident("to"sv)) {
|
|
|
|
|
|
child_tokens.discard_a_token(); // to
|
|
|
|
|
|
selectors.append(Percentage(100));
|
|
|
|
|
|
read_a_selector = true;
|
|
|
|
|
|
} else if (next_token.is(Token::Type::Percentage)) {
|
|
|
|
|
|
child_tokens.discard_a_token(); // <percentage>
|
|
|
|
|
|
selectors.append(Percentage(next_token.token().percentage()));
|
2024-10-31 16:03:06 +00:00
|
|
|
|
read_a_selector = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (read_a_selector) {
|
|
|
|
|
|
child_tokens.discard_whitespace();
|
2026-04-15 17:16:10 +01:00
|
|
|
|
if (child_tokens.next_token().is(Token::Type::Comma)) {
|
|
|
|
|
|
child_tokens.discard_a_token(); // ,
|
2024-10-31 16:03:06 +00:00
|
|
|
|
continue;
|
2026-04-15 17:16:10 +01:00
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PropertiesAndCustomProperties properties;
|
2025-07-23 10:27:53 +01:00
|
|
|
|
qualified_rule.for_each_as_declaration_list("keyframe"_fly_string, [&](auto const& declaration) {
|
2024-10-31 16:03:06 +00:00
|
|
|
|
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-04-14 16:02:29 +01:00
|
|
|
|
return CSSKeyframesRule::create(realm(), name, CSSRuleList::create(realm(), 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>
|
2025-07-23 10:34:28 +01:00
|
|
|
|
auto tokens = TokenStream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@namespace"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Must be a statement, not a block."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (rule.prelude.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@namespace"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()) {
|
2025-04-08 14:33:01 +01:00
|
|
|
|
// "A URI string parsed from the URI syntax must be treated as a literal string: as with the STRING syntax, no
|
|
|
|
|
|
// URI-specific normalization is applied."
|
|
|
|
|
|
// https://drafts.csswg.org/css-namespaces/#syntax
|
|
|
|
|
|
namespace_uri = url->url();
|
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 {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@namespace"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Unable to parse <url>."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
if (tokens.has_next_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@namespace"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after <url> in prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-05 12:08:27 +00:00
|
|
|
|
return CSSNamespaceRule::create(realm(), prefix, namespace_uri);
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 16:00:11 +13:00
|
|
|
|
template<typename NestedDeclarationsRule>
|
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>
|
|
|
|
|
|
// }
|
2025-07-23 10:34:28 +01:00
|
|
|
|
auto supports_tokens = TokenStream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@supports"_fly_string,
|
|
|
|
|
|
.prelude = supports_tokens.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@supports"_fly_string,
|
|
|
|
|
|
.prelude = supports_tokens.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto supports = parse_a_supports(supports_tokens);
|
|
|
|
|
|
if (!supports) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@supports"_fly_string,
|
|
|
|
|
|
.prelude = supports_tokens.dump_string(),
|
|
|
|
|
|
.description = "Supports clause invalid."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-14 16:02:29 +01:00
|
|
|
|
GC::RootVector<GC::Ref<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) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
if (auto child_rule = convert_to_rule<NestedDeclarationsRule>(rule, nested))
|
2025-04-14 16:02:29 +01:00
|
|
|
|
child_rules.append(*child_rule);
|
2024-11-04 17:26:19 +00:00
|
|
|
|
},
|
|
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
child_rules.append(NestedDeclarationsRule::create(realm(), *this, 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>
|
|
|
|
|
|
// }
|
2025-07-23 10:34:28 +01:00
|
|
|
|
auto prelude_stream = TokenStream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@property"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@property"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
auto const& token = prelude_stream.consume_a_token();
|
|
|
|
|
|
if (!token.is_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@property"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Name must be an ident."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto name_token = token.token();
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@property"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after name in prelude."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 10:34:28 +01:00
|
|
|
|
if (!name_token.is(Token::Type::Ident) || !is_a_custom_property_name_string(name_token.ident())) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@property"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Name must be an ident starting with '--'."_string,
|
|
|
|
|
|
});
|
2024-10-31 16:03:06 +00:00
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto const& name = name_token.ident();
|
|
|
|
|
|
|
|
|
|
|
|
Optional<FlyString> syntax_maybe;
|
|
|
|
|
|
Optional<bool> inherits_maybe;
|
2025-08-08 10:11:51 +01:00
|
|
|
|
RefPtr<StyleValue const> initial_value_maybe;
|
2024-10-31 16:03:06 +00:00
|
|
|
|
|
|
|
|
|
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
2025-04-07 11:34:53 +01:00
|
|
|
|
if (auto descriptor = convert_to_descriptor(AtRuleID::Property, declaration); descriptor.has_value()) {
|
2026-03-05 00:03:57 +13:00
|
|
|
|
if (descriptor->descriptor_name_and_id.id() == DescriptorID::Syntax) {
|
2025-04-07 11:34:53 +01:00
|
|
|
|
if (descriptor->value->is_string())
|
|
|
|
|
|
syntax_maybe = descriptor->value->as_string().string_value();
|
|
|
|
|
|
return;
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
2026-03-05 00:03:57 +13:00
|
|
|
|
if (descriptor->descriptor_name_and_id.id() == DescriptorID::Inherits) {
|
2025-04-07 11:34:53 +01:00
|
|
|
|
switch (descriptor->value->to_keyword()) {
|
|
|
|
|
|
case Keyword::True:
|
|
|
|
|
|
inherits_maybe = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case Keyword::False:
|
|
|
|
|
|
inherits_maybe = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
break;
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
2025-04-07 11:34:53 +01:00
|
|
|
|
return;
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
2026-03-05 00:03:57 +13:00
|
|
|
|
if (descriptor->descriptor_name_and_id.id() == DescriptorID::InitialValue) {
|
2025-04-07 11:54:51 +01:00
|
|
|
|
initial_value_maybe = *descriptor->value;
|
2025-04-07 11:34:53 +01:00
|
|
|
|
return;
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-07-23 12:20:01 +02:00
|
|
|
|
// @property rules require a syntax and inherits descriptor; if either are missing, the entire rule is invalid and must be ignored.
|
|
|
|
|
|
if (!syntax_maybe.has_value() || syntax_maybe->is_empty() || !inherits_maybe.has_value()) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 10:22:47 +05:30
|
|
|
|
CSS::Parser::ParsingParams parsing_params;
|
|
|
|
|
|
if (document())
|
|
|
|
|
|
parsing_params = CSS::Parser::ParsingParams { *document() };
|
|
|
|
|
|
else
|
|
|
|
|
|
parsing_params = CSS::Parser::ParsingParams { realm() };
|
|
|
|
|
|
|
2025-07-23 12:20:01 +02:00
|
|
|
|
auto syntax_component_values = parse_component_values_list(parsing_params, syntax_maybe.value());
|
2026-03-09 18:08:07 +13:00
|
|
|
|
auto maybe_syntax = parse_as_syntax(syntax_component_values, LimitSingleComponentIdentToCustomIdent::Yes);
|
2025-07-23 12:20:01 +02:00
|
|
|
|
|
|
|
|
|
|
// If the provided string is not a valid syntax string (if it returns failure when consume
|
|
|
|
|
|
// a syntax definition is called on it), the descriptor is invalid and must be ignored.
|
|
|
|
|
|
if (!maybe_syntax) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
// The initial-value descriptor is optional only if the syntax is the universal syntax definition,
|
|
|
|
|
|
// otherwise the descriptor is required; if it’s missing, the entire rule is invalid and must be ignored.
|
|
|
|
|
|
if (!initial_value_maybe && maybe_syntax->type() != CSS::Parser::SyntaxNode::NodeType::Universal) {
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (initial_value_maybe) {
|
|
|
|
|
|
initial_value_maybe = Web::CSS::Parser::parse_with_a_syntax(parsing_params, initial_value_maybe->tokenize(), *maybe_syntax);
|
2026-03-08 16:19:00 +13:00
|
|
|
|
|
2025-07-23 12:20:01 +02:00
|
|
|
|
// Otherwise, if the value of the syntax descriptor is not the universal syntax definition,
|
|
|
|
|
|
// the following conditions must be met for the @property rule to be valid:
|
2026-03-08 16:19:00 +13:00
|
|
|
|
if (maybe_syntax->type() != CSS::Parser::SyntaxNode::NodeType::Universal) {
|
|
|
|
|
|
// - The initial-value descriptor must be present.
|
|
|
|
|
|
// - The initial-value descriptor’s value must parse successfully according to the grammar specified by the syntax definition.
|
|
|
|
|
|
// - The initial-value must be computationally independent.
|
|
|
|
|
|
if (!initial_value_maybe || initial_value_maybe->is_guaranteed_invalid() || !initial_value_maybe->is_computationally_independent())
|
|
|
|
|
|
return {};
|
2025-07-23 12:20:01 +02:00
|
|
|
|
}
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
2025-07-23 12:20:01 +02:00
|
|
|
|
|
|
|
|
|
|
return CSSPropertyRule::create(realm(), name, syntax_maybe.value(), inherits_maybe.value(), move(initial_value_maybe));
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 11:57:30 +00:00
|
|
|
|
// https://drafts.csswg.org/css-conditional-5/#container-rule
|
|
|
|
|
|
template<typename NestedDeclarationsRule>
|
|
|
|
|
|
GC::Ptr<CSSContainerRule> Parser::convert_to_container_rule(AtRule const& rule, Nested nested)
|
|
|
|
|
|
{
|
|
|
|
|
|
// @container <container-condition># {
|
|
|
|
|
|
// <block-contents>
|
|
|
|
|
|
// }
|
|
|
|
|
|
// <container-condition> = [ <container-name>? <container-query>? ]!
|
|
|
|
|
|
// <container-name> = <custom-ident>
|
|
|
|
|
|
|
|
|
|
|
|
TokenStream prelude_stream { rule.prelude };
|
|
|
|
|
|
if (!rule.is_block_rule) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@container"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto prelude_item_values = parse_a_comma_separated_list_of_component_values(prelude_stream);
|
|
|
|
|
|
if (prelude_item_values.is_empty()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@container"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vector<CSSContainerRule::Condition> conditions;
|
|
|
|
|
|
conditions.ensure_capacity(prelude_item_values.size());
|
|
|
|
|
|
|
|
|
|
|
|
for (auto const& prelude_item : prelude_item_values) {
|
|
|
|
|
|
TokenStream item_tokens { prelude_item };
|
|
|
|
|
|
item_tokens.discard_whitespace();
|
|
|
|
|
|
// https://drafts.csswg.org/css-conditional-5/#container-name
|
|
|
|
|
|
// The keywords none, and, not, and or are excluded from this <custom-ident>.
|
|
|
|
|
|
auto container_name = parse_custom_ident(item_tokens, { { "none"sv, "and"sv, "not"sv, "or"sv } });
|
|
|
|
|
|
item_tokens.discard_whitespace();
|
|
|
|
|
|
auto container_query = parse_container_query(item_tokens);
|
|
|
|
|
|
if (!container_name.has_value() && !container_query) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@container"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Missing container name or query."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
item_tokens.discard_whitespace();
|
|
|
|
|
|
if (item_tokens.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@container"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after name and query."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
conditions.unchecked_empend(move(container_name), move(container_query));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GC::RootVector<GC::Ref<CSSRule>> child_rules { realm().heap() };
|
|
|
|
|
|
for (auto const& child : rule.child_rules_and_lists_of_declarations) {
|
|
|
|
|
|
child.visit(
|
|
|
|
|
|
[&](Rule const& child_rule) {
|
|
|
|
|
|
if (auto converted_rule = convert_to_rule<NestedDeclarationsRule>(child_rule, nested))
|
|
|
|
|
|
child_rules.append(*converted_rule);
|
|
|
|
|
|
},
|
|
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
|
|
|
|
|
child_rules.append(CSSNestedDeclarations::create(realm(), *convert_to_style_declaration(declarations)));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto rule_list = CSSRuleList::create(realm(), child_rules);
|
|
|
|
|
|
return CSSContainerRule::create(realm(), move(conditions), rule_list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 01:03:29 +13:00
|
|
|
|
GC::Ptr<CSSCounterStyleRule> Parser::convert_to_counter_style_rule(AtRule const& rule)
|
|
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#the-counter-style-rule
|
|
|
|
|
|
TokenStream prelude_stream { rule.prelude };
|
|
|
|
|
|
if (!rule.is_block_rule) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@counter-style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (rule.prelude.is_empty()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@counter-style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty prelude."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto name = parse_counter_style_name(prelude_stream);
|
|
|
|
|
|
if (!name.has_value()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@counter-style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Missing counter style name."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@counter-style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after name in prelude."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-counter-styles-3/#typedef-counter-style-name
|
|
|
|
|
|
// When used here, to define a counter style, it also cannot be any of the non-overridable counter-style names
|
|
|
|
|
|
// FIXME: We should allow these in the UA stylesheet in order to initially define them.
|
2026-02-07 22:01:03 +13:00
|
|
|
|
if (CSSCounterStyleRule::matches_non_overridable_counter_style_name(name.value()) && m_is_ua_style_sheet != IsUAStyleSheet::Yes) {
|
2026-01-31 01:03:29 +13:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@counter-style"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Non-overridable counter style name."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 21:09:11 +13:00
|
|
|
|
RefPtr<StyleValue const> system;
|
2026-01-31 21:24:22 +13:00
|
|
|
|
RefPtr<StyleValue const> negative;
|
2026-01-31 21:33:51 +13:00
|
|
|
|
RefPtr<StyleValue const> prefix;
|
|
|
|
|
|
RefPtr<StyleValue const> suffix;
|
2026-01-31 21:50:45 +13:00
|
|
|
|
RefPtr<StyleValue const> range;
|
2026-01-31 22:04:08 +13:00
|
|
|
|
RefPtr<StyleValue const> pad;
|
2026-01-31 22:13:31 +13:00
|
|
|
|
RefPtr<StyleValue const> fallback;
|
2026-01-31 23:11:29 +13:00
|
|
|
|
RefPtr<StyleValue const> symbols;
|
2026-02-01 11:09:47 +13:00
|
|
|
|
RefPtr<StyleValue const> additive_symbols;
|
2026-02-01 11:15:14 +13:00
|
|
|
|
RefPtr<StyleValue const> speak_as;
|
2026-01-31 01:03:29 +13:00
|
|
|
|
|
2026-01-31 21:09:11 +13:00
|
|
|
|
rule.for_each_as_declaration_list([&](auto& declaration) {
|
|
|
|
|
|
auto const& descriptor = convert_to_descriptor(AtRuleID::CounterStyle, declaration);
|
|
|
|
|
|
if (!descriptor.has_value())
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
2026-03-05 00:03:57 +13:00
|
|
|
|
switch (descriptor->descriptor_name_and_id.id()) {
|
2026-01-31 21:09:11 +13:00
|
|
|
|
case DescriptorID::System:
|
|
|
|
|
|
system = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 21:24:22 +13:00
|
|
|
|
case DescriptorID::Negative:
|
|
|
|
|
|
negative = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 21:33:51 +13:00
|
|
|
|
case DescriptorID::Prefix:
|
|
|
|
|
|
prefix = descriptor->value;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case DescriptorID::Suffix:
|
|
|
|
|
|
suffix = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 21:50:45 +13:00
|
|
|
|
case DescriptorID::Range:
|
|
|
|
|
|
range = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 22:04:08 +13:00
|
|
|
|
case DescriptorID::Pad:
|
|
|
|
|
|
pad = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 22:13:31 +13:00
|
|
|
|
case DescriptorID::Fallback:
|
|
|
|
|
|
fallback = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 23:11:29 +13:00
|
|
|
|
case DescriptorID::Symbols:
|
|
|
|
|
|
symbols = descriptor->value;
|
|
|
|
|
|
break;
|
2026-02-01 11:09:47 +13:00
|
|
|
|
case DescriptorID::AdditiveSymbols:
|
|
|
|
|
|
additive_symbols = descriptor->value;
|
|
|
|
|
|
break;
|
2026-02-01 11:15:14 +13:00
|
|
|
|
case DescriptorID::SpeakAs:
|
|
|
|
|
|
speak_as = descriptor->value;
|
|
|
|
|
|
break;
|
2026-01-31 21:09:11 +13:00
|
|
|
|
default:
|
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-02-01 11:15:14 +13:00
|
|
|
|
return CSSCounterStyleRule::create(realm(), name.release_value(), move(system), move(negative), move(prefix), move(suffix), move(range), move(pad), move(fallback), move(symbols), move(additive_symbols), move(speak_as));
|
2026-01-31 01:03:29 +13:00
|
|
|
|
}
|
|
|
|
|
|
|
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-07-23 10:34:28 +01:00
|
|
|
|
TokenStream prelude_stream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@font-face"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
2025-07-23 10:51:23 +01:00
|
|
|
|
|
|
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@font-face"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Prelude is not allowed."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2025-06-24 19:47:49 +01:00
|
|
|
|
|
2025-05-13 13:54:43 +01:00
|
|
|
|
DescriptorList descriptors { AtRuleID::FontFace };
|
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()) {
|
|
|
|
|
|
descriptors.append(descriptor.release_value());
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-13 13:54:43 +01:00
|
|
|
|
return CSSFontFaceRule::create(realm(), CSSFontFaceDescriptors::create(realm(), descriptors.release_descriptors()));
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-14 17:06:59 +13:00
|
|
|
|
Optional<Vector<FlyString>> Parser::parse_comma_separated_family_name_list(TokenStream<ComponentValue>& tokens)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector<FlyString> family_names;
|
|
|
|
|
|
auto comma_separated_families = parse_a_comma_separated_list_of_component_values(tokens);
|
|
|
|
|
|
|
|
|
|
|
|
if (comma_separated_families.is_empty()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@font-feature-values"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Empty family name list."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (auto const& family_component_values : comma_separated_families) {
|
|
|
|
|
|
TokenStream family_stream { family_component_values };
|
|
|
|
|
|
auto family_name = parse_family_name_value(family_stream);
|
|
|
|
|
|
|
|
|
|
|
|
if (!family_name || family_stream.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@font-feature-values"_fly_string,
|
|
|
|
|
|
.prelude = family_stream.dump_string(),
|
|
|
|
|
|
.description = "Invalid family name."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
family_names.append(string_from_style_value(family_name.release_nonnull()));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return family_names;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GC::Ptr<CSSFontFeatureValuesRule> Parser::convert_to_font_feature_values_rule(AtRule const& rule)
|
|
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-fonts-4/#font-feature-values-syntax
|
|
|
|
|
|
// @font-feature-values = @font-feature-values <family-name># { <declaration-rule-list> }
|
|
|
|
|
|
auto prelude_stream = TokenStream { rule.prelude };
|
|
|
|
|
|
if (!rule.is_block_rule) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@font-feature-values"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto family_names = parse_comma_separated_family_name_list(prelude_stream);
|
|
|
|
|
|
|
|
|
|
|
|
if (!family_names.has_value())
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
auto font_feature_values_rule = CSSFontFeatureValuesRule::create(realm(), family_names.release_value());
|
|
|
|
|
|
|
|
|
|
|
|
rule.for_each_as_declaration_rule_list(
|
|
|
|
|
|
[&](AtRule const& at_rule) {
|
|
|
|
|
|
// <font-feature-value-type> = <@stylistic> | <@historical-forms> | <@styleset> | <@character-variant> | <@swash> | <@ornaments> | <@annotation>
|
|
|
|
|
|
// @stylistic = @stylistic { <declaration-list> }
|
|
|
|
|
|
// @historical-forms = @historical-forms { <declaration-list> }
|
|
|
|
|
|
// @styleset = @styleset { <declaration-list> }
|
|
|
|
|
|
// @character-variant = @character-variant { <declaration-list> }
|
|
|
|
|
|
// @swash = @swash { <declaration-list> }
|
|
|
|
|
|
// @ornaments = @ornaments { <declaration-list> }
|
|
|
|
|
|
// @annotation = @annotation { <declaration-list> }
|
|
|
|
|
|
|
|
|
|
|
|
GC::Ptr<CSSFontFeatureValuesMap> feature_values_map;
|
|
|
|
|
|
size_t max_value_count = 1;
|
|
|
|
|
|
|
|
|
|
|
|
if (at_rule.name.equals_ignoring_ascii_case("stylistic"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->stylistic();
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("historical-forms"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->historical_forms();
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("styleset"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->styleset();
|
|
|
|
|
|
max_value_count = NumericLimits<size_t>::max();
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("character-variant"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->character_variant();
|
|
|
|
|
|
max_value_count = 2;
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("swash"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->swash();
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("ornaments"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->ornaments();
|
|
|
|
|
|
} else if (at_rule.name.equals_ignoring_ascii_case("annotation"sv)) {
|
|
|
|
|
|
feature_values_map = font_feature_values_rule->annotation();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// NB: Other at-rules are disallowed in this context and should have already been dropped
|
|
|
|
|
|
VERIFY_NOT_REACHED();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
at_rule.for_each_as_declaration_list([&](Declaration const& declaration) {
|
|
|
|
|
|
auto value_stream = TokenStream { declaration.value };
|
|
|
|
|
|
|
|
|
|
|
|
if (declaration.important == Important::Yes) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", at_rule.name)),
|
|
|
|
|
|
.prelude = value_stream.dump_string(),
|
|
|
|
|
|
.description = "Declarations in @font-feature-values rules cannot be marked !important."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
value_stream.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
if (!value_stream.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", at_rule.name)),
|
|
|
|
|
|
.prelude = value_stream.dump_string(),
|
|
|
|
|
|
.description = "Empty feature value."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vector<u32> values;
|
|
|
|
|
|
|
|
|
|
|
|
while (value_stream.has_next_token()) {
|
|
|
|
|
|
auto token = value_stream.consume_a_token();
|
|
|
|
|
|
|
|
|
|
|
|
// FIXME: Support calc()
|
|
|
|
|
|
if (!token.is(Token::Type::Number) || !token.token().number().is_integer() || token.token().number().value() < 0) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", at_rule.name)),
|
|
|
|
|
|
.prelude = value_stream.dump_string(),
|
|
|
|
|
|
.description = "Feature value entry must be a non-negative integer."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
values.append(token.token().number().integer_value());
|
|
|
|
|
|
|
|
|
|
|
|
value_stream.discard_whitespace();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (values.size() > max_value_count) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", at_rule.name)),
|
|
|
|
|
|
.prelude = value_stream.dump_string(),
|
|
|
|
|
|
.description = MUST(String::formatted("Too many feature values provided (maximum {})."_string, max_value_count)),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MUST(feature_values_map->set(declaration.name.to_string(), move(values)));
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
[&](Declaration const&) {
|
|
|
|
|
|
// FIXME: Handle the `font-display` descriptor here, see
|
|
|
|
|
|
// https://drafts.csswg.org/css-fonts-4/#font-display-font-feature-values
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return font_feature_values_rule;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 02:07:49 +13:00
|
|
|
|
static OwnPtr<SyntaxNode> parse_css_type(TokenStream<ComponentValue>& tokens)
|
|
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-mixins-1/#function-rule
|
|
|
|
|
|
// <css-type> = <syntax-component> | <type()>
|
|
|
|
|
|
// <type()> = type( <syntax> )
|
|
|
|
|
|
|
|
|
|
|
|
auto transaction = tokens.begin_transaction();
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
// <syntax-component>
|
|
|
|
|
|
if (auto maybe_syntax_component = parse_syntax_component(tokens)) {
|
|
|
|
|
|
transaction.commit();
|
|
|
|
|
|
return maybe_syntax_component;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// <type()>
|
|
|
|
|
|
auto maybe_type_function_token = tokens.consume_a_token();
|
|
|
|
|
|
|
|
|
|
|
|
if (!maybe_type_function_token.is_function("type"sv))
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
if (auto maybe_type_function_syntax = parse_as_syntax(maybe_type_function_token.function().value)) {
|
|
|
|
|
|
transaction.commit();
|
|
|
|
|
|
return maybe_type_function_syntax;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Optional<Parser::FunctionPrelude> Parser::parse_function_prelude(TokenStream<ComponentValue>& tokens)
|
|
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-mixins-1/#function-rule
|
|
|
|
|
|
// <function-token> <function-parameter>#? ) [ returns <css-type> ]?
|
|
|
|
|
|
// <function-parameter> = <custom-property-name> <css-type>? [ : <default-value> ]?
|
|
|
|
|
|
// <default-value> = <declaration-value>
|
|
|
|
|
|
auto transaction = tokens.begin_transaction();
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
auto const& function_token = tokens.consume_a_token();
|
|
|
|
|
|
|
|
|
|
|
|
if (!function_token.is_function()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Prelude must start with a function token."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto function_name = function_token.function().name;
|
|
|
|
|
|
|
|
|
|
|
|
// The <function-token> production must start with two dashes (U+002D HYPHEN-MINUS), similar to <dashed-ident>, or
|
|
|
|
|
|
// else the definition is invalid.
|
|
|
|
|
|
if (!function_name.starts_with_bytes("--"sv)) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Function name must start with two dashes."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Vector<FunctionParameterInternal> parsed_parameters;
|
|
|
|
|
|
|
|
|
|
|
|
TokenStream parameters_tokens { function_token.function().value };
|
|
|
|
|
|
parameters_tokens.discard_whitespace();
|
|
|
|
|
|
auto parameters_component_values = parse_a_comma_separated_list_of_component_values(parameters_tokens);
|
|
|
|
|
|
|
|
|
|
|
|
// <function-parameter>#?
|
|
|
|
|
|
for (auto const& parameter_component_values : parameters_component_values) {
|
|
|
|
|
|
TokenStream parameter_tokens { parameter_component_values };
|
|
|
|
|
|
parameter_tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
// <custom-property-name>
|
|
|
|
|
|
auto maybe_name = parse_dashed_ident(parameter_tokens);
|
|
|
|
|
|
if (!maybe_name.has_value() || !is_a_custom_property_name_string(maybe_name.value())) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = parameter_tokens.dump_string(),
|
|
|
|
|
|
.description = "Parameter must have a name."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// <css-type>?
|
|
|
|
|
|
NonnullOwnPtr<SyntaxNode> type = UniversalSyntaxNode::create();
|
|
|
|
|
|
if (auto maybe_type = parse_css_type(parameter_tokens))
|
|
|
|
|
|
type = maybe_type.release_nonnull();
|
|
|
|
|
|
|
|
|
|
|
|
parameter_tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
// [ : <default-value> ]?
|
|
|
|
|
|
Optional<Vector<ComponentValue>> default_value;
|
2026-04-16 11:48:39 +01:00
|
|
|
|
if (parameter_tokens.next_token().is(Token::Type::Colon)) {
|
2026-03-07 02:07:49 +13:00
|
|
|
|
parameter_tokens.discard_a_token(); // :
|
|
|
|
|
|
parameter_tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
auto maybe_default_value = parse_declaration_value(parameter_tokens);
|
|
|
|
|
|
|
|
|
|
|
|
if (!maybe_default_value.has_value()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = parameter_tokens.dump_string(),
|
|
|
|
|
|
.description = "Expected default value after ':' in parameter"_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// If a default value and a parameter type are both provided, then the default value must parse successfully
|
|
|
|
|
|
// according to that parameter type’s syntax. Otherwise, the @function rule is invalid.
|
2026-03-28 22:41:48 +13:00
|
|
|
|
// FIXME: Chrome allows ASFs regardless of the parameter's type
|
2026-03-07 02:07:49 +13:00
|
|
|
|
TokenStream default_value_token_stream { maybe_default_value.value() };
|
|
|
|
|
|
if (!parse_according_to_syntax_node(default_value_token_stream, *type) || !default_value_token_stream.is_empty())
|
|
|
|
|
|
return {};
|
|
|
|
|
|
|
|
|
|
|
|
default_value = maybe_default_value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parameter_tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
if (!parameter_tokens.is_empty()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = parameter_tokens.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens after parameter"_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
parsed_parameters.append({ maybe_name.release_value(), move(type), move(default_value) });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
NonnullOwnPtr<SyntaxNode> return_type = UniversalSyntaxNode::create();
|
2026-04-16 11:48:39 +01:00
|
|
|
|
if (tokens.next_token().is_ident("returns"sv)) {
|
2026-03-07 02:07:49 +13:00
|
|
|
|
tokens.discard_a_token();
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
auto maybe_return_type = parse_css_type(tokens);
|
|
|
|
|
|
|
|
|
|
|
|
if (!maybe_return_type) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Expected return type after 'returns' in prelude."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return_type = maybe_return_type.release_nonnull();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokens.discard_whitespace();
|
|
|
|
|
|
|
|
|
|
|
|
if (tokens.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Trailing tokens in prelude."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
transaction.commit();
|
|
|
|
|
|
return FunctionPrelude { move(function_name), move(parsed_parameters), move(return_type) };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
GC::Ptr<CSSFunctionRule> Parser::convert_to_function_rule(AtRule const& function_rule)
|
|
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-mixins-1/#function-rule
|
|
|
|
|
|
TokenStream prelude_stream { function_rule.prelude };
|
|
|
|
|
|
|
|
|
|
|
|
if (!function_rule.is_block_rule) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@function"_fly_string,
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto prelude = parse_function_prelude(prelude_stream);
|
|
|
|
|
|
|
|
|
|
|
|
if (!prelude.has_value())
|
|
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
Vector<GC::Ref<CSSRule>> child_rules {};
|
|
|
|
|
|
|
|
|
|
|
|
// https://drafts.csswg.org/css-mixins-1/#function-body
|
|
|
|
|
|
for (auto const& child : function_rule.child_rules_and_lists_of_declarations) {
|
|
|
|
|
|
child.visit(
|
|
|
|
|
|
[&](Rule const& rule) {
|
|
|
|
|
|
if (auto child_rule = convert_to_rule<CSSFunctionDeclarations>(rule, Nested::Yes))
|
|
|
|
|
|
child_rules.append(*child_rule);
|
|
|
|
|
|
},
|
|
|
|
|
|
[&](Vector<Declaration> const& declarations) {
|
|
|
|
|
|
child_rules.append(CSSFunctionDeclarations::create(realm(), *this, declarations));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return CSSFunctionRule::create(realm(), CSSRuleList::create(realm(), child_rules), move(prelude->name), move(prelude->parameters), move(prelude->return_type));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-15 11:48:56 +01:00
|
|
|
|
GC::Ptr<CSSPageRule> Parser::convert_to_page_rule(AtRule const& page_rule)
|
2025-05-13 13:05:08 +01:00
|
|
|
|
{
|
|
|
|
|
|
// https://drafts.csswg.org/css-page-3/#syntax-page-selector
|
|
|
|
|
|
// @page = @page <page-selector-list>? { <declaration-rule-list> }
|
2025-07-23 10:34:28 +01:00
|
|
|
|
TokenStream tokens { page_rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!page_rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = "@page"_fly_string,
|
|
|
|
|
|
.prelude = tokens.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-16 11:37:53 +01:00
|
|
|
|
auto page_selectors = parse_a_page_selector_list(tokens);
|
|
|
|
|
|
if (page_selectors.is_error())
|
2025-05-13 13:05:08 +01:00
|
|
|
|
return nullptr;
|
|
|
|
|
|
|
2025-05-13 12:17:41 +01:00
|
|
|
|
GC::RootVector<GC::Ref<CSSRule>> child_rules { realm().heap() };
|
2025-05-13 13:54:43 +01:00
|
|
|
|
DescriptorList descriptors { AtRuleID::Page };
|
2025-05-15 11:48:56 +01:00
|
|
|
|
page_rule.for_each_as_declaration_rule_list(
|
2025-05-13 12:17:41 +01:00
|
|
|
|
[&](auto& at_rule) {
|
2026-03-05 16:00:11 +13:00
|
|
|
|
if (auto converted_rule = convert_to_rule<CSSNestedDeclarations>(at_rule, Nested::No)) {
|
2025-05-15 11:48:56 +01:00
|
|
|
|
if (is<CSSMarginRule>(*converted_rule)) {
|
|
|
|
|
|
child_rules.append(*converted_rule);
|
|
|
|
|
|
} else {
|
2025-07-23 10:27:53 +01:00
|
|
|
|
ErrorReporter::the().report(InvalidRuleLocationError {
|
|
|
|
|
|
.outer_rule_name = "@page"_fly_string,
|
|
|
|
|
|
.inner_rule_name = MUST(FlyString::from_utf8(converted_rule->class_name())),
|
|
|
|
|
|
});
|
2025-05-15 11:48:56 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-05-13 12:17:41 +01:00
|
|
|
|
},
|
|
|
|
|
|
[&](auto& declaration) {
|
|
|
|
|
|
if (auto descriptor = convert_to_descriptor(AtRuleID::Page, declaration); descriptor.has_value()) {
|
|
|
|
|
|
descriptors.append(descriptor.release_value());
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
auto rule_list = CSSRuleList::create(realm(), child_rules);
|
2025-05-13 13:54:43 +01:00
|
|
|
|
return CSSPageRule::create(realm(), page_selectors.release_value(), CSSPageDescriptors::create(realm(), descriptors.release_descriptors()), rule_list);
|
2025-05-13 12:17:41 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-15 11:48:56 +01:00
|
|
|
|
GC::Ptr<CSSMarginRule> Parser::convert_to_margin_rule(AtRule const& rule)
|
|
|
|
|
|
{
|
2025-07-23 10:34:28 +01:00
|
|
|
|
TokenStream prelude_stream { rule.prelude };
|
2025-06-24 19:47:49 +01:00
|
|
|
|
if (!rule.is_block_rule) {
|
2025-07-23 10:34:28 +01:00
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", rule.name)),
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Must be a block, not a statement."_string,
|
|
|
|
|
|
});
|
2025-06-24 19:47:49 +01:00
|
|
|
|
return nullptr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-23 10:51:23 +01:00
|
|
|
|
prelude_stream.discard_whitespace();
|
|
|
|
|
|
if (prelude_stream.has_next_token()) {
|
|
|
|
|
|
ErrorReporter::the().report(CSS::Parser::InvalidRuleError {
|
|
|
|
|
|
.rule_name = MUST(String::formatted("@{}", rule.name)),
|
|
|
|
|
|
.prelude = prelude_stream.dump_string(),
|
|
|
|
|
|
.description = "Prelude is not allowed."_string,
|
|
|
|
|
|
});
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2025-07-23 10:34:28 +01:00
|
|
|
|
|
2025-05-15 11:48:56 +01:00
|
|
|
|
// https://drafts.csswg.org/css-page-3/#syntax-page-selector
|
|
|
|
|
|
// There are lots of these, but they're all in the format:
|
|
|
|
|
|
// @foo = @foo { <declaration-list> };
|
|
|
|
|
|
|
|
|
|
|
|
// FIXME: The declaration list should be a CSSMarginDescriptors, but that has no spec definition:
|
|
|
|
|
|
// https://github.com/w3c/csswg-drafts/issues/10106
|
|
|
|
|
|
// So, we just parse a CSSStyleProperties instead for now.
|
|
|
|
|
|
PropertiesAndCustomProperties properties;
|
|
|
|
|
|
rule.for_each_as_declaration_list([&](auto const& declaration) {
|
|
|
|
|
|
extract_property(declaration, properties);
|
|
|
|
|
|
});
|
|
|
|
|
|
auto style = CSSStyleProperties::create(realm(), move(properties.properties), move(properties.custom_properties));
|
|
|
|
|
|
return CSSMarginRule::create(realm(), rule.name, style);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-04 22:40:51 +13:00
|
|
|
|
template<typename Descriptors>
|
|
|
|
|
|
GC::Ref<Descriptors> Parser::convert_to_descriptors(AtRuleID at_rule_id, Vector<Declaration> const& declarations)
|
|
|
|
|
|
{
|
|
|
|
|
|
DescriptorList descriptor_list { at_rule_id };
|
|
|
|
|
|
|
|
|
|
|
|
for (auto const& declaration : declarations) {
|
|
|
|
|
|
if (auto descriptor = convert_to_descriptor(at_rule_id, declaration); descriptor.has_value())
|
|
|
|
|
|
descriptor_list.append(descriptor.release_value());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Descriptors::create(realm(), descriptor_list.release_descriptors());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
template GC::Ref<CSSFunctionDescriptors> Parser::convert_to_descriptors(AtRuleID at_rule_id, Vector<Declaration> const& declarations);
|
|
|
|
|
|
|
2026-03-27 11:57:30 +00:00
|
|
|
|
template GC::Ptr<CSSRule> Parser::convert_to_rule<CSSNestedDeclarations>(Rule const&, Nested);
|
|
|
|
|
|
template GC::Ptr<CSSRule> Parser::convert_to_rule<CSSFunctionDeclarations>(Rule const&, Nested);
|
|
|
|
|
|
|
|
|
|
|
|
template GC::Ptr<CSSContainerRule> Parser::convert_to_container_rule<CSSNestedDeclarations>(AtRule const&, Nested);
|
|
|
|
|
|
template GC::Ptr<CSSContainerRule> Parser::convert_to_container_rule<CSSFunctionDeclarations>(AtRule const&, Nested);
|
2026-03-05 16:00:11 +13:00
|
|
|
|
|
2026-03-27 11:57:30 +00:00
|
|
|
|
template GC::Ptr<CSSRule> Parser::convert_to_layer_rule<CSSNestedDeclarations>(AtRule const& rule, Nested);
|
2026-03-05 16:00:11 +13:00
|
|
|
|
|
2026-03-27 11:57:30 +00:00
|
|
|
|
template GC::Ptr<CSSSupportsRule> Parser::convert_to_supports_rule<CSSNestedDeclarations>(AtRule const&, Nested);
|
|
|
|
|
|
template GC::Ptr<CSSSupportsRule> Parser::convert_to_supports_rule<CSSFunctionDeclarations>(AtRule const&, Nested);
|
2026-03-05 16:00:11 +13:00
|
|
|
|
|
2024-10-31 16:03:06 +00:00
|
|
|
|
}
|