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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions style/custom_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ impl CssEnvironment {
return None;
}

#[cfg(feature = "servo")]
if *name == Atom::from("preferred-text-scale") {
Comment thread
lanyue-llk marked this conversation as resolved.
return Some(VariableValue::from_token(
Token::Number {
has_sign: false,
value: device.preferred_text_scale(),
int_value: None,
},
url_data,
));
}

if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) {
return Some((var.evaluator)(device, url_data));
}
Expand Down
82 changes: 82 additions & 0 deletions style/device/servo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ impl Default for ServoMediaFeaturePreferences {
pub(super) struct ExtraDeviceData {
/// The current media type used by de device.
media_type: MediaType,
/// User preferred text scale exposed through CSS env().
preferred_text_scale: f32,
/// The current viewport size, in CSS pixels.
viewport_size: Size2D<f32, CSSPixel>,
/// The current screen size, in device pixels.
Expand Down Expand Up @@ -148,6 +150,7 @@ impl Device {
body_text_color: AtomicU32::new(AbsoluteColor::BLACK.to_nscolor()),
extra: ExtraDeviceData {
media_type,
preferred_text_scale: 1.0,
viewport_size,
device_size,
device_pixel_ratio,
Expand All @@ -162,6 +165,21 @@ impl Device {
}
}

/// User preferred text scale exposed by `env(preferred-text-scale)`.
pub fn preferred_text_scale(&self) -> f32 {
self.extra.preferred_text_scale
}

/// Set the CSS environment text scale. The embedder must invalidate styles
/// that depend on environment variables when this value changes.
pub fn set_preferred_text_scale(&mut self, scale: f32) {
assert!(
scale.is_finite() && scale > 0.0,
"preferred text scale must be finite and positive"
);
self.extra.preferred_text_scale = scale;
Comment thread
lanyue-llk marked this conversation as resolved.
}

/// Returns the computed line-height for the font in a given computed values instance.
///
/// If you pass down an element, then the used line-height is returned.
Expand Down Expand Up @@ -571,6 +589,70 @@ fn used_color_scheme_is_dark(
mod tests {
use super::*;

#[derive(Debug)]
struct TestFontMetrics;

impl FontMetricsProvider for TestFontMetrics {
fn query_font_metrics(
&self,
_vertical: bool,
_font: &Font,
_base_size: CSSPixelLength,
_flags: QueryFontMetricsFlags,
) -> FontMetrics {
FontMetrics::default()
}

fn base_size_for_generic(&self, _generic: GenericFontFamily) -> Length {
Length::new(16.0)
}
}

#[test]
fn preferred_text_scale_environment_tracks_device_and_rejects_invalid_values() {
let mut device = Device::new(
MediaType::screen(),
QuirksMode::NoQuirks,
euclid::size2(100.0, 100.0),
euclid::size2(100.0, 100.0),
Scale::new(1.0),
Box::new(TestFontMetrics),
ComputedValues::initial_values_with_font_override(Font::initial_values()),
PrefersColorScheme::Light,
PointerCapabilities::default(),
PointerCapabilities::default(),
);
let url_data = crate::stylesheets::UrlExtraData::from(
url::Url::parse("https://example.test/").unwrap(),
);
let text_scale = crate::Atom::from("preferred-text-scale");
let value = |device: &Device| {
device
.environment()
.get(&text_scale, device, &url_data)
.unwrap()
.css
.parse::<f32>()
.unwrap()
};
assert_eq!(value(&device), 1.0);
device.set_preferred_text_scale(1.5);
assert_eq!(value(&device), 1.5);
device.set_preferred_text_scale(2.0);
assert_eq!(value(&device), 2.0);
assert!(device
.environment()
.get(&crate::Atom::from("unknown"), &device, &url_data)
.is_none());
for invalid in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
device.set_preferred_text_scale(invalid);
}))
.is_err());
assert_eq!(value(&device), 2.0);
}
}

#[test]
fn used_color_scheme_honors_supported_schemes_and_user_preference() {
assert!(!used_color_scheme_is_dark(
Expand Down
4 changes: 2 additions & 2 deletions style/values/generics/counters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,8 @@ fn is_decimal(counter_type: &CounterStyle) -> bool {
)]
#[repr(C)]
pub struct GenericContentItems<Image> {
/// The actual content items. Note that, past the alt marker, only some subset (strings,
/// attr(), counter())
/// The actual content items. Past the alt marker, only strings, attr(), counter(),
/// and counters() are allowed.
pub items: thin_vec::ThinVec<GenericContentItem<Image>>,
/// The index at which alt text starts, always non-zero. If equal to items.len(), no alt text
/// exists.
Expand Down
115 changes: 109 additions & 6 deletions style/values/specified/counters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,16 +280,21 @@ impl Parse for Content {
));
},
Token::Function(ref name) => {
// FIXME(emilio): counter() / counters() should be valid per spec past
// the alt marker, but it's likely non-trivial to support and other
// browsers don't support it either, so restricting it for now.
// CSS Content permits counter() and counters() after the alt marker.
// Moli enables this grammar for native declarations and CSSOM serialization.
// The resulting style values retain counter expressions; this parser does
// not resolve them into accessible text. Embedders must resolve each
// element's counter scope and refresh accessibility text when DOM, styles,
// or counter values change.
// TODO(moli): Validate downstream alt-counter evaluation and invalidation,
// including Gecko consumers of this shared parser.
let result = match_ignore_ascii_case! { &name,
"counter" if alt_start.is_none() => input.parse_nested_block(|input| {
"counter" => input.parse_nested_block(|input| {
let name = CustomIdent::parse(input, &[])?;
let style = Content::parse_counter_style(context, input);
Ok(generics::ContentItem::Counter(name, style))
}),
"counters" if alt_start.is_none() => input.parse_nested_block(|input| {
"counters" => input.parse_nested_block(|input| {
let name = CustomIdent::parse(input, &[])?;
input.expect_comma()?;
let separator = input.expect_string()?.as_ref().to_owned().into();
Expand Down Expand Up @@ -344,7 +349,7 @@ impl Parse for Content {
},
}
}
if items.is_empty() {
if items.is_empty() || alt_start == Some(items.len()) {
return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
}
let alt_start = alt_start.unwrap_or(items.len());
Expand All @@ -354,3 +359,101 @@ impl Parse for Content {
}))
}
}

#[cfg(all(test, feature = "servo"))]
mod tests {
use super::*;
use crate::context::QuirksMode;
use crate::custom_properties::AttrTaint;
use crate::stylesheets::{CssRuleType, Origin, UrlExtraData};
use cssparser::ParserInput;
use style_traits::ParsingMode;

fn parse_content(value: &str) -> Option<Content> {
static_prefs::set_pref!("layout.css.content.alt-text.enabled", true);
let url_data = UrlExtraData::from(url::Url::parse("https://example.com/").unwrap());
let context = ParserContext::new(
Origin::Author,
&url_data,
Some(CssRuleType::Style),
ParsingMode::DEFAULT,
QuirksMode::NoQuirks,
Default::default(),
None,
None,
AttrTaint::default(),
);
let mut input = ParserInput::new(value);
Parser::new(&mut input)
.parse_entirely(|input| Content::parse(&context, input))
.ok()
}

#[test]
fn content_alt_counters_parse_and_serialize() {
for (input, expected, main_count) in [
(
r#""Chapter " / "Chapter " counter(chapter)"#,
r#""Chapter " / "Chapter " counter(chapter)"#,
1,
),
(
r#""main / label" / /* alt */ COUNTER(chapter, DECIMAL)"#,
r#""main / label" / counter(chapter)"#,
1,
),
(
r#"counters(chapter, "/") " " / counters(chapter, "/", upper-roman)"#,
r#"counters(chapter, "/") " " / counters(chapter, "/", upper-roman)"#,
2,
),
(
r#"url("https://example.com/icon.svg") / attr(data-label) counter(chapter)"#,
r#"url("https://example.com/icon.svg") / attr(data-label) counter(chapter)"#,
1,
),
(
r#""label" / c\6f unter(chapter)"#,
r#""label" / counter(chapter)"#,
1,
),
(r#""label" / """#, r#""label" / """#, 1),
] {
let parsed = parse_content(input).unwrap_or_else(|| panic!("{input} should parse"));
let Content::Items(ref items) = parsed else {
panic!("{input} should produce content items");
};
assert_eq!(items.alt_start, main_count, "{input}");
assert!(items.items.len() > items.alt_start, "{input}");
assert_eq!(parsed.to_css_string(), expected, "{input}");
assert_eq!(parse_content(expected), Some(parsed), "{input}");
}
}

#[test]
fn content_alt_rejects_invalid_components_and_counter_arguments() {
for input in [
r#"none / counter(chapter)"#,
r#"normal / counter(chapter)"#,
r#"/ counter(chapter)"#,
r#""label" /"#,
r#""label" / /* empty alt */"#,
r#""label" / counter()"#,
r#""label" / counter(1)"#,
r#""label" / counter(chapter,)"#,
r#""label" / counter(chapter, decimal, extra)"#,
r#""label" / counters(chapter)"#,
r#""label" / counters(chapter, 1)"#,
r#""label" / attr() counter(chapter)"#,
r#""label" / url(alt.svg) counter(chapter)"#,
r#""label" / open-quote counter(chapter)"#,
r#""label" / close-quote counter(chapter)"#,
r#""label" / no-open-quote counter(chapter)"#,
r#""label" / no-close-quote counter(chapter)"#,
r#""label" / counter(chapter) / "extra""#,
r#""label" / counter(chapter) }"#,
] {
assert!(parse_content(input).is_none(), "{input} should be invalid");
}
}
}