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 visited: bool,
26 pub backdrop: bool,
28 pub dragging: bool,
30 pub drag_over: bool,
32}
33
34impl PseudoStateFlags {
35 #[must_use] pub const fn has_state(&self, state: PseudoStateType) -> bool {
37 match state {
38 PseudoStateType::Normal => true,
39 PseudoStateType::Hover => self.hover,
40 PseudoStateType::Active => self.active,
41 PseudoStateType::Focus => self.focused,
42 PseudoStateType::Disabled => self.disabled,
43 PseudoStateType::CheckedTrue => self.checked,
44 PseudoStateType::CheckedFalse => !self.checked,
45 PseudoStateType::FocusWithin => self.focus_within,
46 PseudoStateType::Visited => self.visited,
47 PseudoStateType::Backdrop => self.backdrop,
48 PseudoStateType::Dragging => self.dragging,
49 PseudoStateType::DragOver => self.drag_over,
50 }
51 }
52}
53
54#[repr(C, u8)]
57#[derive(Debug, Clone, PartialEq)]
58pub enum DynamicSelector {
59 Os(OsCondition) = 0,
61 OsVersion(OsVersionCondition) = 1,
63 Media(MediaType) = 2,
65 ViewportWidth(MinMaxRange) = 3,
67 ViewportHeight(MinMaxRange) = 4,
69 ContainerWidth(MinMaxRange) = 5,
71 ContainerHeight(MinMaxRange) = 6,
73 ContainerName(AzString) = 7,
75 Theme(ThemeCondition) = 8,
77 AspectRatio(MinMaxRange) = 9,
79 Orientation(OrientationType) = 10,
81 PrefersReducedMotion(BoolCondition) = 11,
83 PrefersHighContrast(BoolCondition) = 12,
85 PseudoState(PseudoStateType) = 13,
87 Language(LanguageCondition) = 14,
90}
91
92impl_option!(
93 DynamicSelector,
94 OptionDynamicSelector,
95 copy = false,
96 [Debug, Clone, PartialEq, Eq]
97);
98
99impl_vec!(DynamicSelector, DynamicSelectorVec, DynamicSelectorVecDestructor, DynamicSelectorVecDestructorType, DynamicSelectorVecSlice, OptionDynamicSelector);
100impl_vec_clone!(
101 DynamicSelector,
102 DynamicSelectorVec,
103 DynamicSelectorVecDestructor
104);
105impl_vec_debug!(DynamicSelector, DynamicSelectorVec);
106impl_vec_partialeq!(DynamicSelector, DynamicSelectorVec);
107
108impl DynamicSelector {
109 const fn variant_tag(&self) -> u8 {
112 match self {
113 Self::Os(_) => 0,
114 Self::OsVersion(_) => 1,
115 Self::Media(_) => 2,
116 Self::ViewportWidth(_) => 3,
117 Self::ViewportHeight(_) => 4,
118 Self::ContainerWidth(_) => 5,
119 Self::ContainerHeight(_) => 6,
120 Self::ContainerName(_) => 7,
121 Self::Theme(_) => 8,
122 Self::AspectRatio(_) => 9,
123 Self::Orientation(_) => 10,
124 Self::PrefersReducedMotion(_) => 11,
125 Self::PrefersHighContrast(_) => 12,
126 Self::PseudoState(_) => 13,
127 Self::Language(_) => 14,
128 }
129 }
130}
131
132impl Eq for DynamicSelector {}
139
140impl PartialOrd for DynamicSelector {
141 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
142 Some(self.cmp(other))
143 }
144}
145
146impl Ord for DynamicSelector {
147 #[allow(clippy::match_same_arms)]
150 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
151 use core::cmp::Ordering;
152 match self.variant_tag().cmp(&other.variant_tag()) {
153 Ordering::Equal => {}
154 non_eq => return non_eq,
155 }
156 match (self, other) {
158 (Self::Os(a), Self::Os(b)) => a.cmp(b),
159 (Self::OsVersion(a), Self::OsVersion(b)) => a.cmp(b),
160 (Self::Media(a), Self::Media(b)) => a.cmp(b),
161 (Self::ContainerName(a), Self::ContainerName(b)) => a.cmp(b),
162 (Self::Theme(a), Self::Theme(b)) => a.cmp(b),
163 (Self::Orientation(a), Self::Orientation(b)) => a.cmp(b),
164 (Self::PrefersReducedMotion(a), Self::PrefersReducedMotion(b)) => {
165 a.cmp(b)
166 }
167 (Self::PrefersHighContrast(a), Self::PrefersHighContrast(b)) => {
168 a.cmp(b)
169 }
170 (Self::PseudoState(a), Self::PseudoState(b)) => a.cmp(b),
171 (Self::Language(a), Self::Language(b)) => a.cmp(b),
172 (Self::ViewportWidth(a), Self::ViewportWidth(b))
173 | (Self::ViewportHeight(a), Self::ViewportHeight(b))
174 | (Self::ContainerWidth(a), Self::ContainerWidth(b))
175 | (Self::ContainerHeight(a), Self::ContainerHeight(b))
176 | (Self::AspectRatio(a), Self::AspectRatio(b)) => {
177 (a.min.to_bits(), a.max.to_bits()).cmp(&(b.min.to_bits(), b.max.to_bits()))
178 }
179 _ => Ordering::Equal,
181 }
182 }
183}
184
185impl core::hash::Hash for DynamicSelector {
186 #[allow(clippy::match_same_arms)]
189 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
190 self.variant_tag().hash(state);
191 match self {
192 Self::Os(x) => x.hash(state),
193 Self::OsVersion(x) => x.hash(state),
194 Self::Media(x) => x.hash(state),
195 Self::ContainerName(x) => x.hash(state),
196 Self::Theme(x) => x.hash(state),
197 Self::Orientation(x) => x.hash(state),
198 Self::PrefersReducedMotion(x) => x.hash(state),
199 Self::PrefersHighContrast(x) => x.hash(state),
200 Self::PseudoState(x) => x.hash(state),
201 Self::Language(x) => x.hash(state),
202 Self::ViewportWidth(r)
203 | Self::ViewportHeight(r)
204 | Self::ContainerWidth(r)
205 | Self::ContainerHeight(r)
206 | Self::AspectRatio(r) => {
207 r.min.to_bits().hash(state);
208 r.max.to_bits().hash(state);
209 }
210 }
211 }
212}
213
214#[repr(C)]
216#[derive(Debug, Clone, Copy)]
217pub struct MinMaxRange {
218 pub min: f32,
220 pub max: f32,
222}
223
224impl PartialEq for MinMaxRange {
233 fn eq(&self, other: &Self) -> bool {
234 self.min.to_bits() == other.min.to_bits() && self.max.to_bits() == other.max.to_bits()
235 }
236}
237
238impl Eq for MinMaxRange {}
239
240impl PartialOrd for MinMaxRange {
247 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
248 Some(
249 (self.min.to_bits(), self.max.to_bits())
250 .cmp(&(other.min.to_bits(), other.max.to_bits())),
251 )
252 }
253}
254
255impl MinMaxRange {
256 #[must_use] pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
257 Self {
258 min: if let Some(m) = min { m } else { f32::NAN },
259 max: if let Some(m) = max { m } else { f32::NAN },
260 }
261 }
262
263 #[must_use] pub const fn with_min(min_val: f32) -> Self {
265 Self {
266 min: min_val,
267 max: f32::NAN,
268 }
269 }
270
271 #[must_use] pub const fn with_max(max_val: f32) -> Self {
273 Self {
274 min: f32::NAN,
275 max: max_val,
276 }
277 }
278
279 #[must_use] pub const fn min(&self) -> Option<f32> {
280 if self.min.is_nan() {
281 None
282 } else {
283 Some(self.min)
284 }
285 }
286
287 #[must_use] pub const fn max(&self) -> Option<f32> {
288 if self.max.is_nan() {
289 None
290 } else {
291 Some(self.max)
292 }
293 }
294
295 #[must_use] pub fn matches(&self, value: f32) -> bool {
296 let min_ok = self.min.is_nan() || value >= self.min;
297 let max_ok = self.max.is_nan() || value <= self.max;
298 min_ok && max_ok
299 }
300}
301
302#[repr(C)]
304#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
305pub enum BoolCondition {
306 #[default]
307 False,
308 True,
309}
310
311impl From<bool> for BoolCondition {
312 fn from(b: bool) -> Self {
313 if b {
314 Self::True
315 } else {
316 Self::False
317 }
318 }
319}
320
321impl From<BoolCondition> for bool {
322 fn from(b: BoolCondition) -> Self {
323 matches!(b, BoolCondition::True)
324 }
325}
326
327#[repr(C)]
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
330pub enum OsCondition {
331 Any,
332 Apple, MacOS,
334 IOS,
335 Linux,
336 Windows,
337 Android,
338 Web, }
340
341impl_option!(
342 OsCondition,
343 OptionOsCondition,
344 [Debug, Clone, Copy, PartialEq, Eq, Hash]
345);
346
347impl OsCondition {
348 #[must_use] pub const fn from_system_platform(platform: &crate::system::Platform) -> Self {
350 use crate::system::Platform;
351 match platform {
352 Platform::Windows => Self::Windows,
353 Platform::MacOs => Self::MacOS,
354 Platform::Linux(_) => Self::Linux,
355 Platform::Android => Self::Android,
356 Platform::Ios => Self::IOS,
357 Platform::Unknown => Self::Any,
358 }
359 }
360}
361
362#[repr(C, u8)]
363#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
364pub enum OsVersionCondition {
365 Min(OsVersion),
368 Max(OsVersion),
370 Exact(OsVersion),
372 DesktopEnvironment(LinuxDesktopEnv),
374 DesktopEnvMin(DesktopEnvVersion),
376 DesktopEnvMax(DesktopEnvVersion),
378 DesktopEnvExact(DesktopEnvVersion),
380}
381
382#[repr(C)]
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
386pub struct DesktopEnvVersion {
387 pub env: LinuxDesktopEnv,
388 pub version_id: u32,
389}
390
391#[repr(C)]
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
397pub struct OsVersion {
398 pub os: OsFamily,
400 pub version_id: u32,
403}
404
405impl Default for OsVersion {
406 fn default() -> Self {
407 Self::unknown()
408 }
409}
410
411impl OsVersion {
412 #[must_use] pub const fn new(os: OsFamily, version_id: u32) -> Self {
413 Self { os, version_id }
414 }
415
416 #[must_use] pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
419 if self.os == other.os {
420 Some(self.version_id.cmp(&other.version_id))
421 } else {
422 None }
424 }
425
426 #[must_use] pub fn is_at_least(&self, other: &Self) -> bool {
428 self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Less)
429 }
430
431 #[must_use] pub fn is_at_most(&self, other: &Self) -> bool {
433 self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Greater)
434 }
435}
436
437impl_option!(
438 OsVersion,
439 OptionOsVersion,
440 [Debug, Clone, Copy, PartialEq, Eq, Hash]
441);
442
443impl OsVersion {
444
445 #[must_use] pub fn is_exactly(&self, other: &Self) -> bool {
447 self.compare(other) == Some(core::cmp::Ordering::Equal)
448 }
449}
450
451#[repr(C)]
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
454pub enum OsFamily {
455 Windows,
456 MacOS,
457 IOS,
458 Linux,
459 Android,
460}
461
462impl OsVersion {
468 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);
523 pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
524 pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
525 pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
526 pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
527 pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
528 pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
529 pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
530 pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
531 pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
532 pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
533 pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
534 pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
535 pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
536 pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
537 pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
538 pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
539 pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
540
541 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);
566 pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
567 pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
568 pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
569 pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
570
571 #[must_use] pub const fn unknown() -> Self {
573 Self {
574 os: OsFamily::Linux, version_id: 0,
576 }
577 }
578}
579
580#[must_use] pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
583 let version_str = version_str.trim().to_lowercase();
584 let version_str = version_str.as_str();
585
586 match os {
587 OsFamily::Windows => parse_windows_version(version_str),
588 OsFamily::MacOS => parse_macos_version(version_str),
589 OsFamily::IOS => parse_ios_version(version_str),
590 OsFamily::Android => parse_android_version(version_str),
591 OsFamily::Linux => parse_linux_version(version_str),
592 }
593}
594
595fn parse_windows_version(s: &str) -> Option<OsVersion> {
596 let core = strip_os_prefix(s, &["windows", "win"]);
599 match core {
600 "2000" | "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
602 "xp" | "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
603 "vista" | "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
604 "7" | "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
605 "8" | "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
606 "8.1" | "8-1" | "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
607 "10" | "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
608 "11" => Some(OsVersion::WIN_11),
609 _ => None,
610 }
611}
612
613fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
617 for p in prefixes {
618 if let Some(rest) = s.strip_prefix(p) {
619 return rest.strip_prefix(['-', '_']).unwrap_or(rest);
620 }
621 }
622 s
623}
624
625fn parse_macos_version(s: &str) -> Option<OsVersion> {
626 match s {
627 "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
628 "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
629 "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
630 "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
631 "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
632 "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
633 "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
634 "lion" | "10.7" => Some(OsVersion::MACOS_LION),
635 "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
636 "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
637 "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
638 "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
639 "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
640 "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
641 "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
642 "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
643 "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
644 "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
645 "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
646 "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
647 "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
648 "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
649 _ => None,
650 }
651}
652
653fn parse_ios_version(s: &str) -> Option<OsVersion> {
654 match s {
655 "1" | "1.0" => Some(OsVersion::IOS_1),
656 "2" | "2.0" => Some(OsVersion::IOS_2),
657 "3" | "3.0" => Some(OsVersion::IOS_3),
658 "4" | "4.0" => Some(OsVersion::IOS_4),
659 "5" | "5.0" => Some(OsVersion::IOS_5),
660 "6" | "6.0" => Some(OsVersion::IOS_6),
661 "7" | "7.0" => Some(OsVersion::IOS_7),
662 "8" | "8.0" => Some(OsVersion::IOS_8),
663 "9" | "9.0" => Some(OsVersion::IOS_9),
664 "10" | "10.0" => Some(OsVersion::IOS_10),
665 "11" | "11.0" => Some(OsVersion::IOS_11),
666 "12" | "12.0" => Some(OsVersion::IOS_12),
667 "13" | "13.0" => Some(OsVersion::IOS_13),
668 "14" | "14.0" => Some(OsVersion::IOS_14),
669 "15" | "15.0" => Some(OsVersion::IOS_15),
670 "16" | "16.0" => Some(OsVersion::IOS_16),
671 "17" | "17.0" => Some(OsVersion::IOS_17),
672 "18" | "18.0" => Some(OsVersion::IOS_18),
673 _ => None,
674 }
675}
676
677fn parse_android_version(s: &str) -> Option<OsVersion> {
678 match s {
679 "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
680 "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
681 "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
682 "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
683 "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
684 "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
685 "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
686 "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
687 "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
688 "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
689 "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
690 "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
691 "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
692 "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
693 "10" | "q" => Some(OsVersion::ANDROID_10),
694 "11" | "r" => Some(OsVersion::ANDROID_11),
695 "12" | "s" => Some(OsVersion::ANDROID_12),
696 "12l" | "12L" => Some(OsVersion::ANDROID_12L),
697 "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
698 "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
699 "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
700 _ => {
701 if let Some(api) = s.strip_prefix("api") {
703 if let Ok(level) = api.trim().parse::<u32>() {
704 return Some(OsVersion::new(OsFamily::Android, level));
705 }
706 }
707 None
708 }
709 }
710}
711
712fn parse_linux_version(s: &str) -> Option<OsVersion> {
713 let s = strip_os_prefix(s, &["linux"]);
715 let mut parts = s.split('.');
717 let major = parts.next()?.parse::<u32>().ok()?;
718 let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
719 let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
720 let encoded = major
724 .checked_mul(1000)?
725 .checked_add(minor.checked_mul(10)?)?
726 .checked_add(patch)?;
727 Some(OsVersion::new(OsFamily::Linux, encoded))
728}
729
730#[repr(C)]
736#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
737pub enum LinuxDesktopEnv {
738 Gnome,
739 KDE,
740 XFCE,
742 Unity,
744 Cinnamon,
746 MATE,
748 Other,
749}
750
751impl LinuxDesktopEnv {
752 #[must_use] pub const fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
754 use crate::system::DesktopEnvironment;
755 match de {
756 DesktopEnvironment::Gnome => Self::Gnome,
757 DesktopEnvironment::Kde => Self::KDE,
758 DesktopEnvironment::Other(_) => Self::Other,
759 }
760 }
761}
762
763#[repr(C)]
765#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
766pub enum MediaType {
767 Screen,
768 Print,
769 All,
770}
771#[allow(variant_size_differences)] #[repr(C, u8)]
773#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
774pub enum ThemeCondition {
775 Light,
776 Dark,
777 Custom(AzString),
778 SystemPreferred,
780}
781
782impl_option!(
783 ThemeCondition,
784 OptionThemeCondition,
785 copy = false,
786 [Debug, Clone, PartialEq, Eq, Hash]
787);
788
789impl ThemeCondition {
790 #[must_use] pub const fn from_system_theme(theme: crate::system::Theme) -> Self {
792 use crate::system::Theme;
793 match theme {
794 Theme::Light => Self::Light,
795 Theme::Dark => Self::Dark,
796 }
797 }
798}
799
800#[repr(C)]
802#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
803pub enum OrientationType {
804 Portrait,
805 Landscape,
806}
807
808#[repr(C, u8)]
811#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
812pub enum LanguageCondition {
813 Exact(AzString),
815 Prefix(AzString),
817}
818
819impl LanguageCondition {
820 #[must_use] pub fn matches(&self, language: &str) -> bool {
822 match self {
823 Self::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
824 Self::Prefix(prefix) => {
825 let prefix_str = prefix.as_str();
826 if language.len() < prefix_str.len() {
827 return false;
828 }
829 let Some(lang_prefix) = language.get(..prefix_str.len()) else {
835 return false;
836 };
837 if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
838 return false;
839 }
840 language.len() == prefix_str.len()
842 || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
843 }
844 }
845 }
846}
847
848#[repr(C)]
849#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
850pub enum PseudoStateType {
851 Normal,
853 Hover,
855 Active,
857 Focus,
859 Disabled,
861 CheckedTrue,
863 CheckedFalse,
865 FocusWithin,
867 Visited,
869 Backdrop,
871 Dragging,
873 DragOver,
875}
876
877impl_option!(
878 LinuxDesktopEnv,
879 OptionLinuxDesktopEnv,
880 [Debug, Clone, Copy, PartialEq, Eq, Hash]
881);
882
883pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
885pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
887
888#[repr(C)]
901#[derive(Debug, Clone)]
902pub struct DynamicSelectorContext {
903 pub os: OsCondition,
905 pub os_version: OsVersion,
906 pub desktop_env: OptionLinuxDesktopEnv,
907 pub de_version: u32,
912
913 pub theme: ThemeCondition,
915
916 pub media_type: MediaType,
918 pub viewport_width: f32,
919 pub viewport_height: f32,
920
921 pub container_width: f32,
924 pub container_height: f32,
925 pub container_name: OptionString,
926
927 pub prefers_reduced_motion: BoolCondition,
929 pub prefers_high_contrast: BoolCondition,
930
931 pub orientation: OrientationType,
933
934 pub pseudo_state: PseudoStateFlags,
936
937 pub language: AzString,
939
940 pub window_focused: bool,
943}
944
945impl PartialEq for DynamicSelectorContext {
946 fn eq(&self, other: &Self) -> bool {
947 self.os == other.os
951 && self.os_version == other.os_version
952 && self.desktop_env == other.desktop_env
953 && self.de_version == other.de_version
954 && self.theme == other.theme
955 && self.media_type == other.media_type
956 && self.viewport_width.to_bits() == other.viewport_width.to_bits()
957 && self.viewport_height.to_bits() == other.viewport_height.to_bits()
958 && self.container_width.to_bits() == other.container_width.to_bits()
959 && self.container_height.to_bits() == other.container_height.to_bits()
960 && self.container_name == other.container_name
961 && self.prefers_reduced_motion == other.prefers_reduced_motion
962 && self.prefers_high_contrast == other.prefers_high_contrast
963 && self.orientation == other.orientation
964 && self.pseudo_state == other.pseudo_state
965 && self.language == other.language
966 && self.window_focused == other.window_focused
967 }
968}
969
970impl Default for DynamicSelectorContext {
971 fn default() -> Self {
972 Self {
973 os: OsCondition::Any,
974 os_version: OsVersion::unknown(),
975 desktop_env: OptionLinuxDesktopEnv::None,
976 de_version: 0,
977 theme: ThemeCondition::Light,
978 media_type: MediaType::Screen,
979 viewport_width: DEFAULT_VIEWPORT_WIDTH,
980 viewport_height: DEFAULT_VIEWPORT_HEIGHT,
981 container_width: f32::NAN,
982 container_height: f32::NAN,
983 container_name: OptionString::None,
984 prefers_reduced_motion: BoolCondition::False,
985 prefers_high_contrast: BoolCondition::False,
986 orientation: OrientationType::Landscape,
987 pseudo_state: PseudoStateFlags::default(),
988 language: AzString::from_const_str("en-US"),
989 window_focused: true,
990 }
991 }
992}
993
994impl DynamicSelectorContext {
995 #[must_use] pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
997 let os = OsCondition::from_system_platform(&system_style.platform);
998 let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
999 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
1000 } else {
1001 OptionLinuxDesktopEnv::None
1002 };
1003 let theme = ThemeCondition::from_system_theme(system_style.theme);
1004
1005 Self {
1006 os,
1007 os_version: system_style.os_version, desktop_env,
1009 de_version: 0, theme,
1011 media_type: MediaType::Screen,
1012 viewport_width: DEFAULT_VIEWPORT_WIDTH, viewport_height: DEFAULT_VIEWPORT_HEIGHT,
1014 container_width: f32::NAN,
1015 container_height: f32::NAN,
1016 container_name: OptionString::None,
1017 prefers_reduced_motion: system_style.prefers_reduced_motion,
1018 prefers_high_contrast: system_style.prefers_high_contrast,
1019 orientation: OrientationType::Landscape,
1020 pseudo_state: PseudoStateFlags::default(),
1021 language: system_style.language.clone(),
1022 window_focused: true,
1023 }
1024 }
1025
1026 #[must_use] pub fn with_viewport(&self, width: f32, height: f32) -> Self {
1028 let mut ctx = self.clone();
1029 ctx.viewport_width = width;
1030 ctx.viewport_height = height;
1031 ctx.orientation = if width > height {
1032 OrientationType::Landscape
1033 } else {
1034 OrientationType::Portrait
1035 };
1036 ctx
1037 }
1038
1039 #[must_use] pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
1041 let mut ctx = self.clone();
1042 ctx.container_width = width;
1043 ctx.container_height = height;
1044 ctx.container_name = name.into();
1045 ctx
1046 }
1047
1048 #[must_use] pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
1050 let mut ctx = self.clone();
1051 ctx.pseudo_state = state;
1052 ctx
1053 }
1054
1055 #[must_use] pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
1057 for bp in breakpoints {
1058 let self_above = self.viewport_width >= *bp;
1059 let other_above = other.viewport_width >= *bp;
1060 if self_above != other_above {
1061 return true;
1062 }
1063 }
1064 false
1065 }
1066}
1067
1068impl DynamicSelector {
1069 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1071 match self {
1072 Self::Os(os) => Self::match_os(*os, ctx.os),
1073 Self::OsVersion(ver) => Self::match_os_version(ver, ctx.os_version, ctx.desktop_env, ctx.de_version),
1074 Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
1075 Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
1076 Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
1077 Self::ContainerWidth(range) => {
1078 !ctx.container_width.is_nan() && range.matches(ctx.container_width)
1079 }
1080 Self::ContainerHeight(range) => {
1081 !ctx.container_height.is_nan() && range.matches(ctx.container_height)
1082 }
1083 Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
1084 Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
1085 Self::AspectRatio(range) => {
1086 let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
1087 range.matches(ratio)
1088 }
1089 Self::Orientation(orient) => *orient == ctx.orientation,
1090 Self::PrefersReducedMotion(pref) => {
1091 bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
1092 }
1093 Self::PrefersHighContrast(pref) => {
1094 bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
1095 }
1096 Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
1097 Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
1098 }
1099 }
1100
1101 fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
1102 match condition {
1103 OsCondition::Any => true,
1104 OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
1105 _ => condition == actual,
1106 }
1107 }
1108
1109 fn match_os_version(
1110 condition: &OsVersionCondition,
1111 actual: OsVersion,
1112 desktop_env: OptionLinuxDesktopEnv,
1113 de_version: u32,
1114 ) -> bool {
1115 let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
1118 match condition {
1119 OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
1120 OsVersionCondition::Min(ver) => actual.is_at_least(ver),
1121 OsVersionCondition::Max(ver) => actual.is_at_most(ver),
1122 OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
1123 OsVersionCondition::DesktopEnvMin(d) =>
1124 de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
1125 OsVersionCondition::DesktopEnvMax(d) =>
1126 de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
1127 OsVersionCondition::DesktopEnvExact(d) =>
1128 de_matches(&d.env) && de_version != 0 && de_version == d.version_id,
1132 }
1133 }
1134
1135 fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
1136 match (condition, actual) {
1137 (ThemeCondition::SystemPreferred, _) => true,
1138 _ => condition == actual,
1139 }
1140 }
1141
1142 const fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
1143 let node_state = &ctx.pseudo_state;
1144 match state {
1145 PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
1147 PseudoStateType::Active => node_state.active,
1148 PseudoStateType::Focus => node_state.focused,
1149 PseudoStateType::Disabled => node_state.disabled,
1150 PseudoStateType::CheckedTrue => node_state.checked,
1151 PseudoStateType::CheckedFalse => !node_state.checked,
1152 PseudoStateType::FocusWithin => node_state.focus_within,
1153 PseudoStateType::Visited => node_state.visited,
1154 PseudoStateType::Backdrop => node_state.backdrop,
1155 PseudoStateType::Dragging => node_state.dragging,
1156 PseudoStateType::DragOver => node_state.drag_over,
1157 }
1158 }
1159}
1160
1161#[cfg(feature = "parser")]
1175#[must_use] pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
1176 let trimmed = content.trim();
1177 let inner = trimmed
1178 .strip_prefix('(').and_then(|s| s.strip_suffix(')'))
1179 .unwrap_or(trimmed)
1180 .trim();
1181 let inner = inner
1182 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1183 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1184 .unwrap_or(inner)
1185 .trim();
1186 if inner.is_empty() {
1187 return None;
1188 }
1189
1190 let (subject, op_and_version) = split_op_and_version(inner);
1192 let subject = subject.trim();
1193
1194 let (family_str, de_str) = match subject.split_once(':') {
1196 Some((f, d)) => (f.trim(), Some(d.trim())),
1197 None => (subject, None),
1198 };
1199
1200 let family = parse_os_family_token(family_str)?;
1201 let de = match de_str {
1202 Some(s) if !s.is_empty() => Some(parse_de_token(s)),
1203 _ => None,
1204 };
1205
1206 let mut out = Vec::new();
1207 out.push(DynamicSelector::Os(family));
1211
1212 match (de, op_and_version) {
1213 (Some(env), None) => {
1215 out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
1216 }
1217 (Some(env), Some((op, ver_str))) => {
1219 let v: u32 = ver_str.parse().ok()?;
1220 let dev = DesktopEnvVersion { env, version_id: v };
1221 let cond = match op {
1222 VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
1223 VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
1224 VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
1225 };
1226 out.push(DynamicSelector::OsVersion(cond));
1227 }
1228 (None, Some((op, ver_str))) => {
1230 let os_family = match family {
1231 OsCondition::Linux => OsFamily::Linux,
1232 OsCondition::Windows => OsFamily::Windows,
1233 OsCondition::MacOS => OsFamily::MacOS,
1234 OsCondition::IOS => OsFamily::IOS,
1235 OsCondition::Android => OsFamily::Android,
1236 _ => return None,
1238 };
1239 let version = parse_os_version(os_family, ver_str)?;
1240 let cond = match op {
1241 VersionOp::Min => OsVersionCondition::Min(version),
1242 VersionOp::Max => OsVersionCondition::Max(version),
1243 VersionOp::Exact => OsVersionCondition::Exact(version),
1244 };
1245 out.push(DynamicSelector::OsVersion(cond));
1246 }
1247 (None, None) => {}
1249 }
1250
1251 Some(out)
1252}
1253
1254#[cfg(feature = "parser")]
1255#[derive(Copy, Clone)]
1256enum VersionOp { Min, Max, Exact }
1257
1258#[cfg(feature = "parser")]
1261fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
1262 let candidates: &[(&str, VersionOp)] = &[
1264 (">=", VersionOp::Min),
1265 ("<=", VersionOp::Max),
1266 ("=", VersionOp::Exact),
1267 (">", VersionOp::Min),
1268 ("<", VersionOp::Max),
1269 ];
1270 let mut best: Option<(usize, usize, VersionOp)> = None;
1271 for (op_str, op) in candidates {
1272 if let Some(pos) = s.find(op_str) {
1273 let len = op_str.len();
1274 best = Some(match best {
1275 None => (pos, len, *op),
1276 Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
1277 Some(b) => b,
1278 });
1279 }
1280 }
1281 match best {
1282 Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
1283 None => (s, None),
1284 }
1285}
1286
1287#[cfg(feature = "parser")]
1288fn parse_os_family_token(s: &str) -> Option<OsCondition> {
1289 match s.to_lowercase().as_str() {
1290 "linux" => Some(OsCondition::Linux),
1291 "windows" | "win" => Some(OsCondition::Windows),
1292 "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1293 "ios" => Some(OsCondition::IOS),
1294 "android" => Some(OsCondition::Android),
1295 "apple" => Some(OsCondition::Apple),
1296 "web" | "wasm" => Some(OsCondition::Web),
1297 "any" | "all" | "*" => Some(OsCondition::Any),
1298 _ => None,
1299 }
1300}
1301
1302#[cfg(feature = "parser")]
1303fn parse_de_token(s: &str) -> LinuxDesktopEnv {
1304 match s.to_lowercase().as_str() {
1305 "gnome" => LinuxDesktopEnv::Gnome,
1306 "kde" => LinuxDesktopEnv::KDE,
1307 "xfce" => LinuxDesktopEnv::XFCE,
1308 "unity" => LinuxDesktopEnv::Unity,
1309 "cinnamon" => LinuxDesktopEnv::Cinnamon,
1310 "mate" => LinuxDesktopEnv::MATE,
1311 _ => LinuxDesktopEnv::Other,
1312 }
1313}
1314
1315#[repr(C)]
1325#[derive(Debug, Clone, PartialEq)]
1326pub struct CssPropertyWithConditions {
1327 pub property: CssProperty,
1329 pub apply_if: DynamicSelectorVec,
1332}
1333
1334impl_option!(
1335 CssPropertyWithConditions,
1336 OptionCssPropertyWithConditions,
1337 copy = false,
1338 [Debug, Clone, PartialEq, Eq, PartialOrd]
1339);
1340
1341impl Eq for CssPropertyWithConditions {}
1342
1343pub fn collect_viewport_thresholds(
1351 conds: &[DynamicSelector],
1352 widths: &mut Vec<f32>,
1353 heights: &mut Vec<f32>,
1354) {
1355 for c in conds {
1356 match c {
1357 DynamicSelector::ViewportWidth(r) => {
1358 if r.min.is_finite() {
1359 widths.push(r.min);
1360 }
1361 if r.max.is_finite() {
1362 widths.push(r.max);
1363 }
1364 }
1365 DynamicSelector::ViewportHeight(r) => {
1366 if r.min.is_finite() {
1367 heights.push(r.min);
1368 }
1369 if r.max.is_finite() {
1370 heights.push(r.max);
1371 }
1372 }
1373 _ => {}
1374 }
1375 }
1376}
1377
1378impl PartialOrd for CssPropertyWithConditions {
1379 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1380 Some(self.cmp(other))
1381 }
1382}
1383
1384impl Ord for CssPropertyWithConditions {
1385 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1386 self.property
1392 .cmp(&other.property)
1393 .then_with(|| self.apply_if.as_slice().cmp(other.apply_if.as_slice()))
1394 }
1395}
1396
1397impl CssPropertyWithConditions {
1398 #[must_use] pub const fn simple(property: CssProperty) -> Self {
1400 Self {
1401 property,
1402 apply_if: DynamicSelectorVec::from_const_slice(&[]),
1403 }
1404 }
1405
1406 #[must_use] pub const fn with_single_condition(
1408 property: CssProperty,
1409 conditions: &'static [DynamicSelector],
1410 ) -> Self {
1411 Self {
1412 property,
1413 apply_if: DynamicSelectorVec::from_const_slice(conditions),
1414 }
1415 }
1416
1417 #[must_use] pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
1419 Self {
1420 property,
1421 apply_if: DynamicSelectorVec::from_vec(vec![condition]),
1422 }
1423 }
1424
1425 #[must_use] pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
1427 Self {
1428 property,
1429 apply_if: conditions,
1430 }
1431 }
1432
1433 #[must_use] pub const fn on_hover(property: CssProperty) -> Self {
1435 Self::with_single_condition(
1436 property,
1437 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
1438 )
1439 }
1440
1441 #[must_use] pub const fn on_active(property: CssProperty) -> Self {
1443 Self::with_single_condition(
1444 property,
1445 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1446 )
1447 }
1448
1449 #[must_use] pub const fn on_focus(property: CssProperty) -> Self {
1451 Self::with_single_condition(
1452 property,
1453 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1454 )
1455 }
1456
1457 #[must_use] pub const fn when_disabled(property: CssProperty) -> Self {
1459 Self::with_single_condition(
1460 property,
1461 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1462 )
1463 }
1464
1465 #[must_use] pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1467 Self::with_condition(property, DynamicSelector::Os(os))
1468 }
1469
1470 #[must_use] pub const fn dark_theme(property: CssProperty) -> Self {
1472 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1473 }
1474
1475 #[must_use] pub const fn light_theme(property: CssProperty) -> Self {
1477 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1478 }
1479
1480 #[must_use] pub const fn on_windows(property: CssProperty) -> Self {
1482 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1483 }
1484
1485 #[must_use] pub const fn on_macos(property: CssProperty) -> Self {
1487 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1488 }
1489
1490 #[must_use] pub const fn on_linux(property: CssProperty) -> Self {
1492 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1493 }
1494
1495 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1497 if self.apply_if.as_slice().is_empty() {
1499 return true;
1500 }
1501
1502 self.apply_if
1504 .as_slice()
1505 .iter()
1506 .all(|selector| selector.matches(ctx))
1507 }
1508
1509 #[must_use] pub fn is_conditional(&self) -> bool {
1511 !self.apply_if.as_slice().is_empty()
1512 }
1513
1514 #[must_use] pub fn is_pseudo_state_only(&self) -> bool {
1517 let conditions = self.apply_if.as_slice();
1518 !conditions.is_empty()
1519 && conditions
1520 .iter()
1521 .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1522 }
1523
1524 #[must_use] pub const fn is_layout_affecting(&self) -> bool {
1530 self.property.get_type().can_trigger_relayout()
1531 }
1532}
1533
1534impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
1535impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1536impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1537impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1538impl_vec_clone!(
1539 CssPropertyWithConditions,
1540 CssPropertyWithConditionsVec,
1541 CssPropertyWithConditionsVecDestructor
1542);
1543
1544impl Eq for CssPropertyWithConditionsVec {}
1546
1547impl Ord for CssPropertyWithConditionsVec {
1548 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1549 self.as_slice().cmp(other.as_slice())
1552 }
1553}
1554
1555impl core::hash::Hash for CssPropertyWithConditions {
1556 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1557 self.property.hash(state);
1558 self.apply_if.as_slice().hash(state);
1562 }
1563}
1564
1565impl core::hash::Hash for CssPropertyWithConditionsVec {
1566 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1567 self.as_slice().hash(state);
1569 }
1570}
1571
1572impl CssPropertyWithConditionsVec {
1573 #[cfg(feature = "parser")]
1593 #[must_use] pub fn parse(style: &str) -> Self {
1594 Self::parse_with_conditions(style, &[])
1595 }
1596
1597 #[cfg(feature = "parser")]
1599 fn parse_with_conditions(style: &str, inherited_conditions: &[DynamicSelector]) -> Self {
1600 use crate::props::property::{
1601 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1602 CssPropertyType,
1603 };
1604
1605 let mut props = Vec::new();
1606 let key_map = CssKeyMap::get();
1607 let style = style.trim();
1608
1609 if style.is_empty() {
1610 return Self::from_vec(props);
1611 }
1612
1613 let chars = style.chars();
1615 let mut current_segment = String::new();
1616 let mut brace_depth = 0;
1617
1618 for c in chars {
1619 match c {
1620 '{' => {
1621 brace_depth += 1;
1622 current_segment.push(c);
1623 }
1624 '}' => {
1625 brace_depth -= 1;
1626 current_segment.push(c);
1627
1628 if brace_depth == 0 {
1629 let segment = current_segment.trim().to_string();
1631 current_segment.clear();
1632
1633 if let Some(parsed) = Self::parse_block_segment(&segment, inherited_conditions, &key_map) {
1634 props.extend(parsed);
1635 }
1636 }
1637 }
1638 ';' if brace_depth == 0 => {
1639 let segment = current_segment.trim().to_string();
1641 current_segment.clear();
1642
1643 if !segment.is_empty() {
1644 if let Some(parsed) = Self::parse_property_segment(&segment, inherited_conditions, &key_map) {
1645 props.extend(parsed);
1646 }
1647 }
1648 }
1649 _ => {
1650 current_segment.push(c);
1651 }
1652 }
1653 }
1654
1655 let remaining = current_segment.trim();
1657 if !remaining.is_empty() && !remaining.contains('{') {
1658 if let Some(parsed) = Self::parse_property_segment(remaining, inherited_conditions, &key_map) {
1659 props.extend(parsed);
1660 }
1661 }
1662
1663 Self::from_vec(props)
1664 }
1665
1666 #[cfg(feature = "parser")]
1668 fn parse_block_segment(
1669 segment: &str,
1670 inherited_conditions: &[DynamicSelector],
1671 key_map: &crate::props::property::CssKeyMap,
1672 ) -> Option<Vec<CssPropertyWithConditions>> {
1673 let brace_pos = segment.find('{')?;
1675 let selector = segment[..brace_pos].trim();
1676
1677 let content_start = brace_pos + 1;
1679 let content_end = segment.rfind('}')?;
1680 if content_end <= content_start {
1681 return None;
1682 }
1683 let content = &segment[content_start..content_end];
1684
1685 let mut conditions = inherited_conditions.to_vec();
1687
1688 if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1689 conditions.extend(new_conditions);
1690 } else {
1691 return None;
1693 }
1694
1695 let parsed = Self::parse_with_conditions(content, &conditions);
1697 Some(parsed.into_library_owned_vec())
1698 }
1699
1700 #[cfg(feature = "parser")]
1702 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1703 let selector = selector.trim();
1704
1705 if let Some(pseudo) = selector.strip_prefix(':') {
1707 match pseudo {
1708 "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1709 "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
1710 "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1711 "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
1712 "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
1713 "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
1714 "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
1715 "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
1716 "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
1717 "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
1718 _ => return None,
1719 }
1720 }
1721
1722 if let Some(rule_content) = selector.strip_prefix('@') {
1724 return Self::parse_at_rule(rule_content);
1725 }
1726
1727 if selector == "*" {
1729 return Some(vec![]);
1730 }
1731
1732 if selector.is_empty() {
1734 return Some(vec![]);
1735 }
1736
1737 None
1738 }
1739
1740 #[cfg(feature = "parser")]
1744 fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
1745 if let Some(rest) = rule_content
1751 .strip_prefix("os ")
1752 .or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
1753 {
1754 if let Some(conds) = parse_os_at_rule_content(rest) {
1755 return Some(conds);
1756 }
1757 }
1758
1759 if let Some(rest) = rule_content.strip_prefix("media ") {
1761 let media_query = rest.trim();
1762 if let Some(media_conds) = Self::parse_media_query(media_query) {
1763 return Some(media_conds);
1764 }
1765 }
1766
1767 if let Some(rest) = rule_content.strip_prefix("theme ") {
1769 let theme = rest.trim();
1770 match theme {
1771 "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
1772 "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1773 _ => return None,
1774 }
1775 }
1776
1777 let lang_body = rule_content
1779 .strip_prefix("lang(")
1780 .map(|r| r.trim_end_matches(')').trim())
1781 .or_else(|| rule_content.strip_prefix("lang ").map(str::trim));
1782 if let Some(lang_str) = lang_body {
1783 let lang_str = lang_str
1784 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1785 .or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1786 .unwrap_or(lang_str);
1787 if !lang_str.is_empty() {
1788 return Some(vec![DynamicSelector::Language(
1789 LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
1790 )]);
1791 }
1792 }
1793
1794 if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
1796 let container_str = if rule_content.starts_with("container(") {
1797 &rule_content[9..] } else {
1799 rule_content[10..].trim()
1800 };
1801 let mut conds = Vec::new();
1802 let (name_part, query_part) = if container_str.starts_with('(') {
1804 (None, container_str)
1805 } else if let Some(paren_idx) = container_str.find('(') {
1806 let name = container_str[..paren_idx].trim();
1807 if name.is_empty() {
1808 (None, container_str)
1809 } else {
1810 (Some(name), &container_str[paren_idx..])
1811 }
1812 } else {
1813 if !container_str.is_empty() {
1814 return Some(vec![DynamicSelector::ContainerName(
1815 AzString::from(container_str.to_string())
1816 )]);
1817 }
1818 return None;
1819 };
1820 if let Some(name) = name_part {
1821 conds.push(DynamicSelector::ContainerName(
1822 AzString::from(name.to_string())
1823 ));
1824 }
1825 if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1827 if let Some((key, value)) = inner.split_once(':') {
1828 let key = key.trim();
1829 let value = value.trim();
1830 let px_value = value.strip_suffix("px")
1831 .and_then(|v| v.trim().parse::<f32>().ok())
1832 .filter(|px| !px.is_nan()); match key {
1834 "min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
1835 "max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
1836 "min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
1837 "max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
1838 _ => {}
1839 }
1840 }
1841 }
1842 if !conds.is_empty() {
1843 return Some(conds);
1844 }
1845 }
1846
1847 if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
1849 return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
1850 }
1851
1852 if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
1854 return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
1855 }
1856
1857 None
1858 }
1859
1860 #[cfg(feature = "parser")]
1862 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1863 let query = query.trim();
1864
1865 if query.starts_with('(') && query.ends_with(')') {
1867 let inner = &query[1..query.len()-1];
1868 if let Some((key, value)) = inner.split_once(':') {
1869 let key = key.trim();
1870 let value = value.trim();
1871
1872 let px_value = value.strip_suffix("px")
1878 .and_then(|v| v.trim().parse::<f32>().ok())
1879 .filter(|px| !px.is_nan());
1880
1881 match key {
1882 "min-width" => {
1883 if let Some(px) = px_value {
1884 return Some(vec![DynamicSelector::ViewportWidth(
1885 MinMaxRange::with_min(px)
1886 )]);
1887 }
1888 }
1889 "max-width" => {
1890 if let Some(px) = px_value {
1891 return Some(vec![DynamicSelector::ViewportWidth(
1892 MinMaxRange::with_max(px)
1893 )]);
1894 }
1895 }
1896 "min-height" => {
1897 if let Some(px) = px_value {
1898 return Some(vec![DynamicSelector::ViewportHeight(
1899 MinMaxRange::with_min(px)
1900 )]);
1901 }
1902 }
1903 "max-height" => {
1904 if let Some(px) = px_value {
1905 return Some(vec![DynamicSelector::ViewportHeight(
1906 MinMaxRange::with_max(px)
1907 )]);
1908 }
1909 }
1910 other => {
1911 if let Some(sel) = Self::parse_media_feature_inline(other, value) {
1913 return Some(vec![sel]);
1914 }
1915 }
1916 }
1917 }
1918 }
1919
1920 match query {
1922 "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
1923 "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
1924 "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
1925 _ => None,
1926 }
1927 }
1928
1929 #[cfg(feature = "parser")]
1932 fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
1933 match key {
1934 "orientation" => {
1935 if value.eq_ignore_ascii_case("portrait") {
1936 Some(DynamicSelector::Orientation(OrientationType::Portrait))
1937 } else if value.eq_ignore_ascii_case("landscape") {
1938 Some(DynamicSelector::Orientation(OrientationType::Landscape))
1939 } else {
1940 None
1941 }
1942 }
1943 "prefers-color-scheme" => {
1944 if value.eq_ignore_ascii_case("dark") {
1945 Some(DynamicSelector::Theme(ThemeCondition::Dark))
1946 } else if value.eq_ignore_ascii_case("light") {
1947 Some(DynamicSelector::Theme(ThemeCondition::Light))
1948 } else {
1949 None
1950 }
1951 }
1952 "prefers-reduced-motion" => {
1953 if value.eq_ignore_ascii_case("reduce") {
1954 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
1955 } else if value.eq_ignore_ascii_case("no-preference") {
1956 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
1957 } else {
1958 None
1959 }
1960 }
1961 "prefers-contrast" | "prefers-high-contrast" => {
1962 if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
1963 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
1964 } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
1965 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
1966 } else {
1967 None
1968 }
1969 }
1970 _ => None,
1971 }
1972 }
1973
1974 #[cfg(feature = "parser")]
1976 fn parse_property_segment(
1977 segment: &str,
1978 inherited_conditions: &[DynamicSelector],
1979 key_map: &crate::props::property::CssKeyMap,
1980 ) -> Option<Vec<CssPropertyWithConditions>> {
1981 use crate::props::property::{
1982 parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
1983 CssPropertyType,
1984 };
1985
1986 let segment = segment.trim();
1987 if segment.is_empty() {
1988 return None;
1989 }
1990
1991 let (key, value) = segment.split_once(':')?;
1992 let key = key.trim();
1993 let value = value.trim();
1994
1995 let mut props = Vec::new();
1996 let conditions = if inherited_conditions.is_empty() {
1997 DynamicSelectorVec::from_const_slice(&[])
1998 } else {
1999 DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
2000 };
2001
2002 if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
2004 if let Ok(prop) = parse_css_property(prop_type, value) {
2005 props.push(CssPropertyWithConditions {
2006 property: prop,
2007 apply_if: conditions,
2008 });
2009 return Some(props);
2010 }
2011 }
2012
2013 if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
2015 if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
2016 for prop in expanded_props {
2017 props.push(CssPropertyWithConditions {
2018 property: prop,
2019 apply_if: conditions.clone(),
2020 });
2021 }
2022 return Some(props);
2023 }
2024 }
2025
2026 None
2027 }
2028
2029}
2030
2031#[cfg(test)]
2032mod tests {
2033 use super::*;
2034
2035 #[test]
2036 fn test_inline_overflow_parse() {
2037 let style = "overflow: scroll;";
2038 let parsed = CssPropertyWithConditionsVec::parse(style);
2039 let props = parsed.into_library_owned_vec();
2040 assert!(!props.is_empty(), "Expected overflow to parse into at least 1 property");
2041 }
2042
2043 #[test]
2044 fn test_inline_overflow_y_parse() {
2045 let style = "overflow-y: scroll;";
2046 let parsed = CssPropertyWithConditionsVec::parse(style);
2047 let props = parsed.into_library_owned_vec();
2048 assert!(!props.is_empty(), "Expected overflow-y to parse into at least 1 property");
2049 }
2050
2051 #[test]
2052 fn test_inline_combined_style_with_overflow() {
2053 let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
2054 let parsed = CssPropertyWithConditionsVec::parse(style);
2055 let props = parsed.into_library_owned_vec();
2056 assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
2058 }
2059
2060 #[test]
2061 fn test_inline_grid_template_columns_parse() {
2062 use crate::props::layout::grid::GridTrackSizing;
2063 let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
2064 let parsed = CssPropertyWithConditionsVec::parse(style);
2065 let props = parsed.into_library_owned_vec();
2066 let grid_cols = props.iter().find(|p| {
2068 matches!(p.property, CssProperty::GridTemplateColumns(_))
2069 }).expect("Expected GridTemplateColumns property");
2070
2071 if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
2072 let template = value.get_property().expect("Expected Exact value");
2073 let tracks = template.tracks.as_ref();
2074 assert_eq!(tracks.len(), 4, "Expected 4 tracks");
2075 for (i, track) in tracks.iter().enumerate() {
2076 assert!(matches!(track, GridTrackSizing::Fixed(_)),
2077 "Track {i} should be Fixed(160px), got {track:?}");
2078 }
2079 } else {
2080 panic!("Expected CssProperty::GridTemplateColumns");
2081 }
2082 }
2083}
2084
2085#[cfg(test)]
2086#[allow(
2087 clippy::float_cmp,
2088 clippy::too_many_lines,
2089 clippy::cast_precision_loss,
2090 clippy::field_reassign_with_default,
2091 clippy::unreadable_literal
2092)]
2093mod autotest_generated {
2094 use core::cmp::Ordering;
2095 use std::collections::hash_map::DefaultHasher;
2096 use std::hash::{Hash, Hasher};
2097
2098 use super::*;
2099 use crate::props::property::CssPropertyType;
2100
2101 fn hash_of<T: Hash>(t: &T) -> u64 {
2106 let mut h = DefaultHasher::new();
2107 t.hash(&mut h);
2108 h.finish()
2109 }
2110
2111 fn paint_prop() -> CssProperty {
2113 CssProperty::const_none(CssPropertyType::TextColor)
2114 }
2115
2116 fn layout_prop() -> CssProperty {
2118 CssProperty::const_none(CssPropertyType::Width)
2119 }
2120
2121 fn all_selector_variants() -> Vec<DynamicSelector> {
2123 vec![
2124 DynamicSelector::Os(OsCondition::Linux),
2125 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
2126 DynamicSelector::Media(MediaType::Print),
2127 DynamicSelector::ViewportWidth(MinMaxRange::with_min(1.0)),
2128 DynamicSelector::ViewportHeight(MinMaxRange::with_max(2.0)),
2129 DynamicSelector::ContainerWidth(MinMaxRange::new(Some(1.0), Some(2.0))),
2130 DynamicSelector::ContainerHeight(MinMaxRange::new(None, None)),
2131 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")),
2132 DynamicSelector::Theme(ThemeCondition::Dark),
2133 DynamicSelector::AspectRatio(MinMaxRange::with_min(0.5)),
2134 DynamicSelector::Orientation(OrientationType::Portrait),
2135 DynamicSelector::PrefersReducedMotion(BoolCondition::True),
2136 DynamicSelector::PrefersHighContrast(BoolCondition::False),
2137 DynamicSelector::PseudoState(PseudoStateType::Hover),
2138 DynamicSelector::Language(LanguageCondition::Prefix(AzString::from_const_str("de"))),
2139 ]
2140 }
2141
2142 fn nasty_strings() -> Vec<String> {
2144 vec![
2145 String::new(),
2146 " ".to_string(),
2147 " \t\n\r ".to_string(),
2148 "\0".to_string(),
2149 "0".to_string(),
2150 "-0".to_string(),
2151 "-1".to_string(),
2152 "NaN".to_string(),
2153 "nan".to_string(),
2154 "inf".to_string(),
2155 "-inf".to_string(),
2156 "infinity".to_string(),
2157 "1e400".to_string(),
2158 i64::MAX.to_string(),
2159 i64::MIN.to_string(),
2160 u32::MAX.to_string(),
2161 u64::MAX.to_string(),
2162 "9999999999999999999999999999".to_string(),
2163 "1.7976931348623157e308".to_string(),
2164 "\u{1F600}".to_string(),
2165 "e\u{301}\u{301}\u{301}".to_string(),
2166 "日本語".to_string(),
2167 "\u{202e}gnome".to_string(),
2168 " linux ".to_string(),
2169 "linux;garbage".to_string(),
2170 "linux)".to_string(),
2171 "((((".to_string(),
2172 "))))".to_string(),
2173 ">=".to_string(),
2174 "<=<=<=".to_string(),
2175 ":::::".to_string(),
2176 "-".to_string(),
2177 "_".to_string(),
2178 ".".to_string(),
2179 "..".to_string(),
2180 "...".to_string(),
2181 "1.2.3.4.5.6".to_string(),
2182 "%s%s%n".to_string(),
2183 "\\x00\\xff".to_string(),
2184 "a".repeat(100_000),
2185 "1".repeat(100_000),
2186 ".".repeat(10_000),
2187 "(".repeat(5_000),
2188 "🦀".repeat(10_000),
2189 ]
2190 }
2191
2192 #[test]
2197 fn has_state_default_flags_only_normal_and_checked_false() {
2198 let flags = PseudoStateFlags::default();
2199 assert!(flags.has_state(PseudoStateType::Normal));
2201 assert!(flags.has_state(PseudoStateType::CheckedFalse));
2203 for state in [
2204 PseudoStateType::Hover,
2205 PseudoStateType::Active,
2206 PseudoStateType::Focus,
2207 PseudoStateType::Disabled,
2208 PseudoStateType::CheckedTrue,
2209 PseudoStateType::FocusWithin,
2210 PseudoStateType::Visited,
2211 PseudoStateType::Backdrop,
2212 PseudoStateType::Dragging,
2213 PseudoStateType::DragOver,
2214 ] {
2215 assert!(!flags.has_state(state), "{state:?} must be off by default");
2216 }
2217 }
2218
2219 #[test]
2220 fn has_state_all_flags_set_reports_every_state_except_checked_false() {
2221 let flags = PseudoStateFlags {
2222 hover: true,
2223 active: true,
2224 focused: true,
2225 disabled: true,
2226 checked: true,
2227 focus_within: true,
2228 visited: true,
2229 backdrop: true,
2230 dragging: true,
2231 drag_over: true,
2232 };
2233 assert!(flags.has_state(PseudoStateType::Hover));
2234 assert!(flags.has_state(PseudoStateType::CheckedTrue));
2235 assert!(!flags.has_state(PseudoStateType::CheckedFalse));
2237 assert!(flags.has_state(PseudoStateType::DragOver));
2238 assert!(flags.has_state(PseudoStateType::Normal));
2239 }
2240
2241 #[test]
2242 fn has_state_checked_true_and_false_are_never_both_active() {
2243 for checked in [false, true] {
2244 let flags = PseudoStateFlags {
2245 checked,
2246 ..PseudoStateFlags::default()
2247 };
2248 assert_ne!(
2249 flags.has_state(PseudoStateType::CheckedTrue),
2250 flags.has_state(PseudoStateType::CheckedFalse),
2251 "checked={checked}: CheckedTrue/CheckedFalse must be complementary"
2252 );
2253 }
2254 }
2255
2256 #[test]
2261 fn variant_tag_matches_declared_repr_discriminants() {
2262 for (expected, sel) in all_selector_variants().iter().enumerate() {
2263 let expected = u8::try_from(expected).expect("15 variants fit in u8");
2264 assert_eq!(
2265 sel.variant_tag(),
2266 expected,
2267 "variant_tag drifted from the #[repr(C, u8)] discriminant for {sel:?}"
2268 );
2269 }
2270 }
2271
2272 #[test]
2273 fn variant_tag_is_unique_per_variant() {
2274 let variants = all_selector_variants();
2275 let mut tags: Vec<u8> = variants.iter().map(DynamicSelector::variant_tag).collect();
2276 tags.sort_unstable();
2277 tags.dedup();
2278 assert_eq!(tags.len(), variants.len(), "variant tags must be unique");
2279 }
2280
2281 #[test]
2282 fn ord_is_keyed_on_variant_tag_first() {
2283 let variants = all_selector_variants();
2284 for w in variants.windows(2) {
2285 assert_eq!(
2286 w[0].cmp(&w[1]),
2287 Ordering::Less,
2288 "selectors must sort by variant tag: {:?} < {:?}",
2289 w[0],
2290 w[1]
2291 );
2292 }
2293 }
2294
2295 #[test]
2296 fn hash_distinguishes_variants_carrying_the_same_payload() {
2297 let range = MinMaxRange::with_min(800.0);
2300 let a = DynamicSelector::ViewportWidth(range);
2301 let b = DynamicSelector::ContainerWidth(range);
2302 assert_ne!(hash_of(&a), hash_of(&b));
2303 assert_ne!(a.cmp(&b), Ordering::Equal);
2304 }
2305
2306 #[test]
2307 fn hash_and_ord_agree_for_nan_carrying_ranges() {
2308 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2311 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2312 assert_eq!(a.cmp(&b), Ordering::Equal);
2313 assert_eq!(hash_of(&a), hash_of(&b));
2314 }
2315
2316 #[test]
2323 fn nan_sentinel_range_selector_is_reflexive_under_partial_eq() {
2324 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2325 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2326 assert_eq!(a, b, "Eq requires reflexivity, but the NaN `max` sentinel breaks it");
2327 }
2328
2329 #[test]
2331 fn ord_equal_implies_partial_eq_for_range_selectors() {
2332 let a = DynamicSelector::ViewportHeight(MinMaxRange::with_max(600.0));
2333 let b = a.clone();
2334 assert_eq!(a.cmp(&b), Ordering::Equal);
2335 assert!(
2336 a == b,
2337 "cmp() == Equal must imply == (Ord/Eq contract); NaN sentinel breaks it"
2338 );
2339 }
2340
2341 #[test]
2346 fn min_max_range_new_roundtrips_finite_values() {
2347 let r = MinMaxRange::new(Some(1.5), Some(9.5));
2348 assert_eq!(r.min(), Some(1.5));
2349 assert_eq!(r.max(), Some(9.5));
2350 }
2351
2352 #[test]
2353 fn min_max_range_new_none_encodes_nan_sentinel() {
2354 let r = MinMaxRange::new(None, None);
2355 assert!(r.min.is_nan());
2356 assert!(r.max.is_nan());
2357 assert_eq!(r.min(), None);
2358 assert_eq!(r.max(), None);
2359 }
2360
2361 #[test]
2362 fn min_max_range_new_nan_argument_is_indistinguishable_from_none() {
2363 let r = MinMaxRange::new(Some(f32::NAN), Some(f32::NAN));
2366 assert_eq!(r.min(), None);
2367 assert_eq!(r.max(), None);
2368 assert!(r.matches(0.0));
2369 assert!(r.matches(f32::MAX));
2370 }
2371
2372 #[test]
2373 fn min_max_range_with_min_and_with_max_leave_the_other_side_open() {
2374 let lo = MinMaxRange::with_min(-0.0);
2375 assert_eq!(lo.min(), Some(-0.0));
2376 assert_eq!(lo.max(), None);
2377
2378 let hi = MinMaxRange::with_max(f32::MAX);
2379 assert_eq!(hi.min(), None);
2380 assert_eq!(hi.max(), Some(f32::MAX));
2381 }
2382
2383 #[test]
2384 fn min_max_range_getters_survive_extreme_values() {
2385 for v in [
2386 0.0_f32,
2387 -0.0,
2388 f32::MIN,
2389 f32::MAX,
2390 f32::MIN_POSITIVE,
2391 f32::EPSILON,
2392 f32::INFINITY,
2393 f32::NEG_INFINITY,
2394 ] {
2395 let r = MinMaxRange::new(Some(v), Some(v));
2396 assert_eq!(r.min(), Some(v));
2397 assert_eq!(r.max(), Some(v));
2398 }
2399 }
2400
2401 #[test]
2406 fn matches_zero_boundary_is_inclusive() {
2407 assert!(MinMaxRange::with_min(0.0).matches(0.0));
2408 assert!(MinMaxRange::with_max(0.0).matches(0.0));
2409 assert!(MinMaxRange::new(Some(0.0), Some(0.0)).matches(0.0));
2410 assert!(MinMaxRange::with_min(0.0).matches(-0.0));
2412 assert!(MinMaxRange::with_max(0.0).matches(-0.0));
2413 }
2414
2415 #[test]
2416 fn matches_negative_values_are_ordered_correctly() {
2417 let r = MinMaxRange::new(Some(-10.0), Some(-1.0));
2418 assert!(r.matches(-10.0));
2419 assert!(r.matches(-5.0));
2420 assert!(r.matches(-1.0));
2421 assert!(!r.matches(-10.001));
2422 assert!(!r.matches(0.0));
2423 }
2424
2425 #[test]
2426 fn matches_at_float_extremes_does_not_panic() {
2427 let open = MinMaxRange::new(None, None);
2428 let bounded = MinMaxRange::new(Some(f32::MIN), Some(f32::MAX));
2429 for v in [
2430 f32::MIN,
2431 f32::MAX,
2432 f32::INFINITY,
2433 f32::NEG_INFINITY,
2434 f32::MIN_POSITIVE,
2435 -f32::MIN_POSITIVE,
2436 ] {
2437 assert!(open.matches(v), "open range must accept {v}");
2439 }
2440 assert!(bounded.matches(0.0));
2441 assert!(bounded.matches(f32::MIN));
2442 assert!(bounded.matches(f32::MAX));
2443 assert!(!bounded.matches(f32::INFINITY));
2445 assert!(!bounded.matches(f32::NEG_INFINITY));
2446 }
2447
2448 #[test]
2449 fn matches_nan_value_is_rejected_by_any_real_bound() {
2450 assert!(!MinMaxRange::with_min(0.0).matches(f32::NAN));
2452 assert!(!MinMaxRange::with_max(0.0).matches(f32::NAN));
2453 assert!(!MinMaxRange::new(Some(1.0), Some(2.0)).matches(f32::NAN));
2454 assert!(MinMaxRange::new(None, None).matches(f32::NAN));
2456 }
2457
2458 #[test]
2459 fn matches_infinite_bounds_are_deterministic() {
2460 let min_inf = MinMaxRange::with_min(f32::INFINITY);
2461 assert!(!min_inf.matches(f32::MAX));
2462 assert!(min_inf.matches(f32::INFINITY));
2463
2464 let max_neg_inf = MinMaxRange::with_max(f32::NEG_INFINITY);
2465 assert!(!max_neg_inf.matches(f32::MIN));
2466 assert!(max_neg_inf.matches(f32::NEG_INFINITY));
2467 }
2468
2469 #[test]
2470 fn matches_inverted_range_matches_nothing() {
2471 let inverted = MinMaxRange::new(Some(10.0), Some(5.0));
2472 for v in [-1.0_f32, 0.0, 5.0, 7.5, 10.0, 1e30] {
2473 assert!(!inverted.matches(v), "inverted range must reject {v}");
2474 }
2475 }
2476
2477 #[test]
2482 fn os_condition_from_system_platform_covers_every_platform() {
2483 use crate::system::{DesktopEnvironment, Platform};
2484 assert_eq!(
2485 OsCondition::from_system_platform(&Platform::Windows),
2486 OsCondition::Windows
2487 );
2488 assert_eq!(
2489 OsCondition::from_system_platform(&Platform::MacOs),
2490 OsCondition::MacOS
2491 );
2492 assert_eq!(
2493 OsCondition::from_system_platform(&Platform::Ios),
2494 OsCondition::IOS
2495 );
2496 assert_eq!(
2497 OsCondition::from_system_platform(&Platform::Android),
2498 OsCondition::Android
2499 );
2500 assert_eq!(
2501 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Gnome)),
2502 OsCondition::Linux
2503 );
2504 assert_eq!(
2505 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Other(
2506 AzString::from_const_str(""),
2507 ))),
2508 OsCondition::Linux
2509 );
2510 assert_eq!(
2512 OsCondition::from_system_platform(&Platform::Unknown),
2513 OsCondition::Any
2514 );
2515 }
2516
2517 #[test]
2522 fn os_version_new_stores_fields_verbatim_at_boundaries() {
2523 for id in [0, 1, u32::MAX - 1, u32::MAX] {
2524 let v = OsVersion::new(OsFamily::Linux, id);
2525 assert_eq!(v.os, OsFamily::Linux);
2526 assert_eq!(v.version_id, id);
2527 }
2528 }
2529
2530 #[test]
2531 fn os_version_unknown_is_the_default_and_has_id_zero() {
2532 let u = OsVersion::unknown();
2533 assert_eq!(u.version_id, 0);
2534 assert_eq!(u, OsVersion::default());
2535 }
2536
2537 #[test]
2538 fn os_version_compare_is_none_across_families() {
2539 let win = OsVersion::WIN_11;
2540 let mac = OsVersion::MACOS_SONOMA;
2541 assert_eq!(win.compare(&mac), None);
2542 assert_eq!(mac.compare(&win), None);
2543 assert!(!win.is_at_least(&mac));
2546 assert!(!win.is_at_most(&mac));
2547 assert!(!win.is_exactly(&mac));
2548 }
2549
2550 #[test]
2551 fn os_version_compare_within_family_orders_by_id() {
2552 assert_eq!(
2553 OsVersion::WIN_10.compare(&OsVersion::WIN_11),
2554 Some(Ordering::Less)
2555 );
2556 assert_eq!(
2557 OsVersion::WIN_11.compare(&OsVersion::WIN_10),
2558 Some(Ordering::Greater)
2559 );
2560 assert_eq!(
2561 OsVersion::WIN_11.compare(&OsVersion::WIN_11_21H2),
2562 Some(Ordering::Equal)
2563 );
2564 }
2565
2566 #[test]
2567 fn os_version_compare_at_id_extremes() {
2568 let lo = OsVersion::new(OsFamily::Android, 0);
2569 let hi = OsVersion::new(OsFamily::Android, u32::MAX);
2570 assert_eq!(lo.compare(&hi), Some(Ordering::Less));
2571 assert_eq!(hi.compare(&lo), Some(Ordering::Greater));
2572 assert!(hi.is_at_least(&lo));
2573 assert!(!hi.is_at_most(&lo));
2574 assert!(lo.is_at_most(&hi));
2575 }
2576
2577 #[test]
2578 fn os_version_predicates_are_reflexive_and_consistent() {
2579 for v in [
2580 OsVersion::unknown(),
2581 OsVersion::WIN_XP,
2582 OsVersion::MACOS_TAHOE,
2583 OsVersion::IOS_18,
2584 OsVersion::ANDROID_15,
2585 OsVersion::new(OsFamily::Linux, u32::MAX),
2586 ] {
2587 assert!(v.is_at_least(&v), "{v:?} >= itself");
2588 assert!(v.is_at_most(&v), "{v:?} <= itself");
2589 assert!(v.is_exactly(&v), "{v:?} == itself");
2590 }
2591 }
2592
2593 #[test]
2594 fn os_version_at_least_is_the_strict_complement_of_less_than() {
2595 let a = OsVersion::WIN_10;
2596 let b = OsVersion::WIN_11;
2597 assert!(!a.is_at_least(&b));
2598 assert!(a.is_at_most(&b));
2599 assert!(!a.is_exactly(&b));
2600 assert!(b.is_at_least(&a));
2601 assert!(!b.is_at_most(&a));
2602 }
2603
2604 #[test]
2605 fn os_version_unknown_never_satisfies_a_min_constraint_on_a_real_version() {
2606 assert!(!OsVersion::unknown().is_at_least(&OsVersion::WIN_11));
2609 assert!(!OsVersion::unknown().is_at_least(&OsVersion::LINUX_6_0));
2610 }
2611
2612 #[test]
2617 fn parse_os_version_valid_minimal_positive_controls() {
2618 assert_eq!(
2619 parse_os_version(OsFamily::Windows, "11"),
2620 Some(OsVersion::WIN_11)
2621 );
2622 assert_eq!(
2623 parse_os_version(OsFamily::MacOS, "sonoma"),
2624 Some(OsVersion::MACOS_SONOMA)
2625 );
2626 assert_eq!(
2627 parse_os_version(OsFamily::IOS, "17.0"),
2628 Some(OsVersion::IOS_17)
2629 );
2630 assert_eq!(
2631 parse_os_version(OsFamily::Android, "tiramisu"),
2632 Some(OsVersion::ANDROID_13)
2633 );
2634 assert_eq!(
2635 parse_os_version(OsFamily::Linux, "6.0"),
2636 Some(OsVersion::LINUX_6_0)
2637 );
2638 }
2639
2640 #[test]
2641 fn parse_os_version_trims_and_lowercases() {
2642 assert_eq!(
2643 parse_os_version(OsFamily::Windows, " WIN-11 "),
2644 Some(OsVersion::WIN_11)
2645 );
2646 assert_eq!(
2647 parse_os_version(OsFamily::MacOS, "\tBIG-SUR\n"),
2648 Some(OsVersion::MACOS_BIG_SUR)
2649 );
2650 assert_eq!(
2651 parse_os_version(OsFamily::Android, " KitKat "),
2652 Some(OsVersion::ANDROID_KITKAT)
2653 );
2654 }
2655
2656 #[test]
2657 fn parse_os_version_empty_and_whitespace_is_none_for_every_family() {
2658 for os in [
2659 OsFamily::Windows,
2660 OsFamily::MacOS,
2661 OsFamily::IOS,
2662 OsFamily::Android,
2663 OsFamily::Linux,
2664 ] {
2665 assert_eq!(parse_os_version(os, ""), None, "{os:?} empty");
2666 assert_eq!(parse_os_version(os, " "), None, "{os:?} spaces");
2667 assert_eq!(parse_os_version(os, "\t\n\r"), None, "{os:?} ws");
2668 }
2669 }
2670
2671 #[test]
2672 fn parse_os_version_garbage_and_unicode_never_panics() {
2673 for os in [
2674 OsFamily::Windows,
2675 OsFamily::MacOS,
2676 OsFamily::IOS,
2677 OsFamily::Android,
2678 OsFamily::Linux,
2679 ] {
2680 for s in nasty_strings() {
2681 let a = parse_os_version(os, &s);
2683 let b = parse_os_version(os, &s);
2684 assert_eq!(a, b, "{os:?} not deterministic for {s:?}");
2685 }
2686 }
2687 }
2688
2689 #[test]
2690 fn parse_os_version_leading_trailing_junk_is_rejected() {
2691 assert_eq!(parse_os_version(OsFamily::Windows, "win-11;drop"), None);
2692 assert_eq!(parse_os_version(OsFamily::MacOS, "sonoma!"), None);
2693 assert_eq!(parse_os_version(OsFamily::IOS, "17.0.0.0"), None);
2694 assert_eq!(parse_os_version(OsFamily::Android, "api"), None);
2695 assert_eq!(parse_os_version(OsFamily::Android, "api abc"), None);
2696 }
2697
2698 #[test]
2699 fn parse_windows_version_prefix_forms_all_collapse_to_the_same_version() {
2700 for s in [
2701 "11",
2702 "win11",
2703 "win-11",
2704 "win_11",
2705 "windows11",
2706 "windows-11",
2707 "windows_11",
2708 ] {
2709 assert_eq!(
2710 parse_windows_version(s),
2711 Some(OsVersion::WIN_11),
2712 "{s} should parse to WIN_11"
2713 );
2714 }
2715 }
2716
2717 #[test]
2718 fn parse_windows_version_bare_prefix_and_separator_only_are_none() {
2719 assert_eq!(parse_windows_version("win"), None);
2720 assert_eq!(parse_windows_version("windows"), None);
2721 assert_eq!(parse_windows_version("win-"), None);
2722 assert_eq!(parse_windows_version("windows_"), None);
2723 assert_eq!(parse_windows_version(""), None);
2724 }
2725
2726 #[test]
2727 fn parse_windows_version_nt_aliases_agree_with_names() {
2728 assert_eq!(parse_windows_version("xp"), parse_windows_version("nt5.1"));
2729 assert_eq!(parse_windows_version("vista"), parse_windows_version("6.0"));
2730 assert_eq!(parse_windows_version("8.1"), parse_windows_version("8-1"));
2731 assert_eq!(parse_windows_version("10"), Some(OsVersion::WIN_10));
2732 }
2733
2734 #[test]
2735 fn parse_windows_version_boundary_numbers_are_none() {
2736 for s in ["0", "-0", "-1", "NaN", "inf", "99999999999999999999"] {
2737 assert_eq!(parse_windows_version(s), None, "{s} must not parse");
2738 }
2739 }
2740
2741 #[test]
2742 fn parse_windows_version_huge_and_unicode_input_terminates() {
2743 assert_eq!(parse_windows_version(&"win".repeat(200_000)), None);
2744 assert_eq!(parse_windows_version(&"1".repeat(1_000_000)), None);
2745 assert_eq!(parse_windows_version("\u{1F600}"), None);
2746 assert_eq!(parse_windows_version("win-\u{1F600}"), None);
2747 }
2748
2749 #[test]
2750 fn strip_os_prefix_strips_prefix_and_optional_separator() {
2751 assert_eq!(strip_os_prefix("win-11", &["win"]), "11");
2752 assert_eq!(strip_os_prefix("win_11", &["win"]), "11");
2753 assert_eq!(strip_os_prefix("win11", &["win"]), "11");
2754 assert_eq!(strip_os_prefix("windows-11", &["windows", "win"]), "11");
2756 assert_eq!(strip_os_prefix("windows-11", &["win", "windows"]), "dows-11");
2757 }
2758
2759 #[test]
2760 fn strip_os_prefix_leaves_non_matching_input_untouched() {
2761 assert_eq!(strip_os_prefix("11", &["win"]), "11");
2762 assert_eq!(strip_os_prefix("", &["win"]), "");
2763 assert_eq!(strip_os_prefix("anything", &[]), "anything");
2764 assert_eq!(strip_os_prefix("\u{1F600}win", &["win"]), "\u{1F600}win");
2765 }
2766
2767 #[test]
2768 fn strip_os_prefix_only_strips_one_separator() {
2769 assert_eq!(strip_os_prefix("win--11", &["win"]), "-11");
2770 assert_eq!(strip_os_prefix("win-", &["win"]), "");
2771 assert_eq!(strip_os_prefix("win", &["win"]), "");
2772 }
2773
2774 #[test]
2775 fn strip_os_prefix_with_empty_prefix_is_identity() {
2776 assert_eq!(strip_os_prefix("日本語", &[""]), "日本語");
2779 assert_eq!(strip_os_prefix("-日本語", &[""]), "日本語");
2780 }
2781
2782 #[test]
2783 fn parse_macos_version_names_and_numbers_agree() {
2784 assert_eq!(parse_macos_version("cheetah"), parse_macos_version("10.0"));
2785 assert_eq!(parse_macos_version("big-sur"), parse_macos_version("bigsur"));
2786 assert_eq!(parse_macos_version("bigsur"), parse_macos_version("11.0"));
2787 assert_eq!(parse_macos_version("tahoe"), Some(OsVersion::MACOS_TAHOE));
2788 assert_eq!(
2789 parse_macos_version("snow-leopard"),
2790 parse_macos_version("snowleopard")
2791 );
2792 }
2793
2794 #[test]
2795 fn parse_macos_version_rejects_junk_and_terminates_on_huge_input() {
2796 for s in ["", " ", "sonoma ", "SONOMA", "10.16", "27", "🍎"] {
2797 assert_eq!(parse_macos_version(s), None, "{s:?} must not parse");
2798 }
2799 assert_eq!(parse_macos_version(&"10.".repeat(100_000)), None);
2800 }
2801
2802 #[test]
2803 fn parse_macos_version_is_ordered_monotonically() {
2804 let names = [
2805 "cheetah", "puma", "jaguar", "panther", "tiger", "leopard", "lion", "mojave",
2806 "catalina", "bigsur", "monterey", "ventura", "sonoma", "sequoia", "tahoe",
2807 ];
2808 let ids: Vec<u32> = names
2809 .iter()
2810 .map(|n| {
2811 parse_macos_version(n)
2812 .unwrap_or_else(|| panic!("{n} must parse"))
2813 .version_id
2814 })
2815 .collect();
2816 for w in ids.windows(2) {
2817 assert!(w[0] < w[1], "macOS version ids must increase: {w:?}");
2818 }
2819 }
2820
2821 #[test]
2822 fn parse_ios_version_boundaries() {
2823 assert_eq!(parse_ios_version("1"), Some(OsVersion::IOS_1));
2824 assert_eq!(parse_ios_version("18.0"), Some(OsVersion::IOS_18));
2825 assert_eq!(parse_ios_version("0"), None);
2826 assert_eq!(parse_ios_version("19"), None);
2827 assert_eq!(parse_ios_version(""), None);
2828 assert_eq!(parse_ios_version("-1"), None);
2829 assert_eq!(parse_ios_version("NaN"), None);
2830 assert_eq!(parse_ios_version(&"9".repeat(500_000)), None);
2831 }
2832
2833 #[test]
2834 fn parse_android_version_api_level_escape_hatch() {
2835 assert_eq!(
2836 parse_android_version("api34"),
2837 Some(OsVersion::new(OsFamily::Android, 34))
2838 );
2839 assert_eq!(
2840 parse_android_version("api 34"),
2841 Some(OsVersion::new(OsFamily::Android, 34))
2842 );
2843 assert_eq!(
2844 parse_android_version("api0"),
2845 Some(OsVersion::new(OsFamily::Android, 0))
2846 );
2847 assert_eq!(
2848 parse_android_version(&format!("api{}", u32::MAX)),
2849 Some(OsVersion::new(OsFamily::Android, u32::MAX))
2850 );
2851 }
2852
2853 #[test]
2854 fn parse_android_version_api_level_out_of_range_is_none_not_a_panic() {
2855 assert_eq!(parse_android_version("api4294967296"), None);
2857 assert_eq!(parse_android_version("api-1"), None);
2858 assert_eq!(parse_android_version("api+1"), Some(OsVersion::new(OsFamily::Android, 1)));
2859 assert_eq!(parse_android_version("apiNaN"), None);
2860 assert_eq!(parse_android_version(&format!("api{}", "9".repeat(100_000))), None);
2861 }
2862
2863 #[test]
2864 fn parse_android_version_named_releases() {
2865 assert_eq!(parse_android_version("q"), Some(OsVersion::ANDROID_10));
2866 assert_eq!(parse_android_version("13"), parse_android_version("t"));
2867 assert_eq!(
2868 parse_android_version("13"),
2869 parse_android_version("tiramisu")
2870 );
2871 assert_eq!(parse_android_version("15"), Some(OsVersion::ANDROID_15));
2872 assert_eq!(parse_android_version(""), None);
2873 assert_eq!(parse_android_version("🤖"), None);
2874 }
2875
2876 #[test]
2877 fn parse_linux_version_accepts_bare_major_and_prefixes() {
2878 assert_eq!(
2879 parse_linux_version("5"),
2880 Some(OsVersion::new(OsFamily::Linux, 5000))
2881 );
2882 assert_eq!(parse_linux_version("6.0"), Some(OsVersion::LINUX_6_0));
2883 assert_eq!(parse_linux_version("linux6.0"), Some(OsVersion::LINUX_6_0));
2884 assert_eq!(parse_linux_version("linux-6.0"), Some(OsVersion::LINUX_6_0));
2885 assert_eq!(parse_linux_version("linux_6.0"), Some(OsVersion::LINUX_6_0));
2886 assert_eq!(
2887 parse_linux_version("6.17.0"),
2888 Some(OsVersion::new(OsFamily::Linux, 6170))
2889 );
2890 }
2891
2892 #[test]
2893 fn parse_linux_version_rejects_malformed_and_unicode() {
2894 for s in [
2895 "", " ", ".", "..", "-1", "6.-1", "6.x", "x.6", "NaN", "inf", "🐧", "linux", "linux-",
2896 ] {
2897 assert_eq!(parse_linux_version(s), None, "{s:?} must not parse");
2898 }
2899 }
2900
2901 #[test]
2902 fn parse_linux_version_ignores_everything_past_the_patch_component() {
2903 assert_eq!(
2907 parse_linux_version("6.1.2"),
2908 Some(OsVersion::new(OsFamily::Linux, 6012))
2909 );
2910 assert_eq!(
2911 parse_linux_version("6.1.2.3"),
2912 Some(OsVersion::new(OsFamily::Linux, 6012))
2913 );
2914 assert_eq!(
2915 parse_linux_version("6.0.0.0extra"),
2916 Some(OsVersion::LINUX_6_0),
2917 "a 4th component is never parsed, so trailing junk is accepted"
2918 );
2919 }
2920
2921 #[test]
2926 fn parse_linux_version_huge_major_does_not_overflow() {
2927 assert_eq!(
2928 parse_linux_version("5000000"),
2929 None,
2930 "out-of-range kernel major must be rejected, not overflow u32"
2931 );
2932 }
2933
2934 #[test]
2936 fn parse_linux_version_huge_minor_does_not_overflow() {
2937 assert_eq!(
2938 parse_linux_version("1.999999999"),
2939 None,
2940 "out-of-range kernel minor must be rejected, not overflow u32"
2941 );
2942 }
2943
2944 #[test]
2945 fn parse_linux_version_max_u32_component_is_rejected_by_parse_not_by_math() {
2946 assert_eq!(parse_linux_version("4294967296"), None);
2948 }
2949
2950 #[test]
2955 fn linux_desktop_env_from_system_maps_unknown_des_to_other() {
2956 use crate::system::DesktopEnvironment;
2957 assert_eq!(
2958 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Gnome),
2959 LinuxDesktopEnv::Gnome
2960 );
2961 assert_eq!(
2962 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Kde),
2963 LinuxDesktopEnv::KDE
2964 );
2965 for name in ["xfce", "", "🖥", &"x".repeat(10_000)] {
2967 assert_eq!(
2968 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Other(
2969 AzString::from(name.to_string())
2970 )),
2971 LinuxDesktopEnv::Other
2972 );
2973 }
2974 }
2975
2976 #[test]
2977 fn theme_condition_from_system_theme_is_total() {
2978 use crate::system::Theme;
2979 assert_eq!(
2980 ThemeCondition::from_system_theme(Theme::Light),
2981 ThemeCondition::Light
2982 );
2983 assert_eq!(
2984 ThemeCondition::from_system_theme(Theme::Dark),
2985 ThemeCondition::Dark
2986 );
2987 }
2988
2989 #[test]
2994 fn language_exact_is_case_insensitive_and_strict() {
2995 let cond = LanguageCondition::Exact(AzString::from_const_str("de-DE"));
2996 assert!(cond.matches("de-DE"));
2997 assert!(cond.matches("DE-de"));
2998 assert!(!cond.matches("de"));
2999 assert!(!cond.matches("de-AT"));
3000 assert!(!cond.matches("de-DE-x"));
3001 assert!(!cond.matches(""));
3002 }
3003
3004 #[test]
3005 fn language_prefix_matches_subtags_only_at_a_dash_boundary() {
3006 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
3007 assert!(cond.matches("de"));
3008 assert!(cond.matches("de-DE"));
3009 assert!(cond.matches("DE-at"));
3010 assert!(!cond.matches("den"));
3012 assert!(!cond.matches("deu"));
3013 assert!(!cond.matches("d"));
3014 assert!(!cond.matches(""));
3015 }
3016
3017 #[test]
3018 fn language_empty_prefix_matches_only_the_empty_tag() {
3019 let cond = LanguageCondition::Prefix(AzString::from_const_str(""));
3025 assert!(cond.matches(""));
3026 assert!(!cond.matches("en-US"));
3027 }
3028
3029 #[test]
3030 fn language_prefix_longer_than_input_is_false() {
3031 let cond = LanguageCondition::Prefix(AzString::from_const_str("de-DE-1996"));
3032 assert!(!cond.matches("de"));
3033 assert!(!cond.matches(""));
3034 }
3035
3036 #[test]
3037 fn language_matches_huge_input_terminates() {
3038 let cond = LanguageCondition::Prefix(AzString::from_const_str("en"));
3039 let huge = format!("en-{}", "a".repeat(1_000_000));
3040 assert!(cond.matches(&huge));
3041 let cond_exact = LanguageCondition::Exact(AzString::from_const_str("en"));
3042 assert!(!cond_exact.matches(&huge));
3043 }
3044
3045 #[test]
3051 fn language_prefix_does_not_panic_on_multibyte_language_tag() {
3052 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
3053 assert!(!cond.matches("日本語"));
3055 }
3056
3057 #[test]
3059 fn language_prefix_does_not_panic_on_two_byte_language_tag() {
3060 let cond = LanguageCondition::Prefix(AzString::from_const_str("d"));
3061 assert!(!cond.matches("é"));
3062 }
3063
3064 #[test]
3069 fn context_from_system_style_default_is_coherent() {
3070 let style = crate::system::SystemStyle::default();
3071 let ctx = DynamicSelectorContext::from_system_style(&style);
3072 assert_eq!(ctx.os, OsCondition::Any);
3074 assert_eq!(ctx.desktop_env, OptionLinuxDesktopEnv::None);
3075 assert_eq!(ctx.de_version, 0);
3076 assert_eq!(ctx.theme, ThemeCondition::Light);
3077 assert_eq!(ctx.media_type, MediaType::Screen);
3078 assert_eq!(ctx.viewport_width, DEFAULT_VIEWPORT_WIDTH);
3079 assert_eq!(ctx.viewport_height, DEFAULT_VIEWPORT_HEIGHT);
3080 assert!(ctx.container_width.is_nan());
3083 assert!(ctx.container_height.is_nan());
3084 assert!(ctx.window_focused);
3085 }
3086
3087 #[test]
3088 fn context_from_system_style_linux_carries_the_desktop_env() {
3089 use crate::system::{DesktopEnvironment, Platform};
3090 let mut style = crate::system::SystemStyle::default();
3091 style.platform = Platform::Linux(DesktopEnvironment::Kde);
3092 let ctx = DynamicSelectorContext::from_system_style(&style);
3093 assert_eq!(ctx.os, OsCondition::Linux);
3094 assert_eq!(
3095 ctx.desktop_env,
3096 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE)
3097 );
3098 }
3099
3100 #[test]
3101 fn with_viewport_updates_orientation_and_is_deterministic_at_extremes() {
3102 let base = DynamicSelectorContext::default();
3103 assert_eq!(
3104 base.with_viewport(1920.0, 1080.0).orientation,
3105 OrientationType::Landscape
3106 );
3107 assert_eq!(
3108 base.with_viewport(1080.0, 1920.0).orientation,
3109 OrientationType::Portrait
3110 );
3111 assert_eq!(
3113 base.with_viewport(500.0, 500.0).orientation,
3114 OrientationType::Portrait
3115 );
3116 assert_eq!(
3118 base.with_viewport(f32::NAN, f32::NAN).orientation,
3119 OrientationType::Portrait
3120 );
3121 assert_eq!(
3122 base.with_viewport(f32::INFINITY, f32::NEG_INFINITY).orientation,
3123 OrientationType::Landscape
3124 );
3125 let z = base.with_viewport(0.0, 0.0);
3127 assert_eq!(z.viewport_width, 0.0);
3128 assert_eq!(z.orientation, OrientationType::Portrait);
3129 let neg = base.with_viewport(-100.0, -200.0);
3130 assert_eq!(neg.orientation, OrientationType::Landscape);
3131 }
3132
3133 #[test]
3134 fn with_viewport_does_not_disturb_unrelated_fields() {
3135 let base = DynamicSelectorContext::default();
3136 let updated = base.with_viewport(1.0, 2.0);
3137 assert_eq!(updated.os, base.os);
3138 assert_eq!(updated.theme, base.theme);
3139 assert_eq!(updated.language, base.language);
3140 assert_eq!(updated.pseudo_state, base.pseudo_state);
3141 }
3142
3143 #[test]
3144 fn with_container_stores_name_and_extreme_dimensions() {
3145 let base = DynamicSelectorContext::default();
3146 let named = base.with_container(
3147 f32::MAX,
3148 f32::NEG_INFINITY,
3149 Some(AzString::from_const_str("sidebar")),
3150 );
3151 assert_eq!(named.container_width, f32::MAX);
3152 assert_eq!(named.container_height, f32::NEG_INFINITY);
3153 assert_eq!(
3154 named.container_name.as_ref(),
3155 Some(&AzString::from_const_str("sidebar"))
3156 );
3157
3158 let unnamed = base.with_container(0.0, 0.0, None);
3159 assert_eq!(unnamed.container_name.as_ref(), None);
3160 }
3161
3162 #[test]
3163 fn with_container_nan_dimensions_stay_unmatched() {
3164 let ctx = DynamicSelectorContext::default().with_container(f32::NAN, f32::NAN, None);
3165 let open = DynamicSelector::ContainerWidth(MinMaxRange::new(None, None));
3168 assert!(!open.matches(&ctx));
3169 let open_h = DynamicSelector::ContainerHeight(MinMaxRange::new(None, None));
3170 assert!(!open_h.matches(&ctx));
3171 }
3172
3173 #[test]
3174 fn with_pseudo_state_replaces_the_whole_flag_set() {
3175 let base = DynamicSelectorContext::default();
3176 let hovered = base.with_pseudo_state(PseudoStateFlags {
3177 hover: true,
3178 ..PseudoStateFlags::default()
3179 });
3180 assert!(hovered.pseudo_state.hover);
3181 assert!(!hovered.pseudo_state.active);
3182 let active = hovered.with_pseudo_state(PseudoStateFlags {
3184 active: true,
3185 ..PseudoStateFlags::default()
3186 });
3187 assert!(!active.pseudo_state.hover);
3188 assert!(active.pseudo_state.active);
3189 }
3190
3191 #[test]
3192 fn viewport_breakpoint_changed_detects_crossings_only() {
3193 let bps = [480.0_f32, 768.0, 1024.0];
3194 let base = DynamicSelectorContext::default();
3195 let small = base.with_viewport(320.0, 480.0);
3196 let medium = base.with_viewport(800.0, 600.0);
3197 let also_medium = base.with_viewport(900.0, 600.0);
3198
3199 assert!(small.viewport_breakpoint_changed(&medium, &bps));
3200 assert!(medium.viewport_breakpoint_changed(&small, &bps));
3201 assert!(!medium.viewport_breakpoint_changed(&also_medium, &bps));
3202 assert!(!small.viewport_breakpoint_changed(&small, &bps));
3203 }
3204
3205 #[test]
3206 fn viewport_breakpoint_changed_is_exactly_on_the_boundary() {
3207 let bps = [800.0_f32];
3208 let base = DynamicSelectorContext::default();
3209 let at = base.with_viewport(800.0, 600.0);
3211 let just_below = base.with_viewport(799.99, 600.0);
3212 assert!(at.viewport_breakpoint_changed(&just_below, &bps));
3213 assert!(!at.viewport_breakpoint_changed(&at, &bps));
3214 }
3215
3216 #[test]
3217 fn viewport_breakpoint_changed_handles_empty_and_degenerate_breakpoints() {
3218 let base = DynamicSelectorContext::default();
3219 let a = base.with_viewport(100.0, 100.0);
3220 let b = base.with_viewport(5000.0, 100.0);
3221 assert!(!a.viewport_breakpoint_changed(&b, &[]));
3222 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NAN]));
3224 assert!(!a.viewport_breakpoint_changed(&b, &[f32::INFINITY]));
3226 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NEG_INFINITY]));
3227 let nan_vp = base.with_viewport(f32::NAN, 100.0);
3229 assert!(nan_vp.viewport_breakpoint_changed(&b, &[800.0]));
3230 }
3231
3232 #[test]
3233 fn viewport_breakpoint_changed_with_many_breakpoints_terminates() {
3234 let bps: Vec<f32> = (0..100_000).map(|i| i as f32).collect();
3235 let base = DynamicSelectorContext::default();
3236 let a = base.with_viewport(0.0, 100.0);
3237 let b = base.with_viewport(99_999.0, 100.0);
3238 assert!(a.viewport_breakpoint_changed(&b, &bps));
3239 }
3240
3241 #[test]
3246 fn match_os_any_matches_everything() {
3247 for actual in [
3248 OsCondition::Any,
3249 OsCondition::Apple,
3250 OsCondition::MacOS,
3251 OsCondition::IOS,
3252 OsCondition::Linux,
3253 OsCondition::Windows,
3254 OsCondition::Android,
3255 OsCondition::Web,
3256 ] {
3257 assert!(DynamicSelector::match_os(OsCondition::Any, actual));
3258 }
3259 }
3260
3261 #[test]
3262 fn match_os_apple_is_the_macos_ios_union() {
3263 assert!(DynamicSelector::match_os(
3264 OsCondition::Apple,
3265 OsCondition::MacOS
3266 ));
3267 assert!(DynamicSelector::match_os(
3268 OsCondition::Apple,
3269 OsCondition::IOS
3270 ));
3271 assert!(!DynamicSelector::match_os(
3272 OsCondition::Apple,
3273 OsCondition::Linux
3274 ));
3275 assert!(!DynamicSelector::match_os(
3277 OsCondition::MacOS,
3278 OsCondition::Apple
3279 ));
3280 }
3281
3282 #[test]
3283 fn match_os_concrete_conditions_require_equality() {
3284 assert!(DynamicSelector::match_os(
3285 OsCondition::Linux,
3286 OsCondition::Linux
3287 ));
3288 assert!(!DynamicSelector::match_os(
3289 OsCondition::Linux,
3290 OsCondition::Windows
3291 ));
3292 assert!(!DynamicSelector::match_os(
3294 OsCondition::Windows,
3295 OsCondition::Any
3296 ));
3297 }
3298
3299 #[test]
3300 fn match_os_version_min_max_exact_at_zero_and_u32_max() {
3301 let zero = OsVersion::new(OsFamily::Linux, 0);
3302 let max = OsVersion::new(OsFamily::Linux, u32::MAX);
3303 let none = OptionLinuxDesktopEnv::None;
3304
3305 assert!(DynamicSelector::match_os_version(
3306 &OsVersionCondition::Min(zero),
3307 max,
3308 none,
3309 0
3310 ));
3311 assert!(!DynamicSelector::match_os_version(
3312 &OsVersionCondition::Min(max),
3313 zero,
3314 none,
3315 0
3316 ));
3317 assert!(DynamicSelector::match_os_version(
3318 &OsVersionCondition::Max(max),
3319 zero,
3320 none,
3321 0
3322 ));
3323 assert!(DynamicSelector::match_os_version(
3324 &OsVersionCondition::Exact(max),
3325 max,
3326 none,
3327 0
3328 ));
3329 assert!(!DynamicSelector::match_os_version(
3330 &OsVersionCondition::Exact(zero),
3331 max,
3332 none,
3333 0
3334 ));
3335 }
3336
3337 #[test]
3338 fn match_os_version_cross_family_never_matches() {
3339 let none = OptionLinuxDesktopEnv::None;
3340 for cond in [
3342 OsVersionCondition::Min(OsVersion::WIN_10),
3343 OsVersionCondition::Max(OsVersion::WIN_10),
3344 OsVersionCondition::Exact(OsVersion::WIN_10),
3345 ] {
3346 assert!(
3347 !DynamicSelector::match_os_version(
3348 &cond,
3349 OsVersion::MACOS_SONOMA,
3350 none,
3351 0
3352 ),
3353 "{cond:?} must not match a macOS runtime"
3354 );
3355 }
3356 }
3357
3358 #[test]
3359 fn match_os_version_desktop_environment_requires_the_env_to_be_present() {
3360 let cond = OsVersionCondition::DesktopEnvironment(LinuxDesktopEnv::Gnome);
3361 assert!(DynamicSelector::match_os_version(
3362 &cond,
3363 OsVersion::unknown(),
3364 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3365 0
3366 ));
3367 assert!(!DynamicSelector::match_os_version(
3368 &cond,
3369 OsVersion::unknown(),
3370 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3371 0
3372 ));
3373 assert!(!DynamicSelector::match_os_version(
3374 &cond,
3375 OsVersion::unknown(),
3376 OptionLinuxDesktopEnv::None,
3377 0
3378 ));
3379 }
3380
3381 #[test]
3382 fn match_os_version_desktop_env_min_max_respect_the_unknown_zero_sentinel() {
3383 let gnome = OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome);
3384 let dev = DesktopEnvVersion {
3385 env: LinuxDesktopEnv::Gnome,
3386 version_id: 40,
3387 };
3388 assert!(!DynamicSelector::match_os_version(
3390 &OsVersionCondition::DesktopEnvMin(dev),
3391 OsVersion::unknown(),
3392 gnome,
3393 0
3394 ));
3395 assert!(!DynamicSelector::match_os_version(
3396 &OsVersionCondition::DesktopEnvMax(dev),
3397 OsVersion::unknown(),
3398 gnome,
3399 0
3400 ));
3401 assert!(DynamicSelector::match_os_version(
3403 &OsVersionCondition::DesktopEnvMin(dev),
3404 OsVersion::unknown(),
3405 gnome,
3406 40
3407 ));
3408 assert!(DynamicSelector::match_os_version(
3409 &OsVersionCondition::DesktopEnvMin(dev),
3410 OsVersion::unknown(),
3411 gnome,
3412 u32::MAX
3413 ));
3414 assert!(!DynamicSelector::match_os_version(
3415 &OsVersionCondition::DesktopEnvMin(dev),
3416 OsVersion::unknown(),
3417 gnome,
3418 39
3419 ));
3420 assert!(DynamicSelector::match_os_version(
3421 &OsVersionCondition::DesktopEnvMax(dev),
3422 OsVersion::unknown(),
3423 gnome,
3424 40
3425 ));
3426 assert!(!DynamicSelector::match_os_version(
3427 &OsVersionCondition::DesktopEnvMax(dev),
3428 OsVersion::unknown(),
3429 gnome,
3430 41
3431 ));
3432 }
3433
3434 #[test]
3435 fn match_os_version_desktop_env_exact_needs_the_matching_env() {
3436 let dev = DesktopEnvVersion {
3437 env: LinuxDesktopEnv::Gnome,
3438 version_id: 45,
3439 };
3440 assert!(DynamicSelector::match_os_version(
3441 &OsVersionCondition::DesktopEnvExact(dev),
3442 OsVersion::unknown(),
3443 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3444 45
3445 ));
3446 assert!(!DynamicSelector::match_os_version(
3447 &OsVersionCondition::DesktopEnvExact(dev),
3448 OsVersion::unknown(),
3449 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3450 45
3451 ));
3452 assert!(!DynamicSelector::match_os_version(
3453 &OsVersionCondition::DesktopEnvExact(dev),
3454 OsVersion::unknown(),
3455 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3456 46
3457 ));
3458 }
3459
3460 #[test]
3466 fn match_os_version_desktop_env_exact_zero_fails_when_de_version_is_unknown() {
3467 let dev = DesktopEnvVersion {
3468 env: LinuxDesktopEnv::Gnome,
3469 version_id: 0,
3470 };
3471 assert!(
3472 !DynamicSelector::match_os_version(
3473 &OsVersionCondition::DesktopEnvExact(dev),
3474 OsVersion::unknown(),
3475 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3476 0
3477 ),
3478 "de_version == 0 is the 'unknown' sentinel; it must not satisfy an exact match"
3479 );
3480 }
3481
3482 #[test]
3483 fn match_theme_system_preferred_is_a_wildcard() {
3484 for actual in [
3485 ThemeCondition::Light,
3486 ThemeCondition::Dark,
3487 ThemeCondition::SystemPreferred,
3488 ThemeCondition::Custom(AzString::from_const_str("solarized")),
3489 ] {
3490 assert!(DynamicSelector::match_theme(
3491 &ThemeCondition::SystemPreferred,
3492 &actual
3493 ));
3494 }
3495 }
3496
3497 #[test]
3498 fn match_theme_custom_compares_the_name() {
3499 let a = ThemeCondition::Custom(AzString::from_const_str("nord"));
3500 let b = ThemeCondition::Custom(AzString::from_const_str("nord"));
3501 let c = ThemeCondition::Custom(AzString::from_const_str("Nord"));
3502 assert!(DynamicSelector::match_theme(&a, &b));
3503 assert!(!DynamicSelector::match_theme(&a, &c));
3505 assert!(!DynamicSelector::match_theme(&a, &ThemeCondition::Dark));
3506 assert!(!DynamicSelector::match_theme(
3508 &ThemeCondition::Dark,
3509 &ThemeCondition::SystemPreferred
3510 ));
3511 }
3512
3513 #[test]
3514 fn match_pseudo_state_reads_through_to_the_context_flags() {
3515 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3516 hover: true,
3517 checked: true,
3518 ..PseudoStateFlags::default()
3519 });
3520 assert!(DynamicSelector::match_pseudo_state(
3521 PseudoStateType::Hover,
3522 &ctx
3523 ));
3524 assert!(DynamicSelector::match_pseudo_state(
3525 PseudoStateType::CheckedTrue,
3526 &ctx
3527 ));
3528 assert!(!DynamicSelector::match_pseudo_state(
3529 PseudoStateType::CheckedFalse,
3530 &ctx
3531 ));
3532 assert!(!DynamicSelector::match_pseudo_state(
3533 PseudoStateType::Active,
3534 &ctx
3535 ));
3536 assert!(DynamicSelector::match_pseudo_state(
3537 PseudoStateType::Normal,
3538 &ctx
3539 ));
3540 }
3541
3542 #[test]
3543 fn match_pseudo_state_agrees_with_has_state_for_every_state() {
3544 let flags = PseudoStateFlags {
3545 hover: true,
3546 focused: true,
3547 visited: true,
3548 drag_over: true,
3549 ..PseudoStateFlags::default()
3550 };
3551 let ctx = DynamicSelectorContext::default().with_pseudo_state(flags);
3552 for state in [
3553 PseudoStateType::Normal,
3554 PseudoStateType::Hover,
3555 PseudoStateType::Active,
3556 PseudoStateType::Focus,
3557 PseudoStateType::Disabled,
3558 PseudoStateType::CheckedTrue,
3559 PseudoStateType::CheckedFalse,
3560 PseudoStateType::FocusWithin,
3561 PseudoStateType::Visited,
3562 PseudoStateType::Backdrop,
3563 PseudoStateType::Dragging,
3564 PseudoStateType::DragOver,
3565 ] {
3566 assert_eq!(
3567 DynamicSelector::match_pseudo_state(state, &ctx),
3568 flags.has_state(state),
3569 "match_pseudo_state and has_state disagree on {state:?}"
3570 );
3571 }
3572 }
3573
3574 #[test]
3575 fn selector_matches_every_variant_against_the_default_context() {
3576 let ctx = DynamicSelectorContext::default();
3578 for sel in all_selector_variants() {
3579 let a = sel.matches(&ctx);
3580 let b = sel.matches(&ctx);
3581 assert_eq!(a, b, "{sel:?} is not deterministic");
3582 }
3583 }
3584
3585 #[test]
3586 fn selector_matches_media_all_is_a_wildcard() {
3587 let ctx = DynamicSelectorContext::default();
3588 assert_eq!(ctx.media_type, MediaType::Screen);
3589 assert!(DynamicSelector::Media(MediaType::All).matches(&ctx));
3590 assert!(DynamicSelector::Media(MediaType::Screen).matches(&ctx));
3591 assert!(!DynamicSelector::Media(MediaType::Print).matches(&ctx));
3592 }
3593
3594 #[test]
3595 fn selector_matches_aspect_ratio_never_divides_by_zero() {
3596 let base = DynamicSelectorContext::default();
3597 let flat = base.with_viewport(800.0, 0.0);
3599 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3600 .matches(&flat));
3601 let nan_h = base.with_viewport(800.0, f32::NAN);
3603 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3604 .matches(&nan_h));
3605 let nan_w = base.with_viewport(f32::NAN, 600.0);
3607 assert!(!DynamicSelector::AspectRatio(MinMaxRange::with_min(0.0)).matches(&nan_w));
3608 }
3609
3610 #[test]
3611 fn selector_matches_container_name_requires_an_exact_name() {
3612 let ctx = DynamicSelectorContext::default().with_container(
3613 100.0,
3614 100.0,
3615 Some(AzString::from_const_str("sidebar")),
3616 );
3617 assert!(
3618 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&ctx)
3619 );
3620 assert!(
3621 !DynamicSelector::ContainerName(AzString::from_const_str("Sidebar")).matches(&ctx)
3622 );
3623 assert!(!DynamicSelector::ContainerName(AzString::from_const_str("main")).matches(&ctx));
3624 let no_ctr = DynamicSelectorContext::default();
3626 assert!(
3627 !DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&no_ctr)
3628 );
3629 }
3630
3631 #[test]
3632 fn selector_matches_bool_conditions_compare_both_polarities() {
3633 let ctx = DynamicSelectorContext::default();
3634 assert_eq!(ctx.prefers_reduced_motion, BoolCondition::False);
3635 assert!(DynamicSelector::PrefersReducedMotion(BoolCondition::False).matches(&ctx));
3637 assert!(!DynamicSelector::PrefersReducedMotion(BoolCondition::True).matches(&ctx));
3638 assert!(DynamicSelector::PrefersHighContrast(BoolCondition::False).matches(&ctx));
3639 assert!(!DynamicSelector::PrefersHighContrast(BoolCondition::True).matches(&ctx));
3640 }
3641
3642 #[test]
3643 fn bool_condition_roundtrips_through_bool() {
3644 for b in [false, true] {
3645 assert_eq!(bool::from(BoolCondition::from(b)), b);
3646 }
3647 assert_eq!(BoolCondition::default(), BoolCondition::False);
3648 assert!(!bool::from(BoolCondition::False));
3649 assert!(bool::from(BoolCondition::True));
3650 }
3651
3652 #[test]
3657 fn simple_property_is_unconditional_and_always_matches() {
3658 let p = CssPropertyWithConditions::simple(paint_prop());
3659 assert!(p.apply_if.as_slice().is_empty());
3660 assert!(!p.is_conditional());
3661 assert!(!p.is_pseudo_state_only());
3662 assert!(p.matches(&DynamicSelectorContext::default()));
3663 }
3664
3665 #[test]
3666 fn with_condition_and_with_single_condition_agree() {
3667 let a = CssPropertyWithConditions::with_condition(
3668 paint_prop(),
3669 DynamicSelector::PseudoState(PseudoStateType::Hover),
3670 );
3671 let b = CssPropertyWithConditions::on_hover(paint_prop());
3672 assert_eq!(a.apply_if.as_slice(), b.apply_if.as_slice());
3673 assert_eq!(a.apply_if.as_slice().len(), 1);
3674 }
3675
3676 #[test]
3677 fn with_conditions_preserves_order_and_length() {
3678 let conds = all_selector_variants();
3679 let p = CssPropertyWithConditions::with_conditions(
3680 paint_prop(),
3681 DynamicSelectorVec::from_vec(conds.clone()),
3682 );
3683 assert_eq!(p.apply_if.as_slice().len(), conds.len());
3684 for (i, c) in conds.iter().enumerate() {
3685 assert_eq!(p.apply_if.as_slice()[i].variant_tag(), c.variant_tag());
3686 }
3687 assert!(p.is_conditional());
3688 }
3689
3690 #[test]
3691 fn with_conditions_empty_vec_is_unconditional() {
3692 let p = CssPropertyWithConditions::with_conditions(
3693 paint_prop(),
3694 DynamicSelectorVec::from_vec(vec![]),
3695 );
3696 assert!(!p.is_conditional());
3697 assert!(p.matches(&DynamicSelectorContext::default()));
3698 }
3699
3700 #[test]
3701 fn pseudo_state_constructors_build_the_right_condition() {
3702 let cases = [
3703 (
3704 CssPropertyWithConditions::on_hover(paint_prop()),
3705 PseudoStateType::Hover,
3706 ),
3707 (
3708 CssPropertyWithConditions::on_active(paint_prop()),
3709 PseudoStateType::Active,
3710 ),
3711 (
3712 CssPropertyWithConditions::on_focus(paint_prop()),
3713 PseudoStateType::Focus,
3714 ),
3715 (
3716 CssPropertyWithConditions::when_disabled(paint_prop()),
3717 PseudoStateType::Disabled,
3718 ),
3719 ];
3720 for (prop, expected) in cases {
3721 assert_eq!(
3722 prop.apply_if.as_slice(),
3723 &[DynamicSelector::PseudoState(expected)]
3724 );
3725 assert!(prop.is_pseudo_state_only());
3726 assert!(prop.is_conditional());
3727 }
3728 }
3729
3730 #[test]
3731 fn os_and_theme_constructors_build_the_right_condition() {
3732 assert_eq!(
3733 CssPropertyWithConditions::on_windows(paint_prop())
3734 .apply_if
3735 .as_slice(),
3736 &[DynamicSelector::Os(OsCondition::Windows)]
3737 );
3738 assert_eq!(
3739 CssPropertyWithConditions::on_macos(paint_prop())
3740 .apply_if
3741 .as_slice(),
3742 &[DynamicSelector::Os(OsCondition::MacOS)]
3743 );
3744 assert_eq!(
3745 CssPropertyWithConditions::on_linux(paint_prop())
3746 .apply_if
3747 .as_slice(),
3748 &[DynamicSelector::Os(OsCondition::Linux)]
3749 );
3750 assert_eq!(
3751 CssPropertyWithConditions::on_os(paint_prop(), OsCondition::Web)
3752 .apply_if
3753 .as_slice(),
3754 &[DynamicSelector::Os(OsCondition::Web)]
3755 );
3756 assert_eq!(
3757 CssPropertyWithConditions::dark_theme(paint_prop())
3758 .apply_if
3759 .as_slice(),
3760 &[DynamicSelector::Theme(ThemeCondition::Dark)]
3761 );
3762 assert_eq!(
3763 CssPropertyWithConditions::light_theme(paint_prop())
3764 .apply_if
3765 .as_slice(),
3766 &[DynamicSelector::Theme(ThemeCondition::Light)]
3767 );
3768 assert!(!CssPropertyWithConditions::on_linux(paint_prop()).is_pseudo_state_only());
3770 assert!(!CssPropertyWithConditions::dark_theme(paint_prop()).is_pseudo_state_only());
3771 }
3772
3773 #[test]
3774 fn matches_requires_all_conditions_to_hold() {
3775 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3776 hover: true,
3777 ..PseudoStateFlags::default()
3778 });
3779 let both = CssPropertyWithConditions::with_conditions(
3781 paint_prop(),
3782 DynamicSelectorVec::from_vec(vec![
3783 DynamicSelector::PseudoState(PseudoStateType::Hover),
3784 DynamicSelector::PseudoState(PseudoStateType::Focus),
3785 ]),
3786 );
3787 assert!(!both.matches(&ctx));
3788
3789 let ok = CssPropertyWithConditions::with_conditions(
3791 paint_prop(),
3792 DynamicSelectorVec::from_vec(vec![
3793 DynamicSelector::PseudoState(PseudoStateType::Hover),
3794 DynamicSelector::PseudoState(PseudoStateType::Normal),
3795 ]),
3796 );
3797 assert!(ok.matches(&ctx));
3798 }
3799
3800 #[test]
3801 fn matches_with_a_large_condition_list_terminates() {
3802 let conds: Vec<DynamicSelector> = (0..50_000)
3803 .map(|_| DynamicSelector::PseudoState(PseudoStateType::Normal))
3804 .collect();
3805 let p = CssPropertyWithConditions::with_conditions(
3806 paint_prop(),
3807 DynamicSelectorVec::from_vec(conds),
3808 );
3809 assert!(p.matches(&DynamicSelectorContext::default()));
3810 }
3811
3812 #[test]
3813 fn is_pseudo_state_only_is_false_for_empty_and_for_mixed_lists() {
3814 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_pseudo_state_only());
3816 let mixed = CssPropertyWithConditions::with_conditions(
3818 paint_prop(),
3819 DynamicSelectorVec::from_vec(vec![
3820 DynamicSelector::PseudoState(PseudoStateType::Hover),
3821 DynamicSelector::Os(OsCondition::Linux),
3822 ]),
3823 );
3824 assert!(!mixed.is_pseudo_state_only());
3825 let all_pseudo = CssPropertyWithConditions::with_conditions(
3827 paint_prop(),
3828 DynamicSelectorVec::from_vec(vec![
3829 DynamicSelector::PseudoState(PseudoStateType::Hover),
3830 DynamicSelector::PseudoState(PseudoStateType::Focus),
3831 ]),
3832 );
3833 assert!(all_pseudo.is_pseudo_state_only());
3834 }
3835
3836 #[test]
3837 fn is_layout_affecting_splits_layout_from_paint() {
3838 assert!(CssPropertyWithConditions::simple(layout_prop()).is_layout_affecting());
3839 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_layout_affecting());
3840 assert!(CssPropertyWithConditions::on_hover(layout_prop()).is_layout_affecting());
3842 assert!(!CssPropertyWithConditions::on_hover(paint_prop()).is_layout_affecting());
3843 }
3844
3845 #[test]
3846 fn css_property_with_conditions_hash_agrees_with_eq() {
3847 let a = CssPropertyWithConditions::on_hover(paint_prop());
3848 let b = CssPropertyWithConditions::on_hover(paint_prop());
3849 assert_eq!(a, b);
3850 assert_eq!(hash_of(&a), hash_of(&b));
3851 assert_eq!(a.cmp(&b), Ordering::Equal);
3852
3853 let c = CssPropertyWithConditions::on_focus(paint_prop());
3856 assert_ne!(a, c);
3857 assert_ne!(a.cmp(&c), Ordering::Equal);
3858 assert_ne!(hash_of(&a), hash_of(&c));
3859 }
3860
3861 #[test]
3862 fn css_property_with_conditions_ord_is_lexicographic_over_conditions() {
3863 let short = CssPropertyWithConditions::on_hover(paint_prop());
3864 let long = CssPropertyWithConditions::with_conditions(
3865 paint_prop(),
3866 DynamicSelectorVec::from_vec(vec![
3867 DynamicSelector::PseudoState(PseudoStateType::Hover),
3868 DynamicSelector::PseudoState(PseudoStateType::Focus),
3869 ]),
3870 );
3871 assert_eq!(short.cmp(&long), Ordering::Less);
3873 assert_eq!(long.cmp(&short), Ordering::Greater);
3874 }
3875
3876 #[cfg(feature = "parser")]
3881 #[test]
3882 fn parse_os_at_rule_bare_and_parenthesized_forms_agree() {
3883 let bare = parse_os_at_rule_content("linux").expect("bare linux");
3884 let paren = parse_os_at_rule_content("(linux)").expect("(linux)");
3885 let quoted = parse_os_at_rule_content("(\"linux\")").expect("quoted");
3886 let spaced = parse_os_at_rule_content(" ( linux ) ").expect("spaced");
3887 assert_eq!(bare, vec![DynamicSelector::Os(OsCondition::Linux)]);
3888 assert_eq!(bare, paren);
3889 assert_eq!(bare, quoted);
3890 assert_eq!(bare, spaced);
3891 }
3892
3893 #[cfg(feature = "parser")]
3894 #[test]
3895 fn parse_os_at_rule_emits_the_family_even_for_any() {
3896 for s in ["(any)", "(all)", "(*)"] {
3899 let conds = parse_os_at_rule_content(s).unwrap_or_else(|| panic!("{s} must parse"));
3900 assert_eq!(conds, vec![DynamicSelector::Os(OsCondition::Any)]);
3901 assert!(conds[0].matches(&DynamicSelectorContext::default()));
3902 }
3903 }
3904
3905 #[cfg(feature = "parser")]
3906 #[test]
3907 fn parse_os_at_rule_desktop_env_forms() {
3908 assert_eq!(
3909 parse_os_at_rule_content("(linux:gnome)"),
3910 Some(vec![
3911 DynamicSelector::Os(OsCondition::Linux),
3912 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3913 LinuxDesktopEnv::Gnome
3914 )),
3915 ])
3916 );
3917 assert_eq!(
3919 parse_os_at_rule_content("(linux:notarealde)"),
3920 Some(vec![
3921 DynamicSelector::Os(OsCondition::Linux),
3922 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3923 LinuxDesktopEnv::Other
3924 )),
3925 ])
3926 );
3927 assert_eq!(
3929 parse_os_at_rule_content("(linux:)"),
3930 Some(vec![DynamicSelector::Os(OsCondition::Linux)])
3931 );
3932 }
3933
3934 #[cfg(feature = "parser")]
3935 #[test]
3936 fn parse_os_at_rule_version_operators() {
3937 assert_eq!(
3938 parse_os_at_rule_content("(windows >= win-11)"),
3939 Some(vec![
3940 DynamicSelector::Os(OsCondition::Windows),
3941 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
3942 ])
3943 );
3944 assert_eq!(
3946 parse_os_at_rule_content("(windows > win-11)"),
3947 parse_os_at_rule_content("(windows >= win-11)")
3948 );
3949 assert_eq!(
3950 parse_os_at_rule_content("(macos <= sonoma)"),
3951 Some(vec![
3952 DynamicSelector::Os(OsCondition::MacOS),
3953 DynamicSelector::OsVersion(OsVersionCondition::Max(OsVersion::MACOS_SONOMA)),
3954 ])
3955 );
3956 assert_eq!(
3957 parse_os_at_rule_content("(ios = 17)"),
3958 Some(vec![
3959 DynamicSelector::Os(OsCondition::IOS),
3960 DynamicSelector::OsVersion(OsVersionCondition::Exact(OsVersion::IOS_17)),
3961 ])
3962 );
3963 }
3964
3965 #[cfg(feature = "parser")]
3966 #[test]
3967 fn parse_os_at_rule_desktop_env_version() {
3968 assert_eq!(
3969 parse_os_at_rule_content("(linux:gnome > 40)"),
3970 Some(vec![
3971 DynamicSelector::Os(OsCondition::Linux),
3972 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvMin(
3973 DesktopEnvVersion {
3974 env: LinuxDesktopEnv::Gnome,
3975 version_id: 40,
3976 }
3977 )),
3978 ])
3979 );
3980 assert_eq!(parse_os_at_rule_content("(linux:gnome > 4294967296)"), None);
3982 assert_eq!(parse_os_at_rule_content("(linux:gnome > -1)"), None);
3983 assert_eq!(parse_os_at_rule_content("(linux:gnome > abc)"), None);
3984 assert_eq!(parse_os_at_rule_content("(linux:gnome > )"), None);
3985 }
3986
3987 #[cfg(feature = "parser")]
3988 #[test]
3989 fn parse_os_at_rule_rejects_versions_on_versionless_families() {
3990 assert_eq!(parse_os_at_rule_content("(apple >= 14)"), None);
3992 assert_eq!(parse_os_at_rule_content("(web >= 1)"), None);
3993 assert_eq!(parse_os_at_rule_content("(any >= 1)"), None);
3994 }
3995
3996 #[cfg(feature = "parser")]
3997 #[test]
3998 fn parse_os_at_rule_empty_and_garbage_is_none() {
3999 for s in [
4000 "",
4001 " ",
4002 "\t\n",
4003 "()",
4004 "( )",
4005 "(\"\")",
4006 "('')",
4007 "(:)",
4008 "(:gnome)",
4009 "(notanos)",
4010 "(linux;drop)",
4011 "\u{1F600}",
4012 "(\u{1F600})",
4013 "(linux linux)",
4014 ] {
4015 assert_eq!(parse_os_at_rule_content(s), None, "{s:?} must not parse");
4016 }
4017 }
4018
4019 #[cfg(feature = "parser")]
4020 #[test]
4021 fn parse_os_at_rule_huge_and_deeply_parenthesized_input_terminates() {
4022 let nested = format!("{}linux{}", "(".repeat(10_000), ")".repeat(10_000));
4024 assert_eq!(parse_os_at_rule_content(&nested), None);
4025 assert_eq!(parse_os_at_rule_content(&"a".repeat(1_000_000)), None);
4026 assert_eq!(
4027 parse_os_at_rule_content(&format!("(linux >= {})", "9".repeat(100_000))),
4028 None
4029 );
4030 }
4031
4032 #[cfg(feature = "parser")]
4033 #[test]
4034 fn split_op_and_version_picks_the_earliest_then_longest_operator() {
4035 let (subject, op) = split_op_and_version("linux >= 6.0");
4036 assert_eq!(subject, "linux ");
4037 let (op, ver) = op.expect("operator found");
4038 assert!(matches!(op, VersionOp::Min));
4039 assert_eq!(ver, "6.0");
4040
4041 let (_, op) = split_op_and_version("a>=1");
4043 assert!(matches!(op.expect("op").0, VersionOp::Min));
4044 let (_, op) = split_op_and_version("a<=1");
4045 assert!(matches!(op.expect("op").0, VersionOp::Max));
4046 let (_, op) = split_op_and_version("a=1");
4047 assert!(matches!(op.expect("op").0, VersionOp::Exact));
4048 }
4049
4050 #[cfg(feature = "parser")]
4051 #[test]
4052 fn split_op_and_version_with_no_operator_returns_the_whole_string() {
4053 let (subject, op) = split_op_and_version("linux");
4054 assert_eq!(subject, "linux");
4055 assert!(op.is_none());
4056
4057 let (subject, op) = split_op_and_version("");
4058 assert_eq!(subject, "");
4059 assert!(op.is_none());
4060 }
4061
4062 #[cfg(feature = "parser")]
4063 #[test]
4064 fn split_op_and_version_operator_only_yields_empty_sides() {
4065 let (subject, op) = split_op_and_version(">=");
4066 assert_eq!(subject, "");
4067 assert_eq!(op.expect("op").1, "");
4068
4069 let (subject, op) = split_op_and_version("=");
4070 assert_eq!(subject, "");
4071 assert_eq!(op.expect("op").1, "");
4072 }
4073
4074 #[cfg(feature = "parser")]
4075 #[test]
4076 fn split_op_and_version_does_not_split_inside_a_multibyte_char() {
4077 let (subject, op) = split_op_and_version("日本語 >= 6.0");
4080 assert_eq!(subject, "日本語 ");
4081 assert_eq!(op.expect("op").1, "6.0");
4082 let (subject, op) = split_op_and_version("🦀🦀🦀");
4084 assert_eq!(subject, "🦀🦀🦀");
4085 assert!(op.is_none());
4086 }
4087
4088 #[cfg(feature = "parser")]
4089 #[test]
4090 fn parse_os_family_token_accepts_aliases_case_insensitively() {
4091 assert_eq!(parse_os_family_token("LINUX"), Some(OsCondition::Linux));
4092 assert_eq!(parse_os_family_token("Win"), Some(OsCondition::Windows));
4093 assert_eq!(parse_os_family_token("windows"), Some(OsCondition::Windows));
4094 assert_eq!(parse_os_family_token("osx"), Some(OsCondition::MacOS));
4095 assert_eq!(parse_os_family_token("mac"), Some(OsCondition::MacOS));
4096 assert_eq!(parse_os_family_token("wasm"), Some(OsCondition::Web));
4097 assert_eq!(parse_os_family_token("*"), Some(OsCondition::Any));
4098 assert_eq!(parse_os_family_token("all"), Some(OsCondition::Any));
4099 }
4100
4101 #[cfg(feature = "parser")]
4102 #[test]
4103 fn parse_os_family_token_rejects_empty_padded_and_junk() {
4104 for s in [
4106 "",
4107 " ",
4108 " linux",
4109 "linux ",
4110 "lin",
4111 "linux2",
4112 "0",
4113 "-1",
4114 "NaN",
4115 "\u{1F600}",
4116 ] {
4117 assert_eq!(parse_os_family_token(s), None, "{s:?} must not parse");
4118 }
4119 assert_eq!(parse_os_family_token(&"linux".repeat(100_000)), None);
4120 }
4121
4122 #[cfg(feature = "parser")]
4123 #[test]
4124 fn parse_de_token_is_total_and_falls_back_to_other() {
4125 assert_eq!(parse_de_token("GNOME"), LinuxDesktopEnv::Gnome);
4126 assert_eq!(parse_de_token("kde"), LinuxDesktopEnv::KDE);
4127 assert_eq!(parse_de_token("XFCE"), LinuxDesktopEnv::XFCE);
4128 assert_eq!(parse_de_token("unity"), LinuxDesktopEnv::Unity);
4129 assert_eq!(parse_de_token("Cinnamon"), LinuxDesktopEnv::Cinnamon);
4130 assert_eq!(parse_de_token("mate"), LinuxDesktopEnv::MATE);
4131 for s in ["", " ", "gnome ", "\u{1F600}", "日本語", "\0"] {
4133 assert_eq!(parse_de_token(s), LinuxDesktopEnv::Other, "{s:?}");
4134 }
4135 assert_eq!(
4136 parse_de_token(&"gnome".repeat(200_000)),
4137 LinuxDesktopEnv::Other
4138 );
4139 }
4140
4141 #[cfg(feature = "parser")]
4146 fn parse_len(style: &str) -> usize {
4147 CssPropertyWithConditionsVec::parse(style)
4148 .into_library_owned_vec()
4149 .len()
4150 }
4151
4152 #[cfg(feature = "parser")]
4153 #[test]
4154 fn parse_valid_minimal_positive_control() {
4155 let props = CssPropertyWithConditionsVec::parse("color: red;").into_library_owned_vec();
4156 assert_eq!(props.len(), 1);
4157 assert!(!props[0].is_conditional());
4158 assert!(matches!(props[0].property, CssProperty::TextColor(_)));
4159 }
4160
4161 #[cfg(feature = "parser")]
4162 #[test]
4163 fn parse_empty_and_whitespace_yields_no_properties() {
4164 for s in ["", " ", "\t\n\r ", ";", ";;;;", " ; ; "] {
4165 assert_eq!(parse_len(s), 0, "{s:?} must yield no properties");
4166 }
4167 }
4168
4169 #[cfg(feature = "parser")]
4170 #[test]
4171 fn parse_garbage_never_panics_and_yields_nothing() {
4172 for s in [
4173 "not css at all",
4174 "color",
4175 "color:",
4176 ":",
4177 "::::",
4178 "{}",
4179 "}{",
4180 "}",
4181 "{",
4182 "{{{",
4183 "}}}",
4184 "}{color: red}",
4185 "color: ;",
4186 "\0: \0;",
4187 "%s%n%s",
4188 "\u{1F600}: \u{1F600};",
4189 "日本語: 赤;",
4190 ] {
4191 let _ = parse_len(s);
4193 }
4194 assert_eq!(parse_len("not css at all"), 0);
4195 assert_eq!(parse_len("color:"), 0);
4196 assert_eq!(parse_len("}{"), 0);
4197 }
4198
4199 #[cfg(feature = "parser")]
4200 #[test]
4201 fn parse_tolerates_a_missing_trailing_semicolon() {
4202 assert_eq!(parse_len("color: red"), 1);
4203 assert_eq!(parse_len("color: red;color: blue"), 2);
4204 }
4205
4206 #[cfg(feature = "parser")]
4207 #[test]
4208 fn parse_pseudo_selector_block_attaches_the_condition() {
4209 let props = CssPropertyWithConditionsVec::parse(":hover { color: red; }")
4210 .into_library_owned_vec();
4211 assert_eq!(props.len(), 1);
4212 assert!(props[0].is_pseudo_state_only());
4213 assert_eq!(
4214 props[0].apply_if.as_slice(),
4215 &[DynamicSelector::PseudoState(PseudoStateType::Hover)]
4216 );
4217 }
4218
4219 #[cfg(feature = "parser")]
4220 #[test]
4221 fn parse_unknown_selector_block_is_dropped_wholesale() {
4222 assert_eq!(parse_len(":nosuchstate { color: red; }"), 0);
4225 assert_eq!(parse_len("@nosuchrule { color: red; }"), 0);
4226 assert_eq!(parse_len("div { color: red; }"), 0);
4227 }
4228
4229 #[cfg(feature = "parser")]
4230 #[test]
4231 fn parse_nesting_accumulates_inherited_conditions() {
4232 let props =
4233 CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
4234 .into_library_owned_vec();
4235 assert_eq!(props.len(), 2);
4236 let font = props
4238 .iter()
4239 .find(|p| matches!(p.property, CssProperty::FontSize(_)))
4240 .expect("font-size present");
4241 assert_eq!(
4242 font.apply_if.as_slice(),
4243 &[DynamicSelector::Os(OsCondition::Linux)]
4244 );
4245 let color = props
4246 .iter()
4247 .find(|p| matches!(p.property, CssProperty::TextColor(_)))
4248 .expect("color present");
4249 assert_eq!(
4250 color.apply_if.as_slice(),
4251 &[
4252 DynamicSelector::Os(OsCondition::Linux),
4253 DynamicSelector::PseudoState(PseudoStateType::Hover),
4254 ]
4255 );
4256 assert!(!color.is_pseudo_state_only());
4257 }
4258
4259 #[cfg(feature = "parser")]
4260 #[test]
4261 fn parse_moderately_deep_nesting_terminates() {
4262 const DEPTH: usize = 50;
4267 let style = format!(
4268 "{}color: red;{}",
4269 ":hover {".repeat(DEPTH),
4270 "}".repeat(DEPTH)
4271 );
4272 let props = CssPropertyWithConditionsVec::parse(&style).into_library_owned_vec();
4273 assert_eq!(props.len(), 1);
4274 assert_eq!(props[0].apply_if.as_slice().len(), DEPTH);
4275 assert!(props[0].is_pseudo_state_only());
4276 }
4277
4278 #[cfg(feature = "parser")]
4279 #[test]
4280 fn parse_unbalanced_braces_do_not_panic() {
4281 for s in [
4283 "color: red; }",
4284 "{ color: red;",
4285 ":hover { color: red;",
4286 ":hover }",
4287 &"{".repeat(1_000),
4288 &"}".repeat(1_000),
4289 ] {
4290 let _ = parse_len(s);
4291 }
4292 }
4293
4294 #[cfg(feature = "parser")]
4295 #[test]
4296 fn parse_very_long_input_terminates() {
4297 let long = "color: red;".repeat(5_000);
4298 assert_eq!(parse_len(&long), 5_000);
4299 assert_eq!(parse_len(&"a".repeat(500_000)), 0);
4301 assert_eq!(parse_len(&format!("color: {};", "z".repeat(500_000))), 0);
4302 }
4303
4304 #[cfg(feature = "parser")]
4305 #[test]
4306 fn parse_selector_to_conditions_covers_every_pseudo_class() {
4307 let cases = [
4308 ("hover", PseudoStateType::Hover),
4309 ("active", PseudoStateType::Active),
4310 ("focus", PseudoStateType::Focus),
4311 ("focus-within", PseudoStateType::FocusWithin),
4312 ("disabled", PseudoStateType::Disabled),
4313 ("checked", PseudoStateType::CheckedTrue),
4314 ("visited", PseudoStateType::Visited),
4315 ("backdrop", PseudoStateType::Backdrop),
4316 ("dragging", PseudoStateType::Dragging),
4317 ("drag-over", PseudoStateType::DragOver),
4318 ];
4319 for (name, expected) in cases {
4320 assert_eq!(
4321 CssPropertyWithConditionsVec::parse_selector_to_conditions(&format!(":{name}")),
4322 Some(vec![DynamicSelector::PseudoState(expected)]),
4323 ":{name} must parse"
4324 );
4325 }
4326 }
4327
4328 #[cfg(feature = "parser")]
4329 #[test]
4330 fn parse_selector_to_conditions_wildcards_are_unconditional() {
4331 assert_eq!(
4332 CssPropertyWithConditionsVec::parse_selector_to_conditions("*"),
4333 Some(vec![])
4334 );
4335 assert_eq!(
4336 CssPropertyWithConditionsVec::parse_selector_to_conditions(""),
4337 Some(vec![])
4338 );
4339 assert_eq!(
4340 CssPropertyWithConditionsVec::parse_selector_to_conditions(" "),
4341 Some(vec![])
4342 );
4343 }
4344
4345 #[cfg(feature = "parser")]
4346 #[test]
4347 fn parse_selector_to_conditions_rejects_unknown_selectors() {
4348 for s in [
4349 ":", ":hoverr", ":HOVER", "div", "#id", ".class", "@", "\u{1F600}", ":\u{1F600}",
4350 ] {
4351 assert_eq!(
4352 CssPropertyWithConditionsVec::parse_selector_to_conditions(s),
4353 None,
4354 "{s:?} must not parse"
4355 );
4356 }
4357 }
4358
4359 #[cfg(feature = "parser")]
4360 #[test]
4361 fn parse_at_rule_theme_lang_and_accessibility() {
4362 assert_eq!(
4363 CssPropertyWithConditionsVec::parse_at_rule("theme dark"),
4364 Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)])
4365 );
4366 assert_eq!(
4367 CssPropertyWithConditionsVec::parse_at_rule("theme light"),
4368 Some(vec![DynamicSelector::Theme(ThemeCondition::Light)])
4369 );
4370 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("theme neon"), None);
4371
4372 assert_eq!(
4373 CssPropertyWithConditionsVec::parse_at_rule("lang(\"de-DE\")"),
4374 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4375 AzString::from_const_str("de-DE")
4376 ))])
4377 );
4378 assert_eq!(
4379 CssPropertyWithConditionsVec::parse_at_rule("lang de"),
4380 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4381 AzString::from_const_str("de")
4382 ))])
4383 );
4384 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang()"), None);
4385 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang(\"\")"), None);
4386
4387 assert_eq!(
4388 CssPropertyWithConditionsVec::parse_at_rule("prefers-reduced-motion"),
4389 Some(vec![DynamicSelector::PrefersReducedMotion(
4390 BoolCondition::True
4391 )])
4392 );
4393 assert_eq!(
4394 CssPropertyWithConditionsVec::parse_at_rule("high-contrast"),
4395 Some(vec![DynamicSelector::PrefersHighContrast(
4396 BoolCondition::True
4397 )])
4398 );
4399 }
4400
4401 #[cfg(feature = "parser")]
4402 #[test]
4403 fn parse_at_rule_container_named_and_sized() {
4404 assert_eq!(
4405 CssPropertyWithConditionsVec::parse_at_rule("container sidebar"),
4406 Some(vec![DynamicSelector::ContainerName(
4407 AzString::from_const_str("sidebar")
4408 )])
4409 );
4410 let conds = CssPropertyWithConditionsVec::parse_at_rule("container (min-width: 400px)")
4411 .expect("sized container must parse");
4412 assert_eq!(conds.len(), 1);
4413 match conds[0] {
4417 DynamicSelector::ContainerWidth(r) => {
4418 assert_eq!(r.min(), Some(400.0));
4419 assert_eq!(r.max(), None);
4420 }
4421 ref other => panic!("expected ContainerWidth, got {other:?}"),
4422 }
4423 let named = CssPropertyWithConditionsVec::parse_at_rule(
4424 "container sidebar (max-height: 200px)",
4425 )
4426 .expect("named + sized container must parse");
4427 assert_eq!(named.len(), 2);
4428 assert!(matches!(named[0], DynamicSelector::ContainerName(_)));
4429 assert!(matches!(named[1], DynamicSelector::ContainerHeight(_)));
4430 }
4431
4432 #[cfg(feature = "parser")]
4433 #[test]
4434 fn parse_at_rule_empty_and_garbage_is_none() {
4435 for s in [
4436 "",
4437 " ",
4438 "os",
4439 "os ",
4440 "os()",
4441 "os(notanos)",
4442 "media",
4443 "media ",
4444 "media (min-width: abc)",
4445 "theme",
4446 "container",
4447 "container ()",
4448 "nosuchrule",
4449 "\u{1F600}",
4450 ] {
4451 assert_eq!(
4452 CssPropertyWithConditionsVec::parse_at_rule(s),
4453 None,
4454 "{s:?} must not parse"
4455 );
4456 }
4457 }
4458
4459 #[cfg(feature = "parser")]
4460 #[test]
4461 fn parse_at_rule_huge_input_terminates() {
4462 assert_eq!(
4463 CssPropertyWithConditionsVec::parse_at_rule(&format!("os {}", "(".repeat(50_000))),
4464 None
4465 );
4466 assert_eq!(
4467 CssPropertyWithConditionsVec::parse_at_rule(&"z".repeat(500_000)),
4468 None
4469 );
4470 let long_lang = "e".repeat(100_000);
4472 assert_eq!(
4473 CssPropertyWithConditionsVec::parse_at_rule(&format!("lang {long_lang}")),
4474 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4475 AzString::from(long_lang)
4476 ))])
4477 );
4478 }
4479
4480 #[cfg(feature = "parser")]
4481 #[test]
4482 fn parse_media_query_media_types_and_dimensions() {
4483 assert_eq!(
4484 CssPropertyWithConditionsVec::parse_media_query("screen"),
4485 Some(vec![DynamicSelector::Media(MediaType::Screen)])
4486 );
4487 assert_eq!(
4488 CssPropertyWithConditionsVec::parse_media_query("print"),
4489 Some(vec![DynamicSelector::Media(MediaType::Print)])
4490 );
4491 assert_eq!(
4492 CssPropertyWithConditionsVec::parse_media_query("all"),
4493 Some(vec![DynamicSelector::Media(MediaType::All)])
4494 );
4495 let w = CssPropertyWithConditionsVec::parse_media_query("(min-width: 800px)")
4496 .expect("min-width must parse");
4497 assert_eq!(w.len(), 1);
4498 match w[0] {
4499 DynamicSelector::ViewportWidth(r) => {
4500 assert_eq!(r.min(), Some(800.0));
4501 assert_eq!(r.max(), None);
4502 }
4503 ref other => panic!("expected ViewportWidth, got {other:?}"),
4504 }
4505 }
4506
4507 #[cfg(feature = "parser")]
4508 #[test]
4509 fn parse_media_query_boundary_pixel_values() {
4510 let zero = CssPropertyWithConditionsVec::parse_media_query("(min-width: 0px)")
4512 .expect("0px must parse");
4513 match zero[0] {
4514 DynamicSelector::ViewportWidth(r) => assert_eq!(r.min(), Some(0.0)),
4515 ref other => panic!("expected ViewportWidth, got {other:?}"),
4516 }
4517 let neg = CssPropertyWithConditionsVec::parse_media_query("(max-height: -1px)")
4518 .expect("-1px must parse");
4519 match neg[0] {
4520 DynamicSelector::ViewportHeight(r) => assert_eq!(r.max(), Some(-1.0)),
4521 ref other => panic!("expected ViewportHeight, got {other:?}"),
4522 }
4523 assert_eq!(
4525 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800)"),
4526 None
4527 );
4528 assert_eq!(
4529 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800em)"),
4530 None
4531 );
4532 }
4533
4534 #[cfg(feature = "parser")]
4535 #[test]
4536 fn parse_media_query_empty_and_garbage_is_none() {
4537 for s in [
4538 "",
4539 " ",
4540 "(",
4541 ")",
4542 "()",
4543 "(:)",
4544 "(min-width)",
4545 "(min-width: )",
4546 "(nosuchfeature: 1px)",
4547 "SCREEN",
4548 "\u{1F600}",
4549 "(🦀: 1px)",
4550 ] {
4551 assert_eq!(
4552 CssPropertyWithConditionsVec::parse_media_query(s),
4553 None,
4554 "{s:?} must not parse"
4555 );
4556 }
4557 assert_eq!(
4558 CssPropertyWithConditionsVec::parse_media_query(&"(".repeat(100_000)),
4559 None
4560 );
4561 }
4562
4563 #[cfg(feature = "parser")]
4569 #[test]
4570 fn parse_media_query_nan_pixel_value_is_rejected() {
4571 assert_eq!(
4572 CssPropertyWithConditionsVec::parse_media_query("(min-width: NaNpx)"),
4573 None,
4574 "a NaN px value collapses into the 'no limit' sentinel and matches everything"
4575 );
4576 }
4577
4578 #[cfg(feature = "parser")]
4579 #[test]
4580 fn parse_media_query_infinite_pixel_value_never_matches() {
4581 let q = CssPropertyWithConditionsVec::parse_media_query("(min-width: infpx)")
4584 .expect("infpx currently parses");
4585 let ctx = DynamicSelectorContext::default().with_viewport(1e30, 1000.0);
4586 assert!(!q[0].matches(&ctx), "an infinite min-width can never be met");
4587 }
4588
4589 #[cfg(feature = "parser")]
4590 #[test]
4591 fn parse_media_feature_inline_known_features() {
4592 assert_eq!(
4593 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "PORTRAIT"),
4594 Some(DynamicSelector::Orientation(OrientationType::Portrait))
4595 );
4596 assert_eq!(
4597 CssPropertyWithConditionsVec::parse_media_feature_inline(
4598 "prefers-color-scheme",
4599 "Dark"
4600 ),
4601 Some(DynamicSelector::Theme(ThemeCondition::Dark))
4602 );
4603 assert_eq!(
4604 CssPropertyWithConditionsVec::parse_media_feature_inline(
4605 "prefers-reduced-motion",
4606 "reduce"
4607 ),
4608 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
4609 );
4610 assert_eq!(
4611 CssPropertyWithConditionsVec::parse_media_feature_inline(
4612 "prefers-reduced-motion",
4613 "no-preference"
4614 ),
4615 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
4616 );
4617 assert_eq!(
4618 CssPropertyWithConditionsVec::parse_media_feature_inline("prefers-contrast", "more"),
4619 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
4620 );
4621 assert_eq!(
4622 CssPropertyWithConditionsVec::parse_media_feature_inline(
4623 "prefers-high-contrast",
4624 "none"
4625 ),
4626 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
4627 );
4628 }
4629
4630 #[cfg(feature = "parser")]
4631 #[test]
4632 fn parse_media_feature_inline_rejects_unknown_keys_and_values() {
4633 assert_eq!(
4634 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", ""),
4635 None
4636 );
4637 assert_eq!(
4638 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "sideways"),
4639 None
4640 );
4641 assert_eq!(
4642 CssPropertyWithConditionsVec::parse_media_feature_inline("", ""),
4643 None
4644 );
4645 assert_eq!(
4646 CssPropertyWithConditionsVec::parse_media_feature_inline("nosuchkey", "dark"),
4647 None
4648 );
4649 assert_eq!(
4650 CssPropertyWithConditionsVec::parse_media_feature_inline(
4651 "prefers-color-scheme",
4652 "\u{1F600}"
4653 ),
4654 None
4655 );
4656 assert_eq!(
4658 CssPropertyWithConditionsVec::parse_media_feature_inline(" orientation", "portrait"),
4659 None
4660 );
4661 assert_eq!(
4662 CssPropertyWithConditionsVec::parse_media_feature_inline(
4663 &"k".repeat(200_000),
4664 &"v".repeat(200_000)
4665 ),
4666 None
4667 );
4668 }
4669
4670 #[cfg(feature = "parser")]
4671 #[test]
4672 fn parse_property_segment_valid_and_invalid() {
4673 let key_map = crate::props::property::CssKeyMap::get();
4674 let ok = CssPropertyWithConditionsVec::parse_property_segment("color: red", &[], &key_map)
4675 .expect("color: red must parse");
4676 assert_eq!(ok.len(), 1);
4677 assert!(!ok[0].is_conditional());
4678
4679 let inherited = vec![DynamicSelector::PseudoState(PseudoStateType::Hover)];
4681 let shorthand = CssPropertyWithConditionsVec::parse_property_segment(
4682 "padding: 10px",
4683 &inherited,
4684 &key_map,
4685 )
4686 .expect("padding shorthand must parse");
4687 assert!(shorthand.len() > 1, "padding must expand to >1 property");
4688 for p in &shorthand {
4689 assert_eq!(p.apply_if.as_slice(), inherited.as_slice());
4690 }
4691
4692 for s in ["", " ", "color", "color:", ": red", "nosuchprop: red", "\u{1F600}"] {
4693 assert!(
4694 CssPropertyWithConditionsVec::parse_property_segment(s, &[], &key_map).is_none(),
4695 "{s:?} must not parse"
4696 );
4697 }
4698 }
4699
4700 #[cfg(feature = "parser")]
4701 #[test]
4702 fn parse_block_segment_requires_balanced_braces() {
4703 let key_map = crate::props::property::CssKeyMap::get();
4704 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4706 "color: red",
4707 &[],
4708 &key_map
4709 )
4710 .is_none());
4711 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4713 ":hover { color: red",
4714 &[],
4715 &key_map
4716 )
4717 .is_none());
4718 assert!(
4720 CssPropertyWithConditionsVec::parse_block_segment("}{", &[], &key_map).is_none()
4721 );
4722 let empty = CssPropertyWithConditionsVec::parse_block_segment(
4724 ":hover {}",
4725 &[],
4726 &key_map,
4727 );
4728 assert!(empty.is_none());
4730 let ok = CssPropertyWithConditionsVec::parse_block_segment(
4732 ":hover { color: red; }",
4733 &[],
4734 &key_map,
4735 )
4736 .expect("valid block");
4737 assert_eq!(ok.len(), 1);
4738 assert!(ok[0].is_pseudo_state_only());
4739 }
4740
4741 #[cfg(feature = "parser")]
4742 #[test]
4743 fn parse_with_conditions_prepends_inherited_conditions() {
4744 let inherited = vec![DynamicSelector::Os(OsCondition::Linux)];
4745 let props =
4746 CssPropertyWithConditionsVec::parse_with_conditions("color: red;", &inherited)
4747 .into_library_owned_vec();
4748 assert_eq!(props.len(), 1);
4749 assert_eq!(props[0].apply_if.as_slice(), inherited.as_slice());
4750
4751 let none = CssPropertyWithConditionsVec::parse_with_conditions(" ", &inherited)
4753 .into_library_owned_vec();
4754 assert!(none.is_empty());
4755 }
4756
4757 #[cfg(feature = "parser")]
4758 #[test]
4759 fn parsed_media_selector_evaluates_against_a_context() {
4760 let props =
4763 CssPropertyWithConditionsVec::parse("@media (min-width: 800px) { color: red; }")
4764 .into_library_owned_vec();
4765 assert_eq!(props.len(), 1);
4766 let base = DynamicSelectorContext::default();
4767 assert!(props[0].matches(&base.with_viewport(1024.0, 768.0)));
4768 assert!(props[0].matches(&base.with_viewport(800.0, 600.0)));
4769 assert!(!props[0].matches(&base.with_viewport(799.0, 600.0)));
4770 }
4771}