diff --git a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json index 19817e24a..10f8a3f4c 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json +++ b/experiments/WebScene.NativeEngine.Probe/native/css_property_metadata.json @@ -157,7 +157,8 @@ "column-count", "columns", "css-float", "direction", "empty-cells", "fill-opacity", "fill-rule", "filter", "font-stretch", "font-style", "font-variant", "grid", "grid-template-areas", "inset-block", "inset-inline", - "mask-composite", "mask-image", "mask-position", "mask-repeat", "mask-size", + "-webkit-mask", "-webkit-mask-composite", "-webkit-mask-image", "-webkit-mask-position", "-webkit-mask-repeat", "-webkit-mask-size", + "mask", "mask-composite", "mask-image", "mask-mode", "mask-position", "mask-repeat", "mask-size", "moz-transform", "order", "orphans", "outline-offset", "outline-style", "resize", "stroke-linecap", "stroke-linejoin", "text-decoration", "text-indent", "text-overflow", "webkit-transform", "widows", "zoom" @@ -245,7 +246,8 @@ "grid-area", "grid-auto-columns", "grid-auto-flow", "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", - "list-style", "list-style-position", "list-style-type", "mask-composite", "mask-image", "mask-position", "mask-repeat", "mask-size", "padding-block", + "list-style", "list-style-position", "list-style-type", "-webkit-mask", "-webkit-mask-composite", "-webkit-mask-image", "-webkit-mask-position", "-webkit-mask-repeat", "-webkit-mask-size", + "mask", "mask-composite", "mask-image", "mask-mode", "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", diff --git a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc index 760c582c1..83c0f1923 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_property_identity.inc @@ -209,7 +209,7 @@ inline constexpr std::array native_typed_property_identity_catalog{ native_typed_property_identity{"contain-intrinsic-size", css_property_id::contain_intrinsic_size}, }; -inline constexpr std::array native_storage_only_property_catalog{ +inline constexpr std::array native_storage_only_property_catalog{ "accent-color", "align-content", "animation-direction", @@ -245,8 +245,16 @@ inline constexpr std::array native_storage_only_property_c "grid-template-areas", "inset-block", "inset-inline", + "-webkit-mask", + "-webkit-mask-composite", + "-webkit-mask-image", + "-webkit-mask-position", + "-webkit-mask-repeat", + "-webkit-mask-size", + "mask", "mask-composite", "mask-image", + "mask-mode", "mask-position", "mask-repeat", "mask-size", 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 index d8ad4878b..6bce8fe94 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/generated/webscene_css_supported_properties.inc @@ -7,6 +7,12 @@ struct cssom_supported_property_metadata final { inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"-moz-transform", "MozTransform"}, cssom_supported_property_metadata{"-webkit-font-smoothing", "WebkitFontSmoothing"}, + cssom_supported_property_metadata{"-webkit-mask", "WebkitMask"}, + cssom_supported_property_metadata{"-webkit-mask-composite", "WebkitMaskComposite"}, + cssom_supported_property_metadata{"-webkit-mask-image", "WebkitMaskImage"}, + cssom_supported_property_metadata{"-webkit-mask-position", "WebkitMaskPosition"}, + cssom_supported_property_metadata{"-webkit-mask-repeat", "WebkitMaskRepeat"}, + cssom_supported_property_metadata{"-webkit-mask-size", "WebkitMaskSize"}, cssom_supported_property_metadata{"-webkit-transform", "WebkitTransform"}, cssom_supported_property_metadata{"accent-color", "accentColor"}, cssom_supported_property_metadata{"align-content", "alignContent"}, @@ -168,8 +174,10 @@ inline constexpr std::array cssom_supported_property_catalog{ 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", "mask"}, cssom_supported_property_metadata{"mask-composite", "maskComposite"}, cssom_supported_property_metadata{"mask-image", "maskImage"}, + cssom_supported_property_metadata{"mask-mode", "maskMode"}, cssom_supported_property_metadata{"mask-position", "maskPosition"}, cssom_supported_property_metadata{"mask-repeat", "maskRepeat"}, cssom_supported_property_metadata{"mask-size", "maskSize"}, @@ -238,4 +246,4 @@ inline constexpr std::array cssom_supported_property_catalog{ cssom_supported_property_metadata{"zoom", "zoom"}, }; -inline constexpr auto cssom_style_template_property_accessor_count = 420U; +inline constexpr auto cssom_style_template_property_accessor_count = 435U; diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h index 8d8baabc2..40f8ae3c2 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_application.h @@ -19,6 +19,39 @@ void apply_resolved_declaration(native_document& document,dom_node& node, bool defer_transition_configuration = false) { const auto& name=declaration.name; + if (name == "mask") { + const auto parsed = parse_single_mask_shorthand(value); + if (!parsed.has_value()) { + decision.classification = "unsupported"; + decision.semantic_slice = "single mask layer without geometry-box syntax"; + return; + } + const std::array, 6> components{{ + {"mask-image", &parsed->image}, + {"mask-position", &parsed->position}, + {"mask-size", &parsed->size}, + {"mask-repeat", &parsed->repeat}, + {"mask-composite", &parsed->composite}, + {"mask-mode", &parsed->mode}, + }}; + for (const auto& [component_name, component_value] : components) { + css_declaration component{ + std::string(component_name), *component_value, declaration.important}; + component.property = property_id(component.name); + component.specified = compile_specified_value( + component.property, component.value); + apply_resolved_declaration( + document, + node, + component, + component.value, + inline_origin, + decision, + load_svg, + defer_transition_configuration); + } + return; + } const auto may_require_application_expansion = name.starts_with("border-") || name.starts_with("inset-"); const auto* effective_metadata = may_require_application_expansion diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_declarations.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_declarations.h index 8790b40da..e6ba2345f 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_declarations.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_declarations.h @@ -1,19 +1,16 @@ #pragma once #include "webscene_css_state.h" #include "webscene_css_parser.h" +#include "webscene_css_effect_values.h" namespace webscene_native::css { -inline std::string ascii_lower(std::string_view value) { - auto result=std::string(value); - for (auto &c:result) if(c>='A' && c<='Z') c=static_cast(c+('a'-'A')); - return result; -} inline bool valid_custom_property_name(std::string_view name) { if (!name.starts_with("--") || name.size()<=2) return false; return std::none_of(name.begin(),name.end(),[](unsigned char c) { return c<=0x20 || c==0x7f; }); } + inline void append_declaration( std::vector& result, std::string_view raw_name, @@ -27,16 +24,39 @@ inline void append_declaration( auto value = std::string(raw_value); const auto custom = name.starts_with("--"); if (custom && !valid_custom_property_name(name)) return; + if (name == "-webkit-mask") name = "mask"; + else if (name == "-webkit-mask-image") name = "mask-image"; + else if (name == "-webkit-mask-position") name = "mask-position"; + else if (name == "-webkit-mask-size") name = "mask-size"; + else if (name == "-webkit-mask-repeat") name = "mask-repeat"; + else if (name == "-webkit-mask-composite") name = "mask-composite"; if (!custom && name.starts_with("-") && name != "-moz-transform" && name != "-webkit-transform" && name != "-webkit-font-smoothing") return; if (custom && value.empty() && preserve_empty_custom_properties) value = " "; - if (!name.empty() && !value.empty()) { - css_declaration declaration{std::move(name), std::move(value), important}; + const auto append_one = [&](std::string property_name, std::string property_value) { + css_declaration declaration{ + std::move(property_name), std::move(property_value), important}; declaration.property = property_id(declaration.name); - declaration.specified = compile_specified_value(declaration.property, declaration.value); + declaration.specified = compile_specified_value( + declaration.property, declaration.value); result.push_back(std::move(declaration)); + }; + if (name == "mask" && !value.empty()) { + const auto parsed = parse_single_mask_shorthand(value); + if (!parsed.has_value()) { + append_one(std::move(name), std::move(value)); + return; + } + append_one("mask-image", parsed->image); + append_one("mask-position", parsed->position); + append_one("mask-size", parsed->size); + append_one("mask-repeat", parsed->repeat); + append_one("mask-composite", parsed->composite); + append_one("mask-mode", parsed->mode); + } else if (!name.empty() && !value.empty()) { + append_one(std::move(name), std::move(value)); } } diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_effect_values.h b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_effect_values.h index e4d90a7d5..959df6167 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_css_effect_values.h +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_css_effect_values.h @@ -6,13 +6,26 @@ #include #include #include +#include namespace webscene_native::css { +inline std::string ascii_lower(std::string_view value) +{ + auto result = std::string(value); + for (auto& character : result) { + if (character >= 'A' && character <= 'Z') { + character = static_cast(character + ('a' - 'A')); + } + } + return result; +} + inline bool is_effect_property(std::string_view name) noexcept { return name == "mask-image" || name == "mask-size" || name == "mask-position" - || name == "mask-repeat" || name == "mask-composite" || name == "clip-path" + || name == "mask-repeat" || name == "mask-composite" || name == "mask-mode" + || name == "clip-path" || name == "filter" || name == "backdrop-filter"; } @@ -22,6 +35,7 @@ inline std::string_view effect_initial_value(std::string_view name) noexcept if (name == "mask-position") return "0% 0%"; if (name == "mask-repeat") return "repeat"; if (name == "mask-composite") return "add"; + if (name == "mask-mode") return "match-source"; return "none"; } @@ -111,6 +125,171 @@ inline bool valid_effect_keyword_list( return found; } +struct parsed_mask_shorthand final { + std::string image{"none"}; + std::string position{"0% 0%"}; + std::string size{"auto"}; + std::string repeat{"repeat"}; + std::string composite{"add"}; + std::string mode{"match-source"}; +}; + +inline std::optional parse_single_mask_shorthand( + std::string_view authored) +{ + const auto trim = [](std::string_view value) { + while (!value.empty() + && std::isspace(static_cast(value.front()))) { + value.remove_prefix(1U); + } + while (!value.empty() + && std::isspace(static_cast(value.back()))) { + value.remove_suffix(1U); + } + return value; + }; + authored = trim(authored); + if (authored.empty()) return std::nullopt; + const auto lowered = ascii_lower(authored); + if (lowered.starts_with("var(") && lowered.ends_with(')')) { + auto depth = 0; + for (size_t index = 0U; index < lowered.size(); ++index) { + if (lowered[index] == '(') ++depth; + else if (lowered[index] == ')') { + if (depth == 0) return std::nullopt; + --depth; + if (depth == 0 && index != lowered.size() - 1U) { + depth = 1; + break; + } + } + } + if (depth == 0) return std::nullopt; + } + if (lowered == "initial" || lowered == "inherit" || lowered == "unset" + || lowered == "revert" || lowered == "revert-layer") { + parsed_mask_shorthand result; + result.image = result.position = result.size = result.repeat = + result.composite = result.mode = lowered; + return result; + } + if (lowered == "none") return parsed_mask_shorthand{}; + + std::vector tokens; + auto start = std::string_view::npos; + auto depth = 0U; + char quote = 0; + for (size_t index = 0U; index <= authored.size(); ++index) { + const auto at_end = index == authored.size(); + const auto character = at_end ? ' ' : authored[index]; + if (quote != 0) { + if (!at_end && character == '\\' && index + 1U < authored.size()) ++index; + else if (!at_end && character == quote) quote = 0; + } else if (!at_end && (character == '\'' || character == '"')) { + quote = character; + } else if (!at_end && character == '(') { + ++depth; + } else if (!at_end && character == ')' && depth != 0U) { + --depth; + } else if (!at_end && character == ',' && depth == 0U) { + return std::nullopt; + } + const auto separator = depth == 0U && quote == 0 + && (at_end || std::isspace(static_cast(character)) + || character == '/'); + if (!separator && start == std::string_view::npos) start = index; + if (separator && start != std::string_view::npos) { + tokens.emplace_back(authored.substr(start, index - start)); + start = std::string_view::npos; + } + if (!at_end && depth == 0U && quote == 0 && character == '/') { + tokens.emplace_back("/"); + } + } + if (depth != 0U || quote != 0 || tokens.empty()) return std::nullopt; + + parsed_mask_shorthand result; + std::vector position; + std::vector size; + auto after_slash = false; + auto has_image = false; + for (const auto& token : tokens) { + const auto lower = ascii_lower(token); + if (token == "/") { + if (after_slash || position.empty()) return std::nullopt; + after_slash = true; + continue; + } + if (lower.starts_with("url(") + || lower.starts_with("linear-gradient(") + || lower.starts_with("radial-gradient(") + || lower.starts_with("repeating-linear-gradient(") + || lower.starts_with("repeating-radial-gradient(")) { + if (has_image || after_slash) return std::nullopt; + result.image = token; + has_image = true; + continue; + } + if (lower == "none") { + if (has_image || after_slash) return std::nullopt; + result.image = lower; + has_image = true; + continue; + } + if (lower == "no-repeat" || lower == "repeat" || lower == "repeat-x" + || lower == "repeat-y") { + if (after_slash || result.repeat != "repeat") return std::nullopt; + result.repeat = lower; + continue; + } + if (lower == "add" || lower == "exclude" || lower == "intersect" + || lower == "subtract") { + if (after_slash || result.composite != "add") return std::nullopt; + result.composite = lower; + continue; + } + if (lower == "alpha" || lower == "luminance" || lower == "match-source") { + if (after_slash || result.mode != "match-source") return std::nullopt; + result.mode = lower; + continue; + } + if (lower == "border-box" || lower == "padding-box" + || lower == "content-box" || lower == "fill-box" + || lower == "stroke-box" || lower == "view-box" || lower == "no-clip") { + return std::nullopt; + } + const auto numeric = !lower.empty() + && (std::isdigit(static_cast(lower.front())) + || lower.front() == '.' || lower.front() == '-' + || lower.front() == '+'); + const auto functional_length = lower.starts_with("var(") + || lower.starts_with("calc(") || lower.starts_with("min(") + || lower.starts_with("max(") || lower.starts_with("clamp("); + const auto position_keyword = lower == "left" || lower == "right" + || lower == "top" || lower == "bottom" || lower == "center"; + const auto size_keyword = lower == "auto" || lower == "contain" + || lower == "cover"; + if (after_slash + ? !numeric && !functional_length && !size_keyword + : !numeric && !functional_length && !position_keyword) { + return std::nullopt; + } + auto& geometry = after_slash ? size : position; + if (geometry.size() == 2U) return std::nullopt; + geometry.push_back(token); + } + if (after_slash && size.empty()) return std::nullopt; + if (!position.empty()) { + result.position = position.front(); + if (position.size() > 1U) result.position += " " + position[1]; + } + if (!size.empty()) { + result.size = size.front(); + if (size.size() > 1U) result.size += " " + size[1]; + } + return result; +} + inline std::optional normalize_effect_value( std::string_view name, std::string_view input) @@ -124,7 +303,11 @@ inline std::optional normalize_effect_value( }); if (normalized == "initial" || normalized == "unset" || normalized == "revert" || normalized == "revert-layer") return std::string(effect_initial_value(name)); - if (normalized == "inherit" || normalized == "none") return normalized; + if (normalized == "inherit") return normalized; + if (normalized == "none") { + if (name == "mask-mode") return std::nullopt; + return normalized; + } if (name == "filter" || name == "backdrop-filter") { return contains_only_named_functions(normalized, { @@ -152,6 +335,10 @@ inline std::optional normalize_effect_value( return valid_effect_keyword_list(normalized, {"add", "exclude", "intersect", "subtract"}) ? std::optional(normalized) : std::nullopt; } + if (name == "mask-mode") { + return valid_effect_keyword_list(normalized, {"alpha", "luminance", "match-source"}) + ? std::optional(normalized) : std::nullopt; + } if (name == "mask-size") { if (normalized.find(';') != std::string::npos) return std::nullopt; return normalized.find_first_of("0123456789") != std::string::npos diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_css_effect_values_tests.cpp b/experiments/WebScene.NativeEngine.Probe/tests/native_css_effect_values_tests.cpp index 5d404adc4..34fcbb55c 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_css_effect_values_tests.cpp +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_css_effect_values_tests.cpp @@ -268,8 +268,8 @@ int main() rules.textContent = ` #effects > span { display: block; width: 8px; height: 2px; background: rgb(40, 80, 120); - mask-image: linear-gradient(black, transparent); mask-size: 8px 2px; - mask-position: 0px 0px; mask-repeat: no-repeat; mask-composite: add; + -webkit-mask: linear-gradient(black, transparent) no-repeat 0px 0px / 8px 2px; + mask: linear-gradient(black, transparent) no-repeat 0px 0px / 8px 2px; clip-path: inset(0px 1px); filter: brightness(0.5); backdrop-filter: blur(1px); } #effects.alternate > span { clip-path: circle(25%); filter: contrast(2); } #effects > span:first-child { transform: scale(1.25) rotate(3deg); } @@ -293,6 +293,10 @@ int main() if (style.getPropertyValue('filter') !== 'brightness(0.5)' || style.getPropertyValue('clip-path') !== 'inset(0px 1px)' || style.getPropertyValue('mask-repeat') !== 'no-repeat' + || style.getPropertyValue('mask-position') !== '0px 0px' + || style.getPropertyValue('mask-size') !== '8px 2px' + || style.getPropertyValue('mask-composite') !== 'add' + || style.getPropertyValue('mask-mode') !== 'match-source' || first.offsetWidth !== 8 || first.offsetHeight !== 2) { throw new Error('initial effect values failed'); } @@ -414,8 +418,7 @@ int main() rules.textContent = ` #effects > span { display: block; width: 8px; height: 2px; background: rgb(40, 80, 120); - mask-image: linear-gradient(black, transparent); mask-size: 8px 2px; - mask-position: 0px 0px; mask-repeat: no-repeat; mask-composite: add; + mask: linear-gradient(black, transparent) no-repeat 0px 0px / 8px 2px; clip-path: inset(0px 1px); filter: brightness(0.5); backdrop-filter: blur(1px); } #effects.alternate > span { clip-path: circle(25%); filter: contrast(2); } `; diff --git a/src/WebScene.Css/CssPropertyCatalog.cs b/src/WebScene.Css/CssPropertyCatalog.cs index a2a9c8a3e..aefda2de5 100644 --- a/src/WebScene.Css/CssPropertyCatalog.cs +++ b/src/WebScene.Css/CssPropertyCatalog.cs @@ -92,6 +92,8 @@ public static bool IsValidCssomValue(string propertyName, string value) "no-repeat", "repeat", "repeat-x", "repeat-y", "round", "space"), "mask-composite" => HasOnlyKeywords(normalizedValue, "add", "exclude", "intersect", "subtract"), + "mask-mode" => HasOnlyKeywords(normalizedValue, + "alpha", "luminance", "match-source"), "mask-size" => normalizedValue.IndexOfAny(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) >= 0 || HasOnlyKeywords(normalizedValue, "auto", "contain", "cover"), "mask-position" => normalizedValue.IndexOfAny(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) >= 0 diff --git a/src/WebScene.Css/CssPropertyMetadata.Generated.cs b/src/WebScene.Css/CssPropertyMetadata.Generated.cs index b3527db8d..e6f525df1 100644 --- a/src/WebScene.Css/CssPropertyMetadata.Generated.cs +++ b/src/WebScene.Css/CssPropertyMetadata.Generated.cs @@ -117,6 +117,12 @@ internal static class CssGeneratedPropertyMetadata [ "-moz-transform", "-webkit-font-smoothing", + "-webkit-mask", + "-webkit-mask-composite", + "-webkit-mask-image", + "-webkit-mask-position", + "-webkit-mask-repeat", + "-webkit-mask-size", "-webkit-transform", "accent-color", "align-content", @@ -278,8 +284,10 @@ internal static class CssGeneratedPropertyMetadata "margin-left", "margin-right", "margin-top", + "mask", "mask-composite", "mask-image", + "mask-mode", "mask-position", "mask-repeat", "mask-size", diff --git a/tests/WebPlatformSubset/contracts/css-retained-effect-values.html b/tests/WebPlatformSubset/contracts/css-retained-effect-values.html index 2a648b540..806ba2664 100644 --- a/tests/WebPlatformSubset/contracts/css-retained-effect-values.html +++ b/tests/WebPlatformSubset/contracts/css-retained-effect-values.html @@ -9,11 +9,8 @@ #effect { width: 120px; height: 80px; - mask-image: linear-gradient(black, transparent); - mask-size: 20px 30px; - mask-position: 4px 6px; - mask-repeat: no-repeat; - mask-composite: add; + -webkit-mask: linear-gradient(black, transparent) no-repeat 4px 6px / 20px 30px; + mask: linear-gradient(black, transparent) no-repeat 4px 6px / 20px 30px; clip-path: inset(1px 2px 3px 4px); filter: brightness(0.5) grayscale(1); backdrop-filter: blur(3px); @@ -36,6 +33,7 @@ assert_equals(style.getPropertyValue('mask-position'), '0% 0%'); assert_equals(style.getPropertyValue('mask-repeat'), 'repeat'); assert_equals(style.getPropertyValue('mask-composite'), 'add'); + assert_equals(style.getPropertyValue('mask-mode'), 'match-source'); assert_equals(style.getPropertyValue('clip-path'), 'none'); assert_equals(style.getPropertyValue('filter'), 'none'); assert_equals(style.getPropertyValue('backdrop-filter'), 'none'); @@ -61,6 +59,17 @@ assert_equals(style.getPropertyValue('mask-composite'), 'add'); }, 'mask repeat and composite expose computed values'); + test(() => { + effect.style.setProperty('mask', + 'linear-gradient(black, transparent) no-repeat center / 40px 30px'); + const style = getComputedStyle(effect); + assert_regexp_match(style.getPropertyValue('mask-image'), /^linear-gradient\(/); + assert_equals(style.getPropertyValue('mask-position'), 'center'); + assert_equals(style.getPropertyValue('mask-size'), '40px 30px'); + assert_equals(style.getPropertyValue('mask-repeat'), 'no-repeat'); + effect.style.removeProperty('mask'); + }, 'single-layer mask shorthand expands into effective retained longhands'); + test(() => { assert_equals(getComputedStyle(effect).getPropertyValue('clip-path'), 'inset(1px 2px 3px 4px)'); diff --git a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs index 31a9047a0..5e242828d 100644 --- a/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyCatalogTests.cs @@ -85,6 +85,10 @@ public void DoesNotExposeUnknownOrCustomPropertiesAsIdlAttributes(string name) [InlineData("color-scheme", "sepia", false)] [InlineData("accent-color", "auto", true)] [InlineData("mask-image", "linear-gradient(black, transparent)", true)] + [InlineData("mask", "url(icon.svg) no-repeat center / 16px 16px", true)] + [InlineData("mask-mode", "match-source", true)] + [InlineData("mask-mode", "luminance", true)] + [InlineData("mask-mode", "none", false)] [InlineData("mask-repeat", "no-repeat", true)] [InlineData("mask-repeat", "bounce", false)] [InlineData("clip-path", "inset(1px 2px)", true)] diff --git a/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs b/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs index 95fb37a4c..865614efd 100644 --- a/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs +++ b/tests/WebScene.Css.Tests/CssPropertyStorageTests.cs @@ -7,7 +7,7 @@ public sealed class CssPropertyStorageTests [Fact] public void KnownPropertyCatalogHasStableCaseInsensitiveIds() { - Assert.Equal(105, CssKnownProperties.Count); + Assert.Equal(106, CssKnownProperties.Count); Assert.Equal(CssGeneratedPropertyMetadata.KnownNames, CssKnownProperties.Names); for (var index = 0; index < CssKnownProperties.Names.Length; index++)