LibWeb: Start implementing the CSS cascade
The 'C' in "CSS" is for Cascade, so let's actually implement the cascade in LibWeb. :^) StyleResolver::resolve_style() now begins by collecting all the matching CSS rules for the given DOM::Element. Rules are then processed in the spec's cascade order (instead of in the order we encounter them.) With this, "!important" is now honored on CSS properties. After performing the cascade, we do another pass of what the spec calls "defaulting" where we resolve "inherit" and "initial" values. I've left a FIXME about supporting correct "initial" values for every property, since we're currently lacking some coverage there. Note that this mechanism now resolves every known CSS property. This is *not* space-efficient and we'll eventually need to come up with some strategies to reduce memory usage around this. However, this will do fine until we have more of the engine working correctly. :^)
This commit is contained in:
parent
c55909f175
commit
d965a9552f
Notes:
sideshowbarker
2024-07-18 03:35:30 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/d965a9552fe
4 changed files with 150 additions and 77 deletions
Userland/Libraries/LibWeb/CSS
|
@ -36,48 +36,20 @@ NonnullRefPtr<StyleProperties> StyleProperties::clone() const
|
|||
|
||||
void StyleProperties::set_property(CSS::PropertyID id, NonnullRefPtr<StyleValue> value)
|
||||
{
|
||||
m_property_values.set((unsigned)id, move(value));
|
||||
m_property_values.set(id, move(value));
|
||||
}
|
||||
|
||||
void StyleProperties::set_property(CSS::PropertyID id, const StringView& value)
|
||||
{
|
||||
m_property_values.set((unsigned)id, StringStyleValue::create(value));
|
||||
m_property_values.set(id, StringStyleValue::create(value));
|
||||
}
|
||||
|
||||
Optional<NonnullRefPtr<StyleValue>> StyleProperties::property(CSS::PropertyID id) const
|
||||
Optional<NonnullRefPtr<StyleValue>> StyleProperties::property(CSS::PropertyID property_id) const
|
||||
{
|
||||
auto fetch_initial = [](CSS::PropertyID id) -> Optional<NonnullRefPtr<StyleValue>> {
|
||||
auto initial_value = property_initial_value(id);
|
||||
if (initial_value)
|
||||
return initial_value.release_nonnull();
|
||||
auto it = m_property_values.find(property_id);
|
||||
if (it == m_property_values.end())
|
||||
return {};
|
||||
};
|
||||
auto fetch_inherited = [](CSS::PropertyID) -> Optional<NonnullRefPtr<StyleValue>> {
|
||||
// FIXME: Implement inheritance
|
||||
return {};
|
||||
};
|
||||
|
||||
auto it = m_property_values.find((unsigned)id);
|
||||
if (it == m_property_values.end()) {
|
||||
if (is_inherited_property(id))
|
||||
return fetch_inherited(id);
|
||||
return fetch_initial(id);
|
||||
}
|
||||
|
||||
auto& value = it->value;
|
||||
if (value->is_initial())
|
||||
return fetch_initial(id);
|
||||
if (value->is_inherit())
|
||||
return fetch_inherited(id);
|
||||
if (value->is_unset()) {
|
||||
if (is_inherited_property(id)) {
|
||||
return fetch_inherited(id);
|
||||
} else {
|
||||
return fetch_initial(id);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
return it->value;
|
||||
}
|
||||
|
||||
Length StyleProperties::length_or_fallback(CSS::PropertyID id, const Length& fallback) const
|
||||
|
|
|
@ -82,7 +82,9 @@ public:
|
|||
Optional<int> z_index() const;
|
||||
|
||||
private:
|
||||
HashMap<unsigned, NonnullRefPtr<StyleValue>> m_property_values;
|
||||
friend class StyleResolver;
|
||||
|
||||
HashMap<CSS::PropertyID, NonnullRefPtr<StyleValue>> m_property_values;
|
||||
Optional<CSS::Overflow> overflow(CSS::PropertyID) const;
|
||||
|
||||
void load_font(Layout::Node const&) const;
|
||||
|
|
|
@ -52,22 +52,26 @@ static StyleSheet& quirks_mode_stylesheet()
|
|||
}
|
||||
|
||||
template<typename Callback>
|
||||
void StyleResolver::for_each_stylesheet(Callback callback) const
|
||||
void StyleResolver::for_each_stylesheet(CascadeOrigin cascade_origin, Callback callback) const
|
||||
{
|
||||
callback(default_stylesheet());
|
||||
if (document().in_quirks_mode())
|
||||
callback(quirks_mode_stylesheet());
|
||||
for (auto& sheet : document().style_sheets().sheets()) {
|
||||
callback(sheet);
|
||||
if (cascade_origin == CascadeOrigin::Any || cascade_origin == CascadeOrigin::UserAgent) {
|
||||
callback(default_stylesheet());
|
||||
if (document().in_quirks_mode())
|
||||
callback(quirks_mode_stylesheet());
|
||||
}
|
||||
if (cascade_origin == CascadeOrigin::Any || cascade_origin == CascadeOrigin::Author) {
|
||||
for (auto& sheet : document().style_sheets().sheets()) {
|
||||
callback(sheet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vector<MatchingRule> StyleResolver::collect_matching_rules(DOM::Element const& element) const
|
||||
Vector<MatchingRule> StyleResolver::collect_matching_rules(DOM::Element const& element, CascadeOrigin declaration_type) const
|
||||
{
|
||||
Vector<MatchingRule> matching_rules;
|
||||
|
||||
size_t style_sheet_index = 0;
|
||||
for_each_stylesheet([&](auto& sheet) {
|
||||
for_each_stylesheet(declaration_type, [&](auto& sheet) {
|
||||
size_t rule_index = 0;
|
||||
static_cast<CSSStyleSheet const&>(sheet).for_each_effective_style_rule([&](auto& rule) {
|
||||
size_t selector_index = 0;
|
||||
|
@ -531,54 +535,129 @@ Optional<StyleProperty> StyleResolver::resolve_custom_property(DOM::Element& ele
|
|||
return resolved_with_specificity.style;
|
||||
}
|
||||
|
||||
NonnullRefPtr<StyleProperties> StyleResolver::resolve_style(DOM::Element& element) const
|
||||
struct MatchingDeclarations {
|
||||
Vector<MatchingRule> user_agent_rules;
|
||||
Vector<MatchingRule> author_rules;
|
||||
};
|
||||
|
||||
void StyleResolver::cascade_declarations(StyleProperties& style, DOM::Element& element, Vector<MatchingRule> const& matching_rules, CascadeOrigin cascade_origin, bool important) const
|
||||
{
|
||||
auto style = StyleProperties::create();
|
||||
auto* parent_style = element.parent_element() ? element.parent_element()->specified_css_values() : nullptr;
|
||||
if (parent_style) {
|
||||
parent_style->for_each_property([&](auto property_id, auto& value) {
|
||||
if (is_inherited_property(property_id))
|
||||
set_property_expanding_shorthands(style, property_id, value, m_document);
|
||||
});
|
||||
}
|
||||
|
||||
element.apply_presentational_hints(*style);
|
||||
|
||||
auto matching_rules = collect_matching_rules(element);
|
||||
sort_matching_rules(matching_rules);
|
||||
|
||||
for (auto& match : matching_rules) {
|
||||
for (auto& property : verify_cast<PropertyOwningCSSStyleDeclaration>(match.rule->declaration()).properties()) {
|
||||
if (important != property.important)
|
||||
continue;
|
||||
auto property_value = property.value;
|
||||
if (property.value->is_custom_property()) {
|
||||
auto prop = reinterpret_cast<CSS::CustomStyleValue const*>(property.value.ptr());
|
||||
auto custom_prop_name = prop->custom_property_name();
|
||||
auto resolved = resolve_custom_property(element, custom_prop_name);
|
||||
auto custom_property_name = static_cast<CSS::CustomStyleValue const&>(*property.value).custom_property_name();
|
||||
auto resolved = resolve_custom_property(element, custom_property_name);
|
||||
if (resolved.has_value()) {
|
||||
property_value = resolved.value().value;
|
||||
}
|
||||
}
|
||||
// FIXME: This also captures shorthands of which we ideally want to resolve the long names separately.
|
||||
if (property_value->is_inherit()) {
|
||||
// HACK: Trying to resolve the font property here lead to copious amounts of debug-spam
|
||||
if (property.property_id == CSS::PropertyID::Font)
|
||||
continue;
|
||||
if (parent_style) {
|
||||
auto maybe_parent_property_value = parent_style->property(property.property_id);
|
||||
if (maybe_parent_property_value.has_value())
|
||||
property_value = maybe_parent_property_value.release_value();
|
||||
}
|
||||
}
|
||||
set_property_expanding_shorthands(style, property.property_id, property_value, m_document);
|
||||
}
|
||||
}
|
||||
|
||||
if (auto* inline_style = verify_cast<ElementInlineCSSStyleDeclaration>(element.inline_style())) {
|
||||
for (auto& property : inline_style->properties()) {
|
||||
set_property_expanding_shorthands(style, property.property_id, property.value, m_document);
|
||||
if (cascade_origin == CascadeOrigin::Author) {
|
||||
if (auto* inline_style = verify_cast<ElementInlineCSSStyleDeclaration>(element.inline_style())) {
|
||||
for (auto& property : inline_style->properties()) {
|
||||
if (important != property.important)
|
||||
continue;
|
||||
set_property_expanding_shorthands(style, property.property_id, property.value, m_document);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-cascade/#cascading
|
||||
void StyleResolver::compute_cascaded_values(StyleProperties& style, DOM::Element& element) const
|
||||
{
|
||||
// First, we collect all the CSS rules whose selectors match `element`:
|
||||
MatchingRuleSet matching_rule_set;
|
||||
matching_rule_set.user_agent_rules = collect_matching_rules(element, CascadeOrigin::UserAgent);
|
||||
sort_matching_rules(matching_rule_set.user_agent_rules);
|
||||
matching_rule_set.author_rules = collect_matching_rules(element, CascadeOrigin::Author);
|
||||
sort_matching_rules(matching_rule_set.author_rules);
|
||||
|
||||
// Then we apply the declarations from the matched rules in cascade order:
|
||||
|
||||
// Normal user agent declarations
|
||||
cascade_declarations(style, element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, false);
|
||||
|
||||
// FIXME: Normal user declarations
|
||||
|
||||
// Normal author declarations
|
||||
cascade_declarations(style, element, matching_rule_set.author_rules, CascadeOrigin::Author, false);
|
||||
|
||||
// Author presentational hints (NOTE: The spec doesn't say exactly how to prioritize these.)
|
||||
element.apply_presentational_hints(style);
|
||||
|
||||
// FIXME: Animation declarations [css-animations-1]
|
||||
|
||||
// Important author declarations
|
||||
cascade_declarations(style, element, matching_rule_set.author_rules, CascadeOrigin::Author, true);
|
||||
|
||||
// FIXME: Important user declarations
|
||||
|
||||
// Important user agent declarations
|
||||
cascade_declarations(style, element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, true);
|
||||
|
||||
// FIXME: Transition declarations [css-transitions-1]
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-cascade/#defaulting
|
||||
void StyleResolver::compute_defaulted_values(StyleProperties& style, DOM::Element const& element) const
|
||||
{
|
||||
// FIXME: If we don't know the correct initial value for a property, we fall back to InitialStyleValue.
|
||||
|
||||
auto get_initial_value = [&](PropertyID property_id) -> NonnullRefPtr<StyleValue> {
|
||||
auto value = property_initial_value(property_id);
|
||||
if (!value)
|
||||
return InitialStyleValue::the();
|
||||
return value.release_nonnull();
|
||||
};
|
||||
|
||||
auto get_inherit_value = [&](PropertyID property_id) -> NonnullRefPtr<StyleValue> {
|
||||
if (!element.parent_element() || !element.parent_element()->specified_css_values())
|
||||
return get_initial_value(property_id);
|
||||
auto& map = element.parent_element()->specified_css_values()->m_property_values;
|
||||
auto it = map.find(property_id);
|
||||
VERIFY(it != map.end());
|
||||
return const_cast<StyleValue&>(*it->value);
|
||||
};
|
||||
|
||||
// Walk the list of all known CSS properties and:
|
||||
// - Add them to `style` if they are missing.
|
||||
// - Resolve `inherit` and `initial` as needed.
|
||||
for (auto i = to_underlying(CSS::first_property_id); i <= to_underlying(CSS::last_property_id); ++i) {
|
||||
auto property_id = (CSS::PropertyID)i;
|
||||
auto it = style.m_property_values.find(property_id);
|
||||
if (it == style.m_property_values.end()) {
|
||||
|
||||
if (is_inherited_property(property_id))
|
||||
style.m_property_values.set(property_id, get_inherit_value(property_id));
|
||||
else
|
||||
style.m_property_values.set(property_id, get_initial_value(property_id));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it->value->is_initial()) {
|
||||
it->value = get_initial_value(property_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (it->value->is_inherit()) {
|
||||
it->value = get_inherit_value(property_id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NonnullRefPtr<StyleProperties> StyleResolver::resolve_style(DOM::Element& element) const
|
||||
{
|
||||
auto style = StyleProperties::create();
|
||||
compute_cascaded_values(style, element);
|
||||
compute_defaulted_values(style, element);
|
||||
return style;
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,17 @@ public:
|
|||
|
||||
NonnullRefPtr<StyleProperties> resolve_style(DOM::Element&) const;
|
||||
|
||||
Vector<MatchingRule> collect_matching_rules(DOM::Element const&) const;
|
||||
// https://www.w3.org/TR/css-cascade/#origin
|
||||
enum class CascadeOrigin {
|
||||
Any, // FIXME: This is not part of the spec. Get rid of it.
|
||||
Author,
|
||||
User,
|
||||
UserAgent,
|
||||
Animation,
|
||||
Transition,
|
||||
};
|
||||
|
||||
Vector<MatchingRule> collect_matching_rules(DOM::Element const&, CascadeOrigin = CascadeOrigin::Any) const;
|
||||
void sort_matching_rules(Vector<MatchingRule>&) const;
|
||||
struct CustomPropertyResolutionTuple {
|
||||
Optional<StyleProperty> style {};
|
||||
|
@ -42,8 +52,18 @@ public:
|
|||
Optional<StyleProperty> resolve_custom_property(DOM::Element&, String const&) const;
|
||||
|
||||
private:
|
||||
void compute_cascaded_values(StyleProperties&, DOM::Element&) const;
|
||||
void compute_defaulted_values(StyleProperties&, DOM::Element const&) const;
|
||||
|
||||
template<typename Callback>
|
||||
void for_each_stylesheet(Callback) const;
|
||||
void for_each_stylesheet(CascadeOrigin, Callback) const;
|
||||
|
||||
struct MatchingRuleSet {
|
||||
Vector<MatchingRule> user_agent_rules;
|
||||
Vector<MatchingRule> author_rules;
|
||||
};
|
||||
|
||||
void cascade_declarations(StyleProperties&, DOM::Element&, Vector<MatchingRule> const&, CascadeOrigin, bool important) const;
|
||||
|
||||
DOM::Document& m_document;
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue