Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/validation/css-effective-property-cascade-20260917.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Effective-property cascade validation (2026-09-17)

This stage of issue #235 moves rollback and overlapping inline declarations from
authored property names to the effective longhands they control. It covers the
high-frequency box shorthands, `all`, and CSSOM declaration order without adding
expansion work to the ordinary no-rollback stylesheet cascade.

## Browser contract

`tests/WebPlatformSubset/contracts/css-effective-property-cascade.html` is a
product-neutral 12-subtest contract. Chrome 153.0.8010.48 passes 12/12. Before
the implementation, the native engine passed 1/8 of the original contract;
margin, padding, inset, `all`, and inline ordering checks failed.

The final native engine passes 12/12 with both parser configurations:

- current CSSParser build: 12/12
- legacy parser build: 12/12
- unchanged cascade winner matrix: 17/17

The contract checks effective-longhand rollback for margin, padding, inset,
border width/color, gap, and overflow; `all: revert-layer`; important
`all: unset`; font/line-height winner identity; and inline CSSOM ordering,
removal, serialization, and recascade behavior.

## Scaling and compatibility checks

The native cascade-layer scaling regression retains exact work invariance as
unrelated DOM size grows:

| affected | unrelated | selector candidates | fallback visits | rule checks | applications | cascade candidates |
| ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| 8 | 32 | 16 | 0 | 32 | 18 | 32 |
| 8 | 1024 | 16 | 0 | 32 | 18 | 32 |
| 128 | 32 | 256 | 0 | 512 | 258 | 512 |
| 128 | 1024 | 256 | 0 | 512 | 258 | 512 |

Adjacent native contracts also pass: `css-all-unset-primitives` (2/2),
`cssom-inline-declaration-validity` (4/4), `cssom-padding-assignment` (5/5),
and `cssom-border-assignment` (5/5). The shared NativeWeb CSS service, shared
styles, and compiler CTests pass 3/3.

## Performance boundary

The common stylesheet cascade path remains a direct declaration replay. Only a
sheet containing `revert` or `revert-layer` constructs effective-longhand
winners and tokenizes recognized shorthands. Inline declarations use authored
order only when CSSOM mutation, `all`, or overlapping shorthand masks require a
full replay.

This is a bounded stage, not a claim of complete CSS Cascade support. Complex
shorthands such as `background`, `flex`, `transition`, and grid shorthands still
need a general expansion model. `all: initial` and `all: inherit`, UA/user
origins, broader inherited-property coverage, and complete CSSOM priority
projection remain follow-up work in issue #235.

Machine-readable results are summarized in
`docs/validation/evidence/css-effective-property-cascade-20260917.json`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"date": "2026-09-17",
"issue": 235,
"contract": "tests/WebPlatformSubset/contracts/css-effective-property-cascade.html",
"chrome": {
"version": "153.0.8010.48",
"documentsPassed": 1,
"documentsTotal": 1,
"subtestsPassed": 12,
"subtestsTotal": 12
},
"nativeBaseline": {
"subtestsPassed": 1,
"subtestsTotal": 8
},
"nativeCurrentParser": {
"subtestsPassed": 12,
"subtestsTotal": 12
},
"nativeLegacyParser": {
"subtestsPassed": 12,
"subtestsTotal": 12
},
"unchangedCascadeWinnerMatrix": {
"subtestsPassed": 17,
"subtestsTotal": 17
},
"nativeWebCtest": {
"testsPassed": 3,
"testsTotal": 3
},
"scaling": [
{"affected": 8, "unrelated": 32, "selectorCandidates": 16, "fallbackVisits": 0, "ruleChecks": 32, "cascadeApplications": 18, "cascadeCandidates": 32},
{"affected": 8, "unrelated": 1024, "selectorCandidates": 16, "fallbackVisits": 0, "ruleChecks": 32, "cascadeApplications": 18, "cascadeCandidates": 32},
{"affected": 128, "unrelated": 32, "selectorCandidates": 256, "fallbackVisits": 0, "ruleChecks": 512, "cascadeApplications": 258, "cascadeCandidates": 512},
{"affected": 128, "unrelated": 1024, "selectorCandidates": 256, "fallbackVisits": 0, "ruleChecks": 512, "cascadeApplications": 258, "cascadeCandidates": 512}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ void apply_resolved_declaration(native_document& document,dom_node& node,
& property) != 0U;
};
if (name == "all") {
if (value != "unset" || declaration.important) {
if (!cascade_keyword_is(value, "unset")) {
decision.classification = "unsupported";
decision.semantic_slice =
"non-important unset across modeled properties, excluding custom properties";
"unset across modeled properties, excluding custom properties";
return;
}

css::apply_all_unset(node);
css::apply_all_unset(node, declaration.important, inline_origin);
decision.classification = "partially-supported";
decision.semantic_slice =
"non-important unset across modeled properties, excluding custom properties";
"unset across modeled properties, excluding custom properties";
return;
}
if (name == "color" && !is_inline(inline_color)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,36 @@ void apply_matched_declarations(dom_node& node,const cascaded_rule_order& order,
// Temporarily removing the inline guard lets a declaration update its own
// computed value while the important-origin mask still prevents a
// normal inline declaration from overriding an author !important rule.
for (const auto& [name, value] : node.authored_style().declarations) {
uint64_t inline_groups = 0U;
bool ordered_inline_replay =
node.authored_style().requires_full_replay;
node.authored_style().for_each_declaration(
[&](const std::string& name, const std::string&) {
if (name.starts_with("--")
|| node.authored_style().important_declarations.contains(name)) {
return;
}
const auto group = css::property_mask(name);
ordered_inline_replay = ordered_inline_replay || name == "all"
|| (group != 0U && (inline_groups & group) != 0U);
inline_groups |= group;
});
node.authored_style().for_each_declaration(
[&](const std::string& name, const std::string& value) {
const auto inline_important =
node.authored_style().important_declarations.contains(name);
const auto inherited_dimension = (name == "width" || name == "height")
&& trim_css_view(value) == "inherit";
if (name.starts_with("--") || inline_important
|| (value.find("var(") == std::string::npos && !inherited_dimension)) continue;
|| (!ordered_inline_replay
&& value.find("var(") == std::string::npos
&& !inherited_dimension)) return;
const auto property_mask = css::property_mask(name);
const auto retained_inline_mask = node.style.inline_property_mask;
node.style.inline_property_mask &= ~property_mask;
apply({name, value, false}, true);
node.style.inline_property_mask = retained_inline_mask;
}
});
// Inline transition declarations are stored separately from the hot
// style object. Recascade clears cold animation state before applying
// stylesheet rules, so restore the inline origin afterward while
Expand Down Expand Up @@ -72,17 +89,18 @@ void apply_matched_declarations(dom_node& node,const cascaded_rule_order& order,
// rule list is applied. Replaying only the authored important tier
// restores the CSS cascade order without reapplying every ordinary
// inline declaration or adding a mutation-lived winner cache.
for (const auto& [name, value] : node.authored_style().declarations) {
node.authored_style().for_each_declaration(
[&](const std::string& name, const std::string& value) {
if (name.starts_with("--")
|| !node.authored_style().important_declarations.contains(name)) {
continue;
return;
}
const auto property_mask = css::property_mask(name);
const auto retained_inline_mask = node.style.inline_property_mask;
node.style.inline_property_mask &= ~property_mask;
apply({name, value, true}, true);
node.style.inline_property_mask = retained_inline_mask;
}
});
}

template<typename Apply>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,14 @@ inline void recompute_cascaded_line_height(dom_node& node, const cascaded_rule_o
if (declaration.name == "font" || declaration.name == "line-height")
return "line-height";
return declaration.name;
});
});
for (const auto important : {false, true}) {
for (const auto& [name, value] : node.authored_style().declarations) {
node.authored_style().for_each_declaration(
[&](const std::string& name, const std::string& value) {
if (node.authored_style().important_declarations.contains(name) != important)
continue;
return;
consider({name, value, important});
}
});
}
if (winning_value.has_value()) {
const auto font_size = node.style.font_size >= 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ inline void reset_cascaded_style(dom_node& node,
// Rebuild from authored inline values, not a previous computed
// value that may have been replaced by an important stylesheet.
for (const auto important : {false, true}) {
for (const auto& [name, value] : node.authored_style().declarations) {
node.authored_style().for_each_declaration(
[&](const std::string& name, const std::string& value) {
if (margin_sides(name) != 0U
&& node.authored_style().important_declarations.contains(name) == important) {
apply_margin_declaration(node.style, name, resolve_value(node,value,variables));
}
}
});
}
}
if ((node.style.inline_property_mask & inline_border) == 0U) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ bool apply_native_cascade(native_document& document,dom_node& node,
auto& authored=node.mutable_authored_style();
if(!declaration.important && authored.important_declarations.contains(declaration.name)) continue;
authored.declarations[declaration.name]=declaration.value;
authored.record_declaration_order(declaration.name);
if(declaration.important) authored.important_declarations.insert(declaration.name);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include "webscene_css_box_values.h"
#include <limits>

namespace webscene_native::css {
// Existing native style storage groups used for inline/important precedence.
Expand Down Expand Up @@ -74,6 +75,7 @@ enum inline_style_property : uint64_t {
};
inline uint64_t property_mask(std::string_view name)
{
if (name == "all") return std::numeric_limits<uint64_t>::max();
if (name == "stroke-width") return inline_svg_stroke_width;
if (name == "scrollbar-width") return inline_scrollbar_width;
if (name == "scrollbar-color") return inline_scrollbar_color;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,29 @@
namespace webscene_native::css {
// Existing modeled behavior for a non-important author all:unset declaration.
// Callers handle unsupported reset keywords and cascade ordering before entry.
inline void apply_all_unset(dom_node& node)
inline void apply_all_unset(
dom_node& node,
bool important = false,
bool inline_origin = false)
{
const auto is_inline=[&](uint64_t property) {
return ((node.style.inline_property_mask|node.style.important_property_mask)&property)!=0;
if (inline_origin) {
// An inline `all` replaces earlier declarations in its own block.
// Only a higher author-important rule survives inline-normal all.
return !important
&& (node.style.important_property_mask & property) != 0U;
}
if (!important) {
return ((node.style.inline_property_mask
| node.style.important_property_mask) & property) != 0U;
}
// Author-important all still yields to inline-important declarations.
return std::any_of(
node.authored_style().important_declarations.begin(),
node.authored_style().important_declarations.end(),
[&](const std::string& name) {
return (property_mask(name) & property) != 0U;
});
};
// `all: unset` is a common component-control reset. Start from the
// modeled initial/inherited sentinels, then restore declarations
Expand All @@ -20,17 +39,32 @@ inline void apply_all_unset(dom_node& node)
// element's declarations.
auto previous = node.style;
auto reset = node_style{};
reset.display = native_default_display_for_node(node);
reset.inline_property_mask = previous.inline_property_mask;
reset.important_property_mask = previous.important_property_mask;
reset.important_margin_sides = previous.important_margin_sides;
// `unset` selects the CSS initial value for non-inherited properties. The
// element-specific block/table defaults belong to the lower UA origin and
// must not replace author `all: unset` (whose initial display is inline).
reset.display = display_mode::inline_flow;
reset.inline_property_mask = inline_origin
? std::numeric_limits<uint64_t>::max()
: previous.inline_property_mask;
reset.important_property_mask = important
? std::numeric_limits<uint64_t>::max()
: previous.important_property_mask;
reset.important_margin_sides = important
? 15U
: previous.important_margin_sides;
reset.move_custom_properties_from(previous);
reset.move_pseudo_elements_from(previous);

const auto has_inline = [&](std::initializer_list<std::string_view> names) {
return std::any_of(names.begin(), names.end(), [&](std::string_view candidate) {
return node.authored_style().declarations.contains(
std::string(candidate));
if (!node.authored_style().declarations.contains(
std::string(candidate))) {
return false;
}
if (inline_origin) return false;
return !important
|| node.authored_style().important_declarations.contains(
std::string(candidate));
});
};
if (is_inline(inline_width)) reset.width = previous.width;
Expand Down
Loading
Loading