diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b509ab2b..e737c433 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -85,6 +85,10 @@ jobs: run: | python -m unittest scripts/test_css_capability_matrix.py -v python scripts/css_capability_matrix.py --check + - name: CSS effective-property metadata contracts + run: | + python -m unittest scripts/test_css_property_metadata.py -v + python tools/generate_css_property_metadata.py --check - name: Code OSS Web API ledger contracts run: | python -m unittest scripts/test_code_oss_web_api_ledger.py -v diff --git a/docs/validation/css-property-metadata-20260917.md b/docs/validation/css-property-metadata-20260917.md new file mode 100644 index 00000000..f611ad61 --- /dev/null +++ b/docs/validation/css-property-metadata-20260917.md @@ -0,0 +1,72 @@ +# Generated effective-property metadata validation (2026-09-17) + +This stage of #235 moves the effective-property knowledge introduced by #305 +out of cascade-specific conditionals and into a generated registry. The source +catalog is `experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json`; +`tools/generate_css_property_metadata.py` validates it and emits the sorted, +constexpr native table. + +## Structural result + +The registry currently contains 61 physical, logical, and shorthand names for +margin, padding, inset, border width/color, gap, and overflow. Each row owns its +modeled style mask and zero, one, two, or four effective longhands. Cascade +rollback and property-mask lookup now consume that shared data instead of +maintaining separate property-name lists. + +Six logical shorthands whose prior ordinary application path lost their second +component are marked for application-time expansion: `inset-block`, +`inset-inline`, `border-block-width`, `border-inline-width`, +`border-block-color`, and `border-inline-color`. Other common shorthands retain +their existing direct application path; high-frequency direct properties such +as width, display, background, color, font, and transform still return before a +metadata lookup. Expansion remains allocation-free until one of those six +logical shorthands or the rare rollback path is actually used. + +The generator rejects duplicate names, unknown effective targets, invalid mask +symbols, invalid expansion arity, and stale checked-in output. CI runs both its +unit tests and `--check` mode. + +## Browser and native behavior + +The browser-referenced effective-property contract was extended from 12 to 15 +subtests. The three additions cover logical inset rollback, logical border +width/color rollback, and two-value inline CSSOM application followed by a +recascade. + +- Chrome 153.0.8010.48: 15/15 +- native current CSSParser: 15/15 +- native legacy parser: 15/15 +- unchanged cascade winner matrix: 17/17 +- adjacent all/margin/CSSOM contracts: 21/21 +- NativeWeb CSS service/shared styles/compiler: 3/3 CTest + +The cascade-layer mutation gate retains exact work invariance when unrelated +nodes grow from 32 to 1,024: + +| 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 | + +## Lookup A/B + +`webscene_css_property_metadata_benchmark` performs 24,000,000 lookups per run +over a fixed mix of 15 direct hot properties and nine effective-property +entries. For the retained A/B, the same benchmark source and AppleClang 21 +`-O3` command were compiled once against merged #305 and once against this +branch. Nine alternating runs produced: + +- merged #305 median: 502.446 ms (495.039–521.802 ms) +- generated registry median: 424.271 ms (418.068–440.586 ms) +- median change: -15.56% + +This microbenchmark establishes that centralization did not trade maintainable +metadata for slower property dispatch. It is not an end-to-end Spotify resize +claim; physical resize and media-threshold acceptance remains later work in +#235. + +Machine-readable evidence is in +`docs/validation/evidence/css-property-metadata-20260917.json`. diff --git a/docs/validation/evidence/css-property-metadata-20260917.json b/docs/validation/evidence/css-property-metadata-20260917.json new file mode 100644 index 00000000..bb80178a --- /dev/null +++ b/docs/validation/evidence/css-property-metadata-20260917.json @@ -0,0 +1,30 @@ +{ + "date": "2026-09-17", + "issue": 235, + "catalogEntries": 61, + "applicationExpandedShorthands": 6, + "contracts": { + "chrome": {"passed": 15, "total": 15, "version": "153.0.8010.48"}, + "nativeCurrentParser": {"passed": 15, "total": 15}, + "nativeLegacyParser": {"passed": 15, "total": 15}, + "cascadeWinnerMatrix": {"passed": 17, "total": 17}, + "adjacentCssom": {"passed": 21, "total": 21}, + "nativeWebCtest": {"passed": 3, "total": 3}, + "generatorUnitTests": {"passed": 3, "total": 3} + }, + "lookupBenchmark": { + "compiler": "AppleClang 21 -O3", + "lookupsPerRun": 24000000, + "baselineMilliseconds": [507.901, 502.446, 495.039, 521.802, 495.865, 495.389, 501.778, 505.427, 506.480], + "candidateMilliseconds": [426.629, 423.418, 424.271, 434.849, 418.370, 430.056, 421.227, 440.586, 418.068], + "baselineMedianMilliseconds": 502.446, + "candidateMedianMilliseconds": 424.271, + "medianChangePercent": -15.56 + }, + "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} + ] +} diff --git a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt index d5521eca..32a5b1ee 100644 --- a/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt +++ b/experiments/WebScene.NativeEngine.Probe/CMakeLists.txt @@ -243,6 +243,8 @@ option(WEBSCENE_NATIVE_ENGINE_BUILD_HTML_PARSER_BENCHMARK "Build the V8-free html5ever parser microbenchmark" OFF) option(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PARSER_BENCHMARK "Build the V8-free selected CSS syntax parser microbenchmark" OFF) +option(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PROPERTY_METADATA_BENCHMARK + "Build the V8-free CSS property metadata lookup benchmark" OFF) option(WEBSCENE_NATIVE_ENGINE_BUILD_DOM_BINDING_BENCHMARK "Build the native V8 DOM binding lifecycle benchmark" OFF) option(WEBSCENE_NATIVE_ENGINE_FRAGMENT_ATTACH_CONTROL @@ -945,6 +947,13 @@ else() message(STATUS "WebScene native engine: V8 intentionally disabled") endif() +if(WEBSCENE_NATIVE_ENGINE_BUILD_CSS_PROPERTY_METADATA_BENCHMARK) + add_executable(webscene_css_property_metadata_benchmark + benchmarks/css_property_metadata_benchmark.cpp) + target_compile_features(webscene_css_property_metadata_benchmark PRIVATE cxx_std_20) + target_include_directories(webscene_css_property_metadata_benchmark PRIVATE native) +endif() + if(WEBSCENE_NATIVE_ENGINE_BUILD_LAYOUT_SCRATCH_BENCHMARK) foreach(layout_benchmark_target IN ITEMS webscene_layout_scratch_control_benchmark diff --git a/experiments/WebScene.NativeEngine.Probe/benchmarks/css_property_metadata_benchmark.cpp b/experiments/WebScene.NativeEngine.Probe/benchmarks/css_property_metadata_benchmark.cpp new file mode 100644 index 00000000..23e21555 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/benchmarks/css_property_metadata_benchmark.cpp @@ -0,0 +1,34 @@ +#include "webscene_css_property_mask.h" + +#include +#include +#include +#include +#include + +int main() +{ + // Keep the mix stable: fifteen high-frequency direct fields and nine + // effective-property registry entries. This measures dispatch overhead, + // not parser or layout work. + constexpr std::array names{ + "width", "height", "display", "position", "color", "background", + "background-color", "font-size", "font-family", "font-weight", + "line-height", "text-align", "visibility", "opacity", "transform", + "margin", "margin-left", "padding", "padding-inline-start", "gap", + "border-width", "border-top-color", "inset-inline-start", "overflow"}; + constexpr size_t rounds = 1000000U; + uint64_t checksum = 0U; + const auto started = std::chrono::steady_clock::now(); + for (size_t round = 0; round < rounds; ++round) { + for (const auto name : names) { + checksum ^= webscene_native::css::property_mask(name); + checksum = (checksum << 1U) | (checksum >> 63U); + } + } + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - started).count(); + std::cout << "lookups=" << rounds * names.size() + << " elapsed_ms=" << elapsed + << " checksum=" << checksum << '\n'; +} diff --git a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json new file mode 100644 index 00000000..181824e5 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -0,0 +1,66 @@ +{ + "version": 1, + "properties": [ + {"name": "border-block-color", "mask": ["inline_border"], "expansion": ["border-top-color", "border-bottom-color"], "applyExpansion": true}, + {"name": "border-block-end-color", "mask": ["inline_border"], "alias": "border-bottom-color"}, + {"name": "border-block-end-width", "mask": ["inline_border"], "alias": "border-bottom-width"}, + {"name": "border-block-start-color", "mask": ["inline_border"], "alias": "border-top-color"}, + {"name": "border-block-start-width", "mask": ["inline_border"], "alias": "border-top-width"}, + {"name": "border-block-width", "mask": ["inline_border"], "expansion": ["border-top-width", "border-bottom-width"], "applyExpansion": true}, + {"name": "border-bottom-color", "mask": ["inline_border"]}, + {"name": "border-bottom-width", "mask": ["inline_border"]}, + {"name": "border-color", "mask": ["inline_border"], "expansion": ["border-top-color", "border-right-color", "border-bottom-color", "border-left-color"]}, + {"name": "border-inline-color", "mask": ["inline_border"], "expansion": ["border-left-color", "border-right-color"], "applyExpansion": true}, + {"name": "border-inline-end-color", "mask": ["inline_border"], "alias": "border-right-color"}, + {"name": "border-inline-end-width", "mask": ["inline_border"], "alias": "border-right-width"}, + {"name": "border-inline-start-color", "mask": ["inline_border"], "alias": "border-left-color"}, + {"name": "border-inline-start-width", "mask": ["inline_border"], "alias": "border-left-width"}, + {"name": "border-inline-width", "mask": ["inline_border"], "expansion": ["border-left-width", "border-right-width"], "applyExpansion": true}, + {"name": "border-left-color", "mask": ["inline_border"]}, + {"name": "border-left-width", "mask": ["inline_border"]}, + {"name": "border-right-color", "mask": ["inline_border"]}, + {"name": "border-right-width", "mask": ["inline_border"]}, + {"name": "border-top-color", "mask": ["inline_border"]}, + {"name": "border-top-width", "mask": ["inline_border"]}, + {"name": "border-width", "mask": ["inline_border"], "expansion": ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"]}, + {"name": "bottom", "mask": ["inline_bottom"]}, + {"name": "column-gap", "mask": ["inline_gap"]}, + {"name": "gap", "mask": ["inline_gap"], "expansion": ["row-gap", "column-gap"]}, + {"name": "inset", "mask": ["inline_top", "inline_right", "inline_bottom", "inline_left"], "expansion": ["top", "right", "bottom", "left"]}, + {"name": "inset-block", "mask": ["inline_top", "inline_bottom"], "expansion": ["top", "bottom"], "applyExpansion": true}, + {"name": "inset-block-end", "mask": ["inline_bottom"], "alias": "bottom"}, + {"name": "inset-block-start", "mask": ["inline_top"], "alias": "top"}, + {"name": "inset-inline", "mask": ["inline_left", "inline_right"], "expansion": ["left", "right"], "applyExpansion": true}, + {"name": "inset-inline-end", "mask": ["inline_right"], "alias": "right"}, + {"name": "inset-inline-start", "mask": ["inline_left"], "alias": "left"}, + {"name": "left", "mask": ["inline_left"]}, + {"name": "margin", "mask": ["inline_margin"], "expansion": ["margin-top", "margin-right", "margin-bottom", "margin-left"]}, + {"name": "margin-block", "mask": ["inline_margin"], "expansion": ["margin-top", "margin-bottom"]}, + {"name": "margin-block-end", "mask": ["inline_margin"], "alias": "margin-bottom"}, + {"name": "margin-block-start", "mask": ["inline_margin"], "alias": "margin-top"}, + {"name": "margin-bottom", "mask": ["inline_margin"]}, + {"name": "margin-inline", "mask": ["inline_margin"], "expansion": ["margin-left", "margin-right"]}, + {"name": "margin-inline-end", "mask": ["inline_margin"], "alias": "margin-right"}, + {"name": "margin-inline-start", "mask": ["inline_margin"], "alias": "margin-left"}, + {"name": "margin-left", "mask": ["inline_margin"]}, + {"name": "margin-right", "mask": ["inline_margin"]}, + {"name": "margin-top", "mask": ["inline_margin"]}, + {"name": "overflow", "mask": ["inline_overflow"], "expansion": ["overflow-x", "overflow-y"]}, + {"name": "overflow-x", "mask": ["inline_overflow"]}, + {"name": "overflow-y", "mask": ["inline_overflow"]}, + {"name": "padding", "mask": ["inline_padding"], "expansion": ["padding-top", "padding-right", "padding-bottom", "padding-left"]}, + {"name": "padding-block", "mask": ["inline_padding"], "expansion": ["padding-top", "padding-bottom"]}, + {"name": "padding-block-end", "mask": ["inline_padding"], "alias": "padding-bottom"}, + {"name": "padding-block-start", "mask": ["inline_padding"], "alias": "padding-top"}, + {"name": "padding-bottom", "mask": ["inline_padding"]}, + {"name": "padding-inline", "mask": ["inline_padding"], "expansion": ["padding-left", "padding-right"]}, + {"name": "padding-inline-end", "mask": ["inline_padding"], "alias": "padding-right"}, + {"name": "padding-inline-start", "mask": ["inline_padding"], "alias": "padding-left"}, + {"name": "padding-left", "mask": ["inline_padding"]}, + {"name": "padding-right", "mask": ["inline_padding"]}, + {"name": "padding-top", "mask": ["inline_padding"]}, + {"name": "right", "mask": ["inline_right"]}, + {"name": "row-gap", "mask": ["inline_gap"]}, + {"name": "top", "mask": ["inline_top"]} + ] +} diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_metadata.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_metadata.inc new file mode 100644 index 00000000..ff2322e4 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_metadata.inc @@ -0,0 +1,64 @@ +// Generated by tools/generate_css_property_metadata.py. Do not edit. +inline constexpr std::array effective_property_metadata_catalog{ + effective_property_metadata{"border-block-color", inline_border, {"border-top-color", "border-bottom-color", "", ""}, 2U, true}, + effective_property_metadata{"border-block-end-color", inline_border, {"border-bottom-color", "", "", ""}, 1U, false}, + effective_property_metadata{"border-block-end-width", inline_border, {"border-bottom-width", "", "", ""}, 1U, false}, + effective_property_metadata{"border-block-start-color", inline_border, {"border-top-color", "", "", ""}, 1U, false}, + effective_property_metadata{"border-block-start-width", inline_border, {"border-top-width", "", "", ""}, 1U, false}, + effective_property_metadata{"border-block-width", inline_border, {"border-top-width", "border-bottom-width", "", ""}, 2U, true}, + effective_property_metadata{"border-bottom-color", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-bottom-width", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-color", inline_border, {"border-top-color", "border-right-color", "border-bottom-color", "border-left-color"}, 4U, false}, + effective_property_metadata{"border-inline-color", inline_border, {"border-left-color", "border-right-color", "", ""}, 2U, true}, + effective_property_metadata{"border-inline-end-color", inline_border, {"border-right-color", "", "", ""}, 1U, false}, + effective_property_metadata{"border-inline-end-width", inline_border, {"border-right-width", "", "", ""}, 1U, false}, + effective_property_metadata{"border-inline-start-color", inline_border, {"border-left-color", "", "", ""}, 1U, false}, + effective_property_metadata{"border-inline-start-width", inline_border, {"border-left-width", "", "", ""}, 1U, false}, + effective_property_metadata{"border-inline-width", inline_border, {"border-left-width", "border-right-width", "", ""}, 2U, true}, + effective_property_metadata{"border-left-color", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-left-width", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-right-color", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-right-width", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-top-color", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-top-width", inline_border, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"border-width", inline_border, {"border-top-width", "border-right-width", "border-bottom-width", "border-left-width"}, 4U, false}, + effective_property_metadata{"bottom", inline_bottom, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"column-gap", inline_gap, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"gap", inline_gap, {"row-gap", "column-gap", "", ""}, 2U, false}, + effective_property_metadata{"inset", inline_top | inline_right | inline_bottom | inline_left, {"top", "right", "bottom", "left"}, 4U, false}, + effective_property_metadata{"inset-block", inline_top | inline_bottom, {"top", "bottom", "", ""}, 2U, true}, + effective_property_metadata{"inset-block-end", inline_bottom, {"bottom", "", "", ""}, 1U, false}, + effective_property_metadata{"inset-block-start", inline_top, {"top", "", "", ""}, 1U, false}, + effective_property_metadata{"inset-inline", inline_left | inline_right, {"left", "right", "", ""}, 2U, true}, + effective_property_metadata{"inset-inline-end", inline_right, {"right", "", "", ""}, 1U, false}, + effective_property_metadata{"inset-inline-start", inline_left, {"left", "", "", ""}, 1U, false}, + effective_property_metadata{"left", inline_left, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"margin", inline_margin, {"margin-top", "margin-right", "margin-bottom", "margin-left"}, 4U, false}, + effective_property_metadata{"margin-block", inline_margin, {"margin-top", "margin-bottom", "", ""}, 2U, false}, + effective_property_metadata{"margin-block-end", inline_margin, {"margin-bottom", "", "", ""}, 1U, false}, + effective_property_metadata{"margin-block-start", inline_margin, {"margin-top", "", "", ""}, 1U, false}, + effective_property_metadata{"margin-bottom", inline_margin, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"margin-inline", inline_margin, {"margin-left", "margin-right", "", ""}, 2U, false}, + effective_property_metadata{"margin-inline-end", inline_margin, {"margin-right", "", "", ""}, 1U, false}, + effective_property_metadata{"margin-inline-start", inline_margin, {"margin-left", "", "", ""}, 1U, false}, + effective_property_metadata{"margin-left", inline_margin, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"margin-right", inline_margin, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"margin-top", inline_margin, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"overflow", inline_overflow, {"overflow-x", "overflow-y", "", ""}, 2U, false}, + effective_property_metadata{"overflow-x", inline_overflow, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"overflow-y", inline_overflow, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"padding", inline_padding, {"padding-top", "padding-right", "padding-bottom", "padding-left"}, 4U, false}, + effective_property_metadata{"padding-block", inline_padding, {"padding-top", "padding-bottom", "", ""}, 2U, false}, + effective_property_metadata{"padding-block-end", inline_padding, {"padding-bottom", "", "", ""}, 1U, false}, + effective_property_metadata{"padding-block-start", inline_padding, {"padding-top", "", "", ""}, 1U, false}, + effective_property_metadata{"padding-bottom", inline_padding, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"padding-inline", inline_padding, {"padding-left", "padding-right", "", ""}, 2U, false}, + effective_property_metadata{"padding-inline-end", inline_padding, {"padding-right", "", "", ""}, 1U, false}, + effective_property_metadata{"padding-inline-start", inline_padding, {"padding-left", "", "", ""}, 1U, false}, + effective_property_metadata{"padding-left", inline_padding, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"padding-right", inline_padding, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"padding-top", inline_padding, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"right", inline_right, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"row-gap", inline_gap, {"", "", "", ""}, 0U, false}, + effective_property_metadata{"top", inline_top, {"", "", "", ""}, 0U, false}, +}; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h index 169d0073..8dfac95e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h @@ -18,6 +18,37 @@ void apply_resolved_declaration(native_document& document,dom_node& node, bool inline_origin,Decision& decision,LoadSvg&& load_svg) { const auto& name=declaration.name; + const auto may_require_application_expansion = + name.starts_with("border-") || name.starts_with("inset-"); + const auto* effective_metadata = may_require_application_expansion + ? find_effective_property_metadata(name) : nullptr; + if (effective_metadata != nullptr && effective_metadata->apply_expansion) { + std::array, 4> components{}; + size_t component_count = 0U; + const auto expanded = for_each_effective_property_component( + name, + value, + [&](std::string_view component_name, std::string_view component_value) { + components[component_count++] = {component_name, component_value}; + }); + if (expanded) { + for (size_t index = 0; index < component_count; ++index) { + const css_declaration component{ + std::string(components[index].first), + std::string(components[index].second), + declaration.important}; + apply_resolved_declaration( + document, + node, + component, + component.value, + inline_origin, + decision, + load_svg); + } + return; + } + } if(name=="stroke-width") { apply_text_value(node,name,value,decision,[&](uint64_t mask) { return !declaration.important && ((node.style.inline_property_mask|node.style.important_property_mask)&mask)!=0; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_property_mask.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_property_mask.h index 117d4864..49ea6905 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_property_mask.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_property_mask.h @@ -73,6 +73,91 @@ enum inline_style_property : uint64_t { inline_transition = inline_transition_property | inline_transition_duration | inline_transition_delay | inline_transition_timing }; + +struct effective_property_metadata final { + std::string_view name; + uint64_t mask; + std::array longhands; + uint8_t longhand_count; + bool apply_expansion; +}; + +#include "generated/webscene_css_property_metadata.inc" + +inline const effective_property_metadata* find_effective_property_metadata( + std::string_view name) +{ + size_t first = 0U; + size_t count = effective_property_metadata_catalog.size(); + while (count != 0U) { + const auto step = count / 2U; + const auto index = first + step; + if (effective_property_metadata_catalog[index].name < name) { + first = index + 1U; + count -= step + 1U; + } else { + count = step; + } + } + if (first == effective_property_metadata_catalog.size() + || effective_property_metadata_catalog[first].name != name) { + return nullptr; + } + return &effective_property_metadata_catalog[first]; +} + +template +bool for_each_effective_property_component( + std::string_view name, + std::string_view value, + Apply&& apply) +{ + const auto* metadata = find_effective_property_metadata(name); + if (metadata == nullptr || metadata->longhand_count == 0U) { + apply(name, value); + return false; + } + if (metadata->longhand_count == 1U) { + apply(metadata->longhands[0], value); + return true; + } + std::array values{}; + size_t value_count = 0U; + size_t start = std::string_view::npos; + int depth = 0; + for (size_t index = 0; index <= value.size(); ++index) { + const auto character = index < value.size() ? value[index] : ' '; + if (character == '(') ++depth; + else if (character == ')' && depth > 0) --depth; + if (std::isspace(static_cast(character)) && depth == 0) { + if (start == std::string_view::npos) continue; + if (value_count == values.size()) { + apply(name, value); + return false; + } + values[value_count++] = value.substr(start, index - start); + start = std::string_view::npos; + } else if (start == std::string_view::npos) { + start = index; + } + } + if (value_count == 0U || value_count > metadata->longhand_count) { + apply(name, value); + return false; + } + if (metadata->longhand_count == 2U) { + apply(metadata->longhands[0], values[0]); + apply(metadata->longhands[1], value_count > 1U ? values[1] : values[0]); + return true; + } + apply(metadata->longhands[0], values[0]); + apply(metadata->longhands[1], value_count > 1U ? values[1] : values[0]); + apply(metadata->longhands[2], value_count > 2U ? values[2] : values[0]); + apply(metadata->longhands[3], value_count > 3U ? values[3] + : value_count > 1U ? values[1] : values[0]); + return true; +} + inline uint64_t property_mask(std::string_view name) { if (name == "all") return std::numeric_limits::max(); @@ -85,13 +170,6 @@ inline uint64_t property_mask(std::string_view name) if (name == "min-height" || name == "min-block-size") return inline_min_height; if (name == "max-width" || name == "max-inline-size") return inline_max_width; if (name == "max-height" || name == "max-block-size") return inline_max_height; - if (name == "left" || name == "inset-inline-start") return inline_left; - if (name == "top" || name == "inset-block-start") return inline_top; - if (name == "right" || name == "inset-inline-end") return inline_right; - if (name == "bottom" || name == "inset-block-end") return inline_bottom; - if (name == "inset") { - return inline_left | inline_top | inline_right | inline_bottom; - } if (name == "display") return inline_display; if (name == "position") return inline_position; if (name == "contain") return inline_contain; @@ -133,7 +211,6 @@ inline uint64_t property_mask(std::string_view name) if (name == "background-image" || name == "backgroundImage") { return inline_background_image; } - if (name == "overflow" || name == "overflow-x" || name == "overflow-y") return inline_overflow; if (name == "visibility") return inline_visibility; if (name == "pointer-events") return inline_pointer_events; if (name == "color") return inline_color; @@ -156,17 +233,26 @@ inline uint64_t property_mask(std::string_view name) if (name == "word-spacing") return inline_word_spacing; if (name == "text-align") return inline_text_align; if (name == "white-space") return inline_white_space; - if (name == "padding" || name.starts_with("padding-")) return inline_padding; - const auto canonical_name = canonical_property_name(name); - if (canonical_name == "margin" || canonical_name.starts_with("margin-")) { - return inline_margin; + const auto direct_metadata = find_effective_property_metadata(name); + if (direct_metadata != nullptr) return direct_metadata->mask; + std::string canonical_storage; + auto canonical_name = name; + const auto needs_canonical_name = name == "grid-gap" + || name == "grid-row-gap" || name == "grid-column-gap" + || std::any_of(name.begin(), name.end(), [](unsigned char character) { + return std::isupper(character) != 0; + }); + if (needs_canonical_name) { + canonical_storage = canonical_property_name(name); + canonical_name = canonical_storage; + const auto canonical_metadata = + find_effective_property_metadata(canonical_name); + if (canonical_metadata != nullptr) return canonical_metadata->mask; } if (name == "align-items") return inline_align_items; if (name == "align-self") return inline_align_self; if (name == "align-content" || name == "alignContent") return inline_align_content; if (name == "justify-content") return inline_justify_content; - if (name == "gap" || name == "row-gap" || name == "column-gap" - || name == "rowGap" || name == "columnGap") return inline_gap; if (name == "border-spacing" || name == "borderSpacing" || name == "border-collapse" || name == "borderCollapse") { return inline_table_border_model; @@ -174,13 +260,9 @@ inline uint64_t property_mask(std::string_view name) if (name == "box-sizing") return inline_box_sizing; if (name == "box-shadow" || name == "boxShadow") return inline_box_shadow; if (canonical_name == "border" - || canonical_name == "border-width" || canonical_name == "border-style" - || canonical_name == "border-color" || (canonical_name.starts_with("border-") - && (canonical_name.ends_with("-width") - || canonical_name.ends_with("-style") - || canonical_name.ends_with("-color"))) + && canonical_name.ends_with("-style")) || canonical_name == "border-top" || canonical_name == "border-right" || canonical_name == "border-bottom" diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_rule_operations.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_rule_operations.h index 3049f20f..7b7871af 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_rule_operations.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_rule_operations.h @@ -108,173 +108,18 @@ struct cascade_layer_property_hash final { // zero-expansion declaration replay, while a sheet containing revert keywords // pays the small amount of parsing needed to distinguish shorthand components. template -void for_each_effective_box_declaration( +void for_each_effective_declaration( const css_declaration& declaration, Apply&& apply) { - const auto emit = [&](std::string_view name, std::string_view value) { - css_declaration effective{ - std::string(name), std::string(value), declaration.important}; - apply(effective, name); - }; - const auto& name = declaration.name; - if (name == "margin-inline-start") { - emit("margin-left", declaration.value); - return; - } - if (name == "margin-inline-end") { - emit("margin-right", declaration.value); - return; - } - if (name == "margin-block-start") { - emit("margin-top", declaration.value); - return; - } - if (name == "margin-block-end") { - emit("margin-bottom", declaration.value); - return; - } - if (name == "padding-inline-start") { - emit("padding-left", declaration.value); - return; - } - if (name == "padding-inline-end") { - emit("padding-right", declaration.value); - return; - } - if (name == "padding-block-start") { - emit("padding-top", declaration.value); - return; - } - if (name == "padding-block-end") { - emit("padding-bottom", declaration.value); - return; - } - if (name == "inset-inline-start") { - emit("left", declaration.value); - return; - } - if (name == "inset-inline-end") { - emit("right", declaration.value); - return; - } - if (name == "inset-block-start") { - emit("top", declaration.value); - return; - } - if (name == "inset-block-end") { - emit("bottom", declaration.value); - return; - } - if (name == "border-inline-start-width") { - emit("border-left-width", declaration.value); - return; - } - if (name == "border-inline-end-width") { - emit("border-right-width", declaration.value); - return; - } - if (name == "border-block-start-width") { - emit("border-top-width", declaration.value); - return; - } - if (name == "border-block-end-width") { - emit("border-bottom-width", declaration.value); - return; - } - if (name == "border-inline-start-color") { - emit("border-left-color", declaration.value); - return; - } - if (name == "border-inline-end-color") { - emit("border-right-color", declaration.value); - return; - } - if (name == "border-block-start-color") { - emit("border-top-color", declaration.value); - return; - } - if (name == "border-block-end-color") { - emit("border-bottom-color", declaration.value); - return; - } - const auto expands_four = name == "margin" || name == "padding" - || name == "inset" || name == "border-width" - || name == "border-color"; - const auto expands_two = name == "margin-block" || name == "margin-inline" - || name == "padding-block" || name == "padding-inline" - || name == "gap" || name == "overflow"; - if (!expands_four && !expands_two) { - apply(declaration, std::string_view(declaration.name)); - return; - } - const auto tokens = [&]() { - std::array result{}; - size_t count = 0; - size_t start = std::string_view::npos; - int depth = 0; - const auto value = std::string_view(declaration.value); - for (size_t index = 0; index <= value.size(); ++index) { - const auto character = index < value.size() ? value[index] : ' '; - if (character == '(') ++depth; - else if (character == ')' && depth > 0) --depth; - if (std::isspace(static_cast(character)) && depth == 0) { - if (start == std::string_view::npos) continue; - if (count == result.size()) return std::pair{result, size_t{0}}; - result[count++] = value.substr(start, index - start); - start = std::string_view::npos; - } else if (start == std::string_view::npos) { - start = index; - } - } - return std::pair{result, count}; - }(); - const auto& values = tokens.first; - const auto count = tokens.second; - const auto four_sides = [&](std::string_view top, std::string_view right, - std::string_view bottom, std::string_view left) { - if (count == 0U || count > 4U) { - apply(declaration, std::string_view(declaration.name)); - return; - } - emit(top, values[0]); - emit(right, count > 1U ? values[1] : values[0]); - emit(bottom, count > 2U ? values[2] : values[0]); - emit(left, count > 3U ? values[3] : count > 1U ? values[1] : values[0]); - }; - const auto two_sides = [&](std::string_view start, std::string_view end) { - if (count == 0U || count > 2U) { - apply(declaration, std::string_view(declaration.name)); - return; - } - emit(start, values[0]); - emit(end, count > 1U ? values[1] : values[0]); - }; - if (name == "margin") { - four_sides("margin-top", "margin-right", "margin-bottom", "margin-left"); - } else if (name == "margin-block") { - two_sides("margin-top", "margin-bottom"); - } else if (name == "margin-inline") { - two_sides("margin-left", "margin-right"); - } else if (name == "padding") { - four_sides("padding-top", "padding-right", "padding-bottom", "padding-left"); - } else if (name == "padding-block") { - two_sides("padding-top", "padding-bottom"); - } else if (name == "padding-inline") { - two_sides("padding-left", "padding-right"); - } else if (name == "inset") { - four_sides("top", "right", "bottom", "left"); - } else if (name == "border-width") { - four_sides("border-top-width", "border-right-width", - "border-bottom-width", "border-left-width"); - } else if (name == "border-color") { - four_sides("border-top-color", "border-right-color", - "border-bottom-color", "border-left-color"); - } else if (name == "gap") { - two_sides("row-gap", "column-gap"); - } else if (name == "overflow") { - two_sides("overflow-x", "overflow-y"); - } + for_each_effective_property_component( + declaration.name, + declaration.value, + [&](std::string_view name, std::string_view value) { + css_declaration effective{ + std::string(name), std::string(value), declaration.important}; + apply(effective, name); + }); } struct cascade_rollback_winner final { @@ -324,7 +169,7 @@ void for_each_cascaded_declaration( || declaration.important != important) { continue; } - for_each_effective_box_declaration( + for_each_effective_declaration( declaration, [&](const css_declaration& effective, std::string_view effective_name) { @@ -379,7 +224,7 @@ void for_each_cascaded_declaration( || declaration.important != important) { continue; } - for_each_effective_box_declaration( + for_each_effective_declaration( declaration, [&](const css_declaration& effective, std::string_view effective_name) { diff --git a/scripts/test_css_property_metadata.py b/scripts/test_css_property_metadata.py new file mode 100644 index 00000000..a1c7f41c --- /dev/null +++ b/scripts/test_css_property_metadata.py @@ -0,0 +1,55 @@ +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "tools/generate_css_property_metadata.py" +SPEC = importlib.util.spec_from_file_location("css_property_metadata_generator", MODULE_PATH) +assert SPEC and SPEC.loader +GENERATOR = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(GENERATOR) + + +class CssPropertyMetadataTests(unittest.TestCase): + def test_checked_in_header_matches_catalog(self): + entries = GENERATOR.load_catalog(GENERATOR.DEFAULT_INPUT) + expected = GENERATOR.generate(entries) + self.assertEqual( + expected, + GENERATOR.DEFAULT_OUTPUT.read_text(encoding="utf-8"), + ) + + def test_rejects_alias_to_unknown_property(self): + payload = { + "version": 1, + "properties": [ + {"name": "left", "mask": ["inline_left"]}, + {"name": "logical-left", "mask": ["inline_left"], "alias": "missing"}, + ], + } + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "metadata.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "unknown effective property"): + GENERATOR.load_catalog(path) + + def test_rejects_duplicate_property(self): + payload = { + "version": 1, + "properties": [ + {"name": "left", "mask": ["inline_left"]}, + {"name": "left", "mask": ["inline_left"]}, + ], + } + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "metadata.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "duplicate property"): + GENERATOR.load_catalog(path) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/NativeWeb/css_service.cpp b/tests/NativeWeb/css_service.cpp index e1f503f1..e8c26c3b 100644 --- a/tests/NativeWeb/css_service.cpp +++ b/tests/NativeWeb/css_service.cpp @@ -741,6 +741,38 @@ int main(int argc,char** argv) { property_mask("inset-inline-start")!=property_mask("left") || property_mask("borderTopColor")!=property_mask("border") || property_mask("--custom")!=0 || property_mask("text-anchor")<=0xFFFFFFFFULL) return 89; + const auto& property_catalog=webscene_native::css::effective_property_metadata_catalog; + for(size_t index=0;index0 && property_catalog[index-1].name>=metadata.name)) return 179; + for(uint8_t longhand=0;longhandlonghand_count!=0U) return 179; + } + } + std::vector effective_declarations; + webscene_native::css::for_each_effective_declaration( + {"border-inline-color","red blue",false}, + [&](const auto& declaration,std::string_view) { + effective_declarations.push_back(declaration); + }); + webscene_native::css::for_each_effective_declaration( + {"inset-block","3px 7px",true}, + [&](const auto& declaration,std::string_view) { + effective_declarations.push_back(declaration); + }); + if(effective_declarations.size()!=4 || + effective_declarations[0].name!="border-left-color" || + effective_declarations[0].value!="red" || + effective_declarations[1].name!="border-right-color" || + effective_declarations[1].value!="blue" || + effective_declarations[2].name!="top" || + effective_declarations[2].value!="3px" || + !effective_declarations[2].important || + effective_declarations[3].name!="bottom" || + effective_declarations[3].value!="7px") return 180; auto& reset_node=animated_document.create_element("button"); reset_node.style.width={90,webscene_native::length_unit::pixels}; reset_node.style.height={70,webscene_native::length_unit::pixels}; diff --git a/tests/WebPlatformSubset/contracts/css-effective-property-cascade.html b/tests/WebPlatformSubset/contracts/css-effective-property-cascade.html index 5b5bfb4a..4a485724 100644 --- a/tests/WebPlatformSubset/contracts/css-effective-property-cascade.html +++ b/tests/WebPlatformSubset/contracts/css-effective-property-cascade.html @@ -43,6 +43,18 @@ overflow-x: hidden; overflow-y: scroll; } + #logical-inset-rollback { + position: absolute; + inset-block: 3px 4px; + inset-inline: 5px 6px; + } + #logical-border-rollback { + border-style: solid; + border-block-width: 1px 2px; + border-inline-width: 3px 4px; + border-block-color: rgb(1, 2, 3) rgb(4, 5, 6); + border-inline-color: rgb(7, 8, 9) rgb(10, 11, 12); + } } @layer effective-theme { @@ -88,6 +100,20 @@ overflow: auto; overflow-y: revert-layer; } + #logical-inset-rollback { + inset-block: 30px 40px; + inset-inline: 50px 60px; + inset-block-start: revert-layer; + inset-inline-end: revert-layer; + } + #logical-border-rollback { + border-block-width: 10px 20px; + border-inline-width: 30px 40px; + border-block-color: rgb(21, 22, 23) rgb(24, 25, 26); + border-inline-color: rgb(27, 28, 29) rgb(30, 31, 32); + border-block-end-width: revert-layer; + border-inline-start-color: revert-layer; + } } #all-important-parent { color: rgb(61, 62, 63); } @@ -110,9 +136,12 @@
+
+
+