From 582a0b598eb079379aa308b182b821c7a36df0ef Mon Sep 17 00:00:00 2001 From: Dan Walmsley <4672627+danwalmsley@users.noreply.github.com> Date: Thu, 17 Sep 2026 19:45:26 +0100 Subject: [PATCH] Generate shared CSS property identity metadata --- .gitignore | 1 + .../css-property-identity-20260917.md | 82 ++++ .../css-property-identity-20260917.json | 76 +++ .../native/css_property_metadata.json | 42 ++ .../webscene_css_supported_properties.inc | 222 +++++++++ .../native/webscene_v8_runtime.cpp | 52 +- scripts/test_css_property_metadata.py | 79 +++- src/WebScene.Css/CssPropertyCatalog.cs | 26 +- .../CssPropertyMetadata.Generated.cs | 446 ++++++++++++++++++ src/WebScene.Css/CssPropertyStorage.cs | 65 +-- tests/WebPlatformSubset/capabilities.json | 4 +- .../cssom-inline-declaration-validity.html | 18 + .../webscene-component-profile.json | 4 +- .../CssPropertyCatalogTests.cs | 18 + .../CssPropertyStorageTests.cs | 3 + tools/generate_css_property_metadata.py | 236 +++++++++ 16 files changed, 1229 insertions(+), 145 deletions(-) create mode 100644 docs/validation/css-property-identity-20260917.md create mode 100644 docs/validation/evidence/css-property-identity-20260917.json create mode 100644 experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc create mode 100644 src/WebScene.Css/CssPropertyMetadata.Generated.cs create mode 100644 tools/generate_css_property_metadata.py diff --git a/.gitignore b/.gitignore index 6f35e0059..05765afee 100644 --- a/.gitignore +++ b/.gitignore @@ -281,6 +281,7 @@ __pycache__/ # Cake - Uncomment if you are using it tools/** # !tools/packages.config +!tools/generate_css_property_metadata.py !tools/webidl-v8-bindings/ !tools/webidl-v8-bindings/dom-exposure.json !tools/webidl-v8-bindings/generate.mjs diff --git a/docs/validation/css-property-identity-20260917.md b/docs/validation/css-property-identity-20260917.md new file mode 100644 index 000000000..915a06302 --- /dev/null +++ b/docs/validation/css-property-identity-20260917.md @@ -0,0 +1,82 @@ +# Generated CSS property identity validation (2026-09-17) + +This stage of #235 removes the remaining independent managed and native +CSSStyleDeclaration property lists. It builds on the effective-property registry +merged in #309; it does not alter the selector timing, containment, or stylesheet +finalization work owned by #238 and #297. + +## Structural result + +`experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json` is now +the checked-in source for three related property views: + +- 105 stable managed storage properties, in ID order; +- 95 additional CSSOM-supported names; and +- 61 effective box-property entries used by native cascade masks and expansion. + +The generator validates and emits: + +- the existing 61-entry native effective-property table; +- a 214-entry native CSSOM table containing canonical CSS names and precomputed IDL + names; and +- managed arrays for stable storage IDs and the same 214 supported CSSOM names, + plus the direct name-to-ID switch. + +The native V8 template previously maintained 152 handwritten IDL names separately +from the managed catalog. It now consumes the generated CSS/IDL pairs. The pairs +are compile-time string views, so template setup does not allocate strings to +convert 214 property names at runtime. Six compatibility names that existed only +in the native list (`animation`, `box-shadow`, `contain`, `grid-row-gap`, +`grid-column-gap`, and `webkit-font-smoothing`) were retained in the shared catalog. + +Managed direct-storage IDs remain byte-for-byte ordered as before. The generator +test pins all 105 names with a SHA-256 sequence fingerprint in addition to checking +the generated outputs. Case-insensitive access still uses the existing frozen-map +fallback, while exact lower-case access retains the direct switch. + +The shared union also closes an observed CSSOM drift: 14 logical effective names, +including `inset-block-end`, `border-block-width`, `border-inline-color`, +`padding-block-start`, and `margin-block-end`, were understood by native style +application but absent from one or both named-property exposure lists. + +The generator itself is now explicitly tracked. The repository's broad `tools/**` +ignore rule had excluded the script introduced with #309 even though CI invoked it; +the new `.gitignore` exception prevents clean checkouts from losing that build input. + +## Browser-shaped regression + +`tests/WebPlatformSubset/contracts/cssom-inline-declaration-validity.html` now has a +fifth subtest covering named exposure and assignment for logical inset, border, +padding, and margin properties. Results: + +- Google Chrome 153.0.8010.48: 5/5; +- native current CSSParser: 5/5; and +- native legacy parser: 5/5. + +The native engine initially failed the new subtest because `inset-block-end` was +missing from its handwritten V8 template list. Replacing that list with generated +metadata is the regression fix; adding only managed tests would not have caught it. + +An adjacent native `cssom-` sweep passes all nine CSS-specific documents. The filter +also selects `dom-document-cssom-hosted-page-primitives.html`; its unrelated +`document.cookie` assertion failed both with default and fresh storage, leaving the +combined sweep at 9/10 documents and 60/61 subtests. No property-exposure assertion +failed. + +## Build and test evidence + +- metadata generator tests: 6/6, followed by a clean `--check`; +- WebScene.Css tests: 383/383 on .NET 8 and 383/383 on .NET 10; +- full `WebScene.sln` Release build: success with 16 pre-existing nullable warnings + and zero errors; +- full managed solution tests: 1,774 passed, 30 platform-dependent tests skipped; +- current-parser native CSS parser CTest: 1/1; +- NativeWeb CSS service/shared-style/compiler CTests: 3/3; and +- both current- and legacy-parser native engine libraries rebuilt successfully. + +This stage centralizes identity and exposure, not all CSS semantics. Property initial +values, inheritance, serialization, grammar selection, complex shorthand expansion, +origins, and CSS-wide keyword behavior remain later metadata slices in #235. + +Machine-readable evidence is in +`docs/validation/evidence/css-property-identity-20260917.json`. diff --git a/docs/validation/evidence/css-property-identity-20260917.json b/docs/validation/evidence/css-property-identity-20260917.json new file mode 100644 index 000000000..6aadac795 --- /dev/null +++ b/docs/validation/evidence/css-property-identity-20260917.json @@ -0,0 +1,76 @@ +{ + "date": "2026-09-17", + "issue": 235, + "catalog": { + "managedKnownProperties": 105, + "managedKnownPropertyOrderSha256": "fc4d77c515998b6df1f119ff7e09b0b82cc812744c34f1c44c6fd142067b1fb2", + "supportedPropertyExtras": 95, + "effectiveProperties": 61, + "unifiedSupportedProperties": 214, + "previousNativeHandwrittenIdlNames": 152 + }, + "generatedOutputs": { + "nativeEffectivePropertyTable": 61, + "nativeCssomCssIdlPairs": 214, + "managedKnownNames": 105, + "managedSupportedNames": 214, + "managedDirectIdSwitchEntries": 105 + }, + "contracts": { + "chrome": { + "version": "153.0.8010.48", + "passed": 5, + "total": 5 + }, + "nativeCurrentParser": { + "passed": 5, + "total": 5 + }, + "nativeLegacyParser": { + "passed": 5, + "total": 5 + } + }, + "tests": { + "managedSolutionBuild": { + "result": "pass", + "warnings": 16, + "newWarnings": 0, + "errors": 0 + }, + "generator": { + "passed": 6, + "total": 6, + "generatedCheck": "pass" + }, + "managedCssNet8": { + "passed": 383, + "total": 383 + }, + "managedCssNet10": { + "passed": 383, + "total": 383 + }, + "managedSolution": { + "passed": 1774, + "skipped": 30, + "failed": 0 + }, + "nativeCurrentCssParser": { + "passed": 1, + "total": 1 + }, + "nativeWebCss": { + "passed": 3, + "total": 3 + } + }, + "adjacentCssomSweep": { + "documentsPassed": 9, + "documentsTotal": 10, + "subtestsPassed": 60, + "subtestsTotal": 61, + "propertyExposureFailures": 0, + "unrelatedFailure": "dom-document-cssom-hosted-page-primitives document.cookie" + } +} diff --git a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json index 181824e52..e94534236 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -1,5 +1,47 @@ { "version": 1, + "managedKnownProperties": [ + "align-content", "align-items", "align-self", "all", "background", "background-color", + "background-image", "border", "border-bottom-color", "border-bottom-left-radius", + "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-color", + "border-left-color", "border-left-style", "border-left-width", "border-radius", + "border-right-color", "border-right-style", "border-right-width", "border-style", + "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", + "border-top-width", "border-width", "bottom", "box-sizing", "clip-rule", "color", + "column-gap", "content", "cursor", "direction", "display", "fill", "fill-rule", "flex", + "flex-basis", "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", "font", + "font-family", "font-size", "font-style", "font-variant", "font-weight", "gap", + "grid", "grid-template-columns", "grid-template-rows", "height", "inset", "justify-content", + "left", "letter-spacing", "line-height", "margin", "margin-bottom", "margin-left", + "margin-right", "margin-top", "max-height", "max-width", "min-height", "min-width", "opacity", + "order", "overflow", "overflow-x", "overflow-y", "padding", "padding-bottom", "padding-left", + "padding-right", "padding-top", "pointer-events", "position", "right", "row-gap", "stroke", + "stroke-linecap", "stroke-linejoin", "stroke-width", "text-align", "text-indent", "text-transform", + "top", "transform", "visibility", "white-space", "width", "word-spacing", "z-index", + "outline", "outline-color", "outline-offset", "outline-style", "outline-width", + "grid-template-areas" + ], + "supportedPropertyExtras": [ + "animation", "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", + "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", + "accent-color", "appearance", "backdrop-filter", "background-attachment", "background-clip", + "background-origin", "background-position", "background-position-x", "background-position-y", + "background-repeat", "background-size", "border-bottom", "border-collapse", "border-left", + "border-right", "border-spacing", "border-top", "clear", "column-count", "columns", "css-float", + "box-shadow", "clip-path", "color-scheme", "contain", "empty-cells", "fill-opacity", "filter", "float", "font-stretch", + "grid-area", "grid-column", "grid-column-end", "grid-column-start", "grid-row", "grid-row-end", + "grid-row-start", "grid-gap", "grid-row-gap", "grid-column-gap", "inset-block", "inset-inline", "inset-inline-start", + "inset-inline-end", "margin-block", "margin-inline", "margin-inline-start", "margin-inline-end", + "mask-composite", "mask-image", "mask-position", "mask-repeat", "mask-size", "padding-block", + "padding-inline", "padding-inline-start", "padding-inline-end", "border-inline-start", + "border-inline-end", "border-inline-start-width", "border-inline-end-width", + "border-inline-start-color", "border-inline-end-color", "border-start-start-radius", + "border-start-end-radius", "border-end-start-radius", "border-end-end-radius", "-moz-transform", + "moz-transform", "-webkit-transform", "webkit-transform", "webkit-font-smoothing", "orphans", "resize", "table-layout", + "text-decoration", "text-overflow", "vertical-align", "transition", "transition-delay", + "transition-duration", "transition-property", "transition-timing-function", "transform-origin", + "widows", "zoom" + ], "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"}, diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc new file mode 100644 index 000000000..95499f7c8 --- /dev/null +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc @@ -0,0 +1,222 @@ +// Generated by tools/generate_css_property_metadata.py. Do not edit. +struct cssom_supported_property_metadata final { + std::string_view css_name; + std::string_view idl_name; +}; + +inline constexpr std::array cssom_supported_property_catalog{ + cssom_supported_property_metadata{"-moz-transform", "MozTransform"}, + cssom_supported_property_metadata{"-webkit-transform", "WebkitTransform"}, + cssom_supported_property_metadata{"accent-color", "accentColor"}, + cssom_supported_property_metadata{"align-content", "alignContent"}, + cssom_supported_property_metadata{"align-items", "alignItems"}, + cssom_supported_property_metadata{"align-self", "alignSelf"}, + cssom_supported_property_metadata{"all", "all"}, + cssom_supported_property_metadata{"animation", "animation"}, + cssom_supported_property_metadata{"animation-delay", "animationDelay"}, + cssom_supported_property_metadata{"animation-direction", "animationDirection"}, + cssom_supported_property_metadata{"animation-duration", "animationDuration"}, + cssom_supported_property_metadata{"animation-fill-mode", "animationFillMode"}, + cssom_supported_property_metadata{"animation-iteration-count", "animationIterationCount"}, + cssom_supported_property_metadata{"animation-name", "animationName"}, + cssom_supported_property_metadata{"animation-play-state", "animationPlayState"}, + cssom_supported_property_metadata{"animation-timing-function", "animationTimingFunction"}, + cssom_supported_property_metadata{"appearance", "appearance"}, + cssom_supported_property_metadata{"backdrop-filter", "backdropFilter"}, + cssom_supported_property_metadata{"background", "background"}, + cssom_supported_property_metadata{"background-attachment", "backgroundAttachment"}, + cssom_supported_property_metadata{"background-clip", "backgroundClip"}, + cssom_supported_property_metadata{"background-color", "backgroundColor"}, + cssom_supported_property_metadata{"background-image", "backgroundImage"}, + cssom_supported_property_metadata{"background-origin", "backgroundOrigin"}, + cssom_supported_property_metadata{"background-position", "backgroundPosition"}, + cssom_supported_property_metadata{"background-position-x", "backgroundPositionX"}, + cssom_supported_property_metadata{"background-position-y", "backgroundPositionY"}, + cssom_supported_property_metadata{"background-repeat", "backgroundRepeat"}, + cssom_supported_property_metadata{"background-size", "backgroundSize"}, + cssom_supported_property_metadata{"border", "border"}, + cssom_supported_property_metadata{"border-block-color", "borderBlockColor"}, + cssom_supported_property_metadata{"border-block-end-color", "borderBlockEndColor"}, + cssom_supported_property_metadata{"border-block-end-width", "borderBlockEndWidth"}, + cssom_supported_property_metadata{"border-block-start-color", "borderBlockStartColor"}, + cssom_supported_property_metadata{"border-block-start-width", "borderBlockStartWidth"}, + cssom_supported_property_metadata{"border-block-width", "borderBlockWidth"}, + cssom_supported_property_metadata{"border-bottom", "borderBottom"}, + cssom_supported_property_metadata{"border-bottom-color", "borderBottomColor"}, + cssom_supported_property_metadata{"border-bottom-left-radius", "borderBottomLeftRadius"}, + cssom_supported_property_metadata{"border-bottom-right-radius", "borderBottomRightRadius"}, + cssom_supported_property_metadata{"border-bottom-style", "borderBottomStyle"}, + cssom_supported_property_metadata{"border-bottom-width", "borderBottomWidth"}, + cssom_supported_property_metadata{"border-collapse", "borderCollapse"}, + cssom_supported_property_metadata{"border-color", "borderColor"}, + cssom_supported_property_metadata{"border-end-end-radius", "borderEndEndRadius"}, + cssom_supported_property_metadata{"border-end-start-radius", "borderEndStartRadius"}, + cssom_supported_property_metadata{"border-inline-color", "borderInlineColor"}, + cssom_supported_property_metadata{"border-inline-end", "borderInlineEnd"}, + cssom_supported_property_metadata{"border-inline-end-color", "borderInlineEndColor"}, + cssom_supported_property_metadata{"border-inline-end-width", "borderInlineEndWidth"}, + cssom_supported_property_metadata{"border-inline-start", "borderInlineStart"}, + cssom_supported_property_metadata{"border-inline-start-color", "borderInlineStartColor"}, + cssom_supported_property_metadata{"border-inline-start-width", "borderInlineStartWidth"}, + cssom_supported_property_metadata{"border-inline-width", "borderInlineWidth"}, + cssom_supported_property_metadata{"border-left", "borderLeft"}, + cssom_supported_property_metadata{"border-left-color", "borderLeftColor"}, + cssom_supported_property_metadata{"border-left-style", "borderLeftStyle"}, + cssom_supported_property_metadata{"border-left-width", "borderLeftWidth"}, + cssom_supported_property_metadata{"border-radius", "borderRadius"}, + cssom_supported_property_metadata{"border-right", "borderRight"}, + cssom_supported_property_metadata{"border-right-color", "borderRightColor"}, + cssom_supported_property_metadata{"border-right-style", "borderRightStyle"}, + cssom_supported_property_metadata{"border-right-width", "borderRightWidth"}, + cssom_supported_property_metadata{"border-spacing", "borderSpacing"}, + cssom_supported_property_metadata{"border-start-end-radius", "borderStartEndRadius"}, + cssom_supported_property_metadata{"border-start-start-radius", "borderStartStartRadius"}, + cssom_supported_property_metadata{"border-style", "borderStyle"}, + cssom_supported_property_metadata{"border-top", "borderTop"}, + cssom_supported_property_metadata{"border-top-color", "borderTopColor"}, + cssom_supported_property_metadata{"border-top-left-radius", "borderTopLeftRadius"}, + cssom_supported_property_metadata{"border-top-right-radius", "borderTopRightRadius"}, + cssom_supported_property_metadata{"border-top-style", "borderTopStyle"}, + cssom_supported_property_metadata{"border-top-width", "borderTopWidth"}, + cssom_supported_property_metadata{"border-width", "borderWidth"}, + cssom_supported_property_metadata{"bottom", "bottom"}, + cssom_supported_property_metadata{"box-shadow", "boxShadow"}, + cssom_supported_property_metadata{"box-sizing", "boxSizing"}, + cssom_supported_property_metadata{"clear", "clear"}, + cssom_supported_property_metadata{"clip-path", "clipPath"}, + cssom_supported_property_metadata{"clip-rule", "clipRule"}, + cssom_supported_property_metadata{"color", "color"}, + cssom_supported_property_metadata{"color-scheme", "colorScheme"}, + cssom_supported_property_metadata{"column-count", "columnCount"}, + cssom_supported_property_metadata{"column-gap", "columnGap"}, + cssom_supported_property_metadata{"columns", "columns"}, + cssom_supported_property_metadata{"contain", "contain"}, + cssom_supported_property_metadata{"content", "content"}, + cssom_supported_property_metadata{"css-float", "cssFloat"}, + cssom_supported_property_metadata{"cursor", "cursor"}, + cssom_supported_property_metadata{"direction", "direction"}, + cssom_supported_property_metadata{"display", "display"}, + cssom_supported_property_metadata{"empty-cells", "emptyCells"}, + cssom_supported_property_metadata{"fill", "fill"}, + cssom_supported_property_metadata{"fill-opacity", "fillOpacity"}, + cssom_supported_property_metadata{"fill-rule", "fillRule"}, + cssom_supported_property_metadata{"filter", "filter"}, + cssom_supported_property_metadata{"flex", "flex"}, + cssom_supported_property_metadata{"flex-basis", "flexBasis"}, + cssom_supported_property_metadata{"flex-direction", "flexDirection"}, + cssom_supported_property_metadata{"flex-flow", "flexFlow"}, + cssom_supported_property_metadata{"flex-grow", "flexGrow"}, + cssom_supported_property_metadata{"flex-shrink", "flexShrink"}, + cssom_supported_property_metadata{"flex-wrap", "flexWrap"}, + cssom_supported_property_metadata{"float", "cssFloat"}, + cssom_supported_property_metadata{"font", "font"}, + cssom_supported_property_metadata{"font-family", "fontFamily"}, + cssom_supported_property_metadata{"font-size", "fontSize"}, + cssom_supported_property_metadata{"font-stretch", "fontStretch"}, + cssom_supported_property_metadata{"font-style", "fontStyle"}, + cssom_supported_property_metadata{"font-variant", "fontVariant"}, + cssom_supported_property_metadata{"font-weight", "fontWeight"}, + cssom_supported_property_metadata{"gap", "gap"}, + cssom_supported_property_metadata{"grid", "grid"}, + cssom_supported_property_metadata{"grid-area", "gridArea"}, + cssom_supported_property_metadata{"grid-column", "gridColumn"}, + cssom_supported_property_metadata{"grid-column-end", "gridColumnEnd"}, + cssom_supported_property_metadata{"grid-column-gap", "gridColumnGap"}, + cssom_supported_property_metadata{"grid-column-start", "gridColumnStart"}, + cssom_supported_property_metadata{"grid-gap", "gridGap"}, + cssom_supported_property_metadata{"grid-row", "gridRow"}, + cssom_supported_property_metadata{"grid-row-end", "gridRowEnd"}, + cssom_supported_property_metadata{"grid-row-gap", "gridRowGap"}, + cssom_supported_property_metadata{"grid-row-start", "gridRowStart"}, + cssom_supported_property_metadata{"grid-template-areas", "gridTemplateAreas"}, + cssom_supported_property_metadata{"grid-template-columns", "gridTemplateColumns"}, + cssom_supported_property_metadata{"grid-template-rows", "gridTemplateRows"}, + cssom_supported_property_metadata{"height", "height"}, + cssom_supported_property_metadata{"inset", "inset"}, + cssom_supported_property_metadata{"inset-block", "insetBlock"}, + cssom_supported_property_metadata{"inset-block-end", "insetBlockEnd"}, + cssom_supported_property_metadata{"inset-block-start", "insetBlockStart"}, + cssom_supported_property_metadata{"inset-inline", "insetInline"}, + cssom_supported_property_metadata{"inset-inline-end", "insetInlineEnd"}, + cssom_supported_property_metadata{"inset-inline-start", "insetInlineStart"}, + cssom_supported_property_metadata{"justify-content", "justifyContent"}, + cssom_supported_property_metadata{"left", "left"}, + cssom_supported_property_metadata{"letter-spacing", "letterSpacing"}, + cssom_supported_property_metadata{"line-height", "lineHeight"}, + cssom_supported_property_metadata{"margin", "margin"}, + cssom_supported_property_metadata{"margin-block", "marginBlock"}, + cssom_supported_property_metadata{"margin-block-end", "marginBlockEnd"}, + cssom_supported_property_metadata{"margin-block-start", "marginBlockStart"}, + cssom_supported_property_metadata{"margin-bottom", "marginBottom"}, + cssom_supported_property_metadata{"margin-inline", "marginInline"}, + cssom_supported_property_metadata{"margin-inline-end", "marginInlineEnd"}, + cssom_supported_property_metadata{"margin-inline-start", "marginInlineStart"}, + cssom_supported_property_metadata{"margin-left", "marginLeft"}, + cssom_supported_property_metadata{"margin-right", "marginRight"}, + cssom_supported_property_metadata{"margin-top", "marginTop"}, + cssom_supported_property_metadata{"mask-composite", "maskComposite"}, + cssom_supported_property_metadata{"mask-image", "maskImage"}, + cssom_supported_property_metadata{"mask-position", "maskPosition"}, + cssom_supported_property_metadata{"mask-repeat", "maskRepeat"}, + cssom_supported_property_metadata{"mask-size", "maskSize"}, + cssom_supported_property_metadata{"max-height", "maxHeight"}, + cssom_supported_property_metadata{"max-width", "maxWidth"}, + cssom_supported_property_metadata{"min-height", "minHeight"}, + cssom_supported_property_metadata{"min-width", "minWidth"}, + cssom_supported_property_metadata{"moz-transform", "mozTransform"}, + cssom_supported_property_metadata{"opacity", "opacity"}, + cssom_supported_property_metadata{"order", "order"}, + cssom_supported_property_metadata{"orphans", "orphans"}, + cssom_supported_property_metadata{"outline", "outline"}, + cssom_supported_property_metadata{"outline-color", "outlineColor"}, + cssom_supported_property_metadata{"outline-offset", "outlineOffset"}, + cssom_supported_property_metadata{"outline-style", "outlineStyle"}, + cssom_supported_property_metadata{"outline-width", "outlineWidth"}, + cssom_supported_property_metadata{"overflow", "overflow"}, + cssom_supported_property_metadata{"overflow-x", "overflowX"}, + cssom_supported_property_metadata{"overflow-y", "overflowY"}, + cssom_supported_property_metadata{"padding", "padding"}, + cssom_supported_property_metadata{"padding-block", "paddingBlock"}, + cssom_supported_property_metadata{"padding-block-end", "paddingBlockEnd"}, + cssom_supported_property_metadata{"padding-block-start", "paddingBlockStart"}, + cssom_supported_property_metadata{"padding-bottom", "paddingBottom"}, + cssom_supported_property_metadata{"padding-inline", "paddingInline"}, + cssom_supported_property_metadata{"padding-inline-end", "paddingInlineEnd"}, + cssom_supported_property_metadata{"padding-inline-start", "paddingInlineStart"}, + cssom_supported_property_metadata{"padding-left", "paddingLeft"}, + cssom_supported_property_metadata{"padding-right", "paddingRight"}, + cssom_supported_property_metadata{"padding-top", "paddingTop"}, + cssom_supported_property_metadata{"pointer-events", "pointerEvents"}, + cssom_supported_property_metadata{"position", "position"}, + cssom_supported_property_metadata{"resize", "resize"}, + cssom_supported_property_metadata{"right", "right"}, + cssom_supported_property_metadata{"row-gap", "rowGap"}, + cssom_supported_property_metadata{"stroke", "stroke"}, + cssom_supported_property_metadata{"stroke-linecap", "strokeLinecap"}, + cssom_supported_property_metadata{"stroke-linejoin", "strokeLinejoin"}, + cssom_supported_property_metadata{"stroke-width", "strokeWidth"}, + cssom_supported_property_metadata{"table-layout", "tableLayout"}, + cssom_supported_property_metadata{"text-align", "textAlign"}, + cssom_supported_property_metadata{"text-decoration", "textDecoration"}, + cssom_supported_property_metadata{"text-indent", "textIndent"}, + cssom_supported_property_metadata{"text-overflow", "textOverflow"}, + cssom_supported_property_metadata{"text-transform", "textTransform"}, + cssom_supported_property_metadata{"top", "top"}, + cssom_supported_property_metadata{"transform", "transform"}, + cssom_supported_property_metadata{"transform-origin", "transformOrigin"}, + cssom_supported_property_metadata{"transition", "transition"}, + cssom_supported_property_metadata{"transition-delay", "transitionDelay"}, + cssom_supported_property_metadata{"transition-duration", "transitionDuration"}, + cssom_supported_property_metadata{"transition-property", "transitionProperty"}, + cssom_supported_property_metadata{"transition-timing-function", "transitionTimingFunction"}, + cssom_supported_property_metadata{"vertical-align", "verticalAlign"}, + cssom_supported_property_metadata{"visibility", "visibility"}, + cssom_supported_property_metadata{"webkit-font-smoothing", "webkitFontSmoothing"}, + cssom_supported_property_metadata{"webkit-transform", "webkitTransform"}, + cssom_supported_property_metadata{"white-space", "whiteSpace"}, + cssom_supported_property_metadata{"widows", "widows"}, + cssom_supported_property_metadata{"width", "width"}, + cssom_supported_property_metadata{"word-spacing", "wordSpacing"}, + cssom_supported_property_metadata{"z-index", "zIndex"}, + cssom_supported_property_metadata{"zoom", "zoom"}, +}; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp index 5c8929b2e..44617a06e 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime.cpp @@ -260,6 +260,7 @@ extern "C" WEBSCENE_API uint64_t webscene_selector_sibling_benchmark_pointer_cop namespace webscene_native { namespace { +#include "generated/webscene_css_supported_properties.inc" #include "webscene_v8_runtime_support.inc" #include "webscene_precompiled_javascript_support.inc" } // namespace @@ -756,51 +757,14 @@ struct v8_dom_runtime::implementation final { style->SetInternalFieldCount(2); style->SetNativeDataProperty( js_string(isolate, "cssText"), get_style_css_text, set_style_css_text); - const char* properties[] = { - "width", "height", "minWidth", "minHeight", "maxWidth", "maxHeight", - "left", "top", "right", "bottom", "inset", "insetInlineStart", "insetInlineEnd", - "display", "position", "contain", "cssFloat", "flexDirection", "flexFlow", - "flexGrow", "flexShrink", "flexBasis", "flexWrap", - "alignItems", "alignSelf", "alignContent", "justifyContent", "gap", "rowGap", "columnGap", - "gridGap", "gridRowGap", "gridColumnGap", - "padding", "paddingInline", "paddingBlock", - "paddingLeft", "paddingTop", "paddingRight", "paddingBottom", - "paddingInlineStart", "paddingInlineEnd", "paddingBlockStart", "paddingBlockEnd", - "margin", "marginInline", "marginBlock", - "marginLeft", "marginTop", "marginRight", "marginBottom", - "boxSizing", "borderRadius", "boxShadow", "transform", "transformOrigin", - "transition", "transitionProperty", "transitionDuration", "transitionDelay", - "transitionTimingFunction", "animation", "animationName", - "animationDuration", "animationDelay", "animationTimingFunction", - "animationDirection", "animationFillMode", "animationIterationCount", - "animationPlayState", "appearance", "zIndex", "opacity", - "background", "backgroundColor", "backgroundImage", "backgroundRepeat", - "backgroundAttachment", "backgroundClip", "backgroundOrigin", - "backgroundPosition", "backgroundPositionX", "backgroundPositionY", "backgroundSize", - "border", "borderWidth", "borderStyle", "borderColor", - "borderTop", "borderRight", "borderBottom", "borderLeft", - "borderTopWidth", "borderRightWidth", - "borderBottomWidth", "borderLeftWidth", "borderTopStyle", "borderTopColor", - "borderRightColor", "borderBottomColor", "borderLeftColor", - "borderTopLeftRadius", "borderTopRightRadius", "borderBottomRightRadius", - "borderBottomLeftRadius", "borderCollapse", "borderSpacing", "clear", - "columnCount", "columns", "emptyCells", "fillOpacity", "float", - "gridTemplateAreas", "gridArea", "gridColumn", "gridColumnEnd", "gridColumnStart", - "gridRow", "gridRowEnd", "gridRowStart", "order", "orphans", - "outlineColor", "outlineWidth", "outlineStyle", - "overflow", "overflowX", "overflowY", "color", - "font", "fontSize", "fontFamily", "webkitFontSmoothing", "fontStretch", - "fontWeight", "lineHeight", - "letterSpacing", "wordSpacing", "resize", "tableLayout", "textAlign", - "textDecoration", "textOverflow", "verticalAlign", "whiteSpace", "widows", "zoom", - "visibility", "direction", "pointerEvents", "cursor" - }; - for (const auto* property : properties) { - style->SetNativeDataProperty(js_string(isolate, property), get_style_property, set_style_property); - const auto css_name = canonical_css_property_name(property); - if (css_name != property) { + for (const auto& property : cssom_supported_property_catalog) { + style->SetNativeDataProperty( + js_string(isolate, property.idl_name.data()), + get_style_property, + set_style_property); + if (property.css_name != property.idl_name) { style->SetNativeDataProperty( - js_string(isolate, css_name.c_str()), + js_string(isolate, property.css_name.data()), get_style_property, set_style_property); } diff --git a/scripts/test_css_property_metadata.py b/scripts/test_css_property_metadata.py index a1c7f41c7..588a4969a 100644 --- a/scripts/test_css_property_metadata.py +++ b/scripts/test_css_property_metadata.py @@ -1,3 +1,4 @@ +import hashlib import importlib.util import json import tempfile @@ -14,22 +15,53 @@ class CssPropertyMetadataTests(unittest.TestCase): - def test_checked_in_header_matches_catalog(self): - entries = GENERATOR.load_catalog(GENERATOR.DEFAULT_INPUT) - expected = GENERATOR.generate(entries) + @staticmethod + def _payload(properties, known=None, extras=None): + return { + "version": 1, + "managedKnownProperties": known or ["left"], + "supportedPropertyExtras": extras or [], + "properties": properties, + } + + def test_checked_in_outputs_match_catalog(self): + catalog = GENERATOR.load_catalog(GENERATOR.DEFAULT_INPUT) self.assertEqual( - expected, + GENERATOR.generate_native(catalog), GENERATOR.DEFAULT_OUTPUT.read_text(encoding="utf-8"), ) + self.assertEqual( + GENERATOR.generate_native_supported(catalog), + GENERATOR.DEFAULT_NATIVE_SUPPORTED_OUTPUT.read_text(encoding="utf-8"), + ) + self.assertEqual( + GENERATOR.generate_managed(catalog), + GENERATOR.DEFAULT_MANAGED_OUTPUT.read_text(encoding="utf-8"), + ) + + def test_catalog_preserves_known_ids_and_unifies_supported_names(self): + catalog = GENERATOR.load_catalog(GENERATOR.DEFAULT_INPUT) + self.assertEqual(105, len(catalog.managed_known_properties)) + self.assertEqual("align-content", catalog.managed_known_properties[0]) + self.assertEqual("grid-template-areas", catalog.managed_known_properties[-1]) + self.assertEqual( + "fc4d77c515998b6df1f119ff7e09b0b82cc812744c34f1c44c6fd142067b1fb2", + hashlib.sha256("\n".join(catalog.managed_known_properties).encode()).hexdigest(), + ) + self.assertEqual(214, len(GENERATOR.supported_names(catalog))) + self.assertEqual("gridGap", GENERATOR.css_idl_name("grid-gap")) + self.assertEqual("MozTransform", GENERATOR.css_idl_name("-moz-transform")) + self.assertEqual("cssFloat", GENERATOR.css_idl_name("float")) + + rendered = GENERATOR.generate_managed(catalog) + for name in ("inset-block-end", "border-block-width", "padding-block-start"): + self.assertIn(f' "{name}",', rendered) def test_rejects_alias_to_unknown_property(self): - payload = { - "version": 1, - "properties": [ + payload = self._payload([ {"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") @@ -37,19 +69,38 @@ def test_rejects_alias_to_unknown_property(self): GENERATOR.load_catalog(path) def test_rejects_duplicate_property(self): - payload = { - "version": 1, - "properties": [ + payload = self._payload([ {"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) + def test_rejects_duplicate_managed_names(self): + payload = self._payload( + [{"name": "left", "mask": ["inline_left"]}], + known=["left", "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 managedKnownProperties"): + GENERATOR.load_catalog(path) + + def test_rejects_duplicate_supported_extras(self): + payload = self._payload( + [{"name": "left", "mask": ["inline_left"]}], + extras=["float", "float"], + ) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "metadata.json" + path.write_text(json.dumps(payload), encoding="utf-8") + with self.assertRaisesRegex(ValueError, "duplicate supportedPropertyExtras"): + GENERATOR.load_catalog(path) + if __name__ == "__main__": unittest.main() diff --git a/src/WebScene.Css/CssPropertyCatalog.cs b/src/WebScene.Css/CssPropertyCatalog.cs index 9115d00d7..a2a9c8a3e 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -11,31 +11,7 @@ namespace WebScene.Css; /// public static class CssPropertyCatalog { - private static readonly FrozenSet s_supported = CssKnownProperties.Names - .Concat(new[] - { - "animation-delay", "animation-direction", "animation-duration", "animation-fill-mode", - "animation-iteration-count", "animation-name", "animation-play-state", "animation-timing-function", - "accent-color", "appearance", "backdrop-filter", "background-attachment", "background-clip", "background-origin", - "background-position", "background-position-x", "background-position-y", "background-repeat", - "background-size", "border-bottom", "border-collapse", "border-left", "border-right", - "border-spacing", "border-top", "clear", "column-count", "columns", "css-float", - "clip-path", "color-scheme", "empty-cells", "fill-opacity", "filter", "float", "font-stretch", "grid-area", "grid-column", - "grid-column-end", "grid-column-start", "grid-row", "grid-row-end", "grid-row-start", - "grid-gap", "inset-block", "inset-inline", "inset-inline-start", "inset-inline-end", - "margin-block", "margin-inline", "margin-inline-start", "margin-inline-end", - "mask-composite", "mask-image", "mask-position", "mask-repeat", "mask-size", - "padding-block", "padding-inline", "padding-inline-start", "padding-inline-end", - "border-inline-start", "border-inline-end", - "border-inline-start-width", "border-inline-end-width", - "border-inline-start-color", "border-inline-end-color", - "border-start-start-radius", "border-start-end-radius", - "border-end-start-radius", "border-end-end-radius", - "-moz-transform", "moz-transform", "-webkit-transform", "webkit-transform", - "orphans", "resize", "table-layout", "text-decoration", "text-overflow", "vertical-align", - "transition", "transition-delay", "transition-duration", "transition-property", - "transition-timing-function", "transform-origin", "widows", "zoom" - }) + private static readonly FrozenSet s_supported = CssGeneratedPropertyMetadata.SupportedNames .ToFrozenSet(StringComparer.OrdinalIgnoreCase); public static bool IsSupported(string? propertyName) diff --git a/src/WebScene.Css/CssPropertyMetadata.Generated.cs b/src/WebScene.Css/CssPropertyMetadata.Generated.cs new file mode 100644 index 000000000..2835cd2de --- /dev/null +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -0,0 +1,446 @@ +// Generated by tools/generate_css_property_metadata.py. Do not edit. +namespace WebScene.Css; + +internal static class CssGeneratedPropertyMetadata +{ + internal static readonly string[] KnownNames = + [ + "align-content", + "align-items", + "align-self", + "all", + "background", + "background-color", + "background-image", + "border", + "border-bottom-color", + "border-bottom-left-radius", + "border-bottom-right-radius", + "border-bottom-style", + "border-bottom-width", + "border-color", + "border-left-color", + "border-left-style", + "border-left-width", + "border-radius", + "border-right-color", + "border-right-style", + "border-right-width", + "border-style", + "border-top-color", + "border-top-left-radius", + "border-top-right-radius", + "border-top-style", + "border-top-width", + "border-width", + "bottom", + "box-sizing", + "clip-rule", + "color", + "column-gap", + "content", + "cursor", + "direction", + "display", + "fill", + "fill-rule", + "flex", + "flex-basis", + "flex-direction", + "flex-flow", + "flex-grow", + "flex-shrink", + "flex-wrap", + "font", + "font-family", + "font-size", + "font-style", + "font-variant", + "font-weight", + "gap", + "grid", + "grid-template-columns", + "grid-template-rows", + "height", + "inset", + "justify-content", + "left", + "letter-spacing", + "line-height", + "margin", + "margin-bottom", + "margin-left", + "margin-right", + "margin-top", + "max-height", + "max-width", + "min-height", + "min-width", + "opacity", + "order", + "overflow", + "overflow-x", + "overflow-y", + "padding", + "padding-bottom", + "padding-left", + "padding-right", + "padding-top", + "pointer-events", + "position", + "right", + "row-gap", + "stroke", + "stroke-linecap", + "stroke-linejoin", + "stroke-width", + "text-align", + "text-indent", + "text-transform", + "top", + "transform", + "visibility", + "white-space", + "width", + "word-spacing", + "z-index", + "outline", + "outline-color", + "outline-offset", + "outline-style", + "outline-width", + "grid-template-areas", + ]; + + internal static readonly string[] SupportedNames = + [ + "-moz-transform", + "-webkit-transform", + "accent-color", + "align-content", + "align-items", + "align-self", + "all", + "animation", + "animation-delay", + "animation-direction", + "animation-duration", + "animation-fill-mode", + "animation-iteration-count", + "animation-name", + "animation-play-state", + "animation-timing-function", + "appearance", + "backdrop-filter", + "background", + "background-attachment", + "background-clip", + "background-color", + "background-image", + "background-origin", + "background-position", + "background-position-x", + "background-position-y", + "background-repeat", + "background-size", + "border", + "border-block-color", + "border-block-end-color", + "border-block-end-width", + "border-block-start-color", + "border-block-start-width", + "border-block-width", + "border-bottom", + "border-bottom-color", + "border-bottom-left-radius", + "border-bottom-right-radius", + "border-bottom-style", + "border-bottom-width", + "border-collapse", + "border-color", + "border-end-end-radius", + "border-end-start-radius", + "border-inline-color", + "border-inline-end", + "border-inline-end-color", + "border-inline-end-width", + "border-inline-start", + "border-inline-start-color", + "border-inline-start-width", + "border-inline-width", + "border-left", + "border-left-color", + "border-left-style", + "border-left-width", + "border-radius", + "border-right", + "border-right-color", + "border-right-style", + "border-right-width", + "border-spacing", + "border-start-end-radius", + "border-start-start-radius", + "border-style", + "border-top", + "border-top-color", + "border-top-left-radius", + "border-top-right-radius", + "border-top-style", + "border-top-width", + "border-width", + "bottom", + "box-shadow", + "box-sizing", + "clear", + "clip-path", + "clip-rule", + "color", + "color-scheme", + "column-count", + "column-gap", + "columns", + "contain", + "content", + "css-float", + "cursor", + "direction", + "display", + "empty-cells", + "fill", + "fill-opacity", + "fill-rule", + "filter", + "flex", + "flex-basis", + "flex-direction", + "flex-flow", + "flex-grow", + "flex-shrink", + "flex-wrap", + "float", + "font", + "font-family", + "font-size", + "font-stretch", + "font-style", + "font-variant", + "font-weight", + "gap", + "grid", + "grid-area", + "grid-column", + "grid-column-end", + "grid-column-gap", + "grid-column-start", + "grid-gap", + "grid-row", + "grid-row-end", + "grid-row-gap", + "grid-row-start", + "grid-template-areas", + "grid-template-columns", + "grid-template-rows", + "height", + "inset", + "inset-block", + "inset-block-end", + "inset-block-start", + "inset-inline", + "inset-inline-end", + "inset-inline-start", + "justify-content", + "left", + "letter-spacing", + "line-height", + "margin", + "margin-block", + "margin-block-end", + "margin-block-start", + "margin-bottom", + "margin-inline", + "margin-inline-end", + "margin-inline-start", + "margin-left", + "margin-right", + "margin-top", + "mask-composite", + "mask-image", + "mask-position", + "mask-repeat", + "mask-size", + "max-height", + "max-width", + "min-height", + "min-width", + "moz-transform", + "opacity", + "order", + "orphans", + "outline", + "outline-color", + "outline-offset", + "outline-style", + "outline-width", + "overflow", + "overflow-x", + "overflow-y", + "padding", + "padding-block", + "padding-block-end", + "padding-block-start", + "padding-bottom", + "padding-inline", + "padding-inline-end", + "padding-inline-start", + "padding-left", + "padding-right", + "padding-top", + "pointer-events", + "position", + "resize", + "right", + "row-gap", + "stroke", + "stroke-linecap", + "stroke-linejoin", + "stroke-width", + "table-layout", + "text-align", + "text-decoration", + "text-indent", + "text-overflow", + "text-transform", + "top", + "transform", + "transform-origin", + "transition", + "transition-delay", + "transition-duration", + "transition-property", + "transition-timing-function", + "vertical-align", + "visibility", + "webkit-font-smoothing", + "webkit-transform", + "white-space", + "widows", + "width", + "word-spacing", + "z-index", + "zoom", + ]; + + internal static bool TryGetKnownId(string name, out int id) + { + id = name switch + { + "align-content" => 0, + "align-items" => 1, + "align-self" => 2, + "all" => 3, + "background" => 4, + "background-color" => 5, + "background-image" => 6, + "border" => 7, + "border-bottom-color" => 8, + "border-bottom-left-radius" => 9, + "border-bottom-right-radius" => 10, + "border-bottom-style" => 11, + "border-bottom-width" => 12, + "border-color" => 13, + "border-left-color" => 14, + "border-left-style" => 15, + "border-left-width" => 16, + "border-radius" => 17, + "border-right-color" => 18, + "border-right-style" => 19, + "border-right-width" => 20, + "border-style" => 21, + "border-top-color" => 22, + "border-top-left-radius" => 23, + "border-top-right-radius" => 24, + "border-top-style" => 25, + "border-top-width" => 26, + "border-width" => 27, + "bottom" => 28, + "box-sizing" => 29, + "clip-rule" => 30, + "color" => 31, + "column-gap" => 32, + "content" => 33, + "cursor" => 34, + "direction" => 35, + "display" => 36, + "fill" => 37, + "fill-rule" => 38, + "flex" => 39, + "flex-basis" => 40, + "flex-direction" => 41, + "flex-flow" => 42, + "flex-grow" => 43, + "flex-shrink" => 44, + "flex-wrap" => 45, + "font" => 46, + "font-family" => 47, + "font-size" => 48, + "font-style" => 49, + "font-variant" => 50, + "font-weight" => 51, + "gap" => 52, + "grid" => 53, + "grid-template-columns" => 54, + "grid-template-rows" => 55, + "height" => 56, + "inset" => 57, + "justify-content" => 58, + "left" => 59, + "letter-spacing" => 60, + "line-height" => 61, + "margin" => 62, + "margin-bottom" => 63, + "margin-left" => 64, + "margin-right" => 65, + "margin-top" => 66, + "max-height" => 67, + "max-width" => 68, + "min-height" => 69, + "min-width" => 70, + "opacity" => 71, + "order" => 72, + "overflow" => 73, + "overflow-x" => 74, + "overflow-y" => 75, + "padding" => 76, + "padding-bottom" => 77, + "padding-left" => 78, + "padding-right" => 79, + "padding-top" => 80, + "pointer-events" => 81, + "position" => 82, + "right" => 83, + "row-gap" => 84, + "stroke" => 85, + "stroke-linecap" => 86, + "stroke-linejoin" => 87, + "stroke-width" => 88, + "text-align" => 89, + "text-indent" => 90, + "text-transform" => 91, + "top" => 92, + "transform" => 93, + "visibility" => 94, + "white-space" => 95, + "width" => 96, + "word-spacing" => 97, + "z-index" => 98, + "outline" => 99, + "outline-color" => 100, + "outline-offset" => 101, + "outline-style" => 102, + "outline-width" => 103, + "grid-template-areas" => 104, + _ => -1, + }; + return id >= 0; + } +} diff --git a/src/WebScene.Css/CssPropertyStorage.cs b/src/WebScene.Css/CssPropertyStorage.cs index 78358e71c..4aaffe56d 100644 --- a/src/WebScene.Css/CssPropertyStorage.cs +++ b/src/WebScene.Css/CssPropertyStorage.cs @@ -12,28 +12,7 @@ namespace WebScene.Css; /// internal static class CssKnownProperties { - internal static readonly string[] Names = - [ - "align-content", "align-items", "align-self", "all", "background", "background-color", - "background-image", "border", "border-bottom-color", "border-bottom-left-radius", - "border-bottom-right-radius", "border-bottom-style", "border-bottom-width", "border-color", - "border-left-color", "border-left-style", "border-left-width", "border-radius", - "border-right-color", "border-right-style", "border-right-width", "border-style", - "border-top-color", "border-top-left-radius", "border-top-right-radius", "border-top-style", - "border-top-width", "border-width", "bottom", "box-sizing", "clip-rule", "color", - "column-gap", "content", "cursor", "direction", "display", "fill", "fill-rule", "flex", - "flex-basis", "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", "font", - "font-family", "font-size", "font-style", "font-variant", "font-weight", "gap", - "grid", "grid-template-columns", "grid-template-rows", "height", "inset", "justify-content", - "left", "letter-spacing", "line-height", "margin", "margin-bottom", "margin-left", - "margin-right", "margin-top", "max-height", "max-width", "min-height", "min-width", "opacity", - "order", "overflow", "overflow-x", "overflow-y", "padding", "padding-bottom", "padding-left", - "padding-right", "padding-top", "pointer-events", "position", "right", "row-gap", "stroke", - "stroke-linecap", "stroke-linejoin", "stroke-width", "text-align", "text-indent", "text-transform", - "top", "transform", "visibility", "white-space", "width", "word-spacing", "z-index", - "outline", "outline-color", "outline-offset", "outline-style", "outline-width", - "grid-template-areas" - ]; + internal static readonly string[] Names = CssGeneratedPropertyMetadata.KnownNames; private static readonly FrozenDictionary s_ids = Names .Select(static (name, index) => new KeyValuePair(name, index)) @@ -43,42 +22,12 @@ internal static class CssKnownProperties internal static bool TryGetId(string name, out int id) { - id = name switch - { - "align-content" => 0, "align-items" => 1, "align-self" => 2, "all" => 3, - "background" => 4, "background-color" => 5, "background-image" => 6, "border" => 7, - "border-bottom-color" => 8, "border-bottom-left-radius" => 9, - "border-bottom-right-radius" => 10, "border-bottom-style" => 11, - "border-bottom-width" => 12, "border-color" => 13, "border-left-color" => 14, - "border-left-style" => 15, "border-left-width" => 16, "border-radius" => 17, - "border-right-color" => 18, "border-right-style" => 19, "border-right-width" => 20, - "border-style" => 21, "border-top-color" => 22, "border-top-left-radius" => 23, - "border-top-right-radius" => 24, "border-top-style" => 25, "border-top-width" => 26, - "border-width" => 27, "bottom" => 28, "box-sizing" => 29, "clip-rule" => 30, - "color" => 31, "column-gap" => 32, "content" => 33, "cursor" => 34, - "direction" => 35, "display" => 36, "fill" => 37, "fill-rule" => 38, "flex" => 39, - "flex-basis" => 40, "flex-direction" => 41, "flex-flow" => 42, "flex-grow" => 43, - "flex-shrink" => 44, "flex-wrap" => 45, "font" => 46, "font-family" => 47, - "font-size" => 48, "font-style" => 49, "font-variant" => 50, "font-weight" => 51, - "gap" => 52, "grid" => 53, "grid-template-columns" => 54, "grid-template-rows" => 55, - "height" => 56, "inset" => 57, "justify-content" => 58, "left" => 59, - "letter-spacing" => 60, "line-height" => 61, "margin" => 62, "margin-bottom" => 63, - "margin-left" => 64, "margin-right" => 65, "margin-top" => 66, "max-height" => 67, - "max-width" => 68, "min-height" => 69, "min-width" => 70, "opacity" => 71, - "order" => 72, "overflow" => 73, "overflow-x" => 74, "overflow-y" => 75, - "padding" => 76, "padding-bottom" => 77, "padding-left" => 78, - "padding-right" => 79, "padding-top" => 80, "pointer-events" => 81, - "position" => 82, "right" => 83, "row-gap" => 84, "stroke" => 85, - "stroke-linecap" => 86, "stroke-linejoin" => 87, "stroke-width" => 88, - "text-align" => 89, "text-indent" => 90, "text-transform" => 91, "top" => 92, - "transform" => 93, "visibility" => 94, "white-space" => 95, "width" => 96, - "word-spacing" => 97, "z-index" => 98, "outline" => 99, "outline-color" => 100, - "outline-offset" => 101, "outline-style" => 102, "outline-width" => 103, - "grid-template-areas" => 104, - _ => -1 - }; - - return id >= 0 || s_ids.TryGetValue(name, out id); + if (CssGeneratedPropertyMetadata.TryGetKnownId(name, out id)) + { + return true; + } + + return s_ids.TryGetValue(name, out id); } } diff --git a/tests/WebPlatformSubset/capabilities.json b/tests/WebPlatformSubset/capabilities.json index 3e4bcd0cd..57a5067ad 100644 --- a/tests/WebPlatformSubset/capabilities.json +++ b/tests/WebPlatformSubset/capabilities.json @@ -167,11 +167,11 @@ }, { "family": "cssom-inline-declaration-model", - "required": ["supported CSS IDL properties remain distinct from JavaScript expandos", "invalid declaration assignment preserves the previous authored value", "empty-string assignment removes the declaration while preserving the style attribute", "disconnected computed style is empty while inline authored declarations remain readable", "case-sensitive custom-property names", "CSS-whitespace-only token trimming", "vertical-tab token preservation", "custom-property overwrite and importance", "empty custom properties excluded from named-property lookup", "computed unknown getPropertyValue empty-string fallback", "computed unsupported named-property absence", "computed custom properties remain method-only", "computed supported camel-case and kebab-case aliases", "computed snapshot connectivity identity", "detached computed style does not survive reattachment", "stylesheet recascade and box suppression after reattachment", "detached STYLE activation after connection", "padding shorthand retention and physical longhand override", "border shorthand, width expansion, style composition, and physical-side override", "content-box computed width and height distinct from border-box geometry", "padding declaration removal and synchronous layout invalidation", "four-value inset shorthand assignment", "flex-basis inline retention through connection and recascade", "font-relative computed-value serialization"], + "required": ["supported CSS IDL properties remain distinct from JavaScript expandos", "generated logical CSS properties share native and managed CSSOM exposure", "invalid declaration assignment preserves the previous authored value", "empty-string assignment removes the declaration while preserving the style attribute", "disconnected computed style is empty while inline authored declarations remain readable", "case-sensitive custom-property names", "CSS-whitespace-only token trimming", "vertical-tab token preservation", "custom-property overwrite and importance", "empty custom properties excluded from named-property lookup", "computed unknown getPropertyValue empty-string fallback", "computed unsupported named-property absence", "computed custom properties remain method-only", "computed supported camel-case and kebab-case aliases", "computed snapshot connectivity identity", "detached computed style does not survive reattachment", "stylesheet recascade and box suppression after reattachment", "detached STYLE activation after connection", "padding shorthand retention and physical longhand override", "border shorthand, width expansion, style composition, and physical-side override", "content-box computed width and height distinct from border-box geometry", "padding declaration removal and synchronous layout invalidation", "four-value inset shorthand assignment", "flex-basis inline retention through connection and recascade", "font-relative computed-value serialization"], "candidate": [], "productEvidence": ["jQuery 4.0.0 CSS feature detection and detached element construction", "future CSS-in-JS and component-library declaration probes"], "coverage": ["contracts/cssom-inline-declaration-validity.html", "contracts/css-custom-property-cssom-tokens.html", "contracts/cssom-padding-assignment.html", "contracts/cssom-border-assignment.html", "contracts/cssom-computed-style-named-properties.html", "contracts/css-detached-computed-style-reattach.html", "contracts/css-font-relative-box-used-values.html", "css/cssom/css-style-attribute-modifications.html", "css/css-variables/variable-definition.html", "tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs", "tests/WebScene.Backend.Avalonia.Tests/ComputedStyleReadOptimizationTests.cs", "test_detached_style_retains_text_and_activates_when_connected", "test_font_relative_box_lengths_follow_inherited_font_context", "artifacts/web-platform-required-custom-property-promoted-v1-20260723", "artifacts/web-platform-required-detached-computed-reattach-v1-20260723", "artifacts/web-platform-required-font-relative-promoted-v1-20260723"], - "gap": "Chrome and native pass the declaration-validity, custom-property CSSOM/lifecycle, padding/border box-edge, hidden-subtree dimension, z-index assignment/connection, computed named-property, detached snapshot lifecycle, and font-relative box contracts; the native runner also pass all 73/73 assertions in the unchanged pinned CSS Variables WPT. The enlarged required profile passes 104/104 documents and 414/414 assertions after the independent inline generated-content and baseline promotion. The box-edge, z-index, named-property, reattachment, and font-relative claims are deliberately bounded to the exercised shorthand, physical-side, content-box, geometry, integer/pixel serialization, inline precedence, connection, removal, supported/unsupported/custom access, connectivity transitions, and inherited-font mutation; complete border and stacking-context grammars or CSSStyleDeclaration enumeration are not implied. getPropertyPriority/setProperty priority argument coverage, registered custom properties, CSS Typed OM, complete grammar validation, and the remaining composed computed/used-value geometry are not claimed." + "gap": "Chrome and native pass the 5/5 declaration-validity checks, including generated logical-property named exposure, plus the custom-property CSSOM/lifecycle, padding/border box-edge, hidden-subtree dimension, z-index assignment/connection, computed named-property, detached snapshot lifecycle, and font-relative box contracts; the native runner also passes all 73/73 assertions in the unchanged pinned CSS Variables WPT. The enlarged required profile passes 104/104 documents and 414/414 assertions after the independent inline generated-content and baseline promotion. The box-edge, z-index, named-property, reattachment, and font-relative claims are deliberately bounded to the exercised shorthand, physical-side, content-box, geometry, integer/pixel serialization, inline precedence, connection, removal, supported/unsupported/custom access, connectivity transitions, and inherited-font mutation; complete border and stacking-context grammars or CSSStyleDeclaration enumeration are not implied. getPropertyPriority/setProperty priority argument coverage, registered custom properties, CSS Typed OM, complete grammar validation, and the remaining composed computed/used-value geometry are not claimed." }, { "family": "cascade-and-selectors", diff --git a/tests/WebPlatformSubset/contracts/cssom-inline-declaration-validity.html b/tests/WebPlatformSubset/contracts/cssom-inline-declaration-validity.html index 04224527a..7155665ff 100644 --- a/tests/WebPlatformSubset/contracts/cssom-inline-declaration-validity.html +++ b/tests/WebPlatformSubset/contracts/cssom-inline-declaration-validity.html @@ -42,6 +42,24 @@ "an expando leaked into the CSS declaration block"); }); + test("generated logical property metadata is exposed through CSSOM", () => { + const style = document.createElement("div").style; + const cases = [ + ["inset-block-end", "insetBlockEnd", "7px"], + ["border-block-width", "borderBlockWidth", "2px"], + ["border-inline-color", "borderInlineColor", "red"], + ["padding-block-start", "paddingBlockStart", "3px"], + ["margin-block-end", "marginBlockEnd", "4px"] + ]; + for (const [cssName, idlName, value] of cases) { + assert(cssName in style && idlName in style, + `${cssName}/${idlName} was absent from CSSStyleDeclaration`); + style[idlName] = value; + assert(style.getPropertyValue(cssName) === value, + `${idlName} did not update ${cssName}: ${style.getPropertyValue(cssName)}`); + } + }); + test("invalid declarations preserve the previous authored value", () => { const style = document.createElement("div").style; style.position = "absolute"; diff --git a/tests/WebPlatformSubset/webscene-component-profile.json b/tests/WebPlatformSubset/webscene-component-profile.json index ef09a2817..cd0977805 100644 --- a/tests/WebPlatformSubset/webscene-component-profile.json +++ b/tests/WebPlatformSubset/webscene-component-profile.json @@ -1021,9 +1021,9 @@ { "path": "contracts/cssom-inline-declaration-validity.html", "type": "contract", - "capabilities": ["cssstyledeclaration-supported-idl", "cssstyledeclaration-expandos", "invalid-declaration-retention", "empty-declaration-removal", "disconnected-resolved-style"], + "capabilities": ["cssstyledeclaration-supported-idl", "generated-logical-property-cssom-exposure", "cssstyledeclaration-expandos", "invalid-declaration-retention", "empty-declaration-removal", "disconnected-resolved-style"], "evidence": ["jquery-4.0.0-css-unit-reduction", "direct-chrome-neutral-contract", "managed-and-native-neutral-contract"], - "reason": "Required after the same 4/4 product-neutral assertions pass direct Chrome and rebuilt native. The contract bounds CSSStyleDeclaration feature detection, invalid-value retention, empty-value removal, expando isolation, and disconnected resolved-style behavior exposed by jQuery's unchanged css.js suite. Evidence: artifacts/web-platform-cssom-inline-validity-chrome-v1-20260723/results.json, artifacts/web-platform-cssom-inline-validity-managed-v2-20260723/results.json, and artifacts/web-platform-cssom-inline-validity-native-v1-20260723/results.json." + "reason": "Required after the same 5/5 product-neutral assertions pass Chrome 153 and rebuilt native with both current and legacy CSS parsers. The contract bounds CSSStyleDeclaration feature detection, generated logical-property exposure, invalid-value retention, empty-value removal, expando isolation, and disconnected resolved-style behavior exposed by jQuery's unchanged css.js suite. Evidence: artifacts/web-platform-cssom-inline-validity-chrome-v1-20260723/results.json, artifacts/web-platform-cssom-inline-validity-managed-v2-20260723/results.json, artifacts/web-platform-cssom-inline-validity-native-v1-20260723/results.json, and docs/validation/css-property-identity-20260917.md." }, { "path": "contracts/css-connected-used-value-geometry.html", diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index c2ec4a227..31a9047a0 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -32,6 +32,11 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) [InlineData("WebkitTransform")] [InlineData("grid-gap")] [InlineData("insetInlineStart")] + [InlineData("insetBlockEnd")] + [InlineData("borderBlockWidth")] + [InlineData("border-inline-color")] + [InlineData("paddingBlockStart")] + [InlineData("margin-block-end")] [InlineData("padding-inline-end")] [InlineData("borderStartEndRadius")] [InlineData("transitionTimingFunction")] @@ -46,6 +51,19 @@ public void DoesNotMisclassifyNonColorValueProperties(string propertyName) public void ExposesSupportedCssomProperties(string name) => Assert.True(CssPropertyCatalog.IsSupported(name)); + [Fact] + public void ExposesEveryGeneratedSupportedProperty() + { + Assert.Equal( + CssGeneratedPropertyMetadata.SupportedNames.Length, + CssGeneratedPropertyMetadata.SupportedNames.Distinct(StringComparer.OrdinalIgnoreCase).Count()); + + foreach (var name in CssGeneratedPropertyMetadata.SupportedNames) + { + Assert.True(CssPropertyCatalog.IsSupported(name), name); + } + } + [Theory] [InlineData("fakeProperty")] [InlineData("WebkitFakeProperty")] diff --git a/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs b/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs index 6ad4ab92d..95fb37a4c 100644 --- a/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs @@ -7,6 +7,9 @@ public sealed class CssPropertyStorageTests [Fact] public void KnownPropertyCatalogHasStableCaseInsensitiveIds() { + Assert.Equal(105, CssKnownProperties.Count); + Assert.Equal(CssGeneratedPropertyMetadata.KnownNames, CssKnownProperties.Names); + for (var index = 0; index < CssKnownProperties.Names.Length; index++) { var name = CssKnownProperties.Names[index]; diff --git a/tools/generate_css_property_metadata.py b/tools/generate_css_property_metadata.py new file mode 100644 index 000000000..e7092faa9 --- /dev/null +++ b/tools/generate_css_property_metadata.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Generate native and managed CSS metadata from the checked-in JSON catalog.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +DEFAULT_INPUT = ROOT / "experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json" +DEFAULT_OUTPUT = ROOT / "experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_metadata.inc" +DEFAULT_NATIVE_SUPPORTED_OUTPUT = ( + ROOT / "experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc") +DEFAULT_MANAGED_OUTPUT = ROOT / "src/WebScene.Css/CssPropertyMetadata.Generated.cs" +ALLOWED_MASKS = { + "inline_top", "inline_right", "inline_bottom", "inline_left", + "inline_margin", "inline_padding", "inline_border", "inline_gap", "inline_overflow", +} + + +class Catalog: + def __init__( + self, + properties: list[dict[str, object]], + managed_known_properties: list[str], + supported_property_extras: list[str], + ) -> None: + self.properties = properties + self.managed_known_properties = managed_known_properties + self.supported_property_extras = supported_property_extras + + +def _load_name_list(payload: dict[str, object], key: str, *, allow_empty: bool = False) -> list[str]: + values = payload.get(key) + if not isinstance(values, list) or (not values and not allow_empty): + qualifier = "an array" if allow_empty else "a non-empty array" + raise ValueError(f"{key} must be {qualifier}") + + names: list[str] = [] + seen: set[str] = set() + for value in values: + if not isinstance(value, str) or not value or value.lower() != value: + raise ValueError(f"{key} name must be lower-case: {value!r}") + if value in seen: + raise ValueError(f"duplicate {key} name: {value}") + seen.add(value) + names.append(value) + return names + + +def load_catalog(path: Path) -> Catalog: + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError("CSS property metadata must be an object") + if payload.get("version") != 1: + raise ValueError("css property metadata version must be 1") + managed_known_properties = _load_name_list(payload, "managedKnownProperties") + supported_property_extras = _load_name_list( + payload, "supportedPropertyExtras", allow_empty=True) + entries = payload.get("properties") + if not isinstance(entries, list) or not entries: + raise ValueError("properties must be a non-empty array") + names: set[str] = set() + known_names = {entry.get("name") for entry in entries if isinstance(entry, dict)} + for entry in entries: + if not isinstance(entry, dict): + raise ValueError("each property must be an object") + name = entry.get("name") + if not isinstance(name, str) or not name or name.lower() != name: + raise ValueError(f"property name must be lower-case: {name!r}") + if name in names: + raise ValueError(f"duplicate property: {name}") + names.add(name) + masks = entry.get("mask") + if not isinstance(masks, list) or not masks or not all(mask in ALLOWED_MASKS for mask in masks): + raise ValueError(f"invalid mask list for {name}: {masks!r}") + alias = entry.get("alias") + expansion = entry.get("expansion") + apply_expansion = entry.get("applyExpansion", False) + if not isinstance(apply_expansion, bool): + raise ValueError(f"applyExpansion for {name} must be a boolean") + if alias is not None and expansion is not None: + raise ValueError(f"{name} cannot define both alias and expansion") + targets = [alias] if alias is not None else expansion or [] + if alias is not None and not isinstance(alias, str): + raise ValueError(f"alias for {name} must be a string") + if expansion is not None and (not isinstance(expansion, list) or len(expansion) not in (2, 4)): + raise ValueError(f"expansion for {name} must have two or four longhands") + if apply_expansion and expansion is None: + raise ValueError(f"{name} applies expansion without defining an expansion") + for target in targets: + if target not in known_names: + raise ValueError(f"{name} references unknown effective property {target!r}") + return Catalog( + properties=sorted(entries, key=lambda entry: entry["name"]), + managed_known_properties=managed_known_properties, + supported_property_extras=supported_property_extras, + ) + + +def generate_native(catalog: Catalog) -> str: + lines = [ + "// Generated by tools/generate_css_property_metadata.py. Do not edit.", + "inline constexpr std::array effective_property_metadata_catalog{", + ] + for entry in catalog.properties: + targets = ([entry["alias"]] if "alias" in entry else entry.get("expansion", [])) + padded = list(targets) + [""] * (4 - len(targets)) + mask = " | ".join(entry["mask"]) + quoted = ", ".join(f'"{target}"' for target in padded) + apply_expansion = "true" if entry.get("applyExpansion", False) else "false" + lines.append( + f' effective_property_metadata{{"{entry["name"]}", {mask}, ' + f'{{{quoted}}}, {len(targets)}U, {apply_expansion}}},' + ) + lines.extend(["};", ""]) + return "\n".join(lines) + + +def supported_names(catalog: Catalog) -> list[str]: + return sorted(set( + catalog.managed_known_properties + + catalog.supported_property_extras + + [entry["name"] for entry in catalog.properties] + )) + + +def css_idl_name(name: str) -> str: + if name == "float": + return "cssFloat" + parts = name.split("-") + return parts[0] + "".join(part[:1].upper() + part[1:] for part in parts[1:]) + + +def generate_native_supported(catalog: Catalog) -> str: + lines = [ + "// Generated by tools/generate_css_property_metadata.py. Do not edit.", + "struct cssom_supported_property_metadata final {", + " std::string_view css_name;", + " std::string_view idl_name;", + "};", + "", + "inline constexpr std::array cssom_supported_property_catalog{", + ] + for name in supported_names(catalog): + lines.append( + f' cssom_supported_property_metadata{{"{name}", "{css_idl_name(name)}"}},') + lines.extend(["};", ""]) + return "\n".join(lines) + + +def _append_csharp_array(lines: list[str], name: str, values: list[str]) -> None: + lines.extend([ + f" internal static readonly string[] {name} =", + " [", + ]) + for value in values: + lines.append(f' "{value}",') + lines.extend([" ];", ""]) + + +def generate_managed(catalog: Catalog) -> str: + lines = [ + "// Generated by tools/generate_css_property_metadata.py. Do not edit.", + "namespace WebScene.Css;", + "", + "internal static class CssGeneratedPropertyMetadata", + "{", + ] + _append_csharp_array(lines, "KnownNames", catalog.managed_known_properties) + _append_csharp_array(lines, "SupportedNames", supported_names(catalog)) + lines.extend([ + " internal static bool TryGetKnownId(string name, out int id)", + " {", + " id = name switch", + " {", + ]) + for index, name in enumerate(catalog.managed_known_properties): + lines.append(f' "{name}" => {index},') + lines.extend([ + " _ => -1,", + " };", + " return id >= 0;", + " }", + "}", + "", + ]) + return "\n".join(lines) + + +# Retain the original helper name for callers that only generate the native table. +def generate(catalog: Catalog) -> str: + return generate_native(catalog) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--input", type=Path, default=DEFAULT_INPUT) + parser.add_argument("--output", type=Path, default=DEFAULT_OUTPUT) + parser.add_argument( + "--native-supported-output", type=Path, default=DEFAULT_NATIVE_SUPPORTED_OUTPUT) + parser.add_argument("--managed-output", type=Path, default=DEFAULT_MANAGED_OUTPUT) + parser.add_argument("--check", action="store_true") + args = parser.parse_args() + catalog = load_catalog(args.input) + rendered_native = generate_native(catalog) + rendered_native_supported = generate_native_supported(catalog) + rendered_managed = generate_managed(catalog) + if args.check: + stale: list[Path] = [] + if not args.output.exists() or args.output.read_text(encoding="utf-8") != rendered_native: + stale.append(args.output) + if (not args.native_supported_output.exists() + or args.native_supported_output.read_text(encoding="utf-8") + != rendered_native_supported): + stale.append(args.native_supported_output) + if (not args.managed_output.exists() + or args.managed_output.read_text(encoding="utf-8") != rendered_managed): + stale.append(args.managed_output) + if stale: + joined = ", ".join(str(path) for path in stale) + raise SystemExit(f"stale generated CSS property metadata: {joined}") + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered_native, encoding="utf-8") + args.native_supported_output.parent.mkdir(parents=True, exist_ok=True) + args.native_supported_output.write_text(rendered_native_supported, encoding="utf-8") + args.managed_output.parent.mkdir(parents=True, exist_ok=True) + args.managed_output.write_text(rendered_managed, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())