From 60e35f2a97941b322e30cae0cbe2af58226c2338 Mon Sep 17 00:00:00 2001 From: Jonah Date: Sat, 29 Jul 2023 11:51:15 -0500 Subject: [PATCH] LibWeb: Rough implementation of CSS namespace rule This provides a rough implementation of the CSS @namespace rule. Currently we just support default namespaces, namespace prefixes are still to come. --- .../expected/css-namespace-rule-matches.txt | 13 ++++ .../expected/css-namespace-rule-no-match.txt | 13 ++++ .../input/css-namespace-rule-matches.html | 19 ++++++ .../input/css-namespace-rule-no-match.html | 19 ++++++ Userland/Libraries/LibWeb/CMakeLists.txt | 1 + .../Libraries/LibWeb/CSS/CSSNamespaceRule.cpp | 59 +++++++++++++++++++ .../Libraries/LibWeb/CSS/CSSNamespaceRule.h | 37 ++++++++++++ .../Libraries/LibWeb/CSS/CSSNamespaceRule.idl | 8 +++ Userland/Libraries/LibWeb/CSS/CSSRule.h | 1 + Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp | 4 ++ .../Libraries/LibWeb/CSS/CSSStyleSheet.cpp | 14 +++++ Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h | 2 + .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 32 ++++++++++ .../Libraries/LibWeb/CSS/StyleComputer.cpp | 28 ++++++++- Userland/Libraries/LibWeb/CSS/StyleComputer.h | 3 + Userland/Libraries/LibWeb/Dump.cpp | 13 ++++ Userland/Libraries/LibWeb/Dump.h | 2 + Userland/Libraries/LibWeb/idl_files.cmake | 1 + 18 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 Tests/LibWeb/Layout/expected/css-namespace-rule-matches.txt create mode 100644 Tests/LibWeb/Layout/expected/css-namespace-rule-no-match.txt create mode 100644 Tests/LibWeb/Layout/input/css-namespace-rule-matches.html create mode 100644 Tests/LibWeb/Layout/input/css-namespace-rule-no-match.html create mode 100644 Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.cpp create mode 100644 Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.h create mode 100644 Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.idl diff --git a/Tests/LibWeb/Layout/expected/css-namespace-rule-matches.txt b/Tests/LibWeb/Layout/expected/css-namespace-rule-matches.txt new file mode 100644 index 0000000000..18d869efbd --- /dev/null +++ b/Tests/LibWeb/Layout/expected/css-namespace-rule-matches.txt @@ -0,0 +1,13 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x600 [BFC] children: not-inline + BlockContainer at (8,20) content-size 784x21.828125 children: not-inline + BlockContainer

at (8,20) content-size 784x21.828125 children: inline + line 0 width: 183.875, height: 21.828125, bottom: 21.828125, baseline: 16.890625 + frag 0 from TextNode start: 0, length: 15, rect: [18,20 163.875x21.828125] + "Should be green" + TextNode <#text> + InlineNode + TextNode <#text> + TextNode <#text> + BlockContainer <(anonymous)> at (8,61.828125) content-size 784x0 children: inline + TextNode <#text> diff --git a/Tests/LibWeb/Layout/expected/css-namespace-rule-no-match.txt b/Tests/LibWeb/Layout/expected/css-namespace-rule-no-match.txt new file mode 100644 index 0000000000..850327f876 --- /dev/null +++ b/Tests/LibWeb/Layout/expected/css-namespace-rule-no-match.txt @@ -0,0 +1,13 @@ +Viewport <#document> at (0,0) content-size 800x600 children: not-inline + BlockContainer at (0,0) content-size 800x600 [BFC] children: not-inline + BlockContainer at (8,20) content-size 784x21.828125 children: not-inline + BlockContainer

at (8,20) content-size 784x21.828125 children: inline + line 0 width: 151.328125, height: 21.828125, bottom: 21.828125, baseline: 16.890625 + frag 0 from TextNode start: 0, length: 13, rect: [13,20 141.328125x21.828125] + "Should be red" + TextNode <#text> + InlineNode + TextNode <#text> + TextNode <#text> + BlockContainer <(anonymous)> at (8,61.828125) content-size 784x0 children: inline + TextNode <#text> diff --git a/Tests/LibWeb/Layout/input/css-namespace-rule-matches.html b/Tests/LibWeb/Layout/input/css-namespace-rule-matches.html new file mode 100644 index 0000000000..06441d75ff --- /dev/null +++ b/Tests/LibWeb/Layout/input/css-namespace-rule-matches.html @@ -0,0 +1,19 @@ + + +

+ Should be green +

diff --git a/Tests/LibWeb/Layout/input/css-namespace-rule-no-match.html b/Tests/LibWeb/Layout/input/css-namespace-rule-no-match.html new file mode 100644 index 0000000000..5c49093a11 --- /dev/null +++ b/Tests/LibWeb/Layout/input/css-namespace-rule-no-match.html @@ -0,0 +1,19 @@ + + +

+ Should be red +

diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt index 77faf067bd..3c8d65e836 100644 --- a/Userland/Libraries/LibWeb/CMakeLists.txt +++ b/Userland/Libraries/LibWeb/CMakeLists.txt @@ -29,6 +29,7 @@ set(SOURCES CSS/CSSFontFaceRule.cpp CSS/CSSMediaRule.cpp CSS/CSSNumericType.cpp + CSS/CSSNamespaceRule.cpp CSS/CSSRule.cpp CSS/CSSRuleList.cpp CSS/CSSStyleDeclaration.cpp diff --git a/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.cpp new file mode 100644 index 0000000000..b3f8e3f81a --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.cpp @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2023, Jonah Shafran + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +namespace Web::CSS { + +CSSNamespaceRule::CSSNamespaceRule(JS::Realm& realm, Optional prefix, StringView namespace_uri) + : CSSRule(realm) + , m_namespace_uri(namespace_uri) + , m_prefix(prefix.has_value() ? prefix.value() : ""sv) +{ +} + +WebIDL::ExceptionOr> CSSNamespaceRule::create(JS::Realm& realm, Optional prefix, AK::StringView namespace_uri) +{ + return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, prefix, namespace_uri)); +} + +JS::ThrowCompletionOr CSSNamespaceRule::initialize(JS::Realm& realm) +{ + MUST_OR_THROW_OOM(Base::initialize(realm)); + set_prototype(&Bindings::ensure_web_prototype(realm, "CSSNamespaceRule")); + + return {}; +} + +// https://www.w3.org/TR/cssom/#serialize-a-css-rule +DeprecatedString CSSNamespaceRule::serialized() const +{ + StringBuilder builder; + // The literal string "@namespace", followed by a single SPACE (U+0020), + builder.append("@namespace "sv); + + // followed by the serialization as an identifier of the prefix attribute (if any), + if (!m_prefix.is_empty() && !m_prefix.is_null()) { + builder.append(m_prefix); + // followed by a single SPACE (U+0020) if there is a prefix, + builder.append(" "sv); + } + + // followed by the serialization as URL of the namespaceURI attribute, + builder.append(m_namespace_uri); + + // followed the character ";" (U+003B). + builder.append(";"sv); + + return builder.to_deprecated_string(); +} + +} diff --git a/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.h b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.h new file mode 100644 index 0000000000..4d77a2517e --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023, Jonah Shafran + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include + +namespace Web::CSS { + +class CSSNamespaceRule final : public CSSRule { + WEB_PLATFORM_OBJECT(CSSNamespaceRule, CSSRule); + +public: + static WebIDL::ExceptionOr> create(JS::Realm&, Optional prefix, StringView namespace_uri); + + virtual ~CSSNamespaceRule() = default; + + void set_namespace_uri(DeprecatedString value) { m_namespace_uri = move(value); } + DeprecatedString namespace_uri() const { return m_namespace_uri; } + void set_prefix(DeprecatedString value) { m_prefix = move(value); } + DeprecatedString prefix() const { return m_prefix; } + virtual Type type() const override { return Type::Namespace; } + +private: + CSSNamespaceRule(JS::Realm&, Optional prefix, StringView namespace_uri); + + virtual JS::ThrowCompletionOr initialize(JS::Realm&) override; + + virtual DeprecatedString serialized() const override; + DeprecatedString m_namespace_uri; + DeprecatedString m_prefix; +}; + +} diff --git a/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.idl b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.idl new file mode 100644 index 0000000000..d2ffeaad1c --- /dev/null +++ b/Userland/Libraries/LibWeb/CSS/CSSNamespaceRule.idl @@ -0,0 +1,8 @@ +#import + +// https://www.w3.org/TR/cssom/#the-cssnamespacerule-interface +[Exposed=Window] +interface CSSNamespaceRule : CSSRule { + readonly attribute CSSOMString namespaceURI; + readonly attribute CSSOMString prefix; +}; diff --git a/Userland/Libraries/LibWeb/CSS/CSSRule.h b/Userland/Libraries/LibWeb/CSS/CSSRule.h index db9ae0bcb7..b9049ab37d 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSRule.h +++ b/Userland/Libraries/LibWeb/CSS/CSSRule.h @@ -29,6 +29,7 @@ public: FontFace = 5, Keyframes = 7, Keyframe = 8, + Namespace = 10, Supports = 12, }; diff --git a/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp b/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp index 60b9894a84..93a8dbdfdf 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp +++ b/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp @@ -144,6 +144,7 @@ void CSSRuleList::for_each_effective_style_rule(Function(*rule)); break; + case CSSRule::Type::Namespace: + break; } } } @@ -212,6 +215,7 @@ bool CSSRuleList::evaluate_media_queries(HTML::Window const& window) } case CSSRule::Type::Keyframe: case CSSRule::Type::Keyframes: + case CSSRule::Type::Namespace: break; } } diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp index 7c09a573aa..d3a29d41f9 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp +++ b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -137,4 +138,17 @@ void CSSStyleSheet::set_style_sheet_list(Badge, StyleSheetList* m_style_sheet_list = list; } +Optional CSSStyleSheet::namespace_filter() const +{ + for (JS::NonnullGCPtr rule : *m_rules) { + if (rule->type() == CSSRule::Type::Namespace) { + auto& namespace_rule = verify_cast(*rule); + if (!namespace_rule.namespace_uri().is_empty() && namespace_rule.prefix().is_empty()) + return namespace_rule.namespace_uri().view(); + } + } + + return {}; +} + } diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h index f153b84da9..c6d031f71e 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h +++ b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h @@ -48,6 +48,8 @@ public: void set_style_sheet_list(Badge, StyleSheetList*); + Optional namespace_filter() const; + private: CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional location); diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 154af0e13c..75eaa55598 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -3241,6 +3242,37 @@ CSSRule* Parser::convert_to_rule(NonnullRefPtr rule) return CSSKeyframesRule::create(m_context.realm(), name, move(keyframes)).release_value_but_fixme_should_propagate_errors(); } + if (rule->at_rule_name().equals_ignoring_ascii_case("namespace"sv)) { + // https://drafts.csswg.org/css-namespaces/#syntax + auto token_stream = TokenStream { rule->prelude() }; + token_stream.skip_whitespace(); + + auto token = token_stream.next_token(); + Optional prefix = {}; + if (token.is(Token::Type::Ident)) { + prefix = token.token().ident(); + token_stream.skip_whitespace(); + token = token_stream.next_token(); + } + + DeprecatedString namespace_uri; + if (token.is(Token::Type::String)) { + namespace_uri = token.token().string(); + } else if (auto url = parse_url_function(token, AllowedDataUrlType::None); url.has_value()) { + namespace_uri = url.value().to_deprecated_string(); + } else { + dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @namespace rule invalid; discarding."); + return {}; + } + + token_stream.skip_whitespace(); + if (token_stream.has_next_token()) { + dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @namespace rule invalid; discarding."); + return {}; + } + + return CSSNamespaceRule::create(m_context.realm(), prefix, namespace_uri).release_value_but_fixme_should_propagate_errors(); + } // FIXME: More at rules! dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", rule->at_rule_name()); diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 719df8e9b0..e951d292f4 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -220,19 +220,40 @@ StyleComputer::RuleCache const& StyleComputer::rule_cache_for_cascade_origin(Cas } } +Vector StyleComputer::filter_namespace_rules(DOM::Element const& element, Vector const& rules) const +{ + Vector filtered_rules; + + for (auto const& rule : rules) { + auto namespace_uri = rule.sheet->namespace_filter(); + if (namespace_uri.has_value()) { + if (namespace_uri.value() == element.namespace_uri()) + filtered_rules.append(rule); + } else { + filtered_rules.append(rule); + } + } + + // FIXME: Filter out non-default namespace using prefixes + + return filtered_rules; +} + Vector StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional pseudo_element) const { auto const& rule_cache = rule_cache_for_cascade_origin(cascade_origin); Vector rules_to_run; auto add_rules_to_run = [&](Vector const& rules) { + Vector namespace_filtered_rules = filter_namespace_rules(element, rules); + if (pseudo_element.has_value()) { - for (auto& rule : rules) { + for (auto& rule : namespace_filtered_rules) { if (rule.contains_pseudo_element) rules_to_run.append(rule); } } else { - rules_to_run.extend(rules); + rules_to_run.extend(namespace_filtered_rules); } }; @@ -2556,10 +2577,11 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca for (CSS::Selector const& selector : rule.selectors()) { MatchingRule matching_rule { &rule, + sheet, style_sheet_index, rule_index, selector_index, - selector.specificity(), + selector.specificity() }; for (auto const& simple_selector : selector.compound_selectors().last().simple_selectors) { diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h index 9b53e9e563..9079e2d5c8 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h @@ -24,6 +24,7 @@ namespace Web::CSS { struct MatchingRule { JS::GCPtr rule; + JS::GCPtr sheet; size_t style_sheet_index { 0 }; size_t rule_index { 0 }; size_t selector_index { 0 }; @@ -173,6 +174,8 @@ private: void build_rule_cache(); void build_rule_cache_if_needed() const; + Vector filter_namespace_rules(DOM::Element const&, Vector const&) const; + JS::NonnullGCPtr m_document; struct AnimationKeyFrameSet { diff --git a/Userland/Libraries/LibWeb/Dump.cpp b/Userland/Libraries/LibWeb/Dump.cpp index 14ba654afa..0111ff993a 100644 --- a/Userland/Libraries/LibWeb/Dump.cpp +++ b/Userland/Libraries/LibWeb/Dump.cpp @@ -679,6 +679,9 @@ ErrorOr dump_rule(StringBuilder& builder, CSS::CSSRule const& rule, int in case CSS::CSSRule::Type::Keyframe: case CSS::CSSRule::Type::Keyframes: break; + case CSS::CSSRule::Type::Namespace: + TRY(dump_namespace_rule(builder, verify_cast(rule), indent_levels)); + break; } return {}; } @@ -835,4 +838,14 @@ void dump_tree(StringBuilder& builder, Painting::Paintable const& paintable, boo } } +ErrorOr dump_namespace_rule(StringBuilder& builder, CSS::CSSNamespaceRule const& namespace_, int indent_levels) +{ + indent(builder, indent_levels); + TRY(builder.try_appendff(" Namespace: {}\n", namespace_.namespace_uri())); + if (!namespace_.prefix().is_null() && !namespace_.prefix().is_empty()) + TRY(builder.try_appendff(" Prefix: {}\n", namespace_.prefix())); + + return {}; +} + } diff --git a/Userland/Libraries/LibWeb/Dump.h b/Userland/Libraries/LibWeb/Dump.h index 7a2387a1b7..58f3035f31 100644 --- a/Userland/Libraries/LibWeb/Dump.h +++ b/Userland/Libraries/LibWeb/Dump.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include namespace Web { @@ -27,6 +28,7 @@ void dump_import_rule(StringBuilder&, CSS::CSSImportRule const&, int indent_leve ErrorOr dump_media_rule(StringBuilder&, CSS::CSSMediaRule const&, int indent_levels = 0); ErrorOr dump_style_rule(StringBuilder&, CSS::CSSStyleRule const&, int indent_levels = 0); ErrorOr dump_supports_rule(StringBuilder&, CSS::CSSSupportsRule const&, int indent_levels = 0); +ErrorOr dump_namespace_rule(StringBuilder&, CSS::CSSNamespaceRule const&, int indent_levels = 0); void dump_selector(StringBuilder&, CSS::Selector const&); void dump_selector(CSS::Selector const&); diff --git a/Userland/Libraries/LibWeb/idl_files.cmake b/Userland/Libraries/LibWeb/idl_files.cmake index dad7ea04ba..0c37ffbe40 100644 --- a/Userland/Libraries/LibWeb/idl_files.cmake +++ b/Userland/Libraries/LibWeb/idl_files.cmake @@ -11,6 +11,7 @@ libweb_js_bindings(CSS/CSSKeyframeRule) libweb_js_bindings(CSS/CSSKeyframesRule) libweb_js_bindings(CSS/CSSMediaRule) libweb_js_bindings(CSS/CSS NAMESPACE) +libweb_js_bindings(CSS/CSSNamespaceRule) libweb_js_bindings(CSS/CSSRule) libweb_js_bindings(CSS/CSSRuleList) libweb_js_bindings(CSS/CSSStyleDeclaration)