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)]
890#[derive(Debug, Clone)]
891pub struct DynamicSelectorContext {
892 pub os: OsCondition,
894 pub os_version: OsVersion,
895 pub desktop_env: OptionLinuxDesktopEnv,
896 pub de_version: u32,
901
902 pub theme: ThemeCondition,
904
905 pub media_type: MediaType,
907 pub viewport_width: f32,
908 pub viewport_height: f32,
909
910 pub container_width: f32,
913 pub container_height: f32,
914 pub container_name: OptionString,
915
916 pub prefers_reduced_motion: BoolCondition,
918 pub prefers_high_contrast: BoolCondition,
919
920 pub orientation: OrientationType,
922
923 pub pseudo_state: PseudoStateFlags,
925
926 pub language: AzString,
928
929 pub window_focused: bool,
932}
933
934impl Default for DynamicSelectorContext {
935 fn default() -> Self {
936 Self {
937 os: OsCondition::Any,
938 os_version: OsVersion::unknown(),
939 desktop_env: OptionLinuxDesktopEnv::None,
940 de_version: 0,
941 theme: ThemeCondition::Light,
942 media_type: MediaType::Screen,
943 viewport_width: DEFAULT_VIEWPORT_WIDTH,
944 viewport_height: DEFAULT_VIEWPORT_HEIGHT,
945 container_width: f32::NAN,
946 container_height: f32::NAN,
947 container_name: OptionString::None,
948 prefers_reduced_motion: BoolCondition::False,
949 prefers_high_contrast: BoolCondition::False,
950 orientation: OrientationType::Landscape,
951 pseudo_state: PseudoStateFlags::default(),
952 language: AzString::from_const_str("en-US"),
953 window_focused: true,
954 }
955 }
956}
957
958impl DynamicSelectorContext {
959 #[must_use] pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
961 let os = OsCondition::from_system_platform(&system_style.platform);
962 let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
963 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
964 } else {
965 OptionLinuxDesktopEnv::None
966 };
967 let theme = ThemeCondition::from_system_theme(system_style.theme);
968
969 Self {
970 os,
971 os_version: system_style.os_version, desktop_env,
973 de_version: 0, theme,
975 media_type: MediaType::Screen,
976 viewport_width: DEFAULT_VIEWPORT_WIDTH, viewport_height: DEFAULT_VIEWPORT_HEIGHT,
978 container_width: f32::NAN,
979 container_height: f32::NAN,
980 container_name: OptionString::None,
981 prefers_reduced_motion: system_style.prefers_reduced_motion,
982 prefers_high_contrast: system_style.prefers_high_contrast,
983 orientation: OrientationType::Landscape,
984 pseudo_state: PseudoStateFlags::default(),
985 language: system_style.language.clone(),
986 window_focused: true,
987 }
988 }
989
990 #[must_use] pub fn with_viewport(&self, width: f32, height: f32) -> Self {
992 let mut ctx = self.clone();
993 ctx.viewport_width = width;
994 ctx.viewport_height = height;
995 ctx.orientation = if width > height {
996 OrientationType::Landscape
997 } else {
998 OrientationType::Portrait
999 };
1000 ctx
1001 }
1002
1003 #[must_use] pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
1005 let mut ctx = self.clone();
1006 ctx.container_width = width;
1007 ctx.container_height = height;
1008 ctx.container_name = name.into();
1009 ctx
1010 }
1011
1012 #[must_use] pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
1014 let mut ctx = self.clone();
1015 ctx.pseudo_state = state;
1016 ctx
1017 }
1018
1019 #[must_use] pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
1021 for bp in breakpoints {
1022 let self_above = self.viewport_width >= *bp;
1023 let other_above = other.viewport_width >= *bp;
1024 if self_above != other_above {
1025 return true;
1026 }
1027 }
1028 false
1029 }
1030}
1031
1032impl DynamicSelector {
1033 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1035 match self {
1036 Self::Os(os) => Self::match_os(*os, ctx.os),
1037 Self::OsVersion(ver) => Self::match_os_version(ver, ctx.os_version, ctx.desktop_env, ctx.de_version),
1038 Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
1039 Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
1040 Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
1041 Self::ContainerWidth(range) => {
1042 !ctx.container_width.is_nan() && range.matches(ctx.container_width)
1043 }
1044 Self::ContainerHeight(range) => {
1045 !ctx.container_height.is_nan() && range.matches(ctx.container_height)
1046 }
1047 Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
1048 Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
1049 Self::AspectRatio(range) => {
1050 let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
1051 range.matches(ratio)
1052 }
1053 Self::Orientation(orient) => *orient == ctx.orientation,
1054 Self::PrefersReducedMotion(pref) => {
1055 bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
1056 }
1057 Self::PrefersHighContrast(pref) => {
1058 bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
1059 }
1060 Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
1061 Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
1062 }
1063 }
1064
1065 fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
1066 match condition {
1067 OsCondition::Any => true,
1068 OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
1069 _ => condition == actual,
1070 }
1071 }
1072
1073 fn match_os_version(
1074 condition: &OsVersionCondition,
1075 actual: OsVersion,
1076 desktop_env: OptionLinuxDesktopEnv,
1077 de_version: u32,
1078 ) -> bool {
1079 let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
1082 match condition {
1083 OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
1084 OsVersionCondition::Min(ver) => actual.is_at_least(ver),
1085 OsVersionCondition::Max(ver) => actual.is_at_most(ver),
1086 OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
1087 OsVersionCondition::DesktopEnvMin(d) =>
1088 de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
1089 OsVersionCondition::DesktopEnvMax(d) =>
1090 de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
1091 OsVersionCondition::DesktopEnvExact(d) =>
1092 de_matches(&d.env) && de_version != 0 && de_version == d.version_id,
1096 }
1097 }
1098
1099 fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
1100 match (condition, actual) {
1101 (ThemeCondition::SystemPreferred, _) => true,
1102 _ => condition == actual,
1103 }
1104 }
1105
1106 const fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
1107 let node_state = &ctx.pseudo_state;
1108 match state {
1109 PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
1111 PseudoStateType::Active => node_state.active,
1112 PseudoStateType::Focus => node_state.focused,
1113 PseudoStateType::Disabled => node_state.disabled,
1114 PseudoStateType::CheckedTrue => node_state.checked,
1115 PseudoStateType::CheckedFalse => !node_state.checked,
1116 PseudoStateType::FocusWithin => node_state.focus_within,
1117 PseudoStateType::Visited => node_state.visited,
1118 PseudoStateType::Backdrop => node_state.backdrop,
1119 PseudoStateType::Dragging => node_state.dragging,
1120 PseudoStateType::DragOver => node_state.drag_over,
1121 }
1122 }
1123}
1124
1125#[cfg(feature = "parser")]
1139#[must_use] pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
1140 let trimmed = content.trim();
1141 let inner = trimmed
1142 .strip_prefix('(').and_then(|s| s.strip_suffix(')'))
1143 .unwrap_or(trimmed)
1144 .trim();
1145 let inner = inner
1146 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1147 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1148 .unwrap_or(inner)
1149 .trim();
1150 if inner.is_empty() {
1151 return None;
1152 }
1153
1154 let (subject, op_and_version) = split_op_and_version(inner);
1156 let subject = subject.trim();
1157
1158 let (family_str, de_str) = match subject.split_once(':') {
1160 Some((f, d)) => (f.trim(), Some(d.trim())),
1161 None => (subject, None),
1162 };
1163
1164 let family = parse_os_family_token(family_str)?;
1165 let de = match de_str {
1166 Some(s) if !s.is_empty() => Some(parse_de_token(s)),
1167 _ => None,
1168 };
1169
1170 let mut out = Vec::new();
1171 out.push(DynamicSelector::Os(family));
1175
1176 match (de, op_and_version) {
1177 (Some(env), None) => {
1179 out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
1180 }
1181 (Some(env), Some((op, ver_str))) => {
1183 let v: u32 = ver_str.parse().ok()?;
1184 let dev = DesktopEnvVersion { env, version_id: v };
1185 let cond = match op {
1186 VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
1187 VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
1188 VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
1189 };
1190 out.push(DynamicSelector::OsVersion(cond));
1191 }
1192 (None, Some((op, ver_str))) => {
1194 let os_family = match family {
1195 OsCondition::Linux => OsFamily::Linux,
1196 OsCondition::Windows => OsFamily::Windows,
1197 OsCondition::MacOS => OsFamily::MacOS,
1198 OsCondition::IOS => OsFamily::IOS,
1199 OsCondition::Android => OsFamily::Android,
1200 _ => return None,
1202 };
1203 let version = parse_os_version(os_family, ver_str)?;
1204 let cond = match op {
1205 VersionOp::Min => OsVersionCondition::Min(version),
1206 VersionOp::Max => OsVersionCondition::Max(version),
1207 VersionOp::Exact => OsVersionCondition::Exact(version),
1208 };
1209 out.push(DynamicSelector::OsVersion(cond));
1210 }
1211 (None, None) => {}
1213 }
1214
1215 Some(out)
1216}
1217
1218#[cfg(feature = "parser")]
1219#[derive(Copy, Clone)]
1220enum VersionOp { Min, Max, Exact }
1221
1222#[cfg(feature = "parser")]
1225fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
1226 let candidates: &[(&str, VersionOp)] = &[
1228 (">=", VersionOp::Min),
1229 ("<=", VersionOp::Max),
1230 ("=", VersionOp::Exact),
1231 (">", VersionOp::Min),
1232 ("<", VersionOp::Max),
1233 ];
1234 let mut best: Option<(usize, usize, VersionOp)> = None;
1235 for (op_str, op) in candidates {
1236 if let Some(pos) = s.find(op_str) {
1237 let len = op_str.len();
1238 best = Some(match best {
1239 None => (pos, len, *op),
1240 Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
1241 Some(b) => b,
1242 });
1243 }
1244 }
1245 match best {
1246 Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
1247 None => (s, None),
1248 }
1249}
1250
1251#[cfg(feature = "parser")]
1252fn parse_os_family_token(s: &str) -> Option<OsCondition> {
1253 match s.to_lowercase().as_str() {
1254 "linux" => Some(OsCondition::Linux),
1255 "windows" | "win" => Some(OsCondition::Windows),
1256 "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1257 "ios" => Some(OsCondition::IOS),
1258 "android" => Some(OsCondition::Android),
1259 "apple" => Some(OsCondition::Apple),
1260 "web" | "wasm" => Some(OsCondition::Web),
1261 "any" | "all" | "*" => Some(OsCondition::Any),
1262 _ => None,
1263 }
1264}
1265
1266#[cfg(feature = "parser")]
1267fn parse_de_token(s: &str) -> LinuxDesktopEnv {
1268 match s.to_lowercase().as_str() {
1269 "gnome" => LinuxDesktopEnv::Gnome,
1270 "kde" => LinuxDesktopEnv::KDE,
1271 "xfce" => LinuxDesktopEnv::XFCE,
1272 "unity" => LinuxDesktopEnv::Unity,
1273 "cinnamon" => LinuxDesktopEnv::Cinnamon,
1274 "mate" => LinuxDesktopEnv::MATE,
1275 _ => LinuxDesktopEnv::Other,
1276 }
1277}
1278
1279#[repr(C)]
1289#[derive(Debug, Clone, PartialEq)]
1290pub struct CssPropertyWithConditions {
1291 pub property: CssProperty,
1293 pub apply_if: DynamicSelectorVec,
1296}
1297
1298impl_option!(
1299 CssPropertyWithConditions,
1300 OptionCssPropertyWithConditions,
1301 copy = false,
1302 [Debug, Clone, PartialEq, Eq, PartialOrd]
1303);
1304
1305impl Eq for CssPropertyWithConditions {}
1306
1307impl PartialOrd for CssPropertyWithConditions {
1308 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1309 Some(self.cmp(other))
1310 }
1311}
1312
1313impl Ord for CssPropertyWithConditions {
1314 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1315 self.property
1321 .cmp(&other.property)
1322 .then_with(|| self.apply_if.as_slice().cmp(other.apply_if.as_slice()))
1323 }
1324}
1325
1326impl CssPropertyWithConditions {
1327 #[must_use] pub const fn simple(property: CssProperty) -> Self {
1329 Self {
1330 property,
1331 apply_if: DynamicSelectorVec::from_const_slice(&[]),
1332 }
1333 }
1334
1335 #[must_use] pub const fn with_single_condition(
1337 property: CssProperty,
1338 conditions: &'static [DynamicSelector],
1339 ) -> Self {
1340 Self {
1341 property,
1342 apply_if: DynamicSelectorVec::from_const_slice(conditions),
1343 }
1344 }
1345
1346 #[must_use] pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
1348 Self {
1349 property,
1350 apply_if: DynamicSelectorVec::from_vec(vec![condition]),
1351 }
1352 }
1353
1354 #[must_use] pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
1356 Self {
1357 property,
1358 apply_if: conditions,
1359 }
1360 }
1361
1362 #[must_use] pub const fn on_hover(property: CssProperty) -> Self {
1364 Self::with_single_condition(
1365 property,
1366 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
1367 )
1368 }
1369
1370 #[must_use] pub const fn on_active(property: CssProperty) -> Self {
1372 Self::with_single_condition(
1373 property,
1374 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1375 )
1376 }
1377
1378 #[must_use] pub const fn on_focus(property: CssProperty) -> Self {
1380 Self::with_single_condition(
1381 property,
1382 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1383 )
1384 }
1385
1386 #[must_use] pub const fn when_disabled(property: CssProperty) -> Self {
1388 Self::with_single_condition(
1389 property,
1390 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1391 )
1392 }
1393
1394 #[must_use] pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1396 Self::with_condition(property, DynamicSelector::Os(os))
1397 }
1398
1399 #[must_use] pub const fn dark_theme(property: CssProperty) -> Self {
1401 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1402 }
1403
1404 #[must_use] pub const fn light_theme(property: CssProperty) -> Self {
1406 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1407 }
1408
1409 #[must_use] pub const fn on_windows(property: CssProperty) -> Self {
1411 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1412 }
1413
1414 #[must_use] pub const fn on_macos(property: CssProperty) -> Self {
1416 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1417 }
1418
1419 #[must_use] pub const fn on_linux(property: CssProperty) -> Self {
1421 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1422 }
1423
1424 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1426 if self.apply_if.as_slice().is_empty() {
1428 return true;
1429 }
1430
1431 self.apply_if
1433 .as_slice()
1434 .iter()
1435 .all(|selector| selector.matches(ctx))
1436 }
1437
1438 #[must_use] pub fn is_conditional(&self) -> bool {
1440 !self.apply_if.as_slice().is_empty()
1441 }
1442
1443 #[must_use] pub fn is_pseudo_state_only(&self) -> bool {
1446 let conditions = self.apply_if.as_slice();
1447 !conditions.is_empty()
1448 && conditions
1449 .iter()
1450 .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1451 }
1452
1453 #[must_use] pub const fn is_layout_affecting(&self) -> bool {
1459 self.property.get_type().can_trigger_relayout()
1460 }
1461}
1462
1463impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
1464impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1465impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1466impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1467impl_vec_clone!(
1468 CssPropertyWithConditions,
1469 CssPropertyWithConditionsVec,
1470 CssPropertyWithConditionsVecDestructor
1471);
1472
1473impl Eq for CssPropertyWithConditionsVec {}
1475
1476impl Ord for CssPropertyWithConditionsVec {
1477 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1478 self.as_slice().cmp(other.as_slice())
1481 }
1482}
1483
1484impl core::hash::Hash for CssPropertyWithConditions {
1485 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1486 self.property.hash(state);
1487 self.apply_if.as_slice().hash(state);
1491 }
1492}
1493
1494impl core::hash::Hash for CssPropertyWithConditionsVec {
1495 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1496 self.as_slice().hash(state);
1498 }
1499}
1500
1501impl CssPropertyWithConditionsVec {
1502 #[cfg(feature = "parser")]
1522 #[must_use] pub fn parse(style: &str) -> Self {
1523 Self::parse_with_conditions(style, &[])
1524 }
1525
1526 #[cfg(feature = "parser")]
1528 fn parse_with_conditions(style: &str, inherited_conditions: &[DynamicSelector]) -> Self {
1529 use crate::props::property::{
1530 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1531 CssPropertyType,
1532 };
1533
1534 let mut props = Vec::new();
1535 let key_map = CssKeyMap::get();
1536 let style = style.trim();
1537
1538 if style.is_empty() {
1539 return Self::from_vec(props);
1540 }
1541
1542 let chars = style.chars();
1544 let mut current_segment = String::new();
1545 let mut brace_depth = 0;
1546
1547 for c in chars {
1548 match c {
1549 '{' => {
1550 brace_depth += 1;
1551 current_segment.push(c);
1552 }
1553 '}' => {
1554 brace_depth -= 1;
1555 current_segment.push(c);
1556
1557 if brace_depth == 0 {
1558 let segment = current_segment.trim().to_string();
1560 current_segment.clear();
1561
1562 if let Some(parsed) = Self::parse_block_segment(&segment, inherited_conditions, &key_map) {
1563 props.extend(parsed);
1564 }
1565 }
1566 }
1567 ';' if brace_depth == 0 => {
1568 let segment = current_segment.trim().to_string();
1570 current_segment.clear();
1571
1572 if !segment.is_empty() {
1573 if let Some(parsed) = Self::parse_property_segment(&segment, inherited_conditions, &key_map) {
1574 props.extend(parsed);
1575 }
1576 }
1577 }
1578 _ => {
1579 current_segment.push(c);
1580 }
1581 }
1582 }
1583
1584 let remaining = current_segment.trim();
1586 if !remaining.is_empty() && !remaining.contains('{') {
1587 if let Some(parsed) = Self::parse_property_segment(remaining, inherited_conditions, &key_map) {
1588 props.extend(parsed);
1589 }
1590 }
1591
1592 Self::from_vec(props)
1593 }
1594
1595 #[cfg(feature = "parser")]
1597 fn parse_block_segment(
1598 segment: &str,
1599 inherited_conditions: &[DynamicSelector],
1600 key_map: &crate::props::property::CssKeyMap,
1601 ) -> Option<Vec<CssPropertyWithConditions>> {
1602 let brace_pos = segment.find('{')?;
1604 let selector = segment[..brace_pos].trim();
1605
1606 let content_start = brace_pos + 1;
1608 let content_end = segment.rfind('}')?;
1609 if content_end <= content_start {
1610 return None;
1611 }
1612 let content = &segment[content_start..content_end];
1613
1614 let mut conditions = inherited_conditions.to_vec();
1616
1617 if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1618 conditions.extend(new_conditions);
1619 } else {
1620 return None;
1622 }
1623
1624 let parsed = Self::parse_with_conditions(content, &conditions);
1626 Some(parsed.into_library_owned_vec())
1627 }
1628
1629 #[cfg(feature = "parser")]
1631 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1632 let selector = selector.trim();
1633
1634 if let Some(pseudo) = selector.strip_prefix(':') {
1636 match pseudo {
1637 "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1638 "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
1639 "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1640 "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
1641 "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
1642 "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
1643 "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
1644 "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
1645 "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
1646 "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
1647 _ => return None,
1648 }
1649 }
1650
1651 if let Some(rule_content) = selector.strip_prefix('@') {
1653 return Self::parse_at_rule(rule_content);
1654 }
1655
1656 if selector == "*" {
1658 return Some(vec![]);
1659 }
1660
1661 if selector.is_empty() {
1663 return Some(vec![]);
1664 }
1665
1666 None
1667 }
1668
1669 #[cfg(feature = "parser")]
1673 fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
1674 if let Some(rest) = rule_content
1680 .strip_prefix("os ")
1681 .or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
1682 {
1683 if let Some(conds) = parse_os_at_rule_content(rest) {
1684 return Some(conds);
1685 }
1686 }
1687
1688 if let Some(rest) = rule_content.strip_prefix("media ") {
1690 let media_query = rest.trim();
1691 if let Some(media_conds) = Self::parse_media_query(media_query) {
1692 return Some(media_conds);
1693 }
1694 }
1695
1696 if let Some(rest) = rule_content.strip_prefix("theme ") {
1698 let theme = rest.trim();
1699 match theme {
1700 "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
1701 "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1702 _ => return None,
1703 }
1704 }
1705
1706 let lang_body = rule_content
1708 .strip_prefix("lang(")
1709 .map(|r| r.trim_end_matches(')').trim())
1710 .or_else(|| rule_content.strip_prefix("lang ").map(str::trim));
1711 if let Some(lang_str) = lang_body {
1712 let lang_str = lang_str
1713 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1714 .or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1715 .unwrap_or(lang_str);
1716 if !lang_str.is_empty() {
1717 return Some(vec![DynamicSelector::Language(
1718 LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
1719 )]);
1720 }
1721 }
1722
1723 if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
1725 let container_str = if rule_content.starts_with("container(") {
1726 &rule_content[9..] } else {
1728 rule_content[10..].trim()
1729 };
1730 let mut conds = Vec::new();
1731 let (name_part, query_part) = if container_str.starts_with('(') {
1733 (None, container_str)
1734 } else if let Some(paren_idx) = container_str.find('(') {
1735 let name = container_str[..paren_idx].trim();
1736 if name.is_empty() {
1737 (None, container_str)
1738 } else {
1739 (Some(name), &container_str[paren_idx..])
1740 }
1741 } else {
1742 if !container_str.is_empty() {
1743 return Some(vec![DynamicSelector::ContainerName(
1744 AzString::from(container_str.to_string())
1745 )]);
1746 }
1747 return None;
1748 };
1749 if let Some(name) = name_part {
1750 conds.push(DynamicSelector::ContainerName(
1751 AzString::from(name.to_string())
1752 ));
1753 }
1754 if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1756 if let Some((key, value)) = inner.split_once(':') {
1757 let key = key.trim();
1758 let value = value.trim();
1759 let px_value = value.strip_suffix("px")
1760 .and_then(|v| v.trim().parse::<f32>().ok())
1761 .filter(|px| !px.is_nan()); match key {
1763 "min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
1764 "max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
1765 "min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
1766 "max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
1767 _ => {}
1768 }
1769 }
1770 }
1771 if !conds.is_empty() {
1772 return Some(conds);
1773 }
1774 }
1775
1776 if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
1778 return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
1779 }
1780
1781 if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
1783 return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
1784 }
1785
1786 None
1787 }
1788
1789 #[cfg(feature = "parser")]
1791 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1792 let query = query.trim();
1793
1794 if query.starts_with('(') && query.ends_with(')') {
1796 let inner = &query[1..query.len()-1];
1797 if let Some((key, value)) = inner.split_once(':') {
1798 let key = key.trim();
1799 let value = value.trim();
1800
1801 let px_value = value.strip_suffix("px")
1807 .and_then(|v| v.trim().parse::<f32>().ok())
1808 .filter(|px| !px.is_nan());
1809
1810 match key {
1811 "min-width" => {
1812 if let Some(px) = px_value {
1813 return Some(vec![DynamicSelector::ViewportWidth(
1814 MinMaxRange::with_min(px)
1815 )]);
1816 }
1817 }
1818 "max-width" => {
1819 if let Some(px) = px_value {
1820 return Some(vec![DynamicSelector::ViewportWidth(
1821 MinMaxRange::with_max(px)
1822 )]);
1823 }
1824 }
1825 "min-height" => {
1826 if let Some(px) = px_value {
1827 return Some(vec![DynamicSelector::ViewportHeight(
1828 MinMaxRange::with_min(px)
1829 )]);
1830 }
1831 }
1832 "max-height" => {
1833 if let Some(px) = px_value {
1834 return Some(vec![DynamicSelector::ViewportHeight(
1835 MinMaxRange::with_max(px)
1836 )]);
1837 }
1838 }
1839 other => {
1840 if let Some(sel) = Self::parse_media_feature_inline(other, value) {
1842 return Some(vec![sel]);
1843 }
1844 }
1845 }
1846 }
1847 }
1848
1849 match query {
1851 "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
1852 "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
1853 "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
1854 _ => None,
1855 }
1856 }
1857
1858 #[cfg(feature = "parser")]
1861 fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
1862 match key {
1863 "orientation" => {
1864 if value.eq_ignore_ascii_case("portrait") {
1865 Some(DynamicSelector::Orientation(OrientationType::Portrait))
1866 } else if value.eq_ignore_ascii_case("landscape") {
1867 Some(DynamicSelector::Orientation(OrientationType::Landscape))
1868 } else {
1869 None
1870 }
1871 }
1872 "prefers-color-scheme" => {
1873 if value.eq_ignore_ascii_case("dark") {
1874 Some(DynamicSelector::Theme(ThemeCondition::Dark))
1875 } else if value.eq_ignore_ascii_case("light") {
1876 Some(DynamicSelector::Theme(ThemeCondition::Light))
1877 } else {
1878 None
1879 }
1880 }
1881 "prefers-reduced-motion" => {
1882 if value.eq_ignore_ascii_case("reduce") {
1883 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
1884 } else if value.eq_ignore_ascii_case("no-preference") {
1885 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
1886 } else {
1887 None
1888 }
1889 }
1890 "prefers-contrast" | "prefers-high-contrast" => {
1891 if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
1892 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
1893 } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
1894 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
1895 } else {
1896 None
1897 }
1898 }
1899 _ => None,
1900 }
1901 }
1902
1903 #[cfg(feature = "parser")]
1905 fn parse_property_segment(
1906 segment: &str,
1907 inherited_conditions: &[DynamicSelector],
1908 key_map: &crate::props::property::CssKeyMap,
1909 ) -> Option<Vec<CssPropertyWithConditions>> {
1910 use crate::props::property::{
1911 parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
1912 CssPropertyType,
1913 };
1914
1915 let segment = segment.trim();
1916 if segment.is_empty() {
1917 return None;
1918 }
1919
1920 let (key, value) = segment.split_once(':')?;
1921 let key = key.trim();
1922 let value = value.trim();
1923
1924 let mut props = Vec::new();
1925 let conditions = if inherited_conditions.is_empty() {
1926 DynamicSelectorVec::from_const_slice(&[])
1927 } else {
1928 DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
1929 };
1930
1931 if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
1933 if let Ok(prop) = parse_css_property(prop_type, value) {
1934 props.push(CssPropertyWithConditions {
1935 property: prop,
1936 apply_if: conditions,
1937 });
1938 return Some(props);
1939 }
1940 }
1941
1942 if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
1944 if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
1945 for prop in expanded_props {
1946 props.push(CssPropertyWithConditions {
1947 property: prop,
1948 apply_if: conditions.clone(),
1949 });
1950 }
1951 return Some(props);
1952 }
1953 }
1954
1955 None
1956 }
1957
1958}
1959
1960#[cfg(test)]
1961mod tests {
1962 use super::*;
1963
1964 #[test]
1965 fn test_inline_overflow_parse() {
1966 let style = "overflow: scroll;";
1967 let parsed = CssPropertyWithConditionsVec::parse(style);
1968 let props = parsed.into_library_owned_vec();
1969 assert!(!props.is_empty(), "Expected overflow to parse into at least 1 property");
1970 }
1971
1972 #[test]
1973 fn test_inline_overflow_y_parse() {
1974 let style = "overflow-y: scroll;";
1975 let parsed = CssPropertyWithConditionsVec::parse(style);
1976 let props = parsed.into_library_owned_vec();
1977 assert!(!props.is_empty(), "Expected overflow-y to parse into at least 1 property");
1978 }
1979
1980 #[test]
1981 fn test_inline_combined_style_with_overflow() {
1982 let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
1983 let parsed = CssPropertyWithConditionsVec::parse(style);
1984 let props = parsed.into_library_owned_vec();
1985 assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
1987 }
1988
1989 #[test]
1990 fn test_inline_grid_template_columns_parse() {
1991 use crate::props::layout::grid::GridTrackSizing;
1992 let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
1993 let parsed = CssPropertyWithConditionsVec::parse(style);
1994 let props = parsed.into_library_owned_vec();
1995 let grid_cols = props.iter().find(|p| {
1997 matches!(p.property, CssProperty::GridTemplateColumns(_))
1998 }).expect("Expected GridTemplateColumns property");
1999
2000 if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
2001 let template = value.get_property().expect("Expected Exact value");
2002 let tracks = template.tracks.as_ref();
2003 assert_eq!(tracks.len(), 4, "Expected 4 tracks");
2004 for (i, track) in tracks.iter().enumerate() {
2005 assert!(matches!(track, GridTrackSizing::Fixed(_)),
2006 "Track {i} should be Fixed(160px), got {track:?}");
2007 }
2008 } else {
2009 panic!("Expected CssProperty::GridTemplateColumns");
2010 }
2011 }
2012}
2013
2014#[cfg(test)]
2015#[allow(
2016 clippy::float_cmp,
2017 clippy::too_many_lines,
2018 clippy::cast_precision_loss,
2019 clippy::field_reassign_with_default,
2020 clippy::unreadable_literal
2021)]
2022mod autotest_generated {
2023 use core::cmp::Ordering;
2024 use std::collections::hash_map::DefaultHasher;
2025 use std::hash::{Hash, Hasher};
2026
2027 use super::*;
2028 use crate::props::property::CssPropertyType;
2029
2030 fn hash_of<T: Hash>(t: &T) -> u64 {
2035 let mut h = DefaultHasher::new();
2036 t.hash(&mut h);
2037 h.finish()
2038 }
2039
2040 fn paint_prop() -> CssProperty {
2042 CssProperty::const_none(CssPropertyType::TextColor)
2043 }
2044
2045 fn layout_prop() -> CssProperty {
2047 CssProperty::const_none(CssPropertyType::Width)
2048 }
2049
2050 fn all_selector_variants() -> Vec<DynamicSelector> {
2052 vec![
2053 DynamicSelector::Os(OsCondition::Linux),
2054 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
2055 DynamicSelector::Media(MediaType::Print),
2056 DynamicSelector::ViewportWidth(MinMaxRange::with_min(1.0)),
2057 DynamicSelector::ViewportHeight(MinMaxRange::with_max(2.0)),
2058 DynamicSelector::ContainerWidth(MinMaxRange::new(Some(1.0), Some(2.0))),
2059 DynamicSelector::ContainerHeight(MinMaxRange::new(None, None)),
2060 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")),
2061 DynamicSelector::Theme(ThemeCondition::Dark),
2062 DynamicSelector::AspectRatio(MinMaxRange::with_min(0.5)),
2063 DynamicSelector::Orientation(OrientationType::Portrait),
2064 DynamicSelector::PrefersReducedMotion(BoolCondition::True),
2065 DynamicSelector::PrefersHighContrast(BoolCondition::False),
2066 DynamicSelector::PseudoState(PseudoStateType::Hover),
2067 DynamicSelector::Language(LanguageCondition::Prefix(AzString::from_const_str("de"))),
2068 ]
2069 }
2070
2071 fn nasty_strings() -> Vec<String> {
2073 vec![
2074 String::new(),
2075 " ".to_string(),
2076 " \t\n\r ".to_string(),
2077 "\0".to_string(),
2078 "0".to_string(),
2079 "-0".to_string(),
2080 "-1".to_string(),
2081 "NaN".to_string(),
2082 "nan".to_string(),
2083 "inf".to_string(),
2084 "-inf".to_string(),
2085 "infinity".to_string(),
2086 "1e400".to_string(),
2087 i64::MAX.to_string(),
2088 i64::MIN.to_string(),
2089 u32::MAX.to_string(),
2090 u64::MAX.to_string(),
2091 "9999999999999999999999999999".to_string(),
2092 "1.7976931348623157e308".to_string(),
2093 "\u{1F600}".to_string(),
2094 "e\u{301}\u{301}\u{301}".to_string(),
2095 "日本語".to_string(),
2096 "\u{202e}gnome".to_string(),
2097 " linux ".to_string(),
2098 "linux;garbage".to_string(),
2099 "linux)".to_string(),
2100 "((((".to_string(),
2101 "))))".to_string(),
2102 ">=".to_string(),
2103 "<=<=<=".to_string(),
2104 ":::::".to_string(),
2105 "-".to_string(),
2106 "_".to_string(),
2107 ".".to_string(),
2108 "..".to_string(),
2109 "...".to_string(),
2110 "1.2.3.4.5.6".to_string(),
2111 "%s%s%n".to_string(),
2112 "\\x00\\xff".to_string(),
2113 "a".repeat(100_000),
2114 "1".repeat(100_000),
2115 ".".repeat(10_000),
2116 "(".repeat(5_000),
2117 "🦀".repeat(10_000),
2118 ]
2119 }
2120
2121 #[test]
2126 fn has_state_default_flags_only_normal_and_checked_false() {
2127 let flags = PseudoStateFlags::default();
2128 assert!(flags.has_state(PseudoStateType::Normal));
2130 assert!(flags.has_state(PseudoStateType::CheckedFalse));
2132 for state in [
2133 PseudoStateType::Hover,
2134 PseudoStateType::Active,
2135 PseudoStateType::Focus,
2136 PseudoStateType::Disabled,
2137 PseudoStateType::CheckedTrue,
2138 PseudoStateType::FocusWithin,
2139 PseudoStateType::Visited,
2140 PseudoStateType::Backdrop,
2141 PseudoStateType::Dragging,
2142 PseudoStateType::DragOver,
2143 ] {
2144 assert!(!flags.has_state(state), "{state:?} must be off by default");
2145 }
2146 }
2147
2148 #[test]
2149 fn has_state_all_flags_set_reports_every_state_except_checked_false() {
2150 let flags = PseudoStateFlags {
2151 hover: true,
2152 active: true,
2153 focused: true,
2154 disabled: true,
2155 checked: true,
2156 focus_within: true,
2157 visited: true,
2158 backdrop: true,
2159 dragging: true,
2160 drag_over: true,
2161 };
2162 assert!(flags.has_state(PseudoStateType::Hover));
2163 assert!(flags.has_state(PseudoStateType::CheckedTrue));
2164 assert!(!flags.has_state(PseudoStateType::CheckedFalse));
2166 assert!(flags.has_state(PseudoStateType::DragOver));
2167 assert!(flags.has_state(PseudoStateType::Normal));
2168 }
2169
2170 #[test]
2171 fn has_state_checked_true_and_false_are_never_both_active() {
2172 for checked in [false, true] {
2173 let flags = PseudoStateFlags {
2174 checked,
2175 ..PseudoStateFlags::default()
2176 };
2177 assert_ne!(
2178 flags.has_state(PseudoStateType::CheckedTrue),
2179 flags.has_state(PseudoStateType::CheckedFalse),
2180 "checked={checked}: CheckedTrue/CheckedFalse must be complementary"
2181 );
2182 }
2183 }
2184
2185 #[test]
2190 fn variant_tag_matches_declared_repr_discriminants() {
2191 for (expected, sel) in all_selector_variants().iter().enumerate() {
2192 let expected = u8::try_from(expected).expect("15 variants fit in u8");
2193 assert_eq!(
2194 sel.variant_tag(),
2195 expected,
2196 "variant_tag drifted from the #[repr(C, u8)] discriminant for {sel:?}"
2197 );
2198 }
2199 }
2200
2201 #[test]
2202 fn variant_tag_is_unique_per_variant() {
2203 let variants = all_selector_variants();
2204 let mut tags: Vec<u8> = variants.iter().map(DynamicSelector::variant_tag).collect();
2205 tags.sort_unstable();
2206 tags.dedup();
2207 assert_eq!(tags.len(), variants.len(), "variant tags must be unique");
2208 }
2209
2210 #[test]
2211 fn ord_is_keyed_on_variant_tag_first() {
2212 let variants = all_selector_variants();
2213 for w in variants.windows(2) {
2214 assert_eq!(
2215 w[0].cmp(&w[1]),
2216 Ordering::Less,
2217 "selectors must sort by variant tag: {:?} < {:?}",
2218 w[0],
2219 w[1]
2220 );
2221 }
2222 }
2223
2224 #[test]
2225 fn hash_distinguishes_variants_carrying_the_same_payload() {
2226 let range = MinMaxRange::with_min(800.0);
2229 let a = DynamicSelector::ViewportWidth(range);
2230 let b = DynamicSelector::ContainerWidth(range);
2231 assert_ne!(hash_of(&a), hash_of(&b));
2232 assert_ne!(a.cmp(&b), Ordering::Equal);
2233 }
2234
2235 #[test]
2236 fn hash_and_ord_agree_for_nan_carrying_ranges() {
2237 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2240 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2241 assert_eq!(a.cmp(&b), Ordering::Equal);
2242 assert_eq!(hash_of(&a), hash_of(&b));
2243 }
2244
2245 #[test]
2252 fn nan_sentinel_range_selector_is_reflexive_under_partial_eq() {
2253 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2254 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2255 assert_eq!(a, b, "Eq requires reflexivity, but the NaN `max` sentinel breaks it");
2256 }
2257
2258 #[test]
2260 fn ord_equal_implies_partial_eq_for_range_selectors() {
2261 let a = DynamicSelector::ViewportHeight(MinMaxRange::with_max(600.0));
2262 let b = a.clone();
2263 assert_eq!(a.cmp(&b), Ordering::Equal);
2264 assert!(
2265 a == b,
2266 "cmp() == Equal must imply == (Ord/Eq contract); NaN sentinel breaks it"
2267 );
2268 }
2269
2270 #[test]
2275 fn min_max_range_new_roundtrips_finite_values() {
2276 let r = MinMaxRange::new(Some(1.5), Some(9.5));
2277 assert_eq!(r.min(), Some(1.5));
2278 assert_eq!(r.max(), Some(9.5));
2279 }
2280
2281 #[test]
2282 fn min_max_range_new_none_encodes_nan_sentinel() {
2283 let r = MinMaxRange::new(None, None);
2284 assert!(r.min.is_nan());
2285 assert!(r.max.is_nan());
2286 assert_eq!(r.min(), None);
2287 assert_eq!(r.max(), None);
2288 }
2289
2290 #[test]
2291 fn min_max_range_new_nan_argument_is_indistinguishable_from_none() {
2292 let r = MinMaxRange::new(Some(f32::NAN), Some(f32::NAN));
2295 assert_eq!(r.min(), None);
2296 assert_eq!(r.max(), None);
2297 assert!(r.matches(0.0));
2298 assert!(r.matches(f32::MAX));
2299 }
2300
2301 #[test]
2302 fn min_max_range_with_min_and_with_max_leave_the_other_side_open() {
2303 let lo = MinMaxRange::with_min(-0.0);
2304 assert_eq!(lo.min(), Some(-0.0));
2305 assert_eq!(lo.max(), None);
2306
2307 let hi = MinMaxRange::with_max(f32::MAX);
2308 assert_eq!(hi.min(), None);
2309 assert_eq!(hi.max(), Some(f32::MAX));
2310 }
2311
2312 #[test]
2313 fn min_max_range_getters_survive_extreme_values() {
2314 for v in [
2315 0.0_f32,
2316 -0.0,
2317 f32::MIN,
2318 f32::MAX,
2319 f32::MIN_POSITIVE,
2320 f32::EPSILON,
2321 f32::INFINITY,
2322 f32::NEG_INFINITY,
2323 ] {
2324 let r = MinMaxRange::new(Some(v), Some(v));
2325 assert_eq!(r.min(), Some(v));
2326 assert_eq!(r.max(), Some(v));
2327 }
2328 }
2329
2330 #[test]
2335 fn matches_zero_boundary_is_inclusive() {
2336 assert!(MinMaxRange::with_min(0.0).matches(0.0));
2337 assert!(MinMaxRange::with_max(0.0).matches(0.0));
2338 assert!(MinMaxRange::new(Some(0.0), Some(0.0)).matches(0.0));
2339 assert!(MinMaxRange::with_min(0.0).matches(-0.0));
2341 assert!(MinMaxRange::with_max(0.0).matches(-0.0));
2342 }
2343
2344 #[test]
2345 fn matches_negative_values_are_ordered_correctly() {
2346 let r = MinMaxRange::new(Some(-10.0), Some(-1.0));
2347 assert!(r.matches(-10.0));
2348 assert!(r.matches(-5.0));
2349 assert!(r.matches(-1.0));
2350 assert!(!r.matches(-10.001));
2351 assert!(!r.matches(0.0));
2352 }
2353
2354 #[test]
2355 fn matches_at_float_extremes_does_not_panic() {
2356 let open = MinMaxRange::new(None, None);
2357 let bounded = MinMaxRange::new(Some(f32::MIN), Some(f32::MAX));
2358 for v in [
2359 f32::MIN,
2360 f32::MAX,
2361 f32::INFINITY,
2362 f32::NEG_INFINITY,
2363 f32::MIN_POSITIVE,
2364 -f32::MIN_POSITIVE,
2365 ] {
2366 assert!(open.matches(v), "open range must accept {v}");
2368 }
2369 assert!(bounded.matches(0.0));
2370 assert!(bounded.matches(f32::MIN));
2371 assert!(bounded.matches(f32::MAX));
2372 assert!(!bounded.matches(f32::INFINITY));
2374 assert!(!bounded.matches(f32::NEG_INFINITY));
2375 }
2376
2377 #[test]
2378 fn matches_nan_value_is_rejected_by_any_real_bound() {
2379 assert!(!MinMaxRange::with_min(0.0).matches(f32::NAN));
2381 assert!(!MinMaxRange::with_max(0.0).matches(f32::NAN));
2382 assert!(!MinMaxRange::new(Some(1.0), Some(2.0)).matches(f32::NAN));
2383 assert!(MinMaxRange::new(None, None).matches(f32::NAN));
2385 }
2386
2387 #[test]
2388 fn matches_infinite_bounds_are_deterministic() {
2389 let min_inf = MinMaxRange::with_min(f32::INFINITY);
2390 assert!(!min_inf.matches(f32::MAX));
2391 assert!(min_inf.matches(f32::INFINITY));
2392
2393 let max_neg_inf = MinMaxRange::with_max(f32::NEG_INFINITY);
2394 assert!(!max_neg_inf.matches(f32::MIN));
2395 assert!(max_neg_inf.matches(f32::NEG_INFINITY));
2396 }
2397
2398 #[test]
2399 fn matches_inverted_range_matches_nothing() {
2400 let inverted = MinMaxRange::new(Some(10.0), Some(5.0));
2401 for v in [-1.0_f32, 0.0, 5.0, 7.5, 10.0, 1e30] {
2402 assert!(!inverted.matches(v), "inverted range must reject {v}");
2403 }
2404 }
2405
2406 #[test]
2411 fn os_condition_from_system_platform_covers_every_platform() {
2412 use crate::system::{DesktopEnvironment, Platform};
2413 assert_eq!(
2414 OsCondition::from_system_platform(&Platform::Windows),
2415 OsCondition::Windows
2416 );
2417 assert_eq!(
2418 OsCondition::from_system_platform(&Platform::MacOs),
2419 OsCondition::MacOS
2420 );
2421 assert_eq!(
2422 OsCondition::from_system_platform(&Platform::Ios),
2423 OsCondition::IOS
2424 );
2425 assert_eq!(
2426 OsCondition::from_system_platform(&Platform::Android),
2427 OsCondition::Android
2428 );
2429 assert_eq!(
2430 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Gnome)),
2431 OsCondition::Linux
2432 );
2433 assert_eq!(
2434 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Other(
2435 AzString::from_const_str(""),
2436 ))),
2437 OsCondition::Linux
2438 );
2439 assert_eq!(
2441 OsCondition::from_system_platform(&Platform::Unknown),
2442 OsCondition::Any
2443 );
2444 }
2445
2446 #[test]
2451 fn os_version_new_stores_fields_verbatim_at_boundaries() {
2452 for id in [0, 1, u32::MAX - 1, u32::MAX] {
2453 let v = OsVersion::new(OsFamily::Linux, id);
2454 assert_eq!(v.os, OsFamily::Linux);
2455 assert_eq!(v.version_id, id);
2456 }
2457 }
2458
2459 #[test]
2460 fn os_version_unknown_is_the_default_and_has_id_zero() {
2461 let u = OsVersion::unknown();
2462 assert_eq!(u.version_id, 0);
2463 assert_eq!(u, OsVersion::default());
2464 }
2465
2466 #[test]
2467 fn os_version_compare_is_none_across_families() {
2468 let win = OsVersion::WIN_11;
2469 let mac = OsVersion::MACOS_SONOMA;
2470 assert_eq!(win.compare(&mac), None);
2471 assert_eq!(mac.compare(&win), None);
2472 assert!(!win.is_at_least(&mac));
2475 assert!(!win.is_at_most(&mac));
2476 assert!(!win.is_exactly(&mac));
2477 }
2478
2479 #[test]
2480 fn os_version_compare_within_family_orders_by_id() {
2481 assert_eq!(
2482 OsVersion::WIN_10.compare(&OsVersion::WIN_11),
2483 Some(Ordering::Less)
2484 );
2485 assert_eq!(
2486 OsVersion::WIN_11.compare(&OsVersion::WIN_10),
2487 Some(Ordering::Greater)
2488 );
2489 assert_eq!(
2490 OsVersion::WIN_11.compare(&OsVersion::WIN_11_21H2),
2491 Some(Ordering::Equal)
2492 );
2493 }
2494
2495 #[test]
2496 fn os_version_compare_at_id_extremes() {
2497 let lo = OsVersion::new(OsFamily::Android, 0);
2498 let hi = OsVersion::new(OsFamily::Android, u32::MAX);
2499 assert_eq!(lo.compare(&hi), Some(Ordering::Less));
2500 assert_eq!(hi.compare(&lo), Some(Ordering::Greater));
2501 assert!(hi.is_at_least(&lo));
2502 assert!(!hi.is_at_most(&lo));
2503 assert!(lo.is_at_most(&hi));
2504 }
2505
2506 #[test]
2507 fn os_version_predicates_are_reflexive_and_consistent() {
2508 for v in [
2509 OsVersion::unknown(),
2510 OsVersion::WIN_XP,
2511 OsVersion::MACOS_TAHOE,
2512 OsVersion::IOS_18,
2513 OsVersion::ANDROID_15,
2514 OsVersion::new(OsFamily::Linux, u32::MAX),
2515 ] {
2516 assert!(v.is_at_least(&v), "{v:?} >= itself");
2517 assert!(v.is_at_most(&v), "{v:?} <= itself");
2518 assert!(v.is_exactly(&v), "{v:?} == itself");
2519 }
2520 }
2521
2522 #[test]
2523 fn os_version_at_least_is_the_strict_complement_of_less_than() {
2524 let a = OsVersion::WIN_10;
2525 let b = OsVersion::WIN_11;
2526 assert!(!a.is_at_least(&b));
2527 assert!(a.is_at_most(&b));
2528 assert!(!a.is_exactly(&b));
2529 assert!(b.is_at_least(&a));
2530 assert!(!b.is_at_most(&a));
2531 }
2532
2533 #[test]
2534 fn os_version_unknown_never_satisfies_a_min_constraint_on_a_real_version() {
2535 assert!(!OsVersion::unknown().is_at_least(&OsVersion::WIN_11));
2538 assert!(!OsVersion::unknown().is_at_least(&OsVersion::LINUX_6_0));
2539 }
2540
2541 #[test]
2546 fn parse_os_version_valid_minimal_positive_controls() {
2547 assert_eq!(
2548 parse_os_version(OsFamily::Windows, "11"),
2549 Some(OsVersion::WIN_11)
2550 );
2551 assert_eq!(
2552 parse_os_version(OsFamily::MacOS, "sonoma"),
2553 Some(OsVersion::MACOS_SONOMA)
2554 );
2555 assert_eq!(
2556 parse_os_version(OsFamily::IOS, "17.0"),
2557 Some(OsVersion::IOS_17)
2558 );
2559 assert_eq!(
2560 parse_os_version(OsFamily::Android, "tiramisu"),
2561 Some(OsVersion::ANDROID_13)
2562 );
2563 assert_eq!(
2564 parse_os_version(OsFamily::Linux, "6.0"),
2565 Some(OsVersion::LINUX_6_0)
2566 );
2567 }
2568
2569 #[test]
2570 fn parse_os_version_trims_and_lowercases() {
2571 assert_eq!(
2572 parse_os_version(OsFamily::Windows, " WIN-11 "),
2573 Some(OsVersion::WIN_11)
2574 );
2575 assert_eq!(
2576 parse_os_version(OsFamily::MacOS, "\tBIG-SUR\n"),
2577 Some(OsVersion::MACOS_BIG_SUR)
2578 );
2579 assert_eq!(
2580 parse_os_version(OsFamily::Android, " KitKat "),
2581 Some(OsVersion::ANDROID_KITKAT)
2582 );
2583 }
2584
2585 #[test]
2586 fn parse_os_version_empty_and_whitespace_is_none_for_every_family() {
2587 for os in [
2588 OsFamily::Windows,
2589 OsFamily::MacOS,
2590 OsFamily::IOS,
2591 OsFamily::Android,
2592 OsFamily::Linux,
2593 ] {
2594 assert_eq!(parse_os_version(os, ""), None, "{os:?} empty");
2595 assert_eq!(parse_os_version(os, " "), None, "{os:?} spaces");
2596 assert_eq!(parse_os_version(os, "\t\n\r"), None, "{os:?} ws");
2597 }
2598 }
2599
2600 #[test]
2601 fn parse_os_version_garbage_and_unicode_never_panics() {
2602 for os in [
2603 OsFamily::Windows,
2604 OsFamily::MacOS,
2605 OsFamily::IOS,
2606 OsFamily::Android,
2607 OsFamily::Linux,
2608 ] {
2609 for s in nasty_strings() {
2610 let a = parse_os_version(os, &s);
2612 let b = parse_os_version(os, &s);
2613 assert_eq!(a, b, "{os:?} not deterministic for {s:?}");
2614 }
2615 }
2616 }
2617
2618 #[test]
2619 fn parse_os_version_leading_trailing_junk_is_rejected() {
2620 assert_eq!(parse_os_version(OsFamily::Windows, "win-11;drop"), None);
2621 assert_eq!(parse_os_version(OsFamily::MacOS, "sonoma!"), None);
2622 assert_eq!(parse_os_version(OsFamily::IOS, "17.0.0.0"), None);
2623 assert_eq!(parse_os_version(OsFamily::Android, "api"), None);
2624 assert_eq!(parse_os_version(OsFamily::Android, "api abc"), None);
2625 }
2626
2627 #[test]
2628 fn parse_windows_version_prefix_forms_all_collapse_to_the_same_version() {
2629 for s in [
2630 "11",
2631 "win11",
2632 "win-11",
2633 "win_11",
2634 "windows11",
2635 "windows-11",
2636 "windows_11",
2637 ] {
2638 assert_eq!(
2639 parse_windows_version(s),
2640 Some(OsVersion::WIN_11),
2641 "{s} should parse to WIN_11"
2642 );
2643 }
2644 }
2645
2646 #[test]
2647 fn parse_windows_version_bare_prefix_and_separator_only_are_none() {
2648 assert_eq!(parse_windows_version("win"), None);
2649 assert_eq!(parse_windows_version("windows"), None);
2650 assert_eq!(parse_windows_version("win-"), None);
2651 assert_eq!(parse_windows_version("windows_"), None);
2652 assert_eq!(parse_windows_version(""), None);
2653 }
2654
2655 #[test]
2656 fn parse_windows_version_nt_aliases_agree_with_names() {
2657 assert_eq!(parse_windows_version("xp"), parse_windows_version("nt5.1"));
2658 assert_eq!(parse_windows_version("vista"), parse_windows_version("6.0"));
2659 assert_eq!(parse_windows_version("8.1"), parse_windows_version("8-1"));
2660 assert_eq!(parse_windows_version("10"), Some(OsVersion::WIN_10));
2661 }
2662
2663 #[test]
2664 fn parse_windows_version_boundary_numbers_are_none() {
2665 for s in ["0", "-0", "-1", "NaN", "inf", "99999999999999999999"] {
2666 assert_eq!(parse_windows_version(s), None, "{s} must not parse");
2667 }
2668 }
2669
2670 #[test]
2671 fn parse_windows_version_huge_and_unicode_input_terminates() {
2672 assert_eq!(parse_windows_version(&"win".repeat(200_000)), None);
2673 assert_eq!(parse_windows_version(&"1".repeat(1_000_000)), None);
2674 assert_eq!(parse_windows_version("\u{1F600}"), None);
2675 assert_eq!(parse_windows_version("win-\u{1F600}"), None);
2676 }
2677
2678 #[test]
2679 fn strip_os_prefix_strips_prefix_and_optional_separator() {
2680 assert_eq!(strip_os_prefix("win-11", &["win"]), "11");
2681 assert_eq!(strip_os_prefix("win_11", &["win"]), "11");
2682 assert_eq!(strip_os_prefix("win11", &["win"]), "11");
2683 assert_eq!(strip_os_prefix("windows-11", &["windows", "win"]), "11");
2685 assert_eq!(strip_os_prefix("windows-11", &["win", "windows"]), "dows-11");
2686 }
2687
2688 #[test]
2689 fn strip_os_prefix_leaves_non_matching_input_untouched() {
2690 assert_eq!(strip_os_prefix("11", &["win"]), "11");
2691 assert_eq!(strip_os_prefix("", &["win"]), "");
2692 assert_eq!(strip_os_prefix("anything", &[]), "anything");
2693 assert_eq!(strip_os_prefix("\u{1F600}win", &["win"]), "\u{1F600}win");
2694 }
2695
2696 #[test]
2697 fn strip_os_prefix_only_strips_one_separator() {
2698 assert_eq!(strip_os_prefix("win--11", &["win"]), "-11");
2699 assert_eq!(strip_os_prefix("win-", &["win"]), "");
2700 assert_eq!(strip_os_prefix("win", &["win"]), "");
2701 }
2702
2703 #[test]
2704 fn strip_os_prefix_with_empty_prefix_is_identity() {
2705 assert_eq!(strip_os_prefix("日本語", &[""]), "日本語");
2708 assert_eq!(strip_os_prefix("-日本語", &[""]), "日本語");
2709 }
2710
2711 #[test]
2712 fn parse_macos_version_names_and_numbers_agree() {
2713 assert_eq!(parse_macos_version("cheetah"), parse_macos_version("10.0"));
2714 assert_eq!(parse_macos_version("big-sur"), parse_macos_version("bigsur"));
2715 assert_eq!(parse_macos_version("bigsur"), parse_macos_version("11.0"));
2716 assert_eq!(parse_macos_version("tahoe"), Some(OsVersion::MACOS_TAHOE));
2717 assert_eq!(
2718 parse_macos_version("snow-leopard"),
2719 parse_macos_version("snowleopard")
2720 );
2721 }
2722
2723 #[test]
2724 fn parse_macos_version_rejects_junk_and_terminates_on_huge_input() {
2725 for s in ["", " ", "sonoma ", "SONOMA", "10.16", "27", "🍎"] {
2726 assert_eq!(parse_macos_version(s), None, "{s:?} must not parse");
2727 }
2728 assert_eq!(parse_macos_version(&"10.".repeat(100_000)), None);
2729 }
2730
2731 #[test]
2732 fn parse_macos_version_is_ordered_monotonically() {
2733 let names = [
2734 "cheetah", "puma", "jaguar", "panther", "tiger", "leopard", "lion", "mojave",
2735 "catalina", "bigsur", "monterey", "ventura", "sonoma", "sequoia", "tahoe",
2736 ];
2737 let ids: Vec<u32> = names
2738 .iter()
2739 .map(|n| {
2740 parse_macos_version(n)
2741 .unwrap_or_else(|| panic!("{n} must parse"))
2742 .version_id
2743 })
2744 .collect();
2745 for w in ids.windows(2) {
2746 assert!(w[0] < w[1], "macOS version ids must increase: {w:?}");
2747 }
2748 }
2749
2750 #[test]
2751 fn parse_ios_version_boundaries() {
2752 assert_eq!(parse_ios_version("1"), Some(OsVersion::IOS_1));
2753 assert_eq!(parse_ios_version("18.0"), Some(OsVersion::IOS_18));
2754 assert_eq!(parse_ios_version("0"), None);
2755 assert_eq!(parse_ios_version("19"), None);
2756 assert_eq!(parse_ios_version(""), None);
2757 assert_eq!(parse_ios_version("-1"), None);
2758 assert_eq!(parse_ios_version("NaN"), None);
2759 assert_eq!(parse_ios_version(&"9".repeat(500_000)), None);
2760 }
2761
2762 #[test]
2763 fn parse_android_version_api_level_escape_hatch() {
2764 assert_eq!(
2765 parse_android_version("api34"),
2766 Some(OsVersion::new(OsFamily::Android, 34))
2767 );
2768 assert_eq!(
2769 parse_android_version("api 34"),
2770 Some(OsVersion::new(OsFamily::Android, 34))
2771 );
2772 assert_eq!(
2773 parse_android_version("api0"),
2774 Some(OsVersion::new(OsFamily::Android, 0))
2775 );
2776 assert_eq!(
2777 parse_android_version(&format!("api{}", u32::MAX)),
2778 Some(OsVersion::new(OsFamily::Android, u32::MAX))
2779 );
2780 }
2781
2782 #[test]
2783 fn parse_android_version_api_level_out_of_range_is_none_not_a_panic() {
2784 assert_eq!(parse_android_version("api4294967296"), None);
2786 assert_eq!(parse_android_version("api-1"), None);
2787 assert_eq!(parse_android_version("api+1"), Some(OsVersion::new(OsFamily::Android, 1)));
2788 assert_eq!(parse_android_version("apiNaN"), None);
2789 assert_eq!(parse_android_version(&format!("api{}", "9".repeat(100_000))), None);
2790 }
2791
2792 #[test]
2793 fn parse_android_version_named_releases() {
2794 assert_eq!(parse_android_version("q"), Some(OsVersion::ANDROID_10));
2795 assert_eq!(parse_android_version("13"), parse_android_version("t"));
2796 assert_eq!(
2797 parse_android_version("13"),
2798 parse_android_version("tiramisu")
2799 );
2800 assert_eq!(parse_android_version("15"), Some(OsVersion::ANDROID_15));
2801 assert_eq!(parse_android_version(""), None);
2802 assert_eq!(parse_android_version("🤖"), None);
2803 }
2804
2805 #[test]
2806 fn parse_linux_version_accepts_bare_major_and_prefixes() {
2807 assert_eq!(
2808 parse_linux_version("5"),
2809 Some(OsVersion::new(OsFamily::Linux, 5000))
2810 );
2811 assert_eq!(parse_linux_version("6.0"), Some(OsVersion::LINUX_6_0));
2812 assert_eq!(parse_linux_version("linux6.0"), Some(OsVersion::LINUX_6_0));
2813 assert_eq!(parse_linux_version("linux-6.0"), Some(OsVersion::LINUX_6_0));
2814 assert_eq!(parse_linux_version("linux_6.0"), Some(OsVersion::LINUX_6_0));
2815 assert_eq!(
2816 parse_linux_version("6.17.0"),
2817 Some(OsVersion::new(OsFamily::Linux, 6170))
2818 );
2819 }
2820
2821 #[test]
2822 fn parse_linux_version_rejects_malformed_and_unicode() {
2823 for s in [
2824 "", " ", ".", "..", "-1", "6.-1", "6.x", "x.6", "NaN", "inf", "🐧", "linux", "linux-",
2825 ] {
2826 assert_eq!(parse_linux_version(s), None, "{s:?} must not parse");
2827 }
2828 }
2829
2830 #[test]
2831 fn parse_linux_version_ignores_everything_past_the_patch_component() {
2832 assert_eq!(
2836 parse_linux_version("6.1.2"),
2837 Some(OsVersion::new(OsFamily::Linux, 6012))
2838 );
2839 assert_eq!(
2840 parse_linux_version("6.1.2.3"),
2841 Some(OsVersion::new(OsFamily::Linux, 6012))
2842 );
2843 assert_eq!(
2844 parse_linux_version("6.0.0.0extra"),
2845 Some(OsVersion::LINUX_6_0),
2846 "a 4th component is never parsed, so trailing junk is accepted"
2847 );
2848 }
2849
2850 #[test]
2855 fn parse_linux_version_huge_major_does_not_overflow() {
2856 assert_eq!(
2857 parse_linux_version("5000000"),
2858 None,
2859 "out-of-range kernel major must be rejected, not overflow u32"
2860 );
2861 }
2862
2863 #[test]
2865 fn parse_linux_version_huge_minor_does_not_overflow() {
2866 assert_eq!(
2867 parse_linux_version("1.999999999"),
2868 None,
2869 "out-of-range kernel minor must be rejected, not overflow u32"
2870 );
2871 }
2872
2873 #[test]
2874 fn parse_linux_version_max_u32_component_is_rejected_by_parse_not_by_math() {
2875 assert_eq!(parse_linux_version("4294967296"), None);
2877 }
2878
2879 #[test]
2884 fn linux_desktop_env_from_system_maps_unknown_des_to_other() {
2885 use crate::system::DesktopEnvironment;
2886 assert_eq!(
2887 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Gnome),
2888 LinuxDesktopEnv::Gnome
2889 );
2890 assert_eq!(
2891 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Kde),
2892 LinuxDesktopEnv::KDE
2893 );
2894 for name in ["xfce", "", "🖥", &"x".repeat(10_000)] {
2896 assert_eq!(
2897 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Other(
2898 AzString::from(name.to_string())
2899 )),
2900 LinuxDesktopEnv::Other
2901 );
2902 }
2903 }
2904
2905 #[test]
2906 fn theme_condition_from_system_theme_is_total() {
2907 use crate::system::Theme;
2908 assert_eq!(
2909 ThemeCondition::from_system_theme(Theme::Light),
2910 ThemeCondition::Light
2911 );
2912 assert_eq!(
2913 ThemeCondition::from_system_theme(Theme::Dark),
2914 ThemeCondition::Dark
2915 );
2916 }
2917
2918 #[test]
2923 fn language_exact_is_case_insensitive_and_strict() {
2924 let cond = LanguageCondition::Exact(AzString::from_const_str("de-DE"));
2925 assert!(cond.matches("de-DE"));
2926 assert!(cond.matches("DE-de"));
2927 assert!(!cond.matches("de"));
2928 assert!(!cond.matches("de-AT"));
2929 assert!(!cond.matches("de-DE-x"));
2930 assert!(!cond.matches(""));
2931 }
2932
2933 #[test]
2934 fn language_prefix_matches_subtags_only_at_a_dash_boundary() {
2935 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
2936 assert!(cond.matches("de"));
2937 assert!(cond.matches("de-DE"));
2938 assert!(cond.matches("DE-at"));
2939 assert!(!cond.matches("den"));
2941 assert!(!cond.matches("deu"));
2942 assert!(!cond.matches("d"));
2943 assert!(!cond.matches(""));
2944 }
2945
2946 #[test]
2947 fn language_empty_prefix_matches_only_the_empty_tag() {
2948 let cond = LanguageCondition::Prefix(AzString::from_const_str(""));
2954 assert!(cond.matches(""));
2955 assert!(!cond.matches("en-US"));
2956 }
2957
2958 #[test]
2959 fn language_prefix_longer_than_input_is_false() {
2960 let cond = LanguageCondition::Prefix(AzString::from_const_str("de-DE-1996"));
2961 assert!(!cond.matches("de"));
2962 assert!(!cond.matches(""));
2963 }
2964
2965 #[test]
2966 fn language_matches_huge_input_terminates() {
2967 let cond = LanguageCondition::Prefix(AzString::from_const_str("en"));
2968 let huge = format!("en-{}", "a".repeat(1_000_000));
2969 assert!(cond.matches(&huge));
2970 let cond_exact = LanguageCondition::Exact(AzString::from_const_str("en"));
2971 assert!(!cond_exact.matches(&huge));
2972 }
2973
2974 #[test]
2980 fn language_prefix_does_not_panic_on_multibyte_language_tag() {
2981 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
2982 assert!(!cond.matches("日本語"));
2984 }
2985
2986 #[test]
2988 fn language_prefix_does_not_panic_on_two_byte_language_tag() {
2989 let cond = LanguageCondition::Prefix(AzString::from_const_str("d"));
2990 assert!(!cond.matches("é"));
2991 }
2992
2993 #[test]
2998 fn context_from_system_style_default_is_coherent() {
2999 let style = crate::system::SystemStyle::default();
3000 let ctx = DynamicSelectorContext::from_system_style(&style);
3001 assert_eq!(ctx.os, OsCondition::Any);
3003 assert_eq!(ctx.desktop_env, OptionLinuxDesktopEnv::None);
3004 assert_eq!(ctx.de_version, 0);
3005 assert_eq!(ctx.theme, ThemeCondition::Light);
3006 assert_eq!(ctx.media_type, MediaType::Screen);
3007 assert_eq!(ctx.viewport_width, DEFAULT_VIEWPORT_WIDTH);
3008 assert_eq!(ctx.viewport_height, DEFAULT_VIEWPORT_HEIGHT);
3009 assert!(ctx.container_width.is_nan());
3012 assert!(ctx.container_height.is_nan());
3013 assert!(ctx.window_focused);
3014 }
3015
3016 #[test]
3017 fn context_from_system_style_linux_carries_the_desktop_env() {
3018 use crate::system::{DesktopEnvironment, Platform};
3019 let mut style = crate::system::SystemStyle::default();
3020 style.platform = Platform::Linux(DesktopEnvironment::Kde);
3021 let ctx = DynamicSelectorContext::from_system_style(&style);
3022 assert_eq!(ctx.os, OsCondition::Linux);
3023 assert_eq!(
3024 ctx.desktop_env,
3025 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE)
3026 );
3027 }
3028
3029 #[test]
3030 fn with_viewport_updates_orientation_and_is_deterministic_at_extremes() {
3031 let base = DynamicSelectorContext::default();
3032 assert_eq!(
3033 base.with_viewport(1920.0, 1080.0).orientation,
3034 OrientationType::Landscape
3035 );
3036 assert_eq!(
3037 base.with_viewport(1080.0, 1920.0).orientation,
3038 OrientationType::Portrait
3039 );
3040 assert_eq!(
3042 base.with_viewport(500.0, 500.0).orientation,
3043 OrientationType::Portrait
3044 );
3045 assert_eq!(
3047 base.with_viewport(f32::NAN, f32::NAN).orientation,
3048 OrientationType::Portrait
3049 );
3050 assert_eq!(
3051 base.with_viewport(f32::INFINITY, f32::NEG_INFINITY).orientation,
3052 OrientationType::Landscape
3053 );
3054 let z = base.with_viewport(0.0, 0.0);
3056 assert_eq!(z.viewport_width, 0.0);
3057 assert_eq!(z.orientation, OrientationType::Portrait);
3058 let neg = base.with_viewport(-100.0, -200.0);
3059 assert_eq!(neg.orientation, OrientationType::Landscape);
3060 }
3061
3062 #[test]
3063 fn with_viewport_does_not_disturb_unrelated_fields() {
3064 let base = DynamicSelectorContext::default();
3065 let updated = base.with_viewport(1.0, 2.0);
3066 assert_eq!(updated.os, base.os);
3067 assert_eq!(updated.theme, base.theme);
3068 assert_eq!(updated.language, base.language);
3069 assert_eq!(updated.pseudo_state, base.pseudo_state);
3070 }
3071
3072 #[test]
3073 fn with_container_stores_name_and_extreme_dimensions() {
3074 let base = DynamicSelectorContext::default();
3075 let named = base.with_container(
3076 f32::MAX,
3077 f32::NEG_INFINITY,
3078 Some(AzString::from_const_str("sidebar")),
3079 );
3080 assert_eq!(named.container_width, f32::MAX);
3081 assert_eq!(named.container_height, f32::NEG_INFINITY);
3082 assert_eq!(
3083 named.container_name.as_ref(),
3084 Some(&AzString::from_const_str("sidebar"))
3085 );
3086
3087 let unnamed = base.with_container(0.0, 0.0, None);
3088 assert_eq!(unnamed.container_name.as_ref(), None);
3089 }
3090
3091 #[test]
3092 fn with_container_nan_dimensions_stay_unmatched() {
3093 let ctx = DynamicSelectorContext::default().with_container(f32::NAN, f32::NAN, None);
3094 let open = DynamicSelector::ContainerWidth(MinMaxRange::new(None, None));
3097 assert!(!open.matches(&ctx));
3098 let open_h = DynamicSelector::ContainerHeight(MinMaxRange::new(None, None));
3099 assert!(!open_h.matches(&ctx));
3100 }
3101
3102 #[test]
3103 fn with_pseudo_state_replaces_the_whole_flag_set() {
3104 let base = DynamicSelectorContext::default();
3105 let hovered = base.with_pseudo_state(PseudoStateFlags {
3106 hover: true,
3107 ..PseudoStateFlags::default()
3108 });
3109 assert!(hovered.pseudo_state.hover);
3110 assert!(!hovered.pseudo_state.active);
3111 let active = hovered.with_pseudo_state(PseudoStateFlags {
3113 active: true,
3114 ..PseudoStateFlags::default()
3115 });
3116 assert!(!active.pseudo_state.hover);
3117 assert!(active.pseudo_state.active);
3118 }
3119
3120 #[test]
3121 fn viewport_breakpoint_changed_detects_crossings_only() {
3122 let bps = [480.0_f32, 768.0, 1024.0];
3123 let base = DynamicSelectorContext::default();
3124 let small = base.with_viewport(320.0, 480.0);
3125 let medium = base.with_viewport(800.0, 600.0);
3126 let also_medium = base.with_viewport(900.0, 600.0);
3127
3128 assert!(small.viewport_breakpoint_changed(&medium, &bps));
3129 assert!(medium.viewport_breakpoint_changed(&small, &bps));
3130 assert!(!medium.viewport_breakpoint_changed(&also_medium, &bps));
3131 assert!(!small.viewport_breakpoint_changed(&small, &bps));
3132 }
3133
3134 #[test]
3135 fn viewport_breakpoint_changed_is_exactly_on_the_boundary() {
3136 let bps = [800.0_f32];
3137 let base = DynamicSelectorContext::default();
3138 let at = base.with_viewport(800.0, 600.0);
3140 let just_below = base.with_viewport(799.99, 600.0);
3141 assert!(at.viewport_breakpoint_changed(&just_below, &bps));
3142 assert!(!at.viewport_breakpoint_changed(&at, &bps));
3143 }
3144
3145 #[test]
3146 fn viewport_breakpoint_changed_handles_empty_and_degenerate_breakpoints() {
3147 let base = DynamicSelectorContext::default();
3148 let a = base.with_viewport(100.0, 100.0);
3149 let b = base.with_viewport(5000.0, 100.0);
3150 assert!(!a.viewport_breakpoint_changed(&b, &[]));
3151 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NAN]));
3153 assert!(!a.viewport_breakpoint_changed(&b, &[f32::INFINITY]));
3155 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NEG_INFINITY]));
3156 let nan_vp = base.with_viewport(f32::NAN, 100.0);
3158 assert!(nan_vp.viewport_breakpoint_changed(&b, &[800.0]));
3159 }
3160
3161 #[test]
3162 fn viewport_breakpoint_changed_with_many_breakpoints_terminates() {
3163 let bps: Vec<f32> = (0..100_000).map(|i| i as f32).collect();
3164 let base = DynamicSelectorContext::default();
3165 let a = base.with_viewport(0.0, 100.0);
3166 let b = base.with_viewport(99_999.0, 100.0);
3167 assert!(a.viewport_breakpoint_changed(&b, &bps));
3168 }
3169
3170 #[test]
3175 fn match_os_any_matches_everything() {
3176 for actual in [
3177 OsCondition::Any,
3178 OsCondition::Apple,
3179 OsCondition::MacOS,
3180 OsCondition::IOS,
3181 OsCondition::Linux,
3182 OsCondition::Windows,
3183 OsCondition::Android,
3184 OsCondition::Web,
3185 ] {
3186 assert!(DynamicSelector::match_os(OsCondition::Any, actual));
3187 }
3188 }
3189
3190 #[test]
3191 fn match_os_apple_is_the_macos_ios_union() {
3192 assert!(DynamicSelector::match_os(
3193 OsCondition::Apple,
3194 OsCondition::MacOS
3195 ));
3196 assert!(DynamicSelector::match_os(
3197 OsCondition::Apple,
3198 OsCondition::IOS
3199 ));
3200 assert!(!DynamicSelector::match_os(
3201 OsCondition::Apple,
3202 OsCondition::Linux
3203 ));
3204 assert!(!DynamicSelector::match_os(
3206 OsCondition::MacOS,
3207 OsCondition::Apple
3208 ));
3209 }
3210
3211 #[test]
3212 fn match_os_concrete_conditions_require_equality() {
3213 assert!(DynamicSelector::match_os(
3214 OsCondition::Linux,
3215 OsCondition::Linux
3216 ));
3217 assert!(!DynamicSelector::match_os(
3218 OsCondition::Linux,
3219 OsCondition::Windows
3220 ));
3221 assert!(!DynamicSelector::match_os(
3223 OsCondition::Windows,
3224 OsCondition::Any
3225 ));
3226 }
3227
3228 #[test]
3229 fn match_os_version_min_max_exact_at_zero_and_u32_max() {
3230 let zero = OsVersion::new(OsFamily::Linux, 0);
3231 let max = OsVersion::new(OsFamily::Linux, u32::MAX);
3232 let none = OptionLinuxDesktopEnv::None;
3233
3234 assert!(DynamicSelector::match_os_version(
3235 &OsVersionCondition::Min(zero),
3236 max,
3237 none,
3238 0
3239 ));
3240 assert!(!DynamicSelector::match_os_version(
3241 &OsVersionCondition::Min(max),
3242 zero,
3243 none,
3244 0
3245 ));
3246 assert!(DynamicSelector::match_os_version(
3247 &OsVersionCondition::Max(max),
3248 zero,
3249 none,
3250 0
3251 ));
3252 assert!(DynamicSelector::match_os_version(
3253 &OsVersionCondition::Exact(max),
3254 max,
3255 none,
3256 0
3257 ));
3258 assert!(!DynamicSelector::match_os_version(
3259 &OsVersionCondition::Exact(zero),
3260 max,
3261 none,
3262 0
3263 ));
3264 }
3265
3266 #[test]
3267 fn match_os_version_cross_family_never_matches() {
3268 let none = OptionLinuxDesktopEnv::None;
3269 for cond in [
3271 OsVersionCondition::Min(OsVersion::WIN_10),
3272 OsVersionCondition::Max(OsVersion::WIN_10),
3273 OsVersionCondition::Exact(OsVersion::WIN_10),
3274 ] {
3275 assert!(
3276 !DynamicSelector::match_os_version(
3277 &cond,
3278 OsVersion::MACOS_SONOMA,
3279 none,
3280 0
3281 ),
3282 "{cond:?} must not match a macOS runtime"
3283 );
3284 }
3285 }
3286
3287 #[test]
3288 fn match_os_version_desktop_environment_requires_the_env_to_be_present() {
3289 let cond = OsVersionCondition::DesktopEnvironment(LinuxDesktopEnv::Gnome);
3290 assert!(DynamicSelector::match_os_version(
3291 &cond,
3292 OsVersion::unknown(),
3293 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3294 0
3295 ));
3296 assert!(!DynamicSelector::match_os_version(
3297 &cond,
3298 OsVersion::unknown(),
3299 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3300 0
3301 ));
3302 assert!(!DynamicSelector::match_os_version(
3303 &cond,
3304 OsVersion::unknown(),
3305 OptionLinuxDesktopEnv::None,
3306 0
3307 ));
3308 }
3309
3310 #[test]
3311 fn match_os_version_desktop_env_min_max_respect_the_unknown_zero_sentinel() {
3312 let gnome = OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome);
3313 let dev = DesktopEnvVersion {
3314 env: LinuxDesktopEnv::Gnome,
3315 version_id: 40,
3316 };
3317 assert!(!DynamicSelector::match_os_version(
3319 &OsVersionCondition::DesktopEnvMin(dev),
3320 OsVersion::unknown(),
3321 gnome,
3322 0
3323 ));
3324 assert!(!DynamicSelector::match_os_version(
3325 &OsVersionCondition::DesktopEnvMax(dev),
3326 OsVersion::unknown(),
3327 gnome,
3328 0
3329 ));
3330 assert!(DynamicSelector::match_os_version(
3332 &OsVersionCondition::DesktopEnvMin(dev),
3333 OsVersion::unknown(),
3334 gnome,
3335 40
3336 ));
3337 assert!(DynamicSelector::match_os_version(
3338 &OsVersionCondition::DesktopEnvMin(dev),
3339 OsVersion::unknown(),
3340 gnome,
3341 u32::MAX
3342 ));
3343 assert!(!DynamicSelector::match_os_version(
3344 &OsVersionCondition::DesktopEnvMin(dev),
3345 OsVersion::unknown(),
3346 gnome,
3347 39
3348 ));
3349 assert!(DynamicSelector::match_os_version(
3350 &OsVersionCondition::DesktopEnvMax(dev),
3351 OsVersion::unknown(),
3352 gnome,
3353 40
3354 ));
3355 assert!(!DynamicSelector::match_os_version(
3356 &OsVersionCondition::DesktopEnvMax(dev),
3357 OsVersion::unknown(),
3358 gnome,
3359 41
3360 ));
3361 }
3362
3363 #[test]
3364 fn match_os_version_desktop_env_exact_needs_the_matching_env() {
3365 let dev = DesktopEnvVersion {
3366 env: LinuxDesktopEnv::Gnome,
3367 version_id: 45,
3368 };
3369 assert!(DynamicSelector::match_os_version(
3370 &OsVersionCondition::DesktopEnvExact(dev),
3371 OsVersion::unknown(),
3372 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3373 45
3374 ));
3375 assert!(!DynamicSelector::match_os_version(
3376 &OsVersionCondition::DesktopEnvExact(dev),
3377 OsVersion::unknown(),
3378 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3379 45
3380 ));
3381 assert!(!DynamicSelector::match_os_version(
3382 &OsVersionCondition::DesktopEnvExact(dev),
3383 OsVersion::unknown(),
3384 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3385 46
3386 ));
3387 }
3388
3389 #[test]
3395 fn match_os_version_desktop_env_exact_zero_fails_when_de_version_is_unknown() {
3396 let dev = DesktopEnvVersion {
3397 env: LinuxDesktopEnv::Gnome,
3398 version_id: 0,
3399 };
3400 assert!(
3401 !DynamicSelector::match_os_version(
3402 &OsVersionCondition::DesktopEnvExact(dev),
3403 OsVersion::unknown(),
3404 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3405 0
3406 ),
3407 "de_version == 0 is the 'unknown' sentinel; it must not satisfy an exact match"
3408 );
3409 }
3410
3411 #[test]
3412 fn match_theme_system_preferred_is_a_wildcard() {
3413 for actual in [
3414 ThemeCondition::Light,
3415 ThemeCondition::Dark,
3416 ThemeCondition::SystemPreferred,
3417 ThemeCondition::Custom(AzString::from_const_str("solarized")),
3418 ] {
3419 assert!(DynamicSelector::match_theme(
3420 &ThemeCondition::SystemPreferred,
3421 &actual
3422 ));
3423 }
3424 }
3425
3426 #[test]
3427 fn match_theme_custom_compares_the_name() {
3428 let a = ThemeCondition::Custom(AzString::from_const_str("nord"));
3429 let b = ThemeCondition::Custom(AzString::from_const_str("nord"));
3430 let c = ThemeCondition::Custom(AzString::from_const_str("Nord"));
3431 assert!(DynamicSelector::match_theme(&a, &b));
3432 assert!(!DynamicSelector::match_theme(&a, &c));
3434 assert!(!DynamicSelector::match_theme(&a, &ThemeCondition::Dark));
3435 assert!(!DynamicSelector::match_theme(
3437 &ThemeCondition::Dark,
3438 &ThemeCondition::SystemPreferred
3439 ));
3440 }
3441
3442 #[test]
3443 fn match_pseudo_state_reads_through_to_the_context_flags() {
3444 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3445 hover: true,
3446 checked: true,
3447 ..PseudoStateFlags::default()
3448 });
3449 assert!(DynamicSelector::match_pseudo_state(
3450 PseudoStateType::Hover,
3451 &ctx
3452 ));
3453 assert!(DynamicSelector::match_pseudo_state(
3454 PseudoStateType::CheckedTrue,
3455 &ctx
3456 ));
3457 assert!(!DynamicSelector::match_pseudo_state(
3458 PseudoStateType::CheckedFalse,
3459 &ctx
3460 ));
3461 assert!(!DynamicSelector::match_pseudo_state(
3462 PseudoStateType::Active,
3463 &ctx
3464 ));
3465 assert!(DynamicSelector::match_pseudo_state(
3466 PseudoStateType::Normal,
3467 &ctx
3468 ));
3469 }
3470
3471 #[test]
3472 fn match_pseudo_state_agrees_with_has_state_for_every_state() {
3473 let flags = PseudoStateFlags {
3474 hover: true,
3475 focused: true,
3476 visited: true,
3477 drag_over: true,
3478 ..PseudoStateFlags::default()
3479 };
3480 let ctx = DynamicSelectorContext::default().with_pseudo_state(flags);
3481 for state in [
3482 PseudoStateType::Normal,
3483 PseudoStateType::Hover,
3484 PseudoStateType::Active,
3485 PseudoStateType::Focus,
3486 PseudoStateType::Disabled,
3487 PseudoStateType::CheckedTrue,
3488 PseudoStateType::CheckedFalse,
3489 PseudoStateType::FocusWithin,
3490 PseudoStateType::Visited,
3491 PseudoStateType::Backdrop,
3492 PseudoStateType::Dragging,
3493 PseudoStateType::DragOver,
3494 ] {
3495 assert_eq!(
3496 DynamicSelector::match_pseudo_state(state, &ctx),
3497 flags.has_state(state),
3498 "match_pseudo_state and has_state disagree on {state:?}"
3499 );
3500 }
3501 }
3502
3503 #[test]
3504 fn selector_matches_every_variant_against_the_default_context() {
3505 let ctx = DynamicSelectorContext::default();
3507 for sel in all_selector_variants() {
3508 let a = sel.matches(&ctx);
3509 let b = sel.matches(&ctx);
3510 assert_eq!(a, b, "{sel:?} is not deterministic");
3511 }
3512 }
3513
3514 #[test]
3515 fn selector_matches_media_all_is_a_wildcard() {
3516 let ctx = DynamicSelectorContext::default();
3517 assert_eq!(ctx.media_type, MediaType::Screen);
3518 assert!(DynamicSelector::Media(MediaType::All).matches(&ctx));
3519 assert!(DynamicSelector::Media(MediaType::Screen).matches(&ctx));
3520 assert!(!DynamicSelector::Media(MediaType::Print).matches(&ctx));
3521 }
3522
3523 #[test]
3524 fn selector_matches_aspect_ratio_never_divides_by_zero() {
3525 let base = DynamicSelectorContext::default();
3526 let flat = base.with_viewport(800.0, 0.0);
3528 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3529 .matches(&flat));
3530 let nan_h = base.with_viewport(800.0, f32::NAN);
3532 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3533 .matches(&nan_h));
3534 let nan_w = base.with_viewport(f32::NAN, 600.0);
3536 assert!(!DynamicSelector::AspectRatio(MinMaxRange::with_min(0.0)).matches(&nan_w));
3537 }
3538
3539 #[test]
3540 fn selector_matches_container_name_requires_an_exact_name() {
3541 let ctx = DynamicSelectorContext::default().with_container(
3542 100.0,
3543 100.0,
3544 Some(AzString::from_const_str("sidebar")),
3545 );
3546 assert!(
3547 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&ctx)
3548 );
3549 assert!(
3550 !DynamicSelector::ContainerName(AzString::from_const_str("Sidebar")).matches(&ctx)
3551 );
3552 assert!(!DynamicSelector::ContainerName(AzString::from_const_str("main")).matches(&ctx));
3553 let no_ctr = DynamicSelectorContext::default();
3555 assert!(
3556 !DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&no_ctr)
3557 );
3558 }
3559
3560 #[test]
3561 fn selector_matches_bool_conditions_compare_both_polarities() {
3562 let ctx = DynamicSelectorContext::default();
3563 assert_eq!(ctx.prefers_reduced_motion, BoolCondition::False);
3564 assert!(DynamicSelector::PrefersReducedMotion(BoolCondition::False).matches(&ctx));
3566 assert!(!DynamicSelector::PrefersReducedMotion(BoolCondition::True).matches(&ctx));
3567 assert!(DynamicSelector::PrefersHighContrast(BoolCondition::False).matches(&ctx));
3568 assert!(!DynamicSelector::PrefersHighContrast(BoolCondition::True).matches(&ctx));
3569 }
3570
3571 #[test]
3572 fn bool_condition_roundtrips_through_bool() {
3573 for b in [false, true] {
3574 assert_eq!(bool::from(BoolCondition::from(b)), b);
3575 }
3576 assert_eq!(BoolCondition::default(), BoolCondition::False);
3577 assert!(!bool::from(BoolCondition::False));
3578 assert!(bool::from(BoolCondition::True));
3579 }
3580
3581 #[test]
3586 fn simple_property_is_unconditional_and_always_matches() {
3587 let p = CssPropertyWithConditions::simple(paint_prop());
3588 assert!(p.apply_if.as_slice().is_empty());
3589 assert!(!p.is_conditional());
3590 assert!(!p.is_pseudo_state_only());
3591 assert!(p.matches(&DynamicSelectorContext::default()));
3592 }
3593
3594 #[test]
3595 fn with_condition_and_with_single_condition_agree() {
3596 let a = CssPropertyWithConditions::with_condition(
3597 paint_prop(),
3598 DynamicSelector::PseudoState(PseudoStateType::Hover),
3599 );
3600 let b = CssPropertyWithConditions::on_hover(paint_prop());
3601 assert_eq!(a.apply_if.as_slice(), b.apply_if.as_slice());
3602 assert_eq!(a.apply_if.as_slice().len(), 1);
3603 }
3604
3605 #[test]
3606 fn with_conditions_preserves_order_and_length() {
3607 let conds = all_selector_variants();
3608 let p = CssPropertyWithConditions::with_conditions(
3609 paint_prop(),
3610 DynamicSelectorVec::from_vec(conds.clone()),
3611 );
3612 assert_eq!(p.apply_if.as_slice().len(), conds.len());
3613 for (i, c) in conds.iter().enumerate() {
3614 assert_eq!(p.apply_if.as_slice()[i].variant_tag(), c.variant_tag());
3615 }
3616 assert!(p.is_conditional());
3617 }
3618
3619 #[test]
3620 fn with_conditions_empty_vec_is_unconditional() {
3621 let p = CssPropertyWithConditions::with_conditions(
3622 paint_prop(),
3623 DynamicSelectorVec::from_vec(vec![]),
3624 );
3625 assert!(!p.is_conditional());
3626 assert!(p.matches(&DynamicSelectorContext::default()));
3627 }
3628
3629 #[test]
3630 fn pseudo_state_constructors_build_the_right_condition() {
3631 let cases = [
3632 (
3633 CssPropertyWithConditions::on_hover(paint_prop()),
3634 PseudoStateType::Hover,
3635 ),
3636 (
3637 CssPropertyWithConditions::on_active(paint_prop()),
3638 PseudoStateType::Active,
3639 ),
3640 (
3641 CssPropertyWithConditions::on_focus(paint_prop()),
3642 PseudoStateType::Focus,
3643 ),
3644 (
3645 CssPropertyWithConditions::when_disabled(paint_prop()),
3646 PseudoStateType::Disabled,
3647 ),
3648 ];
3649 for (prop, expected) in cases {
3650 assert_eq!(
3651 prop.apply_if.as_slice(),
3652 &[DynamicSelector::PseudoState(expected)]
3653 );
3654 assert!(prop.is_pseudo_state_only());
3655 assert!(prop.is_conditional());
3656 }
3657 }
3658
3659 #[test]
3660 fn os_and_theme_constructors_build_the_right_condition() {
3661 assert_eq!(
3662 CssPropertyWithConditions::on_windows(paint_prop())
3663 .apply_if
3664 .as_slice(),
3665 &[DynamicSelector::Os(OsCondition::Windows)]
3666 );
3667 assert_eq!(
3668 CssPropertyWithConditions::on_macos(paint_prop())
3669 .apply_if
3670 .as_slice(),
3671 &[DynamicSelector::Os(OsCondition::MacOS)]
3672 );
3673 assert_eq!(
3674 CssPropertyWithConditions::on_linux(paint_prop())
3675 .apply_if
3676 .as_slice(),
3677 &[DynamicSelector::Os(OsCondition::Linux)]
3678 );
3679 assert_eq!(
3680 CssPropertyWithConditions::on_os(paint_prop(), OsCondition::Web)
3681 .apply_if
3682 .as_slice(),
3683 &[DynamicSelector::Os(OsCondition::Web)]
3684 );
3685 assert_eq!(
3686 CssPropertyWithConditions::dark_theme(paint_prop())
3687 .apply_if
3688 .as_slice(),
3689 &[DynamicSelector::Theme(ThemeCondition::Dark)]
3690 );
3691 assert_eq!(
3692 CssPropertyWithConditions::light_theme(paint_prop())
3693 .apply_if
3694 .as_slice(),
3695 &[DynamicSelector::Theme(ThemeCondition::Light)]
3696 );
3697 assert!(!CssPropertyWithConditions::on_linux(paint_prop()).is_pseudo_state_only());
3699 assert!(!CssPropertyWithConditions::dark_theme(paint_prop()).is_pseudo_state_only());
3700 }
3701
3702 #[test]
3703 fn matches_requires_all_conditions_to_hold() {
3704 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3705 hover: true,
3706 ..PseudoStateFlags::default()
3707 });
3708 let both = CssPropertyWithConditions::with_conditions(
3710 paint_prop(),
3711 DynamicSelectorVec::from_vec(vec![
3712 DynamicSelector::PseudoState(PseudoStateType::Hover),
3713 DynamicSelector::PseudoState(PseudoStateType::Focus),
3714 ]),
3715 );
3716 assert!(!both.matches(&ctx));
3717
3718 let ok = CssPropertyWithConditions::with_conditions(
3720 paint_prop(),
3721 DynamicSelectorVec::from_vec(vec![
3722 DynamicSelector::PseudoState(PseudoStateType::Hover),
3723 DynamicSelector::PseudoState(PseudoStateType::Normal),
3724 ]),
3725 );
3726 assert!(ok.matches(&ctx));
3727 }
3728
3729 #[test]
3730 fn matches_with_a_large_condition_list_terminates() {
3731 let conds: Vec<DynamicSelector> = (0..50_000)
3732 .map(|_| DynamicSelector::PseudoState(PseudoStateType::Normal))
3733 .collect();
3734 let p = CssPropertyWithConditions::with_conditions(
3735 paint_prop(),
3736 DynamicSelectorVec::from_vec(conds),
3737 );
3738 assert!(p.matches(&DynamicSelectorContext::default()));
3739 }
3740
3741 #[test]
3742 fn is_pseudo_state_only_is_false_for_empty_and_for_mixed_lists() {
3743 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_pseudo_state_only());
3745 let mixed = CssPropertyWithConditions::with_conditions(
3747 paint_prop(),
3748 DynamicSelectorVec::from_vec(vec![
3749 DynamicSelector::PseudoState(PseudoStateType::Hover),
3750 DynamicSelector::Os(OsCondition::Linux),
3751 ]),
3752 );
3753 assert!(!mixed.is_pseudo_state_only());
3754 let all_pseudo = CssPropertyWithConditions::with_conditions(
3756 paint_prop(),
3757 DynamicSelectorVec::from_vec(vec![
3758 DynamicSelector::PseudoState(PseudoStateType::Hover),
3759 DynamicSelector::PseudoState(PseudoStateType::Focus),
3760 ]),
3761 );
3762 assert!(all_pseudo.is_pseudo_state_only());
3763 }
3764
3765 #[test]
3766 fn is_layout_affecting_splits_layout_from_paint() {
3767 assert!(CssPropertyWithConditions::simple(layout_prop()).is_layout_affecting());
3768 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_layout_affecting());
3769 assert!(CssPropertyWithConditions::on_hover(layout_prop()).is_layout_affecting());
3771 assert!(!CssPropertyWithConditions::on_hover(paint_prop()).is_layout_affecting());
3772 }
3773
3774 #[test]
3775 fn css_property_with_conditions_hash_agrees_with_eq() {
3776 let a = CssPropertyWithConditions::on_hover(paint_prop());
3777 let b = CssPropertyWithConditions::on_hover(paint_prop());
3778 assert_eq!(a, b);
3779 assert_eq!(hash_of(&a), hash_of(&b));
3780 assert_eq!(a.cmp(&b), Ordering::Equal);
3781
3782 let c = CssPropertyWithConditions::on_focus(paint_prop());
3785 assert_ne!(a, c);
3786 assert_ne!(a.cmp(&c), Ordering::Equal);
3787 assert_ne!(hash_of(&a), hash_of(&c));
3788 }
3789
3790 #[test]
3791 fn css_property_with_conditions_ord_is_lexicographic_over_conditions() {
3792 let short = CssPropertyWithConditions::on_hover(paint_prop());
3793 let long = CssPropertyWithConditions::with_conditions(
3794 paint_prop(),
3795 DynamicSelectorVec::from_vec(vec![
3796 DynamicSelector::PseudoState(PseudoStateType::Hover),
3797 DynamicSelector::PseudoState(PseudoStateType::Focus),
3798 ]),
3799 );
3800 assert_eq!(short.cmp(&long), Ordering::Less);
3802 assert_eq!(long.cmp(&short), Ordering::Greater);
3803 }
3804
3805 #[cfg(feature = "parser")]
3810 #[test]
3811 fn parse_os_at_rule_bare_and_parenthesized_forms_agree() {
3812 let bare = parse_os_at_rule_content("linux").expect("bare linux");
3813 let paren = parse_os_at_rule_content("(linux)").expect("(linux)");
3814 let quoted = parse_os_at_rule_content("(\"linux\")").expect("quoted");
3815 let spaced = parse_os_at_rule_content(" ( linux ) ").expect("spaced");
3816 assert_eq!(bare, vec![DynamicSelector::Os(OsCondition::Linux)]);
3817 assert_eq!(bare, paren);
3818 assert_eq!(bare, quoted);
3819 assert_eq!(bare, spaced);
3820 }
3821
3822 #[cfg(feature = "parser")]
3823 #[test]
3824 fn parse_os_at_rule_emits_the_family_even_for_any() {
3825 for s in ["(any)", "(all)", "(*)"] {
3828 let conds = parse_os_at_rule_content(s).unwrap_or_else(|| panic!("{s} must parse"));
3829 assert_eq!(conds, vec![DynamicSelector::Os(OsCondition::Any)]);
3830 assert!(conds[0].matches(&DynamicSelectorContext::default()));
3831 }
3832 }
3833
3834 #[cfg(feature = "parser")]
3835 #[test]
3836 fn parse_os_at_rule_desktop_env_forms() {
3837 assert_eq!(
3838 parse_os_at_rule_content("(linux:gnome)"),
3839 Some(vec![
3840 DynamicSelector::Os(OsCondition::Linux),
3841 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3842 LinuxDesktopEnv::Gnome
3843 )),
3844 ])
3845 );
3846 assert_eq!(
3848 parse_os_at_rule_content("(linux:notarealde)"),
3849 Some(vec![
3850 DynamicSelector::Os(OsCondition::Linux),
3851 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3852 LinuxDesktopEnv::Other
3853 )),
3854 ])
3855 );
3856 assert_eq!(
3858 parse_os_at_rule_content("(linux:)"),
3859 Some(vec![DynamicSelector::Os(OsCondition::Linux)])
3860 );
3861 }
3862
3863 #[cfg(feature = "parser")]
3864 #[test]
3865 fn parse_os_at_rule_version_operators() {
3866 assert_eq!(
3867 parse_os_at_rule_content("(windows >= win-11)"),
3868 Some(vec![
3869 DynamicSelector::Os(OsCondition::Windows),
3870 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
3871 ])
3872 );
3873 assert_eq!(
3875 parse_os_at_rule_content("(windows > win-11)"),
3876 parse_os_at_rule_content("(windows >= win-11)")
3877 );
3878 assert_eq!(
3879 parse_os_at_rule_content("(macos <= sonoma)"),
3880 Some(vec![
3881 DynamicSelector::Os(OsCondition::MacOS),
3882 DynamicSelector::OsVersion(OsVersionCondition::Max(OsVersion::MACOS_SONOMA)),
3883 ])
3884 );
3885 assert_eq!(
3886 parse_os_at_rule_content("(ios = 17)"),
3887 Some(vec![
3888 DynamicSelector::Os(OsCondition::IOS),
3889 DynamicSelector::OsVersion(OsVersionCondition::Exact(OsVersion::IOS_17)),
3890 ])
3891 );
3892 }
3893
3894 #[cfg(feature = "parser")]
3895 #[test]
3896 fn parse_os_at_rule_desktop_env_version() {
3897 assert_eq!(
3898 parse_os_at_rule_content("(linux:gnome > 40)"),
3899 Some(vec![
3900 DynamicSelector::Os(OsCondition::Linux),
3901 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvMin(
3902 DesktopEnvVersion {
3903 env: LinuxDesktopEnv::Gnome,
3904 version_id: 40,
3905 }
3906 )),
3907 ])
3908 );
3909 assert_eq!(parse_os_at_rule_content("(linux:gnome > 4294967296)"), None);
3911 assert_eq!(parse_os_at_rule_content("(linux:gnome > -1)"), None);
3912 assert_eq!(parse_os_at_rule_content("(linux:gnome > abc)"), None);
3913 assert_eq!(parse_os_at_rule_content("(linux:gnome > )"), None);
3914 }
3915
3916 #[cfg(feature = "parser")]
3917 #[test]
3918 fn parse_os_at_rule_rejects_versions_on_versionless_families() {
3919 assert_eq!(parse_os_at_rule_content("(apple >= 14)"), None);
3921 assert_eq!(parse_os_at_rule_content("(web >= 1)"), None);
3922 assert_eq!(parse_os_at_rule_content("(any >= 1)"), None);
3923 }
3924
3925 #[cfg(feature = "parser")]
3926 #[test]
3927 fn parse_os_at_rule_empty_and_garbage_is_none() {
3928 for s in [
3929 "",
3930 " ",
3931 "\t\n",
3932 "()",
3933 "( )",
3934 "(\"\")",
3935 "('')",
3936 "(:)",
3937 "(:gnome)",
3938 "(notanos)",
3939 "(linux;drop)",
3940 "\u{1F600}",
3941 "(\u{1F600})",
3942 "(linux linux)",
3943 ] {
3944 assert_eq!(parse_os_at_rule_content(s), None, "{s:?} must not parse");
3945 }
3946 }
3947
3948 #[cfg(feature = "parser")]
3949 #[test]
3950 fn parse_os_at_rule_huge_and_deeply_parenthesized_input_terminates() {
3951 let nested = format!("{}linux{}", "(".repeat(10_000), ")".repeat(10_000));
3953 assert_eq!(parse_os_at_rule_content(&nested), None);
3954 assert_eq!(parse_os_at_rule_content(&"a".repeat(1_000_000)), None);
3955 assert_eq!(
3956 parse_os_at_rule_content(&format!("(linux >= {})", "9".repeat(100_000))),
3957 None
3958 );
3959 }
3960
3961 #[cfg(feature = "parser")]
3962 #[test]
3963 fn split_op_and_version_picks_the_earliest_then_longest_operator() {
3964 let (subject, op) = split_op_and_version("linux >= 6.0");
3965 assert_eq!(subject, "linux ");
3966 let (op, ver) = op.expect("operator found");
3967 assert!(matches!(op, VersionOp::Min));
3968 assert_eq!(ver, "6.0");
3969
3970 let (_, op) = split_op_and_version("a>=1");
3972 assert!(matches!(op.expect("op").0, VersionOp::Min));
3973 let (_, op) = split_op_and_version("a<=1");
3974 assert!(matches!(op.expect("op").0, VersionOp::Max));
3975 let (_, op) = split_op_and_version("a=1");
3976 assert!(matches!(op.expect("op").0, VersionOp::Exact));
3977 }
3978
3979 #[cfg(feature = "parser")]
3980 #[test]
3981 fn split_op_and_version_with_no_operator_returns_the_whole_string() {
3982 let (subject, op) = split_op_and_version("linux");
3983 assert_eq!(subject, "linux");
3984 assert!(op.is_none());
3985
3986 let (subject, op) = split_op_and_version("");
3987 assert_eq!(subject, "");
3988 assert!(op.is_none());
3989 }
3990
3991 #[cfg(feature = "parser")]
3992 #[test]
3993 fn split_op_and_version_operator_only_yields_empty_sides() {
3994 let (subject, op) = split_op_and_version(">=");
3995 assert_eq!(subject, "");
3996 assert_eq!(op.expect("op").1, "");
3997
3998 let (subject, op) = split_op_and_version("=");
3999 assert_eq!(subject, "");
4000 assert_eq!(op.expect("op").1, "");
4001 }
4002
4003 #[cfg(feature = "parser")]
4004 #[test]
4005 fn split_op_and_version_does_not_split_inside_a_multibyte_char() {
4006 let (subject, op) = split_op_and_version("日本語 >= 6.0");
4009 assert_eq!(subject, "日本語 ");
4010 assert_eq!(op.expect("op").1, "6.0");
4011 let (subject, op) = split_op_and_version("🦀🦀🦀");
4013 assert_eq!(subject, "🦀🦀🦀");
4014 assert!(op.is_none());
4015 }
4016
4017 #[cfg(feature = "parser")]
4018 #[test]
4019 fn parse_os_family_token_accepts_aliases_case_insensitively() {
4020 assert_eq!(parse_os_family_token("LINUX"), Some(OsCondition::Linux));
4021 assert_eq!(parse_os_family_token("Win"), Some(OsCondition::Windows));
4022 assert_eq!(parse_os_family_token("windows"), Some(OsCondition::Windows));
4023 assert_eq!(parse_os_family_token("osx"), Some(OsCondition::MacOS));
4024 assert_eq!(parse_os_family_token("mac"), Some(OsCondition::MacOS));
4025 assert_eq!(parse_os_family_token("wasm"), Some(OsCondition::Web));
4026 assert_eq!(parse_os_family_token("*"), Some(OsCondition::Any));
4027 assert_eq!(parse_os_family_token("all"), Some(OsCondition::Any));
4028 }
4029
4030 #[cfg(feature = "parser")]
4031 #[test]
4032 fn parse_os_family_token_rejects_empty_padded_and_junk() {
4033 for s in [
4035 "",
4036 " ",
4037 " linux",
4038 "linux ",
4039 "lin",
4040 "linux2",
4041 "0",
4042 "-1",
4043 "NaN",
4044 "\u{1F600}",
4045 ] {
4046 assert_eq!(parse_os_family_token(s), None, "{s:?} must not parse");
4047 }
4048 assert_eq!(parse_os_family_token(&"linux".repeat(100_000)), None);
4049 }
4050
4051 #[cfg(feature = "parser")]
4052 #[test]
4053 fn parse_de_token_is_total_and_falls_back_to_other() {
4054 assert_eq!(parse_de_token("GNOME"), LinuxDesktopEnv::Gnome);
4055 assert_eq!(parse_de_token("kde"), LinuxDesktopEnv::KDE);
4056 assert_eq!(parse_de_token("XFCE"), LinuxDesktopEnv::XFCE);
4057 assert_eq!(parse_de_token("unity"), LinuxDesktopEnv::Unity);
4058 assert_eq!(parse_de_token("Cinnamon"), LinuxDesktopEnv::Cinnamon);
4059 assert_eq!(parse_de_token("mate"), LinuxDesktopEnv::MATE);
4060 for s in ["", " ", "gnome ", "\u{1F600}", "日本語", "\0"] {
4062 assert_eq!(parse_de_token(s), LinuxDesktopEnv::Other, "{s:?}");
4063 }
4064 assert_eq!(
4065 parse_de_token(&"gnome".repeat(200_000)),
4066 LinuxDesktopEnv::Other
4067 );
4068 }
4069
4070 #[cfg(feature = "parser")]
4075 fn parse_len(style: &str) -> usize {
4076 CssPropertyWithConditionsVec::parse(style)
4077 .into_library_owned_vec()
4078 .len()
4079 }
4080
4081 #[cfg(feature = "parser")]
4082 #[test]
4083 fn parse_valid_minimal_positive_control() {
4084 let props = CssPropertyWithConditionsVec::parse("color: red;").into_library_owned_vec();
4085 assert_eq!(props.len(), 1);
4086 assert!(!props[0].is_conditional());
4087 assert!(matches!(props[0].property, CssProperty::TextColor(_)));
4088 }
4089
4090 #[cfg(feature = "parser")]
4091 #[test]
4092 fn parse_empty_and_whitespace_yields_no_properties() {
4093 for s in ["", " ", "\t\n\r ", ";", ";;;;", " ; ; "] {
4094 assert_eq!(parse_len(s), 0, "{s:?} must yield no properties");
4095 }
4096 }
4097
4098 #[cfg(feature = "parser")]
4099 #[test]
4100 fn parse_garbage_never_panics_and_yields_nothing() {
4101 for s in [
4102 "not css at all",
4103 "color",
4104 "color:",
4105 ":",
4106 "::::",
4107 "{}",
4108 "}{",
4109 "}",
4110 "{",
4111 "{{{",
4112 "}}}",
4113 "}{color: red}",
4114 "color: ;",
4115 "\0: \0;",
4116 "%s%n%s",
4117 "\u{1F600}: \u{1F600};",
4118 "日本語: 赤;",
4119 ] {
4120 let _ = parse_len(s);
4122 }
4123 assert_eq!(parse_len("not css at all"), 0);
4124 assert_eq!(parse_len("color:"), 0);
4125 assert_eq!(parse_len("}{"), 0);
4126 }
4127
4128 #[cfg(feature = "parser")]
4129 #[test]
4130 fn parse_tolerates_a_missing_trailing_semicolon() {
4131 assert_eq!(parse_len("color: red"), 1);
4132 assert_eq!(parse_len("color: red;color: blue"), 2);
4133 }
4134
4135 #[cfg(feature = "parser")]
4136 #[test]
4137 fn parse_pseudo_selector_block_attaches_the_condition() {
4138 let props = CssPropertyWithConditionsVec::parse(":hover { color: red; }")
4139 .into_library_owned_vec();
4140 assert_eq!(props.len(), 1);
4141 assert!(props[0].is_pseudo_state_only());
4142 assert_eq!(
4143 props[0].apply_if.as_slice(),
4144 &[DynamicSelector::PseudoState(PseudoStateType::Hover)]
4145 );
4146 }
4147
4148 #[cfg(feature = "parser")]
4149 #[test]
4150 fn parse_unknown_selector_block_is_dropped_wholesale() {
4151 assert_eq!(parse_len(":nosuchstate { color: red; }"), 0);
4154 assert_eq!(parse_len("@nosuchrule { color: red; }"), 0);
4155 assert_eq!(parse_len("div { color: red; }"), 0);
4156 }
4157
4158 #[cfg(feature = "parser")]
4159 #[test]
4160 fn parse_nesting_accumulates_inherited_conditions() {
4161 let props =
4162 CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
4163 .into_library_owned_vec();
4164 assert_eq!(props.len(), 2);
4165 let font = props
4167 .iter()
4168 .find(|p| matches!(p.property, CssProperty::FontSize(_)))
4169 .expect("font-size present");
4170 assert_eq!(
4171 font.apply_if.as_slice(),
4172 &[DynamicSelector::Os(OsCondition::Linux)]
4173 );
4174 let color = props
4175 .iter()
4176 .find(|p| matches!(p.property, CssProperty::TextColor(_)))
4177 .expect("color present");
4178 assert_eq!(
4179 color.apply_if.as_slice(),
4180 &[
4181 DynamicSelector::Os(OsCondition::Linux),
4182 DynamicSelector::PseudoState(PseudoStateType::Hover),
4183 ]
4184 );
4185 assert!(!color.is_pseudo_state_only());
4186 }
4187
4188 #[cfg(feature = "parser")]
4189 #[test]
4190 fn parse_moderately_deep_nesting_terminates() {
4191 const DEPTH: usize = 50;
4196 let style = format!(
4197 "{}color: red;{}",
4198 ":hover {".repeat(DEPTH),
4199 "}".repeat(DEPTH)
4200 );
4201 let props = CssPropertyWithConditionsVec::parse(&style).into_library_owned_vec();
4202 assert_eq!(props.len(), 1);
4203 assert_eq!(props[0].apply_if.as_slice().len(), DEPTH);
4204 assert!(props[0].is_pseudo_state_only());
4205 }
4206
4207 #[cfg(feature = "parser")]
4208 #[test]
4209 fn parse_unbalanced_braces_do_not_panic() {
4210 for s in [
4212 "color: red; }",
4213 "{ color: red;",
4214 ":hover { color: red;",
4215 ":hover }",
4216 &"{".repeat(1_000),
4217 &"}".repeat(1_000),
4218 ] {
4219 let _ = parse_len(s);
4220 }
4221 }
4222
4223 #[cfg(feature = "parser")]
4224 #[test]
4225 fn parse_very_long_input_terminates() {
4226 let long = "color: red;".repeat(5_000);
4227 assert_eq!(parse_len(&long), 5_000);
4228 assert_eq!(parse_len(&"a".repeat(500_000)), 0);
4230 assert_eq!(parse_len(&format!("color: {};", "z".repeat(500_000))), 0);
4231 }
4232
4233 #[cfg(feature = "parser")]
4234 #[test]
4235 fn parse_selector_to_conditions_covers_every_pseudo_class() {
4236 let cases = [
4237 ("hover", PseudoStateType::Hover),
4238 ("active", PseudoStateType::Active),
4239 ("focus", PseudoStateType::Focus),
4240 ("focus-within", PseudoStateType::FocusWithin),
4241 ("disabled", PseudoStateType::Disabled),
4242 ("checked", PseudoStateType::CheckedTrue),
4243 ("visited", PseudoStateType::Visited),
4244 ("backdrop", PseudoStateType::Backdrop),
4245 ("dragging", PseudoStateType::Dragging),
4246 ("drag-over", PseudoStateType::DragOver),
4247 ];
4248 for (name, expected) in cases {
4249 assert_eq!(
4250 CssPropertyWithConditionsVec::parse_selector_to_conditions(&format!(":{name}")),
4251 Some(vec![DynamicSelector::PseudoState(expected)]),
4252 ":{name} must parse"
4253 );
4254 }
4255 }
4256
4257 #[cfg(feature = "parser")]
4258 #[test]
4259 fn parse_selector_to_conditions_wildcards_are_unconditional() {
4260 assert_eq!(
4261 CssPropertyWithConditionsVec::parse_selector_to_conditions("*"),
4262 Some(vec![])
4263 );
4264 assert_eq!(
4265 CssPropertyWithConditionsVec::parse_selector_to_conditions(""),
4266 Some(vec![])
4267 );
4268 assert_eq!(
4269 CssPropertyWithConditionsVec::parse_selector_to_conditions(" "),
4270 Some(vec![])
4271 );
4272 }
4273
4274 #[cfg(feature = "parser")]
4275 #[test]
4276 fn parse_selector_to_conditions_rejects_unknown_selectors() {
4277 for s in [
4278 ":", ":hoverr", ":HOVER", "div", "#id", ".class", "@", "\u{1F600}", ":\u{1F600}",
4279 ] {
4280 assert_eq!(
4281 CssPropertyWithConditionsVec::parse_selector_to_conditions(s),
4282 None,
4283 "{s:?} must not parse"
4284 );
4285 }
4286 }
4287
4288 #[cfg(feature = "parser")]
4289 #[test]
4290 fn parse_at_rule_theme_lang_and_accessibility() {
4291 assert_eq!(
4292 CssPropertyWithConditionsVec::parse_at_rule("theme dark"),
4293 Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)])
4294 );
4295 assert_eq!(
4296 CssPropertyWithConditionsVec::parse_at_rule("theme light"),
4297 Some(vec![DynamicSelector::Theme(ThemeCondition::Light)])
4298 );
4299 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("theme neon"), None);
4300
4301 assert_eq!(
4302 CssPropertyWithConditionsVec::parse_at_rule("lang(\"de-DE\")"),
4303 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4304 AzString::from_const_str("de-DE")
4305 ))])
4306 );
4307 assert_eq!(
4308 CssPropertyWithConditionsVec::parse_at_rule("lang de"),
4309 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4310 AzString::from_const_str("de")
4311 ))])
4312 );
4313 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang()"), None);
4314 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang(\"\")"), None);
4315
4316 assert_eq!(
4317 CssPropertyWithConditionsVec::parse_at_rule("prefers-reduced-motion"),
4318 Some(vec![DynamicSelector::PrefersReducedMotion(
4319 BoolCondition::True
4320 )])
4321 );
4322 assert_eq!(
4323 CssPropertyWithConditionsVec::parse_at_rule("high-contrast"),
4324 Some(vec![DynamicSelector::PrefersHighContrast(
4325 BoolCondition::True
4326 )])
4327 );
4328 }
4329
4330 #[cfg(feature = "parser")]
4331 #[test]
4332 fn parse_at_rule_container_named_and_sized() {
4333 assert_eq!(
4334 CssPropertyWithConditionsVec::parse_at_rule("container sidebar"),
4335 Some(vec![DynamicSelector::ContainerName(
4336 AzString::from_const_str("sidebar")
4337 )])
4338 );
4339 let conds = CssPropertyWithConditionsVec::parse_at_rule("container (min-width: 400px)")
4340 .expect("sized container must parse");
4341 assert_eq!(conds.len(), 1);
4342 match conds[0] {
4346 DynamicSelector::ContainerWidth(r) => {
4347 assert_eq!(r.min(), Some(400.0));
4348 assert_eq!(r.max(), None);
4349 }
4350 ref other => panic!("expected ContainerWidth, got {other:?}"),
4351 }
4352 let named = CssPropertyWithConditionsVec::parse_at_rule(
4353 "container sidebar (max-height: 200px)",
4354 )
4355 .expect("named + sized container must parse");
4356 assert_eq!(named.len(), 2);
4357 assert!(matches!(named[0], DynamicSelector::ContainerName(_)));
4358 assert!(matches!(named[1], DynamicSelector::ContainerHeight(_)));
4359 }
4360
4361 #[cfg(feature = "parser")]
4362 #[test]
4363 fn parse_at_rule_empty_and_garbage_is_none() {
4364 for s in [
4365 "",
4366 " ",
4367 "os",
4368 "os ",
4369 "os()",
4370 "os(notanos)",
4371 "media",
4372 "media ",
4373 "media (min-width: abc)",
4374 "theme",
4375 "container",
4376 "container ()",
4377 "nosuchrule",
4378 "\u{1F600}",
4379 ] {
4380 assert_eq!(
4381 CssPropertyWithConditionsVec::parse_at_rule(s),
4382 None,
4383 "{s:?} must not parse"
4384 );
4385 }
4386 }
4387
4388 #[cfg(feature = "parser")]
4389 #[test]
4390 fn parse_at_rule_huge_input_terminates() {
4391 assert_eq!(
4392 CssPropertyWithConditionsVec::parse_at_rule(&format!("os {}", "(".repeat(50_000))),
4393 None
4394 );
4395 assert_eq!(
4396 CssPropertyWithConditionsVec::parse_at_rule(&"z".repeat(500_000)),
4397 None
4398 );
4399 let long_lang = "e".repeat(100_000);
4401 assert_eq!(
4402 CssPropertyWithConditionsVec::parse_at_rule(&format!("lang {long_lang}")),
4403 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4404 AzString::from(long_lang)
4405 ))])
4406 );
4407 }
4408
4409 #[cfg(feature = "parser")]
4410 #[test]
4411 fn parse_media_query_media_types_and_dimensions() {
4412 assert_eq!(
4413 CssPropertyWithConditionsVec::parse_media_query("screen"),
4414 Some(vec![DynamicSelector::Media(MediaType::Screen)])
4415 );
4416 assert_eq!(
4417 CssPropertyWithConditionsVec::parse_media_query("print"),
4418 Some(vec![DynamicSelector::Media(MediaType::Print)])
4419 );
4420 assert_eq!(
4421 CssPropertyWithConditionsVec::parse_media_query("all"),
4422 Some(vec![DynamicSelector::Media(MediaType::All)])
4423 );
4424 let w = CssPropertyWithConditionsVec::parse_media_query("(min-width: 800px)")
4425 .expect("min-width must parse");
4426 assert_eq!(w.len(), 1);
4427 match w[0] {
4428 DynamicSelector::ViewportWidth(r) => {
4429 assert_eq!(r.min(), Some(800.0));
4430 assert_eq!(r.max(), None);
4431 }
4432 ref other => panic!("expected ViewportWidth, got {other:?}"),
4433 }
4434 }
4435
4436 #[cfg(feature = "parser")]
4437 #[test]
4438 fn parse_media_query_boundary_pixel_values() {
4439 let zero = CssPropertyWithConditionsVec::parse_media_query("(min-width: 0px)")
4441 .expect("0px must parse");
4442 match zero[0] {
4443 DynamicSelector::ViewportWidth(r) => assert_eq!(r.min(), Some(0.0)),
4444 ref other => panic!("expected ViewportWidth, got {other:?}"),
4445 }
4446 let neg = CssPropertyWithConditionsVec::parse_media_query("(max-height: -1px)")
4447 .expect("-1px must parse");
4448 match neg[0] {
4449 DynamicSelector::ViewportHeight(r) => assert_eq!(r.max(), Some(-1.0)),
4450 ref other => panic!("expected ViewportHeight, got {other:?}"),
4451 }
4452 assert_eq!(
4454 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800)"),
4455 None
4456 );
4457 assert_eq!(
4458 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800em)"),
4459 None
4460 );
4461 }
4462
4463 #[cfg(feature = "parser")]
4464 #[test]
4465 fn parse_media_query_empty_and_garbage_is_none() {
4466 for s in [
4467 "",
4468 " ",
4469 "(",
4470 ")",
4471 "()",
4472 "(:)",
4473 "(min-width)",
4474 "(min-width: )",
4475 "(nosuchfeature: 1px)",
4476 "SCREEN",
4477 "\u{1F600}",
4478 "(🦀: 1px)",
4479 ] {
4480 assert_eq!(
4481 CssPropertyWithConditionsVec::parse_media_query(s),
4482 None,
4483 "{s:?} must not parse"
4484 );
4485 }
4486 assert_eq!(
4487 CssPropertyWithConditionsVec::parse_media_query(&"(".repeat(100_000)),
4488 None
4489 );
4490 }
4491
4492 #[cfg(feature = "parser")]
4498 #[test]
4499 fn parse_media_query_nan_pixel_value_is_rejected() {
4500 assert_eq!(
4501 CssPropertyWithConditionsVec::parse_media_query("(min-width: NaNpx)"),
4502 None,
4503 "a NaN px value collapses into the 'no limit' sentinel and matches everything"
4504 );
4505 }
4506
4507 #[cfg(feature = "parser")]
4508 #[test]
4509 fn parse_media_query_infinite_pixel_value_never_matches() {
4510 let q = CssPropertyWithConditionsVec::parse_media_query("(min-width: infpx)")
4513 .expect("infpx currently parses");
4514 let ctx = DynamicSelectorContext::default().with_viewport(1e30, 1000.0);
4515 assert!(!q[0].matches(&ctx), "an infinite min-width can never be met");
4516 }
4517
4518 #[cfg(feature = "parser")]
4519 #[test]
4520 fn parse_media_feature_inline_known_features() {
4521 assert_eq!(
4522 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "PORTRAIT"),
4523 Some(DynamicSelector::Orientation(OrientationType::Portrait))
4524 );
4525 assert_eq!(
4526 CssPropertyWithConditionsVec::parse_media_feature_inline(
4527 "prefers-color-scheme",
4528 "Dark"
4529 ),
4530 Some(DynamicSelector::Theme(ThemeCondition::Dark))
4531 );
4532 assert_eq!(
4533 CssPropertyWithConditionsVec::parse_media_feature_inline(
4534 "prefers-reduced-motion",
4535 "reduce"
4536 ),
4537 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
4538 );
4539 assert_eq!(
4540 CssPropertyWithConditionsVec::parse_media_feature_inline(
4541 "prefers-reduced-motion",
4542 "no-preference"
4543 ),
4544 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
4545 );
4546 assert_eq!(
4547 CssPropertyWithConditionsVec::parse_media_feature_inline("prefers-contrast", "more"),
4548 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
4549 );
4550 assert_eq!(
4551 CssPropertyWithConditionsVec::parse_media_feature_inline(
4552 "prefers-high-contrast",
4553 "none"
4554 ),
4555 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
4556 );
4557 }
4558
4559 #[cfg(feature = "parser")]
4560 #[test]
4561 fn parse_media_feature_inline_rejects_unknown_keys_and_values() {
4562 assert_eq!(
4563 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", ""),
4564 None
4565 );
4566 assert_eq!(
4567 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "sideways"),
4568 None
4569 );
4570 assert_eq!(
4571 CssPropertyWithConditionsVec::parse_media_feature_inline("", ""),
4572 None
4573 );
4574 assert_eq!(
4575 CssPropertyWithConditionsVec::parse_media_feature_inline("nosuchkey", "dark"),
4576 None
4577 );
4578 assert_eq!(
4579 CssPropertyWithConditionsVec::parse_media_feature_inline(
4580 "prefers-color-scheme",
4581 "\u{1F600}"
4582 ),
4583 None
4584 );
4585 assert_eq!(
4587 CssPropertyWithConditionsVec::parse_media_feature_inline(" orientation", "portrait"),
4588 None
4589 );
4590 assert_eq!(
4591 CssPropertyWithConditionsVec::parse_media_feature_inline(
4592 &"k".repeat(200_000),
4593 &"v".repeat(200_000)
4594 ),
4595 None
4596 );
4597 }
4598
4599 #[cfg(feature = "parser")]
4600 #[test]
4601 fn parse_property_segment_valid_and_invalid() {
4602 let key_map = crate::props::property::CssKeyMap::get();
4603 let ok = CssPropertyWithConditionsVec::parse_property_segment("color: red", &[], &key_map)
4604 .expect("color: red must parse");
4605 assert_eq!(ok.len(), 1);
4606 assert!(!ok[0].is_conditional());
4607
4608 let inherited = vec![DynamicSelector::PseudoState(PseudoStateType::Hover)];
4610 let shorthand = CssPropertyWithConditionsVec::parse_property_segment(
4611 "padding: 10px",
4612 &inherited,
4613 &key_map,
4614 )
4615 .expect("padding shorthand must parse");
4616 assert!(shorthand.len() > 1, "padding must expand to >1 property");
4617 for p in &shorthand {
4618 assert_eq!(p.apply_if.as_slice(), inherited.as_slice());
4619 }
4620
4621 for s in ["", " ", "color", "color:", ": red", "nosuchprop: red", "\u{1F600}"] {
4622 assert!(
4623 CssPropertyWithConditionsVec::parse_property_segment(s, &[], &key_map).is_none(),
4624 "{s:?} must not parse"
4625 );
4626 }
4627 }
4628
4629 #[cfg(feature = "parser")]
4630 #[test]
4631 fn parse_block_segment_requires_balanced_braces() {
4632 let key_map = crate::props::property::CssKeyMap::get();
4633 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4635 "color: red",
4636 &[],
4637 &key_map
4638 )
4639 .is_none());
4640 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4642 ":hover { color: red",
4643 &[],
4644 &key_map
4645 )
4646 .is_none());
4647 assert!(
4649 CssPropertyWithConditionsVec::parse_block_segment("}{", &[], &key_map).is_none()
4650 );
4651 let empty = CssPropertyWithConditionsVec::parse_block_segment(
4653 ":hover {}",
4654 &[],
4655 &key_map,
4656 );
4657 assert!(empty.is_none());
4659 let ok = CssPropertyWithConditionsVec::parse_block_segment(
4661 ":hover { color: red; }",
4662 &[],
4663 &key_map,
4664 )
4665 .expect("valid block");
4666 assert_eq!(ok.len(), 1);
4667 assert!(ok[0].is_pseudo_state_only());
4668 }
4669
4670 #[cfg(feature = "parser")]
4671 #[test]
4672 fn parse_with_conditions_prepends_inherited_conditions() {
4673 let inherited = vec![DynamicSelector::Os(OsCondition::Linux)];
4674 let props =
4675 CssPropertyWithConditionsVec::parse_with_conditions("color: red;", &inherited)
4676 .into_library_owned_vec();
4677 assert_eq!(props.len(), 1);
4678 assert_eq!(props[0].apply_if.as_slice(), inherited.as_slice());
4679
4680 let none = CssPropertyWithConditionsVec::parse_with_conditions(" ", &inherited)
4682 .into_library_owned_vec();
4683 assert!(none.is_empty());
4684 }
4685
4686 #[cfg(feature = "parser")]
4687 #[test]
4688 fn parsed_media_selector_evaluates_against_a_context() {
4689 let props =
4692 CssPropertyWithConditionsVec::parse("@media (min-width: 800px) { color: red; }")
4693 .into_library_owned_vec();
4694 assert_eq!(props.len(), 1);
4695 let base = DynamicSelectorContext::default();
4696 assert!(props[0].matches(&base.with_viewport(1024.0, 768.0)));
4697 assert!(props[0].matches(&base.with_viewport(800.0, 600.0)));
4698 assert!(!props[0].matches(&base.with_viewport(799.0, 600.0)));
4699 }
4700}