commit 0ab0e71fbce04e7cba91c7a372df5251821a44cd
parent 99b0d82c4a92a28d894d4106cb03b8bc514b9313
Author: Swarup Ukil <sukil@mozilla.com>
Date: Fri, 9 Jan 2026 20:11:44 +0000
Bug 1998245 - Implement support for <number> and <attr-unit>. r=emilio,firefox-style-system-reviewers
Differential Revision: https://phabricator.services.mozilla.com/D277823
Diffstat:
13 files changed, 144 insertions(+), 151 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -6694,6 +6694,8 @@ dependencies = [
"smallvec",
"static_assertions",
"static_prefs",
+ "strum",
+ "strum_macros",
"style_derive",
"style_traits",
"thin-vec",
diff --git a/servo/components/style/Cargo.toml b/servo/components/style/Cargo.toml
@@ -89,6 +89,8 @@ smallvec = "1.0"
static_assertions = "1.1"
static_prefs = { path = "../../../modules/libpref/init/static_prefs" }
string_cache = { version = "0.8", optional = true }
+strum = "0.27"
+strum_macros = "0.27"
style_derive = {path = "../style_derive"}
style_traits = {path = "../style_traits"}
to_shmem = {path = "../to_shmem"}
diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs
@@ -27,6 +27,7 @@ use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet};
use crate::stylesheets::UrlExtraData;
use crate::stylist::Stylist;
use crate::values::computed::{self, ToComputedValue};
+use crate::values::generics::calc::SortKey as AttrUnit;
use crate::values::specified::FontRelativeLength;
use crate::values::AtomIdent;
use crate::Atom;
@@ -476,8 +477,9 @@ enum SubstitutionFunctionKind {
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, Parse)]
enum AttributeType {
None,
+ RawString,
Type(Descriptor),
- Unit,
+ Unit(AttrUnit),
}
#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
@@ -841,18 +843,12 @@ fn parse_declaration_value_block<'i, 't>(
name.as_ref()
});
- let mut attribute_syntax = AttributeType::None;
- if substitution_kind == SubstitutionFunctionKind::Attr
- && input
- .try_parse(|input| input.expect_function_matching("type"))
- .is_ok()
- {
- // TODO(descalante): determine what to do for `type(garbage)` bug 2006626
- attribute_syntax = input
- .parse_nested_block(Descriptor::from_css_parser)
- .ok()
- .map_or(AttributeType::None, AttributeType::Type);
- }
+ let attribute_syntax =
+ if substitution_kind == SubstitutionFunctionKind::Attr {
+ parse_attr_type(input)
+ } else {
+ AttributeType::None
+ };
// We want the order of the references to match source order. So we need to reserve our slot
// now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since
@@ -972,6 +968,32 @@ fn parse_declaration_value_block<'i, 't>(
Ok((first_token_type, last_token_type))
}
+/// Parse <attr-type> = type( <syntax> ) | raw-string | number | <attr-unit>.
+/// https://drafts.csswg.org/css-values-5/#attr-notation
+fn parse_attr_type<'i, 't>(input: &mut Parser<'i, 't>) -> AttributeType {
+ input
+ .try_parse(|input| {
+ Ok(match input.next()? {
+ Token::Function(ref name) if name.eq_ignore_ascii_case("type") => {
+ AttributeType::Type(input.parse_nested_block(Descriptor::from_css_parser)?)
+ },
+ Token::Ident(ref ident) => {
+ if ident.eq_ignore_ascii_case("raw-string") {
+ AttributeType::RawString
+ } else {
+ let unit = AttrUnit::from_ident(ident).map_err(|_| {
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ })?;
+ AttributeType::Unit(unit)
+ }
+ },
+ Token::Delim('%') => AttributeType::Unit(AttrUnit::Percentage),
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ })
+ })
+ .unwrap_or(AttributeType::None)
+}
+
/// A struct that takes care of encapsulating the cascade process for custom properties.
pub struct CustomPropertiesBuilder<'a, 'b: 'a> {
seen: PrecomputedHashSet<&'a Name>,
@@ -2149,6 +2171,13 @@ fn substitute_one_reference<'a>(
references: &mut std::iter::Peekable<std::slice::Iter<SubstitutionFunctionReference>>,
attr_provider: &dyn AttributeProvider,
) -> Result<Substitution<'a>, ()> {
+ let simple_subst = |s: &str| {
+ Some(Substitution::new(
+ Cow::Owned(quoted_css_string(s)),
+ TokenSerializationType::Nothing,
+ TokenSerializationType::Nothing,
+ ))
+ };
let substitution: Option<_> = match reference.substitution_kind {
SubstitutionFunctionKind::Var => {
let registration = stylist.get_custom_property_registration(&reference.name);
@@ -2163,27 +2192,56 @@ fn substitute_one_reference<'a>(
.get(&reference.name, device, url_data)
.map(Substitution::from_value)
},
+ // https://drafts.csswg.org/css-values-5/#attr-substitution
SubstitutionFunctionKind::Attr => attr_provider
.get_attr(AtomIdent::cast(&reference.name))
- .and_then(|attr| {
- let AttributeType::Type(syntax) = &reference.attribute_syntax else {
- return Some(Substitution::new(
- Cow::Owned(quoted_css_string(&attr)),
- TokenSerializationType::Nothing,
- TokenSerializationType::Nothing,
- ));
- };
- let mut input = ParserInput::new(&attr);
- let mut parser = Parser::new(&mut input);
- let value = SpecifiedRegisteredValue::parse(
- &mut parser,
- syntax,
- url_data,
- AllowComputationallyDependent::Yes,
- )
- .ok()?;
- Some(Substitution::from_value(value.to_variable_value()))
- }),
+ .map_or_else(
+ || {
+ // Special case when fallback and <attr-type> are omitted.
+ // See FAILURE: https://drafts.csswg.org/css-values-5/#attr-substitution
+ if reference.fallback.is_none()
+ && reference.attribute_syntax == AttributeType::None
+ {
+ simple_subst("")
+ } else {
+ None
+ }
+ },
+ |attr| {
+ let mut input = ParserInput::new(&attr);
+ let mut parser = Parser::new(&mut input);
+ match &reference.attribute_syntax {
+ AttributeType::Unit(unit) => {
+ let css = {
+ // Verify that attribute data is a <number-token>.
+ parser.expect_number().ok()?;
+ let mut s = attr.clone();
+ s.push_str(unit.as_ref());
+ s
+ };
+ let serialization = match unit {
+ AttrUnit::Number => TokenSerializationType::Number,
+ AttrUnit::Percentage => TokenSerializationType::Percentage,
+ _ => TokenSerializationType::Dimension,
+ };
+ let value =
+ ComputedValue::new(css, url_data, serialization, serialization);
+ Some(Substitution::from_value(value))
+ },
+ AttributeType::Type(syntax) => {
+ let value = SpecifiedRegisteredValue::parse(
+ &mut parser,
+ syntax,
+ url_data,
+ AllowComputationallyDependent::Yes,
+ )
+ .ok()?;
+ Some(Substitution::from_value(value.to_variable_value()))
+ },
+ AttributeType::RawString | AttributeType::None => simple_subst(&attr),
+ }
+ },
+ ),
};
if let Some(s) = substitution {
diff --git a/servo/components/style/properties_and_values/syntax/mod.rs b/servo/components/style/properties_and_values/syntax/mod.rs
@@ -58,8 +58,19 @@ impl Descriptor {
/// https://drafts.csswg.org/css-values-5/#typedef-syntax
#[inline]
pub fn from_css_parser<'i>(input: &mut CSSParser<'i, '_>) -> Result<Self, StyleParseError<'i>> {
- //TODO(bug 2006624): Should also accept <syntax-string>
let mut components = vec![];
+
+ if input.try_parse(|i| i.expect_delim('*')).is_ok() {
+ return Ok(Self::universal());
+ }
+
+ // Parse <syntax-string> if given.
+ if let Ok(syntax_string) = input.try_parse(|i| i.expect_string_cloned()) {
+ return Self::from_str(syntax_string.as_ref(), /* save_specified = */ true).or_else(
+ |err| Err(input.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err))),
+ );
+ }
+
loop {
let name = Self::try_parse_component_name(input).map_err(|err| {
input.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err))
diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs
@@ -11,9 +11,11 @@ use crate::values::generics::length::GenericAnchorSizeFunction;
use crate::values::generics::position::{GenericAnchorFunction, GenericAnchorSide};
use num_traits::Zero;
use smallvec::SmallVec;
+use std::convert::AsRef;
use std::fmt::{self, Write};
use std::ops::{Add, Mul, Neg, Rem, Sub};
use std::{cmp, mem};
+use strum_macros::AsRefStr;
use style_traits::{CssWriter, NumericValue, ToCss, ToTyped, TypedValue};
use thin_vec::ThinVec;
@@ -116,10 +118,16 @@ pub enum RoundingStrategy {
/// This determines the order in which we serialize members of a calc() sum.
///
/// See https://drafts.csswg.org/css-values-4/#sort-a-calculations-children
-#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[derive(
+ AsRefStr, Clone, Copy, Debug, Eq, Ord, Parse, PartialEq, PartialOrd, MallocSizeOf, ToShmem,
+)]
+#[strum(serialize_all = "lowercase")]
#[allow(missing_docs)]
pub enum SortKey {
+ #[strum(serialize = "")]
Number,
+ #[css(skip)]
+ #[strum(serialize = "%")]
Percentage,
Cap,
Ch,
@@ -147,6 +155,7 @@ pub enum SortKey {
Lvmax,
Lvmin,
Lvw,
+ Ms,
Px,
Rcap,
Rch,
@@ -154,7 +163,7 @@ pub enum SortKey {
Rex,
Ric,
Rlh,
- Sec,
+ S, // Sec
Svb,
Svh,
Svi,
@@ -167,7 +176,9 @@ pub enum SortKey {
Vmax,
Vmin,
Vw,
+ #[css(skip)]
ColorComponent,
+ #[css(skip)]
Other,
}
diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs
@@ -306,7 +306,7 @@ impl generic::CalcNodeLeaf for Leaf {
match *self {
Self::Number(..) => SortKey::Number,
Self::Percentage(..) => SortKey::Percentage,
- Self::Time(..) => SortKey::Sec,
+ Self::Time(..) => SortKey::S,
Self::Resolution(..) => SortKey::Dppx,
Self::Angle(..) => SortKey::Deg,
Self::Length(ref l) => match *l {
diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs
@@ -226,7 +226,7 @@ impl Parse for Content {
let style = Content::parse_counter_style(context, input);
Ok(generics::ContentItem::Counters(name, separator, style))
}),
- "attr" => input.parse_nested_block(|input| {
+ "attr" if !static_prefs::pref!("layout.css.attr.enabled") => input.parse_nested_block(|input| {
Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?))
}),
_ => {
diff --git a/testing/web-platform/meta/css/css-values/attr-all-types.html.ini b/testing/web-platform/meta/css/css-values/attr-all-types.html.ini
@@ -1,108 +1,15 @@
[attr-all-types.html]
- [CSS Values and Units Test: attr]
- expected: FAIL
-
- [CSS Values and Units Test: attr 2]
- expected: FAIL
-
- [CSS Values and Units Test: attr 3]
- expected: FAIL
-
- [CSS Values and Units Test: attr 6]
- expected: FAIL
-
- [CSS Values and Units Test: attr 7]
- expected: FAIL
-
- [CSS Values and Units Test: attr 11]
- expected: FAIL
-
- [CSS Values and Units Test: attr 55]
- expected: FAIL
-
- [CSS Values and Units Test: attr 56]
- expected: FAIL
-
- [CSS Values and Units Test: attr 57]
- expected: FAIL
-
- [CSS Values and Units Test: attr 58]
- expected: FAIL
-
- [CSS Values and Units Test: attr 59]
- expected: FAIL
-
- [CSS Values and Units Test: attr 63]
- expected: FAIL
-
- [CSS Values and Units Test: attr 64]
- expected: FAIL
-
- [CSS Values and Units Test: attr 66]
- expected: FAIL
-
- [CSS Values and Units Test: attr 68]
- expected: FAIL
-
- [CSS Values and Units Test: attr 70]
- expected: FAIL
-
- [CSS Values and Units Test: attr 72]
- expected: FAIL
-
[CSS Values and Units Test: attr 74]
expected: FAIL
-
- [CSS Values and Units Test: attr 76]
- expected: FAIL
-
- [CSS Values and Units Test: attr 78]
- expected: FAIL
-
- [CSS Values and Units Test: attr 80]
- expected: FAIL
-
- [CSS Values and Units Test: attr 82]
- expected: FAIL
-
- [CSS Values and Units Test: attr 84]
- expected: FAIL
-
- [CSS Values and Units Test: attr 86]
- expected: FAIL
-
- [CSS Values and Units Test: attr 88]
- expected: FAIL
-
- [CSS Values and Units Test: attr 90]
- expected: FAIL
-
- [CSS Values and Units Test: attr 92]
- expected: FAIL
-
- [CSS Values and Units Test: attr 94]
- expected: FAIL
-
- [CSS Values and Units Test: attr 96]
- expected: FAIL
-
- [CSS Values and Units Test: attr 108]
- expected: FAIL
-
- [CSS Values and Units Test: attr 1]
- expected: FAIL
-
- [CSS Values and Units Test: attr 12]
- expected: FAIL
-
- [CSS Values and Units Test: attr 65]
+
+ [CSS Values and Units Test: attr 75]
expected: FAIL
- [CSS Values and Units Test: attr 98]
+ [CSS Values and Units Test: attr 76]
expected: FAIL
- [CSS Values and Units Test: attr 110]
+ [CSS Values and Units Test: attr 77]
expected: FAIL
- [CSS Values and Units Test: attr 69]
+ [CSS Values and Units Test: attr 79]
expected: FAIL
diff --git a/testing/web-platform/meta/css/css-values/attr-cycle.html.ini b/testing/web-platform/meta/css/css-values/attr-cycle.html.ini
@@ -1,7 +1,4 @@
[attr-cycle.html]
- [CSS Values and Units Test: attr]
- expected: FAIL
-
[CSS Values and Units Test: attr 2]
expected: FAIL
@@ -35,18 +32,12 @@
[CSS Values and Units Test: attr 17]
expected: FAIL
- [CSS Values and Units Test: attr 21]
- expected: FAIL
-
[CSS Values and Units Test: attr 19]
expected: FAIL
[CSS Values and Units Test: attr 22]
expected: FAIL
- [CSS Values and Units Test: attr 23]
- expected: FAIL
-
[CSS Values and Units Test: attr 26]
expected: FAIL
diff --git a/testing/web-platform/meta/css/css-values/attr-length-specified.html.ini b/testing/web-platform/meta/css/css-values/attr-length-specified.html.ini
@@ -1,3 +0,0 @@
-[attr-length-specified.html]
- [Unit (em) number]
- expected: FAIL
diff --git a/testing/web-platform/meta/css/css-values/attr-null-namespace.xhtml.ini b/testing/web-platform/meta/css/css-values/attr-null-namespace.xhtml.ini
@@ -1,6 +1,3 @@
[attr-null-namespace.xhtml]
- [Attribute in null-namespace is substituted]
- expected: FAIL
-
[Attribute in null-namespace is substituted (JS)]
expected: FAIL
diff --git a/testing/web-platform/meta/css/css-values/attr-security.html.ini b/testing/web-platform/meta/css/css-values/attr-security.html.ini
@@ -20,6 +20,9 @@
['background-image: image-set(if(style(--true): url(https://does-not-exist.test/404.png);\n style(--condition-val): url(https://does-not-exist.test/404.png);\n else: url(https://does-not-exist.test/404.png);))' with data-foo="attr(data-foo type(*))"]
expected: FAIL
+ ['background-image: attr(data-foo type(*))' with data-foo="url(https://does-not-exist.test/404.png), linear-gradient(black, white)"]
+ expected: FAIL
+
['--x: image-set(if(style(--condition-val: if(style(--true): attr(data-foo type(*));)): url(https://does-not-exist.test/404.png);))' with data-foo="3"]
expected: FAIL
diff --git a/testing/web-platform/tests/css/css-values/attr-all-types.html b/testing/web-platform/tests/css/css-values/attr-all-types.html
@@ -35,7 +35,7 @@
const dimensionTypeToUnits = {
"length": ["em", "ex", "cap", "ch", "ic", "rem", "lh", "rlh", "vw", "vh", "vi", "vb", "vmin", "vmax"],
"angle": ["deg", "grad", "rad", "turn"],
- "time": ["ms", "ms"],
+ "time": ["s", "ms"],
"frequency": ["Hz", "kHz"]
};
@@ -127,7 +127,9 @@
test_valid_attr('content', 'attr(data-foo type(<string>))', '"attr(data-foo)"', '"attr(data-foo)"');
test_valid_attr('content', 'attr(data-foo)', '', '""');
test_valid_attr('font-family', 'attr(non-existent)', '', '""');
+ test_valid_attr('font-family', 'attr(non-existent, serif)', '', 'serif');
test_valid_attr('font-family', 'attr(non-existent string)', '', '');
+ test_valid_attr('font-family', 'attr(non-existent raw-string)', '', '');
test_invalid_attr('font-family', 'attr(non-existent type(<string>))', '');
test_valid_attr('animation-name', 'attr(data-foo type(<custom-ident>))', 'anim', 'anim');
@@ -177,10 +179,21 @@
test_valid_attr('transition-duration', 'attr(data-foo type(<time>), 30s)', '10m', '30s');
test_valid_attr('transition-duration', 'attr(data-foo type(<time>), calc(10s + 20s))', '10m', '30s');
+ test_valid_attr('width', 'attr(data-foo type(invalid))', '0px', '');
+ test_valid_attr('width', 'attr(data-foo type(invalid), 10px)', '0px', '10px');
+ test_invalid_attr('width', 'attr(data-foo type(invalid |), 10px)', '0px');
+
+ test_valid_attr('background-color', 'attr(data-foo type("<color>")', '#ff0099aa', '#ff0099aa');
+ test_valid_attr('background-color', 'attr(data-foo type("invalid | blue")', 'blue', 'blue');
+ test_valid_attr('width', 'attr(data-foo type("<length> | <percentage>")', '10%', '10%');
+ test_invalid_attr('width', 'attr(data-foo type("<invalid>")', '10px');
+
test_valid_attr('height', 'attr(data-foo px)', '10', '10px');
test_valid_attr('width', 'calc(attr(data-foo px) + 1px)', '10', '11px');
test_valid_attr('font-size', 'attr(data-foo %)', '10', '10%');
test_valid_attr('--x', 'attr(data-foo px) 11px', '10', '10px 11px');
+ test_valid_attr('height', 'attr(data-foo px)', 'abc', '');
+ test_valid_attr('width', 'attr(non-existent px, 3px)', '10', '3px');
test_valid_attr('font-weight', 'attr(data-foo number)', '10', '10');
@@ -244,6 +257,7 @@
test_invalid_attr('width', 'attr(data-foo px)', '10px');
test_invalid_attr('width', 'attr(data-foo <px>)', '10');
test_invalid_attr('width', 'attr(data-foo xx)', '10');
+ test_invalid_attr('width', 'attr(data-foo xx, 3px)', '10');
test_invalid_attr('width', 'attr(data-foo px)', 'calc(1 + 3)');
test_invalid_attr('width', 'attr(data-foo px)', 'var(--number)');