mirror of
https://github.com/LadybirdBrowser/ladybird
synced 2026-04-26 01:35:08 +02:00
Toggling CSSStyleSheet::disabled previously cleared the cached media match bits and reloaded fonts, but never informed the owning documents or shadow roots that style resolution was now stale. Worse, the IDL binding for the disabled attribute dispatches through a non-virtual setter on StyleSheet, so any override on CSSStyleSheet was bypassed entirely. Make set_disabled() virtual so the CSSStyleSheet override actually runs, snapshot the pre-mutation shadow-root stylesheet effects before flipping the flag, and hand them to invalidate_owners() so a disable that strips the last host-reaching rule still tears down host-side style correctly.
676 lines
26 KiB
C++
676 lines
26 KiB
C++
/*
|
||
* Copyright (c) 2019-2022, Andreas Kling <andreas@ladybird.org>
|
||
* Copyright (c) 2022-2026, Sam Atkins <sam@ladybird.org>
|
||
* Copyright (c) 2024-2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
|
||
*
|
||
* SPDX-License-Identifier: BSD-2-Clause
|
||
*/
|
||
|
||
#include <LibURL/Parser.h>
|
||
#include <LibWeb/Bindings/CSSStyleSheet.h>
|
||
#include <LibWeb/Bindings/Intrinsics.h>
|
||
#include <LibWeb/CSS/CSSCounterStyleRule.h>
|
||
#include <LibWeb/CSS/CSSImportRule.h>
|
||
#include <LibWeb/CSS/CSSKeyframesRule.h>
|
||
#include <LibWeb/CSS/CSSStyleRule.h>
|
||
#include <LibWeb/CSS/CSSStyleSheet.h>
|
||
#include <LibWeb/CSS/FontComputer.h>
|
||
#include <LibWeb/CSS/Parser/Parser.h>
|
||
#include <LibWeb/CSS/StyleComputer.h>
|
||
#include <LibWeb/CSS/StyleSheetInvalidation.h>
|
||
#include <LibWeb/CSS/StyleSheetList.h>
|
||
#include <LibWeb/DOM/Document.h>
|
||
#include <LibWeb/DOM/StyleElementBase.h>
|
||
#include <LibWeb/HTML/HTMLLinkElement.h>
|
||
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
|
||
#include <LibWeb/HTML/Window.h>
|
||
#include <LibWeb/Platform/EventLoopPlugin.h>
|
||
#include <LibWeb/WebIDL/ExceptionOr.h>
|
||
|
||
namespace Web::CSS {
|
||
|
||
GC_DEFINE_ALLOCATOR(CSSStyleSheet);
|
||
|
||
GC::Ref<CSSStyleSheet> CSSStyleSheet::create(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location)
|
||
{
|
||
return realm.create<CSSStyleSheet>(realm, rules, media, move(location));
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-cssstylesheet
|
||
WebIDL::ExceptionOr<GC::Ref<CSSStyleSheet>> CSSStyleSheet::construct_impl(JS::Realm& realm, Optional<CSSStyleSheetInit> const& options)
|
||
{
|
||
// 1. Construct a new CSSStyleSheet object sheet.
|
||
auto sheet = create(realm, CSSRuleList::create(realm), CSS::MediaList::create(realm, {}), {});
|
||
|
||
// 2. Set sheet’s location to the base URL of the associated Document for the current principal global object.
|
||
auto associated_document = as<HTML::Window>(realm.global_object()).document();
|
||
sheet->set_location(associated_document->base_url());
|
||
|
||
// 3. Set sheet’s stylesheet base URL to the baseURL attribute value from options.
|
||
if (options.has_value() && options->base_url.has_value()) {
|
||
Optional<::URL::URL> sheet_location_url;
|
||
if (sheet->location().has_value())
|
||
sheet_location_url = sheet->location().release_value();
|
||
|
||
// AD-HOC: This isn't explicitly mentioned in the specification, but multiple modern browsers do this.
|
||
Optional<::URL::URL> url = sheet->location().has_value() ? sheet_location_url->complete_url(options->base_url.value()) : ::URL::Parser::basic_parse(options->base_url.value());
|
||
if (!url.has_value())
|
||
return WebIDL::NotAllowedError::create(realm, "Constructed style sheets must have a valid base URL"_utf16);
|
||
|
||
sheet->set_base_url(url);
|
||
}
|
||
|
||
// 4. Set sheet’s parent CSS style sheet to null.
|
||
sheet->set_parent_css_style_sheet(nullptr);
|
||
|
||
// 5. Set sheet’s owner node to null.
|
||
sheet->set_owner_node(nullptr);
|
||
|
||
// 6. Set sheet’s owner CSS rule to null.
|
||
sheet->set_owner_css_rule(nullptr);
|
||
|
||
// 7. Set sheet’s title to the empty string.
|
||
sheet->set_title(String {});
|
||
|
||
// 8. Unset sheet’s alternate flag.
|
||
sheet->set_alternate(false);
|
||
|
||
// 9. Set sheet’s origin-clean flag.
|
||
sheet->set_origin_clean(true);
|
||
|
||
// 10. Set sheet’s constructed flag.
|
||
sheet->set_constructed(true);
|
||
|
||
// 11. Set sheet’s Constructor document to the associated Document for the current global object.
|
||
sheet->set_constructor_document(associated_document);
|
||
|
||
// 12. If the media attribute of options is a string, create a MediaList object from the string and assign it as sheet’s media.
|
||
// Otherwise, serialize a media query list from the attribute and then create a MediaList object from the resulting string and set it as sheet’s media.
|
||
if (options.has_value()) {
|
||
if (options->media.has<String>()) {
|
||
sheet->set_media(options->media.get<String>());
|
||
} else {
|
||
sheet->m_media = *options->media.get<GC::Root<MediaList>>();
|
||
}
|
||
}
|
||
|
||
// 13. If the disabled attribute of options is true, set sheet’s disabled flag.
|
||
if (options.has_value() && options->disabled)
|
||
sheet->set_disabled(true);
|
||
|
||
// 14. Return sheet
|
||
return sheet;
|
||
}
|
||
|
||
CSSStyleSheet::CSSStyleSheet(JS::Realm& realm, CSSRuleList& rules, MediaList& media, Optional<::URL::URL> location)
|
||
: StyleSheet(realm, media)
|
||
, m_rules(&rules)
|
||
{
|
||
if (location.has_value())
|
||
set_location(move(location));
|
||
|
||
for (auto& rule : *m_rules)
|
||
rule->set_parent_style_sheet(this);
|
||
|
||
recalculate_rule_caches();
|
||
|
||
m_rules->on_change = [this]() {
|
||
recalculate_rule_caches();
|
||
};
|
||
}
|
||
|
||
void CSSStyleSheet::initialize(JS::Realm& realm)
|
||
{
|
||
WEB_SET_PROTOTYPE_FOR_INTERFACE(CSSStyleSheet);
|
||
Base::initialize(realm);
|
||
}
|
||
|
||
void CSSStyleSheet::visit_edges(Cell::Visitor& visitor)
|
||
{
|
||
Base::visit_edges(visitor);
|
||
visitor.visit(m_rules);
|
||
visitor.visit(m_owner_css_rule);
|
||
visitor.visit(m_default_namespace_rule);
|
||
visitor.visit(m_constructor_document);
|
||
visitor.visit(m_namespace_rules);
|
||
visitor.visit(m_import_rules);
|
||
visitor.visit(m_owning_documents_or_shadow_roots);
|
||
for (auto& subresource : m_critical_subresources)
|
||
subresource.visit_edges(visitor);
|
||
}
|
||
|
||
// https://www.w3.org/TR/cssom/#dom-cssstylesheet-insertrule
|
||
WebIDL::ExceptionOr<unsigned> CSSStyleSheet::insert_rule(StringView rule, unsigned index)
|
||
{
|
||
// FIXME: 1. If the origin-clean flag is unset, throw a SecurityError exception.
|
||
|
||
// If the disallow modification flag is set, throw a NotAllowedError DOMException.
|
||
if (disallow_modification())
|
||
return WebIDL::NotAllowedError::create(realm(), "Can't call insert_rule() on non-modifiable stylesheets."_utf16);
|
||
|
||
// 3. Let parsed rule be the return value of invoking parse a rule with rule.
|
||
auto parsed_rule = parse_css_rule(make_parsing_params(), rule);
|
||
|
||
// 4. If parsed rule is a syntax error, return parsed rule.
|
||
if (!parsed_rule)
|
||
return WebIDL::SyntaxError::create(realm(), "Unable to parse CSS rule."_utf16);
|
||
|
||
// 5. If parsed rule is an @import rule, and the constructed flag is set, throw a SyntaxError DOMException.
|
||
if (constructed() && parsed_rule->type() == CSSRule::Type::Import)
|
||
return WebIDL::SyntaxError::create(realm(), "Can't insert @import rules into a constructed stylesheet."_utf16);
|
||
|
||
// 6. Return the result of invoking insert a CSS rule rule in the CSS rules at index.
|
||
auto result = m_rules->insert_a_css_rule(parsed_rule, index, CSSRuleList::Nested::No, declared_namespaces());
|
||
|
||
if (!result.is_exception()) {
|
||
// NOTE: The spec doesn't say where to set the parent style sheet, so we'll do it here.
|
||
parsed_rule->set_parent_style_sheet(this);
|
||
|
||
if (!constructed() && owner_node() && owner_node()->is_html_style_element() && parsed_rule->type() == CSSRule::Type::Keyframes)
|
||
invalidate_owners_for_inserted_keyframes_rule(*this, as<CSSKeyframesRule>(*parsed_rule));
|
||
else if (!constructed() && owner_node() && owner_node()->is_html_style_element() && parsed_rule->type() == CSSRule::Type::Style)
|
||
invalidate_owners_for_inserted_style_rule(*this, as<CSSStyleRule>(*parsed_rule), DOM::StyleInvalidationReason::StyleSheetInsertRule);
|
||
else
|
||
invalidate_owners(DOM::StyleInvalidationReason::StyleSheetInsertRule);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// https://www.w3.org/TR/cssom/#dom-cssstylesheet-deleterule
|
||
WebIDL::ExceptionOr<void> CSSStyleSheet::delete_rule(unsigned index)
|
||
{
|
||
// FIXME: 1. If the origin-clean flag is unset, throw a SecurityError exception.
|
||
|
||
// 2. If the disallow modification flag is set, throw a NotAllowedError DOMException.
|
||
if (disallow_modification())
|
||
return WebIDL::NotAllowedError::create(realm(), "Can't call delete_rule() on non-modifiable stylesheets."_utf16);
|
||
|
||
// 3. Remove a CSS rule in the CSS rules at index.
|
||
auto previous_sheet_effects = determine_shadow_root_stylesheet_effects(*this);
|
||
auto result = m_rules->remove_a_css_rule(index);
|
||
if (!result.is_exception()) {
|
||
invalidate_owners(DOM::StyleInvalidationReason::StyleSheetDeleteRule, &previous_sheet_effects);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-replace
|
||
GC::Ref<WebIDL::Promise> CSSStyleSheet::replace(String text)
|
||
{
|
||
auto& realm = this->realm();
|
||
|
||
// 1. Let promise be a promise
|
||
auto promise = WebIDL::create_promise(realm);
|
||
|
||
// 2. If the constructed flag is not set, or the disallow modification flag is set, reject promise with a NotAllowedError DOMException and return promise.
|
||
if (!constructed()) {
|
||
WebIDL::reject_promise(realm, promise, WebIDL::NotAllowedError::create(realm, "Can't call replace() on non-constructed stylesheets"_utf16));
|
||
return promise;
|
||
}
|
||
|
||
if (disallow_modification()) {
|
||
WebIDL::reject_promise(realm, promise, WebIDL::NotAllowedError::create(realm, "Can't call replace() on non-modifiable stylesheets"_utf16));
|
||
return promise;
|
||
}
|
||
|
||
// 3. Set the disallow modification flag.
|
||
set_disallow_modification(true);
|
||
|
||
// 4. In parallel, do these steps:
|
||
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, this, text = move(text), promise = GC::Root(promise)] {
|
||
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
|
||
auto previous_sheet_effects = determine_shadow_root_stylesheet_effects(*this);
|
||
|
||
// 1. Let rules be the result of running parse a stylesheet’s contents from text.
|
||
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
|
||
|
||
// 2. If rules contains one or more @import rules, remove those rules from rules.
|
||
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm.heap());
|
||
for (auto rule : rules) {
|
||
if (rule->type() != CSSRule::Type::Import)
|
||
rules_without_import.append(rule);
|
||
}
|
||
|
||
// NOTE: The spec doesn't say where to set the parent style sheet, so we'll do it here.
|
||
for (auto& rule : rules_without_import)
|
||
rule->set_parent_style_sheet(this);
|
||
|
||
// 3. Set sheet’s CSS rules to rules.
|
||
m_rules->set_rules({}, rules_without_import);
|
||
invalidate_owners(DOM::StyleInvalidationReason::StyleSheetReplace, &previous_sheet_effects);
|
||
|
||
// 4. Unset sheet’s disallow modification flag.
|
||
set_disallow_modification(false);
|
||
|
||
// 5. Resolve promise with sheet.
|
||
WebIDL::resolve_promise(realm, *promise, this);
|
||
}));
|
||
|
||
return promise;
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-replacesync
|
||
WebIDL::ExceptionOr<void> CSSStyleSheet::replace_sync(StringView text)
|
||
{
|
||
// 1. If the constructed flag is not set, or the disallow modification flag is set, throw a NotAllowedError DOMException.
|
||
if (!constructed())
|
||
return WebIDL::NotAllowedError::create(realm(), "Can't call replaceSync() on non-constructed stylesheets"_utf16);
|
||
if (disallow_modification())
|
||
return WebIDL::NotAllowedError::create(realm(), "Can't call replaceSync() on non-modifiable stylesheets"_utf16);
|
||
|
||
// 2. Let rules be the result of running parse a stylesheet’s contents from text.
|
||
auto previous_sheet_effects = determine_shadow_root_stylesheet_effects(*this);
|
||
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
|
||
|
||
// 3. If rules contains one or more @import rules, remove those rules from rules.
|
||
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm().heap());
|
||
for (auto rule : rules) {
|
||
if (rule->type() != CSSRule::Type::Import)
|
||
rules_without_import.append(rule);
|
||
}
|
||
|
||
// NOTE: The spec doesn't say where to set the parent style sheet, so we'll do it here.
|
||
for (auto& rule : rules_without_import) {
|
||
rule->set_parent_style_sheet(this);
|
||
}
|
||
|
||
// 4. Set sheet’s CSS rules to rules.
|
||
m_rules->set_rules({}, rules_without_import);
|
||
invalidate_owners(DOM::StyleInvalidationReason::StyleSheetReplace, &previous_sheet_effects);
|
||
|
||
return {};
|
||
}
|
||
|
||
// https://drafts.csswg.org/cssom/#dom-cssstylesheet-addrule
|
||
WebIDL::ExceptionOr<WebIDL::Long> CSSStyleSheet::add_rule(Optional<String> selector, Optional<String> style, Optional<WebIDL::UnsignedLong> index)
|
||
{
|
||
// 1. Let rule be an empty string.
|
||
StringBuilder rule;
|
||
|
||
// 2. Append selector to rule.
|
||
if (selector.has_value())
|
||
rule.append(selector.release_value());
|
||
|
||
// 3. Append " { " to rule.
|
||
rule.append('{');
|
||
|
||
// 4. If block is not empty, append block, followed by a space, to rule.
|
||
if (style.has_value() && !style->is_empty())
|
||
rule.appendff("{} ", style.release_value());
|
||
|
||
// 5. Append "}" to rule.
|
||
rule.append('}');
|
||
|
||
// 6. Let index be optionalIndex if provided, or the number of CSS rules in the stylesheet otherwise.
|
||
// 7. Call insertRule(), with rule and index as arguments.
|
||
TRY(insert_rule(rule.string_view(), index.value_or(rules().length())));
|
||
|
||
// 8. Return -1.
|
||
return -1;
|
||
}
|
||
|
||
// https://www.w3.org/TR/cssom/#dom-cssstylesheet-removerule
|
||
WebIDL::ExceptionOr<void> CSSStyleSheet::remove_rule(Optional<WebIDL::UnsignedLong> index)
|
||
{
|
||
// The removeRule(index) method must run the same steps as deleteRule().
|
||
return delete_rule(index.value_or(0));
|
||
}
|
||
|
||
void CSSStyleSheet::for_each_effective_rule(TraversalOrder order, Function<void(Web::CSS::CSSRule const&)> const& callback) const
|
||
{
|
||
if (m_media->matches())
|
||
m_rules->for_each_effective_rule(order, callback);
|
||
}
|
||
|
||
void CSSStyleSheet::for_each_effective_style_producing_rule(Function<void(CSSRule const&)> const& callback) const
|
||
{
|
||
for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
|
||
if (rule.type() == CSSRule::Type::Style || rule.type() == CSSRule::Type::NestedDeclarations)
|
||
callback(rule);
|
||
});
|
||
}
|
||
|
||
void CSSStyleSheet::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const
|
||
{
|
||
for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
|
||
if (rule.type() == CSSRule::Type::Keyframes)
|
||
callback(static_cast<CSSKeyframesRule const&>(rule));
|
||
});
|
||
}
|
||
|
||
void CSSStyleSheet::for_each_effective_counter_style_at_rule(Function<void(CSSCounterStyleRule const&)> const& callback) const
|
||
{
|
||
for_each_effective_rule(TraversalOrder::Preorder, [&](CSSRule const& rule) {
|
||
if (rule.type() == CSSRule::Type::CounterStyle)
|
||
callback(static_cast<CSSCounterStyleRule const&>(rule));
|
||
});
|
||
}
|
||
|
||
void CSSStyleSheet::add_owning_document_or_shadow_root(DOM::Node& document_or_shadow_root)
|
||
{
|
||
VERIFY(document_or_shadow_root.is_document() || document_or_shadow_root.is_shadow_root());
|
||
m_owning_documents_or_shadow_roots.set(document_or_shadow_root);
|
||
|
||
// CSSOM's "add a CSS style sheet" steps bail out once the disabled flag is set, so ownership alone should not
|
||
// make a disabled sheet observable in the destination document. Delay CSS-connected font activation until the
|
||
// sheet actually becomes enabled.
|
||
if (!disabled() && this->owning_documents_or_shadow_roots().size() == 1)
|
||
document_or_shadow_root.document().font_computer().load_fonts_from_sheet(*this);
|
||
|
||
for (auto const& import_rule : m_import_rules) {
|
||
if (import_rule->loaded_style_sheet())
|
||
import_rule->loaded_style_sheet()->add_owning_document_or_shadow_root(document_or_shadow_root);
|
||
}
|
||
}
|
||
|
||
void CSSStyleSheet::remove_owning_document_or_shadow_root(DOM::Node& document_or_shadow_root)
|
||
{
|
||
m_owning_documents_or_shadow_roots.remove(document_or_shadow_root);
|
||
|
||
// All owning documents or shadow roots must be part of the same document so we only need to unload this style
|
||
// sheet's fonts once we have none remaining.
|
||
if (this->owning_documents_or_shadow_roots().size() == 0)
|
||
document_or_shadow_root.document().font_computer().unload_fonts_from_sheet(*this);
|
||
|
||
for (auto const& import_rule : m_import_rules) {
|
||
if (import_rule->loaded_style_sheet())
|
||
import_rule->loaded_style_sheet()->remove_owning_document_or_shadow_root(document_or_shadow_root);
|
||
}
|
||
}
|
||
|
||
void CSSStyleSheet::set_disabled(bool disabled)
|
||
{
|
||
if (this->disabled() == disabled)
|
||
return;
|
||
|
||
auto previous_sheet_effects = determine_shadow_root_stylesheet_effects(*this);
|
||
auto document = owning_document();
|
||
// When a stylesheet is disabled we stop evaluating its media queries, so both the cached top-level match bit
|
||
// and the MediaList's internal state can go stale across viewport changes. Clear the cache for both
|
||
// directions, and eagerly refresh on re-enable so subsequent rule-cache rebuilds see the current media state
|
||
// instead of the pre-disable one.
|
||
m_did_match = {};
|
||
StyleSheet::set_disabled(disabled);
|
||
|
||
if (!disabled) {
|
||
if (document) {
|
||
evaluate_media_queries(*document);
|
||
document->font_computer().load_fonts_from_sheet(*this);
|
||
load_pending_image_resources(*document);
|
||
}
|
||
} else if (document) {
|
||
document->font_computer().unload_fonts_from_sheet(*this);
|
||
}
|
||
|
||
invalidate_owners(DOM::StyleInvalidationReason::StyleSheetDisabledStateChange, &previous_sheet_effects);
|
||
}
|
||
|
||
void CSSStyleSheet::for_each_owning_style_scope(Function<void(StyleScope&)> const& callback) const
|
||
{
|
||
for (auto& document_or_shadow_root : m_owning_documents_or_shadow_roots) {
|
||
auto& style_scope = document_or_shadow_root->is_shadow_root()
|
||
? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
|
||
: document_or_shadow_root->document().style_scope();
|
||
|
||
callback(style_scope);
|
||
}
|
||
}
|
||
|
||
void CSSStyleSheet::invalidate_owners(DOM::StyleInvalidationReason reason, ShadowRootStylesheetEffects const* previous_sheet_effects)
|
||
{
|
||
m_did_match = {};
|
||
|
||
for (auto& document_or_shadow_root : m_owning_documents_or_shadow_roots) {
|
||
auto& style_scope = document_or_shadow_root->is_shadow_root()
|
||
? as<DOM::ShadowRoot>(*document_or_shadow_root).style_scope()
|
||
: document_or_shadow_root->document().style_scope();
|
||
|
||
style_scope.invalidate_rule_cache();
|
||
style_scope.node().invalidate_style(reason);
|
||
|
||
auto* shadow_root = as_if<DOM::ShadowRoot>(style_scope.node());
|
||
if (!shadow_root)
|
||
continue;
|
||
|
||
invalidate_assigned_elements_for_dirty_slots(*shadow_root);
|
||
|
||
auto* host = shadow_root->host();
|
||
if (!host)
|
||
continue;
|
||
|
||
auto effects = determine_shadow_root_stylesheet_effects(*shadow_root);
|
||
if (effects.may_match_light_dom_under_shadow_host && !effects.may_match_shadow_host) {
|
||
host->invalidate_style(reason);
|
||
} else if (effects.may_match_light_dom_under_shadow_host) {
|
||
host->root().invalidate_style(reason);
|
||
} else if (effects.may_affect_assigned_nodes_via_slots) {
|
||
host->invalidate_style(reason);
|
||
} else if (effects.may_match_shadow_host) {
|
||
host->invalidate_style(reason);
|
||
shadow_root->set_needs_style_update(true);
|
||
} else if (previous_sheet_effects) {
|
||
// `replaceSync("")`, `deleteRule()`, or disabling the last host-reaching rule can remove all evidence of
|
||
// host-side effects from the post-mutation stylesheet set. Fall back to the pre-mutation snapshot so the
|
||
// host side still gets the right invalidation.
|
||
if (previous_sheet_effects->may_match_light_dom_under_shadow_host && !previous_sheet_effects->may_match_shadow_host) {
|
||
host->invalidate_style(reason);
|
||
} else if (previous_sheet_effects->may_match_light_dom_under_shadow_host) {
|
||
host->root().invalidate_style(reason);
|
||
} else if (previous_sheet_effects->may_affect_assigned_nodes_via_slots) {
|
||
host->invalidate_style(reason);
|
||
} else if (previous_sheet_effects->may_match_shadow_host) {
|
||
host->invalidate_style(reason);
|
||
shadow_root->set_needs_style_update(true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
GC::Ptr<DOM::Document> CSSStyleSheet::owning_document() const
|
||
{
|
||
if (!m_owning_documents_or_shadow_roots.is_empty())
|
||
return (*m_owning_documents_or_shadow_roots.begin())->document();
|
||
|
||
if (auto* element = const_cast<CSSStyleSheet*>(this)->owner_node())
|
||
return element->document();
|
||
|
||
return nullptr;
|
||
}
|
||
|
||
void CSSStyleSheet::load_pending_image_resources(DOM::Document& document)
|
||
{
|
||
if (disabled())
|
||
return;
|
||
|
||
auto pending = move(m_pending_image_values);
|
||
for (auto const& weak_image_value : pending) {
|
||
if (auto* image_value = weak_image_value.ptr())
|
||
image_value->load_any_resources(document);
|
||
}
|
||
}
|
||
|
||
bool CSSStyleSheet::evaluate_media_queries(DOM::Document const& document)
|
||
{
|
||
bool any_media_queries_changed_match_state = false;
|
||
|
||
bool now_matches = m_media->evaluate(document);
|
||
if (!m_did_match.has_value() || m_did_match.value() != now_matches)
|
||
any_media_queries_changed_match_state = true;
|
||
if (now_matches && m_rules->evaluate_media_queries(document))
|
||
any_media_queries_changed_match_state = true;
|
||
|
||
m_did_match = now_matches;
|
||
|
||
return any_media_queries_changed_match_state;
|
||
}
|
||
|
||
Optional<FlyString> CSSStyleSheet::default_namespace() const
|
||
{
|
||
if (m_default_namespace_rule)
|
||
return m_default_namespace_rule->namespace_uri();
|
||
|
||
return {};
|
||
}
|
||
|
||
HashTable<FlyString> CSSStyleSheet::declared_namespaces() const
|
||
{
|
||
HashTable<FlyString> declared_namespaces;
|
||
|
||
for (auto namespace_ : m_namespace_rules.keys()) {
|
||
declared_namespaces.set(namespace_);
|
||
}
|
||
|
||
return declared_namespaces;
|
||
}
|
||
|
||
Optional<FlyString> CSSStyleSheet::namespace_uri(StringView namespace_prefix) const
|
||
{
|
||
return m_namespace_rules.get(namespace_prefix)
|
||
.map([](GC::Ptr<CSSNamespaceRule> namespace_) {
|
||
return namespace_->namespace_uri();
|
||
});
|
||
}
|
||
|
||
void CSSStyleSheet::recalculate_rule_caches()
|
||
{
|
||
m_default_namespace_rule = nullptr;
|
||
m_namespace_rules.clear();
|
||
m_import_rules.clear();
|
||
|
||
for (auto const& rule : *m_rules) {
|
||
// "Any @import rules must precede all other valid at-rules and style rules in a style sheet
|
||
// (ignoring @charset and @layer statement rules) and must not have any other valid at-rules
|
||
// or style rules between it and previous @import rules, or else the @import rule is invalid."
|
||
// https://drafts.csswg.org/css-cascade-5/#at-import
|
||
//
|
||
// "Any @namespace rules must follow all @charset and @import rules and precede all other
|
||
// non-ignored at-rules and style rules in a style sheet.
|
||
// ...
|
||
// A syntactically invalid @namespace rule (whether malformed or misplaced) must be ignored."
|
||
// https://drafts.csswg.org/css-namespaces/#syntax
|
||
switch (rule->type()) {
|
||
case CSSRule::Type::Import: {
|
||
// @import rules must appear before @namespace rules, so skip this if we've seen @namespace.
|
||
if (!m_namespace_rules.is_empty())
|
||
continue;
|
||
m_import_rules.append(as<CSSImportRule>(*rule));
|
||
break;
|
||
}
|
||
case CSSRule::Type::Namespace: {
|
||
auto& namespace_rule = as<CSSNamespaceRule>(*rule);
|
||
if (!namespace_rule.namespace_uri().is_empty() && namespace_rule.prefix().is_empty())
|
||
m_default_namespace_rule = namespace_rule;
|
||
|
||
m_namespace_rules.set(namespace_rule.prefix(), namespace_rule);
|
||
break;
|
||
}
|
||
default:
|
||
// Any other types mean that further @namespace rules are invalid, so we can stop here.
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
void CSSStyleSheet::set_source_text(String source)
|
||
{
|
||
m_source_text = move(source);
|
||
}
|
||
|
||
Optional<String> CSSStyleSheet::source_text(Badge<DOM::Document>) const
|
||
{
|
||
return m_source_text;
|
||
}
|
||
|
||
void CSSStyleSheet::add_critical_subresource(Subresource& subresource)
|
||
{
|
||
m_critical_subresources.append(subresource);
|
||
}
|
||
|
||
void CSSStyleSheet::remove_critical_subresource(Subresource& subresource)
|
||
{
|
||
m_critical_subresources.remove_first_matching([&](auto const& it) { return &it == &subresource; });
|
||
check_if_loading_completed();
|
||
}
|
||
|
||
CSSStyleSheet::LoadingState CSSStyleSheet::loading_state() const
|
||
{
|
||
bool any_loading = false;
|
||
bool any_errors = false;
|
||
|
||
for (auto const& subresource : m_critical_subresources) {
|
||
switch (subresource.loading_state()) {
|
||
case LoadingState::Unloaded:
|
||
case LoadingState::Loading:
|
||
any_loading = true;
|
||
break;
|
||
case LoadingState::Loaded:
|
||
break;
|
||
case LoadingState::Error:
|
||
any_errors = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (any_loading)
|
||
return LoadingState::Loading;
|
||
|
||
if (any_errors)
|
||
return LoadingState::Error;
|
||
|
||
return LoadingState::Loaded;
|
||
}
|
||
|
||
void CSSStyleSheet::check_if_loading_completed()
|
||
{
|
||
auto state = loading_state();
|
||
if (state == LoadingState::Loaded || state == LoadingState::Error) {
|
||
// We're finished loading, so propagate that to our owner.
|
||
if (auto* style_element = as_if<DOM::StyleElementBase>(owner_node())) {
|
||
style_element->finished_loading_critical_subresources(state == LoadingState::Error ? DOM::StyleElementBase::AnyFailed::Yes : DOM::StyleElementBase::AnyFailed::No);
|
||
} else if (auto* link_element = as_if<HTML::HTMLLinkElement>(owner_node())) {
|
||
link_element->finished_loading_critical_style_subresources(state == LoadingState::Error ? HTML::HTMLLinkElement::AnyFailed::Yes : HTML::HTMLLinkElement::AnyFailed::No);
|
||
} else if (auto* import_rule = as_if<CSSImportRule>(owner_rule().ptr())) {
|
||
import_rule->set_loading_state(state);
|
||
}
|
||
}
|
||
}
|
||
|
||
Parser::ParsingParams CSSStyleSheet::make_parsing_params() const
|
||
{
|
||
Parser::ParsingParams parsing_params;
|
||
if (auto document = owning_document())
|
||
parsing_params = Parser::ParsingParams { *document };
|
||
else
|
||
parsing_params = Parser::ParsingParams { realm() };
|
||
|
||
parsing_params.declared_namespaces = declared_namespaces();
|
||
return parsing_params;
|
||
}
|
||
|
||
StringView CSSStyleSheet::loading_state_name(LoadingState loading_state)
|
||
{
|
||
switch (loading_state) {
|
||
case LoadingState::Unloaded:
|
||
return "Unloaded"sv;
|
||
case LoadingState::Loading:
|
||
return "Loading"sv;
|
||
case LoadingState::Loaded:
|
||
return "Loaded"sv;
|
||
case LoadingState::Error:
|
||
return "Error"sv;
|
||
}
|
||
VERIFY_NOT_REACHED();
|
||
}
|
||
|
||
void CSSStyleSheet::Subresource::set_loading_state(LoadingState loading_state)
|
||
{
|
||
m_loading_state = loading_state;
|
||
if (loading_state == LoadingState::Loaded || loading_state == LoadingState::Error) {
|
||
if (auto style_sheet = parent_style_sheet_for_subresource())
|
||
style_sheet->check_if_loading_completed();
|
||
}
|
||
}
|
||
|
||
}
|