From b2cfa2b6b08174165e2ee585731b4366ead3ebfb Mon Sep 17 00:00:00 2001 From: cgqaq Date: Wed, 14 Jan 2026 16:44:34 +0800 Subject: [PATCH 01/12] fix(css): preserve author sheet DOM order and base URL - Cache author stylesheets in DOM order via StyleEngine and match against that list to preserve cascade semantics and avoid matching inline/link sheets twice. - Reuse cached UA default HTML stylesheet RuleSet instead of rebuilding per match. - Track pseudo-element matches during normal collection; only resolve ::before/::after when `content` is present, prefilter pseudo selector buckets, and clear stale pseudo styles via a per-element sent mask. - Initialize Document URL from script/module sourceURL (and Dart-provided URL for parseHTML/bytecode eval) so @import/url() never leak about:* bases across the bridge. - Add gtests for pseudo style gating. --- .../css/blink_pseudo_style_gating_test.cc | 108 ++ bridge/core/css/element_rule_collector.cc | 54 +- bridge/core/css/element_rule_collector.h | 11 + bridge/core/css/parser/css_property_parser.cc | 9 +- bridge/core/css/resolver/style_resolver.cc | 84 +- bridge/core/css/style_engine.cc | 1519 ++++++++++------- bridge/core/css/style_engine.h | 10 + bridge/core/css/style_rule_import.cc | 22 +- bridge/core/dom/document.cc | 18 +- bridge/core/dom/document.h | 5 + bridge/core/dom/element.h | 9 + bridge/core/executing_context.cc | 49 + bridge/core/executing_context.h | 6 + bridge/core/page.cc | 13 + bridge/core/page.h | 2 + bridge/include/webf_bridge.h | 2 + bridge/webf_bridge.cc | 6 +- webf/lib/src/bridge/from_native.dart | 10 +- webf/lib/src/bridge/to_native.dart | 52 +- webf/lib/src/html/head.dart | 3 +- webf/lib/src/html/script.dart | 2 +- webf/lib/src/launcher/controller.dart | 2 +- 22 files changed, 1243 insertions(+), 753 deletions(-) create mode 100644 bridge/core/css/blink_pseudo_style_gating_test.cc diff --git a/bridge/core/css/blink_pseudo_style_gating_test.cc b/bridge/core/css/blink_pseudo_style_gating_test.cc new file mode 100644 index 0000000000..0350cc637e --- /dev/null +++ b/bridge/core/css/blink_pseudo_style_gating_test.cc @@ -0,0 +1,108 @@ +#include "gtest/gtest.h" + +#include + +#include "foundation/string/wtf_string.h" +#include "foundation/ui_command_buffer.h" +#include "webf_test_env.h" + +using namespace webf; + +namespace { + +std::string CommandArg01ToUTF8(const UICommandItem& item) { + if (item.string_01 == 0 || item.args_01_length == 0) { + return ""; + } + const auto* utf16 = reinterpret_cast(static_cast(item.string_01)); + return String(utf16, static_cast(item.args_01_length)).ToUTF8String(); +} + +int64_t CountPseudoCommands(const UICommandItem* items, + int64_t length, + UICommand command, + const std::string& pseudo_type) { + int64_t count = 0; + for (int64_t i = 0; i < length; ++i) { + const UICommandItem& item = items[i]; + if (item.type != static_cast(command)) { + continue; + } + if (CommandArg01ToUTF8(item) == pseudo_type) { + count++; + } + } + return count; +} + +} // namespace + +TEST(BlinkPseudoStyleGating, DoesNotEmitPseudoStylesWithoutContent) { + auto env = TEST_init(nullptr, nullptr, 0, /*enable_blink=*/1); + auto* context = env->page()->executingContext(); + + // Flush bootstrap microtasks and drop any initial commands. + TEST_runLoop(context); + context->uiCommandBuffer()->clear(); + + const char* setup = R"JS( + const style = document.createElement('style'); + style.textContent = `.box::before { color: red; } .box { color: blue; }`; + document.body.appendChild(style); + + const div = document.createElement('div'); + div.className = 'box'; + div.textContent = 'hi'; + document.body.appendChild(div); + )JS"; + env->page()->evaluateScript(setup, strlen(setup), "vm://", 0); + TEST_runLoop(context); + + // Ignore DOM creation commands; only inspect style export. + context->uiCommandBuffer()->clear(); + { + MemberMutationScope scope{context}; + context->document()->UpdateStyleForThisDocument(); + } + context->uiCommandBuffer()->SyncAllPackages(); + + auto* pack = static_cast(context->uiCommandBuffer()->data()); + auto* items = static_cast(pack->data); + + EXPECT_EQ(CountPseudoCommands(items, pack->length, UICommand::kSetPseudoStyle, "before"), 0); + EXPECT_EQ(CountPseudoCommands(items, pack->length, UICommand::kClearPseudoStyle, "before"), 0); +} + +TEST(BlinkPseudoStyleGating, EmitsPseudoStylesWhenContentPresent) { + auto env = TEST_init(nullptr, nullptr, 0, /*enable_blink=*/1); + auto* context = env->page()->executingContext(); + + TEST_runLoop(context); + context->uiCommandBuffer()->clear(); + + const char* setup = R"JS( + const style = document.createElement('style'); + style.textContent = `.box::before { content: ""; color: red; }`; + document.body.appendChild(style); + + const div = document.createElement('div'); + div.className = 'box'; + div.textContent = 'hi'; + document.body.appendChild(div); + )JS"; + env->page()->evaluateScript(setup, strlen(setup), "vm://", 0); + TEST_runLoop(context); + + context->uiCommandBuffer()->clear(); + { + MemberMutationScope scope{context}; + context->document()->UpdateStyleForThisDocument(); + } + context->uiCommandBuffer()->SyncAllPackages(); + + auto* pack = static_cast(context->uiCommandBuffer()->data()); + auto* items = static_cast(pack->data); + + EXPECT_GT(CountPseudoCommands(items, pack->length, UICommand::kSetPseudoStyle, "before"), 0); + EXPECT_GT(CountPseudoCommands(items, pack->length, UICommand::kClearPseudoStyle, "before"), 0); +} diff --git a/bridge/core/css/element_rule_collector.cc b/bridge/core/css/element_rule_collector.cc index 45af33c395..ec15d76718 100644 --- a/bridge/core/css/element_rule_collector.cc +++ b/bridge/core/css/element_rule_collector.cc @@ -69,6 +69,14 @@ static bool RightmostCompoundTagMatchesElement(const RuleData& rule_data, return element.TagQName().LocalNameUpper() == actual.UpperASCII(); } +static uint32_t PseudoIdBit(PseudoId pseudo_id) { + unsigned id = static_cast(pseudo_id); + if (id >= 32) { + return 0; + } + return 1u << id; +} + ElementRuleCollector::ElementRuleCollector(StyleResolverState& state, SelectorChecker::Mode mode) : state_(state), element_(&state.GetElement()), @@ -209,7 +217,33 @@ void ElementRuleCollector::CollectMatchingRulesForList( if (!rule_data) { continue; } - + + // Fast prefilter for pseudo-element style collection: when a concrete + // pseudo-element (e.g. ::before) is requested, only selectors that actually + // target that pseudo-element can ever match. Without this, pseudo style + // resolution ends up iterating through all normal element rules in the same + // buckets and paying the full matcher cost only to fail at the end. + if (pseudo_element_id_ != kPseudoIdNone) { + // Only selectors that actually contain the requested pseudo-element can + // match during pseudo-element style collection. The pseudo-element is not + // always the rightmost simple selector (e.g. ".a::before:hover"), so scan + // the rightmost compound for a pseudo-element match. + bool targets_requested_pseudo = false; + for (const CSSSelector* s = &rule_data->Selector(); s; s = s->NextSimpleSelector()) { + if (s->Match() == CSSSelector::kPseudoElement) { + targets_requested_pseudo = CSSSelector::GetPseudoId(s->GetPseudoType()) == pseudo_element_id_; + break; + } + CSSSelector::RelationType rel = s->Relation(); + if (rel != CSSSelector::kSubSelector && rel != CSSSelector::kScopeActivation) { + break; + } + } + if (!targets_requested_pseudo) { + continue; + } + } + // Prevent processing too many rules if (++processed_count > MAX_RULES_TO_PROCESS) { break; @@ -249,6 +283,14 @@ void ElementRuleCollector::CollectMatchingRulesForList( // ::before/::after) may "match" only to mark pseudo presence. They must // not contribute properties to the originating element's style. if (pseudo_element_id_ == kPseudoIdNone && ua_match_result.dynamic_pseudo != kPseudoIdNone) { + uint32_t bit = PseudoIdBit(ua_match_result.dynamic_pseudo); + if (bit) { + matched_pseudo_element_mask_ |= bit; + if (rule_data->Rule() && + rule_data->Rule()->Properties().HasProperty(CSSPropertyID::kContent)) { + matched_pseudo_element_with_content_mask_ |= bit; + } + } continue; } DidMatchRule(rule_data, cascade_origin, cascade_layer, match_request); @@ -271,6 +313,14 @@ void ElementRuleCollector::CollectMatchingRulesForList( // ::before/::after) may "match" only to mark pseudo presence. They must // not contribute properties to the originating element's style. if (pseudo_element_id_ == kPseudoIdNone && match_result.dynamic_pseudo != kPseudoIdNone) { + uint32_t bit = PseudoIdBit(match_result.dynamic_pseudo); + if (bit) { + matched_pseudo_element_mask_ |= bit; + if (rule_data->Rule() && + rule_data->Rule()->Properties().HasProperty(CSSPropertyID::kContent)) { + matched_pseudo_element_with_content_mask_ |= bit; + } + } continue; } DidMatchRule(rule_data, cascade_origin, cascade_layer, match_request); @@ -406,6 +456,8 @@ void ElementRuleCollector::ClearMatchedRules() { matched_rules_.clear(); result_.Clear(); current_cascade_order_ = 0; + matched_pseudo_element_mask_ = 0; + matched_pseudo_element_with_content_mask_ = 0; } void ElementRuleCollector::AddElementStyleProperties( diff --git a/bridge/core/css/element_rule_collector.h b/bridge/core/css/element_rule_collector.h index c6ec0b5a36..aa0941549f 100644 --- a/bridge/core/css/element_rule_collector.h +++ b/bridge/core/css/element_rule_collector.h @@ -27,6 +27,7 @@ #ifndef WEBF_CSS_ELEMENT_RULE_COLLECTOR_H #define WEBF_CSS_ELEMENT_RULE_COLLECTOR_H +#include #include #include #include "core/css/css_rule_list.h" @@ -105,6 +106,13 @@ class ElementRuleCollector { // Get the match result const MatchResult& GetMatchResult() const { return result_; } + // Bitmask of pseudo-elements that matched while collecting normal (non-pseudo) + // element rules. Each bit is (1u << PseudoId) when PseudoId < 32. + uint32_t MatchedPseudoElementMask() const { return matched_pseudo_element_mask_; } + // Subset of MatchedPseudoElementMask() where at least one matched rule + // declares a `content` property (used to gate ::before/::after creation). + uint32_t MatchedPseudoElementWithContentMask() const { return matched_pseudo_element_with_content_mask_; } + // Pseudo element matching void SetPseudoElementStyleRequest(const PseudoElementStyleRequest&); void SetMatchingFromScope(bool matching_from_scope) { @@ -174,6 +182,9 @@ class ElementRuleCollector { bool is_collecting_for_pseudo_element_ = false; bool is_ua_rule_ = false; uint16_t current_cascade_order_ = 0; + + uint32_t matched_pseudo_element_mask_ = 0; + uint32_t matched_pseudo_element_with_content_mask_ = 0; }; } // namespace webf diff --git a/bridge/core/css/parser/css_property_parser.cc b/bridge/core/css/parser/css_property_parser.cc index 669917e856..41b8c23dc9 100644 --- a/bridge/core/css/parser/css_property_parser.cc +++ b/bridge/core/css/parser/css_property_parser.cc @@ -184,13 +184,12 @@ bool CSSPropertyParser::ParseValueStart(webf::CSSPropertyID unresolved_property, // Record the parser context base URL as a base href on this raw value so // that later pipeline stages (e.g. StyleEngine -> UICommand bridge) can // resolve relative url() tokens consistently with the stylesheet URL. - // Treat about:blank as "no href" so consumers fall back to the document or - // controller URL rather than using a synthetic base. + // Treat about:* as "no href" so consumers never receive about: bases across + // the bridge. if (context_) { const KURL& base_url = context_->BaseURL(); - std::string base = base_url.GetString(); - if (!base_url.IsEmpty() && base_url.IsValid() && base != "about:blank") { - raw->SetBaseHref(String::FromUTF8(base)); + if (!base_url.IsEmpty() && base_url.IsValid() && !base_url.ProtocolIsAbout()) { + raw->SetBaseHref(String::FromUTF8(base_url.GetString())); } } AddProperty(property_id, CSSPropertyID::kInvalid, raw, important, diff --git a/bridge/core/css/resolver/style_resolver.cc b/bridge/core/css/resolver/style_resolver.cc index ccfec32004..5a3f67d341 100644 --- a/bridge/core/css/resolver/style_resolver.cc +++ b/bridge/core/css/resolver/style_resolver.cc @@ -320,12 +320,9 @@ void StyleResolver::MatchUARules(ElementRuleCollector& collector) { // Match rules from the default HTML stylesheet auto html_style = CSSDefaultStyleSheets::DefaultHTMLStyle(); if (html_style && html_style->RuleCount() > 0) { - // Create a RuleSet from the stylesheet for matching - // TODO: This should be cached for performance - auto rule_set = std::make_shared(); ExecutingContext* context = document_->GetExecutingContext(); MediaQueryEvaluator evaluator(context); - rule_set->AddRulesFromSheet(html_style, evaluator, kRuleHasNoSpecialState); + auto rule_set = html_style->EnsureRuleSet(evaluator); // Create match request and collect matching rules MatchRequest request(rule_set, CascadeOrigin::kUserAgent); @@ -367,78 +364,31 @@ void StyleResolver::MatchAuthorRules( Element& element, ScopeOrdinal scope_ordinal, ElementRuleCollector& collector) { - // Match rules from author stylesheets (style elements, link elements) + // Match rules from author stylesheets registered with StyleEngine. + // Inline