diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index 6d8e854ed3..7235741122 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -171,36 +171,6 @@ if (DEFINED PLATFORM) endif() endif() -# Generate CSS resource headers -# This section generates C++ header files from CSS source files -set(CSS_RESOURCES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/core/css/resources) - -# Output generated headers into the source tree under code_gen -set(GENERATED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/code_gen/) - -# Ensure the generated directory exists -file(MAKE_DIRECTORY ${GENERATED_DIR}) - -# Read HTML CSS content (no escaping needed for raw string literals) -file(READ ${CSS_RESOURCES_DIR}/html.css HTML_CSS_CONTENT) - -# Generate html_css.h from template -configure_file( - ${CSS_RESOURCES_DIR}/html_css.h.in - ${GENERATED_DIR}/html_css.h - @ONLY -) - -# Read Quirks CSS content (no escaping needed for raw string literals) -file(READ ${CSS_RESOURCES_DIR}/quirks.css QUIRKS_CSS_CONTENT) - -# Generate quirks_css.h from template -configure_file( - ${CSS_RESOURCES_DIR}/quirks_css.h.in - ${GENERATED_DIR}/quirks_css.h - @ONLY -) - # Ensure code_gen is in the include path for generated headers include_directories(${CMAKE_CURRENT_SOURCE_DIR}/code_gen) @@ -451,70 +421,3 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "iOS") ) endif() endif () - - -############################# -# Logging options -############################# - -# Helper to allow env var defaults while preserving -D cache overrides -function(_webf_option_with_env VAR DESC DEFAULT) - set(_default ${DEFAULT}) - if(DEFINED ENV{${VAR}}) - string(TOUPPER "$ENV{${VAR}}" _env_val) - if(_env_val STREQUAL "1" OR _env_val STREQUAL "ON" OR _env_val STREQUAL "TRUE" OR _env_val STREQUAL "YES") - set(_default ON) - elseif(_env_val STREQUAL "0" OR _env_val STREQUAL "OFF" OR _env_val STREQUAL "FALSE" OR _env_val STREQUAL "NO") - set(_default OFF) - else() - message(WARNING "Ignoring invalid value for ${VAR} environment variable: '$ENV{${VAR}}'. Expected ON/OFF/1/0/TRUE/FALSE/YES/NO.") - endif() - endif() - option(${VAR} "${DESC}" ${_default}) -endfunction() - -# Master switch to quickly enable all log categories -_webf_option_with_env(WEBF_LOG_ALL "Enable all WebF category logs" OFF) - -# Per-category logging switches -_webf_option_with_env(WEBF_LOG_PARSER "Enable Parser logs" OFF) -_webf_option_with_env(WEBF_LOG_STYLEENGINE "Enable StyleEngine logs" OFF) -_webf_option_with_env(WEBF_LOG_CASCADE "Enable Cascade logs" OFF) -_webf_option_with_env(WEBF_LOG_COLLECTOR "Enable Collector logs" OFF) -_webf_option_with_env(WEBF_LOG_STYLESHEET "Enable Stylesheet logs" OFF) -_webf_option_with_env(WEBF_LOG_SELECTOR "Enable Selector logs" OFF) -_webf_option_with_env(WEBF_LOG_ATTR "Enable Attr logs" OFF) -_webf_option_with_env(WEBF_LOG_COMMAND "Enable Command logs" OFF) - -macro(_webf_define_log VAR) - if (WEBF_LOG_ALL OR ${VAR}) - add_compile_definitions(${VAR}=1) - list(APPEND _WEBF_ENABLED_LOGS ${VAR}) - endif() -endmacro() - -set(_WEBF_ENABLED_LOGS) -_webf_define_log(WEBF_LOG_PARSER) -_webf_define_log(WEBF_LOG_STYLEENGINE) -_webf_define_log(WEBF_LOG_CASCADE) -_webf_define_log(WEBF_LOG_COLLECTOR) -_webf_define_log(WEBF_LOG_STYLESHEET) -_webf_define_log(WEBF_LOG_SELECTOR) -_webf_define_log(WEBF_LOG_ATTR) -_webf_define_log(WEBF_LOG_COMMAND) - -if (_WEBF_ENABLED_LOGS) - list(JOIN _WEBF_ENABLED_LOGS ", " _WEBF_ENABLED_LOGS_STR) - message(STATUS "WebF logging enabled for: ${_WEBF_ENABLED_LOGS_STR}") - # Ensure targets built in this directory also receive the definitions - foreach(_log ${_WEBF_ENABLED_LOGS}) - if (TARGET webf_core) - target_compile_definitions(webf_core PUBLIC ${_log}=1) - endif() - if (TARGET webf) - target_compile_definitions(webf PUBLIC ${_log}=1) - endif() - endforeach() -else() - message(STATUS "WebF logging: no categories enabled (define WEBF_LOG_* or set WEBF_LOG_ALL)") -endif() diff --git a/bridge/bridge_sources.json5 b/bridge/bridge_sources.json5 index 9c91678b01..50c27ebd15 100644 --- a/bridge/bridge_sources.json5 +++ b/bridge/bridge_sources.json5 @@ -148,7 +148,6 @@ "core/css/if_condition.cc", "core/css/style_sheet_contents.cc", "core/css/css_style_sheet.cc", - "core/css/css_default_style_sheets.cc", "core/css/style_rule_import.cc", "core/css/style_sheet.cc", "core/css/resolver/style_resolver.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/css_default_style_sheets.cc b/bridge/core/css/css_default_style_sheets.cc deleted file mode 100644 index fe2d788543..0000000000 --- a/bridge/core/css/css_default_style_sheets.cc +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2022-present The WebF authors. All rights reserved. - */ - -#include "css_default_style_sheets.h" -#include "core/css/style_sheet_contents.h" -#include "core/css/parser/css_parser_context.h" -#include "core/css/parser/css_parser.h" -#include "core/css/rule_set.h" -#include "foundation/logging.h" -#include "code_gen/html_css.h" -#include "code_gen/quirks_css.h" - -// Undefine Windows macros that conflict with our logging constants -#ifdef ERROR -#undef ERROR -#endif - -namespace webf { - -// Default HTML stylesheet - loaded from bridge/core/css/resources/html.css via CMake -const char* kHTMLDefaultStyle = kHTMLDefaultCSS; - - -// Quirks mode stylesheet -const char* kQuirksDefaultStyle = kQuirksDefaultCSS; - -std::shared_ptr CSSDefaultStyleSheets::default_html_style_; -std::shared_ptr CSSDefaultStyleSheets::default_svg_style_; -std::shared_ptr CSSDefaultStyleSheets::default_mathml_style_; -std::shared_ptr CSSDefaultStyleSheets::media_controls_style_; -std::shared_ptr CSSDefaultStyleSheets::fullscreen_style_; -std::shared_ptr CSSDefaultStyleSheets::quirks_style_; -bool CSSDefaultStyleSheets::is_initialized_ = false; - -void CSSDefaultStyleSheets::Init() { - if (is_initialized_) { - return; - } - - // Parse default HTML stylesheet - // default_html_style_ = ParseUASheet(kHTMLDefaultStyle); - // - // if (default_html_style_) { - // WEBF_LOG(VERBOSE) << "UA stylesheet parsed, rule count: " << default_html_style_->RuleCount(); - // } else { - // WEBF_LOG(ERROR) << "Failed to parse UA stylesheet"; - // } - - // Parse quirks mode stylesheet - quirks_style_ = ParseUASheet(kQuirksDefaultStyle); - - // TODO: Add SVG, MathML, media controls, and fullscreen stylesheets when needed - auto parser_context = std::make_shared(kUASheetMode); - default_svg_style_ = std::make_shared(parser_context); - default_mathml_style_ = std::make_shared(parser_context); - media_controls_style_ = std::make_shared(parser_context); - fullscreen_style_ = std::make_shared(parser_context); - - is_initialized_ = true; -} - -bool CSSDefaultStyleSheets::IsInitialized() { - return is_initialized_; -} - -std::shared_ptr CSSDefaultStyleSheets::DefaultHTMLStyle() { - if (!is_initialized_) { - Init(); - } - return default_html_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::DefaultSVGStyle() { - if (!is_initialized_) { - Init(); - } - return default_svg_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::DefaultMathMLStyle() { - if (!is_initialized_) { - Init(); - } - return default_mathml_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::MediaControlsStyle() { - if (!is_initialized_) { - Init(); - } - return media_controls_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::FullscreenStyle() { - if (!is_initialized_) { - Init(); - } - return fullscreen_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::QuirksStyle() { - if (!is_initialized_) { - Init(); - } - return quirks_style_; -} - -std::shared_ptr CSSDefaultStyleSheets::ParseUASheet(const char* css) { - // UA stylesheets always parse in the UA sheet mode - auto parser_context = std::make_shared(kUASheetMode); - auto sheet = std::make_shared(parser_context); - - - // TODO: remove UA Style all togather. - // Parse the CSS string - we need to use ParseSheet directly with the UA context - // instead of ParseString which creates its own context - // CSSParser::ParseSheet(parser_context, sheet, String::FromUTF8(css)); - - // WEBF_LOG(VERBOSE) << "Parsed UA stylesheet, rule count: " << sheet->RuleCount(); - - return sheet; -} - -void CSSDefaultStyleSheets::Reset() { - // Reset all static style sheets to release memory - default_html_style_.reset(); - default_svg_style_.reset(); - default_mathml_style_.reset(); - media_controls_style_.reset(); - fullscreen_style_.reset(); - quirks_style_.reset(); - - // Mark as uninitialized so they can be recreated if needed - is_initialized_ = false; -} - -} // namespace webf \ No newline at end of file diff --git a/bridge/core/css/css_default_style_sheets.h b/bridge/core/css/css_default_style_sheets.h deleted file mode 100644 index dc757a54ea..0000000000 --- a/bridge/core/css/css_default_style_sheets.h +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2022-present The WebF authors. All rights reserved. - */ - -#ifndef WEBF_CSS_DEFAULT_STYLE_SHEETS_H -#define WEBF_CSS_DEFAULT_STYLE_SHEETS_H - -#include -#include "foundation/macros.h" - -namespace webf { - -class StyleSheetContents; -class Document; - -// Manages the default style sheets for different contexts -class CSSDefaultStyleSheets { - WEBF_STATIC_ONLY(CSSDefaultStyleSheets); - - public: - // Get the default HTML style sheet - static std::shared_ptr DefaultHTMLStyle(); - - // Get the default SVG style sheet - static std::shared_ptr DefaultSVGStyle(); - - // Get the default MathML style sheet - static std::shared_ptr DefaultMathMLStyle(); - - // Get the default media controls style sheet - static std::shared_ptr MediaControlsStyle(); - - // Get the default full screen style sheet - static std::shared_ptr FullscreenStyle(); - - // Get the quirks mode style sheet - static std::shared_ptr QuirksStyle(); - - // Initialize default styles - static void Init(); - - // Check if initialized - static bool IsInitialized(); - - // Reset all default style sheets (for testing) - static void Reset(); - - private: - static std::shared_ptr ParseUASheet(const char* css); - - static std::shared_ptr default_html_style_; - static std::shared_ptr default_svg_style_; - static std::shared_ptr default_mathml_style_; - static std::shared_ptr media_controls_style_; - static std::shared_ptr fullscreen_style_; - static std::shared_ptr quirks_style_; - static bool is_initialized_; -}; - -} // namespace webf - -#endif // WEBF_CSS_DEFAULT_STYLE_SHEETS_H \ No newline at end of file diff --git a/bridge/core/css/css_global_rule_set.cc b/bridge/core/css/css_global_rule_set.cc index 8dde393db7..267807729d 100644 --- a/bridge/core/css/css_global_rule_set.cc +++ b/bridge/core/css/css_global_rule_set.cc @@ -65,7 +65,7 @@ void CSSGlobalRuleSet::Update(Document& document) { is_dirty_ = false; features_.Clear(); - // UA / watched / document-rule selector features are currently not wired. + // Watched / document-rule selector features are currently not wired. // We conservatively aggregate only author-sheet features via StyleEngine. document.EnsureStyleEngine().CollectFeaturesTo(features_); } @@ -75,7 +75,6 @@ void CSSGlobalRuleSet::Dispose() { features_.Clear(); watched_selectors_rule_set_ = nullptr; document_rules_selectors_rule_set_ = nullptr; - has_fullscreen_ua_style_ = false; is_dirty_ = true; */ diff --git a/bridge/core/css/css_global_rule_set.h b/bridge/core/css/css_global_rule_set.h index 21767fd19a..073b4fa57c 100644 --- a/bridge/core/css/css_global_rule_set.h +++ b/bridge/core/css/css_global_rule_set.h @@ -15,8 +15,8 @@ class RuleSet; // A per Document collection of CSS metadata used for style matching and // invalidation. The data is aggregated from author rulesets from all TreeScopes -// in the whole Document as well as UA stylesheets and watched selectors which -// apply to elements in all TreeScopes. +// in the whole Document as well as watched selectors which apply to elements in +// all TreeScopes. // // TODO(futhark@chromium.org): We would like to move as much of this data as // possible to the ScopedStyleResolver as possible to avoid full reconstruction @@ -38,11 +38,9 @@ class CSSGlobalRuleSet final { const RuleFeatureSet& GetRuleFeatureSet() const { return features_; } RuleSet* WatchedSelectorsRuleSet() const { return watched_selectors_rule_set_.get(); } RuleSet* DocumentRulesSelectorsRuleSet() const { return document_rules_selectors_rule_set_.get(); } - bool HasFullscreenUAStyle() const { return has_fullscreen_ua_style_; } private: - // Constructed from rules in all TreeScopes including UA style and style - // injected from extensions. + // Constructed from rules in all TreeScopes and style injected from extensions. RuleFeatureSet features_; // Rules injected from extensions. @@ -52,7 +50,6 @@ class CSSGlobalRuleSet final { // rules. std::shared_ptr document_rules_selectors_rule_set_; - bool has_fullscreen_ua_style_ = false; bool is_dirty_ = true; }; diff --git a/bridge/core/css/element_rule_collector.cc b/bridge/core/css/element_rule_collector.cc index 45af33c395..46d504c45f 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()), @@ -82,15 +90,13 @@ ElementRuleCollector::~ElementRuleCollector() = default; void ElementRuleCollector::CollectMatchingRules(const MatchRequest& match_request) { assert(element_); - // Collect rules from the match request - for (const auto& rule_set : match_request.GetRuleSets()) { - if (rule_set) { - CollectRuleSetMatchingRules(match_request, - rule_set, - match_request.GetOrigin(), - 0); + // Collect rules from the match request. + match_request.ForEachRuleSet([&](const std::shared_ptr& rule_set) { + if (!rule_set) { + return; } - } + CollectRuleSetMatchingRules(match_request, rule_set, match_request.GetOrigin(), 0); + }); } void ElementRuleCollector::CollectRuleSetMatchingRules( @@ -132,24 +138,14 @@ void ElementRuleCollector::CollectRuleSetMatchingRules( // any pure #id variants that may have been created elsewhere. bool typed_exists = false; for (const auto& rd : id_rules) { - if (rd && rd->HasRightmostType()) { typed_exists = true; break; } - } - if (typed_exists) { - std::vector> typed_only; - typed_only.reserve(id_rules.size()); - for (const auto& rd : id_rules) { - if (rd && rd->HasRightmostType()) typed_only.push_back(rd); + if (rd && rd->HasRightmostType()) { + typed_exists = true; + break; } - CollectMatchingRulesForList(typed_only, - cascade_origin, - cascade_layer, - match_request); - } else { - CollectMatchingRulesForList(id_rules, - cascade_origin, - cascade_layer, - match_request); } + + CollectMatchingRulesForList(id_rules, cascade_origin, cascade_layer, match_request, + /*is_id_bucket*/ true, /*typed_rules_only*/ typed_exists); } } @@ -172,51 +168,52 @@ void ElementRuleCollector::CollectMatchingRulesForList( const RuleDataListType& rules, CascadeOrigin cascade_origin, CascadeLayerLevel cascade_layer, - const MatchRequest& match_request) { + const MatchRequest& match_request, + bool is_id_bucket, + bool typed_rules_only) { // Safety check - don't process too many rules to prevent hangs size_t processed_count = 0; const size_t MAX_RULES_TO_PROCESS = 1000; - - // If this is an ID bucket (rules all target the element's id), and there are - // typed compounds (e.g. P#id), prefer those and ignore pure-id variants. This - // guards against accidental id-only entries created upstream and aligns with - // Blink behavior for compound matching specificity. - auto extract_rightmost_id = [](const CSSSelector& sel) -> AtomicString { - const CSSSelector* s = &sel; - while (s) { - if (s->Match() == CSSSelector::kId) { - return s->Value(); - } - s = s->NextSimpleSelector(); - } - return g_null_atom; - }; - - bool maybe_id_bucket = false; - if (!rules.empty() && rules.front()) { - AtomicString idv = extract_rightmost_id(rules.front()->Selector()); - maybe_id_bucket = !idv.IsNull() && element_->HasID() && idv == element_->id(); - } - bool typed_exists_in_bucket = false; - if (maybe_id_bucket) { - for (const auto& rd : rules) { - if (rd && rd->HasRightmostType()) { typed_exists_in_bucket = true; break; } - } - } for (const auto& rule_data : rules) { 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; } // Skip pure-id variants if we have typed compounds in the same ID bucket. - if (maybe_id_bucket && typed_exists_in_bucket && !rule_data->HasRightmostType()) { + if (is_id_bucket && typed_rules_only && !rule_data->HasRightmostType()) { continue; } @@ -236,26 +233,6 @@ void ElementRuleCollector::CollectMatchingRulesForList( continue; } - // UA stylesheet matching must respect full selectors (combinators, pseudos). - // Previously we short-circuited on tag-only which caused rules like - // "td > p:first-child" to (incorrectly) match all

, zeroing margins. - if (cascade_origin == CascadeOrigin::kUserAgent) { - SelectorChecker::MatchResult ua_match_result; - // Ensure UA rule matching respects requested pseudo-element (e.g., ::before/::after) - context.pseudo_id = pseudo_element_id_; - bool ua_matched = selector_checker_.Match(context, ua_match_result); - if (ua_matched) { - // When collecting normal element rules, pseudo-element selectors (e.g. - // ::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) { - continue; - } - DidMatchRule(rule_data, cascade_origin, cascade_layer, match_request); - } - continue; // Handled UA case. - } - SelectorChecker::MatchResult match_result; // Avoid calling TagQName() unless the selector is a tag selector. // Non-tag selectors (class, id, attribute, pseudo, etc.) would hit @@ -271,6 +248,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); @@ -345,7 +330,7 @@ void ElementRuleCollector::SortAndTransferMatchedRules() { void ElementRuleCollector::SortMatchedRules() { // Sort by cascade order - std::stable_sort(matched_rules_.begin(), matched_rules_.end(), + std::sort(matched_rules_.begin(), matched_rules_.end(), [](const MatchedRule& a, const MatchedRule& b) { // CSS Cascade order (aligned with CascadePriority::ForLayerComparison): // 1) Origin (UA < User < Author < Animation < Transition) @@ -406,6 +391,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..6797dd4357 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) { @@ -137,7 +145,9 @@ class ElementRuleCollector { const RuleDataListType& rules, CascadeOrigin, CascadeLayerLevel, - const MatchRequest&); + const MatchRequest&, + bool is_id_bucket = false, + bool typed_rules_only = false); void DidMatchRule(std::shared_ptr, CascadeOrigin, @@ -174,6 +184,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/media_values.cc b/bridge/core/css/media_values.cc index 962b0225dd..e97d2870cf 100644 --- a/bridge/core/css/media_values.cc +++ b/bridge/core/css/media_values.cc @@ -77,10 +77,20 @@ MediaValues* MediaValues::CreateDynamicIfFrameExists(ExecutingContext* context) } double MediaValues::CalculateViewportWidth(ExecutingContext* context) { + if (context) { + if (std::optional cached = context->CachedViewportWidth(); cached.has_value()) { + return *cached; + } + } return GetInnerDimension(context, binding_call_methods::kinnerWidth, kDefaultViewportWidth); } double MediaValues::CalculateViewportHeight(ExecutingContext* context) { + if (context) { + if (std::optional cached = context->CachedViewportHeight(); cached.has_value()) { + return *cached; + } + } return GetInnerDimension(context, binding_call_methods::kinnerHeight, kDefaultViewportHeight); } @@ -136,6 +146,11 @@ bool MediaValues::CalculateStrictMode(ExecutingContext* context) { } float MediaValues::CalculateDevicePixelRatio(ExecutingContext* context) { + if (context) { + if (std::optional cached = context->CachedDevicePixelRatio(); cached.has_value()) { + return *cached; + } + } Window* window = GetWindow(context); if (!window) { return kDefaultDevicePixelRatio; diff --git a/bridge/core/css/media_values_dynamic.cc b/bridge/core/css/media_values_dynamic.cc index a8e8327069..3cbdef31cf 100644 --- a/bridge/core/css/media_values_dynamic.cc +++ b/bridge/core/css/media_values_dynamic.cc @@ -59,6 +59,19 @@ const String MediaValuesDynamic::MediaType() const { } CSSValueID MediaValuesDynamic::PreferredColorScheme() const { + if (context_) { + if (auto cached = context_->CachedPreferredColorScheme(); cached.has_value()) { + switch (*cached) { + case ExecutingContext::PreferredColorScheme::kDark: + return CSSValueID::kDark; + case ExecutingContext::PreferredColorScheme::kLight: + return CSSValueID::kLight; + case ExecutingContext::PreferredColorScheme::kNoPreference: + return CSSValueID::kNoPreference; + } + } + } + Window* window = context_ ? context_->window() : nullptr; if (!window) { return CSSValueID::kLight; @@ -71,11 +84,23 @@ CSSValueID MediaValuesDynamic::PreferredColorScheme() const { NativeValueConverter::FromNativeValue(window->ctx(), std::move(dart_result)); if (scheme == AtomicString::CreateFromUTF8("dark")) { + if (context_) { + context_->SetCachedPreferredColorScheme(ExecutingContext::PreferredColorScheme::kDark); + } return CSSValueID::kDark; } if (scheme == AtomicString::CreateFromUTF8("light")) { + if (context_) { + context_->SetCachedPreferredColorScheme(ExecutingContext::PreferredColorScheme::kLight); + } return CSSValueID::kLight; } + if (scheme == AtomicString::CreateFromUTF8("no-preference")) { + if (context_) { + context_->SetCachedPreferredColorScheme(ExecutingContext::PreferredColorScheme::kNoPreference); + } + return CSSValueID::kNoPreference; + } return CSSValueID::kLight; } 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/cascade_origin.h b/bridge/core/css/resolver/cascade_origin.h index fb3bc01bf2..7b50f1bfa6 100644 --- a/bridge/core/css/resolver/cascade_origin.h +++ b/bridge/core/css/resolver/cascade_origin.h @@ -36,7 +36,7 @@ namespace webf { // https://drafts.csswg.org/css-cascade/#cascade-origin // // The top 5 bits of CascadePriority::priority_ are used to store the -// cascade origin and the important bit. The lower values represent higher +// cascade origin and the important bit. Higher values represent higher // priority. // // The important bit is set by inverting the origin value. This way important @@ -46,8 +46,12 @@ namespace webf { // exist in-between author and !important author, we need to stretch the // cascade origin by one extra bit. // -// All origins (without the important bit) must fit in 5 bits. The important -// bit is used to generate important origins by flipping the origin value. +// Implementation note: +// - The low 4 bits store origin + the important bit (bit 3). +// - Bit 4 is reserved to "stretch" the origin space for transitions. +// - Important origins are produced by inverting only the low 4 bits, keeping +// the transition bit untouched. This matches CascadePriority::ForLayerComparison(), +// which removes importance by flipping only the origin/importance low 4 bits. enum class StyleCascadeOrigin : uint8_t { kUserAgent = 0b00001, kUser = 0b00010, @@ -56,9 +60,9 @@ enum class StyleCascadeOrigin : uint8_t { kAnimation = 0b00101, // Important versions (inverted): - kImportantAuthor = 0b11011, // ~kAuthor & 0x1F - kImportantUser = 0b11101, // ~kUser & 0x1F - kImportantUserAgent = 0b11110, // ~kUserAgent & 0x1F + kImportantAuthor = 0b01011, // ~kAuthor & 0x0F + kImportantUser = 0b01101, // ~kUser & 0x0F + kImportantUserAgent = 0b01110, // ~kUserAgent & 0x0F kTransition = 0b10000, @@ -66,22 +70,29 @@ enum class StyleCascadeOrigin : uint8_t { kMax = kTransition, }; -constexpr uint8_t kCascadeOriginImportantBit = 0b10000; +// Bit 3 of the low 4 origin/importance bits. +constexpr uint8_t kCascadeOriginImportantBit = 0b01000; inline bool IsImportantOrigin(StyleCascadeOrigin origin) { return static_cast(origin) & kCascadeOriginImportantBit; } inline StyleCascadeOrigin ToImportantOrigin(StyleCascadeOrigin origin) { - return static_cast(~static_cast(origin) & 0x1F); + uint8_t value = static_cast(origin); + uint8_t transition_bit = value & 0b10000; + uint8_t flipped = (~value) & 0b01111; + return static_cast(transition_bit | flipped); } inline StyleCascadeOrigin ToNonImportantOrigin(StyleCascadeOrigin origin) { if (!IsImportantOrigin(origin)) return origin; - return static_cast(~static_cast(origin) & 0x1F); + uint8_t value = static_cast(origin); + uint8_t transition_bit = value & 0b10000; + uint8_t flipped = (~value) & 0b01111; + return static_cast(transition_bit | flipped); } } // namespace webf -#endif // WEBF_CORE_CSS_RESOLVER_CASCADE_ORIGIN_H_ \ No newline at end of file +#endif // WEBF_CORE_CSS_RESOLVER_CASCADE_ORIGIN_H_ diff --git a/bridge/core/css/resolver/match_request.h b/bridge/core/css/resolver/match_request.h index cae9a91534..4d13374d8b 100644 --- a/bridge/core/css/resolver/match_request.h +++ b/bridge/core/css/resolver/match_request.h @@ -24,31 +24,45 @@ class MatchRequest { CascadeOrigin origin = CascadeOrigin::kAuthor, unsigned style_sheet_index = 0) : origin_(origin), - style_sheet_index_(style_sheet_index) { - if (rule_set) { - rule_sets_.push_back(rule_set); - } - } + style_sheet_index_(style_sheet_index), + primary_rule_set_(std::move(rule_set)) {} void AddRuleSet(std::shared_ptr rule_set) { - if (rule_set) { - rule_sets_.push_back(rule_set); + if (!rule_set) { + return; + } + if (!primary_rule_set_) { + primary_rule_set_ = std::move(rule_set); + return; } + additional_rule_sets_.push_back(std::move(rule_set)); } - const std::vector>& GetRuleSets() const { - return rule_sets_; + template + void ForEachRuleSet(Callback&& callback) const { + if (primary_rule_set_) { + callback(primary_rule_set_); + } + for (const auto& rule_set : additional_rule_sets_) { + if (rule_set) { + callback(rule_set); + } + } } CascadeOrigin GetOrigin() const { return origin_; } unsigned GetStyleSheetIndex() const { return style_sheet_index_; } private: - std::vector> rule_sets_; + // Most callers match against a single RuleSet. Keep that in an inline slot + // to avoid per-request heap allocation (std::vector would allocate even for + // a single entry). Additional RuleSets are stored in a side vector. + std::shared_ptr primary_rule_set_; + std::vector> additional_rule_sets_; CascadeOrigin origin_ = CascadeOrigin::kAuthor; unsigned style_sheet_index_ = 0; }; } // namespace webf -#endif // WEBF_CSS_RESOLVER_MATCH_REQUEST_H \ No newline at end of file +#endif // WEBF_CSS_RESOLVER_MATCH_REQUEST_H diff --git a/bridge/core/css/resolver/style_builder_test.cc b/bridge/core/css/resolver/style_builder_test.cc index 955e05de6e..d753da2b92 100644 --- a/bridge/core/css/resolver/style_builder_test.cc +++ b/bridge/core/css/resolver/style_builder_test.cc @@ -18,7 +18,6 @@ #include "webf_test_env.h" #include "bindings/qjs/cppgc/mutation_scope.h" #include "bindings/qjs/cppgc/member.h" -#include "core/css/css_default_style_sheets.h" namespace webf { @@ -55,9 +54,6 @@ class StyleBuilderTest : public ::testing::Test { } } - // Reset UA stylesheets to avoid memory leaks - CSSDefaultStyleSheets::Reset(); - document_ = nullptr; context_ = nullptr; env_.reset(); diff --git a/bridge/core/css/resolver/style_cascade.cc b/bridge/core/css/resolver/style_cascade.cc index 29991f6758..085d1d2a27 100644 --- a/bridge/core/css/resolver/style_cascade.cc +++ b/bridge/core/css/resolver/style_cascade.cc @@ -154,119 +154,82 @@ void StyleCascade::AnalyzeMatchResult() { map_.Reset(); const auto& matched_properties = match_result_.GetMatchedProperties(); - uint32_t position = 0; - - // WEBF_LOG(VERBOSE) << "AnalyzeMatchResult: " << matched_properties.size() << " matched property sets"; - + + // MatchResult is expected to be provided in non-decreasing cascade-layer + // order (origin/tree/layer), which allows us to build the CascadeMap in a + // single pass without collecting and sorting per-property priorities. + // This is a major hot path during full subtree style recalculation. uint16_t block_index = 0; - // Collect priorities per-property, then flush to CascadeMap sorted by - // ForLayerComparison to satisfy CascadeMap's insertion invariant. - std::vector> native_priorities(static_cast(kNumCSSProperties)); - std::unordered_map, AtomicString::KeyHasher> custom_priorities; for (const auto& entry : matched_properties) { if (!entry.properties) { + ++block_index; continue; } - - const StylePropertySet& properties = *entry.properties; - - // Convert to cascade origin - CascadeOrigin cascade_origin = CascadeOrigin::kAuthor; - if (entry.origin == webf::CascadeOrigin::kUserAgent) { - cascade_origin = CascadeOrigin::kUserAgent; - } else if (entry.origin == webf::CascadeOrigin::kUser) { - cascade_origin = CascadeOrigin::kUser; + + StyleCascadeOrigin base_origin = StyleCascadeOrigin::kAuthor; + switch (entry.origin) { + case webf::CascadeOrigin::kUserAgent: + base_origin = StyleCascadeOrigin::kUserAgent; + break; + case webf::CascadeOrigin::kUser: + base_origin = StyleCascadeOrigin::kUser; + break; + case webf::CascadeOrigin::kAuthor: + base_origin = StyleCascadeOrigin::kAuthor; + break; + case webf::CascadeOrigin::kAnimation: + base_origin = StyleCascadeOrigin::kAnimation; + break; + case webf::CascadeOrigin::kTransition: + base_origin = StyleCascadeOrigin::kTransition; + break; + default: + base_origin = StyleCascadeOrigin::kAuthor; + break; } - - // Process each property in the set - // WEBF_LOG(VERBOSE) << "Property set has " << properties.PropertyCount() << " properties, origin: " << static_cast(entry.origin); - + + const StylePropertySet& properties = *entry.properties; uint16_t declaration_index = 0; for (unsigned i = 0; i < properties.PropertyCount(); ++i, ++declaration_index) { const StylePropertySet::PropertyReference property = properties.PropertyAt(i); - + if (property.Id() == CSSPropertyID::kInvalid) { + continue; + } if (!property.Value() || !*property.Value()) { continue; } - - // WEBF_LOG(VERBOSE) << "Property ID: " << static_cast(property.Id()) << " value: " << property.Value()->get()->CssText(); - - // Convert cascade origin based on importance - StyleCascadeOrigin style_origin = StyleCascadeOrigin::kAuthor; - if (cascade_origin == CascadeOrigin::kUserAgent) { - style_origin = property.IsImportant() ? - StyleCascadeOrigin::kImportantUserAgent : StyleCascadeOrigin::kUserAgent; - } else if (cascade_origin == CascadeOrigin::kUser) { - style_origin = property.IsImportant() ? - StyleCascadeOrigin::kImportantUser : StyleCascadeOrigin::kUser; - } else { - style_origin = property.IsImportant() ? - StyleCascadeOrigin::kImportantAuthor : StyleCascadeOrigin::kAuthor; + + StyleCascadeOrigin style_origin = base_origin; + if (property.IsImportant()) { + // Only the non-animation origins have important variants. + if (base_origin == StyleCascadeOrigin::kUserAgent || base_origin == StyleCascadeOrigin::kUser || + base_origin == StyleCascadeOrigin::kAuthor || base_origin == StyleCascadeOrigin::kAuthorPresentationalHint) { + style_origin = ToImportantOrigin(base_origin); + } } - - // Build cascade priority using layer order and a stable position - // Encode position as (block_index << 16) | declaration_index like Blink + uint32_t encoded_position = (static_cast(block_index) << 16) | static_cast(declaration_index); - CascadePriority priority( - style_origin, - entry.is_inline_style, // is_inline_style - static_cast(entry.layer_level), // layer_order - encoded_position); // position - - // Collect per property; we will sort by ForLayerComparison before adding - // to CascadeMap to guarantee non-decreasing order. + CascadePriority priority(style_origin, entry.is_inline_style, static_cast(entry.layer_level), + encoded_position); + if (property.Id() == CSSPropertyID::kVariable) { - // Custom property const auto& metadata = property.PropertyMetadata(); if (!metadata.custom_name_.IsNull()) { - custom_priorities[metadata.custom_name_].emplace_back(priority); + map_.Add(metadata.custom_name_, priority); } - } else { - // Regular property - resolve surrogates first - const CSSProperty& css_property = CSSProperty::Get(property.Id()); - const CSSProperty& resolved_property = ResolveSurrogate(css_property); - native_priorities[static_cast(resolved_property.PropertyID())].emplace_back(priority); + continue; } - } - ++block_index; - } - - needs_match_result_analyze_ = false; - // Flush native properties in non-decreasing ForLayerComparison order. - for (size_t pid = 0; pid < native_priorities.size(); ++pid) { - auto& vec = native_priorities[pid]; - if (vec.empty()) continue; - std::stable_sort(vec.begin(), vec.end(), [](const CascadePriority& a, const CascadePriority& b) { - uint64_t fa = a.ForLayerComparison(); - uint64_t fb = b.ForLayerComparison(); - if (fa != fb) return fa < fb; - // Tie-break: prefer non-inline before inline to keep consistency - if (a.IsInlineStyle() != b.IsInlineStyle()) return !a.IsInlineStyle(); - // Final tie-breaker: position - return a.GetPosition() < b.GetPosition(); - }); - CSSPropertyID id = static_cast(pid); - for (const auto& pr : vec) { - map_.Add(id, pr); + const CSSProperty& css_property = CSSProperty::Get(property.Id()); + const CSSProperty& resolved_property = ResolveSurrogate(css_property); + map_.Add(resolved_property.PropertyID(), priority); } - } - // Flush custom properties in non-decreasing ForLayerComparison order. - for (auto& entry : custom_priorities) { - auto& vec = entry.second; - std::stable_sort(vec.begin(), vec.end(), [](const CascadePriority& a, const CascadePriority& b) { - uint64_t fa = a.ForLayerComparison(); - uint64_t fb = b.ForLayerComparison(); - if (fa != fb) return fa < fb; - if (a.IsInlineStyle() != b.IsInlineStyle()) return !a.IsInlineStyle(); - return a.GetPosition() < b.GetPosition(); - }); - for (const auto& pr : vec) { - map_.Add(entry.first, pr); - } + ++block_index; } + + needs_match_result_analyze_ = false; } void StyleCascade::ApplyCascadeAffecting(CascadeResolver& resolver) { diff --git a/bridge/core/css/resolver/style_resolver.cc b/bridge/core/css/resolver/style_resolver.cc index ccfec32004..f5d99fc62e 100644 --- a/bridge/core/css/resolver/style_resolver.cc +++ b/bridge/core/css/resolver/style_resolver.cc @@ -34,10 +34,10 @@ #include "style_resolver.h" #include +#include #include #include "foundation/logging.h" -#include "core/css/css_default_style_sheets.h" #include "core/css/css_identifier_value.h" #include "core/css/css_property_value_set.h" #include "core/css/inline_css_style_declaration.h" @@ -290,10 +290,7 @@ void StyleResolver::MatchAllRules( bool include_smil_properties) { Element& element = state.GetElement(); - - // Match UA rules - MatchUARules(collector); - + // Match user rules MatchUserRules(collector); @@ -313,44 +310,6 @@ void StyleResolver::MatchAllRules( } } -void StyleResolver::MatchUARules(ElementRuleCollector& collector) { - // Initialize UA stylesheets if not already done - CSSDefaultStyleSheets::Init(); - - // 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); - - // Create match request and collect matching rules - MatchRequest request(rule_set, CascadeOrigin::kUserAgent); - collector.CollectMatchingRules(request); - } - - // Apply quirks mode stylesheet if in quirks mode - // TODO: Implement quirks mode detection in Document - // For now, we'll skip quirks mode styles - /* - if (document_->InQuirksMode()) { - auto quirks_style = CSSDefaultStyleSheets::QuirksStyle(); - if (quirks_style && quirks_style->RuleCount() > 0) { - auto rule_set = std::make_shared(); - MediaQueryEvaluator evaluator("screen"); - rule_set->AddRulesFromSheet(quirks_style, evaluator, kRuleHasNoSpecialState); - MatchRequest request(rule_set, CascadeOrigin::kUserAgent); - collector.CollectMatchingRules(request); - } - } - */ - - // TODO: Add SVG and MathML stylesheets when elements support them -} - void StyleResolver::MatchUserRules(ElementRuleCollector& collector) { // TODO: Implement user rules matching // This would match user-defined stylesheets @@ -367,78 +326,40 @@ 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