mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-07-31 05:09:12 +00:00
LibWeb/CSS: Parse @page
selectors
Ideally we'd be able to share the code between page selectors and style ones, but given how simple page selectors are, some code duplication is the simpler option.
This commit is contained in:
parent
aaf07ae30d
commit
1aa5631610
Notes:
github-actions[bot]
2025-05-15 08:54:44 +00:00
Author: https://github.com/AtkinsSJ
Commit: 1aa5631610
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/4735
6 changed files with 163 additions and 27 deletions
|
@ -15,12 +15,56 @@ namespace Web::CSS {
|
|||
|
||||
GC_DEFINE_ALLOCATOR(CSSPageRule);
|
||||
|
||||
GC::Ref<CSSPageRule> CSSPageRule::create(JS::Realm& realm, SelectorList&& selectors, GC::Ref<CSSPageDescriptors> style, CSSRuleList& rules)
|
||||
Optional<PagePseudoClass> 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<FlyString> name, Vector<PagePseudoClass> 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> CSSPageRule::create(JS::Realm& realm, PageSelectorList&& selectors, GC::Ref<CSSPageDescriptors> style, CSSRuleList& rules)
|
||||
{
|
||||
return realm.create<CSSPageRule>(realm, move(selectors), style, rules);
|
||||
}
|
||||
|
||||
CSSPageRule::CSSPageRule(JS::Realm& realm, SelectorList&& selectors, GC::Ref<CSSPageDescriptors> style, CSSRuleList& rules)
|
||||
CSSPageRule::CSSPageRule(JS::Realm& realm, PageSelectorList&& selectors, GC::Ref<CSSPageDescriptors> 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
|
||||
|
|
|
@ -8,17 +8,39 @@
|
|||
|
||||
#include <LibWeb/CSS/CSSGroupingRule.h>
|
||||
#include <LibWeb/CSS/CSSPageDescriptors.h>
|
||||
#include <LibWeb/CSS/Selector.h>
|
||||
|
||||
namespace Web::CSS {
|
||||
|
||||
enum class PagePseudoClass : u8 {
|
||||
Left,
|
||||
Right,
|
||||
First,
|
||||
Blank,
|
||||
};
|
||||
Optional<PagePseudoClass> page_pseudo_class_from_string(StringView);
|
||||
StringView to_string(PagePseudoClass);
|
||||
|
||||
class PageSelector {
|
||||
public:
|
||||
PageSelector(Optional<FlyString> name, Vector<PagePseudoClass>);
|
||||
|
||||
Optional<FlyString> name() const { return m_name; }
|
||||
Vector<PagePseudoClass> const& pseudo_classes() const { return m_pseudo_classes; }
|
||||
String serialize() const;
|
||||
|
||||
private:
|
||||
Optional<FlyString> m_name;
|
||||
Vector<PagePseudoClass> m_pseudo_classes;
|
||||
};
|
||||
using PageSelectorList = Vector<PageSelector>;
|
||||
|
||||
// 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<CSSPageRule> create(JS::Realm&, SelectorList&&, GC::Ref<CSSPageDescriptors>, CSSRuleList&);
|
||||
[[nodiscard]] static GC::Ref<CSSPageRule> create(JS::Realm&, PageSelectorList&&, GC::Ref<CSSPageDescriptors>, CSSRuleList&);
|
||||
|
||||
virtual ~CSSPageRule() override = default;
|
||||
|
||||
|
@ -29,14 +51,26 @@ public:
|
|||
GC::Ref<CSSPageDescriptors const> descriptors() const { return m_style; }
|
||||
|
||||
private:
|
||||
CSSPageRule(JS::Realm&, SelectorList&&, GC::Ref<CSSPageDescriptors>, CSSRuleList&);
|
||||
CSSPageRule(JS::Realm&, PageSelectorList&&, GC::Ref<CSSPageDescriptors>, 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<CSSPageDescriptors> m_style;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace AK {
|
||||
|
||||
template<>
|
||||
struct Formatter<Web::CSS::PageSelector> : Formatter<StringView> {
|
||||
ErrorOr<void> format(FormatBuilder& builder, Web::CSS::PageSelector const& selector)
|
||||
{
|
||||
return Formatter<StringView>::format(builder, selector.serialize());
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -600,19 +600,75 @@ GC::Ptr<CSSFontFaceRule> Parser::convert_to_font_face_rule(AtRule const& rule)
|
|||
return CSSFontFaceRule::create(realm(), CSSFontFaceDescriptors::create(realm(), move(descriptors)));
|
||||
}
|
||||
|
||||
static Optional<PageSelectorList> parse_page_selector_list(Vector<ComponentValue> const& component_values)
|
||||
{
|
||||
// https://drafts.csswg.org/css-page-3/#syntax-page-selector
|
||||
// <page-selector-list> = <page-selector>#
|
||||
// <page-selector> = [ <ident-token>? <pseudo-page>* ]!
|
||||
// <pseudo-page> = : [ left | right | first | blank ]
|
||||
|
||||
PageSelectorList selector_list;
|
||||
|
||||
TokenStream tokens { component_values };
|
||||
tokens.discard_whitespace();
|
||||
|
||||
while (tokens.has_next_token()) {
|
||||
// First optional ident
|
||||
Optional<FlyString> 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<PagePseudoClass> 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<CSSPageRule> Parser::convert_to_page_rule(AtRule const& rule)
|
||||
{
|
||||
// https://drafts.csswg.org/css-page-3/#syntax-page-selector
|
||||
// @page = @page <page-selector-list>? { <declaration-rule-list> }
|
||||
// <page-selector-list> = <page-selector>#
|
||||
// <page-selector> = [ <ident-token>? <pseudo-page>* ]!
|
||||
// <pseudo-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<GC::Ref<CSSRule>> child_rules { realm().heap() };
|
||||
Vector<Descriptor> descriptors;
|
||||
|
@ -636,7 +692,7 @@ GC::Ptr<CSSPageRule> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -2,5 +2,5 @@ Harness status: OK
|
|||
|
||||
Found 1 tests
|
||||
|
||||
1 Fail
|
||||
Fail nested-rules-001
|
||||
1 Pass
|
||||
Pass nested-rules-001
|
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue