1use crate::corety::{AzString, OptionString};
4use crate::props::property::CssProperty;
5
6#[repr(C)]
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
18pub struct PseudoStateFlags {
19 pub hover: bool,
20 pub active: bool,
21 pub focused: bool,
22 pub disabled: bool,
23 pub checked: bool,
24 pub focus_within: bool,
25 pub seat_focused: bool,
27 pub visited: bool,
28 pub backdrop: bool,
30 pub dragging: bool,
32 pub drag_over: bool,
34 pub placeholder: bool,
43}
44
45impl PseudoStateFlags {
46 #[must_use]
48 pub const fn has_state(&self, state: PseudoStateType) -> bool {
49 match state {
50 PseudoStateType::Normal => true,
51 PseudoStateType::Hover => self.hover,
52 PseudoStateType::Active => self.active,
53 PseudoStateType::Focus => self.focused,
54 PseudoStateType::SeatFocus => self.seat_focused,
55 PseudoStateType::Disabled => self.disabled,
56 PseudoStateType::CheckedTrue => self.checked,
57 PseudoStateType::CheckedFalse => !self.checked,
58 PseudoStateType::FocusWithin => self.focus_within,
59 PseudoStateType::Visited => self.visited,
60 PseudoStateType::Backdrop => self.backdrop,
61 PseudoStateType::Dragging => self.dragging,
62 PseudoStateType::DragOver => self.drag_over,
63 PseudoStateType::Placeholder => self.placeholder,
64 }
65 }
66}
67
68#[repr(C, u8)]
71#[derive(Debug, Clone, PartialEq)]
72pub enum DynamicSelector {
73 Os(OsCondition) = 0,
75 OsVersion(OsVersionCondition) = 1,
77 Media(MediaType) = 2,
79 ViewportWidth(MinMaxRange) = 3,
81 ViewportHeight(MinMaxRange) = 4,
83 ContainerWidth(MinMaxRange) = 5,
85 ContainerHeight(MinMaxRange) = 6,
87 ContainerName(AzString) = 7,
89 Theme(ThemeCondition) = 8,
91 AspectRatio(MinMaxRange) = 9,
93 Orientation(OrientationType) = 10,
95 PrefersReducedMotion(BoolCondition) = 11,
97 PrefersHighContrast(BoolCondition) = 12,
99 PseudoState(PseudoStateType) = 13,
101 Language(LanguageCondition) = 14,
104}
105
106impl_option!(
107 DynamicSelector,
108 OptionDynamicSelector,
109 copy = false,
110 [Debug, Clone, PartialEq, Eq]
111);
112
113impl_vec!(
114 DynamicSelector,
115 DynamicSelectorVec,
116 DynamicSelectorVecDestructor,
117 DynamicSelectorVecDestructorType,
118 DynamicSelectorVecSlice,
119 OptionDynamicSelector
120);
121impl_vec_clone!(
122 DynamicSelector,
123 DynamicSelectorVec,
124 DynamicSelectorVecDestructor
125);
126impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
127impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
128
129impl DynamicSelector {
130 const fn variant_tag(&self) -> u8 {
133 match self {
134 Self::Os(_) => 0,
135 Self::OsVersion(_) => 1,
136 Self::Media(_) => 2,
137 Self::ViewportWidth(_) => 3,
138 Self::ViewportHeight(_) => 4,
139 Self::ContainerWidth(_) => 5,
140 Self::ContainerHeight(_) => 6,
141 Self::ContainerName(_) => 7,
142 Self::Theme(_) => 8,
143 Self::AspectRatio(_) => 9,
144 Self::Orientation(_) => 10,
145 Self::PrefersReducedMotion(_) => 11,
146 Self::PrefersHighContrast(_) => 12,
147 Self::PseudoState(_) => 13,
148 Self::Language(_) => 14,
149 }
150 }
151}
152
153impl Eq for DynamicSelector {}
160
161impl PartialOrd for DynamicSelector {
162 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
163 Some(self.cmp(other))
164 }
165}
166
167impl Ord for DynamicSelector {
168 #[allow(clippy::match_same_arms)]
171 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
172 use core::cmp::Ordering;
173 match self.variant_tag().cmp(&other.variant_tag()) {
174 Ordering::Equal => {}
175 non_eq => return non_eq,
176 }
177 match (self, other) {
179 (Self::Os(a), Self::Os(b)) => a.cmp(b),
180 (Self::OsVersion(a), Self::OsVersion(b)) => a.cmp(b),
181 (Self::Media(a), Self::Media(b)) => a.cmp(b),
182 (Self::ContainerName(a), Self::ContainerName(b)) => a.cmp(b),
183 (Self::Theme(a), Self::Theme(b)) => a.cmp(b),
184 (Self::Orientation(a), Self::Orientation(b)) => a.cmp(b),
185 (Self::PrefersReducedMotion(a), Self::PrefersReducedMotion(b)) => a.cmp(b),
186 (Self::PrefersHighContrast(a), Self::PrefersHighContrast(b)) => a.cmp(b),
187 (Self::PseudoState(a), Self::PseudoState(b)) => a.cmp(b),
188 (Self::Language(a), Self::Language(b)) => a.cmp(b),
189 (Self::ViewportWidth(a), Self::ViewportWidth(b))
190 | (Self::ViewportHeight(a), Self::ViewportHeight(b))
191 | (Self::ContainerWidth(a), Self::ContainerWidth(b))
192 | (Self::ContainerHeight(a), Self::ContainerHeight(b))
193 | (Self::AspectRatio(a), Self::AspectRatio(b)) => {
194 (a.min.to_bits(), a.max.to_bits()).cmp(&(b.min.to_bits(), b.max.to_bits()))
195 }
196 _ => Ordering::Equal,
198 }
199 }
200}
201
202impl core::hash::Hash for DynamicSelector {
203 #[allow(clippy::match_same_arms)]
206 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
207 self.variant_tag().hash(state);
208 match self {
209 Self::Os(x) => x.hash(state),
210 Self::OsVersion(x) => x.hash(state),
211 Self::Media(x) => x.hash(state),
212 Self::ContainerName(x) => x.hash(state),
213 Self::Theme(x) => x.hash(state),
214 Self::Orientation(x) => x.hash(state),
215 Self::PrefersReducedMotion(x) => x.hash(state),
216 Self::PrefersHighContrast(x) => x.hash(state),
217 Self::PseudoState(x) => x.hash(state),
218 Self::Language(x) => x.hash(state),
219 Self::ViewportWidth(r)
220 | Self::ViewportHeight(r)
221 | Self::ContainerWidth(r)
222 | Self::ContainerHeight(r)
223 | Self::AspectRatio(r) => {
224 r.min.to_bits().hash(state);
225 r.max.to_bits().hash(state);
226 }
227 }
228 }
229}
230
231#[repr(C)]
233#[derive(Debug, Clone, Copy)]
234pub struct MinMaxRange {
235 pub min: f32,
237 pub max: f32,
239}
240
241impl PartialEq for MinMaxRange {
250 fn eq(&self, other: &Self) -> bool {
251 self.min.to_bits() == other.min.to_bits() && self.max.to_bits() == other.max.to_bits()
252 }
253}
254
255impl Eq for MinMaxRange {}
256
257impl PartialOrd for MinMaxRange {
264 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
265 Some(
266 (self.min.to_bits(), self.max.to_bits())
267 .cmp(&(other.min.to_bits(), other.max.to_bits())),
268 )
269 }
270}
271
272impl MinMaxRange {
273 #[must_use]
274 pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
275 Self {
276 min: if let Some(m) = min { m } else { f32::NAN },
277 max: if let Some(m) = max { m } else { f32::NAN },
278 }
279 }
280
281 #[must_use]
283 pub const fn with_min(min_val: f32) -> Self {
284 Self {
285 min: min_val,
286 max: f32::NAN,
287 }
288 }
289
290 #[must_use]
292 pub const fn with_max(max_val: f32) -> Self {
293 Self {
294 min: f32::NAN,
295 max: max_val,
296 }
297 }
298
299 #[must_use]
300 pub const fn min(&self) -> Option<f32> {
301 if self.min.is_nan() {
302 None
303 } else {
304 Some(self.min)
305 }
306 }
307
308 #[must_use]
309 pub const fn max(&self) -> Option<f32> {
310 if self.max.is_nan() {
311 None
312 } else {
313 Some(self.max)
314 }
315 }
316
317 #[must_use]
318 pub fn matches(&self, value: f32) -> bool {
319 let min_ok = self.min.is_nan() || value >= self.min;
320 let max_ok = self.max.is_nan() || value <= self.max;
321 min_ok && max_ok
322 }
323}
324
325#[repr(C)]
327#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
328pub enum BoolCondition {
329 #[default]
330 False,
331 True,
332}
333
334impl From<bool> for BoolCondition {
335 fn from(b: bool) -> Self {
336 if b {
337 Self::True
338 } else {
339 Self::False
340 }
341 }
342}
343
344impl From<BoolCondition> for bool {
345 fn from(b: BoolCondition) -> Self {
346 matches!(b, BoolCondition::True)
347 }
348}
349
350#[repr(C)]
352#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
353pub enum OsCondition {
354 Any,
355 Apple, MacOS,
357 IOS,
358 Linux,
359 Windows,
360 Android,
361 Web, }
363
364impl_option!(
365 OsCondition,
366 OptionOsCondition,
367 [Debug, Clone, Copy, PartialEq, Eq, Hash]
368);
369
370impl OsCondition {
371 #[must_use]
373 pub const fn from_system_platform(platform: &crate::system::Platform) -> Self {
374 use crate::system::Platform;
375 match platform {
376 Platform::Windows => Self::Windows,
377 Platform::MacOs => Self::MacOS,
378 Platform::Linux(_) => Self::Linux,
379 Platform::Android => Self::Android,
380 Platform::Ios => Self::IOS,
381 Platform::Unknown => Self::Any,
382 }
383 }
384}
385
386#[repr(C, u8)]
387#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
388pub enum OsVersionCondition {
389 Min(OsVersion),
392 Max(OsVersion),
394 Exact(OsVersion),
396 DesktopEnvironment(LinuxDesktopEnv),
398 DesktopEnvMin(DesktopEnvVersion),
400 DesktopEnvMax(DesktopEnvVersion),
402 DesktopEnvExact(DesktopEnvVersion),
404}
405
406#[repr(C)]
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
410pub struct DesktopEnvVersion {
411 pub env: LinuxDesktopEnv,
412 pub version_id: u32,
413}
414
415#[repr(C)]
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
421pub struct OsVersion {
422 pub os: OsFamily,
424 pub version_id: u32,
427}
428
429impl Default for OsVersion {
430 fn default() -> Self {
431 Self::unknown()
432 }
433}
434
435impl OsVersion {
436 #[must_use]
437 pub const fn new(os: OsFamily, version_id: u32) -> Self {
438 Self { os, version_id }
439 }
440
441 #[must_use]
444 pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
445 if self.os == other.os {
446 Some(self.version_id.cmp(&other.version_id))
447 } else {
448 None }
450 }
451
452 #[must_use]
454 pub fn is_at_least(&self, other: &Self) -> bool {
455 self.compare(other)
456 .is_some_and(|o| o != core::cmp::Ordering::Less)
457 }
458
459 #[must_use]
461 pub fn is_at_most(&self, other: &Self) -> bool {
462 self.compare(other)
463 .is_some_and(|o| o != core::cmp::Ordering::Greater)
464 }
465}
466
467impl_option!(
468 OsVersion,
469 OptionOsVersion,
470 [Debug, Clone, Copy, PartialEq, Eq, Hash]
471);
472
473impl OsVersion {
474 #[must_use]
476 pub fn is_exactly(&self, other: &Self) -> bool {
477 self.compare(other) == Some(core::cmp::Ordering::Equal)
478 }
479}
480
481#[repr(C)]
483#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
484pub enum OsFamily {
485 Windows,
486 MacOS,
487 IOS,
488 Linux,
489 Android,
490}
491
492impl OsVersion {
498 pub const WIN_2000: Self = Self::new(OsFamily::Windows, 500); pub const WIN_XP: Self = Self::new(OsFamily::Windows, 501); pub const WIN_XP_64: Self = Self::new(OsFamily::Windows, 502); pub const WIN_VISTA: Self = Self::new(OsFamily::Windows, 600); pub const WIN_7: Self = Self::new(OsFamily::Windows, 601); pub const WIN_8: Self = Self::new(OsFamily::Windows, 602); pub const WIN_8_1: Self = Self::new(OsFamily::Windows, 603); pub const WIN_10: Self = Self::new(OsFamily::Windows, 1000); pub const WIN_10_1507: Self = Self::new(OsFamily::Windows, 1000); pub const WIN_10_1511: Self = Self::new(OsFamily::Windows, 1001); pub const WIN_10_1607: Self = Self::new(OsFamily::Windows, 1002); pub const WIN_10_1703: Self = Self::new(OsFamily::Windows, 1003); pub const WIN_10_1709: Self = Self::new(OsFamily::Windows, 1004); pub const WIN_10_1803: Self = Self::new(OsFamily::Windows, 1005); pub const WIN_10_1809: Self = Self::new(OsFamily::Windows, 1006); pub const WIN_10_1903: Self = Self::new(OsFamily::Windows, 1007); pub const WIN_10_1909: Self = Self::new(OsFamily::Windows, 1008); pub const WIN_10_2004: Self = Self::new(OsFamily::Windows, 1009); pub const WIN_10_20H2: Self = Self::new(OsFamily::Windows, 1010); pub const WIN_10_21H1: Self = Self::new(OsFamily::Windows, 1011); pub const WIN_10_21H2: Self = Self::new(OsFamily::Windows, 1012); pub const WIN_10_22H2: Self = Self::new(OsFamily::Windows, 1013); pub const WIN_11: Self = Self::new(OsFamily::Windows, 1100); pub const WIN_11_21H2: Self = Self::new(OsFamily::Windows, 1100); pub const WIN_11_22H2: Self = Self::new(OsFamily::Windows, 1101); pub const WIN_11_23H2: Self = Self::new(OsFamily::Windows, 1102); pub const WIN_11_24H2: Self = Self::new(OsFamily::Windows, 1103); pub const MACOS_CHEETAH: Self = Self::new(OsFamily::MacOS, 1000); pub const MACOS_PUMA: Self = Self::new(OsFamily::MacOS, 1001); pub const MACOS_JAGUAR: Self = Self::new(OsFamily::MacOS, 1002); pub const MACOS_PANTHER: Self = Self::new(OsFamily::MacOS, 1003); pub const MACOS_TIGER: Self = Self::new(OsFamily::MacOS, 1004); pub const MACOS_LEOPARD: Self = Self::new(OsFamily::MacOS, 1005); pub const MACOS_SNOW_LEOPARD: Self = Self::new(OsFamily::MacOS, 1006); pub const MACOS_LION: Self = Self::new(OsFamily::MacOS, 1007); pub const MACOS_MOUNTAIN_LION: Self = Self::new(OsFamily::MacOS, 1008); pub const MACOS_MAVERICKS: Self = Self::new(OsFamily::MacOS, 1009); pub const MACOS_YOSEMITE: Self = Self::new(OsFamily::MacOS, 1010); pub const MACOS_EL_CAPITAN: Self = Self::new(OsFamily::MacOS, 1011); pub const MACOS_SIERRA: Self = Self::new(OsFamily::MacOS, 1012); pub const MACOS_HIGH_SIERRA: Self = Self::new(OsFamily::MacOS, 1013); pub const MACOS_MOJAVE: Self = Self::new(OsFamily::MacOS, 1014); pub const MACOS_CATALINA: Self = Self::new(OsFamily::MacOS, 1015); pub const MACOS_BIG_SUR: Self = Self::new(OsFamily::MacOS, 1100); pub const MACOS_MONTEREY: Self = Self::new(OsFamily::MacOS, 1200); pub const MACOS_VENTURA: Self = Self::new(OsFamily::MacOS, 1300); pub const MACOS_SONOMA: Self = Self::new(OsFamily::MacOS, 1400); pub const MACOS_SEQUOIA: Self = Self::new(OsFamily::MacOS, 1500); pub const MACOS_TAHOE: Self = Self::new(OsFamily::MacOS, 2600); pub const IOS_1: Self = Self::new(OsFamily::IOS, 100);
553 pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
554 pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
555 pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
556 pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
557 pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
558 pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
559 pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
560 pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
561 pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
562 pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
563 pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
564 pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
565 pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
566 pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
567 pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
568 pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
569 pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
570
571 pub const ANDROID_CUPCAKE: Self = Self::new(OsFamily::Android, 3); pub const ANDROID_DONUT: Self = Self::new(OsFamily::Android, 4); pub const ANDROID_ECLAIR: Self = Self::new(OsFamily::Android, 7); pub const ANDROID_FROYO: Self = Self::new(OsFamily::Android, 8); pub const ANDROID_GINGERBREAD: Self = Self::new(OsFamily::Android, 10); pub const ANDROID_HONEYCOMB: Self = Self::new(OsFamily::Android, 13); pub const ANDROID_ICE_CREAM_SANDWICH: Self = Self::new(OsFamily::Android, 15); pub const ANDROID_JELLY_BEAN: Self = Self::new(OsFamily::Android, 18); pub const ANDROID_KITKAT: Self = Self::new(OsFamily::Android, 19); pub const ANDROID_LOLLIPOP: Self = Self::new(OsFamily::Android, 22); pub const ANDROID_MARSHMALLOW: Self = Self::new(OsFamily::Android, 23); pub const ANDROID_NOUGAT: Self = Self::new(OsFamily::Android, 25); pub const ANDROID_OREO: Self = Self::new(OsFamily::Android, 27); pub const ANDROID_PIE: Self = Self::new(OsFamily::Android, 28); pub const ANDROID_10: Self = Self::new(OsFamily::Android, 29); pub const ANDROID_11: Self = Self::new(OsFamily::Android, 30); pub const ANDROID_12: Self = Self::new(OsFamily::Android, 31); pub const ANDROID_12L: Self = Self::new(OsFamily::Android, 32); pub const ANDROID_13: Self = Self::new(OsFamily::Android, 33); pub const ANDROID_14: Self = Self::new(OsFamily::Android, 34); pub const ANDROID_15: Self = Self::new(OsFamily::Android, 35); pub const LINUX_2_6: Self = Self::new(OsFamily::Linux, 2060);
596 pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
597 pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
598 pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
599 pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
600
601 #[must_use]
603 pub const fn unknown() -> Self {
604 Self {
605 os: OsFamily::Linux, version_id: 0,
607 }
608 }
609}
610
611#[must_use]
614pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
615 let version_str = version_str.trim().to_lowercase();
616 let version_str = version_str.as_str();
617
618 match os {
619 OsFamily::Windows => parse_windows_version(version_str),
620 OsFamily::MacOS => parse_macos_version(version_str),
621 OsFamily::IOS => parse_ios_version(version_str),
622 OsFamily::Android => parse_android_version(version_str),
623 OsFamily::Linux => parse_linux_version(version_str),
624 }
625}
626
627fn parse_windows_version(s: &str) -> Option<OsVersion> {
628 let core = strip_os_prefix(s, &["windows", "win"]);
631 match core {
632 "2000" | "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
634 "xp" | "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
635 "vista" | "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
636 "7" | "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
637 "8" | "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
638 "8.1" | "8-1" | "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
639 "10" | "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
640 "11" => Some(OsVersion::WIN_11),
641 _ => None,
642 }
643}
644
645fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
649 for p in prefixes {
650 if let Some(rest) = s.strip_prefix(p) {
651 return rest.strip_prefix(['-', '_']).unwrap_or(rest);
652 }
653 }
654 s
655}
656
657fn parse_macos_version(s: &str) -> Option<OsVersion> {
658 match s {
659 "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
660 "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
661 "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
662 "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
663 "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
664 "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
665 "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
666 "lion" | "10.7" => Some(OsVersion::MACOS_LION),
667 "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
668 "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
669 "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
670 "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
671 "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
672 "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
673 "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
674 "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
675 "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
676 "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
677 "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
678 "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
679 "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
680 "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
681 _ => None,
682 }
683}
684
685fn parse_ios_version(s: &str) -> Option<OsVersion> {
686 match s {
687 "1" | "1.0" => Some(OsVersion::IOS_1),
688 "2" | "2.0" => Some(OsVersion::IOS_2),
689 "3" | "3.0" => Some(OsVersion::IOS_3),
690 "4" | "4.0" => Some(OsVersion::IOS_4),
691 "5" | "5.0" => Some(OsVersion::IOS_5),
692 "6" | "6.0" => Some(OsVersion::IOS_6),
693 "7" | "7.0" => Some(OsVersion::IOS_7),
694 "8" | "8.0" => Some(OsVersion::IOS_8),
695 "9" | "9.0" => Some(OsVersion::IOS_9),
696 "10" | "10.0" => Some(OsVersion::IOS_10),
697 "11" | "11.0" => Some(OsVersion::IOS_11),
698 "12" | "12.0" => Some(OsVersion::IOS_12),
699 "13" | "13.0" => Some(OsVersion::IOS_13),
700 "14" | "14.0" => Some(OsVersion::IOS_14),
701 "15" | "15.0" => Some(OsVersion::IOS_15),
702 "16" | "16.0" => Some(OsVersion::IOS_16),
703 "17" | "17.0" => Some(OsVersion::IOS_17),
704 "18" | "18.0" => Some(OsVersion::IOS_18),
705 _ => None,
706 }
707}
708
709fn parse_android_version(s: &str) -> Option<OsVersion> {
710 match s {
711 "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
712 "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
713 "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
714 "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
715 "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
716 "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
717 "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
718 "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
719 "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
720 "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
721 "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
722 "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
723 "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
724 "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
725 "10" | "q" => Some(OsVersion::ANDROID_10),
726 "11" | "r" => Some(OsVersion::ANDROID_11),
727 "12" | "s" => Some(OsVersion::ANDROID_12),
728 "12l" | "12L" => Some(OsVersion::ANDROID_12L),
729 "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
730 "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
731 "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
732 _ => {
733 if let Some(api) = s.strip_prefix("api") {
735 if let Ok(level) = api.trim().parse::<u32>() {
736 return Some(OsVersion::new(OsFamily::Android, level));
737 }
738 }
739 None
740 }
741 }
742}
743
744fn parse_linux_version(s: &str) -> Option<OsVersion> {
745 let s = strip_os_prefix(s, &["linux"]);
747 let mut parts = s.split('.');
749 let major = parts.next()?.parse::<u32>().ok()?;
750 let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
751 let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
752 let encoded = major
756 .checked_mul(1000)?
757 .checked_add(minor.checked_mul(10)?)?
758 .checked_add(patch)?;
759 Some(OsVersion::new(OsFamily::Linux, encoded))
760}
761
762#[repr(C)]
768#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
769pub enum LinuxDesktopEnv {
770 Gnome,
771 KDE,
772 XFCE,
774 Unity,
776 Cinnamon,
778 MATE,
780 Other,
781}
782
783impl LinuxDesktopEnv {
784 #[must_use]
786 pub const fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
787 use crate::system::DesktopEnvironment;
788 match de {
789 DesktopEnvironment::Gnome => Self::Gnome,
790 DesktopEnvironment::Kde => Self::KDE,
791 DesktopEnvironment::Other(_) => Self::Other,
792 }
793 }
794}
795
796#[repr(C)]
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
799pub enum MediaType {
800 Screen,
801 Print,
802 All,
803}
804#[allow(variant_size_differences)]
805#[repr(C, u8)]
807#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
808pub enum ThemeCondition {
809 Light,
810 Dark,
811 Custom(AzString),
812 SystemPreferred,
814}
815
816impl_option!(
817 ThemeCondition,
818 OptionThemeCondition,
819 copy = false,
820 [Debug, Clone, PartialEq, Eq, Hash]
821);
822
823impl ThemeCondition {
824 #[must_use]
826 pub const fn from_system_theme(theme: crate::system::Theme) -> Self {
827 use crate::system::Theme;
828 match theme {
829 Theme::Light => Self::Light,
830 Theme::Dark => Self::Dark,
831 }
832 }
833}
834
835#[repr(C)]
837#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
838pub enum OrientationType {
839 Portrait,
840 Landscape,
841}
842
843#[repr(C, u8)]
846#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
847pub enum LanguageCondition {
848 Exact(AzString),
850 Prefix(AzString),
852}
853
854impl LanguageCondition {
855 #[must_use]
857 pub fn matches(&self, language: &str) -> bool {
858 match self {
859 Self::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
860 Self::Prefix(prefix) => {
861 let prefix_str = prefix.as_str();
862 if language.len() < prefix_str.len() {
863 return false;
864 }
865 let Some(lang_prefix) = language.get(..prefix_str.len()) else {
871 return false;
872 };
873 if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
874 return false;
875 }
876 language.len() == prefix_str.len()
878 || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
879 }
880 }
881 }
882}
883
884#[repr(C)]
885#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
886pub enum PseudoStateType {
887 Normal,
889 Hover,
891 Active,
893 Focus,
895 SeatFocus,
897 Disabled,
899 CheckedTrue,
901 CheckedFalse,
903 FocusWithin,
905 Visited,
907 Backdrop,
909 Dragging,
911 DragOver,
913 Placeholder,
916}
917
918impl_option!(
919 LinuxDesktopEnv,
920 OptionLinuxDesktopEnv,
921 [Debug, Clone, Copy, PartialEq, Eq, Hash]
922);
923
924pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
926pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
928
929#[repr(C)]
942#[derive(Debug, Clone)]
943pub struct DynamicSelectorContext {
944 pub os: OsCondition,
946 pub os_version: OsVersion,
947 pub desktop_env: OptionLinuxDesktopEnv,
948 pub de_version: u32,
953
954 pub theme: ThemeCondition,
956
957 pub media_type: MediaType,
959 pub viewport_width: f32,
960 pub viewport_height: f32,
961
962 pub container_width: f32,
965 pub container_height: f32,
966 pub container_name: OptionString,
967
968 pub prefers_reduced_motion: BoolCondition,
970 pub prefers_high_contrast: BoolCondition,
971
972 pub orientation: OrientationType,
974
975 pub pseudo_state: PseudoStateFlags,
977
978 pub language: AzString,
980
981 pub window_focused: bool,
984
985 pub safe_area_top: f32,
996 pub safe_area_right: f32,
997 pub safe_area_bottom: f32,
998 pub safe_area_left: f32,
999 pub keyboard_inset_height: f32,
1003}
1004
1005impl PartialEq for DynamicSelectorContext {
1006 fn eq(&self, other: &Self) -> bool {
1007 self.os == other.os
1011 && self.os_version == other.os_version
1012 && self.desktop_env == other.desktop_env
1013 && self.de_version == other.de_version
1014 && self.theme == other.theme
1015 && self.media_type == other.media_type
1016 && self.viewport_width.to_bits() == other.viewport_width.to_bits()
1017 && self.viewport_height.to_bits() == other.viewport_height.to_bits()
1018 && self.container_width.to_bits() == other.container_width.to_bits()
1019 && self.container_height.to_bits() == other.container_height.to_bits()
1020 && self.container_name == other.container_name
1021 && self.prefers_reduced_motion == other.prefers_reduced_motion
1022 && self.prefers_high_contrast == other.prefers_high_contrast
1023 && self.orientation == other.orientation
1024 && self.pseudo_state == other.pseudo_state
1025 && self.language == other.language
1026 && self.window_focused == other.window_focused
1027 && self.safe_area_top.to_bits() == other.safe_area_top.to_bits()
1028 && self.safe_area_right.to_bits() == other.safe_area_right.to_bits()
1029 && self.safe_area_bottom.to_bits() == other.safe_area_bottom.to_bits()
1030 && self.safe_area_left.to_bits() == other.safe_area_left.to_bits()
1031 && self.keyboard_inset_height.to_bits() == other.keyboard_inset_height.to_bits()
1032 }
1033}
1034
1035impl Default for DynamicSelectorContext {
1036 fn default() -> Self {
1037 Self {
1038 os: OsCondition::Any,
1039 os_version: OsVersion::unknown(),
1040 desktop_env: OptionLinuxDesktopEnv::None,
1041 de_version: 0,
1042 theme: ThemeCondition::Light,
1043 media_type: MediaType::Screen,
1044 viewport_width: DEFAULT_VIEWPORT_WIDTH,
1045 viewport_height: DEFAULT_VIEWPORT_HEIGHT,
1046 container_width: f32::NAN,
1047 container_height: f32::NAN,
1048 container_name: OptionString::None,
1049 prefers_reduced_motion: BoolCondition::False,
1050 prefers_high_contrast: BoolCondition::False,
1051 orientation: OrientationType::Landscape,
1052 pseudo_state: PseudoStateFlags::default(),
1053 language: AzString::from_const_str("en-US"),
1054 window_focused: true,
1055 safe_area_top: f32::NAN,
1056 safe_area_right: f32::NAN,
1057 safe_area_bottom: f32::NAN,
1058 safe_area_left: f32::NAN,
1059 keyboard_inset_height: f32::NAN,
1060 }
1061 }
1062}
1063
1064impl DynamicSelectorContext {
1065 #[must_use]
1067 pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
1068 let os = OsCondition::from_system_platform(&system_style.platform);
1069 let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
1070 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
1071 } else {
1072 OptionLinuxDesktopEnv::None
1073 };
1074 let theme = ThemeCondition::from_system_theme(system_style.theme);
1075
1076 Self {
1077 os,
1078 os_version: system_style.os_version, desktop_env,
1080 de_version: 0, theme,
1082 media_type: MediaType::Screen,
1083 viewport_width: DEFAULT_VIEWPORT_WIDTH, viewport_height: DEFAULT_VIEWPORT_HEIGHT,
1085 container_width: f32::NAN,
1086 container_height: f32::NAN,
1087 container_name: OptionString::None,
1088 prefers_reduced_motion: system_style.prefers_reduced_motion,
1089 prefers_high_contrast: system_style.prefers_high_contrast,
1090 orientation: OrientationType::Landscape,
1091 pseudo_state: PseudoStateFlags::default(),
1092 language: system_style.language.clone(),
1093 window_focused: true,
1094 safe_area_top: f32::NAN,
1098 safe_area_right: f32::NAN,
1099 safe_area_bottom: f32::NAN,
1100 safe_area_left: f32::NAN,
1101 keyboard_inset_height: f32::NAN,
1102 }
1103 }
1104
1105 #[must_use]
1112 pub fn with_safe_area(&self, insets: &crate::system::SafeAreaInsets) -> Self {
1113 use crate::props::basic::pixel::OptionPixelValue;
1114 let px = |v: &OptionPixelValue| -> f32 {
1115 match v {
1116 OptionPixelValue::Some(p) => {
1117 p.to_pixels_absolute().into_option().unwrap_or(f32::NAN)
1118 }
1119 OptionPixelValue::None => f32::NAN,
1120 }
1121 };
1122 let mut ctx = self.clone();
1123 ctx.safe_area_top = px(&insets.top);
1124 ctx.safe_area_right = px(&insets.right);
1125 ctx.safe_area_bottom = px(&insets.bottom);
1126 ctx.safe_area_left = px(&insets.left);
1127 ctx.keyboard_inset_height = px(&insets.keyboard);
1128 ctx
1129 }
1130
1131 #[must_use]
1133 pub fn with_viewport(&self, width: f32, height: f32) -> Self {
1134 let mut ctx = self.clone();
1135 ctx.viewport_width = width;
1136 ctx.viewport_height = height;
1137 ctx.orientation = if width > height {
1138 OrientationType::Landscape
1139 } else {
1140 OrientationType::Portrait
1141 };
1142 ctx
1143 }
1144
1145 #[must_use]
1147 pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
1148 let mut ctx = self.clone();
1149 ctx.container_width = width;
1150 ctx.container_height = height;
1151 ctx.container_name = name.into();
1152 ctx
1153 }
1154
1155 #[must_use]
1157 pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
1158 let mut ctx = self.clone();
1159 ctx.pseudo_state = state;
1160 ctx
1161 }
1162
1163 #[must_use]
1165 pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
1166 for bp in breakpoints {
1167 let self_above = self.viewport_width >= *bp;
1168 let other_above = other.viewport_width >= *bp;
1169 if self_above != other_above {
1170 return true;
1171 }
1172 }
1173 false
1174 }
1175}
1176
1177#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1191pub enum EnvVariable {
1192 SafeAreaInsetTop,
1193 SafeAreaInsetRight,
1194 SafeAreaInsetBottom,
1195 SafeAreaInsetLeft,
1196 KeyboardInsetHeight,
1198}
1199
1200pub const ENV_DYNAMIC_ID_PREFIX: &str = "env:";
1204
1205impl EnvVariable {
1206 pub const ALL: [Self; 5] = [
1207 Self::SafeAreaInsetTop,
1208 Self::SafeAreaInsetRight,
1209 Self::SafeAreaInsetBottom,
1210 Self::SafeAreaInsetLeft,
1211 Self::KeyboardInsetHeight,
1212 ];
1213
1214 #[must_use]
1216 pub const fn as_css_name(&self) -> &'static str {
1217 match self {
1218 Self::SafeAreaInsetTop => "safe-area-inset-top",
1219 Self::SafeAreaInsetRight => "safe-area-inset-right",
1220 Self::SafeAreaInsetBottom => "safe-area-inset-bottom",
1221 Self::SafeAreaInsetLeft => "safe-area-inset-left",
1222 Self::KeyboardInsetHeight => "keyboard-inset-height",
1223 }
1224 }
1225
1226 #[must_use]
1230 pub fn from_css_name(name: &str) -> Option<Self> {
1231 Self::ALL
1232 .iter()
1233 .copied()
1234 .find(|v| v.as_css_name() == name.trim())
1235 }
1236
1237 #[must_use]
1239 pub fn dynamic_id(&self) -> AzString {
1240 alloc::format!("{ENV_DYNAMIC_ID_PREFIX}{}", self.as_css_name()).into()
1241 }
1242
1243 #[must_use]
1246 pub fn from_dynamic_id(id: &str) -> Option<Self> {
1247 id.strip_prefix(ENV_DYNAMIC_ID_PREFIX)
1248 .and_then(Self::from_css_name)
1249 }
1250
1251 #[must_use]
1255 pub fn resolve(&self, ctx: &DynamicSelectorContext) -> Option<f32> {
1256 let v = match self {
1257 Self::SafeAreaInsetTop => ctx.safe_area_top,
1258 Self::SafeAreaInsetRight => ctx.safe_area_right,
1259 Self::SafeAreaInsetBottom => ctx.safe_area_bottom,
1260 Self::SafeAreaInsetLeft => ctx.safe_area_left,
1261 Self::KeyboardInsetHeight => ctx.keyboard_inset_height,
1262 };
1263 v.is_finite().then_some(v)
1264 }
1265}
1266
1267impl DynamicSelector {
1268 #[must_use]
1270 pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1271 match self {
1272 Self::Os(os) => Self::match_os(*os, ctx.os),
1273 Self::OsVersion(ver) => {
1274 Self::match_os_version(ver, ctx.os_version, ctx.desktop_env, ctx.de_version)
1275 }
1276 Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
1277 Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
1278 Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
1279 Self::ContainerWidth(range) => {
1280 !ctx.container_width.is_nan() && range.matches(ctx.container_width)
1281 }
1282 Self::ContainerHeight(range) => {
1283 !ctx.container_height.is_nan() && range.matches(ctx.container_height)
1284 }
1285 Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
1286 Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
1287 Self::AspectRatio(range) => {
1288 let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
1289 range.matches(ratio)
1290 }
1291 Self::Orientation(orient) => *orient == ctx.orientation,
1292 Self::PrefersReducedMotion(pref) => {
1293 bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
1294 }
1295 Self::PrefersHighContrast(pref) => {
1296 bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
1297 }
1298 Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
1299 Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
1300 }
1301 }
1302
1303 fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
1304 match condition {
1305 OsCondition::Any => true,
1306 OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
1307 _ => condition == actual,
1308 }
1309 }
1310
1311 fn match_os_version(
1312 condition: &OsVersionCondition,
1313 actual: OsVersion,
1314 desktop_env: OptionLinuxDesktopEnv,
1315 de_version: u32,
1316 ) -> bool {
1317 let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
1320 match condition {
1321 OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
1322 OsVersionCondition::Min(ver) => actual.is_at_least(ver),
1323 OsVersionCondition::Max(ver) => actual.is_at_most(ver),
1324 OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
1325 OsVersionCondition::DesktopEnvMin(d) => {
1326 de_matches(&d.env) && de_version != 0 && de_version >= d.version_id
1327 }
1328 OsVersionCondition::DesktopEnvMax(d) => {
1329 de_matches(&d.env) && de_version != 0 && de_version <= d.version_id
1330 }
1331 OsVersionCondition::DesktopEnvExact(d) =>
1332 {
1336 de_matches(&d.env) && de_version != 0 && de_version == d.version_id
1337 }
1338 }
1339 }
1340
1341 fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
1342 match (condition, actual) {
1343 (ThemeCondition::SystemPreferred, _) => true,
1344 _ => condition == actual,
1345 }
1346 }
1347
1348 const fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
1349 let node_state = &ctx.pseudo_state;
1350 match state {
1351 PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
1353 PseudoStateType::Active => node_state.active,
1354 PseudoStateType::Focus => node_state.focused,
1355 PseudoStateType::SeatFocus => node_state.seat_focused,
1356 PseudoStateType::Placeholder => node_state.placeholder,
1357 PseudoStateType::Disabled => node_state.disabled,
1358 PseudoStateType::CheckedTrue => node_state.checked,
1359 PseudoStateType::CheckedFalse => !node_state.checked,
1360 PseudoStateType::FocusWithin => node_state.focus_within,
1361 PseudoStateType::Visited => node_state.visited,
1362 PseudoStateType::Backdrop => node_state.backdrop,
1363 PseudoStateType::Dragging => node_state.dragging,
1364 PseudoStateType::DragOver => node_state.drag_over,
1365 }
1366 }
1367}
1368
1369#[cfg(feature = "parser")]
1383#[must_use]
1384pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
1385 let trimmed = content.trim();
1386 let inner = trimmed
1387 .strip_prefix('(')
1388 .and_then(|s| s.strip_suffix(')'))
1389 .unwrap_or(trimmed)
1390 .trim();
1391 let inner = inner
1392 .strip_prefix('"')
1393 .and_then(|s| s.strip_suffix('"'))
1394 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1395 .unwrap_or(inner)
1396 .trim();
1397 if inner.is_empty() {
1398 return None;
1399 }
1400
1401 let (subject, op_and_version) = split_op_and_version(inner);
1403 let subject = subject.trim();
1404
1405 let (family_str, de_str) = match subject.split_once(':') {
1407 Some((f, d)) => (f.trim(), Some(d.trim())),
1408 None => (subject, None),
1409 };
1410
1411 let family = parse_os_family_token(family_str)?;
1412 let de = match de_str {
1413 Some(s) if !s.is_empty() => Some(parse_de_token(s)),
1414 _ => None,
1415 };
1416
1417 let mut out = Vec::new();
1418 out.push(DynamicSelector::Os(family));
1422
1423 match (de, op_and_version) {
1424 (Some(env), None) => {
1426 out.push(DynamicSelector::OsVersion(
1427 OsVersionCondition::DesktopEnvironment(env),
1428 ));
1429 }
1430 (Some(env), Some((op, ver_str))) => {
1432 let v: u32 = ver_str.parse().ok()?;
1433 let dev = DesktopEnvVersion { env, version_id: v };
1434 let cond = match op {
1435 VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
1436 VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
1437 VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
1438 };
1439 out.push(DynamicSelector::OsVersion(cond));
1440 }
1441 (None, Some((op, ver_str))) => {
1443 let os_family = match family {
1444 OsCondition::Linux => OsFamily::Linux,
1445 OsCondition::Windows => OsFamily::Windows,
1446 OsCondition::MacOS => OsFamily::MacOS,
1447 OsCondition::IOS => OsFamily::IOS,
1448 OsCondition::Android => OsFamily::Android,
1449 _ => return None,
1451 };
1452 let version = parse_os_version(os_family, ver_str)?;
1453 let cond = match op {
1454 VersionOp::Min => OsVersionCondition::Min(version),
1455 VersionOp::Max => OsVersionCondition::Max(version),
1456 VersionOp::Exact => OsVersionCondition::Exact(version),
1457 };
1458 out.push(DynamicSelector::OsVersion(cond));
1459 }
1460 (None, None) => {}
1462 }
1463
1464 Some(out)
1465}
1466
1467#[cfg(feature = "parser")]
1468#[derive(Copy, Clone)]
1469enum VersionOp {
1470 Min,
1471 Max,
1472 Exact,
1473}
1474
1475#[cfg(feature = "parser")]
1478fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
1479 let candidates: &[(&str, VersionOp)] = &[
1481 (">=", VersionOp::Min),
1482 ("<=", VersionOp::Max),
1483 ("=", VersionOp::Exact),
1484 (">", VersionOp::Min),
1485 ("<", VersionOp::Max),
1486 ];
1487 let mut best: Option<(usize, usize, VersionOp)> = None;
1488 for (op_str, op) in candidates {
1489 if let Some(pos) = s.find(op_str) {
1490 let len = op_str.len();
1491 best = Some(match best {
1492 None => (pos, len, *op),
1493 Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
1494 Some(b) => b,
1495 });
1496 }
1497 }
1498 match best {
1499 Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
1500 None => (s, None),
1501 }
1502}
1503
1504#[cfg(feature = "parser")]
1505fn parse_os_family_token(s: &str) -> Option<OsCondition> {
1506 match s.to_lowercase().as_str() {
1507 "linux" => Some(OsCondition::Linux),
1508 "windows" | "win" => Some(OsCondition::Windows),
1509 "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1510 "ios" => Some(OsCondition::IOS),
1511 "android" => Some(OsCondition::Android),
1512 "apple" => Some(OsCondition::Apple),
1513 "web" | "wasm" => Some(OsCondition::Web),
1514 "any" | "all" | "*" => Some(OsCondition::Any),
1515 _ => None,
1516 }
1517}
1518
1519#[cfg(feature = "parser")]
1520fn parse_de_token(s: &str) -> LinuxDesktopEnv {
1521 match s.to_lowercase().as_str() {
1522 "gnome" => LinuxDesktopEnv::Gnome,
1523 "kde" => LinuxDesktopEnv::KDE,
1524 "xfce" => LinuxDesktopEnv::XFCE,
1525 "unity" => LinuxDesktopEnv::Unity,
1526 "cinnamon" => LinuxDesktopEnv::Cinnamon,
1527 "mate" => LinuxDesktopEnv::MATE,
1528 _ => LinuxDesktopEnv::Other,
1529 }
1530}
1531
1532#[repr(C)]
1542#[derive(Debug, Clone, PartialEq)]
1543pub struct CssPropertyWithConditions {
1544 pub property: CssProperty,
1546 pub apply_if: DynamicSelectorVec,
1549}
1550
1551impl_option!(
1552 CssPropertyWithConditions,
1553 OptionCssPropertyWithConditions,
1554 copy = false,
1555 [Debug, Clone, PartialEq, Eq, PartialOrd]
1556);
1557
1558impl Eq for CssPropertyWithConditions {}
1559
1560pub fn collect_viewport_thresholds(
1568 conds: &[DynamicSelector],
1569 widths: &mut Vec<f32>,
1570 heights: &mut Vec<f32>,
1571) {
1572 for c in conds {
1573 match c {
1574 DynamicSelector::ViewportWidth(r) => {
1575 if r.min.is_finite() {
1576 widths.push(r.min);
1577 }
1578 if r.max.is_finite() {
1579 widths.push(r.max);
1580 }
1581 }
1582 DynamicSelector::ViewportHeight(r) => {
1583 if r.min.is_finite() {
1584 heights.push(r.min);
1585 }
1586 if r.max.is_finite() {
1587 heights.push(r.max);
1588 }
1589 }
1590 _ => {}
1591 }
1592 }
1593}
1594
1595impl PartialOrd for CssPropertyWithConditions {
1596 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1597 Some(self.cmp(other))
1598 }
1599}
1600
1601impl Ord for CssPropertyWithConditions {
1602 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1603 self.property
1609 .cmp(&other.property)
1610 .then_with(|| self.apply_if.as_slice().cmp(other.apply_if.as_slice()))
1611 }
1612}
1613
1614impl CssPropertyWithConditions {
1615 #[must_use]
1617 pub const fn simple(property: CssProperty) -> Self {
1618 Self {
1619 property,
1620 apply_if: DynamicSelectorVec::from_const_slice(&[]),
1621 }
1622 }
1623
1624 #[must_use]
1626 pub const fn with_single_condition(
1627 property: CssProperty,
1628 conditions: &'static [DynamicSelector],
1629 ) -> Self {
1630 Self {
1631 property,
1632 apply_if: DynamicSelectorVec::from_const_slice(conditions),
1633 }
1634 }
1635
1636 #[must_use]
1638 pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
1639 Self {
1640 property,
1641 apply_if: DynamicSelectorVec::from_vec(vec![condition]),
1642 }
1643 }
1644
1645 #[must_use]
1647 pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
1648 Self {
1649 property,
1650 apply_if: conditions,
1651 }
1652 }
1653
1654 #[must_use]
1656 pub const fn on_hover(property: CssProperty) -> Self {
1657 Self::with_single_condition(
1658 property,
1659 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
1660 )
1661 }
1662
1663 #[must_use]
1665 pub const fn on_active(property: CssProperty) -> Self {
1666 Self::with_single_condition(
1667 property,
1668 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1669 )
1670 }
1671
1672 #[must_use]
1674 pub const fn on_focus(property: CssProperty) -> Self {
1675 Self::with_single_condition(
1676 property,
1677 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1678 )
1679 }
1680
1681 #[must_use]
1684 pub const fn on_placeholder(property: CssProperty) -> Self {
1685 Self::with_single_condition(
1686 property,
1687 &[DynamicSelector::PseudoState(PseudoStateType::Placeholder)],
1688 )
1689 }
1690
1691 #[must_use]
1693 pub const fn when_disabled(property: CssProperty) -> Self {
1694 Self::with_single_condition(
1695 property,
1696 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1697 )
1698 }
1699
1700 #[must_use]
1702 pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1703 Self::with_condition(property, DynamicSelector::Os(os))
1704 }
1705
1706 #[must_use]
1708 pub const fn dark_theme(property: CssProperty) -> Self {
1709 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1710 }
1711
1712 #[must_use]
1714 pub const fn light_theme(property: CssProperty) -> Self {
1715 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1716 }
1717
1718 #[must_use]
1720 pub const fn on_windows(property: CssProperty) -> Self {
1721 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1722 }
1723
1724 #[must_use]
1726 pub const fn on_macos(property: CssProperty) -> Self {
1727 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1728 }
1729
1730 #[must_use]
1732 pub const fn on_linux(property: CssProperty) -> Self {
1733 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1734 }
1735
1736 #[must_use]
1738 pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1739 if self.apply_if.as_slice().is_empty() {
1741 return true;
1742 }
1743
1744 self.apply_if
1746 .as_slice()
1747 .iter()
1748 .all(|selector| selector.matches(ctx))
1749 }
1750
1751 #[must_use]
1753 pub fn is_conditional(&self) -> bool {
1754 !self.apply_if.as_slice().is_empty()
1755 }
1756
1757 #[must_use]
1760 pub fn is_pseudo_state_only(&self) -> bool {
1761 let conditions = self.apply_if.as_slice();
1762 !conditions.is_empty()
1763 && conditions
1764 .iter()
1765 .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1766 }
1767
1768 #[must_use]
1774 pub const fn is_layout_affecting(&self) -> bool {
1775 self.property.get_type().can_trigger_relayout()
1776 }
1777}
1778
1779impl_vec!(
1780 CssPropertyWithConditions,
1781 CssPropertyWithConditionsVec,
1782 CssPropertyWithConditionsVecDestructor,
1783 CssPropertyWithConditionsVecDestructorType,
1784 CssPropertyWithConditionsVecSlice,
1785 OptionCssPropertyWithConditions
1786);
1787impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1788impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1789impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1790impl_vec_clone!(
1791 CssPropertyWithConditions,
1792 CssPropertyWithConditionsVec,
1793 CssPropertyWithConditionsVecDestructor
1794);
1795
1796impl Eq for CssPropertyWithConditionsVec {}
1798
1799impl Ord for CssPropertyWithConditionsVec {
1800 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1801 self.as_slice().cmp(other.as_slice())
1804 }
1805}
1806
1807impl core::hash::Hash for CssPropertyWithConditions {
1808 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1809 self.property.hash(state);
1810 self.apply_if.as_slice().hash(state);
1814 }
1815}
1816
1817impl core::hash::Hash for CssPropertyWithConditionsVec {
1818 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1819 self.as_slice().hash(state);
1821 }
1822}
1823
1824impl CssPropertyWithConditionsVec {
1825 #[cfg(feature = "parser")]
1845 #[must_use]
1846 pub fn parse(style: &str) -> Self {
1847 Self::parse_with_conditions(style, &[])
1848 }
1849
1850 #[cfg(feature = "parser")]
1852 fn parse_with_conditions(style: &str, inherited_conditions: &[DynamicSelector]) -> Self {
1853 use crate::props::property::{
1854 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1855 CssPropertyType,
1856 };
1857
1858 let mut props = Vec::new();
1859 let key_map = CssKeyMap::get();
1860 let style = style.trim();
1861
1862 if style.is_empty() {
1863 return Self::from_vec(props);
1864 }
1865
1866 let chars = style.chars();
1868 let mut current_segment = String::new();
1869 let mut brace_depth = 0;
1870
1871 for c in chars {
1872 match c {
1873 '{' => {
1874 brace_depth += 1;
1875 current_segment.push(c);
1876 }
1877 '}' => {
1878 brace_depth -= 1;
1879 current_segment.push(c);
1880
1881 if brace_depth == 0 {
1882 let segment = current_segment.trim().to_string();
1884 current_segment.clear();
1885
1886 if let Some(parsed) =
1887 Self::parse_block_segment(&segment, inherited_conditions, &key_map)
1888 {
1889 props.extend(parsed);
1890 }
1891 }
1892 }
1893 ';' if brace_depth == 0 => {
1894 let segment = current_segment.trim().to_string();
1896 current_segment.clear();
1897
1898 if !segment.is_empty() {
1899 if let Some(parsed) =
1900 Self::parse_property_segment(&segment, inherited_conditions, &key_map)
1901 {
1902 props.extend(parsed);
1903 }
1904 }
1905 }
1906 _ => {
1907 current_segment.push(c);
1908 }
1909 }
1910 }
1911
1912 let remaining = current_segment.trim();
1914 if !remaining.is_empty() && !remaining.contains('{') {
1915 if let Some(parsed) =
1916 Self::parse_property_segment(remaining, inherited_conditions, &key_map)
1917 {
1918 props.extend(parsed);
1919 }
1920 }
1921
1922 Self::from_vec(props)
1923 }
1924
1925 #[cfg(feature = "parser")]
1927 fn parse_block_segment(
1928 segment: &str,
1929 inherited_conditions: &[DynamicSelector],
1930 key_map: &crate::props::property::CssKeyMap,
1931 ) -> Option<Vec<CssPropertyWithConditions>> {
1932 let brace_pos = segment.find('{')?;
1934 let selector = segment[..brace_pos].trim();
1935
1936 let content_start = brace_pos + 1;
1938 let content_end = segment.rfind('}')?;
1939 if content_end <= content_start {
1940 return None;
1941 }
1942 let content = &segment[content_start..content_end];
1943
1944 let mut conditions = inherited_conditions.to_vec();
1946
1947 if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1948 conditions.extend(new_conditions);
1949 } else {
1950 return None;
1952 }
1953
1954 let parsed = Self::parse_with_conditions(content, &conditions);
1956 Some(parsed.into_library_owned_vec())
1957 }
1958
1959 #[cfg(feature = "parser")]
1961 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1962 let selector = selector.trim();
1963
1964 if let Some(pseudo) = selector.strip_prefix(':') {
1966 match pseudo {
1967 "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1968 "active" => {
1969 return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)])
1970 }
1971 "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1972 "seat-focus" => {
1973 return Some(vec![DynamicSelector::PseudoState(
1974 PseudoStateType::SeatFocus,
1975 )])
1976 }
1977 "focus-within" => {
1978 return Some(vec![DynamicSelector::PseudoState(
1979 PseudoStateType::FocusWithin,
1980 )])
1981 }
1982 "disabled" => {
1983 return Some(vec![DynamicSelector::PseudoState(
1984 PseudoStateType::Disabled,
1985 )])
1986 }
1987 "checked" => {
1988 return Some(vec![DynamicSelector::PseudoState(
1989 PseudoStateType::CheckedTrue,
1990 )])
1991 }
1992 "visited" => {
1993 return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)])
1994 }
1995 "backdrop" => {
1996 return Some(vec![DynamicSelector::PseudoState(
1997 PseudoStateType::Backdrop,
1998 )])
1999 }
2000 "dragging" => {
2001 return Some(vec![DynamicSelector::PseudoState(
2002 PseudoStateType::Dragging,
2003 )])
2004 }
2005 "drag-over" => {
2006 return Some(vec![DynamicSelector::PseudoState(
2007 PseudoStateType::DragOver,
2008 )])
2009 }
2010 _ => return None,
2011 }
2012 }
2013
2014 if let Some(rule_content) = selector.strip_prefix('@') {
2016 return Self::parse_at_rule(rule_content);
2017 }
2018
2019 if selector == "*" {
2021 return Some(vec![]);
2022 }
2023
2024 if selector.is_empty() {
2026 return Some(vec![]);
2027 }
2028
2029 None
2030 }
2031
2032 #[cfg(feature = "parser")]
2036 #[allow(clippy::too_many_lines)] fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
2038 if let Some(rest) = rule_content.strip_prefix("os ").or_else(|| {
2044 if rule_content.starts_with("os(") {
2045 Some(&rule_content[2..])
2046 } else {
2047 None
2048 }
2049 }) {
2050 if let Some(conds) = parse_os_at_rule_content(rest) {
2051 return Some(conds);
2052 }
2053 }
2054
2055 if let Some(rest) = rule_content.strip_prefix("media ") {
2057 let media_query = rest.trim();
2058 if let Some(media_conds) = Self::parse_media_query(media_query) {
2059 return Some(media_conds);
2060 }
2061 }
2062
2063 if let Some(rest) = rule_content.strip_prefix("theme ") {
2065 let theme = rest.trim();
2066 match theme {
2067 "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
2068 "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
2069 _ => return None,
2070 }
2071 }
2072
2073 let lang_body = rule_content
2075 .strip_prefix("lang(")
2076 .map(|r| r.trim_end_matches(')').trim())
2077 .or_else(|| rule_content.strip_prefix("lang ").map(str::trim));
2078 if let Some(lang_str) = lang_body {
2079 let lang_str = lang_str
2080 .strip_prefix('"')
2081 .and_then(|s| s.strip_suffix('"'))
2082 .or_else(|| {
2083 lang_str
2084 .strip_prefix('\'')
2085 .and_then(|s| s.strip_suffix('\''))
2086 })
2087 .unwrap_or(lang_str);
2088 if !lang_str.is_empty() {
2089 return Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
2090 AzString::from(lang_str.to_string()),
2091 ))]);
2092 }
2093 }
2094
2095 if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
2097 let container_str = if rule_content.starts_with("container(") {
2098 &rule_content[9..] } else {
2100 rule_content[10..].trim()
2101 };
2102 let mut conds = Vec::new();
2103 let (name_part, query_part) = if container_str.starts_with('(') {
2105 (None, container_str)
2106 } else if let Some(paren_idx) = container_str.find('(') {
2107 let name = container_str[..paren_idx].trim();
2108 if name.is_empty() {
2109 (None, container_str)
2110 } else {
2111 (Some(name), &container_str[paren_idx..])
2112 }
2113 } else {
2114 if !container_str.is_empty() {
2115 return Some(vec![DynamicSelector::ContainerName(AzString::from(
2116 container_str.to_string(),
2117 ))]);
2118 }
2119 return None;
2120 };
2121 if let Some(name) = name_part {
2122 conds.push(DynamicSelector::ContainerName(AzString::from(
2123 name.to_string(),
2124 )));
2125 }
2126 if let Some(inner) = query_part
2128 .strip_prefix('(')
2129 .and_then(|s| s.strip_suffix(')'))
2130 {
2131 if let Some((key, value)) = inner.split_once(':') {
2132 let key = key.trim();
2133 let value = value.trim();
2134 let px_value = value
2135 .strip_suffix("px")
2136 .and_then(|v| v.trim().parse::<f32>().ok())
2137 .filter(|px| !px.is_nan()); match key {
2139 "min-width" => {
2140 if let Some(px) = px_value {
2141 conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(
2142 px,
2143 )));
2144 }
2145 }
2146 "max-width" => {
2147 if let Some(px) = px_value {
2148 conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(
2149 px,
2150 )));
2151 }
2152 }
2153 "min-height" => {
2154 if let Some(px) = px_value {
2155 conds.push(DynamicSelector::ContainerHeight(
2156 MinMaxRange::with_min(px),
2157 ));
2158 }
2159 }
2160 "max-height" => {
2161 if let Some(px) = px_value {
2162 conds.push(DynamicSelector::ContainerHeight(
2163 MinMaxRange::with_max(px),
2164 ));
2165 }
2166 }
2167 _ => {}
2168 }
2169 }
2170 }
2171 if !conds.is_empty() {
2172 return Some(conds);
2173 }
2174 }
2175
2176 if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
2178 return Some(vec![DynamicSelector::PrefersReducedMotion(
2179 BoolCondition::True,
2180 )]);
2181 }
2182
2183 if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
2185 return Some(vec![DynamicSelector::PrefersHighContrast(
2186 BoolCondition::True,
2187 )]);
2188 }
2189
2190 None
2191 }
2192
2193 #[cfg(feature = "parser")]
2195 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
2196 let query = query.trim();
2197
2198 if query.starts_with('(') && query.ends_with(')') {
2200 let inner = &query[1..query.len() - 1];
2201 if let Some((key, value)) = inner.split_once(':') {
2202 let key = key.trim();
2203 let value = value.trim();
2204
2205 let px_value = value
2211 .strip_suffix("px")
2212 .and_then(|v| v.trim().parse::<f32>().ok())
2213 .filter(|px| !px.is_nan());
2214
2215 match key {
2216 "min-width" => {
2217 if let Some(px) = px_value {
2218 return Some(vec![DynamicSelector::ViewportWidth(
2219 MinMaxRange::with_min(px),
2220 )]);
2221 }
2222 }
2223 "max-width" => {
2224 if let Some(px) = px_value {
2225 return Some(vec![DynamicSelector::ViewportWidth(
2226 MinMaxRange::with_max(px),
2227 )]);
2228 }
2229 }
2230 "min-height" => {
2231 if let Some(px) = px_value {
2232 return Some(vec![DynamicSelector::ViewportHeight(
2233 MinMaxRange::with_min(px),
2234 )]);
2235 }
2236 }
2237 "max-height" => {
2238 if let Some(px) = px_value {
2239 return Some(vec![DynamicSelector::ViewportHeight(
2240 MinMaxRange::with_max(px),
2241 )]);
2242 }
2243 }
2244 other => {
2245 if let Some(sel) = Self::parse_media_feature_inline(other, value) {
2247 return Some(vec![sel]);
2248 }
2249 }
2250 }
2251 }
2252 }
2253
2254 match query {
2256 "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
2257 "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
2258 "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
2259 _ => None,
2260 }
2261 }
2262
2263 #[cfg(feature = "parser")]
2266 fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
2267 match key {
2268 "orientation" => {
2269 if value.eq_ignore_ascii_case("portrait") {
2270 Some(DynamicSelector::Orientation(OrientationType::Portrait))
2271 } else if value.eq_ignore_ascii_case("landscape") {
2272 Some(DynamicSelector::Orientation(OrientationType::Landscape))
2273 } else {
2274 None
2275 }
2276 }
2277 "prefers-color-scheme" => {
2278 if value.eq_ignore_ascii_case("dark") {
2279 Some(DynamicSelector::Theme(ThemeCondition::Dark))
2280 } else if value.eq_ignore_ascii_case("light") {
2281 Some(DynamicSelector::Theme(ThemeCondition::Light))
2282 } else {
2283 None
2284 }
2285 }
2286 "prefers-reduced-motion" => {
2287 if value.eq_ignore_ascii_case("reduce") {
2288 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
2289 } else if value.eq_ignore_ascii_case("no-preference") {
2290 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
2291 } else {
2292 None
2293 }
2294 }
2295 "prefers-contrast" | "prefers-high-contrast" => {
2296 if value.eq_ignore_ascii_case("more")
2297 || value.eq_ignore_ascii_case("high")
2298 || value.eq_ignore_ascii_case("active")
2299 {
2300 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
2301 } else if value.eq_ignore_ascii_case("no-preference")
2302 || value.eq_ignore_ascii_case("none")
2303 {
2304 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
2305 } else {
2306 None
2307 }
2308 }
2309 _ => None,
2310 }
2311 }
2312
2313 #[cfg(feature = "parser")]
2315 fn parse_property_segment(
2316 segment: &str,
2317 inherited_conditions: &[DynamicSelector],
2318 key_map: &crate::props::property::CssKeyMap,
2319 ) -> Option<Vec<CssPropertyWithConditions>> {
2320 use crate::props::property::{
2321 parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
2322 CssPropertyType,
2323 };
2324
2325 let segment = segment.trim();
2326 if segment.is_empty() {
2327 return None;
2328 }
2329
2330 let (key, value) = segment.split_once(':')?;
2331 let key = key.trim();
2332 let value = value.trim();
2333
2334 let mut props = Vec::new();
2335 let conditions = if inherited_conditions.is_empty() {
2336 DynamicSelectorVec::from_const_slice(&[])
2337 } else {
2338 DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
2339 };
2340
2341 if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
2343 if let Ok(prop) = parse_css_property(prop_type, value) {
2344 props.push(CssPropertyWithConditions {
2345 property: prop,
2346 apply_if: conditions,
2347 });
2348 return Some(props);
2349 }
2350 }
2351
2352 if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
2354 if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
2355 for prop in expanded_props {
2356 props.push(CssPropertyWithConditions {
2357 property: prop,
2358 apply_if: conditions.clone(),
2359 });
2360 }
2361 return Some(props);
2362 }
2363 }
2364
2365 None
2366 }
2367}
2368
2369#[cfg(test)]
2370mod tests {
2371 use super::*;
2372
2373 #[test]
2374 fn test_inline_overflow_parse() {
2375 let style = "overflow: scroll;";
2376 let parsed = CssPropertyWithConditionsVec::parse(style);
2377 let props = parsed.into_library_owned_vec();
2378 assert!(
2379 !props.is_empty(),
2380 "Expected overflow to parse into at least 1 property"
2381 );
2382 }
2383
2384 #[test]
2385 fn test_inline_overflow_y_parse() {
2386 let style = "overflow-y: scroll;";
2387 let parsed = CssPropertyWithConditionsVec::parse(style);
2388 let props = parsed.into_library_owned_vec();
2389 assert!(
2390 !props.is_empty(),
2391 "Expected overflow-y to parse into at least 1 property"
2392 );
2393 }
2394
2395 #[test]
2396 fn test_inline_combined_style_with_overflow() {
2397 let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
2398 let parsed = CssPropertyWithConditionsVec::parse(style);
2399 let props = parsed.into_library_owned_vec();
2400 assert!(
2402 props.len() >= 9,
2403 "Expected at least 9 properties, got {}",
2404 props.len()
2405 );
2406 }
2407
2408 #[test]
2409 fn test_inline_grid_template_columns_parse() {
2410 use crate::props::layout::grid::GridTrackSizing;
2411 let style =
2412 "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
2413 let parsed = CssPropertyWithConditionsVec::parse(style);
2414 let props = parsed.into_library_owned_vec();
2415 let grid_cols = props
2417 .iter()
2418 .find(|p| matches!(p.property, CssProperty::GridTemplateColumns(_)))
2419 .expect("Expected GridTemplateColumns property");
2420
2421 if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
2422 let template = value.get_property().expect("Expected Exact value");
2423 let tracks = template.tracks.as_ref();
2424 assert_eq!(tracks.len(), 4, "Expected 4 tracks");
2425 for (i, track) in tracks.iter().enumerate() {
2426 assert!(
2427 matches!(track, GridTrackSizing::Fixed(_)),
2428 "Track {i} should be Fixed(160px), got {track:?}"
2429 );
2430 }
2431 } else {
2432 panic!("Expected CssProperty::GridTemplateColumns");
2433 }
2434 }
2435}
2436
2437#[cfg(test)]
2438#[allow(
2439 clippy::float_cmp,
2440 clippy::too_many_lines,
2441 clippy::cast_precision_loss,
2442 clippy::field_reassign_with_default,
2443 clippy::unreadable_literal
2444)]
2445mod autotest_generated {
2446 use core::cmp::Ordering;
2447 use std::collections::hash_map::DefaultHasher;
2448 use std::hash::{Hash, Hasher};
2449
2450 use super::*;
2451 use crate::props::property::CssPropertyType;
2452
2453 fn hash_of<T: Hash>(t: &T) -> u64 {
2458 let mut h = DefaultHasher::new();
2459 t.hash(&mut h);
2460 h.finish()
2461 }
2462
2463 fn paint_prop() -> CssProperty {
2465 CssProperty::const_none(CssPropertyType::TextColor)
2466 }
2467
2468 fn layout_prop() -> CssProperty {
2470 CssProperty::const_none(CssPropertyType::Width)
2471 }
2472
2473 fn all_selector_variants() -> Vec<DynamicSelector> {
2475 vec![
2476 DynamicSelector::Os(OsCondition::Linux),
2477 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
2478 DynamicSelector::Media(MediaType::Print),
2479 DynamicSelector::ViewportWidth(MinMaxRange::with_min(1.0)),
2480 DynamicSelector::ViewportHeight(MinMaxRange::with_max(2.0)),
2481 DynamicSelector::ContainerWidth(MinMaxRange::new(Some(1.0), Some(2.0))),
2482 DynamicSelector::ContainerHeight(MinMaxRange::new(None, None)),
2483 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")),
2484 DynamicSelector::Theme(ThemeCondition::Dark),
2485 DynamicSelector::AspectRatio(MinMaxRange::with_min(0.5)),
2486 DynamicSelector::Orientation(OrientationType::Portrait),
2487 DynamicSelector::PrefersReducedMotion(BoolCondition::True),
2488 DynamicSelector::PrefersHighContrast(BoolCondition::False),
2489 DynamicSelector::PseudoState(PseudoStateType::Hover),
2490 DynamicSelector::Language(LanguageCondition::Prefix(AzString::from_const_str("de"))),
2491 ]
2492 }
2493
2494 fn nasty_strings() -> Vec<String> {
2496 vec![
2497 String::new(),
2498 " ".to_string(),
2499 " \t\n\r ".to_string(),
2500 "\0".to_string(),
2501 "0".to_string(),
2502 "-0".to_string(),
2503 "-1".to_string(),
2504 "NaN".to_string(),
2505 "nan".to_string(),
2506 "inf".to_string(),
2507 "-inf".to_string(),
2508 "infinity".to_string(),
2509 "1e400".to_string(),
2510 i64::MAX.to_string(),
2511 i64::MIN.to_string(),
2512 u32::MAX.to_string(),
2513 u64::MAX.to_string(),
2514 "9999999999999999999999999999".to_string(),
2515 "1.7976931348623157e308".to_string(),
2516 "\u{1F600}".to_string(),
2517 "e\u{301}\u{301}\u{301}".to_string(),
2518 "日本語".to_string(),
2519 "\u{202e}gnome".to_string(),
2520 " linux ".to_string(),
2521 "linux;garbage".to_string(),
2522 "linux)".to_string(),
2523 "((((".to_string(),
2524 "))))".to_string(),
2525 ">=".to_string(),
2526 "<=<=<=".to_string(),
2527 ":::::".to_string(),
2528 "-".to_string(),
2529 "_".to_string(),
2530 ".".to_string(),
2531 "..".to_string(),
2532 "...".to_string(),
2533 "1.2.3.4.5.6".to_string(),
2534 "%s%s%n".to_string(),
2535 "\\x00\\xff".to_string(),
2536 "a".repeat(100_000),
2537 "1".repeat(100_000),
2538 ".".repeat(10_000),
2539 "(".repeat(5_000),
2540 "🦀".repeat(10_000),
2541 ]
2542 }
2543
2544 #[test]
2549 fn has_state_default_flags_only_normal_and_checked_false() {
2550 let flags = PseudoStateFlags::default();
2551 assert!(flags.has_state(PseudoStateType::Normal));
2553 assert!(flags.has_state(PseudoStateType::CheckedFalse));
2555 for state in [
2556 PseudoStateType::Hover,
2557 PseudoStateType::Active,
2558 PseudoStateType::Focus,
2559 PseudoStateType::Disabled,
2560 PseudoStateType::CheckedTrue,
2561 PseudoStateType::FocusWithin,
2562 PseudoStateType::Visited,
2563 PseudoStateType::SeatFocus,
2564 PseudoStateType::Backdrop,
2565 PseudoStateType::Dragging,
2566 PseudoStateType::DragOver,
2567 ] {
2568 assert!(!flags.has_state(state), "{state:?} must be off by default");
2569 }
2570 }
2571
2572 #[test]
2573 fn has_state_all_flags_set_reports_every_state_except_checked_false() {
2574 let flags = PseudoStateFlags {
2575 hover: true,
2576 active: true,
2577 focused: true,
2578 disabled: true,
2579 checked: true,
2580 focus_within: true,
2581 visited: true,
2582 backdrop: true,
2583 dragging: true,
2584 drag_over: true,
2585 placeholder: true,
2586 seat_focused: false,
2587 };
2588 assert!(flags.has_state(PseudoStateType::Hover));
2589 assert!(flags.has_state(PseudoStateType::Placeholder));
2590 assert!(flags.has_state(PseudoStateType::CheckedTrue));
2591 assert!(!flags.has_state(PseudoStateType::CheckedFalse));
2593 assert!(flags.has_state(PseudoStateType::DragOver));
2594 assert!(flags.has_state(PseudoStateType::Normal));
2595 }
2596
2597 #[test]
2598 fn has_state_checked_true_and_false_are_never_both_active() {
2599 for checked in [false, true] {
2600 let flags = PseudoStateFlags {
2601 checked,
2602 ..PseudoStateFlags::default()
2603 };
2604 assert_ne!(
2605 flags.has_state(PseudoStateType::CheckedTrue),
2606 flags.has_state(PseudoStateType::CheckedFalse),
2607 "checked={checked}: CheckedTrue/CheckedFalse must be complementary"
2608 );
2609 }
2610 }
2611
2612 #[test]
2617 fn variant_tag_matches_declared_repr_discriminants() {
2618 for (expected, sel) in all_selector_variants().iter().enumerate() {
2619 let expected = u8::try_from(expected).expect("15 variants fit in u8");
2620 assert_eq!(
2621 sel.variant_tag(),
2622 expected,
2623 "variant_tag drifted from the #[repr(C, u8)] discriminant for {sel:?}"
2624 );
2625 }
2626 }
2627
2628 #[test]
2629 fn variant_tag_is_unique_per_variant() {
2630 let variants = all_selector_variants();
2631 let mut tags: Vec<u8> = variants.iter().map(DynamicSelector::variant_tag).collect();
2632 tags.sort_unstable();
2633 tags.dedup();
2634 assert_eq!(tags.len(), variants.len(), "variant tags must be unique");
2635 }
2636
2637 #[test]
2638 fn ord_is_keyed_on_variant_tag_first() {
2639 let variants = all_selector_variants();
2640 for w in variants.windows(2) {
2641 assert_eq!(
2642 w[0].cmp(&w[1]),
2643 Ordering::Less,
2644 "selectors must sort by variant tag: {:?} < {:?}",
2645 w[0],
2646 w[1]
2647 );
2648 }
2649 }
2650
2651 #[test]
2652 fn hash_distinguishes_variants_carrying_the_same_payload() {
2653 let range = MinMaxRange::with_min(800.0);
2656 let a = DynamicSelector::ViewportWidth(range);
2657 let b = DynamicSelector::ContainerWidth(range);
2658 assert_ne!(hash_of(&a), hash_of(&b));
2659 assert_ne!(a.cmp(&b), Ordering::Equal);
2660 }
2661
2662 #[test]
2663 fn hash_and_ord_agree_for_nan_carrying_ranges() {
2664 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2667 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2668 assert_eq!(a.cmp(&b), Ordering::Equal);
2669 assert_eq!(hash_of(&a), hash_of(&b));
2670 }
2671
2672 #[test]
2679 fn nan_sentinel_range_selector_is_reflexive_under_partial_eq() {
2680 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2681 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2682 assert_eq!(
2683 a, b,
2684 "Eq requires reflexivity, but the NaN `max` sentinel breaks it"
2685 );
2686 }
2687
2688 #[test]
2690 fn ord_equal_implies_partial_eq_for_range_selectors() {
2691 let a = DynamicSelector::ViewportHeight(MinMaxRange::with_max(600.0));
2692 let b = a.clone();
2693 assert_eq!(a.cmp(&b), Ordering::Equal);
2694 assert!(
2695 a == b,
2696 "cmp() == Equal must imply == (Ord/Eq contract); NaN sentinel breaks it"
2697 );
2698 }
2699
2700 #[test]
2705 fn min_max_range_new_roundtrips_finite_values() {
2706 let r = MinMaxRange::new(Some(1.5), Some(9.5));
2707 assert_eq!(r.min(), Some(1.5));
2708 assert_eq!(r.max(), Some(9.5));
2709 }
2710
2711 #[test]
2712 fn min_max_range_new_none_encodes_nan_sentinel() {
2713 let r = MinMaxRange::new(None, None);
2714 assert!(r.min.is_nan());
2715 assert!(r.max.is_nan());
2716 assert_eq!(r.min(), None);
2717 assert_eq!(r.max(), None);
2718 }
2719
2720 #[test]
2721 fn min_max_range_new_nan_argument_is_indistinguishable_from_none() {
2722 let r = MinMaxRange::new(Some(f32::NAN), Some(f32::NAN));
2725 assert_eq!(r.min(), None);
2726 assert_eq!(r.max(), None);
2727 assert!(r.matches(0.0));
2728 assert!(r.matches(f32::MAX));
2729 }
2730
2731 #[test]
2732 fn min_max_range_with_min_and_with_max_leave_the_other_side_open() {
2733 let lo = MinMaxRange::with_min(-0.0);
2734 assert_eq!(lo.min(), Some(-0.0));
2735 assert_eq!(lo.max(), None);
2736
2737 let hi = MinMaxRange::with_max(f32::MAX);
2738 assert_eq!(hi.min(), None);
2739 assert_eq!(hi.max(), Some(f32::MAX));
2740 }
2741
2742 #[test]
2743 fn min_max_range_getters_survive_extreme_values() {
2744 for v in [
2745 0.0_f32,
2746 -0.0,
2747 f32::MIN,
2748 f32::MAX,
2749 f32::MIN_POSITIVE,
2750 f32::EPSILON,
2751 f32::INFINITY,
2752 f32::NEG_INFINITY,
2753 ] {
2754 let r = MinMaxRange::new(Some(v), Some(v));
2755 assert_eq!(r.min(), Some(v));
2756 assert_eq!(r.max(), Some(v));
2757 }
2758 }
2759
2760 #[test]
2765 fn matches_zero_boundary_is_inclusive() {
2766 assert!(MinMaxRange::with_min(0.0).matches(0.0));
2767 assert!(MinMaxRange::with_max(0.0).matches(0.0));
2768 assert!(MinMaxRange::new(Some(0.0), Some(0.0)).matches(0.0));
2769 assert!(MinMaxRange::with_min(0.0).matches(-0.0));
2771 assert!(MinMaxRange::with_max(0.0).matches(-0.0));
2772 }
2773
2774 #[test]
2775 fn matches_negative_values_are_ordered_correctly() {
2776 let r = MinMaxRange::new(Some(-10.0), Some(-1.0));
2777 assert!(r.matches(-10.0));
2778 assert!(r.matches(-5.0));
2779 assert!(r.matches(-1.0));
2780 assert!(!r.matches(-10.001));
2781 assert!(!r.matches(0.0));
2782 }
2783
2784 #[test]
2785 fn matches_at_float_extremes_does_not_panic() {
2786 let open = MinMaxRange::new(None, None);
2787 let bounded = MinMaxRange::new(Some(f32::MIN), Some(f32::MAX));
2788 for v in [
2789 f32::MIN,
2790 f32::MAX,
2791 f32::INFINITY,
2792 f32::NEG_INFINITY,
2793 f32::MIN_POSITIVE,
2794 -f32::MIN_POSITIVE,
2795 ] {
2796 assert!(open.matches(v), "open range must accept {v}");
2798 }
2799 assert!(bounded.matches(0.0));
2800 assert!(bounded.matches(f32::MIN));
2801 assert!(bounded.matches(f32::MAX));
2802 assert!(!bounded.matches(f32::INFINITY));
2804 assert!(!bounded.matches(f32::NEG_INFINITY));
2805 }
2806
2807 #[test]
2808 fn matches_nan_value_is_rejected_by_any_real_bound() {
2809 assert!(!MinMaxRange::with_min(0.0).matches(f32::NAN));
2811 assert!(!MinMaxRange::with_max(0.0).matches(f32::NAN));
2812 assert!(!MinMaxRange::new(Some(1.0), Some(2.0)).matches(f32::NAN));
2813 assert!(MinMaxRange::new(None, None).matches(f32::NAN));
2815 }
2816
2817 #[test]
2818 fn matches_infinite_bounds_are_deterministic() {
2819 let min_inf = MinMaxRange::with_min(f32::INFINITY);
2820 assert!(!min_inf.matches(f32::MAX));
2821 assert!(min_inf.matches(f32::INFINITY));
2822
2823 let max_neg_inf = MinMaxRange::with_max(f32::NEG_INFINITY);
2824 assert!(!max_neg_inf.matches(f32::MIN));
2825 assert!(max_neg_inf.matches(f32::NEG_INFINITY));
2826 }
2827
2828 #[test]
2829 fn matches_inverted_range_matches_nothing() {
2830 let inverted = MinMaxRange::new(Some(10.0), Some(5.0));
2831 for v in [-1.0_f32, 0.0, 5.0, 7.5, 10.0, 1e30] {
2832 assert!(!inverted.matches(v), "inverted range must reject {v}");
2833 }
2834 }
2835
2836 #[test]
2841 fn os_condition_from_system_platform_covers_every_platform() {
2842 use crate::system::{DesktopEnvironment, Platform};
2843 assert_eq!(
2844 OsCondition::from_system_platform(&Platform::Windows),
2845 OsCondition::Windows
2846 );
2847 assert_eq!(
2848 OsCondition::from_system_platform(&Platform::MacOs),
2849 OsCondition::MacOS
2850 );
2851 assert_eq!(
2852 OsCondition::from_system_platform(&Platform::Ios),
2853 OsCondition::IOS
2854 );
2855 assert_eq!(
2856 OsCondition::from_system_platform(&Platform::Android),
2857 OsCondition::Android
2858 );
2859 assert_eq!(
2860 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Gnome)),
2861 OsCondition::Linux
2862 );
2863 assert_eq!(
2864 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Other(
2865 AzString::from_const_str(""),
2866 ))),
2867 OsCondition::Linux
2868 );
2869 assert_eq!(
2871 OsCondition::from_system_platform(&Platform::Unknown),
2872 OsCondition::Any
2873 );
2874 }
2875
2876 #[test]
2881 fn os_version_new_stores_fields_verbatim_at_boundaries() {
2882 for id in [0, 1, u32::MAX - 1, u32::MAX] {
2883 let v = OsVersion::new(OsFamily::Linux, id);
2884 assert_eq!(v.os, OsFamily::Linux);
2885 assert_eq!(v.version_id, id);
2886 }
2887 }
2888
2889 #[test]
2890 fn os_version_unknown_is_the_default_and_has_id_zero() {
2891 let u = OsVersion::unknown();
2892 assert_eq!(u.version_id, 0);
2893 assert_eq!(u, OsVersion::default());
2894 }
2895
2896 #[test]
2897 fn os_version_compare_is_none_across_families() {
2898 let win = OsVersion::WIN_11;
2899 let mac = OsVersion::MACOS_SONOMA;
2900 assert_eq!(win.compare(&mac), None);
2901 assert_eq!(mac.compare(&win), None);
2902 assert!(!win.is_at_least(&mac));
2905 assert!(!win.is_at_most(&mac));
2906 assert!(!win.is_exactly(&mac));
2907 }
2908
2909 #[test]
2910 fn os_version_compare_within_family_orders_by_id() {
2911 assert_eq!(
2912 OsVersion::WIN_10.compare(&OsVersion::WIN_11),
2913 Some(Ordering::Less)
2914 );
2915 assert_eq!(
2916 OsVersion::WIN_11.compare(&OsVersion::WIN_10),
2917 Some(Ordering::Greater)
2918 );
2919 assert_eq!(
2920 OsVersion::WIN_11.compare(&OsVersion::WIN_11_21H2),
2921 Some(Ordering::Equal)
2922 );
2923 }
2924
2925 #[test]
2926 fn os_version_compare_at_id_extremes() {
2927 let lo = OsVersion::new(OsFamily::Android, 0);
2928 let hi = OsVersion::new(OsFamily::Android, u32::MAX);
2929 assert_eq!(lo.compare(&hi), Some(Ordering::Less));
2930 assert_eq!(hi.compare(&lo), Some(Ordering::Greater));
2931 assert!(hi.is_at_least(&lo));
2932 assert!(!hi.is_at_most(&lo));
2933 assert!(lo.is_at_most(&hi));
2934 }
2935
2936 #[test]
2937 fn os_version_predicates_are_reflexive_and_consistent() {
2938 for v in [
2939 OsVersion::unknown(),
2940 OsVersion::WIN_XP,
2941 OsVersion::MACOS_TAHOE,
2942 OsVersion::IOS_18,
2943 OsVersion::ANDROID_15,
2944 OsVersion::new(OsFamily::Linux, u32::MAX),
2945 ] {
2946 assert!(v.is_at_least(&v), "{v:?} >= itself");
2947 assert!(v.is_at_most(&v), "{v:?} <= itself");
2948 assert!(v.is_exactly(&v), "{v:?} == itself");
2949 }
2950 }
2951
2952 #[test]
2953 fn os_version_at_least_is_the_strict_complement_of_less_than() {
2954 let a = OsVersion::WIN_10;
2955 let b = OsVersion::WIN_11;
2956 assert!(!a.is_at_least(&b));
2957 assert!(a.is_at_most(&b));
2958 assert!(!a.is_exactly(&b));
2959 assert!(b.is_at_least(&a));
2960 assert!(!b.is_at_most(&a));
2961 }
2962
2963 #[test]
2964 fn os_version_unknown_never_satisfies_a_min_constraint_on_a_real_version() {
2965 assert!(!OsVersion::unknown().is_at_least(&OsVersion::WIN_11));
2968 assert!(!OsVersion::unknown().is_at_least(&OsVersion::LINUX_6_0));
2969 }
2970
2971 #[test]
2976 fn parse_os_version_valid_minimal_positive_controls() {
2977 assert_eq!(
2978 parse_os_version(OsFamily::Windows, "11"),
2979 Some(OsVersion::WIN_11)
2980 );
2981 assert_eq!(
2982 parse_os_version(OsFamily::MacOS, "sonoma"),
2983 Some(OsVersion::MACOS_SONOMA)
2984 );
2985 assert_eq!(
2986 parse_os_version(OsFamily::IOS, "17.0"),
2987 Some(OsVersion::IOS_17)
2988 );
2989 assert_eq!(
2990 parse_os_version(OsFamily::Android, "tiramisu"),
2991 Some(OsVersion::ANDROID_13)
2992 );
2993 assert_eq!(
2994 parse_os_version(OsFamily::Linux, "6.0"),
2995 Some(OsVersion::LINUX_6_0)
2996 );
2997 }
2998
2999 #[test]
3000 fn parse_os_version_trims_and_lowercases() {
3001 assert_eq!(
3002 parse_os_version(OsFamily::Windows, " WIN-11 "),
3003 Some(OsVersion::WIN_11)
3004 );
3005 assert_eq!(
3006 parse_os_version(OsFamily::MacOS, "\tBIG-SUR\n"),
3007 Some(OsVersion::MACOS_BIG_SUR)
3008 );
3009 assert_eq!(
3010 parse_os_version(OsFamily::Android, " KitKat "),
3011 Some(OsVersion::ANDROID_KITKAT)
3012 );
3013 }
3014
3015 #[test]
3016 fn parse_os_version_empty_and_whitespace_is_none_for_every_family() {
3017 for os in [
3018 OsFamily::Windows,
3019 OsFamily::MacOS,
3020 OsFamily::IOS,
3021 OsFamily::Android,
3022 OsFamily::Linux,
3023 ] {
3024 assert_eq!(parse_os_version(os, ""), None, "{os:?} empty");
3025 assert_eq!(parse_os_version(os, " "), None, "{os:?} spaces");
3026 assert_eq!(parse_os_version(os, "\t\n\r"), None, "{os:?} ws");
3027 }
3028 }
3029
3030 #[test]
3031 fn parse_os_version_garbage_and_unicode_never_panics() {
3032 for os in [
3033 OsFamily::Windows,
3034 OsFamily::MacOS,
3035 OsFamily::IOS,
3036 OsFamily::Android,
3037 OsFamily::Linux,
3038 ] {
3039 for s in nasty_strings() {
3040 let a = parse_os_version(os, &s);
3042 let b = parse_os_version(os, &s);
3043 assert_eq!(a, b, "{os:?} not deterministic for {s:?}");
3044 }
3045 }
3046 }
3047
3048 #[test]
3049 fn parse_os_version_leading_trailing_junk_is_rejected() {
3050 assert_eq!(parse_os_version(OsFamily::Windows, "win-11;drop"), None);
3051 assert_eq!(parse_os_version(OsFamily::MacOS, "sonoma!"), None);
3052 assert_eq!(parse_os_version(OsFamily::IOS, "17.0.0.0"), None);
3053 assert_eq!(parse_os_version(OsFamily::Android, "api"), None);
3054 assert_eq!(parse_os_version(OsFamily::Android, "api abc"), None);
3055 }
3056
3057 #[test]
3058 fn parse_windows_version_prefix_forms_all_collapse_to_the_same_version() {
3059 for s in [
3060 "11",
3061 "win11",
3062 "win-11",
3063 "win_11",
3064 "windows11",
3065 "windows-11",
3066 "windows_11",
3067 ] {
3068 assert_eq!(
3069 parse_windows_version(s),
3070 Some(OsVersion::WIN_11),
3071 "{s} should parse to WIN_11"
3072 );
3073 }
3074 }
3075
3076 #[test]
3077 fn parse_windows_version_bare_prefix_and_separator_only_are_none() {
3078 assert_eq!(parse_windows_version("win"), None);
3079 assert_eq!(parse_windows_version("windows"), None);
3080 assert_eq!(parse_windows_version("win-"), None);
3081 assert_eq!(parse_windows_version("windows_"), None);
3082 assert_eq!(parse_windows_version(""), None);
3083 }
3084
3085 #[test]
3086 fn parse_windows_version_nt_aliases_agree_with_names() {
3087 assert_eq!(parse_windows_version("xp"), parse_windows_version("nt5.1"));
3088 assert_eq!(parse_windows_version("vista"), parse_windows_version("6.0"));
3089 assert_eq!(parse_windows_version("8.1"), parse_windows_version("8-1"));
3090 assert_eq!(parse_windows_version("10"), Some(OsVersion::WIN_10));
3091 }
3092
3093 #[test]
3094 fn parse_windows_version_boundary_numbers_are_none() {
3095 for s in ["0", "-0", "-1", "NaN", "inf", "99999999999999999999"] {
3096 assert_eq!(parse_windows_version(s), None, "{s} must not parse");
3097 }
3098 }
3099
3100 #[test]
3101 fn parse_windows_version_huge_and_unicode_input_terminates() {
3102 assert_eq!(parse_windows_version(&"win".repeat(200_000)), None);
3103 assert_eq!(parse_windows_version(&"1".repeat(1_000_000)), None);
3104 assert_eq!(parse_windows_version("\u{1F600}"), None);
3105 assert_eq!(parse_windows_version("win-\u{1F600}"), None);
3106 }
3107
3108 #[test]
3109 fn strip_os_prefix_strips_prefix_and_optional_separator() {
3110 assert_eq!(strip_os_prefix("win-11", &["win"]), "11");
3111 assert_eq!(strip_os_prefix("win_11", &["win"]), "11");
3112 assert_eq!(strip_os_prefix("win11", &["win"]), "11");
3113 assert_eq!(strip_os_prefix("windows-11", &["windows", "win"]), "11");
3115 assert_eq!(
3116 strip_os_prefix("windows-11", &["win", "windows"]),
3117 "dows-11"
3118 );
3119 }
3120
3121 #[test]
3122 fn strip_os_prefix_leaves_non_matching_input_untouched() {
3123 assert_eq!(strip_os_prefix("11", &["win"]), "11");
3124 assert_eq!(strip_os_prefix("", &["win"]), "");
3125 assert_eq!(strip_os_prefix("anything", &[]), "anything");
3126 assert_eq!(strip_os_prefix("\u{1F600}win", &["win"]), "\u{1F600}win");
3127 }
3128
3129 #[test]
3130 fn strip_os_prefix_only_strips_one_separator() {
3131 assert_eq!(strip_os_prefix("win--11", &["win"]), "-11");
3132 assert_eq!(strip_os_prefix("win-", &["win"]), "");
3133 assert_eq!(strip_os_prefix("win", &["win"]), "");
3134 }
3135
3136 #[test]
3137 fn strip_os_prefix_with_empty_prefix_is_identity() {
3138 assert_eq!(strip_os_prefix("日本語", &[""]), "日本語");
3141 assert_eq!(strip_os_prefix("-日本語", &[""]), "日本語");
3142 }
3143
3144 #[test]
3145 fn parse_macos_version_names_and_numbers_agree() {
3146 assert_eq!(parse_macos_version("cheetah"), parse_macos_version("10.0"));
3147 assert_eq!(
3148 parse_macos_version("big-sur"),
3149 parse_macos_version("bigsur")
3150 );
3151 assert_eq!(parse_macos_version("bigsur"), parse_macos_version("11.0"));
3152 assert_eq!(parse_macos_version("tahoe"), Some(OsVersion::MACOS_TAHOE));
3153 assert_eq!(
3154 parse_macos_version("snow-leopard"),
3155 parse_macos_version("snowleopard")
3156 );
3157 }
3158
3159 #[test]
3160 fn parse_macos_version_rejects_junk_and_terminates_on_huge_input() {
3161 for s in ["", " ", "sonoma ", "SONOMA", "10.16", "27", "🍎"] {
3162 assert_eq!(parse_macos_version(s), None, "{s:?} must not parse");
3163 }
3164 assert_eq!(parse_macos_version(&"10.".repeat(100_000)), None);
3165 }
3166
3167 #[test]
3168 fn parse_macos_version_is_ordered_monotonically() {
3169 let names = [
3170 "cheetah", "puma", "jaguar", "panther", "tiger", "leopard", "lion", "mojave",
3171 "catalina", "bigsur", "monterey", "ventura", "sonoma", "sequoia", "tahoe",
3172 ];
3173 let ids: Vec<u32> = names
3174 .iter()
3175 .map(|n| {
3176 parse_macos_version(n)
3177 .unwrap_or_else(|| panic!("{n} must parse"))
3178 .version_id
3179 })
3180 .collect();
3181 for w in ids.windows(2) {
3182 assert!(w[0] < w[1], "macOS version ids must increase: {w:?}");
3183 }
3184 }
3185
3186 #[test]
3187 fn parse_ios_version_boundaries() {
3188 assert_eq!(parse_ios_version("1"), Some(OsVersion::IOS_1));
3189 assert_eq!(parse_ios_version("18.0"), Some(OsVersion::IOS_18));
3190 assert_eq!(parse_ios_version("0"), None);
3191 assert_eq!(parse_ios_version("19"), None);
3192 assert_eq!(parse_ios_version(""), None);
3193 assert_eq!(parse_ios_version("-1"), None);
3194 assert_eq!(parse_ios_version("NaN"), None);
3195 assert_eq!(parse_ios_version(&"9".repeat(500_000)), None);
3196 }
3197
3198 #[test]
3199 fn parse_android_version_api_level_escape_hatch() {
3200 assert_eq!(
3201 parse_android_version("api34"),
3202 Some(OsVersion::new(OsFamily::Android, 34))
3203 );
3204 assert_eq!(
3205 parse_android_version("api 34"),
3206 Some(OsVersion::new(OsFamily::Android, 34))
3207 );
3208 assert_eq!(
3209 parse_android_version("api0"),
3210 Some(OsVersion::new(OsFamily::Android, 0))
3211 );
3212 assert_eq!(
3213 parse_android_version(&format!("api{}", u32::MAX)),
3214 Some(OsVersion::new(OsFamily::Android, u32::MAX))
3215 );
3216 }
3217
3218 #[test]
3219 fn parse_android_version_api_level_out_of_range_is_none_not_a_panic() {
3220 assert_eq!(parse_android_version("api4294967296"), None);
3222 assert_eq!(parse_android_version("api-1"), None);
3223 assert_eq!(
3224 parse_android_version("api+1"),
3225 Some(OsVersion::new(OsFamily::Android, 1))
3226 );
3227 assert_eq!(parse_android_version("apiNaN"), None);
3228 assert_eq!(
3229 parse_android_version(&format!("api{}", "9".repeat(100_000))),
3230 None
3231 );
3232 }
3233
3234 #[test]
3235 fn parse_android_version_named_releases() {
3236 assert_eq!(parse_android_version("q"), Some(OsVersion::ANDROID_10));
3237 assert_eq!(parse_android_version("13"), parse_android_version("t"));
3238 assert_eq!(
3239 parse_android_version("13"),
3240 parse_android_version("tiramisu")
3241 );
3242 assert_eq!(parse_android_version("15"), Some(OsVersion::ANDROID_15));
3243 assert_eq!(parse_android_version(""), None);
3244 assert_eq!(parse_android_version("🤖"), None);
3245 }
3246
3247 #[test]
3248 fn parse_linux_version_accepts_bare_major_and_prefixes() {
3249 assert_eq!(
3250 parse_linux_version("5"),
3251 Some(OsVersion::new(OsFamily::Linux, 5000))
3252 );
3253 assert_eq!(parse_linux_version("6.0"), Some(OsVersion::LINUX_6_0));
3254 assert_eq!(parse_linux_version("linux6.0"), Some(OsVersion::LINUX_6_0));
3255 assert_eq!(parse_linux_version("linux-6.0"), Some(OsVersion::LINUX_6_0));
3256 assert_eq!(parse_linux_version("linux_6.0"), Some(OsVersion::LINUX_6_0));
3257 assert_eq!(
3258 parse_linux_version("6.17.0"),
3259 Some(OsVersion::new(OsFamily::Linux, 6170))
3260 );
3261 }
3262
3263 #[test]
3264 fn parse_linux_version_rejects_malformed_and_unicode() {
3265 for s in [
3266 "", " ", ".", "..", "-1", "6.-1", "6.x", "x.6", "NaN", "inf", "🐧", "linux", "linux-",
3267 ] {
3268 assert_eq!(parse_linux_version(s), None, "{s:?} must not parse");
3269 }
3270 }
3271
3272 #[test]
3273 fn parse_linux_version_ignores_everything_past_the_patch_component() {
3274 assert_eq!(
3278 parse_linux_version("6.1.2"),
3279 Some(OsVersion::new(OsFamily::Linux, 6012))
3280 );
3281 assert_eq!(
3282 parse_linux_version("6.1.2.3"),
3283 Some(OsVersion::new(OsFamily::Linux, 6012))
3284 );
3285 assert_eq!(
3286 parse_linux_version("6.0.0.0extra"),
3287 Some(OsVersion::LINUX_6_0),
3288 "a 4th component is never parsed, so trailing junk is accepted"
3289 );
3290 }
3291
3292 #[test]
3297 fn parse_linux_version_huge_major_does_not_overflow() {
3298 assert_eq!(
3299 parse_linux_version("5000000"),
3300 None,
3301 "out-of-range kernel major must be rejected, not overflow u32"
3302 );
3303 }
3304
3305 #[test]
3307 fn parse_linux_version_huge_minor_does_not_overflow() {
3308 assert_eq!(
3309 parse_linux_version("1.999999999"),
3310 None,
3311 "out-of-range kernel minor must be rejected, not overflow u32"
3312 );
3313 }
3314
3315 #[test]
3316 fn parse_linux_version_max_u32_component_is_rejected_by_parse_not_by_math() {
3317 assert_eq!(parse_linux_version("4294967296"), None);
3319 }
3320
3321 #[test]
3326 fn linux_desktop_env_from_system_maps_unknown_des_to_other() {
3327 use crate::system::DesktopEnvironment;
3328 assert_eq!(
3329 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Gnome),
3330 LinuxDesktopEnv::Gnome
3331 );
3332 assert_eq!(
3333 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Kde),
3334 LinuxDesktopEnv::KDE
3335 );
3336 for name in ["xfce", "", "🖥", &"x".repeat(10_000)] {
3338 assert_eq!(
3339 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Other(
3340 AzString::from(name.to_string())
3341 )),
3342 LinuxDesktopEnv::Other
3343 );
3344 }
3345 }
3346
3347 #[test]
3348 fn theme_condition_from_system_theme_is_total() {
3349 use crate::system::Theme;
3350 assert_eq!(
3351 ThemeCondition::from_system_theme(Theme::Light),
3352 ThemeCondition::Light
3353 );
3354 assert_eq!(
3355 ThemeCondition::from_system_theme(Theme::Dark),
3356 ThemeCondition::Dark
3357 );
3358 }
3359
3360 #[test]
3365 fn language_exact_is_case_insensitive_and_strict() {
3366 let cond = LanguageCondition::Exact(AzString::from_const_str("de-DE"));
3367 assert!(cond.matches("de-DE"));
3368 assert!(cond.matches("DE-de"));
3369 assert!(!cond.matches("de"));
3370 assert!(!cond.matches("de-AT"));
3371 assert!(!cond.matches("de-DE-x"));
3372 assert!(!cond.matches(""));
3373 }
3374
3375 #[test]
3376 fn language_prefix_matches_subtags_only_at_a_dash_boundary() {
3377 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
3378 assert!(cond.matches("de"));
3379 assert!(cond.matches("de-DE"));
3380 assert!(cond.matches("DE-at"));
3381 assert!(!cond.matches("den"));
3383 assert!(!cond.matches("deu"));
3384 assert!(!cond.matches("d"));
3385 assert!(!cond.matches(""));
3386 }
3387
3388 #[test]
3389 fn language_empty_prefix_matches_only_the_empty_tag() {
3390 let cond = LanguageCondition::Prefix(AzString::from_const_str(""));
3396 assert!(cond.matches(""));
3397 assert!(!cond.matches("en-US"));
3398 }
3399
3400 #[test]
3401 fn language_prefix_longer_than_input_is_false() {
3402 let cond = LanguageCondition::Prefix(AzString::from_const_str("de-DE-1996"));
3403 assert!(!cond.matches("de"));
3404 assert!(!cond.matches(""));
3405 }
3406
3407 #[test]
3408 fn language_matches_huge_input_terminates() {
3409 let cond = LanguageCondition::Prefix(AzString::from_const_str("en"));
3410 let huge = format!("en-{}", "a".repeat(1_000_000));
3411 assert!(cond.matches(&huge));
3412 let cond_exact = LanguageCondition::Exact(AzString::from_const_str("en"));
3413 assert!(!cond_exact.matches(&huge));
3414 }
3415
3416 #[test]
3422 fn language_prefix_does_not_panic_on_multibyte_language_tag() {
3423 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
3424 assert!(!cond.matches("日本語"));
3426 }
3427
3428 #[test]
3430 fn language_prefix_does_not_panic_on_two_byte_language_tag() {
3431 let cond = LanguageCondition::Prefix(AzString::from_const_str("d"));
3432 assert!(!cond.matches("é"));
3433 }
3434
3435 #[test]
3440 fn context_from_system_style_default_is_coherent() {
3441 let style = crate::system::SystemStyle::default();
3442 let ctx = DynamicSelectorContext::from_system_style(&style);
3443 assert_eq!(ctx.os, OsCondition::Any);
3445 assert_eq!(ctx.desktop_env, OptionLinuxDesktopEnv::None);
3446 assert_eq!(ctx.de_version, 0);
3447 assert_eq!(ctx.theme, ThemeCondition::Light);
3448 assert_eq!(ctx.media_type, MediaType::Screen);
3449 assert_eq!(ctx.viewport_width, DEFAULT_VIEWPORT_WIDTH);
3450 assert_eq!(ctx.viewport_height, DEFAULT_VIEWPORT_HEIGHT);
3451 assert!(ctx.container_width.is_nan());
3454 assert!(ctx.container_height.is_nan());
3455 assert!(ctx.window_focused);
3456 }
3457
3458 #[test]
3459 fn context_from_system_style_linux_carries_the_desktop_env() {
3460 use crate::system::{DesktopEnvironment, Platform};
3461 let mut style = crate::system::SystemStyle::default();
3462 style.platform = Platform::Linux(DesktopEnvironment::Kde);
3463 let ctx = DynamicSelectorContext::from_system_style(&style);
3464 assert_eq!(ctx.os, OsCondition::Linux);
3465 assert_eq!(
3466 ctx.desktop_env,
3467 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE)
3468 );
3469 }
3470
3471 #[test]
3472 fn with_viewport_updates_orientation_and_is_deterministic_at_extremes() {
3473 let base = DynamicSelectorContext::default();
3474 assert_eq!(
3475 base.with_viewport(1920.0, 1080.0).orientation,
3476 OrientationType::Landscape
3477 );
3478 assert_eq!(
3479 base.with_viewport(1080.0, 1920.0).orientation,
3480 OrientationType::Portrait
3481 );
3482 assert_eq!(
3484 base.with_viewport(500.0, 500.0).orientation,
3485 OrientationType::Portrait
3486 );
3487 assert_eq!(
3489 base.with_viewport(f32::NAN, f32::NAN).orientation,
3490 OrientationType::Portrait
3491 );
3492 assert_eq!(
3493 base.with_viewport(f32::INFINITY, f32::NEG_INFINITY)
3494 .orientation,
3495 OrientationType::Landscape
3496 );
3497 let z = base.with_viewport(0.0, 0.0);
3499 assert_eq!(z.viewport_width, 0.0);
3500 assert_eq!(z.orientation, OrientationType::Portrait);
3501 let neg = base.with_viewport(-100.0, -200.0);
3502 assert_eq!(neg.orientation, OrientationType::Landscape);
3503 }
3504
3505 #[test]
3506 fn with_viewport_does_not_disturb_unrelated_fields() {
3507 let base = DynamicSelectorContext::default();
3508 let updated = base.with_viewport(1.0, 2.0);
3509 assert_eq!(updated.os, base.os);
3510 assert_eq!(updated.theme, base.theme);
3511 assert_eq!(updated.language, base.language);
3512 assert_eq!(updated.pseudo_state, base.pseudo_state);
3513 }
3514
3515 #[test]
3516 fn with_container_stores_name_and_extreme_dimensions() {
3517 let base = DynamicSelectorContext::default();
3518 let named = base.with_container(
3519 f32::MAX,
3520 f32::NEG_INFINITY,
3521 Some(AzString::from_const_str("sidebar")),
3522 );
3523 assert_eq!(named.container_width, f32::MAX);
3524 assert_eq!(named.container_height, f32::NEG_INFINITY);
3525 assert_eq!(
3526 named.container_name.as_ref(),
3527 Some(&AzString::from_const_str("sidebar"))
3528 );
3529
3530 let unnamed = base.with_container(0.0, 0.0, None);
3531 assert_eq!(unnamed.container_name.as_ref(), None);
3532 }
3533
3534 #[test]
3535 fn with_container_nan_dimensions_stay_unmatched() {
3536 let ctx = DynamicSelectorContext::default().with_container(f32::NAN, f32::NAN, None);
3537 let open = DynamicSelector::ContainerWidth(MinMaxRange::new(None, None));
3540 assert!(!open.matches(&ctx));
3541 let open_h = DynamicSelector::ContainerHeight(MinMaxRange::new(None, None));
3542 assert!(!open_h.matches(&ctx));
3543 }
3544
3545 #[test]
3546 fn with_pseudo_state_replaces_the_whole_flag_set() {
3547 let base = DynamicSelectorContext::default();
3548 let hovered = base.with_pseudo_state(PseudoStateFlags {
3549 hover: true,
3550 ..PseudoStateFlags::default()
3551 });
3552 assert!(hovered.pseudo_state.hover);
3553 assert!(!hovered.pseudo_state.active);
3554 let active = hovered.with_pseudo_state(PseudoStateFlags {
3556 active: true,
3557 ..PseudoStateFlags::default()
3558 });
3559 assert!(!active.pseudo_state.hover);
3560 assert!(active.pseudo_state.active);
3561 }
3562
3563 #[test]
3564 fn viewport_breakpoint_changed_detects_crossings_only() {
3565 let bps = [480.0_f32, 768.0, 1024.0];
3566 let base = DynamicSelectorContext::default();
3567 let small = base.with_viewport(320.0, 480.0);
3568 let medium = base.with_viewport(800.0, 600.0);
3569 let also_medium = base.with_viewport(900.0, 600.0);
3570
3571 assert!(small.viewport_breakpoint_changed(&medium, &bps));
3572 assert!(medium.viewport_breakpoint_changed(&small, &bps));
3573 assert!(!medium.viewport_breakpoint_changed(&also_medium, &bps));
3574 assert!(!small.viewport_breakpoint_changed(&small, &bps));
3575 }
3576
3577 #[test]
3578 fn viewport_breakpoint_changed_is_exactly_on_the_boundary() {
3579 let bps = [800.0_f32];
3580 let base = DynamicSelectorContext::default();
3581 let at = base.with_viewport(800.0, 600.0);
3583 let just_below = base.with_viewport(799.99, 600.0);
3584 assert!(at.viewport_breakpoint_changed(&just_below, &bps));
3585 assert!(!at.viewport_breakpoint_changed(&at, &bps));
3586 }
3587
3588 #[test]
3589 fn viewport_breakpoint_changed_handles_empty_and_degenerate_breakpoints() {
3590 let base = DynamicSelectorContext::default();
3591 let a = base.with_viewport(100.0, 100.0);
3592 let b = base.with_viewport(5000.0, 100.0);
3593 assert!(!a.viewport_breakpoint_changed(&b, &[]));
3594 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NAN]));
3596 assert!(!a.viewport_breakpoint_changed(&b, &[f32::INFINITY]));
3598 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NEG_INFINITY]));
3599 let nan_vp = base.with_viewport(f32::NAN, 100.0);
3601 assert!(nan_vp.viewport_breakpoint_changed(&b, &[800.0]));
3602 }
3603
3604 #[test]
3605 fn viewport_breakpoint_changed_with_many_breakpoints_terminates() {
3606 let bps: Vec<f32> = (0..100_000).map(|i| i as f32).collect();
3607 let base = DynamicSelectorContext::default();
3608 let a = base.with_viewport(0.0, 100.0);
3609 let b = base.with_viewport(99_999.0, 100.0);
3610 assert!(a.viewport_breakpoint_changed(&b, &bps));
3611 }
3612
3613 #[test]
3618 fn match_os_any_matches_everything() {
3619 for actual in [
3620 OsCondition::Any,
3621 OsCondition::Apple,
3622 OsCondition::MacOS,
3623 OsCondition::IOS,
3624 OsCondition::Linux,
3625 OsCondition::Windows,
3626 OsCondition::Android,
3627 OsCondition::Web,
3628 ] {
3629 assert!(DynamicSelector::match_os(OsCondition::Any, actual));
3630 }
3631 }
3632
3633 #[test]
3634 fn match_os_apple_is_the_macos_ios_union() {
3635 assert!(DynamicSelector::match_os(
3636 OsCondition::Apple,
3637 OsCondition::MacOS
3638 ));
3639 assert!(DynamicSelector::match_os(
3640 OsCondition::Apple,
3641 OsCondition::IOS
3642 ));
3643 assert!(!DynamicSelector::match_os(
3644 OsCondition::Apple,
3645 OsCondition::Linux
3646 ));
3647 assert!(!DynamicSelector::match_os(
3649 OsCondition::MacOS,
3650 OsCondition::Apple
3651 ));
3652 }
3653
3654 #[test]
3655 fn match_os_concrete_conditions_require_equality() {
3656 assert!(DynamicSelector::match_os(
3657 OsCondition::Linux,
3658 OsCondition::Linux
3659 ));
3660 assert!(!DynamicSelector::match_os(
3661 OsCondition::Linux,
3662 OsCondition::Windows
3663 ));
3664 assert!(!DynamicSelector::match_os(
3666 OsCondition::Windows,
3667 OsCondition::Any
3668 ));
3669 }
3670
3671 #[test]
3672 fn match_os_version_min_max_exact_at_zero_and_u32_max() {
3673 let zero = OsVersion::new(OsFamily::Linux, 0);
3674 let max = OsVersion::new(OsFamily::Linux, u32::MAX);
3675 let none = OptionLinuxDesktopEnv::None;
3676
3677 assert!(DynamicSelector::match_os_version(
3678 &OsVersionCondition::Min(zero),
3679 max,
3680 none,
3681 0
3682 ));
3683 assert!(!DynamicSelector::match_os_version(
3684 &OsVersionCondition::Min(max),
3685 zero,
3686 none,
3687 0
3688 ));
3689 assert!(DynamicSelector::match_os_version(
3690 &OsVersionCondition::Max(max),
3691 zero,
3692 none,
3693 0
3694 ));
3695 assert!(DynamicSelector::match_os_version(
3696 &OsVersionCondition::Exact(max),
3697 max,
3698 none,
3699 0
3700 ));
3701 assert!(!DynamicSelector::match_os_version(
3702 &OsVersionCondition::Exact(zero),
3703 max,
3704 none,
3705 0
3706 ));
3707 }
3708
3709 #[test]
3710 fn match_os_version_cross_family_never_matches() {
3711 let none = OptionLinuxDesktopEnv::None;
3712 for cond in [
3714 OsVersionCondition::Min(OsVersion::WIN_10),
3715 OsVersionCondition::Max(OsVersion::WIN_10),
3716 OsVersionCondition::Exact(OsVersion::WIN_10),
3717 ] {
3718 assert!(
3719 !DynamicSelector::match_os_version(&cond, OsVersion::MACOS_SONOMA, none, 0),
3720 "{cond:?} must not match a macOS runtime"
3721 );
3722 }
3723 }
3724
3725 #[test]
3726 fn match_os_version_desktop_environment_requires_the_env_to_be_present() {
3727 let cond = OsVersionCondition::DesktopEnvironment(LinuxDesktopEnv::Gnome);
3728 assert!(DynamicSelector::match_os_version(
3729 &cond,
3730 OsVersion::unknown(),
3731 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3732 0
3733 ));
3734 assert!(!DynamicSelector::match_os_version(
3735 &cond,
3736 OsVersion::unknown(),
3737 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3738 0
3739 ));
3740 assert!(!DynamicSelector::match_os_version(
3741 &cond,
3742 OsVersion::unknown(),
3743 OptionLinuxDesktopEnv::None,
3744 0
3745 ));
3746 }
3747
3748 #[test]
3749 fn match_os_version_desktop_env_min_max_respect_the_unknown_zero_sentinel() {
3750 let gnome = OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome);
3751 let dev = DesktopEnvVersion {
3752 env: LinuxDesktopEnv::Gnome,
3753 version_id: 40,
3754 };
3755 assert!(!DynamicSelector::match_os_version(
3757 &OsVersionCondition::DesktopEnvMin(dev),
3758 OsVersion::unknown(),
3759 gnome,
3760 0
3761 ));
3762 assert!(!DynamicSelector::match_os_version(
3763 &OsVersionCondition::DesktopEnvMax(dev),
3764 OsVersion::unknown(),
3765 gnome,
3766 0
3767 ));
3768 assert!(DynamicSelector::match_os_version(
3770 &OsVersionCondition::DesktopEnvMin(dev),
3771 OsVersion::unknown(),
3772 gnome,
3773 40
3774 ));
3775 assert!(DynamicSelector::match_os_version(
3776 &OsVersionCondition::DesktopEnvMin(dev),
3777 OsVersion::unknown(),
3778 gnome,
3779 u32::MAX
3780 ));
3781 assert!(!DynamicSelector::match_os_version(
3782 &OsVersionCondition::DesktopEnvMin(dev),
3783 OsVersion::unknown(),
3784 gnome,
3785 39
3786 ));
3787 assert!(DynamicSelector::match_os_version(
3788 &OsVersionCondition::DesktopEnvMax(dev),
3789 OsVersion::unknown(),
3790 gnome,
3791 40
3792 ));
3793 assert!(!DynamicSelector::match_os_version(
3794 &OsVersionCondition::DesktopEnvMax(dev),
3795 OsVersion::unknown(),
3796 gnome,
3797 41
3798 ));
3799 }
3800
3801 #[test]
3802 fn match_os_version_desktop_env_exact_needs_the_matching_env() {
3803 let dev = DesktopEnvVersion {
3804 env: LinuxDesktopEnv::Gnome,
3805 version_id: 45,
3806 };
3807 assert!(DynamicSelector::match_os_version(
3808 &OsVersionCondition::DesktopEnvExact(dev),
3809 OsVersion::unknown(),
3810 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3811 45
3812 ));
3813 assert!(!DynamicSelector::match_os_version(
3814 &OsVersionCondition::DesktopEnvExact(dev),
3815 OsVersion::unknown(),
3816 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3817 45
3818 ));
3819 assert!(!DynamicSelector::match_os_version(
3820 &OsVersionCondition::DesktopEnvExact(dev),
3821 OsVersion::unknown(),
3822 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3823 46
3824 ));
3825 }
3826
3827 #[test]
3833 fn match_os_version_desktop_env_exact_zero_fails_when_de_version_is_unknown() {
3834 let dev = DesktopEnvVersion {
3835 env: LinuxDesktopEnv::Gnome,
3836 version_id: 0,
3837 };
3838 assert!(
3839 !DynamicSelector::match_os_version(
3840 &OsVersionCondition::DesktopEnvExact(dev),
3841 OsVersion::unknown(),
3842 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3843 0
3844 ),
3845 "de_version == 0 is the 'unknown' sentinel; it must not satisfy an exact match"
3846 );
3847 }
3848
3849 #[test]
3850 fn match_theme_system_preferred_is_a_wildcard() {
3851 for actual in [
3852 ThemeCondition::Light,
3853 ThemeCondition::Dark,
3854 ThemeCondition::SystemPreferred,
3855 ThemeCondition::Custom(AzString::from_const_str("solarized")),
3856 ] {
3857 assert!(DynamicSelector::match_theme(
3858 &ThemeCondition::SystemPreferred,
3859 &actual
3860 ));
3861 }
3862 }
3863
3864 #[test]
3865 fn match_theme_custom_compares_the_name() {
3866 let a = ThemeCondition::Custom(AzString::from_const_str("nord"));
3867 let b = ThemeCondition::Custom(AzString::from_const_str("nord"));
3868 let c = ThemeCondition::Custom(AzString::from_const_str("Nord"));
3869 assert!(DynamicSelector::match_theme(&a, &b));
3870 assert!(!DynamicSelector::match_theme(&a, &c));
3872 assert!(!DynamicSelector::match_theme(&a, &ThemeCondition::Dark));
3873 assert!(!DynamicSelector::match_theme(
3875 &ThemeCondition::Dark,
3876 &ThemeCondition::SystemPreferred
3877 ));
3878 }
3879
3880 #[test]
3881 fn match_pseudo_state_reads_through_to_the_context_flags() {
3882 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3883 hover: true,
3884 checked: true,
3885 ..PseudoStateFlags::default()
3886 });
3887 assert!(DynamicSelector::match_pseudo_state(
3888 PseudoStateType::Hover,
3889 &ctx
3890 ));
3891 assert!(DynamicSelector::match_pseudo_state(
3892 PseudoStateType::CheckedTrue,
3893 &ctx
3894 ));
3895 assert!(!DynamicSelector::match_pseudo_state(
3896 PseudoStateType::CheckedFalse,
3897 &ctx
3898 ));
3899 assert!(!DynamicSelector::match_pseudo_state(
3900 PseudoStateType::Active,
3901 &ctx
3902 ));
3903 assert!(DynamicSelector::match_pseudo_state(
3904 PseudoStateType::Normal,
3905 &ctx
3906 ));
3907 }
3908
3909 #[test]
3910 fn match_pseudo_state_agrees_with_has_state_for_every_state() {
3911 let flags = PseudoStateFlags {
3912 hover: true,
3913 focused: true,
3914 visited: true,
3915 drag_over: true,
3916 ..PseudoStateFlags::default()
3917 };
3918 let ctx = DynamicSelectorContext::default().with_pseudo_state(flags);
3919 for state in [
3920 PseudoStateType::Normal,
3921 PseudoStateType::Hover,
3922 PseudoStateType::Active,
3923 PseudoStateType::Focus,
3924 PseudoStateType::Disabled,
3925 PseudoStateType::CheckedTrue,
3926 PseudoStateType::CheckedFalse,
3927 PseudoStateType::FocusWithin,
3928 PseudoStateType::Visited,
3929 PseudoStateType::SeatFocus,
3930 PseudoStateType::Backdrop,
3931 PseudoStateType::Dragging,
3932 PseudoStateType::DragOver,
3933 ] {
3934 assert_eq!(
3935 DynamicSelector::match_pseudo_state(state, &ctx),
3936 flags.has_state(state),
3937 "match_pseudo_state and has_state disagree on {state:?}"
3938 );
3939 }
3940 }
3941
3942 #[test]
3943 fn selector_matches_every_variant_against_the_default_context() {
3944 let ctx = DynamicSelectorContext::default();
3946 for sel in all_selector_variants() {
3947 let a = sel.matches(&ctx);
3948 let b = sel.matches(&ctx);
3949 assert_eq!(a, b, "{sel:?} is not deterministic");
3950 }
3951 }
3952
3953 #[test]
3954 fn selector_matches_media_all_is_a_wildcard() {
3955 let ctx = DynamicSelectorContext::default();
3956 assert_eq!(ctx.media_type, MediaType::Screen);
3957 assert!(DynamicSelector::Media(MediaType::All).matches(&ctx));
3958 assert!(DynamicSelector::Media(MediaType::Screen).matches(&ctx));
3959 assert!(!DynamicSelector::Media(MediaType::Print).matches(&ctx));
3960 }
3961
3962 #[test]
3963 fn selector_matches_aspect_ratio_never_divides_by_zero() {
3964 let base = DynamicSelectorContext::default();
3965 let flat = base.with_viewport(800.0, 0.0);
3967 assert!(
3968 DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0))).matches(&flat)
3969 );
3970 let nan_h = base.with_viewport(800.0, f32::NAN);
3972 assert!(
3973 DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3974 .matches(&nan_h)
3975 );
3976 let nan_w = base.with_viewport(f32::NAN, 600.0);
3978 assert!(!DynamicSelector::AspectRatio(MinMaxRange::with_min(0.0)).matches(&nan_w));
3979 }
3980
3981 #[test]
3982 fn selector_matches_container_name_requires_an_exact_name() {
3983 let ctx = DynamicSelectorContext::default().with_container(
3984 100.0,
3985 100.0,
3986 Some(AzString::from_const_str("sidebar")),
3987 );
3988 assert!(DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&ctx));
3989 assert!(!DynamicSelector::ContainerName(AzString::from_const_str("Sidebar")).matches(&ctx));
3990 assert!(!DynamicSelector::ContainerName(AzString::from_const_str("main")).matches(&ctx));
3991 let no_ctr = DynamicSelectorContext::default();
3993 assert!(
3994 !DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&no_ctr)
3995 );
3996 }
3997
3998 #[test]
3999 fn selector_matches_bool_conditions_compare_both_polarities() {
4000 let ctx = DynamicSelectorContext::default();
4001 assert_eq!(ctx.prefers_reduced_motion, BoolCondition::False);
4002 assert!(DynamicSelector::PrefersReducedMotion(BoolCondition::False).matches(&ctx));
4004 assert!(!DynamicSelector::PrefersReducedMotion(BoolCondition::True).matches(&ctx));
4005 assert!(DynamicSelector::PrefersHighContrast(BoolCondition::False).matches(&ctx));
4006 assert!(!DynamicSelector::PrefersHighContrast(BoolCondition::True).matches(&ctx));
4007 }
4008
4009 #[test]
4010 fn bool_condition_roundtrips_through_bool() {
4011 for b in [false, true] {
4012 assert_eq!(bool::from(BoolCondition::from(b)), b);
4013 }
4014 assert_eq!(BoolCondition::default(), BoolCondition::False);
4015 assert!(!bool::from(BoolCondition::False));
4016 assert!(bool::from(BoolCondition::True));
4017 }
4018
4019 #[test]
4024 fn simple_property_is_unconditional_and_always_matches() {
4025 let p = CssPropertyWithConditions::simple(paint_prop());
4026 assert!(p.apply_if.as_slice().is_empty());
4027 assert!(!p.is_conditional());
4028 assert!(!p.is_pseudo_state_only());
4029 assert!(p.matches(&DynamicSelectorContext::default()));
4030 }
4031
4032 #[test]
4033 fn with_condition_and_with_single_condition_agree() {
4034 let a = CssPropertyWithConditions::with_condition(
4035 paint_prop(),
4036 DynamicSelector::PseudoState(PseudoStateType::Hover),
4037 );
4038 let b = CssPropertyWithConditions::on_hover(paint_prop());
4039 assert_eq!(a.apply_if.as_slice(), b.apply_if.as_slice());
4040 assert_eq!(a.apply_if.as_slice().len(), 1);
4041 }
4042
4043 #[test]
4044 fn with_conditions_preserves_order_and_length() {
4045 let conds = all_selector_variants();
4046 let p = CssPropertyWithConditions::with_conditions(
4047 paint_prop(),
4048 DynamicSelectorVec::from_vec(conds.clone()),
4049 );
4050 assert_eq!(p.apply_if.as_slice().len(), conds.len());
4051 for (i, c) in conds.iter().enumerate() {
4052 assert_eq!(p.apply_if.as_slice()[i].variant_tag(), c.variant_tag());
4053 }
4054 assert!(p.is_conditional());
4055 }
4056
4057 #[test]
4058 fn with_conditions_empty_vec_is_unconditional() {
4059 let p = CssPropertyWithConditions::with_conditions(
4060 paint_prop(),
4061 DynamicSelectorVec::from_vec(vec![]),
4062 );
4063 assert!(!p.is_conditional());
4064 assert!(p.matches(&DynamicSelectorContext::default()));
4065 }
4066
4067 #[test]
4068 fn pseudo_state_constructors_build_the_right_condition() {
4069 let cases = [
4070 (
4071 CssPropertyWithConditions::on_hover(paint_prop()),
4072 PseudoStateType::Hover,
4073 ),
4074 (
4075 CssPropertyWithConditions::on_active(paint_prop()),
4076 PseudoStateType::Active,
4077 ),
4078 (
4079 CssPropertyWithConditions::on_focus(paint_prop()),
4080 PseudoStateType::Focus,
4081 ),
4082 (
4083 CssPropertyWithConditions::when_disabled(paint_prop()),
4084 PseudoStateType::Disabled,
4085 ),
4086 ];
4087 for (prop, expected) in cases {
4088 assert_eq!(
4089 prop.apply_if.as_slice(),
4090 &[DynamicSelector::PseudoState(expected)]
4091 );
4092 assert!(prop.is_pseudo_state_only());
4093 assert!(prop.is_conditional());
4094 }
4095 }
4096
4097 #[test]
4098 fn os_and_theme_constructors_build_the_right_condition() {
4099 assert_eq!(
4100 CssPropertyWithConditions::on_windows(paint_prop())
4101 .apply_if
4102 .as_slice(),
4103 &[DynamicSelector::Os(OsCondition::Windows)]
4104 );
4105 assert_eq!(
4106 CssPropertyWithConditions::on_macos(paint_prop())
4107 .apply_if
4108 .as_slice(),
4109 &[DynamicSelector::Os(OsCondition::MacOS)]
4110 );
4111 assert_eq!(
4112 CssPropertyWithConditions::on_linux(paint_prop())
4113 .apply_if
4114 .as_slice(),
4115 &[DynamicSelector::Os(OsCondition::Linux)]
4116 );
4117 assert_eq!(
4118 CssPropertyWithConditions::on_os(paint_prop(), OsCondition::Web)
4119 .apply_if
4120 .as_slice(),
4121 &[DynamicSelector::Os(OsCondition::Web)]
4122 );
4123 assert_eq!(
4124 CssPropertyWithConditions::dark_theme(paint_prop())
4125 .apply_if
4126 .as_slice(),
4127 &[DynamicSelector::Theme(ThemeCondition::Dark)]
4128 );
4129 assert_eq!(
4130 CssPropertyWithConditions::light_theme(paint_prop())
4131 .apply_if
4132 .as_slice(),
4133 &[DynamicSelector::Theme(ThemeCondition::Light)]
4134 );
4135 assert!(!CssPropertyWithConditions::on_linux(paint_prop()).is_pseudo_state_only());
4137 assert!(!CssPropertyWithConditions::dark_theme(paint_prop()).is_pseudo_state_only());
4138 }
4139
4140 #[test]
4141 fn matches_requires_all_conditions_to_hold() {
4142 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
4143 hover: true,
4144 ..PseudoStateFlags::default()
4145 });
4146 let both = CssPropertyWithConditions::with_conditions(
4148 paint_prop(),
4149 DynamicSelectorVec::from_vec(vec![
4150 DynamicSelector::PseudoState(PseudoStateType::Hover),
4151 DynamicSelector::PseudoState(PseudoStateType::Focus),
4152 ]),
4153 );
4154 assert!(!both.matches(&ctx));
4155
4156 let ok = CssPropertyWithConditions::with_conditions(
4158 paint_prop(),
4159 DynamicSelectorVec::from_vec(vec![
4160 DynamicSelector::PseudoState(PseudoStateType::Hover),
4161 DynamicSelector::PseudoState(PseudoStateType::Normal),
4162 ]),
4163 );
4164 assert!(ok.matches(&ctx));
4165 }
4166
4167 #[test]
4168 fn matches_with_a_large_condition_list_terminates() {
4169 let conds: Vec<DynamicSelector> = (0..50_000)
4170 .map(|_| DynamicSelector::PseudoState(PseudoStateType::Normal))
4171 .collect();
4172 let p = CssPropertyWithConditions::with_conditions(
4173 paint_prop(),
4174 DynamicSelectorVec::from_vec(conds),
4175 );
4176 assert!(p.matches(&DynamicSelectorContext::default()));
4177 }
4178
4179 #[test]
4180 fn is_pseudo_state_only_is_false_for_empty_and_for_mixed_lists() {
4181 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_pseudo_state_only());
4183 let mixed = CssPropertyWithConditions::with_conditions(
4185 paint_prop(),
4186 DynamicSelectorVec::from_vec(vec![
4187 DynamicSelector::PseudoState(PseudoStateType::Hover),
4188 DynamicSelector::Os(OsCondition::Linux),
4189 ]),
4190 );
4191 assert!(!mixed.is_pseudo_state_only());
4192 let all_pseudo = CssPropertyWithConditions::with_conditions(
4194 paint_prop(),
4195 DynamicSelectorVec::from_vec(vec![
4196 DynamicSelector::PseudoState(PseudoStateType::Hover),
4197 DynamicSelector::PseudoState(PseudoStateType::Focus),
4198 ]),
4199 );
4200 assert!(all_pseudo.is_pseudo_state_only());
4201 }
4202
4203 #[test]
4204 fn is_layout_affecting_splits_layout_from_paint() {
4205 assert!(CssPropertyWithConditions::simple(layout_prop()).is_layout_affecting());
4206 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_layout_affecting());
4207 assert!(CssPropertyWithConditions::on_hover(layout_prop()).is_layout_affecting());
4209 assert!(!CssPropertyWithConditions::on_hover(paint_prop()).is_layout_affecting());
4210 }
4211
4212 #[test]
4213 fn css_property_with_conditions_hash_agrees_with_eq() {
4214 let a = CssPropertyWithConditions::on_hover(paint_prop());
4215 let b = CssPropertyWithConditions::on_hover(paint_prop());
4216 assert_eq!(a, b);
4217 assert_eq!(hash_of(&a), hash_of(&b));
4218 assert_eq!(a.cmp(&b), Ordering::Equal);
4219
4220 let c = CssPropertyWithConditions::on_focus(paint_prop());
4223 assert_ne!(a, c);
4224 assert_ne!(a.cmp(&c), Ordering::Equal);
4225 assert_ne!(hash_of(&a), hash_of(&c));
4226 }
4227
4228 #[test]
4229 fn css_property_with_conditions_ord_is_lexicographic_over_conditions() {
4230 let short = CssPropertyWithConditions::on_hover(paint_prop());
4231 let long = CssPropertyWithConditions::with_conditions(
4232 paint_prop(),
4233 DynamicSelectorVec::from_vec(vec![
4234 DynamicSelector::PseudoState(PseudoStateType::Hover),
4235 DynamicSelector::PseudoState(PseudoStateType::Focus),
4236 ]),
4237 );
4238 assert_eq!(short.cmp(&long), Ordering::Less);
4240 assert_eq!(long.cmp(&short), Ordering::Greater);
4241 }
4242
4243 #[cfg(feature = "parser")]
4248 #[test]
4249 fn parse_os_at_rule_bare_and_parenthesized_forms_agree() {
4250 let bare = parse_os_at_rule_content("linux").expect("bare linux");
4251 let paren = parse_os_at_rule_content("(linux)").expect("(linux)");
4252 let quoted = parse_os_at_rule_content("(\"linux\")").expect("quoted");
4253 let spaced = parse_os_at_rule_content(" ( linux ) ").expect("spaced");
4254 assert_eq!(bare, vec![DynamicSelector::Os(OsCondition::Linux)]);
4255 assert_eq!(bare, paren);
4256 assert_eq!(bare, quoted);
4257 assert_eq!(bare, spaced);
4258 }
4259
4260 #[cfg(feature = "parser")]
4261 #[test]
4262 fn parse_os_at_rule_emits_the_family_even_for_any() {
4263 for s in ["(any)", "(all)", "(*)"] {
4266 let conds = parse_os_at_rule_content(s).unwrap_or_else(|| panic!("{s} must parse"));
4267 assert_eq!(conds, vec![DynamicSelector::Os(OsCondition::Any)]);
4268 assert!(conds[0].matches(&DynamicSelectorContext::default()));
4269 }
4270 }
4271
4272 #[cfg(feature = "parser")]
4273 #[test]
4274 fn parse_os_at_rule_desktop_env_forms() {
4275 assert_eq!(
4276 parse_os_at_rule_content("(linux:gnome)"),
4277 Some(vec![
4278 DynamicSelector::Os(OsCondition::Linux),
4279 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
4280 LinuxDesktopEnv::Gnome
4281 )),
4282 ])
4283 );
4284 assert_eq!(
4286 parse_os_at_rule_content("(linux:notarealde)"),
4287 Some(vec![
4288 DynamicSelector::Os(OsCondition::Linux),
4289 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
4290 LinuxDesktopEnv::Other
4291 )),
4292 ])
4293 );
4294 assert_eq!(
4296 parse_os_at_rule_content("(linux:)"),
4297 Some(vec![DynamicSelector::Os(OsCondition::Linux)])
4298 );
4299 }
4300
4301 #[cfg(feature = "parser")]
4302 #[test]
4303 fn parse_os_at_rule_version_operators() {
4304 assert_eq!(
4305 parse_os_at_rule_content("(windows >= win-11)"),
4306 Some(vec![
4307 DynamicSelector::Os(OsCondition::Windows),
4308 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
4309 ])
4310 );
4311 assert_eq!(
4313 parse_os_at_rule_content("(windows > win-11)"),
4314 parse_os_at_rule_content("(windows >= win-11)")
4315 );
4316 assert_eq!(
4317 parse_os_at_rule_content("(macos <= sonoma)"),
4318 Some(vec![
4319 DynamicSelector::Os(OsCondition::MacOS),
4320 DynamicSelector::OsVersion(OsVersionCondition::Max(OsVersion::MACOS_SONOMA)),
4321 ])
4322 );
4323 assert_eq!(
4324 parse_os_at_rule_content("(ios = 17)"),
4325 Some(vec![
4326 DynamicSelector::Os(OsCondition::IOS),
4327 DynamicSelector::OsVersion(OsVersionCondition::Exact(OsVersion::IOS_17)),
4328 ])
4329 );
4330 }
4331
4332 #[cfg(feature = "parser")]
4333 #[test]
4334 fn parse_os_at_rule_desktop_env_version() {
4335 assert_eq!(
4336 parse_os_at_rule_content("(linux:gnome > 40)"),
4337 Some(vec![
4338 DynamicSelector::Os(OsCondition::Linux),
4339 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvMin(DesktopEnvVersion {
4340 env: LinuxDesktopEnv::Gnome,
4341 version_id: 40,
4342 })),
4343 ])
4344 );
4345 assert_eq!(parse_os_at_rule_content("(linux:gnome > 4294967296)"), None);
4347 assert_eq!(parse_os_at_rule_content("(linux:gnome > -1)"), None);
4348 assert_eq!(parse_os_at_rule_content("(linux:gnome > abc)"), None);
4349 assert_eq!(parse_os_at_rule_content("(linux:gnome > )"), None);
4350 }
4351
4352 #[cfg(feature = "parser")]
4353 #[test]
4354 fn parse_os_at_rule_rejects_versions_on_versionless_families() {
4355 assert_eq!(parse_os_at_rule_content("(apple >= 14)"), None);
4357 assert_eq!(parse_os_at_rule_content("(web >= 1)"), None);
4358 assert_eq!(parse_os_at_rule_content("(any >= 1)"), None);
4359 }
4360
4361 #[cfg(feature = "parser")]
4362 #[test]
4363 fn parse_os_at_rule_empty_and_garbage_is_none() {
4364 for s in [
4365 "",
4366 " ",
4367 "\t\n",
4368 "()",
4369 "( )",
4370 "(\"\")",
4371 "('')",
4372 "(:)",
4373 "(:gnome)",
4374 "(notanos)",
4375 "(linux;drop)",
4376 "\u{1F600}",
4377 "(\u{1F600})",
4378 "(linux linux)",
4379 ] {
4380 assert_eq!(parse_os_at_rule_content(s), None, "{s:?} must not parse");
4381 }
4382 }
4383
4384 #[cfg(feature = "parser")]
4385 #[test]
4386 fn parse_os_at_rule_huge_and_deeply_parenthesized_input_terminates() {
4387 let nested = format!("{}linux{}", "(".repeat(10_000), ")".repeat(10_000));
4389 assert_eq!(parse_os_at_rule_content(&nested), None);
4390 assert_eq!(parse_os_at_rule_content(&"a".repeat(1_000_000)), None);
4391 assert_eq!(
4392 parse_os_at_rule_content(&format!("(linux >= {})", "9".repeat(100_000))),
4393 None
4394 );
4395 }
4396
4397 #[cfg(feature = "parser")]
4398 #[test]
4399 fn split_op_and_version_picks_the_earliest_then_longest_operator() {
4400 let (subject, op) = split_op_and_version("linux >= 6.0");
4401 assert_eq!(subject, "linux ");
4402 let (op, ver) = op.expect("operator found");
4403 assert!(matches!(op, VersionOp::Min));
4404 assert_eq!(ver, "6.0");
4405
4406 let (_, op) = split_op_and_version("a>=1");
4408 assert!(matches!(op.expect("op").0, VersionOp::Min));
4409 let (_, op) = split_op_and_version("a<=1");
4410 assert!(matches!(op.expect("op").0, VersionOp::Max));
4411 let (_, op) = split_op_and_version("a=1");
4412 assert!(matches!(op.expect("op").0, VersionOp::Exact));
4413 }
4414
4415 #[cfg(feature = "parser")]
4416 #[test]
4417 fn split_op_and_version_with_no_operator_returns_the_whole_string() {
4418 let (subject, op) = split_op_and_version("linux");
4419 assert_eq!(subject, "linux");
4420 assert!(op.is_none());
4421
4422 let (subject, op) = split_op_and_version("");
4423 assert_eq!(subject, "");
4424 assert!(op.is_none());
4425 }
4426
4427 #[cfg(feature = "parser")]
4428 #[test]
4429 fn split_op_and_version_operator_only_yields_empty_sides() {
4430 let (subject, op) = split_op_and_version(">=");
4431 assert_eq!(subject, "");
4432 assert_eq!(op.expect("op").1, "");
4433
4434 let (subject, op) = split_op_and_version("=");
4435 assert_eq!(subject, "");
4436 assert_eq!(op.expect("op").1, "");
4437 }
4438
4439 #[cfg(feature = "parser")]
4440 #[test]
4441 fn split_op_and_version_does_not_split_inside_a_multibyte_char() {
4442 let (subject, op) = split_op_and_version("日本語 >= 6.0");
4445 assert_eq!(subject, "日本語 ");
4446 assert_eq!(op.expect("op").1, "6.0");
4447 let (subject, op) = split_op_and_version("🦀🦀🦀");
4449 assert_eq!(subject, "🦀🦀🦀");
4450 assert!(op.is_none());
4451 }
4452
4453 #[cfg(feature = "parser")]
4454 #[test]
4455 fn parse_os_family_token_accepts_aliases_case_insensitively() {
4456 assert_eq!(parse_os_family_token("LINUX"), Some(OsCondition::Linux));
4457 assert_eq!(parse_os_family_token("Win"), Some(OsCondition::Windows));
4458 assert_eq!(parse_os_family_token("windows"), Some(OsCondition::Windows));
4459 assert_eq!(parse_os_family_token("osx"), Some(OsCondition::MacOS));
4460 assert_eq!(parse_os_family_token("mac"), Some(OsCondition::MacOS));
4461 assert_eq!(parse_os_family_token("wasm"), Some(OsCondition::Web));
4462 assert_eq!(parse_os_family_token("*"), Some(OsCondition::Any));
4463 assert_eq!(parse_os_family_token("all"), Some(OsCondition::Any));
4464 }
4465
4466 #[cfg(feature = "parser")]
4467 #[test]
4468 fn parse_os_family_token_rejects_empty_padded_and_junk() {
4469 for s in [
4471 "",
4472 " ",
4473 " linux",
4474 "linux ",
4475 "lin",
4476 "linux2",
4477 "0",
4478 "-1",
4479 "NaN",
4480 "\u{1F600}",
4481 ] {
4482 assert_eq!(parse_os_family_token(s), None, "{s:?} must not parse");
4483 }
4484 assert_eq!(parse_os_family_token(&"linux".repeat(100_000)), None);
4485 }
4486
4487 #[cfg(feature = "parser")]
4488 #[test]
4489 fn parse_de_token_is_total_and_falls_back_to_other() {
4490 assert_eq!(parse_de_token("GNOME"), LinuxDesktopEnv::Gnome);
4491 assert_eq!(parse_de_token("kde"), LinuxDesktopEnv::KDE);
4492 assert_eq!(parse_de_token("XFCE"), LinuxDesktopEnv::XFCE);
4493 assert_eq!(parse_de_token("unity"), LinuxDesktopEnv::Unity);
4494 assert_eq!(parse_de_token("Cinnamon"), LinuxDesktopEnv::Cinnamon);
4495 assert_eq!(parse_de_token("mate"), LinuxDesktopEnv::MATE);
4496 for s in ["", " ", "gnome ", "\u{1F600}", "日本語", "\0"] {
4498 assert_eq!(parse_de_token(s), LinuxDesktopEnv::Other, "{s:?}");
4499 }
4500 assert_eq!(
4501 parse_de_token(&"gnome".repeat(200_000)),
4502 LinuxDesktopEnv::Other
4503 );
4504 }
4505
4506 #[cfg(feature = "parser")]
4511 fn parse_len(style: &str) -> usize {
4512 CssPropertyWithConditionsVec::parse(style)
4513 .into_library_owned_vec()
4514 .len()
4515 }
4516
4517 #[cfg(feature = "parser")]
4518 #[test]
4519 fn parse_valid_minimal_positive_control() {
4520 let props = CssPropertyWithConditionsVec::parse("color: red;").into_library_owned_vec();
4521 assert_eq!(props.len(), 1);
4522 assert!(!props[0].is_conditional());
4523 assert!(matches!(props[0].property, CssProperty::TextColor(_)));
4524 }
4525
4526 #[cfg(feature = "parser")]
4527 #[test]
4528 fn parse_empty_and_whitespace_yields_no_properties() {
4529 for s in ["", " ", "\t\n\r ", ";", ";;;;", " ; ; "] {
4530 assert_eq!(parse_len(s), 0, "{s:?} must yield no properties");
4531 }
4532 }
4533
4534 #[cfg(feature = "parser")]
4535 #[test]
4536 fn parse_garbage_never_panics_and_yields_nothing() {
4537 for s in [
4538 "not css at all",
4539 "color",
4540 "color:",
4541 ":",
4542 "::::",
4543 "{}",
4544 "}{",
4545 "}",
4546 "{",
4547 "{{{",
4548 "}}}",
4549 "}{color: red}",
4550 "color: ;",
4551 "\0: \0;",
4552 "%s%n%s",
4553 "\u{1F600}: \u{1F600};",
4554 "日本語: 赤;",
4555 ] {
4556 let _ = parse_len(s);
4558 }
4559 assert_eq!(parse_len("not css at all"), 0);
4560 assert_eq!(parse_len("color:"), 0);
4561 assert_eq!(parse_len("}{"), 0);
4562 }
4563
4564 #[cfg(feature = "parser")]
4565 #[test]
4566 fn parse_tolerates_a_missing_trailing_semicolon() {
4567 assert_eq!(parse_len("color: red"), 1);
4568 assert_eq!(parse_len("color: red;color: blue"), 2);
4569 }
4570
4571 #[cfg(feature = "parser")]
4572 #[test]
4573 fn parse_pseudo_selector_block_attaches_the_condition() {
4574 let props =
4575 CssPropertyWithConditionsVec::parse(":hover { color: red; }").into_library_owned_vec();
4576 assert_eq!(props.len(), 1);
4577 assert!(props[0].is_pseudo_state_only());
4578 assert_eq!(
4579 props[0].apply_if.as_slice(),
4580 &[DynamicSelector::PseudoState(PseudoStateType::Hover)]
4581 );
4582 }
4583
4584 #[cfg(feature = "parser")]
4585 #[test]
4586 fn parse_unknown_selector_block_is_dropped_wholesale() {
4587 assert_eq!(parse_len(":nosuchstate { color: red; }"), 0);
4590 assert_eq!(parse_len("@nosuchrule { color: red; }"), 0);
4591 assert_eq!(parse_len("div { color: red; }"), 0);
4592 }
4593
4594 #[cfg(feature = "parser")]
4595 #[test]
4596 fn parse_nesting_accumulates_inherited_conditions() {
4597 let props = CssPropertyWithConditionsVec::parse(
4598 "@os linux { font-size: 14px; :hover { color: red; }}",
4599 )
4600 .into_library_owned_vec();
4601 assert_eq!(props.len(), 2);
4602 let font = props
4604 .iter()
4605 .find(|p| matches!(p.property, CssProperty::FontSize(_)))
4606 .expect("font-size present");
4607 assert_eq!(
4608 font.apply_if.as_slice(),
4609 &[DynamicSelector::Os(OsCondition::Linux)]
4610 );
4611 let color = props
4612 .iter()
4613 .find(|p| matches!(p.property, CssProperty::TextColor(_)))
4614 .expect("color present");
4615 assert_eq!(
4616 color.apply_if.as_slice(),
4617 &[
4618 DynamicSelector::Os(OsCondition::Linux),
4619 DynamicSelector::PseudoState(PseudoStateType::Hover),
4620 ]
4621 );
4622 assert!(!color.is_pseudo_state_only());
4623 }
4624
4625 #[cfg(feature = "parser")]
4626 #[test]
4627 fn parse_moderately_deep_nesting_terminates() {
4628 const DEPTH: usize = 50;
4633 let style = format!(
4634 "{}color: red;{}",
4635 ":hover {".repeat(DEPTH),
4636 "}".repeat(DEPTH)
4637 );
4638 let props = CssPropertyWithConditionsVec::parse(&style).into_library_owned_vec();
4639 assert_eq!(props.len(), 1);
4640 assert_eq!(props[0].apply_if.as_slice().len(), DEPTH);
4641 assert!(props[0].is_pseudo_state_only());
4642 }
4643
4644 #[cfg(feature = "parser")]
4645 #[test]
4646 fn parse_unbalanced_braces_do_not_panic() {
4647 for s in [
4649 "color: red; }",
4650 "{ color: red;",
4651 ":hover { color: red;",
4652 ":hover }",
4653 &"{".repeat(1_000),
4654 &"}".repeat(1_000),
4655 ] {
4656 let _ = parse_len(s);
4657 }
4658 }
4659
4660 #[cfg(feature = "parser")]
4661 #[test]
4662 fn parse_very_long_input_terminates() {
4663 let long = "color: red;".repeat(5_000);
4664 assert_eq!(parse_len(&long), 5_000);
4665 assert_eq!(parse_len(&"a".repeat(500_000)), 0);
4667 assert_eq!(parse_len(&format!("color: {};", "z".repeat(500_000))), 0);
4668 }
4669
4670 #[cfg(feature = "parser")]
4671 #[test]
4672 fn parse_selector_to_conditions_covers_every_pseudo_class() {
4673 let cases = [
4674 ("hover", PseudoStateType::Hover),
4675 ("active", PseudoStateType::Active),
4676 ("focus", PseudoStateType::Focus),
4677 ("seat-focus", PseudoStateType::SeatFocus),
4678 ("focus-within", PseudoStateType::FocusWithin),
4679 ("disabled", PseudoStateType::Disabled),
4680 ("checked", PseudoStateType::CheckedTrue),
4681 ("visited", PseudoStateType::Visited),
4682 ("backdrop", PseudoStateType::Backdrop),
4683 ("dragging", PseudoStateType::Dragging),
4684 ("drag-over", PseudoStateType::DragOver),
4685 ];
4686 for (name, expected) in cases {
4687 assert_eq!(
4688 CssPropertyWithConditionsVec::parse_selector_to_conditions(&format!(":{name}")),
4689 Some(vec![DynamicSelector::PseudoState(expected)]),
4690 ":{name} must parse"
4691 );
4692 }
4693 }
4694
4695 #[cfg(feature = "parser")]
4696 #[test]
4697 fn parse_selector_to_conditions_wildcards_are_unconditional() {
4698 assert_eq!(
4699 CssPropertyWithConditionsVec::parse_selector_to_conditions("*"),
4700 Some(vec![])
4701 );
4702 assert_eq!(
4703 CssPropertyWithConditionsVec::parse_selector_to_conditions(""),
4704 Some(vec![])
4705 );
4706 assert_eq!(
4707 CssPropertyWithConditionsVec::parse_selector_to_conditions(" "),
4708 Some(vec![])
4709 );
4710 }
4711
4712 #[cfg(feature = "parser")]
4713 #[test]
4714 fn parse_selector_to_conditions_rejects_unknown_selectors() {
4715 for s in [
4716 ":",
4717 ":hoverr",
4718 ":HOVER",
4719 "div",
4720 "#id",
4721 ".class",
4722 "@",
4723 "\u{1F600}",
4724 ":\u{1F600}",
4725 ] {
4726 assert_eq!(
4727 CssPropertyWithConditionsVec::parse_selector_to_conditions(s),
4728 None,
4729 "{s:?} must not parse"
4730 );
4731 }
4732 }
4733
4734 #[cfg(feature = "parser")]
4735 #[test]
4736 fn parse_at_rule_theme_lang_and_accessibility() {
4737 assert_eq!(
4738 CssPropertyWithConditionsVec::parse_at_rule("theme dark"),
4739 Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)])
4740 );
4741 assert_eq!(
4742 CssPropertyWithConditionsVec::parse_at_rule("theme light"),
4743 Some(vec![DynamicSelector::Theme(ThemeCondition::Light)])
4744 );
4745 assert_eq!(
4746 CssPropertyWithConditionsVec::parse_at_rule("theme neon"),
4747 None
4748 );
4749
4750 assert_eq!(
4751 CssPropertyWithConditionsVec::parse_at_rule("lang(\"de-DE\")"),
4752 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4753 AzString::from_const_str("de-DE")
4754 ))])
4755 );
4756 assert_eq!(
4757 CssPropertyWithConditionsVec::parse_at_rule("lang de"),
4758 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4759 AzString::from_const_str("de")
4760 ))])
4761 );
4762 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang()"), None);
4763 assert_eq!(
4764 CssPropertyWithConditionsVec::parse_at_rule("lang(\"\")"),
4765 None
4766 );
4767
4768 assert_eq!(
4769 CssPropertyWithConditionsVec::parse_at_rule("prefers-reduced-motion"),
4770 Some(vec![DynamicSelector::PrefersReducedMotion(
4771 BoolCondition::True
4772 )])
4773 );
4774 assert_eq!(
4775 CssPropertyWithConditionsVec::parse_at_rule("high-contrast"),
4776 Some(vec![DynamicSelector::PrefersHighContrast(
4777 BoolCondition::True
4778 )])
4779 );
4780 }
4781
4782 #[cfg(feature = "parser")]
4783 #[test]
4784 fn parse_at_rule_container_named_and_sized() {
4785 assert_eq!(
4786 CssPropertyWithConditionsVec::parse_at_rule("container sidebar"),
4787 Some(vec![DynamicSelector::ContainerName(
4788 AzString::from_const_str("sidebar")
4789 )])
4790 );
4791 let conds = CssPropertyWithConditionsVec::parse_at_rule("container (min-width: 400px)")
4792 .expect("sized container must parse");
4793 assert_eq!(conds.len(), 1);
4794 match conds[0] {
4798 DynamicSelector::ContainerWidth(r) => {
4799 assert_eq!(r.min(), Some(400.0));
4800 assert_eq!(r.max(), None);
4801 }
4802 ref other => panic!("expected ContainerWidth, got {other:?}"),
4803 }
4804 let named =
4805 CssPropertyWithConditionsVec::parse_at_rule("container sidebar (max-height: 200px)")
4806 .expect("named + sized container must parse");
4807 assert_eq!(named.len(), 2);
4808 assert!(matches!(named[0], DynamicSelector::ContainerName(_)));
4809 assert!(matches!(named[1], DynamicSelector::ContainerHeight(_)));
4810 }
4811
4812 #[cfg(feature = "parser")]
4813 #[test]
4814 fn parse_at_rule_empty_and_garbage_is_none() {
4815 for s in [
4816 "",
4817 " ",
4818 "os",
4819 "os ",
4820 "os()",
4821 "os(notanos)",
4822 "media",
4823 "media ",
4824 "media (min-width: abc)",
4825 "theme",
4826 "container",
4827 "container ()",
4828 "nosuchrule",
4829 "\u{1F600}",
4830 ] {
4831 assert_eq!(
4832 CssPropertyWithConditionsVec::parse_at_rule(s),
4833 None,
4834 "{s:?} must not parse"
4835 );
4836 }
4837 }
4838
4839 #[cfg(feature = "parser")]
4840 #[test]
4841 fn parse_at_rule_huge_input_terminates() {
4842 assert_eq!(
4843 CssPropertyWithConditionsVec::parse_at_rule(&format!("os {}", "(".repeat(50_000))),
4844 None
4845 );
4846 assert_eq!(
4847 CssPropertyWithConditionsVec::parse_at_rule(&"z".repeat(500_000)),
4848 None
4849 );
4850 let long_lang = "e".repeat(100_000);
4852 assert_eq!(
4853 CssPropertyWithConditionsVec::parse_at_rule(&format!("lang {long_lang}")),
4854 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4855 AzString::from(long_lang)
4856 ))])
4857 );
4858 }
4859
4860 #[cfg(feature = "parser")]
4861 #[test]
4862 fn parse_media_query_media_types_and_dimensions() {
4863 assert_eq!(
4864 CssPropertyWithConditionsVec::parse_media_query("screen"),
4865 Some(vec![DynamicSelector::Media(MediaType::Screen)])
4866 );
4867 assert_eq!(
4868 CssPropertyWithConditionsVec::parse_media_query("print"),
4869 Some(vec![DynamicSelector::Media(MediaType::Print)])
4870 );
4871 assert_eq!(
4872 CssPropertyWithConditionsVec::parse_media_query("all"),
4873 Some(vec![DynamicSelector::Media(MediaType::All)])
4874 );
4875 let w = CssPropertyWithConditionsVec::parse_media_query("(min-width: 800px)")
4876 .expect("min-width must parse");
4877 assert_eq!(w.len(), 1);
4878 match w[0] {
4879 DynamicSelector::ViewportWidth(r) => {
4880 assert_eq!(r.min(), Some(800.0));
4881 assert_eq!(r.max(), None);
4882 }
4883 ref other => panic!("expected ViewportWidth, got {other:?}"),
4884 }
4885 }
4886
4887 #[cfg(feature = "parser")]
4888 #[test]
4889 fn parse_media_query_boundary_pixel_values() {
4890 let zero = CssPropertyWithConditionsVec::parse_media_query("(min-width: 0px)")
4892 .expect("0px must parse");
4893 match zero[0] {
4894 DynamicSelector::ViewportWidth(r) => assert_eq!(r.min(), Some(0.0)),
4895 ref other => panic!("expected ViewportWidth, got {other:?}"),
4896 }
4897 let neg = CssPropertyWithConditionsVec::parse_media_query("(max-height: -1px)")
4898 .expect("-1px must parse");
4899 match neg[0] {
4900 DynamicSelector::ViewportHeight(r) => assert_eq!(r.max(), Some(-1.0)),
4901 ref other => panic!("expected ViewportHeight, got {other:?}"),
4902 }
4903 assert_eq!(
4905 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800)"),
4906 None
4907 );
4908 assert_eq!(
4909 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800em)"),
4910 None
4911 );
4912 }
4913
4914 #[cfg(feature = "parser")]
4915 #[test]
4916 fn parse_media_query_empty_and_garbage_is_none() {
4917 for s in [
4918 "",
4919 " ",
4920 "(",
4921 ")",
4922 "()",
4923 "(:)",
4924 "(min-width)",
4925 "(min-width: )",
4926 "(nosuchfeature: 1px)",
4927 "SCREEN",
4928 "\u{1F600}",
4929 "(🦀: 1px)",
4930 ] {
4931 assert_eq!(
4932 CssPropertyWithConditionsVec::parse_media_query(s),
4933 None,
4934 "{s:?} must not parse"
4935 );
4936 }
4937 assert_eq!(
4938 CssPropertyWithConditionsVec::parse_media_query(&"(".repeat(100_000)),
4939 None
4940 );
4941 }
4942
4943 #[cfg(feature = "parser")]
4949 #[test]
4950 fn parse_media_query_nan_pixel_value_is_rejected() {
4951 assert_eq!(
4952 CssPropertyWithConditionsVec::parse_media_query("(min-width: NaNpx)"),
4953 None,
4954 "a NaN px value collapses into the 'no limit' sentinel and matches everything"
4955 );
4956 }
4957
4958 #[cfg(feature = "parser")]
4959 #[test]
4960 fn parse_media_query_infinite_pixel_value_never_matches() {
4961 let q = CssPropertyWithConditionsVec::parse_media_query("(min-width: infpx)")
4964 .expect("infpx currently parses");
4965 let ctx = DynamicSelectorContext::default().with_viewport(1e30, 1000.0);
4966 assert!(
4967 !q[0].matches(&ctx),
4968 "an infinite min-width can never be met"
4969 );
4970 }
4971
4972 #[cfg(feature = "parser")]
4973 #[test]
4974 fn parse_media_feature_inline_known_features() {
4975 assert_eq!(
4976 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "PORTRAIT"),
4977 Some(DynamicSelector::Orientation(OrientationType::Portrait))
4978 );
4979 assert_eq!(
4980 CssPropertyWithConditionsVec::parse_media_feature_inline(
4981 "prefers-color-scheme",
4982 "Dark"
4983 ),
4984 Some(DynamicSelector::Theme(ThemeCondition::Dark))
4985 );
4986 assert_eq!(
4987 CssPropertyWithConditionsVec::parse_media_feature_inline(
4988 "prefers-reduced-motion",
4989 "reduce"
4990 ),
4991 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
4992 );
4993 assert_eq!(
4994 CssPropertyWithConditionsVec::parse_media_feature_inline(
4995 "prefers-reduced-motion",
4996 "no-preference"
4997 ),
4998 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
4999 );
5000 assert_eq!(
5001 CssPropertyWithConditionsVec::parse_media_feature_inline("prefers-contrast", "more"),
5002 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
5003 );
5004 assert_eq!(
5005 CssPropertyWithConditionsVec::parse_media_feature_inline(
5006 "prefers-high-contrast",
5007 "none"
5008 ),
5009 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
5010 );
5011 }
5012
5013 #[cfg(feature = "parser")]
5014 #[test]
5015 fn parse_media_feature_inline_rejects_unknown_keys_and_values() {
5016 assert_eq!(
5017 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", ""),
5018 None
5019 );
5020 assert_eq!(
5021 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "sideways"),
5022 None
5023 );
5024 assert_eq!(
5025 CssPropertyWithConditionsVec::parse_media_feature_inline("", ""),
5026 None
5027 );
5028 assert_eq!(
5029 CssPropertyWithConditionsVec::parse_media_feature_inline("nosuchkey", "dark"),
5030 None
5031 );
5032 assert_eq!(
5033 CssPropertyWithConditionsVec::parse_media_feature_inline(
5034 "prefers-color-scheme",
5035 "\u{1F600}"
5036 ),
5037 None
5038 );
5039 assert_eq!(
5041 CssPropertyWithConditionsVec::parse_media_feature_inline(" orientation", "portrait"),
5042 None
5043 );
5044 assert_eq!(
5045 CssPropertyWithConditionsVec::parse_media_feature_inline(
5046 &"k".repeat(200_000),
5047 &"v".repeat(200_000)
5048 ),
5049 None
5050 );
5051 }
5052
5053 #[cfg(feature = "parser")]
5054 #[test]
5055 fn parse_property_segment_valid_and_invalid() {
5056 let key_map = crate::props::property::CssKeyMap::get();
5057 let ok = CssPropertyWithConditionsVec::parse_property_segment("color: red", &[], &key_map)
5058 .expect("color: red must parse");
5059 assert_eq!(ok.len(), 1);
5060 assert!(!ok[0].is_conditional());
5061
5062 let inherited = vec![DynamicSelector::PseudoState(PseudoStateType::Hover)];
5064 let shorthand = CssPropertyWithConditionsVec::parse_property_segment(
5065 "padding: 10px",
5066 &inherited,
5067 &key_map,
5068 )
5069 .expect("padding shorthand must parse");
5070 assert!(shorthand.len() > 1, "padding must expand to >1 property");
5071 for p in &shorthand {
5072 assert_eq!(p.apply_if.as_slice(), inherited.as_slice());
5073 }
5074
5075 for s in [
5076 "",
5077 " ",
5078 "color",
5079 "color:",
5080 ": red",
5081 "nosuchprop: red",
5082 "\u{1F600}",
5083 ] {
5084 assert!(
5085 CssPropertyWithConditionsVec::parse_property_segment(s, &[], &key_map).is_none(),
5086 "{s:?} must not parse"
5087 );
5088 }
5089 }
5090
5091 #[cfg(feature = "parser")]
5092 #[test]
5093 fn parse_block_segment_requires_balanced_braces() {
5094 let key_map = crate::props::property::CssKeyMap::get();
5095 assert!(
5097 CssPropertyWithConditionsVec::parse_block_segment("color: red", &[], &key_map)
5098 .is_none()
5099 );
5100 assert!(CssPropertyWithConditionsVec::parse_block_segment(
5102 ":hover { color: red",
5103 &[],
5104 &key_map
5105 )
5106 .is_none());
5107 assert!(CssPropertyWithConditionsVec::parse_block_segment("}{", &[], &key_map).is_none());
5109 let empty = CssPropertyWithConditionsVec::parse_block_segment(":hover {}", &[], &key_map);
5111 assert!(empty.is_none());
5113 let ok = CssPropertyWithConditionsVec::parse_block_segment(
5115 ":hover { color: red; }",
5116 &[],
5117 &key_map,
5118 )
5119 .expect("valid block");
5120 assert_eq!(ok.len(), 1);
5121 assert!(ok[0].is_pseudo_state_only());
5122 }
5123
5124 #[cfg(feature = "parser")]
5125 #[test]
5126 fn parse_with_conditions_prepends_inherited_conditions() {
5127 let inherited = vec![DynamicSelector::Os(OsCondition::Linux)];
5128 let props = CssPropertyWithConditionsVec::parse_with_conditions("color: red;", &inherited)
5129 .into_library_owned_vec();
5130 assert_eq!(props.len(), 1);
5131 assert_eq!(props[0].apply_if.as_slice(), inherited.as_slice());
5132
5133 let none = CssPropertyWithConditionsVec::parse_with_conditions(" ", &inherited)
5135 .into_library_owned_vec();
5136 assert!(none.is_empty());
5137 }
5138
5139 #[cfg(feature = "parser")]
5140 #[test]
5141 fn parsed_media_selector_evaluates_against_a_context() {
5142 let props =
5145 CssPropertyWithConditionsVec::parse("@media (min-width: 800px) { color: red; }")
5146 .into_library_owned_vec();
5147 assert_eq!(props.len(), 1);
5148 let base = DynamicSelectorContext::default();
5149 assert!(props[0].matches(&base.with_viewport(1024.0, 768.0)));
5150 assert!(props[0].matches(&base.with_viewport(800.0, 600.0)));
5151 assert!(!props[0].matches(&base.with_viewport(799.0, 600.0)));
5152 }
5153}
5154
5155#[cfg(test)]
5159mod safe_area_context_tests {
5160 use super::*;
5161 use crate::{
5162 props::basic::pixel::{OptionPixelValue, PixelValue},
5163 system::SafeAreaInsets,
5164 };
5165
5166 #[test]
5167 fn default_context_reports_no_inset_so_every_env_falls_back() {
5168 let ctx = DynamicSelectorContext::default();
5169 for v in EnvVariable::ALL {
5170 assert_eq!(v.resolve(&ctx), None, "{v:?}");
5171 }
5172 }
5173
5174 #[test]
5175 fn with_safe_area_carries_each_edge_and_the_keyboard() {
5176 let insets = SafeAreaInsets {
5177 top: OptionPixelValue::Some(PixelValue::px(47.0)),
5178 right: OptionPixelValue::None,
5179 bottom: OptionPixelValue::Some(PixelValue::px(34.0)),
5180 left: OptionPixelValue::Some(PixelValue::pt(0.0)),
5181 keyboard: OptionPixelValue::Some(PixelValue::px(300.0)),
5182 };
5183 let ctx = DynamicSelectorContext::default().with_safe_area(&insets);
5184 assert_eq!(EnvVariable::SafeAreaInsetTop.resolve(&ctx), Some(47.0));
5185 assert_eq!(EnvVariable::SafeAreaInsetRight.resolve(&ctx), None);
5186 assert_eq!(EnvVariable::SafeAreaInsetBottom.resolve(&ctx), Some(34.0));
5187 assert_eq!(EnvVariable::SafeAreaInsetLeft.resolve(&ctx), Some(0.0));
5188 assert_eq!(EnvVariable::KeyboardInsetHeight.resolve(&ctx), Some(300.0));
5189 }
5190
5191 #[test]
5192 fn a_relative_inset_is_treated_as_absent_not_invented() {
5193 let insets = SafeAreaInsets {
5196 bottom: OptionPixelValue::Some(PixelValue::em(2.0)),
5197 ..Default::default()
5198 };
5199 let ctx = DynamicSelectorContext::default().with_safe_area(&insets);
5200 assert_eq!(EnvVariable::SafeAreaInsetBottom.resolve(&ctx), None);
5201 }
5202
5203 #[test]
5208 fn inset_changes_are_visible_to_context_equality_and_nan_is_stable() {
5209 let base = DynamicSelectorContext::default();
5210 assert_eq!(base, base.with_safe_area(&SafeAreaInsets::default()));
5211
5212 let with_bottom = base.with_safe_area(&SafeAreaInsets {
5213 bottom: OptionPixelValue::Some(PixelValue::px(34.0)),
5214 ..Default::default()
5215 });
5216 assert_ne!(base, with_bottom);
5217 assert_eq!(with_bottom, with_bottom.clone());
5218
5219 let keyboard_up = with_bottom.with_safe_area(&SafeAreaInsets {
5220 bottom: OptionPixelValue::Some(PixelValue::px(34.0)),
5221 keyboard: OptionPixelValue::Some(PixelValue::px(250.0)),
5222 ..Default::default()
5223 });
5224 assert_ne!(with_bottom, keyboard_up);
5225 }
5226
5227 #[test]
5228 fn names_round_trip() {
5229 for v in EnvVariable::ALL {
5230 assert_eq!(EnvVariable::from_css_name(v.as_css_name()), Some(v));
5231 assert_eq!(
5232 EnvVariable::from_css_name(&format!(" {} ", v.as_css_name())),
5233 Some(v)
5234 );
5235 }
5236 assert_eq!(EnvVariable::from_css_name("safe-area-inset"), None);
5237 assert_eq!(EnvVariable::from_css_name(""), None);
5238 }
5239}