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.
This commit is contained in:
Jonah 2023-07-29 11:51:15 -05:00 committed by Sam Atkins
parent 3f7d97f098
commit 60e35f2a97
Notes: sideshowbarker 2024-07-18 05:01:22 +09:00
18 changed files with 266 additions and 3 deletions

View File

@ -0,0 +1,13 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
BlockContainer <body> at (8,20) content-size 784x21.828125 children: not-inline
BlockContainer <p> 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 <a>
TextNode <#text>
TextNode <#text>
BlockContainer <(anonymous)> at (8,61.828125) content-size 784x0 children: inline
TextNode <#text>

View File

@ -0,0 +1,13 @@
Viewport <#document> at (0,0) content-size 800x600 children: not-inline
BlockContainer <html> at (0,0) content-size 800x600 [BFC] children: not-inline
BlockContainer <body> at (8,20) content-size 784x21.828125 children: not-inline
BlockContainer <p> 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 <a>
TextNode <#text>
TextNode <#text>
BlockContainer <(anonymous)> at (8,61.828125) content-size 784x0 children: inline
TextNode <#text>

View File

@ -0,0 +1,19 @@
<style>
* {
font: 20px SerenitySans;
}
a {
padding: 5px;
color: red;
}
</style>
<style>
@namespace url('http://www.w3.org/1999/xhtml');
a {
padding: 10px;
color: green;
}
</style>
<p>
<a href="#">Should be green</a>
</p>

View File

@ -0,0 +1,19 @@
<style>
* {
font: 20px SerenitySans;
}
a {
padding: 5px;
color: red;
}
</style>
<style>
@namespace url('http://www.w3.org/2000/svg');
a {
padding: 10px;
color: green;
}
</style>
<p>
<a href="#">Should be red</a>
</p>

View File

@ -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

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2023, Jonah Shafran <jonahshafran@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibJS/Heap/Heap.h>
#include <LibJS/Runtime/Realm.h>
#include <LibWeb/Bindings/CSSNamespaceRulePrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNamespaceRule.h>
#include <LibWeb/WebIDL/ExceptionOr.h>
namespace Web::CSS {
CSSNamespaceRule::CSSNamespaceRule(JS::Realm& realm, Optional<StringView> prefix, StringView namespace_uri)
: CSSRule(realm)
, m_namespace_uri(namespace_uri)
, m_prefix(prefix.has_value() ? prefix.value() : ""sv)
{
}
WebIDL::ExceptionOr<JS::NonnullGCPtr<CSSNamespaceRule>> CSSNamespaceRule::create(JS::Realm& realm, Optional<AK::StringView> prefix, AK::StringView namespace_uri)
{
return MUST_OR_THROW_OOM(realm.heap().allocate<CSSNamespaceRule>(realm, realm, prefix, namespace_uri));
}
JS::ThrowCompletionOr<void> CSSNamespaceRule::initialize(JS::Realm& realm)
{
MUST_OR_THROW_OOM(Base::initialize(realm));
set_prototype(&Bindings::ensure_web_prototype<Bindings::CSSNamespaceRulePrototype>(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();
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2023, Jonah Shafran <jonahshafran@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/CSS/CSSRule.h>
namespace Web::CSS {
class CSSNamespaceRule final : public CSSRule {
WEB_PLATFORM_OBJECT(CSSNamespaceRule, CSSRule);
public:
static WebIDL::ExceptionOr<JS::NonnullGCPtr<CSSNamespaceRule>> create(JS::Realm&, Optional<StringView> 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<StringView> prefix, StringView namespace_uri);
virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
virtual DeprecatedString serialized() const override;
DeprecatedString m_namespace_uri;
DeprecatedString m_prefix;
};
}

View File

@ -0,0 +1,8 @@
#import <CSS/CSSRule.idl>
// https://www.w3.org/TR/cssom/#the-cssnamespacerule-interface
[Exposed=Window]
interface CSSNamespaceRule : CSSRule {
readonly attribute CSSOMString namespaceURI;
readonly attribute CSSOMString prefix;
};

View File

@ -29,6 +29,7 @@ public:
FontFace = 5,
Keyframes = 7,
Keyframe = 8,
Namespace = 10,
Supports = 12,
};

View File

@ -144,6 +144,7 @@ void CSSRuleList::for_each_effective_style_rule(Function<void(CSSStyleRule const
break;
case CSSRule::Type::Keyframe:
case CSSRule::Type::Keyframes:
case CSSRule::Type::Namespace:
break;
}
}
@ -174,6 +175,8 @@ void CSSRuleList::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframe
case CSSRule::Type::Keyframes:
callback(static_cast<CSSKeyframesRule const&>(*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;
}
}

View File

@ -6,6 +6,7 @@
#include <LibWeb/Bindings/CSSStyleSheetPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSNamespaceRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/CSS/StyleComputer.h>
@ -137,4 +138,17 @@ void CSSStyleSheet::set_style_sheet_list(Badge<StyleSheetList>, StyleSheetList*
m_style_sheet_list = list;
}
Optional<StringView> CSSStyleSheet::namespace_filter() const
{
for (JS::NonnullGCPtr<CSSRule> rule : *m_rules) {
if (rule->type() == CSSRule::Type::Namespace) {
auto& namespace_rule = verify_cast<CSSNamespaceRule>(*rule);
if (!namespace_rule.namespace_uri().is_empty() && namespace_rule.prefix().is_empty())
return namespace_rule.namespace_uri().view();
}
}
return {};
}
}

View File

@ -48,6 +48,8 @@ public:
void set_style_sheet_list(Badge<StyleSheetList>, StyleSheetList*);
Optional<StringView> namespace_filter() const;
private:
CSSStyleSheet(JS::Realm&, CSSRuleList&, MediaList&, Optional<AK::URL> location);

View File

@ -17,6 +17,7 @@
#include <LibWeb/CSS/CSSKeyframeRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSNamespaceRule.h>
#include <LibWeb/CSS/CSSStyleDeclaration.h>
#include <LibWeb/CSS/CSSStyleRule.h>
#include <LibWeb/CSS/CSSStyleSheet.h>
@ -3241,6 +3242,37 @@ CSSRule* Parser::convert_to_rule(NonnullRefPtr<Rule> 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<StringView> 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());

View File

@ -220,19 +220,40 @@ StyleComputer::RuleCache const& StyleComputer::rule_cache_for_cascade_origin(Cas
}
}
Vector<MatchingRule> StyleComputer::filter_namespace_rules(DOM::Element const& element, Vector<MatchingRule> const& rules) const
{
Vector<MatchingRule> 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<MatchingRule> StyleComputer::collect_matching_rules(DOM::Element const& element, CascadeOrigin cascade_origin, Optional<CSS::Selector::PseudoElement> pseudo_element) const
{
auto const& rule_cache = rule_cache_for_cascade_origin(cascade_origin);
Vector<MatchingRule> rules_to_run;
auto add_rules_to_run = [&](Vector<MatchingRule> const& rules) {
Vector<MatchingRule> 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::RuleCache> 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) {

View File

@ -24,6 +24,7 @@ namespace Web::CSS {
struct MatchingRule {
JS::GCPtr<CSSStyleRule const> rule;
JS::GCPtr<CSSStyleSheet const> 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<MatchingRule> filter_namespace_rules(DOM::Element const&, Vector<MatchingRule> const&) const;
JS::NonnullGCPtr<DOM::Document> m_document;
struct AnimationKeyFrameSet {

View File

@ -679,6 +679,9 @@ ErrorOr<void> 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<CSS::CSSNamespaceRule const>(rule), indent_levels));
break;
}
return {};
}
@ -835,4 +838,14 @@ void dump_tree(StringBuilder& builder, Painting::Paintable const& paintable, boo
}
}
ErrorOr<void> 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 {};
}
}

View File

@ -8,6 +8,7 @@
#pragma once
#include <AK/Forward.h>
#include <LibWeb/CSS/CSSNamespaceRule.h>
#include <LibWeb/Forward.h>
namespace Web {
@ -27,6 +28,7 @@ void dump_import_rule(StringBuilder&, CSS::CSSImportRule const&, int indent_leve
ErrorOr<void> dump_media_rule(StringBuilder&, CSS::CSSMediaRule const&, int indent_levels = 0);
ErrorOr<void> dump_style_rule(StringBuilder&, CSS::CSSStyleRule const&, int indent_levels = 0);
ErrorOr<void> dump_supports_rule(StringBuilder&, CSS::CSSSupportsRule const&, int indent_levels = 0);
ErrorOr<void> dump_namespace_rule(StringBuilder&, CSS::CSSNamespaceRule const&, int indent_levels = 0);
void dump_selector(StringBuilder&, CSS::Selector const&);
void dump_selector(CSS::Selector const&);

View File

@ -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)