diff --git a/Libraries/LibWeb/CSS/CSSPageRule.cpp b/Libraries/LibWeb/CSS/CSSPageRule.cpp index 4acd8b73eea..23f98374d3b 100644 --- a/Libraries/LibWeb/CSS/CSSPageRule.cpp +++ b/Libraries/LibWeb/CSS/CSSPageRule.cpp @@ -15,12 +15,56 @@ namespace Web::CSS { GC_DEFINE_ALLOCATOR(CSSPageRule); -GC::Ref CSSPageRule::create(JS::Realm& realm, SelectorList&& selectors, GC::Ref style, CSSRuleList& rules) +Optional page_pseudo_class_from_string(StringView input) +{ + if (input.equals_ignoring_ascii_case("blank"sv)) + return PagePseudoClass::Blank; + if (input.equals_ignoring_ascii_case("first"sv)) + return PagePseudoClass::First; + if (input.equals_ignoring_ascii_case("left"sv)) + return PagePseudoClass::Left; + if (input.equals_ignoring_ascii_case("right"sv)) + return PagePseudoClass::Right; + return {}; +} + +StringView to_string(PagePseudoClass pseudo_class) +{ + switch (pseudo_class) { + case PagePseudoClass::Blank: + return "blank"sv; + case PagePseudoClass::First: + return "first"sv; + case PagePseudoClass::Left: + return "left"sv; + case PagePseudoClass::Right: + return "right"sv; + } + VERIFY_NOT_REACHED(); +} + +PageSelector::PageSelector(Optional name, Vector pseudo_classes) + : m_name(move(name)) + , m_pseudo_classes(move(pseudo_classes)) +{ +} + +String PageSelector::serialize() const +{ + StringBuilder builder; + if (m_name.has_value()) + builder.append(m_name.value()); + for (auto pseudo_class : m_pseudo_classes) + builder.appendff(":{}", to_string(pseudo_class)); + return builder.to_string_without_validation(); +} + +GC::Ref CSSPageRule::create(JS::Realm& realm, PageSelectorList&& selectors, GC::Ref style, CSSRuleList& rules) { return realm.create(realm, move(selectors), style, rules); } -CSSPageRule::CSSPageRule(JS::Realm& realm, SelectorList&& selectors, GC::Ref style, CSSRuleList& rules) +CSSPageRule::CSSPageRule(JS::Realm& realm, PageSelectorList&& selectors, GC::Ref style, CSSRuleList& rules) : CSSGroupingRule(realm, rules, Type::Page) , m_selectors(move(selectors)) , m_style(style) @@ -38,7 +82,10 @@ void CSSPageRule::initialize(JS::Realm& realm) String CSSPageRule::selector_text() const { // The selectorText attribute, on getting, must return the result of serializing the associated selector list. - return serialize_a_group_of_selectors(m_selectors); + + // https://www.w3.org/TR/cssom/#serialize-a-group-of-selectors + // To serialize a group of selectors serialize each selector in the group of selectors and then serialize a comma-separated list of these serializations. + return MUST(String::join(", "sv, m_selectors)); } // https://drafts.csswg.org/cssom/#dom-csspagerule-selectortext diff --git a/Libraries/LibWeb/CSS/CSSPageRule.h b/Libraries/LibWeb/CSS/CSSPageRule.h index 47cef00860a..85050c8b497 100644 --- a/Libraries/LibWeb/CSS/CSSPageRule.h +++ b/Libraries/LibWeb/CSS/CSSPageRule.h @@ -8,17 +8,39 @@ #include #include -#include namespace Web::CSS { +enum class PagePseudoClass : u8 { + Left, + Right, + First, + Blank, +}; +Optional page_pseudo_class_from_string(StringView); +StringView to_string(PagePseudoClass); + +class PageSelector { +public: + PageSelector(Optional name, Vector); + + Optional name() const { return m_name; } + Vector const& pseudo_classes() const { return m_pseudo_classes; } + String serialize() const; + +private: + Optional m_name; + Vector m_pseudo_classes; +}; +using PageSelectorList = Vector; + // https://drafts.csswg.org/css-page-3/#at-ruledef-page class CSSPageRule final : public CSSGroupingRule { WEB_PLATFORM_OBJECT(CSSPageRule, CSSGroupingRule); GC_DECLARE_ALLOCATOR(CSSPageRule); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, SelectorList&&, GC::Ref, CSSRuleList&); + [[nodiscard]] static GC::Ref create(JS::Realm&, PageSelectorList&&, GC::Ref, CSSRuleList&); virtual ~CSSPageRule() override = default; @@ -29,14 +51,26 @@ public: GC::Ref descriptors() const { return m_style; } private: - CSSPageRule(JS::Realm&, SelectorList&&, GC::Ref, CSSRuleList&); + CSSPageRule(JS::Realm&, PageSelectorList&&, GC::Ref, CSSRuleList&); virtual void initialize(JS::Realm&) override; virtual String serialized() const override; virtual void visit_edges(Visitor&) override; - SelectorList m_selectors; + PageSelectorList m_selectors; GC::Ref m_style; }; } + +namespace AK { + +template<> +struct Formatter : Formatter { + ErrorOr format(FormatBuilder& builder, Web::CSS::PageSelector const& selector) + { + return Formatter::format(builder, selector.serialize()); + } +}; + +} diff --git a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp index a49ee5296a2..ebf6e4f0370 100644 --- a/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp +++ b/Libraries/LibWeb/CSS/Parser/RuleParsing.cpp @@ -600,19 +600,75 @@ GC::Ptr Parser::convert_to_font_face_rule(AtRule const& rule) return CSSFontFaceRule::create(realm(), CSSFontFaceDescriptors::create(realm(), move(descriptors))); } +static Optional parse_page_selector_list(Vector const& component_values) +{ + // https://drafts.csswg.org/css-page-3/#syntax-page-selector + // = # + // = [ ? * ]! + // = : [ left | right | first | blank ] + + PageSelectorList selector_list; + + TokenStream tokens { component_values }; + tokens.discard_whitespace(); + + while (tokens.has_next_token()) { + // First optional ident + Optional maybe_ident; + if (tokens.next_token().is(Token::Type::Ident)) + maybe_ident = tokens.consume_a_token().token().ident(); + + // Then an optional series of pseudo-classes + Vector pseudo_classes; + while (tokens.next_token().is(Token::Type::Colon)) { + tokens.discard_a_token(); // : + if (!tokens.next_token().is(Token::Type::Ident)) { + dbgln_if(CSS_PARSER_DEBUG, "Invalid @page selector: pseudo-class is not an ident: `{}`", tokens.next_token().to_debug_string()); + return {}; + } + auto pseudo_class_name = tokens.consume_a_token().token().ident(); + if (auto pseudo_class = page_pseudo_class_from_string(pseudo_class_name); pseudo_class.has_value()) { + pseudo_classes.append(*pseudo_class); + } else { + dbgln_if(CSS_PARSER_DEBUG, "Invalid @page selector: unrecognized pseudo-class `:{}`", pseudo_class_name); + return {}; + } + } + + if (!maybe_ident.has_value() && pseudo_classes.is_empty()) { + // Nothing parsed + dbgln_if(CSS_PARSER_DEBUG, "Invalid @page selector: is empty"); + return {}; + } + + selector_list.empend(move(maybe_ident), move(pseudo_classes)); + + tokens.discard_whitespace(); + + if (tokens.next_token().is(Token::Type::Comma)) { + tokens.discard_a_token(); // , + tokens.discard_whitespace(); + if (!tokens.has_next_token()) { + dbgln_if(CSS_PARSER_DEBUG, "Invalid @page selector: trailing comma"); + return {}; + } + + } else if (tokens.has_next_token()) { + dbgln_if(CSS_PARSER_DEBUG, "Invalid @page selector: trailing token `{}`", tokens.next_token().to_debug_string()); + return {}; + } + } + + return selector_list; +} + GC::Ptr Parser::convert_to_page_rule(AtRule const& rule) { // https://drafts.csswg.org/css-page-3/#syntax-page-selector // @page = @page ? { } - // = # - // = [ ? * ]! - // = : [ left | right | first | blank ] - SelectorList page_selectors; - // FIXME: Parse page selectors - if (rule.prelude.find_first_index_if([](ComponentValue const& it) { return !it.is(Token::Type::Whitespace); }).has_value()) { - dbgln("@page prelude wasn't empty!"); + auto page_selectors = parse_page_selector_list(rule.prelude); + if (!page_selectors.has_value()) return nullptr; - } GC::RootVector> child_rules { realm().heap() }; Vector descriptors; @@ -636,7 +692,7 @@ GC::Ptr Parser::convert_to_page_rule(AtRule const& rule) }); auto rule_list = CSSRuleList::create(realm(), child_rules); - return CSSPageRule::create(realm(), move(page_selectors), CSSPageDescriptors::create(realm(), move(descriptors)), rule_list); + return CSSPageRule::create(realm(), page_selectors.release_value(), CSSPageDescriptors::create(realm(), move(descriptors)), rule_list); } } diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/cssom/page-001.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/cssom/page-001.txt index a318e551a3a..e1adf5c44c5 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/cssom/page-001.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/cssom/page-001.txt @@ -2,9 +2,9 @@ Harness status: OK Found 4 tests -1 Pass -3 Fail -Fail There should be 3 @page rules. +3 Pass +1 Fail +Pass There should be 3 @page rules. Pass Rule #0 -Fail Rule #1 +Pass Rule #1 Fail Rule #2 \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/nested-rules-001.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/nested-rules-001.txt index 9023f35e41e..ec79d98f4c5 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/nested-rules-001.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/nested-rules-001.txt @@ -2,5 +2,5 @@ Harness status: OK Found 1 tests -1 Fail -Fail nested-rules-001 \ No newline at end of file +1 Pass +Pass nested-rules-001 \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/page-rules-001.txt b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/page-rules-001.txt index 9e52bf0463b..b2cc600fc20 100644 --- a/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/page-rules-001.txt +++ b/Tests/LibWeb/Text/expected/wpt-import/css/css-page/parsing/page-rules-001.txt @@ -2,13 +2,12 @@ Harness status: OK Found 8 tests -5 Pass -3 Fail +8 Pass Pass page-rules-001 Pass @page , { } should be an invalid rule Pass @page { } should be a valid rule -Fail @page a { } should be a valid rule -Fail @page page1 { } should be a valid rule -Fail @page name1, name2 { } should be a valid rule +Pass @page a { } should be a valid rule +Pass @page page1 { } should be a valid rule +Pass @page name1, name2 { } should be a valid rule Pass @page a, { } should be an invalid rule Pass @page ,a { } should be an invalid rule \ No newline at end of file