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, PartialEq, PartialOrd)]
217pub struct MinMaxRange {
218 pub min: f32,
220 pub max: f32,
222}
223
224impl MinMaxRange {
225 #[must_use] pub const fn new(min: Option<f32>, max: Option<f32>) -> Self {
226 Self {
227 min: if let Some(m) = min { m } else { f32::NAN },
228 max: if let Some(m) = max { m } else { f32::NAN },
229 }
230 }
231
232 #[must_use] pub const fn with_min(min_val: f32) -> Self {
234 Self {
235 min: min_val,
236 max: f32::NAN,
237 }
238 }
239
240 #[must_use] pub const fn with_max(max_val: f32) -> Self {
242 Self {
243 min: f32::NAN,
244 max: max_val,
245 }
246 }
247
248 #[must_use] pub const fn min(&self) -> Option<f32> {
249 if self.min.is_nan() {
250 None
251 } else {
252 Some(self.min)
253 }
254 }
255
256 #[must_use] pub const fn max(&self) -> Option<f32> {
257 if self.max.is_nan() {
258 None
259 } else {
260 Some(self.max)
261 }
262 }
263
264 #[must_use] pub fn matches(&self, value: f32) -> bool {
265 let min_ok = self.min.is_nan() || value >= self.min;
266 let max_ok = self.max.is_nan() || value <= self.max;
267 min_ok && max_ok
268 }
269}
270
271#[repr(C)]
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
274pub enum BoolCondition {
275 #[default]
276 False,
277 True,
278}
279
280impl From<bool> for BoolCondition {
281 fn from(b: bool) -> Self {
282 if b {
283 Self::True
284 } else {
285 Self::False
286 }
287 }
288}
289
290impl From<BoolCondition> for bool {
291 fn from(b: BoolCondition) -> Self {
292 matches!(b, BoolCondition::True)
293 }
294}
295
296#[repr(C)]
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
299pub enum OsCondition {
300 Any,
301 Apple, MacOS,
303 IOS,
304 Linux,
305 Windows,
306 Android,
307 Web, }
309
310impl_option!(
311 OsCondition,
312 OptionOsCondition,
313 [Debug, Clone, Copy, PartialEq, Eq, Hash]
314);
315
316impl OsCondition {
317 #[must_use] pub const fn from_system_platform(platform: &crate::system::Platform) -> Self {
319 use crate::system::Platform;
320 match platform {
321 Platform::Windows => Self::Windows,
322 Platform::MacOs => Self::MacOS,
323 Platform::Linux(_) => Self::Linux,
324 Platform::Android => Self::Android,
325 Platform::Ios => Self::IOS,
326 Platform::Unknown => Self::Any,
327 }
328 }
329}
330
331#[repr(C, u8)]
332#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
333pub enum OsVersionCondition {
334 Min(OsVersion),
337 Max(OsVersion),
339 Exact(OsVersion),
341 DesktopEnvironment(LinuxDesktopEnv),
343 DesktopEnvMin(DesktopEnvVersion),
345 DesktopEnvMax(DesktopEnvVersion),
347 DesktopEnvExact(DesktopEnvVersion),
349}
350
351#[repr(C)]
354#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
355pub struct DesktopEnvVersion {
356 pub env: LinuxDesktopEnv,
357 pub version_id: u32,
358}
359
360#[repr(C)]
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
366pub struct OsVersion {
367 pub os: OsFamily,
369 pub version_id: u32,
372}
373
374impl Default for OsVersion {
375 fn default() -> Self {
376 Self::unknown()
377 }
378}
379
380impl OsVersion {
381 #[must_use] pub const fn new(os: OsFamily, version_id: u32) -> Self {
382 Self { os, version_id }
383 }
384
385 #[must_use] pub fn compare(&self, other: &Self) -> Option<core::cmp::Ordering> {
388 if self.os == other.os {
389 Some(self.version_id.cmp(&other.version_id))
390 } else {
391 None }
393 }
394
395 #[must_use] pub fn is_at_least(&self, other: &Self) -> bool {
397 self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Less)
398 }
399
400 #[must_use] pub fn is_at_most(&self, other: &Self) -> bool {
402 self.compare(other).is_some_and(|o| o != core::cmp::Ordering::Greater)
403 }
404}
405
406impl_option!(
407 OsVersion,
408 OptionOsVersion,
409 [Debug, Clone, Copy, PartialEq, Eq, Hash]
410);
411
412impl OsVersion {
413
414 #[must_use] pub fn is_exactly(&self, other: &Self) -> bool {
416 self.compare(other) == Some(core::cmp::Ordering::Equal)
417 }
418}
419
420#[repr(C)]
422#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
423pub enum OsFamily {
424 Windows,
425 MacOS,
426 IOS,
427 Linux,
428 Android,
429}
430
431impl OsVersion {
437 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);
492 pub const IOS_2: Self = Self::new(OsFamily::IOS, 200);
493 pub const IOS_3: Self = Self::new(OsFamily::IOS, 300);
494 pub const IOS_4: Self = Self::new(OsFamily::IOS, 400);
495 pub const IOS_5: Self = Self::new(OsFamily::IOS, 500);
496 pub const IOS_6: Self = Self::new(OsFamily::IOS, 600);
497 pub const IOS_7: Self = Self::new(OsFamily::IOS, 700);
498 pub const IOS_8: Self = Self::new(OsFamily::IOS, 800);
499 pub const IOS_9: Self = Self::new(OsFamily::IOS, 900);
500 pub const IOS_10: Self = Self::new(OsFamily::IOS, 1000);
501 pub const IOS_11: Self = Self::new(OsFamily::IOS, 1100);
502 pub const IOS_12: Self = Self::new(OsFamily::IOS, 1200);
503 pub const IOS_13: Self = Self::new(OsFamily::IOS, 1300);
504 pub const IOS_14: Self = Self::new(OsFamily::IOS, 1400);
505 pub const IOS_15: Self = Self::new(OsFamily::IOS, 1500);
506 pub const IOS_16: Self = Self::new(OsFamily::IOS, 1600);
507 pub const IOS_17: Self = Self::new(OsFamily::IOS, 1700);
508 pub const IOS_18: Self = Self::new(OsFamily::IOS, 1800);
509
510 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);
535 pub const LINUX_3_0: Self = Self::new(OsFamily::Linux, 3000);
536 pub const LINUX_4_0: Self = Self::new(OsFamily::Linux, 4000);
537 pub const LINUX_5_0: Self = Self::new(OsFamily::Linux, 5000);
538 pub const LINUX_6_0: Self = Self::new(OsFamily::Linux, 6000);
539
540 #[must_use] pub const fn unknown() -> Self {
542 Self {
543 os: OsFamily::Linux, version_id: 0,
545 }
546 }
547}
548
549#[must_use] pub fn parse_os_version(os: OsFamily, version_str: &str) -> Option<OsVersion> {
552 let version_str = version_str.trim().to_lowercase();
553 let version_str = version_str.as_str();
554
555 match os {
556 OsFamily::Windows => parse_windows_version(version_str),
557 OsFamily::MacOS => parse_macos_version(version_str),
558 OsFamily::IOS => parse_ios_version(version_str),
559 OsFamily::Android => parse_android_version(version_str),
560 OsFamily::Linux => parse_linux_version(version_str),
561 }
562}
563
564fn parse_windows_version(s: &str) -> Option<OsVersion> {
565 let core = strip_os_prefix(s, &["windows", "win"]);
568 match core {
569 "2000" | "5.0" | "nt5.0" => Some(OsVersion::WIN_2000),
571 "xp" | "5.1" | "nt5.1" => Some(OsVersion::WIN_XP),
572 "vista" | "6.0" | "nt6.0" => Some(OsVersion::WIN_VISTA),
573 "7" | "6.1" | "nt6.1" => Some(OsVersion::WIN_7),
574 "8" | "6.2" | "nt6.2" => Some(OsVersion::WIN_8),
575 "8.1" | "8-1" | "6.3" | "nt6.3" => Some(OsVersion::WIN_8_1),
576 "10" | "10.0" | "nt10.0" => Some(OsVersion::WIN_10),
577 "11" => Some(OsVersion::WIN_11),
578 _ => None,
579 }
580}
581
582fn strip_os_prefix<'a>(s: &'a str, prefixes: &[&str]) -> &'a str {
586 for p in prefixes {
587 if let Some(rest) = s.strip_prefix(p) {
588 return rest.strip_prefix(['-', '_']).unwrap_or(rest);
589 }
590 }
591 s
592}
593
594fn parse_macos_version(s: &str) -> Option<OsVersion> {
595 match s {
596 "cheetah" | "10.0" => Some(OsVersion::MACOS_CHEETAH),
597 "puma" | "10.1" => Some(OsVersion::MACOS_PUMA),
598 "jaguar" | "10.2" => Some(OsVersion::MACOS_JAGUAR),
599 "panther" | "10.3" => Some(OsVersion::MACOS_PANTHER),
600 "tiger" | "10.4" => Some(OsVersion::MACOS_TIGER),
601 "leopard" | "10.5" => Some(OsVersion::MACOS_LEOPARD),
602 "snow-leopard" | "snowleopard" | "10.6" => Some(OsVersion::MACOS_SNOW_LEOPARD),
603 "lion" | "10.7" => Some(OsVersion::MACOS_LION),
604 "mountain-lion" | "mountainlion" | "10.8" => Some(OsVersion::MACOS_MOUNTAIN_LION),
605 "mavericks" | "10.9" => Some(OsVersion::MACOS_MAVERICKS),
606 "yosemite" | "10.10" => Some(OsVersion::MACOS_YOSEMITE),
607 "el-capitan" | "elcapitan" | "10.11" => Some(OsVersion::MACOS_EL_CAPITAN),
608 "sierra" | "10.12" => Some(OsVersion::MACOS_SIERRA),
609 "high-sierra" | "highsierra" | "10.13" => Some(OsVersion::MACOS_HIGH_SIERRA),
610 "mojave" | "10.14" => Some(OsVersion::MACOS_MOJAVE),
611 "catalina" | "10.15" => Some(OsVersion::MACOS_CATALINA),
612 "big-sur" | "bigsur" | "11" | "11.0" => Some(OsVersion::MACOS_BIG_SUR),
613 "monterey" | "12" | "12.0" => Some(OsVersion::MACOS_MONTEREY),
614 "ventura" | "13" | "13.0" => Some(OsVersion::MACOS_VENTURA),
615 "sonoma" | "14" | "14.0" => Some(OsVersion::MACOS_SONOMA),
616 "sequoia" | "15" | "15.0" => Some(OsVersion::MACOS_SEQUOIA),
617 "tahoe" | "26" | "26.0" => Some(OsVersion::MACOS_TAHOE),
618 _ => None,
619 }
620}
621
622fn parse_ios_version(s: &str) -> Option<OsVersion> {
623 match s {
624 "1" | "1.0" => Some(OsVersion::IOS_1),
625 "2" | "2.0" => Some(OsVersion::IOS_2),
626 "3" | "3.0" => Some(OsVersion::IOS_3),
627 "4" | "4.0" => Some(OsVersion::IOS_4),
628 "5" | "5.0" => Some(OsVersion::IOS_5),
629 "6" | "6.0" => Some(OsVersion::IOS_6),
630 "7" | "7.0" => Some(OsVersion::IOS_7),
631 "8" | "8.0" => Some(OsVersion::IOS_8),
632 "9" | "9.0" => Some(OsVersion::IOS_9),
633 "10" | "10.0" => Some(OsVersion::IOS_10),
634 "11" | "11.0" => Some(OsVersion::IOS_11),
635 "12" | "12.0" => Some(OsVersion::IOS_12),
636 "13" | "13.0" => Some(OsVersion::IOS_13),
637 "14" | "14.0" => Some(OsVersion::IOS_14),
638 "15" | "15.0" => Some(OsVersion::IOS_15),
639 "16" | "16.0" => Some(OsVersion::IOS_16),
640 "17" | "17.0" => Some(OsVersion::IOS_17),
641 "18" | "18.0" => Some(OsVersion::IOS_18),
642 _ => None,
643 }
644}
645
646fn parse_android_version(s: &str) -> Option<OsVersion> {
647 match s {
648 "cupcake" | "1.5" => Some(OsVersion::ANDROID_CUPCAKE),
649 "donut" | "1.6" => Some(OsVersion::ANDROID_DONUT),
650 "eclair" | "2.1" => Some(OsVersion::ANDROID_ECLAIR),
651 "froyo" | "2.2" => Some(OsVersion::ANDROID_FROYO),
652 "gingerbread" | "2.3" => Some(OsVersion::ANDROID_GINGERBREAD),
653 "honeycomb" | "3.0" | "3.2" => Some(OsVersion::ANDROID_HONEYCOMB),
654 "ice-cream-sandwich" | "ics" | "4.0" => Some(OsVersion::ANDROID_ICE_CREAM_SANDWICH),
655 "jelly-bean" | "jellybean" | "4.3" => Some(OsVersion::ANDROID_JELLY_BEAN),
656 "kitkat" | "4.4" => Some(OsVersion::ANDROID_KITKAT),
657 "lollipop" | "5.0" | "5.1" => Some(OsVersion::ANDROID_LOLLIPOP),
658 "marshmallow" | "6.0" => Some(OsVersion::ANDROID_MARSHMALLOW),
659 "nougat" | "7.0" | "7.1" => Some(OsVersion::ANDROID_NOUGAT),
660 "oreo" | "8.0" | "8.1" => Some(OsVersion::ANDROID_OREO),
661 "pie" | "9" | "9.0" => Some(OsVersion::ANDROID_PIE),
662 "10" | "q" => Some(OsVersion::ANDROID_10),
663 "11" | "r" => Some(OsVersion::ANDROID_11),
664 "12" | "s" => Some(OsVersion::ANDROID_12),
665 "12l" | "12L" => Some(OsVersion::ANDROID_12L),
666 "13" | "t" | "tiramisu" => Some(OsVersion::ANDROID_13),
667 "14" | "u" | "upside-down-cake" => Some(OsVersion::ANDROID_14),
668 "15" | "v" | "vanilla-ice-cream" => Some(OsVersion::ANDROID_15),
669 _ => {
670 if let Some(api) = s.strip_prefix("api") {
672 if let Ok(level) = api.trim().parse::<u32>() {
673 return Some(OsVersion::new(OsFamily::Android, level));
674 }
675 }
676 None
677 }
678 }
679}
680
681fn parse_linux_version(s: &str) -> Option<OsVersion> {
682 let s = strip_os_prefix(s, &["linux"]);
684 let mut parts = s.split('.');
686 let major = parts.next()?.parse::<u32>().ok()?;
687 let minor = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
688 let patch = parts.next().map_or(Some(0), |p| p.parse::<u32>().ok())?;
689 Some(OsVersion::new(OsFamily::Linux, major * 1000 + minor * 10 + patch))
690}
691
692#[repr(C)]
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
699pub enum LinuxDesktopEnv {
700 Gnome,
701 KDE,
702 XFCE,
704 Unity,
706 Cinnamon,
708 MATE,
710 Other,
711}
712
713impl LinuxDesktopEnv {
714 #[must_use] pub const fn from_system_desktop_env(de: &crate::system::DesktopEnvironment) -> Self {
716 use crate::system::DesktopEnvironment;
717 match de {
718 DesktopEnvironment::Gnome => Self::Gnome,
719 DesktopEnvironment::Kde => Self::KDE,
720 DesktopEnvironment::Other(_) => Self::Other,
721 }
722 }
723}
724
725#[repr(C)]
727#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
728pub enum MediaType {
729 Screen,
730 Print,
731 All,
732}
733#[allow(variant_size_differences)] #[repr(C, u8)]
735#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
736pub enum ThemeCondition {
737 Light,
738 Dark,
739 Custom(AzString),
740 SystemPreferred,
742}
743
744impl_option!(
745 ThemeCondition,
746 OptionThemeCondition,
747 copy = false,
748 [Debug, Clone, PartialEq, Eq, Hash]
749);
750
751impl ThemeCondition {
752 #[must_use] pub const fn from_system_theme(theme: crate::system::Theme) -> Self {
754 use crate::system::Theme;
755 match theme {
756 Theme::Light => Self::Light,
757 Theme::Dark => Self::Dark,
758 }
759 }
760}
761
762#[repr(C)]
764#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
765pub enum OrientationType {
766 Portrait,
767 Landscape,
768}
769
770#[repr(C, u8)]
773#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
774pub enum LanguageCondition {
775 Exact(AzString),
777 Prefix(AzString),
779}
780
781impl LanguageCondition {
782 #[must_use] pub fn matches(&self, language: &str) -> bool {
784 match self {
785 Self::Exact(lang) => language.eq_ignore_ascii_case(lang.as_str()),
786 Self::Prefix(prefix) => {
787 let prefix_str = prefix.as_str();
788 if language.len() < prefix_str.len() {
789 return false;
790 }
791 let lang_prefix = &language[..prefix_str.len()];
793 if !lang_prefix.eq_ignore_ascii_case(prefix_str) {
794 return false;
795 }
796 language.len() == prefix_str.len()
798 || language.as_bytes().get(prefix_str.len()) == Some(&b'-')
799 }
800 }
801 }
802}
803
804#[repr(C)]
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
806pub enum PseudoStateType {
807 Normal,
809 Hover,
811 Active,
813 Focus,
815 Disabled,
817 CheckedTrue,
819 CheckedFalse,
821 FocusWithin,
823 Visited,
825 Backdrop,
827 Dragging,
829 DragOver,
831}
832
833impl_option!(
834 LinuxDesktopEnv,
835 OptionLinuxDesktopEnv,
836 [Debug, Clone, Copy, PartialEq, Eq, Hash]
837);
838
839pub const DEFAULT_VIEWPORT_WIDTH: f32 = 800.0;
841pub const DEFAULT_VIEWPORT_HEIGHT: f32 = 600.0;
843
844#[repr(C)]
846#[derive(Debug, Clone)]
847pub struct DynamicSelectorContext {
848 pub os: OsCondition,
850 pub os_version: OsVersion,
851 pub desktop_env: OptionLinuxDesktopEnv,
852 pub de_version: u32,
857
858 pub theme: ThemeCondition,
860
861 pub media_type: MediaType,
863 pub viewport_width: f32,
864 pub viewport_height: f32,
865
866 pub container_width: f32,
869 pub container_height: f32,
870 pub container_name: OptionString,
871
872 pub prefers_reduced_motion: BoolCondition,
874 pub prefers_high_contrast: BoolCondition,
875
876 pub orientation: OrientationType,
878
879 pub pseudo_state: PseudoStateFlags,
881
882 pub language: AzString,
884
885 pub window_focused: bool,
888}
889
890impl Default for DynamicSelectorContext {
891 fn default() -> Self {
892 Self {
893 os: OsCondition::Any,
894 os_version: OsVersion::unknown(),
895 desktop_env: OptionLinuxDesktopEnv::None,
896 de_version: 0,
897 theme: ThemeCondition::Light,
898 media_type: MediaType::Screen,
899 viewport_width: DEFAULT_VIEWPORT_WIDTH,
900 viewport_height: DEFAULT_VIEWPORT_HEIGHT,
901 container_width: f32::NAN,
902 container_height: f32::NAN,
903 container_name: OptionString::None,
904 prefers_reduced_motion: BoolCondition::False,
905 prefers_high_contrast: BoolCondition::False,
906 orientation: OrientationType::Landscape,
907 pseudo_state: PseudoStateFlags::default(),
908 language: AzString::from_const_str("en-US"),
909 window_focused: true,
910 }
911 }
912}
913
914impl DynamicSelectorContext {
915 #[must_use] pub fn from_system_style(system_style: &crate::system::SystemStyle) -> Self {
917 let os = OsCondition::from_system_platform(&system_style.platform);
918 let desktop_env = if let crate::system::Platform::Linux(de) = &system_style.platform {
919 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::from_system_desktop_env(de))
920 } else {
921 OptionLinuxDesktopEnv::None
922 };
923 let theme = ThemeCondition::from_system_theme(system_style.theme);
924
925 Self {
926 os,
927 os_version: system_style.os_version, desktop_env,
929 de_version: 0, theme,
931 media_type: MediaType::Screen,
932 viewport_width: DEFAULT_VIEWPORT_WIDTH, viewport_height: DEFAULT_VIEWPORT_HEIGHT,
934 container_width: f32::NAN,
935 container_height: f32::NAN,
936 container_name: OptionString::None,
937 prefers_reduced_motion: system_style.prefers_reduced_motion,
938 prefers_high_contrast: system_style.prefers_high_contrast,
939 orientation: OrientationType::Landscape,
940 pseudo_state: PseudoStateFlags::default(),
941 language: system_style.language.clone(),
942 window_focused: true,
943 }
944 }
945
946 #[must_use] pub fn with_viewport(&self, width: f32, height: f32) -> Self {
948 let mut ctx = self.clone();
949 ctx.viewport_width = width;
950 ctx.viewport_height = height;
951 ctx.orientation = if width > height {
952 OrientationType::Landscape
953 } else {
954 OrientationType::Portrait
955 };
956 ctx
957 }
958
959 #[must_use] pub fn with_container(&self, width: f32, height: f32, name: Option<AzString>) -> Self {
961 let mut ctx = self.clone();
962 ctx.container_width = width;
963 ctx.container_height = height;
964 ctx.container_name = name.into();
965 ctx
966 }
967
968 #[must_use] pub fn with_pseudo_state(&self, state: PseudoStateFlags) -> Self {
970 let mut ctx = self.clone();
971 ctx.pseudo_state = state;
972 ctx
973 }
974
975 #[must_use] pub fn viewport_breakpoint_changed(&self, other: &Self, breakpoints: &[f32]) -> bool {
977 for bp in breakpoints {
978 let self_above = self.viewport_width >= *bp;
979 let other_above = other.viewport_width >= *bp;
980 if self_above != other_above {
981 return true;
982 }
983 }
984 false
985 }
986}
987
988impl DynamicSelector {
989 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
991 match self {
992 Self::Os(os) => Self::match_os(*os, ctx.os),
993 Self::OsVersion(ver) => Self::match_os_version(ver, ctx.os_version, ctx.desktop_env, ctx.de_version),
994 Self::Media(media) => *media == ctx.media_type || *media == MediaType::All,
995 Self::ViewportWidth(range) => range.matches(ctx.viewport_width),
996 Self::ViewportHeight(range) => range.matches(ctx.viewport_height),
997 Self::ContainerWidth(range) => {
998 !ctx.container_width.is_nan() && range.matches(ctx.container_width)
999 }
1000 Self::ContainerHeight(range) => {
1001 !ctx.container_height.is_nan() && range.matches(ctx.container_height)
1002 }
1003 Self::ContainerName(name) => ctx.container_name.as_ref() == Some(name),
1004 Self::Theme(theme) => Self::match_theme(theme, &ctx.theme),
1005 Self::AspectRatio(range) => {
1006 let ratio = ctx.viewport_width / ctx.viewport_height.max(1.0);
1007 range.matches(ratio)
1008 }
1009 Self::Orientation(orient) => *orient == ctx.orientation,
1010 Self::PrefersReducedMotion(pref) => {
1011 bool::from(*pref) == bool::from(ctx.prefers_reduced_motion)
1012 }
1013 Self::PrefersHighContrast(pref) => {
1014 bool::from(*pref) == bool::from(ctx.prefers_high_contrast)
1015 }
1016 Self::PseudoState(state) => Self::match_pseudo_state(*state, ctx),
1017 Self::Language(lang_cond) => lang_cond.matches(ctx.language.as_str()),
1018 }
1019 }
1020
1021 fn match_os(condition: OsCondition, actual: OsCondition) -> bool {
1022 match condition {
1023 OsCondition::Any => true,
1024 OsCondition::Apple => matches!(actual, OsCondition::MacOS | OsCondition::IOS),
1025 _ => condition == actual,
1026 }
1027 }
1028
1029 fn match_os_version(
1030 condition: &OsVersionCondition,
1031 actual: OsVersion,
1032 desktop_env: OptionLinuxDesktopEnv,
1033 de_version: u32,
1034 ) -> bool {
1035 let de_matches = |env: &LinuxDesktopEnv| desktop_env.as_ref() == Some(env);
1038 match condition {
1039 OsVersionCondition::Exact(ver) => actual.is_exactly(ver),
1040 OsVersionCondition::Min(ver) => actual.is_at_least(ver),
1041 OsVersionCondition::Max(ver) => actual.is_at_most(ver),
1042 OsVersionCondition::DesktopEnvironment(env) => de_matches(env),
1043 OsVersionCondition::DesktopEnvMin(d) =>
1044 de_matches(&d.env) && de_version != 0 && de_version >= d.version_id,
1045 OsVersionCondition::DesktopEnvMax(d) =>
1046 de_matches(&d.env) && de_version != 0 && de_version <= d.version_id,
1047 OsVersionCondition::DesktopEnvExact(d) =>
1048 de_matches(&d.env) && de_version == d.version_id,
1049 }
1050 }
1051
1052 fn match_theme(condition: &ThemeCondition, actual: &ThemeCondition) -> bool {
1053 match (condition, actual) {
1054 (ThemeCondition::SystemPreferred, _) => true,
1055 _ => condition == actual,
1056 }
1057 }
1058
1059 const fn match_pseudo_state(state: PseudoStateType, ctx: &DynamicSelectorContext) -> bool {
1060 let node_state = &ctx.pseudo_state;
1061 match state {
1062 PseudoStateType::Normal => true, PseudoStateType::Hover => node_state.hover,
1064 PseudoStateType::Active => node_state.active,
1065 PseudoStateType::Focus => node_state.focused,
1066 PseudoStateType::Disabled => node_state.disabled,
1067 PseudoStateType::CheckedTrue => node_state.checked,
1068 PseudoStateType::CheckedFalse => !node_state.checked,
1069 PseudoStateType::FocusWithin => node_state.focus_within,
1070 PseudoStateType::Visited => node_state.visited,
1071 PseudoStateType::Backdrop => node_state.backdrop,
1072 PseudoStateType::Dragging => node_state.dragging,
1073 PseudoStateType::DragOver => node_state.drag_over,
1074 }
1075 }
1076}
1077
1078#[cfg(feature = "parser")]
1092#[must_use] pub fn parse_os_at_rule_content(content: &str) -> Option<Vec<DynamicSelector>> {
1093 let trimmed = content.trim();
1094 let inner = trimmed
1095 .strip_prefix('(').and_then(|s| s.strip_suffix(')'))
1096 .unwrap_or(trimmed)
1097 .trim();
1098 let inner = inner
1099 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1100 .or_else(|| inner.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1101 .unwrap_or(inner)
1102 .trim();
1103 if inner.is_empty() {
1104 return None;
1105 }
1106
1107 let (subject, op_and_version) = split_op_and_version(inner);
1109 let subject = subject.trim();
1110
1111 let (family_str, de_str) = match subject.split_once(':') {
1113 Some((f, d)) => (f.trim(), Some(d.trim())),
1114 None => (subject, None),
1115 };
1116
1117 let family = parse_os_family_token(family_str)?;
1118 let de = match de_str {
1119 Some(s) if !s.is_empty() => Some(parse_de_token(s)),
1120 _ => None,
1121 };
1122
1123 let mut out = Vec::new();
1124 out.push(DynamicSelector::Os(family));
1128
1129 match (de, op_and_version) {
1130 (Some(env), None) => {
1132 out.push(DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(env)));
1133 }
1134 (Some(env), Some((op, ver_str))) => {
1136 let v: u32 = ver_str.parse().ok()?;
1137 let dev = DesktopEnvVersion { env, version_id: v };
1138 let cond = match op {
1139 VersionOp::Min => OsVersionCondition::DesktopEnvMin(dev),
1140 VersionOp::Max => OsVersionCondition::DesktopEnvMax(dev),
1141 VersionOp::Exact => OsVersionCondition::DesktopEnvExact(dev),
1142 };
1143 out.push(DynamicSelector::OsVersion(cond));
1144 }
1145 (None, Some((op, ver_str))) => {
1147 let os_family = match family {
1148 OsCondition::Linux => OsFamily::Linux,
1149 OsCondition::Windows => OsFamily::Windows,
1150 OsCondition::MacOS => OsFamily::MacOS,
1151 OsCondition::IOS => OsFamily::IOS,
1152 OsCondition::Android => OsFamily::Android,
1153 _ => return None,
1155 };
1156 let version = parse_os_version(os_family, ver_str)?;
1157 let cond = match op {
1158 VersionOp::Min => OsVersionCondition::Min(version),
1159 VersionOp::Max => OsVersionCondition::Max(version),
1160 VersionOp::Exact => OsVersionCondition::Exact(version),
1161 };
1162 out.push(DynamicSelector::OsVersion(cond));
1163 }
1164 (None, None) => {}
1166 }
1167
1168 Some(out)
1169}
1170
1171#[cfg(feature = "parser")]
1172#[derive(Copy, Clone)]
1173enum VersionOp { Min, Max, Exact }
1174
1175#[cfg(feature = "parser")]
1178fn split_op_and_version(s: &str) -> (&str, Option<(VersionOp, &str)>) {
1179 let candidates: &[(&str, VersionOp)] = &[
1181 (">=", VersionOp::Min),
1182 ("<=", VersionOp::Max),
1183 ("=", VersionOp::Exact),
1184 (">", VersionOp::Min),
1185 ("<", VersionOp::Max),
1186 ];
1187 let mut best: Option<(usize, usize, VersionOp)> = None;
1188 for (op_str, op) in candidates {
1189 if let Some(pos) = s.find(op_str) {
1190 let len = op_str.len();
1191 best = Some(match best {
1192 None => (pos, len, *op),
1193 Some((bp, bl, _)) if pos < bp || (pos == bp && len > bl) => (pos, len, *op),
1194 Some(b) => b,
1195 });
1196 }
1197 }
1198 match best {
1199 Some((pos, len, op)) => (&s[..pos], Some((op, s[pos + len..].trim()))),
1200 None => (s, None),
1201 }
1202}
1203
1204#[cfg(feature = "parser")]
1205fn parse_os_family_token(s: &str) -> Option<OsCondition> {
1206 match s.to_lowercase().as_str() {
1207 "linux" => Some(OsCondition::Linux),
1208 "windows" | "win" => Some(OsCondition::Windows),
1209 "macos" | "mac" | "osx" => Some(OsCondition::MacOS),
1210 "ios" => Some(OsCondition::IOS),
1211 "android" => Some(OsCondition::Android),
1212 "apple" => Some(OsCondition::Apple),
1213 "web" | "wasm" => Some(OsCondition::Web),
1214 "any" | "all" | "*" => Some(OsCondition::Any),
1215 _ => None,
1216 }
1217}
1218
1219#[cfg(feature = "parser")]
1220fn parse_de_token(s: &str) -> LinuxDesktopEnv {
1221 match s.to_lowercase().as_str() {
1222 "gnome" => LinuxDesktopEnv::Gnome,
1223 "kde" => LinuxDesktopEnv::KDE,
1224 "xfce" => LinuxDesktopEnv::XFCE,
1225 "unity" => LinuxDesktopEnv::Unity,
1226 "cinnamon" => LinuxDesktopEnv::Cinnamon,
1227 "mate" => LinuxDesktopEnv::MATE,
1228 _ => LinuxDesktopEnv::Other,
1229 }
1230}
1231
1232#[repr(C)]
1242#[derive(Debug, Clone, PartialEq)]
1243pub struct CssPropertyWithConditions {
1244 pub property: CssProperty,
1246 pub apply_if: DynamicSelectorVec,
1249}
1250
1251impl_option!(
1252 CssPropertyWithConditions,
1253 OptionCssPropertyWithConditions,
1254 copy = false,
1255 [Debug, Clone, PartialEq, Eq, PartialOrd]
1256);
1257
1258impl Eq for CssPropertyWithConditions {}
1259
1260impl PartialOrd for CssPropertyWithConditions {
1261 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
1262 Some(self.cmp(other))
1263 }
1264}
1265
1266impl Ord for CssPropertyWithConditions {
1267 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1268 self.property
1274 .cmp(&other.property)
1275 .then_with(|| self.apply_if.as_slice().cmp(other.apply_if.as_slice()))
1276 }
1277}
1278
1279impl CssPropertyWithConditions {
1280 #[must_use] pub const fn simple(property: CssProperty) -> Self {
1282 Self {
1283 property,
1284 apply_if: DynamicSelectorVec::from_const_slice(&[]),
1285 }
1286 }
1287
1288 #[must_use] pub const fn with_single_condition(
1290 property: CssProperty,
1291 conditions: &'static [DynamicSelector],
1292 ) -> Self {
1293 Self {
1294 property,
1295 apply_if: DynamicSelectorVec::from_const_slice(conditions),
1296 }
1297 }
1298
1299 #[must_use] pub fn with_condition(property: CssProperty, condition: DynamicSelector) -> Self {
1301 Self {
1302 property,
1303 apply_if: DynamicSelectorVec::from_vec(vec![condition]),
1304 }
1305 }
1306
1307 #[must_use] pub const fn with_conditions(property: CssProperty, conditions: DynamicSelectorVec) -> Self {
1309 Self {
1310 property,
1311 apply_if: conditions,
1312 }
1313 }
1314
1315 #[must_use] pub const fn on_hover(property: CssProperty) -> Self {
1317 Self::with_single_condition(
1318 property,
1319 &[DynamicSelector::PseudoState(PseudoStateType::Hover)],
1320 )
1321 }
1322
1323 #[must_use] pub const fn on_active(property: CssProperty) -> Self {
1325 Self::with_single_condition(
1326 property,
1327 &[DynamicSelector::PseudoState(PseudoStateType::Active)],
1328 )
1329 }
1330
1331 #[must_use] pub const fn on_focus(property: CssProperty) -> Self {
1333 Self::with_single_condition(
1334 property,
1335 &[DynamicSelector::PseudoState(PseudoStateType::Focus)],
1336 )
1337 }
1338
1339 #[must_use] pub const fn when_disabled(property: CssProperty) -> Self {
1341 Self::with_single_condition(
1342 property,
1343 &[DynamicSelector::PseudoState(PseudoStateType::Disabled)],
1344 )
1345 }
1346
1347 #[must_use] pub fn on_os(property: CssProperty, os: OsCondition) -> Self {
1349 Self::with_condition(property, DynamicSelector::Os(os))
1350 }
1351
1352 #[must_use] pub const fn dark_theme(property: CssProperty) -> Self {
1354 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Dark)])
1355 }
1356
1357 #[must_use] pub const fn light_theme(property: CssProperty) -> Self {
1359 Self::with_single_condition(property, &[DynamicSelector::Theme(ThemeCondition::Light)])
1360 }
1361
1362 #[must_use] pub const fn on_windows(property: CssProperty) -> Self {
1364 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Windows)])
1365 }
1366
1367 #[must_use] pub const fn on_macos(property: CssProperty) -> Self {
1369 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::MacOS)])
1370 }
1371
1372 #[must_use] pub const fn on_linux(property: CssProperty) -> Self {
1374 Self::with_single_condition(property, &[DynamicSelector::Os(OsCondition::Linux)])
1375 }
1376
1377 #[must_use] pub fn matches(&self, ctx: &DynamicSelectorContext) -> bool {
1379 if self.apply_if.as_slice().is_empty() {
1381 return true;
1382 }
1383
1384 self.apply_if
1386 .as_slice()
1387 .iter()
1388 .all(|selector| selector.matches(ctx))
1389 }
1390
1391 #[must_use] pub fn is_conditional(&self) -> bool {
1393 !self.apply_if.as_slice().is_empty()
1394 }
1395
1396 #[must_use] pub fn is_pseudo_state_only(&self) -> bool {
1399 let conditions = self.apply_if.as_slice();
1400 !conditions.is_empty()
1401 && conditions
1402 .iter()
1403 .all(|c| matches!(c, DynamicSelector::PseudoState(_)))
1404 }
1405
1406 #[must_use] pub const fn is_layout_affecting(&self) -> bool {
1412 self.property.get_type().can_trigger_relayout()
1413 }
1414}
1415
1416impl_vec!(CssPropertyWithConditions, CssPropertyWithConditionsVec, CssPropertyWithConditionsVecDestructor, CssPropertyWithConditionsVecDestructorType, CssPropertyWithConditionsVecSlice, OptionCssPropertyWithConditions);
1417impl_vec_debug!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1418impl_vec_partialeq!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1419impl_vec_partialord!(CssPropertyWithConditions, CssPropertyWithConditionsVec);
1420impl_vec_clone!(
1421 CssPropertyWithConditions,
1422 CssPropertyWithConditionsVec,
1423 CssPropertyWithConditionsVecDestructor
1424);
1425
1426impl Eq for CssPropertyWithConditionsVec {}
1428
1429impl Ord for CssPropertyWithConditionsVec {
1430 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
1431 self.as_slice().cmp(other.as_slice())
1434 }
1435}
1436
1437impl core::hash::Hash for CssPropertyWithConditions {
1438 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1439 self.property.hash(state);
1440 self.apply_if.as_slice().hash(state);
1444 }
1445}
1446
1447impl core::hash::Hash for CssPropertyWithConditionsVec {
1448 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
1449 self.as_slice().hash(state);
1451 }
1452}
1453
1454impl CssPropertyWithConditionsVec {
1455 #[cfg(feature = "parser")]
1475 #[must_use] pub fn parse(style: &str) -> Self {
1476 Self::parse_with_conditions(style, &[])
1477 }
1478
1479 #[cfg(feature = "parser")]
1481 fn parse_with_conditions(style: &str, inherited_conditions: &[DynamicSelector]) -> Self {
1482 use crate::props::property::{
1483 parse_combined_css_property, parse_css_property, CombinedCssPropertyType, CssKeyMap,
1484 CssPropertyType,
1485 };
1486
1487 let mut props = Vec::new();
1488 let key_map = CssKeyMap::get();
1489 let style = style.trim();
1490
1491 if style.is_empty() {
1492 return Self::from_vec(props);
1493 }
1494
1495 let chars = style.chars();
1497 let mut current_segment = String::new();
1498 let mut brace_depth = 0;
1499
1500 for c in chars {
1501 match c {
1502 '{' => {
1503 brace_depth += 1;
1504 current_segment.push(c);
1505 }
1506 '}' => {
1507 brace_depth -= 1;
1508 current_segment.push(c);
1509
1510 if brace_depth == 0 {
1511 let segment = current_segment.trim().to_string();
1513 current_segment.clear();
1514
1515 if let Some(parsed) = Self::parse_block_segment(&segment, inherited_conditions, &key_map) {
1516 props.extend(parsed);
1517 }
1518 }
1519 }
1520 ';' if brace_depth == 0 => {
1521 let segment = current_segment.trim().to_string();
1523 current_segment.clear();
1524
1525 if !segment.is_empty() {
1526 if let Some(parsed) = Self::parse_property_segment(&segment, inherited_conditions, &key_map) {
1527 props.extend(parsed);
1528 }
1529 }
1530 }
1531 _ => {
1532 current_segment.push(c);
1533 }
1534 }
1535 }
1536
1537 let remaining = current_segment.trim();
1539 if !remaining.is_empty() && !remaining.contains('{') {
1540 if let Some(parsed) = Self::parse_property_segment(remaining, inherited_conditions, &key_map) {
1541 props.extend(parsed);
1542 }
1543 }
1544
1545 Self::from_vec(props)
1546 }
1547
1548 #[cfg(feature = "parser")]
1550 fn parse_block_segment(
1551 segment: &str,
1552 inherited_conditions: &[DynamicSelector],
1553 key_map: &crate::props::property::CssKeyMap,
1554 ) -> Option<Vec<CssPropertyWithConditions>> {
1555 let brace_pos = segment.find('{')?;
1557 let selector = segment[..brace_pos].trim();
1558
1559 let content_start = brace_pos + 1;
1561 let content_end = segment.rfind('}')?;
1562 if content_end <= content_start {
1563 return None;
1564 }
1565 let content = &segment[content_start..content_end];
1566
1567 let mut conditions = inherited_conditions.to_vec();
1569
1570 if let Some(new_conditions) = Self::parse_selector_to_conditions(selector) {
1571 conditions.extend(new_conditions);
1572 } else {
1573 return None;
1575 }
1576
1577 let parsed = Self::parse_with_conditions(content, &conditions);
1579 Some(parsed.into_library_owned_vec())
1580 }
1581
1582 #[cfg(feature = "parser")]
1584 fn parse_selector_to_conditions(selector: &str) -> Option<Vec<DynamicSelector>> {
1585 let selector = selector.trim();
1586
1587 if let Some(pseudo) = selector.strip_prefix(':') {
1589 match pseudo {
1590 "hover" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Hover)]),
1591 "active" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Active)]),
1592 "focus" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Focus)]),
1593 "focus-within" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::FocusWithin)]),
1594 "disabled" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Disabled)]),
1595 "checked" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::CheckedTrue)]),
1596 "visited" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Visited)]),
1597 "backdrop" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Backdrop)]),
1598 "dragging" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::Dragging)]),
1599 "drag-over" => return Some(vec![DynamicSelector::PseudoState(PseudoStateType::DragOver)]),
1600 _ => return None,
1601 }
1602 }
1603
1604 if let Some(rule_content) = selector.strip_prefix('@') {
1606 return Self::parse_at_rule(rule_content);
1607 }
1608
1609 if selector == "*" {
1611 return Some(vec![]);
1612 }
1613
1614 if selector.is_empty() {
1616 return Some(vec![]);
1617 }
1618
1619 None
1620 }
1621
1622 #[cfg(feature = "parser")]
1626 fn parse_at_rule(rule_content: &str) -> Option<Vec<DynamicSelector>> {
1627 if let Some(rest) = rule_content
1633 .strip_prefix("os ")
1634 .or_else(|| if rule_content.starts_with("os(") { Some(&rule_content[2..]) } else { None })
1635 {
1636 if let Some(conds) = parse_os_at_rule_content(rest) {
1637 return Some(conds);
1638 }
1639 }
1640
1641 if let Some(rest) = rule_content.strip_prefix("media ") {
1643 let media_query = rest.trim();
1644 if let Some(media_conds) = Self::parse_media_query(media_query) {
1645 return Some(media_conds);
1646 }
1647 }
1648
1649 if let Some(rest) = rule_content.strip_prefix("theme ") {
1651 let theme = rest.trim();
1652 match theme {
1653 "dark" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)]),
1654 "light" => return Some(vec![DynamicSelector::Theme(ThemeCondition::Light)]),
1655 _ => return None,
1656 }
1657 }
1658
1659 let lang_body = rule_content
1661 .strip_prefix("lang(")
1662 .map(|r| r.trim_end_matches(')').trim())
1663 .or_else(|| rule_content.strip_prefix("lang ").map(str::trim));
1664 if let Some(lang_str) = lang_body {
1665 let lang_str = lang_str
1666 .strip_prefix('"').and_then(|s| s.strip_suffix('"'))
1667 .or_else(|| lang_str.strip_prefix('\'').and_then(|s| s.strip_suffix('\'')))
1668 .unwrap_or(lang_str);
1669 if !lang_str.is_empty() {
1670 return Some(vec![DynamicSelector::Language(
1671 LanguageCondition::Prefix(AzString::from(lang_str.to_string()))
1672 )]);
1673 }
1674 }
1675
1676 if rule_content.starts_with("container ") || rule_content.starts_with("container(") {
1678 let container_str = if rule_content.starts_with("container(") {
1679 &rule_content[9..] } else {
1681 rule_content[10..].trim()
1682 };
1683 let mut conds = Vec::new();
1684 let (name_part, query_part) = if container_str.starts_with('(') {
1686 (None, container_str)
1687 } else if let Some(paren_idx) = container_str.find('(') {
1688 let name = container_str[..paren_idx].trim();
1689 if name.is_empty() {
1690 (None, container_str)
1691 } else {
1692 (Some(name), &container_str[paren_idx..])
1693 }
1694 } else {
1695 if !container_str.is_empty() {
1696 return Some(vec![DynamicSelector::ContainerName(
1697 AzString::from(container_str.to_string())
1698 )]);
1699 }
1700 return None;
1701 };
1702 if let Some(name) = name_part {
1703 conds.push(DynamicSelector::ContainerName(
1704 AzString::from(name.to_string())
1705 ));
1706 }
1707 if let Some(inner) = query_part.strip_prefix('(').and_then(|s| s.strip_suffix(')')) {
1709 if let Some((key, value)) = inner.split_once(':') {
1710 let key = key.trim();
1711 let value = value.trim();
1712 let px_value = value.strip_suffix("px")
1713 .and_then(|v| v.trim().parse::<f32>().ok());
1714 match key {
1715 "min-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_min(px))); } }
1716 "max-width" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerWidth(MinMaxRange::with_max(px))); } }
1717 "min-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_min(px))); } }
1718 "max-height" => { if let Some(px) = px_value { conds.push(DynamicSelector::ContainerHeight(MinMaxRange::with_max(px))); } }
1719 _ => {}
1720 }
1721 }
1722 }
1723 if !conds.is_empty() {
1724 return Some(conds);
1725 }
1726 }
1727
1728 if rule_content == "prefers-reduced-motion" || rule_content == "reduced-motion" {
1730 return Some(vec![DynamicSelector::PrefersReducedMotion(BoolCondition::True)]);
1731 }
1732
1733 if rule_content == "prefers-high-contrast" || rule_content == "high-contrast" {
1735 return Some(vec![DynamicSelector::PrefersHighContrast(BoolCondition::True)]);
1736 }
1737
1738 None
1739 }
1740
1741 #[cfg(feature = "parser")]
1743 fn parse_media_query(query: &str) -> Option<Vec<DynamicSelector>> {
1744 let query = query.trim();
1745
1746 if query.starts_with('(') && query.ends_with(')') {
1748 let inner = &query[1..query.len()-1];
1749 if let Some((key, value)) = inner.split_once(':') {
1750 let key = key.trim();
1751 let value = value.trim();
1752
1753 let px_value = value.strip_suffix("px")
1755 .and_then(|v| v.trim().parse::<f32>().ok());
1756
1757 match key {
1758 "min-width" => {
1759 if let Some(px) = px_value {
1760 return Some(vec![DynamicSelector::ViewportWidth(
1761 MinMaxRange::with_min(px)
1762 )]);
1763 }
1764 }
1765 "max-width" => {
1766 if let Some(px) = px_value {
1767 return Some(vec![DynamicSelector::ViewportWidth(
1768 MinMaxRange::with_max(px)
1769 )]);
1770 }
1771 }
1772 "min-height" => {
1773 if let Some(px) = px_value {
1774 return Some(vec![DynamicSelector::ViewportHeight(
1775 MinMaxRange::with_min(px)
1776 )]);
1777 }
1778 }
1779 "max-height" => {
1780 if let Some(px) = px_value {
1781 return Some(vec![DynamicSelector::ViewportHeight(
1782 MinMaxRange::with_max(px)
1783 )]);
1784 }
1785 }
1786 other => {
1787 if let Some(sel) = Self::parse_media_feature_inline(other, value) {
1789 return Some(vec![sel]);
1790 }
1791 }
1792 }
1793 }
1794 }
1795
1796 match query {
1798 "screen" => Some(vec![DynamicSelector::Media(MediaType::Screen)]),
1799 "print" => Some(vec![DynamicSelector::Media(MediaType::Print)]),
1800 "all" => Some(vec![DynamicSelector::Media(MediaType::All)]),
1801 _ => None,
1802 }
1803 }
1804
1805 #[cfg(feature = "parser")]
1808 fn parse_media_feature_inline(key: &str, value: &str) -> Option<DynamicSelector> {
1809 match key {
1810 "orientation" => {
1811 if value.eq_ignore_ascii_case("portrait") {
1812 Some(DynamicSelector::Orientation(OrientationType::Portrait))
1813 } else if value.eq_ignore_ascii_case("landscape") {
1814 Some(DynamicSelector::Orientation(OrientationType::Landscape))
1815 } else {
1816 None
1817 }
1818 }
1819 "prefers-color-scheme" => {
1820 if value.eq_ignore_ascii_case("dark") {
1821 Some(DynamicSelector::Theme(ThemeCondition::Dark))
1822 } else if value.eq_ignore_ascii_case("light") {
1823 Some(DynamicSelector::Theme(ThemeCondition::Light))
1824 } else {
1825 None
1826 }
1827 }
1828 "prefers-reduced-motion" => {
1829 if value.eq_ignore_ascii_case("reduce") {
1830 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
1831 } else if value.eq_ignore_ascii_case("no-preference") {
1832 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
1833 } else {
1834 None
1835 }
1836 }
1837 "prefers-contrast" | "prefers-high-contrast" => {
1838 if value.eq_ignore_ascii_case("more") || value.eq_ignore_ascii_case("high") || value.eq_ignore_ascii_case("active") {
1839 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
1840 } else if value.eq_ignore_ascii_case("no-preference") || value.eq_ignore_ascii_case("none") {
1841 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
1842 } else {
1843 None
1844 }
1845 }
1846 _ => None,
1847 }
1848 }
1849
1850 #[cfg(feature = "parser")]
1852 fn parse_property_segment(
1853 segment: &str,
1854 inherited_conditions: &[DynamicSelector],
1855 key_map: &crate::props::property::CssKeyMap,
1856 ) -> Option<Vec<CssPropertyWithConditions>> {
1857 use crate::props::property::{
1858 parse_combined_css_property, parse_css_property, CombinedCssPropertyType,
1859 CssPropertyType,
1860 };
1861
1862 let segment = segment.trim();
1863 if segment.is_empty() {
1864 return None;
1865 }
1866
1867 let (key, value) = segment.split_once(':')?;
1868 let key = key.trim();
1869 let value = value.trim();
1870
1871 let mut props = Vec::new();
1872 let conditions = if inherited_conditions.is_empty() {
1873 DynamicSelectorVec::from_const_slice(&[])
1874 } else {
1875 DynamicSelectorVec::from_vec(inherited_conditions.to_vec())
1876 };
1877
1878 if let Some(prop_type) = CssPropertyType::from_str(key, key_map) {
1880 if let Ok(prop) = parse_css_property(prop_type, value) {
1881 props.push(CssPropertyWithConditions {
1882 property: prop,
1883 apply_if: conditions,
1884 });
1885 return Some(props);
1886 }
1887 }
1888
1889 if let Some(combined_type) = CombinedCssPropertyType::from_str(key, key_map) {
1891 if let Ok(expanded_props) = parse_combined_css_property(combined_type, value) {
1892 for prop in expanded_props {
1893 props.push(CssPropertyWithConditions {
1894 property: prop,
1895 apply_if: conditions.clone(),
1896 });
1897 }
1898 return Some(props);
1899 }
1900 }
1901
1902 None
1903 }
1904
1905}
1906
1907#[cfg(test)]
1908mod tests {
1909 use super::*;
1910
1911 #[test]
1912 fn test_inline_overflow_parse() {
1913 let style = "overflow: scroll;";
1914 let parsed = CssPropertyWithConditionsVec::parse(style);
1915 let props = parsed.into_library_owned_vec();
1916 assert!(!props.is_empty(), "Expected overflow to parse into at least 1 property");
1917 }
1918
1919 #[test]
1920 fn test_inline_overflow_y_parse() {
1921 let style = "overflow-y: scroll;";
1922 let parsed = CssPropertyWithConditionsVec::parse(style);
1923 let props = parsed.into_library_owned_vec();
1924 assert!(!props.is_empty(), "Expected overflow-y to parse into at least 1 property");
1925 }
1926
1927 #[test]
1928 fn test_inline_combined_style_with_overflow() {
1929 let style = "padding: 20px; background-color: #f0f0f0; font-size: 14px; color: #222;overflow: scroll;";
1930 let parsed = CssPropertyWithConditionsVec::parse(style);
1931 let props = parsed.into_library_owned_vec();
1932 assert!(props.len() >= 9, "Expected at least 9 properties, got {}", props.len());
1934 }
1935
1936 #[test]
1937 fn test_inline_grid_template_columns_parse() {
1938 use crate::props::layout::grid::GridTrackSizing;
1939 let style = "display: grid; grid-template-columns: repeat(4, 160px); gap: 16px; padding: 10px;";
1940 let parsed = CssPropertyWithConditionsVec::parse(style);
1941 let props = parsed.into_library_owned_vec();
1942 let grid_cols = props.iter().find(|p| {
1944 matches!(p.property, CssProperty::GridTemplateColumns(_))
1945 }).expect("Expected GridTemplateColumns property");
1946
1947 if let CssProperty::GridTemplateColumns(ref value) = grid_cols.property {
1948 let template = value.get_property().expect("Expected Exact value");
1949 let tracks = template.tracks.as_ref();
1950 assert_eq!(tracks.len(), 4, "Expected 4 tracks");
1951 for (i, track) in tracks.iter().enumerate() {
1952 assert!(matches!(track, GridTrackSizing::Fixed(_)),
1953 "Track {i} should be Fixed(160px), got {track:?}");
1954 }
1955 } else {
1956 panic!("Expected CssProperty::GridTemplateColumns");
1957 }
1958 }
1959}
1960
1961#[cfg(test)]
1962#[allow(
1963 clippy::float_cmp,
1964 clippy::too_many_lines,
1965 clippy::cast_precision_loss,
1966 clippy::field_reassign_with_default,
1967 clippy::unreadable_literal
1968)]
1969mod autotest_generated {
1970 use core::cmp::Ordering;
1971 use std::collections::hash_map::DefaultHasher;
1972 use std::hash::{Hash, Hasher};
1973
1974 use super::*;
1975 use crate::props::property::CssPropertyType;
1976
1977 fn hash_of<T: Hash>(t: &T) -> u64 {
1982 let mut h = DefaultHasher::new();
1983 t.hash(&mut h);
1984 h.finish()
1985 }
1986
1987 fn paint_prop() -> CssProperty {
1989 CssProperty::const_none(CssPropertyType::TextColor)
1990 }
1991
1992 fn layout_prop() -> CssProperty {
1994 CssProperty::const_none(CssPropertyType::Width)
1995 }
1996
1997 fn all_selector_variants() -> Vec<DynamicSelector> {
1999 vec![
2000 DynamicSelector::Os(OsCondition::Linux),
2001 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
2002 DynamicSelector::Media(MediaType::Print),
2003 DynamicSelector::ViewportWidth(MinMaxRange::with_min(1.0)),
2004 DynamicSelector::ViewportHeight(MinMaxRange::with_max(2.0)),
2005 DynamicSelector::ContainerWidth(MinMaxRange::new(Some(1.0), Some(2.0))),
2006 DynamicSelector::ContainerHeight(MinMaxRange::new(None, None)),
2007 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")),
2008 DynamicSelector::Theme(ThemeCondition::Dark),
2009 DynamicSelector::AspectRatio(MinMaxRange::with_min(0.5)),
2010 DynamicSelector::Orientation(OrientationType::Portrait),
2011 DynamicSelector::PrefersReducedMotion(BoolCondition::True),
2012 DynamicSelector::PrefersHighContrast(BoolCondition::False),
2013 DynamicSelector::PseudoState(PseudoStateType::Hover),
2014 DynamicSelector::Language(LanguageCondition::Prefix(AzString::from_const_str("de"))),
2015 ]
2016 }
2017
2018 fn nasty_strings() -> Vec<String> {
2020 vec![
2021 String::new(),
2022 " ".to_string(),
2023 " \t\n\r ".to_string(),
2024 "\0".to_string(),
2025 "0".to_string(),
2026 "-0".to_string(),
2027 "-1".to_string(),
2028 "NaN".to_string(),
2029 "nan".to_string(),
2030 "inf".to_string(),
2031 "-inf".to_string(),
2032 "infinity".to_string(),
2033 "1e400".to_string(),
2034 i64::MAX.to_string(),
2035 i64::MIN.to_string(),
2036 u32::MAX.to_string(),
2037 u64::MAX.to_string(),
2038 "9999999999999999999999999999".to_string(),
2039 "1.7976931348623157e308".to_string(),
2040 "\u{1F600}".to_string(),
2041 "e\u{301}\u{301}\u{301}".to_string(),
2042 "日本語".to_string(),
2043 "\u{202e}gnome".to_string(),
2044 " linux ".to_string(),
2045 "linux;garbage".to_string(),
2046 "linux)".to_string(),
2047 "((((".to_string(),
2048 "))))".to_string(),
2049 ">=".to_string(),
2050 "<=<=<=".to_string(),
2051 ":::::".to_string(),
2052 "-".to_string(),
2053 "_".to_string(),
2054 ".".to_string(),
2055 "..".to_string(),
2056 "...".to_string(),
2057 "1.2.3.4.5.6".to_string(),
2058 "%s%s%n".to_string(),
2059 "\\x00\\xff".to_string(),
2060 "a".repeat(100_000),
2061 "1".repeat(100_000),
2062 ".".repeat(10_000),
2063 "(".repeat(5_000),
2064 "🦀".repeat(10_000),
2065 ]
2066 }
2067
2068 #[test]
2073 fn has_state_default_flags_only_normal_and_checked_false() {
2074 let flags = PseudoStateFlags::default();
2075 assert!(flags.has_state(PseudoStateType::Normal));
2077 assert!(flags.has_state(PseudoStateType::CheckedFalse));
2079 for state in [
2080 PseudoStateType::Hover,
2081 PseudoStateType::Active,
2082 PseudoStateType::Focus,
2083 PseudoStateType::Disabled,
2084 PseudoStateType::CheckedTrue,
2085 PseudoStateType::FocusWithin,
2086 PseudoStateType::Visited,
2087 PseudoStateType::Backdrop,
2088 PseudoStateType::Dragging,
2089 PseudoStateType::DragOver,
2090 ] {
2091 assert!(!flags.has_state(state), "{state:?} must be off by default");
2092 }
2093 }
2094
2095 #[test]
2096 fn has_state_all_flags_set_reports_every_state_except_checked_false() {
2097 let flags = PseudoStateFlags {
2098 hover: true,
2099 active: true,
2100 focused: true,
2101 disabled: true,
2102 checked: true,
2103 focus_within: true,
2104 visited: true,
2105 backdrop: true,
2106 dragging: true,
2107 drag_over: true,
2108 };
2109 assert!(flags.has_state(PseudoStateType::Hover));
2110 assert!(flags.has_state(PseudoStateType::CheckedTrue));
2111 assert!(!flags.has_state(PseudoStateType::CheckedFalse));
2113 assert!(flags.has_state(PseudoStateType::DragOver));
2114 assert!(flags.has_state(PseudoStateType::Normal));
2115 }
2116
2117 #[test]
2118 fn has_state_checked_true_and_false_are_never_both_active() {
2119 for checked in [false, true] {
2120 let flags = PseudoStateFlags {
2121 checked,
2122 ..PseudoStateFlags::default()
2123 };
2124 assert_ne!(
2125 flags.has_state(PseudoStateType::CheckedTrue),
2126 flags.has_state(PseudoStateType::CheckedFalse),
2127 "checked={checked}: CheckedTrue/CheckedFalse must be complementary"
2128 );
2129 }
2130 }
2131
2132 #[test]
2137 fn variant_tag_matches_declared_repr_discriminants() {
2138 for (expected, sel) in all_selector_variants().iter().enumerate() {
2139 let expected = u8::try_from(expected).expect("15 variants fit in u8");
2140 assert_eq!(
2141 sel.variant_tag(),
2142 expected,
2143 "variant_tag drifted from the #[repr(C, u8)] discriminant for {sel:?}"
2144 );
2145 }
2146 }
2147
2148 #[test]
2149 fn variant_tag_is_unique_per_variant() {
2150 let variants = all_selector_variants();
2151 let mut tags: Vec<u8> = variants.iter().map(DynamicSelector::variant_tag).collect();
2152 tags.sort_unstable();
2153 tags.dedup();
2154 assert_eq!(tags.len(), variants.len(), "variant tags must be unique");
2155 }
2156
2157 #[test]
2158 fn ord_is_keyed_on_variant_tag_first() {
2159 let variants = all_selector_variants();
2160 for w in variants.windows(2) {
2161 assert_eq!(
2162 w[0].cmp(&w[1]),
2163 Ordering::Less,
2164 "selectors must sort by variant tag: {:?} < {:?}",
2165 w[0],
2166 w[1]
2167 );
2168 }
2169 }
2170
2171 #[test]
2172 fn hash_distinguishes_variants_carrying_the_same_payload() {
2173 let range = MinMaxRange::with_min(800.0);
2176 let a = DynamicSelector::ViewportWidth(range);
2177 let b = DynamicSelector::ContainerWidth(range);
2178 assert_ne!(hash_of(&a), hash_of(&b));
2179 assert_ne!(a.cmp(&b), Ordering::Equal);
2180 }
2181
2182 #[test]
2183 fn hash_and_ord_agree_for_nan_carrying_ranges() {
2184 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2187 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2188 assert_eq!(a.cmp(&b), Ordering::Equal);
2189 assert_eq!(hash_of(&a), hash_of(&b));
2190 }
2191
2192 #[test]
2199 fn nan_sentinel_range_selector_is_reflexive_under_partial_eq() {
2200 let a = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2201 let b = DynamicSelector::ViewportWidth(MinMaxRange::with_min(800.0));
2202 assert_eq!(a, b, "Eq requires reflexivity, but the NaN `max` sentinel breaks it");
2203 }
2204
2205 #[test]
2207 fn ord_equal_implies_partial_eq_for_range_selectors() {
2208 let a = DynamicSelector::ViewportHeight(MinMaxRange::with_max(600.0));
2209 let b = a.clone();
2210 assert_eq!(a.cmp(&b), Ordering::Equal);
2211 assert!(
2212 a == b,
2213 "cmp() == Equal must imply == (Ord/Eq contract); NaN sentinel breaks it"
2214 );
2215 }
2216
2217 #[test]
2222 fn min_max_range_new_roundtrips_finite_values() {
2223 let r = MinMaxRange::new(Some(1.5), Some(9.5));
2224 assert_eq!(r.min(), Some(1.5));
2225 assert_eq!(r.max(), Some(9.5));
2226 }
2227
2228 #[test]
2229 fn min_max_range_new_none_encodes_nan_sentinel() {
2230 let r = MinMaxRange::new(None, None);
2231 assert!(r.min.is_nan());
2232 assert!(r.max.is_nan());
2233 assert_eq!(r.min(), None);
2234 assert_eq!(r.max(), None);
2235 }
2236
2237 #[test]
2238 fn min_max_range_new_nan_argument_is_indistinguishable_from_none() {
2239 let r = MinMaxRange::new(Some(f32::NAN), Some(f32::NAN));
2242 assert_eq!(r.min(), None);
2243 assert_eq!(r.max(), None);
2244 assert!(r.matches(0.0));
2245 assert!(r.matches(f32::MAX));
2246 }
2247
2248 #[test]
2249 fn min_max_range_with_min_and_with_max_leave_the_other_side_open() {
2250 let lo = MinMaxRange::with_min(-0.0);
2251 assert_eq!(lo.min(), Some(-0.0));
2252 assert_eq!(lo.max(), None);
2253
2254 let hi = MinMaxRange::with_max(f32::MAX);
2255 assert_eq!(hi.min(), None);
2256 assert_eq!(hi.max(), Some(f32::MAX));
2257 }
2258
2259 #[test]
2260 fn min_max_range_getters_survive_extreme_values() {
2261 for v in [
2262 0.0_f32,
2263 -0.0,
2264 f32::MIN,
2265 f32::MAX,
2266 f32::MIN_POSITIVE,
2267 f32::EPSILON,
2268 f32::INFINITY,
2269 f32::NEG_INFINITY,
2270 ] {
2271 let r = MinMaxRange::new(Some(v), Some(v));
2272 assert_eq!(r.min(), Some(v));
2273 assert_eq!(r.max(), Some(v));
2274 }
2275 }
2276
2277 #[test]
2282 fn matches_zero_boundary_is_inclusive() {
2283 assert!(MinMaxRange::with_min(0.0).matches(0.0));
2284 assert!(MinMaxRange::with_max(0.0).matches(0.0));
2285 assert!(MinMaxRange::new(Some(0.0), Some(0.0)).matches(0.0));
2286 assert!(MinMaxRange::with_min(0.0).matches(-0.0));
2288 assert!(MinMaxRange::with_max(0.0).matches(-0.0));
2289 }
2290
2291 #[test]
2292 fn matches_negative_values_are_ordered_correctly() {
2293 let r = MinMaxRange::new(Some(-10.0), Some(-1.0));
2294 assert!(r.matches(-10.0));
2295 assert!(r.matches(-5.0));
2296 assert!(r.matches(-1.0));
2297 assert!(!r.matches(-10.001));
2298 assert!(!r.matches(0.0));
2299 }
2300
2301 #[test]
2302 fn matches_at_float_extremes_does_not_panic() {
2303 let open = MinMaxRange::new(None, None);
2304 let bounded = MinMaxRange::new(Some(f32::MIN), Some(f32::MAX));
2305 for v in [
2306 f32::MIN,
2307 f32::MAX,
2308 f32::INFINITY,
2309 f32::NEG_INFINITY,
2310 f32::MIN_POSITIVE,
2311 -f32::MIN_POSITIVE,
2312 ] {
2313 assert!(open.matches(v), "open range must accept {v}");
2315 }
2316 assert!(bounded.matches(0.0));
2317 assert!(bounded.matches(f32::MIN));
2318 assert!(bounded.matches(f32::MAX));
2319 assert!(!bounded.matches(f32::INFINITY));
2321 assert!(!bounded.matches(f32::NEG_INFINITY));
2322 }
2323
2324 #[test]
2325 fn matches_nan_value_is_rejected_by_any_real_bound() {
2326 assert!(!MinMaxRange::with_min(0.0).matches(f32::NAN));
2328 assert!(!MinMaxRange::with_max(0.0).matches(f32::NAN));
2329 assert!(!MinMaxRange::new(Some(1.0), Some(2.0)).matches(f32::NAN));
2330 assert!(MinMaxRange::new(None, None).matches(f32::NAN));
2332 }
2333
2334 #[test]
2335 fn matches_infinite_bounds_are_deterministic() {
2336 let min_inf = MinMaxRange::with_min(f32::INFINITY);
2337 assert!(!min_inf.matches(f32::MAX));
2338 assert!(min_inf.matches(f32::INFINITY));
2339
2340 let max_neg_inf = MinMaxRange::with_max(f32::NEG_INFINITY);
2341 assert!(!max_neg_inf.matches(f32::MIN));
2342 assert!(max_neg_inf.matches(f32::NEG_INFINITY));
2343 }
2344
2345 #[test]
2346 fn matches_inverted_range_matches_nothing() {
2347 let inverted = MinMaxRange::new(Some(10.0), Some(5.0));
2348 for v in [-1.0_f32, 0.0, 5.0, 7.5, 10.0, 1e30] {
2349 assert!(!inverted.matches(v), "inverted range must reject {v}");
2350 }
2351 }
2352
2353 #[test]
2358 fn os_condition_from_system_platform_covers_every_platform() {
2359 use crate::system::{DesktopEnvironment, Platform};
2360 assert_eq!(
2361 OsCondition::from_system_platform(&Platform::Windows),
2362 OsCondition::Windows
2363 );
2364 assert_eq!(
2365 OsCondition::from_system_platform(&Platform::MacOs),
2366 OsCondition::MacOS
2367 );
2368 assert_eq!(
2369 OsCondition::from_system_platform(&Platform::Ios),
2370 OsCondition::IOS
2371 );
2372 assert_eq!(
2373 OsCondition::from_system_platform(&Platform::Android),
2374 OsCondition::Android
2375 );
2376 assert_eq!(
2377 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Gnome)),
2378 OsCondition::Linux
2379 );
2380 assert_eq!(
2381 OsCondition::from_system_platform(&Platform::Linux(DesktopEnvironment::Other(
2382 AzString::from_const_str(""),
2383 ))),
2384 OsCondition::Linux
2385 );
2386 assert_eq!(
2388 OsCondition::from_system_platform(&Platform::Unknown),
2389 OsCondition::Any
2390 );
2391 }
2392
2393 #[test]
2398 fn os_version_new_stores_fields_verbatim_at_boundaries() {
2399 for id in [0, 1, u32::MAX - 1, u32::MAX] {
2400 let v = OsVersion::new(OsFamily::Linux, id);
2401 assert_eq!(v.os, OsFamily::Linux);
2402 assert_eq!(v.version_id, id);
2403 }
2404 }
2405
2406 #[test]
2407 fn os_version_unknown_is_the_default_and_has_id_zero() {
2408 let u = OsVersion::unknown();
2409 assert_eq!(u.version_id, 0);
2410 assert_eq!(u, OsVersion::default());
2411 }
2412
2413 #[test]
2414 fn os_version_compare_is_none_across_families() {
2415 let win = OsVersion::WIN_11;
2416 let mac = OsVersion::MACOS_SONOMA;
2417 assert_eq!(win.compare(&mac), None);
2418 assert_eq!(mac.compare(&win), None);
2419 assert!(!win.is_at_least(&mac));
2422 assert!(!win.is_at_most(&mac));
2423 assert!(!win.is_exactly(&mac));
2424 }
2425
2426 #[test]
2427 fn os_version_compare_within_family_orders_by_id() {
2428 assert_eq!(
2429 OsVersion::WIN_10.compare(&OsVersion::WIN_11),
2430 Some(Ordering::Less)
2431 );
2432 assert_eq!(
2433 OsVersion::WIN_11.compare(&OsVersion::WIN_10),
2434 Some(Ordering::Greater)
2435 );
2436 assert_eq!(
2437 OsVersion::WIN_11.compare(&OsVersion::WIN_11_21H2),
2438 Some(Ordering::Equal)
2439 );
2440 }
2441
2442 #[test]
2443 fn os_version_compare_at_id_extremes() {
2444 let lo = OsVersion::new(OsFamily::Android, 0);
2445 let hi = OsVersion::new(OsFamily::Android, u32::MAX);
2446 assert_eq!(lo.compare(&hi), Some(Ordering::Less));
2447 assert_eq!(hi.compare(&lo), Some(Ordering::Greater));
2448 assert!(hi.is_at_least(&lo));
2449 assert!(!hi.is_at_most(&lo));
2450 assert!(lo.is_at_most(&hi));
2451 }
2452
2453 #[test]
2454 fn os_version_predicates_are_reflexive_and_consistent() {
2455 for v in [
2456 OsVersion::unknown(),
2457 OsVersion::WIN_XP,
2458 OsVersion::MACOS_TAHOE,
2459 OsVersion::IOS_18,
2460 OsVersion::ANDROID_15,
2461 OsVersion::new(OsFamily::Linux, u32::MAX),
2462 ] {
2463 assert!(v.is_at_least(&v), "{v:?} >= itself");
2464 assert!(v.is_at_most(&v), "{v:?} <= itself");
2465 assert!(v.is_exactly(&v), "{v:?} == itself");
2466 }
2467 }
2468
2469 #[test]
2470 fn os_version_at_least_is_the_strict_complement_of_less_than() {
2471 let a = OsVersion::WIN_10;
2472 let b = OsVersion::WIN_11;
2473 assert!(!a.is_at_least(&b));
2474 assert!(a.is_at_most(&b));
2475 assert!(!a.is_exactly(&b));
2476 assert!(b.is_at_least(&a));
2477 assert!(!b.is_at_most(&a));
2478 }
2479
2480 #[test]
2481 fn os_version_unknown_never_satisfies_a_min_constraint_on_a_real_version() {
2482 assert!(!OsVersion::unknown().is_at_least(&OsVersion::WIN_11));
2485 assert!(!OsVersion::unknown().is_at_least(&OsVersion::LINUX_6_0));
2486 }
2487
2488 #[test]
2493 fn parse_os_version_valid_minimal_positive_controls() {
2494 assert_eq!(
2495 parse_os_version(OsFamily::Windows, "11"),
2496 Some(OsVersion::WIN_11)
2497 );
2498 assert_eq!(
2499 parse_os_version(OsFamily::MacOS, "sonoma"),
2500 Some(OsVersion::MACOS_SONOMA)
2501 );
2502 assert_eq!(
2503 parse_os_version(OsFamily::IOS, "17.0"),
2504 Some(OsVersion::IOS_17)
2505 );
2506 assert_eq!(
2507 parse_os_version(OsFamily::Android, "tiramisu"),
2508 Some(OsVersion::ANDROID_13)
2509 );
2510 assert_eq!(
2511 parse_os_version(OsFamily::Linux, "6.0"),
2512 Some(OsVersion::LINUX_6_0)
2513 );
2514 }
2515
2516 #[test]
2517 fn parse_os_version_trims_and_lowercases() {
2518 assert_eq!(
2519 parse_os_version(OsFamily::Windows, " WIN-11 "),
2520 Some(OsVersion::WIN_11)
2521 );
2522 assert_eq!(
2523 parse_os_version(OsFamily::MacOS, "\tBIG-SUR\n"),
2524 Some(OsVersion::MACOS_BIG_SUR)
2525 );
2526 assert_eq!(
2527 parse_os_version(OsFamily::Android, " KitKat "),
2528 Some(OsVersion::ANDROID_KITKAT)
2529 );
2530 }
2531
2532 #[test]
2533 fn parse_os_version_empty_and_whitespace_is_none_for_every_family() {
2534 for os in [
2535 OsFamily::Windows,
2536 OsFamily::MacOS,
2537 OsFamily::IOS,
2538 OsFamily::Android,
2539 OsFamily::Linux,
2540 ] {
2541 assert_eq!(parse_os_version(os, ""), None, "{os:?} empty");
2542 assert_eq!(parse_os_version(os, " "), None, "{os:?} spaces");
2543 assert_eq!(parse_os_version(os, "\t\n\r"), None, "{os:?} ws");
2544 }
2545 }
2546
2547 #[test]
2548 fn parse_os_version_garbage_and_unicode_never_panics() {
2549 for os in [
2550 OsFamily::Windows,
2551 OsFamily::MacOS,
2552 OsFamily::IOS,
2553 OsFamily::Android,
2554 OsFamily::Linux,
2555 ] {
2556 for s in nasty_strings() {
2557 let a = parse_os_version(os, &s);
2559 let b = parse_os_version(os, &s);
2560 assert_eq!(a, b, "{os:?} not deterministic for {s:?}");
2561 }
2562 }
2563 }
2564
2565 #[test]
2566 fn parse_os_version_leading_trailing_junk_is_rejected() {
2567 assert_eq!(parse_os_version(OsFamily::Windows, "win-11;drop"), None);
2568 assert_eq!(parse_os_version(OsFamily::MacOS, "sonoma!"), None);
2569 assert_eq!(parse_os_version(OsFamily::IOS, "17.0.0.0"), None);
2570 assert_eq!(parse_os_version(OsFamily::Android, "api"), None);
2571 assert_eq!(parse_os_version(OsFamily::Android, "api abc"), None);
2572 }
2573
2574 #[test]
2575 fn parse_windows_version_prefix_forms_all_collapse_to_the_same_version() {
2576 for s in [
2577 "11",
2578 "win11",
2579 "win-11",
2580 "win_11",
2581 "windows11",
2582 "windows-11",
2583 "windows_11",
2584 ] {
2585 assert_eq!(
2586 parse_windows_version(s),
2587 Some(OsVersion::WIN_11),
2588 "{s} should parse to WIN_11"
2589 );
2590 }
2591 }
2592
2593 #[test]
2594 fn parse_windows_version_bare_prefix_and_separator_only_are_none() {
2595 assert_eq!(parse_windows_version("win"), None);
2596 assert_eq!(parse_windows_version("windows"), None);
2597 assert_eq!(parse_windows_version("win-"), None);
2598 assert_eq!(parse_windows_version("windows_"), None);
2599 assert_eq!(parse_windows_version(""), None);
2600 }
2601
2602 #[test]
2603 fn parse_windows_version_nt_aliases_agree_with_names() {
2604 assert_eq!(parse_windows_version("xp"), parse_windows_version("nt5.1"));
2605 assert_eq!(parse_windows_version("vista"), parse_windows_version("6.0"));
2606 assert_eq!(parse_windows_version("8.1"), parse_windows_version("8-1"));
2607 assert_eq!(parse_windows_version("10"), Some(OsVersion::WIN_10));
2608 }
2609
2610 #[test]
2611 fn parse_windows_version_boundary_numbers_are_none() {
2612 for s in ["0", "-0", "-1", "NaN", "inf", "99999999999999999999"] {
2613 assert_eq!(parse_windows_version(s), None, "{s} must not parse");
2614 }
2615 }
2616
2617 #[test]
2618 fn parse_windows_version_huge_and_unicode_input_terminates() {
2619 assert_eq!(parse_windows_version(&"win".repeat(200_000)), None);
2620 assert_eq!(parse_windows_version(&"1".repeat(1_000_000)), None);
2621 assert_eq!(parse_windows_version("\u{1F600}"), None);
2622 assert_eq!(parse_windows_version("win-\u{1F600}"), None);
2623 }
2624
2625 #[test]
2626 fn strip_os_prefix_strips_prefix_and_optional_separator() {
2627 assert_eq!(strip_os_prefix("win-11", &["win"]), "11");
2628 assert_eq!(strip_os_prefix("win_11", &["win"]), "11");
2629 assert_eq!(strip_os_prefix("win11", &["win"]), "11");
2630 assert_eq!(strip_os_prefix("windows-11", &["windows", "win"]), "11");
2632 assert_eq!(strip_os_prefix("windows-11", &["win", "windows"]), "dows-11");
2633 }
2634
2635 #[test]
2636 fn strip_os_prefix_leaves_non_matching_input_untouched() {
2637 assert_eq!(strip_os_prefix("11", &["win"]), "11");
2638 assert_eq!(strip_os_prefix("", &["win"]), "");
2639 assert_eq!(strip_os_prefix("anything", &[]), "anything");
2640 assert_eq!(strip_os_prefix("\u{1F600}win", &["win"]), "\u{1F600}win");
2641 }
2642
2643 #[test]
2644 fn strip_os_prefix_only_strips_one_separator() {
2645 assert_eq!(strip_os_prefix("win--11", &["win"]), "-11");
2646 assert_eq!(strip_os_prefix("win-", &["win"]), "");
2647 assert_eq!(strip_os_prefix("win", &["win"]), "");
2648 }
2649
2650 #[test]
2651 fn strip_os_prefix_with_empty_prefix_is_identity() {
2652 assert_eq!(strip_os_prefix("日本語", &[""]), "日本語");
2655 assert_eq!(strip_os_prefix("-日本語", &[""]), "日本語");
2656 }
2657
2658 #[test]
2659 fn parse_macos_version_names_and_numbers_agree() {
2660 assert_eq!(parse_macos_version("cheetah"), parse_macos_version("10.0"));
2661 assert_eq!(parse_macos_version("big-sur"), parse_macos_version("bigsur"));
2662 assert_eq!(parse_macos_version("bigsur"), parse_macos_version("11.0"));
2663 assert_eq!(parse_macos_version("tahoe"), Some(OsVersion::MACOS_TAHOE));
2664 assert_eq!(
2665 parse_macos_version("snow-leopard"),
2666 parse_macos_version("snowleopard")
2667 );
2668 }
2669
2670 #[test]
2671 fn parse_macos_version_rejects_junk_and_terminates_on_huge_input() {
2672 for s in ["", " ", "sonoma ", "SONOMA", "10.16", "27", "🍎"] {
2673 assert_eq!(parse_macos_version(s), None, "{s:?} must not parse");
2674 }
2675 assert_eq!(parse_macos_version(&"10.".repeat(100_000)), None);
2676 }
2677
2678 #[test]
2679 fn parse_macos_version_is_ordered_monotonically() {
2680 let names = [
2681 "cheetah", "puma", "jaguar", "panther", "tiger", "leopard", "lion", "mojave",
2682 "catalina", "bigsur", "monterey", "ventura", "sonoma", "sequoia", "tahoe",
2683 ];
2684 let ids: Vec<u32> = names
2685 .iter()
2686 .map(|n| {
2687 parse_macos_version(n)
2688 .unwrap_or_else(|| panic!("{n} must parse"))
2689 .version_id
2690 })
2691 .collect();
2692 for w in ids.windows(2) {
2693 assert!(w[0] < w[1], "macOS version ids must increase: {w:?}");
2694 }
2695 }
2696
2697 #[test]
2698 fn parse_ios_version_boundaries() {
2699 assert_eq!(parse_ios_version("1"), Some(OsVersion::IOS_1));
2700 assert_eq!(parse_ios_version("18.0"), Some(OsVersion::IOS_18));
2701 assert_eq!(parse_ios_version("0"), None);
2702 assert_eq!(parse_ios_version("19"), None);
2703 assert_eq!(parse_ios_version(""), None);
2704 assert_eq!(parse_ios_version("-1"), None);
2705 assert_eq!(parse_ios_version("NaN"), None);
2706 assert_eq!(parse_ios_version(&"9".repeat(500_000)), None);
2707 }
2708
2709 #[test]
2710 fn parse_android_version_api_level_escape_hatch() {
2711 assert_eq!(
2712 parse_android_version("api34"),
2713 Some(OsVersion::new(OsFamily::Android, 34))
2714 );
2715 assert_eq!(
2716 parse_android_version("api 34"),
2717 Some(OsVersion::new(OsFamily::Android, 34))
2718 );
2719 assert_eq!(
2720 parse_android_version("api0"),
2721 Some(OsVersion::new(OsFamily::Android, 0))
2722 );
2723 assert_eq!(
2724 parse_android_version(&format!("api{}", u32::MAX)),
2725 Some(OsVersion::new(OsFamily::Android, u32::MAX))
2726 );
2727 }
2728
2729 #[test]
2730 fn parse_android_version_api_level_out_of_range_is_none_not_a_panic() {
2731 assert_eq!(parse_android_version("api4294967296"), None);
2733 assert_eq!(parse_android_version("api-1"), None);
2734 assert_eq!(parse_android_version("api+1"), Some(OsVersion::new(OsFamily::Android, 1)));
2735 assert_eq!(parse_android_version("apiNaN"), None);
2736 assert_eq!(parse_android_version(&format!("api{}", "9".repeat(100_000))), None);
2737 }
2738
2739 #[test]
2740 fn parse_android_version_named_releases() {
2741 assert_eq!(parse_android_version("q"), Some(OsVersion::ANDROID_10));
2742 assert_eq!(parse_android_version("13"), parse_android_version("t"));
2743 assert_eq!(
2744 parse_android_version("13"),
2745 parse_android_version("tiramisu")
2746 );
2747 assert_eq!(parse_android_version("15"), Some(OsVersion::ANDROID_15));
2748 assert_eq!(parse_android_version(""), None);
2749 assert_eq!(parse_android_version("🤖"), None);
2750 }
2751
2752 #[test]
2753 fn parse_linux_version_accepts_bare_major_and_prefixes() {
2754 assert_eq!(
2755 parse_linux_version("5"),
2756 Some(OsVersion::new(OsFamily::Linux, 5000))
2757 );
2758 assert_eq!(parse_linux_version("6.0"), Some(OsVersion::LINUX_6_0));
2759 assert_eq!(parse_linux_version("linux6.0"), Some(OsVersion::LINUX_6_0));
2760 assert_eq!(parse_linux_version("linux-6.0"), Some(OsVersion::LINUX_6_0));
2761 assert_eq!(parse_linux_version("linux_6.0"), Some(OsVersion::LINUX_6_0));
2762 assert_eq!(
2763 parse_linux_version("6.17.0"),
2764 Some(OsVersion::new(OsFamily::Linux, 6170))
2765 );
2766 }
2767
2768 #[test]
2769 fn parse_linux_version_rejects_malformed_and_unicode() {
2770 for s in [
2771 "", " ", ".", "..", "-1", "6.-1", "6.x", "x.6", "NaN", "inf", "🐧", "linux", "linux-",
2772 ] {
2773 assert_eq!(parse_linux_version(s), None, "{s:?} must not parse");
2774 }
2775 }
2776
2777 #[test]
2778 fn parse_linux_version_ignores_everything_past_the_patch_component() {
2779 assert_eq!(
2783 parse_linux_version("6.1.2"),
2784 Some(OsVersion::new(OsFamily::Linux, 6012))
2785 );
2786 assert_eq!(
2787 parse_linux_version("6.1.2.3"),
2788 Some(OsVersion::new(OsFamily::Linux, 6012))
2789 );
2790 assert_eq!(
2791 parse_linux_version("6.0.0.0extra"),
2792 Some(OsVersion::LINUX_6_0),
2793 "a 4th component is never parsed, so trailing junk is accepted"
2794 );
2795 }
2796
2797 #[test]
2802 fn parse_linux_version_huge_major_does_not_overflow() {
2803 assert_eq!(
2804 parse_linux_version("5000000"),
2805 None,
2806 "out-of-range kernel major must be rejected, not overflow u32"
2807 );
2808 }
2809
2810 #[test]
2812 fn parse_linux_version_huge_minor_does_not_overflow() {
2813 assert_eq!(
2814 parse_linux_version("1.999999999"),
2815 None,
2816 "out-of-range kernel minor must be rejected, not overflow u32"
2817 );
2818 }
2819
2820 #[test]
2821 fn parse_linux_version_max_u32_component_is_rejected_by_parse_not_by_math() {
2822 assert_eq!(parse_linux_version("4294967296"), None);
2824 }
2825
2826 #[test]
2831 fn linux_desktop_env_from_system_maps_unknown_des_to_other() {
2832 use crate::system::DesktopEnvironment;
2833 assert_eq!(
2834 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Gnome),
2835 LinuxDesktopEnv::Gnome
2836 );
2837 assert_eq!(
2838 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Kde),
2839 LinuxDesktopEnv::KDE
2840 );
2841 for name in ["xfce", "", "🖥", &"x".repeat(10_000)] {
2843 assert_eq!(
2844 LinuxDesktopEnv::from_system_desktop_env(&DesktopEnvironment::Other(
2845 AzString::from(name.to_string())
2846 )),
2847 LinuxDesktopEnv::Other
2848 );
2849 }
2850 }
2851
2852 #[test]
2853 fn theme_condition_from_system_theme_is_total() {
2854 use crate::system::Theme;
2855 assert_eq!(
2856 ThemeCondition::from_system_theme(Theme::Light),
2857 ThemeCondition::Light
2858 );
2859 assert_eq!(
2860 ThemeCondition::from_system_theme(Theme::Dark),
2861 ThemeCondition::Dark
2862 );
2863 }
2864
2865 #[test]
2870 fn language_exact_is_case_insensitive_and_strict() {
2871 let cond = LanguageCondition::Exact(AzString::from_const_str("de-DE"));
2872 assert!(cond.matches("de-DE"));
2873 assert!(cond.matches("DE-de"));
2874 assert!(!cond.matches("de"));
2875 assert!(!cond.matches("de-AT"));
2876 assert!(!cond.matches("de-DE-x"));
2877 assert!(!cond.matches(""));
2878 }
2879
2880 #[test]
2881 fn language_prefix_matches_subtags_only_at_a_dash_boundary() {
2882 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
2883 assert!(cond.matches("de"));
2884 assert!(cond.matches("de-DE"));
2885 assert!(cond.matches("DE-at"));
2886 assert!(!cond.matches("den"));
2888 assert!(!cond.matches("deu"));
2889 assert!(!cond.matches("d"));
2890 assert!(!cond.matches(""));
2891 }
2892
2893 #[test]
2894 fn language_empty_prefix_matches_everything_ascii() {
2895 let cond = LanguageCondition::Prefix(AzString::from_const_str(""));
2896 assert!(cond.matches(""));
2897 assert!(cond.matches("en-US"));
2898 }
2899
2900 #[test]
2901 fn language_prefix_longer_than_input_is_false() {
2902 let cond = LanguageCondition::Prefix(AzString::from_const_str("de-DE-1996"));
2903 assert!(!cond.matches("de"));
2904 assert!(!cond.matches(""));
2905 }
2906
2907 #[test]
2908 fn language_matches_huge_input_terminates() {
2909 let cond = LanguageCondition::Prefix(AzString::from_const_str("en"));
2910 let huge = format!("en-{}", "a".repeat(1_000_000));
2911 assert!(cond.matches(&huge));
2912 let cond_exact = LanguageCondition::Exact(AzString::from_const_str("en"));
2913 assert!(!cond_exact.matches(&huge));
2914 }
2915
2916 #[test]
2922 fn language_prefix_does_not_panic_on_multibyte_language_tag() {
2923 let cond = LanguageCondition::Prefix(AzString::from_const_str("de"));
2924 assert!(!cond.matches("日本語"));
2926 }
2927
2928 #[test]
2930 fn language_prefix_does_not_panic_on_two_byte_language_tag() {
2931 let cond = LanguageCondition::Prefix(AzString::from_const_str("d"));
2932 assert!(!cond.matches("é"));
2933 }
2934
2935 #[test]
2940 fn context_from_system_style_default_is_coherent() {
2941 let style = crate::system::SystemStyle::default();
2942 let ctx = DynamicSelectorContext::from_system_style(&style);
2943 assert_eq!(ctx.os, OsCondition::Any);
2945 assert_eq!(ctx.desktop_env, OptionLinuxDesktopEnv::None);
2946 assert_eq!(ctx.de_version, 0);
2947 assert_eq!(ctx.theme, ThemeCondition::Light);
2948 assert_eq!(ctx.media_type, MediaType::Screen);
2949 assert_eq!(ctx.viewport_width, DEFAULT_VIEWPORT_WIDTH);
2950 assert_eq!(ctx.viewport_height, DEFAULT_VIEWPORT_HEIGHT);
2951 assert!(ctx.container_width.is_nan());
2954 assert!(ctx.container_height.is_nan());
2955 assert!(ctx.window_focused);
2956 }
2957
2958 #[test]
2959 fn context_from_system_style_linux_carries_the_desktop_env() {
2960 use crate::system::{DesktopEnvironment, Platform};
2961 let mut style = crate::system::SystemStyle::default();
2962 style.platform = Platform::Linux(DesktopEnvironment::Kde);
2963 let ctx = DynamicSelectorContext::from_system_style(&style);
2964 assert_eq!(ctx.os, OsCondition::Linux);
2965 assert_eq!(
2966 ctx.desktop_env,
2967 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE)
2968 );
2969 }
2970
2971 #[test]
2972 fn with_viewport_updates_orientation_and_is_deterministic_at_extremes() {
2973 let base = DynamicSelectorContext::default();
2974 assert_eq!(
2975 base.with_viewport(1920.0, 1080.0).orientation,
2976 OrientationType::Landscape
2977 );
2978 assert_eq!(
2979 base.with_viewport(1080.0, 1920.0).orientation,
2980 OrientationType::Portrait
2981 );
2982 assert_eq!(
2984 base.with_viewport(500.0, 500.0).orientation,
2985 OrientationType::Portrait
2986 );
2987 assert_eq!(
2989 base.with_viewport(f32::NAN, f32::NAN).orientation,
2990 OrientationType::Portrait
2991 );
2992 assert_eq!(
2993 base.with_viewport(f32::INFINITY, f32::NEG_INFINITY).orientation,
2994 OrientationType::Landscape
2995 );
2996 let z = base.with_viewport(0.0, 0.0);
2998 assert_eq!(z.viewport_width, 0.0);
2999 assert_eq!(z.orientation, OrientationType::Portrait);
3000 let neg = base.with_viewport(-100.0, -200.0);
3001 assert_eq!(neg.orientation, OrientationType::Landscape);
3002 }
3003
3004 #[test]
3005 fn with_viewport_does_not_disturb_unrelated_fields() {
3006 let base = DynamicSelectorContext::default();
3007 let updated = base.with_viewport(1.0, 2.0);
3008 assert_eq!(updated.os, base.os);
3009 assert_eq!(updated.theme, base.theme);
3010 assert_eq!(updated.language, base.language);
3011 assert_eq!(updated.pseudo_state, base.pseudo_state);
3012 }
3013
3014 #[test]
3015 fn with_container_stores_name_and_extreme_dimensions() {
3016 let base = DynamicSelectorContext::default();
3017 let named = base.with_container(
3018 f32::MAX,
3019 f32::NEG_INFINITY,
3020 Some(AzString::from_const_str("sidebar")),
3021 );
3022 assert_eq!(named.container_width, f32::MAX);
3023 assert_eq!(named.container_height, f32::NEG_INFINITY);
3024 assert_eq!(
3025 named.container_name.as_ref(),
3026 Some(&AzString::from_const_str("sidebar"))
3027 );
3028
3029 let unnamed = base.with_container(0.0, 0.0, None);
3030 assert_eq!(unnamed.container_name.as_ref(), None);
3031 }
3032
3033 #[test]
3034 fn with_container_nan_dimensions_stay_unmatched() {
3035 let ctx = DynamicSelectorContext::default().with_container(f32::NAN, f32::NAN, None);
3036 let open = DynamicSelector::ContainerWidth(MinMaxRange::new(None, None));
3039 assert!(!open.matches(&ctx));
3040 let open_h = DynamicSelector::ContainerHeight(MinMaxRange::new(None, None));
3041 assert!(!open_h.matches(&ctx));
3042 }
3043
3044 #[test]
3045 fn with_pseudo_state_replaces_the_whole_flag_set() {
3046 let base = DynamicSelectorContext::default();
3047 let hovered = base.with_pseudo_state(PseudoStateFlags {
3048 hover: true,
3049 ..PseudoStateFlags::default()
3050 });
3051 assert!(hovered.pseudo_state.hover);
3052 assert!(!hovered.pseudo_state.active);
3053 let active = hovered.with_pseudo_state(PseudoStateFlags {
3055 active: true,
3056 ..PseudoStateFlags::default()
3057 });
3058 assert!(!active.pseudo_state.hover);
3059 assert!(active.pseudo_state.active);
3060 }
3061
3062 #[test]
3063 fn viewport_breakpoint_changed_detects_crossings_only() {
3064 let bps = [480.0_f32, 768.0, 1024.0];
3065 let base = DynamicSelectorContext::default();
3066 let small = base.with_viewport(320.0, 480.0);
3067 let medium = base.with_viewport(800.0, 600.0);
3068 let also_medium = base.with_viewport(900.0, 600.0);
3069
3070 assert!(small.viewport_breakpoint_changed(&medium, &bps));
3071 assert!(medium.viewport_breakpoint_changed(&small, &bps));
3072 assert!(!medium.viewport_breakpoint_changed(&also_medium, &bps));
3073 assert!(!small.viewport_breakpoint_changed(&small, &bps));
3074 }
3075
3076 #[test]
3077 fn viewport_breakpoint_changed_is_exactly_on_the_boundary() {
3078 let bps = [800.0_f32];
3079 let base = DynamicSelectorContext::default();
3080 let at = base.with_viewport(800.0, 600.0);
3082 let just_below = base.with_viewport(799.99, 600.0);
3083 assert!(at.viewport_breakpoint_changed(&just_below, &bps));
3084 assert!(!at.viewport_breakpoint_changed(&at, &bps));
3085 }
3086
3087 #[test]
3088 fn viewport_breakpoint_changed_handles_empty_and_degenerate_breakpoints() {
3089 let base = DynamicSelectorContext::default();
3090 let a = base.with_viewport(100.0, 100.0);
3091 let b = base.with_viewport(5000.0, 100.0);
3092 assert!(!a.viewport_breakpoint_changed(&b, &[]));
3093 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NAN]));
3095 assert!(!a.viewport_breakpoint_changed(&b, &[f32::INFINITY]));
3097 assert!(!a.viewport_breakpoint_changed(&b, &[f32::NEG_INFINITY]));
3098 let nan_vp = base.with_viewport(f32::NAN, 100.0);
3100 assert!(nan_vp.viewport_breakpoint_changed(&b, &[800.0]));
3101 }
3102
3103 #[test]
3104 fn viewport_breakpoint_changed_with_many_breakpoints_terminates() {
3105 let bps: Vec<f32> = (0..100_000).map(|i| i as f32).collect();
3106 let base = DynamicSelectorContext::default();
3107 let a = base.with_viewport(0.0, 100.0);
3108 let b = base.with_viewport(99_999.0, 100.0);
3109 assert!(a.viewport_breakpoint_changed(&b, &bps));
3110 }
3111
3112 #[test]
3117 fn match_os_any_matches_everything() {
3118 for actual in [
3119 OsCondition::Any,
3120 OsCondition::Apple,
3121 OsCondition::MacOS,
3122 OsCondition::IOS,
3123 OsCondition::Linux,
3124 OsCondition::Windows,
3125 OsCondition::Android,
3126 OsCondition::Web,
3127 ] {
3128 assert!(DynamicSelector::match_os(OsCondition::Any, actual));
3129 }
3130 }
3131
3132 #[test]
3133 fn match_os_apple_is_the_macos_ios_union() {
3134 assert!(DynamicSelector::match_os(
3135 OsCondition::Apple,
3136 OsCondition::MacOS
3137 ));
3138 assert!(DynamicSelector::match_os(
3139 OsCondition::Apple,
3140 OsCondition::IOS
3141 ));
3142 assert!(!DynamicSelector::match_os(
3143 OsCondition::Apple,
3144 OsCondition::Linux
3145 ));
3146 assert!(!DynamicSelector::match_os(
3148 OsCondition::MacOS,
3149 OsCondition::Apple
3150 ));
3151 }
3152
3153 #[test]
3154 fn match_os_concrete_conditions_require_equality() {
3155 assert!(DynamicSelector::match_os(
3156 OsCondition::Linux,
3157 OsCondition::Linux
3158 ));
3159 assert!(!DynamicSelector::match_os(
3160 OsCondition::Linux,
3161 OsCondition::Windows
3162 ));
3163 assert!(!DynamicSelector::match_os(
3165 OsCondition::Windows,
3166 OsCondition::Any
3167 ));
3168 }
3169
3170 #[test]
3171 fn match_os_version_min_max_exact_at_zero_and_u32_max() {
3172 let zero = OsVersion::new(OsFamily::Linux, 0);
3173 let max = OsVersion::new(OsFamily::Linux, u32::MAX);
3174 let none = OptionLinuxDesktopEnv::None;
3175
3176 assert!(DynamicSelector::match_os_version(
3177 &OsVersionCondition::Min(zero),
3178 max,
3179 none,
3180 0
3181 ));
3182 assert!(!DynamicSelector::match_os_version(
3183 &OsVersionCondition::Min(max),
3184 zero,
3185 none,
3186 0
3187 ));
3188 assert!(DynamicSelector::match_os_version(
3189 &OsVersionCondition::Max(max),
3190 zero,
3191 none,
3192 0
3193 ));
3194 assert!(DynamicSelector::match_os_version(
3195 &OsVersionCondition::Exact(max),
3196 max,
3197 none,
3198 0
3199 ));
3200 assert!(!DynamicSelector::match_os_version(
3201 &OsVersionCondition::Exact(zero),
3202 max,
3203 none,
3204 0
3205 ));
3206 }
3207
3208 #[test]
3209 fn match_os_version_cross_family_never_matches() {
3210 let none = OptionLinuxDesktopEnv::None;
3211 for cond in [
3213 OsVersionCondition::Min(OsVersion::WIN_10),
3214 OsVersionCondition::Max(OsVersion::WIN_10),
3215 OsVersionCondition::Exact(OsVersion::WIN_10),
3216 ] {
3217 assert!(
3218 !DynamicSelector::match_os_version(
3219 &cond,
3220 OsVersion::MACOS_SONOMA,
3221 none,
3222 0
3223 ),
3224 "{cond:?} must not match a macOS runtime"
3225 );
3226 }
3227 }
3228
3229 #[test]
3230 fn match_os_version_desktop_environment_requires_the_env_to_be_present() {
3231 let cond = OsVersionCondition::DesktopEnvironment(LinuxDesktopEnv::Gnome);
3232 assert!(DynamicSelector::match_os_version(
3233 &cond,
3234 OsVersion::unknown(),
3235 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3236 0
3237 ));
3238 assert!(!DynamicSelector::match_os_version(
3239 &cond,
3240 OsVersion::unknown(),
3241 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3242 0
3243 ));
3244 assert!(!DynamicSelector::match_os_version(
3245 &cond,
3246 OsVersion::unknown(),
3247 OptionLinuxDesktopEnv::None,
3248 0
3249 ));
3250 }
3251
3252 #[test]
3253 fn match_os_version_desktop_env_min_max_respect_the_unknown_zero_sentinel() {
3254 let gnome = OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome);
3255 let dev = DesktopEnvVersion {
3256 env: LinuxDesktopEnv::Gnome,
3257 version_id: 40,
3258 };
3259 assert!(!DynamicSelector::match_os_version(
3261 &OsVersionCondition::DesktopEnvMin(dev),
3262 OsVersion::unknown(),
3263 gnome,
3264 0
3265 ));
3266 assert!(!DynamicSelector::match_os_version(
3267 &OsVersionCondition::DesktopEnvMax(dev),
3268 OsVersion::unknown(),
3269 gnome,
3270 0
3271 ));
3272 assert!(DynamicSelector::match_os_version(
3274 &OsVersionCondition::DesktopEnvMin(dev),
3275 OsVersion::unknown(),
3276 gnome,
3277 40
3278 ));
3279 assert!(DynamicSelector::match_os_version(
3280 &OsVersionCondition::DesktopEnvMin(dev),
3281 OsVersion::unknown(),
3282 gnome,
3283 u32::MAX
3284 ));
3285 assert!(!DynamicSelector::match_os_version(
3286 &OsVersionCondition::DesktopEnvMin(dev),
3287 OsVersion::unknown(),
3288 gnome,
3289 39
3290 ));
3291 assert!(DynamicSelector::match_os_version(
3292 &OsVersionCondition::DesktopEnvMax(dev),
3293 OsVersion::unknown(),
3294 gnome,
3295 40
3296 ));
3297 assert!(!DynamicSelector::match_os_version(
3298 &OsVersionCondition::DesktopEnvMax(dev),
3299 OsVersion::unknown(),
3300 gnome,
3301 41
3302 ));
3303 }
3304
3305 #[test]
3306 fn match_os_version_desktop_env_exact_needs_the_matching_env() {
3307 let dev = DesktopEnvVersion {
3308 env: LinuxDesktopEnv::Gnome,
3309 version_id: 45,
3310 };
3311 assert!(DynamicSelector::match_os_version(
3312 &OsVersionCondition::DesktopEnvExact(dev),
3313 OsVersion::unknown(),
3314 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3315 45
3316 ));
3317 assert!(!DynamicSelector::match_os_version(
3318 &OsVersionCondition::DesktopEnvExact(dev),
3319 OsVersion::unknown(),
3320 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::KDE),
3321 45
3322 ));
3323 assert!(!DynamicSelector::match_os_version(
3324 &OsVersionCondition::DesktopEnvExact(dev),
3325 OsVersion::unknown(),
3326 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3327 46
3328 ));
3329 }
3330
3331 #[test]
3337 fn match_os_version_desktop_env_exact_zero_fails_when_de_version_is_unknown() {
3338 let dev = DesktopEnvVersion {
3339 env: LinuxDesktopEnv::Gnome,
3340 version_id: 0,
3341 };
3342 assert!(
3343 !DynamicSelector::match_os_version(
3344 &OsVersionCondition::DesktopEnvExact(dev),
3345 OsVersion::unknown(),
3346 OptionLinuxDesktopEnv::Some(LinuxDesktopEnv::Gnome),
3347 0
3348 ),
3349 "de_version == 0 is the 'unknown' sentinel; it must not satisfy an exact match"
3350 );
3351 }
3352
3353 #[test]
3354 fn match_theme_system_preferred_is_a_wildcard() {
3355 for actual in [
3356 ThemeCondition::Light,
3357 ThemeCondition::Dark,
3358 ThemeCondition::SystemPreferred,
3359 ThemeCondition::Custom(AzString::from_const_str("solarized")),
3360 ] {
3361 assert!(DynamicSelector::match_theme(
3362 &ThemeCondition::SystemPreferred,
3363 &actual
3364 ));
3365 }
3366 }
3367
3368 #[test]
3369 fn match_theme_custom_compares_the_name() {
3370 let a = ThemeCondition::Custom(AzString::from_const_str("nord"));
3371 let b = ThemeCondition::Custom(AzString::from_const_str("nord"));
3372 let c = ThemeCondition::Custom(AzString::from_const_str("Nord"));
3373 assert!(DynamicSelector::match_theme(&a, &b));
3374 assert!(!DynamicSelector::match_theme(&a, &c));
3376 assert!(!DynamicSelector::match_theme(&a, &ThemeCondition::Dark));
3377 assert!(!DynamicSelector::match_theme(
3379 &ThemeCondition::Dark,
3380 &ThemeCondition::SystemPreferred
3381 ));
3382 }
3383
3384 #[test]
3385 fn match_pseudo_state_reads_through_to_the_context_flags() {
3386 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3387 hover: true,
3388 checked: true,
3389 ..PseudoStateFlags::default()
3390 });
3391 assert!(DynamicSelector::match_pseudo_state(
3392 PseudoStateType::Hover,
3393 &ctx
3394 ));
3395 assert!(DynamicSelector::match_pseudo_state(
3396 PseudoStateType::CheckedTrue,
3397 &ctx
3398 ));
3399 assert!(!DynamicSelector::match_pseudo_state(
3400 PseudoStateType::CheckedFalse,
3401 &ctx
3402 ));
3403 assert!(!DynamicSelector::match_pseudo_state(
3404 PseudoStateType::Active,
3405 &ctx
3406 ));
3407 assert!(DynamicSelector::match_pseudo_state(
3408 PseudoStateType::Normal,
3409 &ctx
3410 ));
3411 }
3412
3413 #[test]
3414 fn match_pseudo_state_agrees_with_has_state_for_every_state() {
3415 let flags = PseudoStateFlags {
3416 hover: true,
3417 focused: true,
3418 visited: true,
3419 drag_over: true,
3420 ..PseudoStateFlags::default()
3421 };
3422 let ctx = DynamicSelectorContext::default().with_pseudo_state(flags);
3423 for state in [
3424 PseudoStateType::Normal,
3425 PseudoStateType::Hover,
3426 PseudoStateType::Active,
3427 PseudoStateType::Focus,
3428 PseudoStateType::Disabled,
3429 PseudoStateType::CheckedTrue,
3430 PseudoStateType::CheckedFalse,
3431 PseudoStateType::FocusWithin,
3432 PseudoStateType::Visited,
3433 PseudoStateType::Backdrop,
3434 PseudoStateType::Dragging,
3435 PseudoStateType::DragOver,
3436 ] {
3437 assert_eq!(
3438 DynamicSelector::match_pseudo_state(state, &ctx),
3439 flags.has_state(state),
3440 "match_pseudo_state and has_state disagree on {state:?}"
3441 );
3442 }
3443 }
3444
3445 #[test]
3446 fn selector_matches_every_variant_against_the_default_context() {
3447 let ctx = DynamicSelectorContext::default();
3449 for sel in all_selector_variants() {
3450 let a = sel.matches(&ctx);
3451 let b = sel.matches(&ctx);
3452 assert_eq!(a, b, "{sel:?} is not deterministic");
3453 }
3454 }
3455
3456 #[test]
3457 fn selector_matches_media_all_is_a_wildcard() {
3458 let ctx = DynamicSelectorContext::default();
3459 assert_eq!(ctx.media_type, MediaType::Screen);
3460 assert!(DynamicSelector::Media(MediaType::All).matches(&ctx));
3461 assert!(DynamicSelector::Media(MediaType::Screen).matches(&ctx));
3462 assert!(!DynamicSelector::Media(MediaType::Print).matches(&ctx));
3463 }
3464
3465 #[test]
3466 fn selector_matches_aspect_ratio_never_divides_by_zero() {
3467 let base = DynamicSelectorContext::default();
3468 let flat = base.with_viewport(800.0, 0.0);
3470 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3471 .matches(&flat));
3472 let nan_h = base.with_viewport(800.0, f32::NAN);
3474 assert!(DynamicSelector::AspectRatio(MinMaxRange::new(Some(799.0), Some(801.0)))
3475 .matches(&nan_h));
3476 let nan_w = base.with_viewport(f32::NAN, 600.0);
3478 assert!(!DynamicSelector::AspectRatio(MinMaxRange::with_min(0.0)).matches(&nan_w));
3479 }
3480
3481 #[test]
3482 fn selector_matches_container_name_requires_an_exact_name() {
3483 let ctx = DynamicSelectorContext::default().with_container(
3484 100.0,
3485 100.0,
3486 Some(AzString::from_const_str("sidebar")),
3487 );
3488 assert!(
3489 DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&ctx)
3490 );
3491 assert!(
3492 !DynamicSelector::ContainerName(AzString::from_const_str("Sidebar")).matches(&ctx)
3493 );
3494 assert!(!DynamicSelector::ContainerName(AzString::from_const_str("main")).matches(&ctx));
3495 let no_ctr = DynamicSelectorContext::default();
3497 assert!(
3498 !DynamicSelector::ContainerName(AzString::from_const_str("sidebar")).matches(&no_ctr)
3499 );
3500 }
3501
3502 #[test]
3503 fn selector_matches_bool_conditions_compare_both_polarities() {
3504 let ctx = DynamicSelectorContext::default();
3505 assert_eq!(ctx.prefers_reduced_motion, BoolCondition::False);
3506 assert!(DynamicSelector::PrefersReducedMotion(BoolCondition::False).matches(&ctx));
3508 assert!(!DynamicSelector::PrefersReducedMotion(BoolCondition::True).matches(&ctx));
3509 assert!(DynamicSelector::PrefersHighContrast(BoolCondition::False).matches(&ctx));
3510 assert!(!DynamicSelector::PrefersHighContrast(BoolCondition::True).matches(&ctx));
3511 }
3512
3513 #[test]
3514 fn bool_condition_roundtrips_through_bool() {
3515 for b in [false, true] {
3516 assert_eq!(bool::from(BoolCondition::from(b)), b);
3517 }
3518 assert_eq!(BoolCondition::default(), BoolCondition::False);
3519 assert!(!bool::from(BoolCondition::False));
3520 assert!(bool::from(BoolCondition::True));
3521 }
3522
3523 #[test]
3528 fn simple_property_is_unconditional_and_always_matches() {
3529 let p = CssPropertyWithConditions::simple(paint_prop());
3530 assert!(p.apply_if.as_slice().is_empty());
3531 assert!(!p.is_conditional());
3532 assert!(!p.is_pseudo_state_only());
3533 assert!(p.matches(&DynamicSelectorContext::default()));
3534 }
3535
3536 #[test]
3537 fn with_condition_and_with_single_condition_agree() {
3538 let a = CssPropertyWithConditions::with_condition(
3539 paint_prop(),
3540 DynamicSelector::PseudoState(PseudoStateType::Hover),
3541 );
3542 let b = CssPropertyWithConditions::on_hover(paint_prop());
3543 assert_eq!(a.apply_if.as_slice(), b.apply_if.as_slice());
3544 assert_eq!(a.apply_if.as_slice().len(), 1);
3545 }
3546
3547 #[test]
3548 fn with_conditions_preserves_order_and_length() {
3549 let conds = all_selector_variants();
3550 let p = CssPropertyWithConditions::with_conditions(
3551 paint_prop(),
3552 DynamicSelectorVec::from_vec(conds.clone()),
3553 );
3554 assert_eq!(p.apply_if.as_slice().len(), conds.len());
3555 for (i, c) in conds.iter().enumerate() {
3556 assert_eq!(p.apply_if.as_slice()[i].variant_tag(), c.variant_tag());
3557 }
3558 assert!(p.is_conditional());
3559 }
3560
3561 #[test]
3562 fn with_conditions_empty_vec_is_unconditional() {
3563 let p = CssPropertyWithConditions::with_conditions(
3564 paint_prop(),
3565 DynamicSelectorVec::from_vec(vec![]),
3566 );
3567 assert!(!p.is_conditional());
3568 assert!(p.matches(&DynamicSelectorContext::default()));
3569 }
3570
3571 #[test]
3572 fn pseudo_state_constructors_build_the_right_condition() {
3573 let cases = [
3574 (
3575 CssPropertyWithConditions::on_hover(paint_prop()),
3576 PseudoStateType::Hover,
3577 ),
3578 (
3579 CssPropertyWithConditions::on_active(paint_prop()),
3580 PseudoStateType::Active,
3581 ),
3582 (
3583 CssPropertyWithConditions::on_focus(paint_prop()),
3584 PseudoStateType::Focus,
3585 ),
3586 (
3587 CssPropertyWithConditions::when_disabled(paint_prop()),
3588 PseudoStateType::Disabled,
3589 ),
3590 ];
3591 for (prop, expected) in cases {
3592 assert_eq!(
3593 prop.apply_if.as_slice(),
3594 &[DynamicSelector::PseudoState(expected)]
3595 );
3596 assert!(prop.is_pseudo_state_only());
3597 assert!(prop.is_conditional());
3598 }
3599 }
3600
3601 #[test]
3602 fn os_and_theme_constructors_build_the_right_condition() {
3603 assert_eq!(
3604 CssPropertyWithConditions::on_windows(paint_prop())
3605 .apply_if
3606 .as_slice(),
3607 &[DynamicSelector::Os(OsCondition::Windows)]
3608 );
3609 assert_eq!(
3610 CssPropertyWithConditions::on_macos(paint_prop())
3611 .apply_if
3612 .as_slice(),
3613 &[DynamicSelector::Os(OsCondition::MacOS)]
3614 );
3615 assert_eq!(
3616 CssPropertyWithConditions::on_linux(paint_prop())
3617 .apply_if
3618 .as_slice(),
3619 &[DynamicSelector::Os(OsCondition::Linux)]
3620 );
3621 assert_eq!(
3622 CssPropertyWithConditions::on_os(paint_prop(), OsCondition::Web)
3623 .apply_if
3624 .as_slice(),
3625 &[DynamicSelector::Os(OsCondition::Web)]
3626 );
3627 assert_eq!(
3628 CssPropertyWithConditions::dark_theme(paint_prop())
3629 .apply_if
3630 .as_slice(),
3631 &[DynamicSelector::Theme(ThemeCondition::Dark)]
3632 );
3633 assert_eq!(
3634 CssPropertyWithConditions::light_theme(paint_prop())
3635 .apply_if
3636 .as_slice(),
3637 &[DynamicSelector::Theme(ThemeCondition::Light)]
3638 );
3639 assert!(!CssPropertyWithConditions::on_linux(paint_prop()).is_pseudo_state_only());
3641 assert!(!CssPropertyWithConditions::dark_theme(paint_prop()).is_pseudo_state_only());
3642 }
3643
3644 #[test]
3645 fn matches_requires_all_conditions_to_hold() {
3646 let ctx = DynamicSelectorContext::default().with_pseudo_state(PseudoStateFlags {
3647 hover: true,
3648 ..PseudoStateFlags::default()
3649 });
3650 let both = CssPropertyWithConditions::with_conditions(
3652 paint_prop(),
3653 DynamicSelectorVec::from_vec(vec![
3654 DynamicSelector::PseudoState(PseudoStateType::Hover),
3655 DynamicSelector::PseudoState(PseudoStateType::Focus),
3656 ]),
3657 );
3658 assert!(!both.matches(&ctx));
3659
3660 let ok = CssPropertyWithConditions::with_conditions(
3662 paint_prop(),
3663 DynamicSelectorVec::from_vec(vec![
3664 DynamicSelector::PseudoState(PseudoStateType::Hover),
3665 DynamicSelector::PseudoState(PseudoStateType::Normal),
3666 ]),
3667 );
3668 assert!(ok.matches(&ctx));
3669 }
3670
3671 #[test]
3672 fn matches_with_a_large_condition_list_terminates() {
3673 let conds: Vec<DynamicSelector> = (0..50_000)
3674 .map(|_| DynamicSelector::PseudoState(PseudoStateType::Normal))
3675 .collect();
3676 let p = CssPropertyWithConditions::with_conditions(
3677 paint_prop(),
3678 DynamicSelectorVec::from_vec(conds),
3679 );
3680 assert!(p.matches(&DynamicSelectorContext::default()));
3681 }
3682
3683 #[test]
3684 fn is_pseudo_state_only_is_false_for_empty_and_for_mixed_lists() {
3685 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_pseudo_state_only());
3687 let mixed = CssPropertyWithConditions::with_conditions(
3689 paint_prop(),
3690 DynamicSelectorVec::from_vec(vec![
3691 DynamicSelector::PseudoState(PseudoStateType::Hover),
3692 DynamicSelector::Os(OsCondition::Linux),
3693 ]),
3694 );
3695 assert!(!mixed.is_pseudo_state_only());
3696 let all_pseudo = CssPropertyWithConditions::with_conditions(
3698 paint_prop(),
3699 DynamicSelectorVec::from_vec(vec![
3700 DynamicSelector::PseudoState(PseudoStateType::Hover),
3701 DynamicSelector::PseudoState(PseudoStateType::Focus),
3702 ]),
3703 );
3704 assert!(all_pseudo.is_pseudo_state_only());
3705 }
3706
3707 #[test]
3708 fn is_layout_affecting_splits_layout_from_paint() {
3709 assert!(CssPropertyWithConditions::simple(layout_prop()).is_layout_affecting());
3710 assert!(!CssPropertyWithConditions::simple(paint_prop()).is_layout_affecting());
3711 assert!(CssPropertyWithConditions::on_hover(layout_prop()).is_layout_affecting());
3713 assert!(!CssPropertyWithConditions::on_hover(paint_prop()).is_layout_affecting());
3714 }
3715
3716 #[test]
3717 fn css_property_with_conditions_hash_agrees_with_eq() {
3718 let a = CssPropertyWithConditions::on_hover(paint_prop());
3719 let b = CssPropertyWithConditions::on_hover(paint_prop());
3720 assert_eq!(a, b);
3721 assert_eq!(hash_of(&a), hash_of(&b));
3722 assert_eq!(a.cmp(&b), Ordering::Equal);
3723
3724 let c = CssPropertyWithConditions::on_focus(paint_prop());
3727 assert_ne!(a, c);
3728 assert_ne!(a.cmp(&c), Ordering::Equal);
3729 assert_ne!(hash_of(&a), hash_of(&c));
3730 }
3731
3732 #[test]
3733 fn css_property_with_conditions_ord_is_lexicographic_over_conditions() {
3734 let short = CssPropertyWithConditions::on_hover(paint_prop());
3735 let long = CssPropertyWithConditions::with_conditions(
3736 paint_prop(),
3737 DynamicSelectorVec::from_vec(vec![
3738 DynamicSelector::PseudoState(PseudoStateType::Hover),
3739 DynamicSelector::PseudoState(PseudoStateType::Focus),
3740 ]),
3741 );
3742 assert_eq!(short.cmp(&long), Ordering::Less);
3744 assert_eq!(long.cmp(&short), Ordering::Greater);
3745 }
3746
3747 #[cfg(feature = "parser")]
3752 #[test]
3753 fn parse_os_at_rule_bare_and_parenthesized_forms_agree() {
3754 let bare = parse_os_at_rule_content("linux").expect("bare linux");
3755 let paren = parse_os_at_rule_content("(linux)").expect("(linux)");
3756 let quoted = parse_os_at_rule_content("(\"linux\")").expect("quoted");
3757 let spaced = parse_os_at_rule_content(" ( linux ) ").expect("spaced");
3758 assert_eq!(bare, vec![DynamicSelector::Os(OsCondition::Linux)]);
3759 assert_eq!(bare, paren);
3760 assert_eq!(bare, quoted);
3761 assert_eq!(bare, spaced);
3762 }
3763
3764 #[cfg(feature = "parser")]
3765 #[test]
3766 fn parse_os_at_rule_emits_the_family_even_for_any() {
3767 for s in ["(any)", "(all)", "(*)"] {
3770 let conds = parse_os_at_rule_content(s).unwrap_or_else(|| panic!("{s} must parse"));
3771 assert_eq!(conds, vec![DynamicSelector::Os(OsCondition::Any)]);
3772 assert!(conds[0].matches(&DynamicSelectorContext::default()));
3773 }
3774 }
3775
3776 #[cfg(feature = "parser")]
3777 #[test]
3778 fn parse_os_at_rule_desktop_env_forms() {
3779 assert_eq!(
3780 parse_os_at_rule_content("(linux:gnome)"),
3781 Some(vec![
3782 DynamicSelector::Os(OsCondition::Linux),
3783 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3784 LinuxDesktopEnv::Gnome
3785 )),
3786 ])
3787 );
3788 assert_eq!(
3790 parse_os_at_rule_content("(linux:notarealde)"),
3791 Some(vec![
3792 DynamicSelector::Os(OsCondition::Linux),
3793 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvironment(
3794 LinuxDesktopEnv::Other
3795 )),
3796 ])
3797 );
3798 assert_eq!(
3800 parse_os_at_rule_content("(linux:)"),
3801 Some(vec![DynamicSelector::Os(OsCondition::Linux)])
3802 );
3803 }
3804
3805 #[cfg(feature = "parser")]
3806 #[test]
3807 fn parse_os_at_rule_version_operators() {
3808 assert_eq!(
3809 parse_os_at_rule_content("(windows >= win-11)"),
3810 Some(vec![
3811 DynamicSelector::Os(OsCondition::Windows),
3812 DynamicSelector::OsVersion(OsVersionCondition::Min(OsVersion::WIN_11)),
3813 ])
3814 );
3815 assert_eq!(
3817 parse_os_at_rule_content("(windows > win-11)"),
3818 parse_os_at_rule_content("(windows >= win-11)")
3819 );
3820 assert_eq!(
3821 parse_os_at_rule_content("(macos <= sonoma)"),
3822 Some(vec![
3823 DynamicSelector::Os(OsCondition::MacOS),
3824 DynamicSelector::OsVersion(OsVersionCondition::Max(OsVersion::MACOS_SONOMA)),
3825 ])
3826 );
3827 assert_eq!(
3828 parse_os_at_rule_content("(ios = 17)"),
3829 Some(vec![
3830 DynamicSelector::Os(OsCondition::IOS),
3831 DynamicSelector::OsVersion(OsVersionCondition::Exact(OsVersion::IOS_17)),
3832 ])
3833 );
3834 }
3835
3836 #[cfg(feature = "parser")]
3837 #[test]
3838 fn parse_os_at_rule_desktop_env_version() {
3839 assert_eq!(
3840 parse_os_at_rule_content("(linux:gnome > 40)"),
3841 Some(vec![
3842 DynamicSelector::Os(OsCondition::Linux),
3843 DynamicSelector::OsVersion(OsVersionCondition::DesktopEnvMin(
3844 DesktopEnvVersion {
3845 env: LinuxDesktopEnv::Gnome,
3846 version_id: 40,
3847 }
3848 )),
3849 ])
3850 );
3851 assert_eq!(parse_os_at_rule_content("(linux:gnome > 4294967296)"), None);
3853 assert_eq!(parse_os_at_rule_content("(linux:gnome > -1)"), None);
3854 assert_eq!(parse_os_at_rule_content("(linux:gnome > abc)"), None);
3855 assert_eq!(parse_os_at_rule_content("(linux:gnome > )"), None);
3856 }
3857
3858 #[cfg(feature = "parser")]
3859 #[test]
3860 fn parse_os_at_rule_rejects_versions_on_versionless_families() {
3861 assert_eq!(parse_os_at_rule_content("(apple >= 14)"), None);
3863 assert_eq!(parse_os_at_rule_content("(web >= 1)"), None);
3864 assert_eq!(parse_os_at_rule_content("(any >= 1)"), None);
3865 }
3866
3867 #[cfg(feature = "parser")]
3868 #[test]
3869 fn parse_os_at_rule_empty_and_garbage_is_none() {
3870 for s in [
3871 "",
3872 " ",
3873 "\t\n",
3874 "()",
3875 "( )",
3876 "(\"\")",
3877 "('')",
3878 "(:)",
3879 "(:gnome)",
3880 "(notanos)",
3881 "(linux;drop)",
3882 "\u{1F600}",
3883 "(\u{1F600})",
3884 "(linux linux)",
3885 ] {
3886 assert_eq!(parse_os_at_rule_content(s), None, "{s:?} must not parse");
3887 }
3888 }
3889
3890 #[cfg(feature = "parser")]
3891 #[test]
3892 fn parse_os_at_rule_huge_and_deeply_parenthesized_input_terminates() {
3893 let nested = format!("{}linux{}", "(".repeat(10_000), ")".repeat(10_000));
3895 assert_eq!(parse_os_at_rule_content(&nested), None);
3896 assert_eq!(parse_os_at_rule_content(&"a".repeat(1_000_000)), None);
3897 assert_eq!(
3898 parse_os_at_rule_content(&format!("(linux >= {})", "9".repeat(100_000))),
3899 None
3900 );
3901 }
3902
3903 #[cfg(feature = "parser")]
3904 #[test]
3905 fn split_op_and_version_picks_the_earliest_then_longest_operator() {
3906 let (subject, op) = split_op_and_version("linux >= 6.0");
3907 assert_eq!(subject, "linux ");
3908 let (op, ver) = op.expect("operator found");
3909 assert!(matches!(op, VersionOp::Min));
3910 assert_eq!(ver, "6.0");
3911
3912 let (_, op) = split_op_and_version("a>=1");
3914 assert!(matches!(op.expect("op").0, VersionOp::Min));
3915 let (_, op) = split_op_and_version("a<=1");
3916 assert!(matches!(op.expect("op").0, VersionOp::Max));
3917 let (_, op) = split_op_and_version("a=1");
3918 assert!(matches!(op.expect("op").0, VersionOp::Exact));
3919 }
3920
3921 #[cfg(feature = "parser")]
3922 #[test]
3923 fn split_op_and_version_with_no_operator_returns_the_whole_string() {
3924 let (subject, op) = split_op_and_version("linux");
3925 assert_eq!(subject, "linux");
3926 assert!(op.is_none());
3927
3928 let (subject, op) = split_op_and_version("");
3929 assert_eq!(subject, "");
3930 assert!(op.is_none());
3931 }
3932
3933 #[cfg(feature = "parser")]
3934 #[test]
3935 fn split_op_and_version_operator_only_yields_empty_sides() {
3936 let (subject, op) = split_op_and_version(">=");
3937 assert_eq!(subject, "");
3938 assert_eq!(op.expect("op").1, "");
3939
3940 let (subject, op) = split_op_and_version("=");
3941 assert_eq!(subject, "");
3942 assert_eq!(op.expect("op").1, "");
3943 }
3944
3945 #[cfg(feature = "parser")]
3946 #[test]
3947 fn split_op_and_version_does_not_split_inside_a_multibyte_char() {
3948 let (subject, op) = split_op_and_version("日本語 >= 6.0");
3951 assert_eq!(subject, "日本語 ");
3952 assert_eq!(op.expect("op").1, "6.0");
3953 let (subject, op) = split_op_and_version("🦀🦀🦀");
3955 assert_eq!(subject, "🦀🦀🦀");
3956 assert!(op.is_none());
3957 }
3958
3959 #[cfg(feature = "parser")]
3960 #[test]
3961 fn parse_os_family_token_accepts_aliases_case_insensitively() {
3962 assert_eq!(parse_os_family_token("LINUX"), Some(OsCondition::Linux));
3963 assert_eq!(parse_os_family_token("Win"), Some(OsCondition::Windows));
3964 assert_eq!(parse_os_family_token("windows"), Some(OsCondition::Windows));
3965 assert_eq!(parse_os_family_token("osx"), Some(OsCondition::MacOS));
3966 assert_eq!(parse_os_family_token("mac"), Some(OsCondition::MacOS));
3967 assert_eq!(parse_os_family_token("wasm"), Some(OsCondition::Web));
3968 assert_eq!(parse_os_family_token("*"), Some(OsCondition::Any));
3969 assert_eq!(parse_os_family_token("all"), Some(OsCondition::Any));
3970 }
3971
3972 #[cfg(feature = "parser")]
3973 #[test]
3974 fn parse_os_family_token_rejects_empty_padded_and_junk() {
3975 for s in [
3977 "",
3978 " ",
3979 " linux",
3980 "linux ",
3981 "lin",
3982 "linux2",
3983 "0",
3984 "-1",
3985 "NaN",
3986 "\u{1F600}",
3987 ] {
3988 assert_eq!(parse_os_family_token(s), None, "{s:?} must not parse");
3989 }
3990 assert_eq!(parse_os_family_token(&"linux".repeat(100_000)), None);
3991 }
3992
3993 #[cfg(feature = "parser")]
3994 #[test]
3995 fn parse_de_token_is_total_and_falls_back_to_other() {
3996 assert_eq!(parse_de_token("GNOME"), LinuxDesktopEnv::Gnome);
3997 assert_eq!(parse_de_token("kde"), LinuxDesktopEnv::KDE);
3998 assert_eq!(parse_de_token("XFCE"), LinuxDesktopEnv::XFCE);
3999 assert_eq!(parse_de_token("unity"), LinuxDesktopEnv::Unity);
4000 assert_eq!(parse_de_token("Cinnamon"), LinuxDesktopEnv::Cinnamon);
4001 assert_eq!(parse_de_token("mate"), LinuxDesktopEnv::MATE);
4002 for s in ["", " ", "gnome ", "\u{1F600}", "日本語", "\0"] {
4004 assert_eq!(parse_de_token(s), LinuxDesktopEnv::Other, "{s:?}");
4005 }
4006 assert_eq!(
4007 parse_de_token(&"gnome".repeat(200_000)),
4008 LinuxDesktopEnv::Other
4009 );
4010 }
4011
4012 #[cfg(feature = "parser")]
4017 fn parse_len(style: &str) -> usize {
4018 CssPropertyWithConditionsVec::parse(style)
4019 .into_library_owned_vec()
4020 .len()
4021 }
4022
4023 #[cfg(feature = "parser")]
4024 #[test]
4025 fn parse_valid_minimal_positive_control() {
4026 let props = CssPropertyWithConditionsVec::parse("color: red;").into_library_owned_vec();
4027 assert_eq!(props.len(), 1);
4028 assert!(!props[0].is_conditional());
4029 assert!(matches!(props[0].property, CssProperty::TextColor(_)));
4030 }
4031
4032 #[cfg(feature = "parser")]
4033 #[test]
4034 fn parse_empty_and_whitespace_yields_no_properties() {
4035 for s in ["", " ", "\t\n\r ", ";", ";;;;", " ; ; "] {
4036 assert_eq!(parse_len(s), 0, "{s:?} must yield no properties");
4037 }
4038 }
4039
4040 #[cfg(feature = "parser")]
4041 #[test]
4042 fn parse_garbage_never_panics_and_yields_nothing() {
4043 for s in [
4044 "not css at all",
4045 "color",
4046 "color:",
4047 ":",
4048 "::::",
4049 "{}",
4050 "}{",
4051 "}",
4052 "{",
4053 "{{{",
4054 "}}}",
4055 "}{color: red}",
4056 "color: ;",
4057 "\0: \0;",
4058 "%s%n%s",
4059 "\u{1F600}: \u{1F600};",
4060 "日本語: 赤;",
4061 ] {
4062 let _ = parse_len(s);
4064 }
4065 assert_eq!(parse_len("not css at all"), 0);
4066 assert_eq!(parse_len("color:"), 0);
4067 assert_eq!(parse_len("}{"), 0);
4068 }
4069
4070 #[cfg(feature = "parser")]
4071 #[test]
4072 fn parse_tolerates_a_missing_trailing_semicolon() {
4073 assert_eq!(parse_len("color: red"), 1);
4074 assert_eq!(parse_len("color: red;color: blue"), 2);
4075 }
4076
4077 #[cfg(feature = "parser")]
4078 #[test]
4079 fn parse_pseudo_selector_block_attaches_the_condition() {
4080 let props = CssPropertyWithConditionsVec::parse(":hover { color: red; }")
4081 .into_library_owned_vec();
4082 assert_eq!(props.len(), 1);
4083 assert!(props[0].is_pseudo_state_only());
4084 assert_eq!(
4085 props[0].apply_if.as_slice(),
4086 &[DynamicSelector::PseudoState(PseudoStateType::Hover)]
4087 );
4088 }
4089
4090 #[cfg(feature = "parser")]
4091 #[test]
4092 fn parse_unknown_selector_block_is_dropped_wholesale() {
4093 assert_eq!(parse_len(":nosuchstate { color: red; }"), 0);
4096 assert_eq!(parse_len("@nosuchrule { color: red; }"), 0);
4097 assert_eq!(parse_len("div { color: red; }"), 0);
4098 }
4099
4100 #[cfg(feature = "parser")]
4101 #[test]
4102 fn parse_nesting_accumulates_inherited_conditions() {
4103 let props =
4104 CssPropertyWithConditionsVec::parse("@os linux { font-size: 14px; :hover { color: red; }}")
4105 .into_library_owned_vec();
4106 assert_eq!(props.len(), 2);
4107 let font = props
4109 .iter()
4110 .find(|p| matches!(p.property, CssProperty::FontSize(_)))
4111 .expect("font-size present");
4112 assert_eq!(
4113 font.apply_if.as_slice(),
4114 &[DynamicSelector::Os(OsCondition::Linux)]
4115 );
4116 let color = props
4117 .iter()
4118 .find(|p| matches!(p.property, CssProperty::TextColor(_)))
4119 .expect("color present");
4120 assert_eq!(
4121 color.apply_if.as_slice(),
4122 &[
4123 DynamicSelector::Os(OsCondition::Linux),
4124 DynamicSelector::PseudoState(PseudoStateType::Hover),
4125 ]
4126 );
4127 assert!(!color.is_pseudo_state_only());
4128 }
4129
4130 #[cfg(feature = "parser")]
4131 #[test]
4132 fn parse_moderately_deep_nesting_terminates() {
4133 const DEPTH: usize = 50;
4138 let style = format!(
4139 "{}color: red;{}",
4140 ":hover {".repeat(DEPTH),
4141 "}".repeat(DEPTH)
4142 );
4143 let props = CssPropertyWithConditionsVec::parse(&style).into_library_owned_vec();
4144 assert_eq!(props.len(), 1);
4145 assert_eq!(props[0].apply_if.as_slice().len(), DEPTH);
4146 assert!(props[0].is_pseudo_state_only());
4147 }
4148
4149 #[cfg(feature = "parser")]
4150 #[test]
4151 fn parse_unbalanced_braces_do_not_panic() {
4152 for s in [
4154 "color: red; }",
4155 "{ color: red;",
4156 ":hover { color: red;",
4157 ":hover }",
4158 &"{".repeat(1_000),
4159 &"}".repeat(1_000),
4160 ] {
4161 let _ = parse_len(s);
4162 }
4163 }
4164
4165 #[cfg(feature = "parser")]
4166 #[test]
4167 fn parse_very_long_input_terminates() {
4168 let long = "color: red;".repeat(5_000);
4169 assert_eq!(parse_len(&long), 5_000);
4170 assert_eq!(parse_len(&"a".repeat(500_000)), 0);
4172 assert_eq!(parse_len(&format!("color: {};", "z".repeat(500_000))), 0);
4173 }
4174
4175 #[cfg(feature = "parser")]
4176 #[test]
4177 fn parse_selector_to_conditions_covers_every_pseudo_class() {
4178 let cases = [
4179 ("hover", PseudoStateType::Hover),
4180 ("active", PseudoStateType::Active),
4181 ("focus", PseudoStateType::Focus),
4182 ("focus-within", PseudoStateType::FocusWithin),
4183 ("disabled", PseudoStateType::Disabled),
4184 ("checked", PseudoStateType::CheckedTrue),
4185 ("visited", PseudoStateType::Visited),
4186 ("backdrop", PseudoStateType::Backdrop),
4187 ("dragging", PseudoStateType::Dragging),
4188 ("drag-over", PseudoStateType::DragOver),
4189 ];
4190 for (name, expected) in cases {
4191 assert_eq!(
4192 CssPropertyWithConditionsVec::parse_selector_to_conditions(&format!(":{name}")),
4193 Some(vec![DynamicSelector::PseudoState(expected)]),
4194 ":{name} must parse"
4195 );
4196 }
4197 }
4198
4199 #[cfg(feature = "parser")]
4200 #[test]
4201 fn parse_selector_to_conditions_wildcards_are_unconditional() {
4202 assert_eq!(
4203 CssPropertyWithConditionsVec::parse_selector_to_conditions("*"),
4204 Some(vec![])
4205 );
4206 assert_eq!(
4207 CssPropertyWithConditionsVec::parse_selector_to_conditions(""),
4208 Some(vec![])
4209 );
4210 assert_eq!(
4211 CssPropertyWithConditionsVec::parse_selector_to_conditions(" "),
4212 Some(vec![])
4213 );
4214 }
4215
4216 #[cfg(feature = "parser")]
4217 #[test]
4218 fn parse_selector_to_conditions_rejects_unknown_selectors() {
4219 for s in [
4220 ":", ":hoverr", ":HOVER", "div", "#id", ".class", "@", "\u{1F600}", ":\u{1F600}",
4221 ] {
4222 assert_eq!(
4223 CssPropertyWithConditionsVec::parse_selector_to_conditions(s),
4224 None,
4225 "{s:?} must not parse"
4226 );
4227 }
4228 }
4229
4230 #[cfg(feature = "parser")]
4231 #[test]
4232 fn parse_at_rule_theme_lang_and_accessibility() {
4233 assert_eq!(
4234 CssPropertyWithConditionsVec::parse_at_rule("theme dark"),
4235 Some(vec![DynamicSelector::Theme(ThemeCondition::Dark)])
4236 );
4237 assert_eq!(
4238 CssPropertyWithConditionsVec::parse_at_rule("theme light"),
4239 Some(vec![DynamicSelector::Theme(ThemeCondition::Light)])
4240 );
4241 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("theme neon"), None);
4242
4243 assert_eq!(
4244 CssPropertyWithConditionsVec::parse_at_rule("lang(\"de-DE\")"),
4245 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4246 AzString::from_const_str("de-DE")
4247 ))])
4248 );
4249 assert_eq!(
4250 CssPropertyWithConditionsVec::parse_at_rule("lang de"),
4251 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4252 AzString::from_const_str("de")
4253 ))])
4254 );
4255 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang()"), None);
4256 assert_eq!(CssPropertyWithConditionsVec::parse_at_rule("lang(\"\")"), None);
4257
4258 assert_eq!(
4259 CssPropertyWithConditionsVec::parse_at_rule("prefers-reduced-motion"),
4260 Some(vec![DynamicSelector::PrefersReducedMotion(
4261 BoolCondition::True
4262 )])
4263 );
4264 assert_eq!(
4265 CssPropertyWithConditionsVec::parse_at_rule("high-contrast"),
4266 Some(vec![DynamicSelector::PrefersHighContrast(
4267 BoolCondition::True
4268 )])
4269 );
4270 }
4271
4272 #[cfg(feature = "parser")]
4273 #[test]
4274 fn parse_at_rule_container_named_and_sized() {
4275 assert_eq!(
4276 CssPropertyWithConditionsVec::parse_at_rule("container sidebar"),
4277 Some(vec![DynamicSelector::ContainerName(
4278 AzString::from_const_str("sidebar")
4279 )])
4280 );
4281 let conds = CssPropertyWithConditionsVec::parse_at_rule("container (min-width: 400px)")
4282 .expect("sized container must parse");
4283 assert_eq!(conds.len(), 1);
4284 match conds[0] {
4288 DynamicSelector::ContainerWidth(r) => {
4289 assert_eq!(r.min(), Some(400.0));
4290 assert_eq!(r.max(), None);
4291 }
4292 ref other => panic!("expected ContainerWidth, got {other:?}"),
4293 }
4294 let named = CssPropertyWithConditionsVec::parse_at_rule(
4295 "container sidebar (max-height: 200px)",
4296 )
4297 .expect("named + sized container must parse");
4298 assert_eq!(named.len(), 2);
4299 assert!(matches!(named[0], DynamicSelector::ContainerName(_)));
4300 assert!(matches!(named[1], DynamicSelector::ContainerHeight(_)));
4301 }
4302
4303 #[cfg(feature = "parser")]
4304 #[test]
4305 fn parse_at_rule_empty_and_garbage_is_none() {
4306 for s in [
4307 "",
4308 " ",
4309 "os",
4310 "os ",
4311 "os()",
4312 "os(notanos)",
4313 "media",
4314 "media ",
4315 "media (min-width: abc)",
4316 "theme",
4317 "container",
4318 "container ()",
4319 "nosuchrule",
4320 "\u{1F600}",
4321 ] {
4322 assert_eq!(
4323 CssPropertyWithConditionsVec::parse_at_rule(s),
4324 None,
4325 "{s:?} must not parse"
4326 );
4327 }
4328 }
4329
4330 #[cfg(feature = "parser")]
4331 #[test]
4332 fn parse_at_rule_huge_input_terminates() {
4333 assert_eq!(
4334 CssPropertyWithConditionsVec::parse_at_rule(&format!("os {}", "(".repeat(50_000))),
4335 None
4336 );
4337 assert_eq!(
4338 CssPropertyWithConditionsVec::parse_at_rule(&"z".repeat(500_000)),
4339 None
4340 );
4341 let long_lang = "e".repeat(100_000);
4343 assert_eq!(
4344 CssPropertyWithConditionsVec::parse_at_rule(&format!("lang {long_lang}")),
4345 Some(vec![DynamicSelector::Language(LanguageCondition::Prefix(
4346 AzString::from(long_lang)
4347 ))])
4348 );
4349 }
4350
4351 #[cfg(feature = "parser")]
4352 #[test]
4353 fn parse_media_query_media_types_and_dimensions() {
4354 assert_eq!(
4355 CssPropertyWithConditionsVec::parse_media_query("screen"),
4356 Some(vec![DynamicSelector::Media(MediaType::Screen)])
4357 );
4358 assert_eq!(
4359 CssPropertyWithConditionsVec::parse_media_query("print"),
4360 Some(vec![DynamicSelector::Media(MediaType::Print)])
4361 );
4362 assert_eq!(
4363 CssPropertyWithConditionsVec::parse_media_query("all"),
4364 Some(vec![DynamicSelector::Media(MediaType::All)])
4365 );
4366 let w = CssPropertyWithConditionsVec::parse_media_query("(min-width: 800px)")
4367 .expect("min-width must parse");
4368 assert_eq!(w.len(), 1);
4369 match w[0] {
4370 DynamicSelector::ViewportWidth(r) => {
4371 assert_eq!(r.min(), Some(800.0));
4372 assert_eq!(r.max(), None);
4373 }
4374 ref other => panic!("expected ViewportWidth, got {other:?}"),
4375 }
4376 }
4377
4378 #[cfg(feature = "parser")]
4379 #[test]
4380 fn parse_media_query_boundary_pixel_values() {
4381 let zero = CssPropertyWithConditionsVec::parse_media_query("(min-width: 0px)")
4383 .expect("0px must parse");
4384 match zero[0] {
4385 DynamicSelector::ViewportWidth(r) => assert_eq!(r.min(), Some(0.0)),
4386 ref other => panic!("expected ViewportWidth, got {other:?}"),
4387 }
4388 let neg = CssPropertyWithConditionsVec::parse_media_query("(max-height: -1px)")
4389 .expect("-1px must parse");
4390 match neg[0] {
4391 DynamicSelector::ViewportHeight(r) => assert_eq!(r.max(), Some(-1.0)),
4392 ref other => panic!("expected ViewportHeight, got {other:?}"),
4393 }
4394 assert_eq!(
4396 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800)"),
4397 None
4398 );
4399 assert_eq!(
4400 CssPropertyWithConditionsVec::parse_media_query("(min-width: 800em)"),
4401 None
4402 );
4403 }
4404
4405 #[cfg(feature = "parser")]
4406 #[test]
4407 fn parse_media_query_empty_and_garbage_is_none() {
4408 for s in [
4409 "",
4410 " ",
4411 "(",
4412 ")",
4413 "()",
4414 "(:)",
4415 "(min-width)",
4416 "(min-width: )",
4417 "(nosuchfeature: 1px)",
4418 "SCREEN",
4419 "\u{1F600}",
4420 "(🦀: 1px)",
4421 ] {
4422 assert_eq!(
4423 CssPropertyWithConditionsVec::parse_media_query(s),
4424 None,
4425 "{s:?} must not parse"
4426 );
4427 }
4428 assert_eq!(
4429 CssPropertyWithConditionsVec::parse_media_query(&"(".repeat(100_000)),
4430 None
4431 );
4432 }
4433
4434 #[cfg(feature = "parser")]
4440 #[test]
4441 fn parse_media_query_nan_pixel_value_is_rejected() {
4442 assert_eq!(
4443 CssPropertyWithConditionsVec::parse_media_query("(min-width: NaNpx)"),
4444 None,
4445 "a NaN px value collapses into the 'no limit' sentinel and matches everything"
4446 );
4447 }
4448
4449 #[cfg(feature = "parser")]
4450 #[test]
4451 fn parse_media_query_infinite_pixel_value_never_matches() {
4452 let q = CssPropertyWithConditionsVec::parse_media_query("(min-width: infpx)")
4455 .expect("infpx currently parses");
4456 let ctx = DynamicSelectorContext::default().with_viewport(1e30, 1000.0);
4457 assert!(!q[0].matches(&ctx), "an infinite min-width can never be met");
4458 }
4459
4460 #[cfg(feature = "parser")]
4461 #[test]
4462 fn parse_media_feature_inline_known_features() {
4463 assert_eq!(
4464 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "PORTRAIT"),
4465 Some(DynamicSelector::Orientation(OrientationType::Portrait))
4466 );
4467 assert_eq!(
4468 CssPropertyWithConditionsVec::parse_media_feature_inline(
4469 "prefers-color-scheme",
4470 "Dark"
4471 ),
4472 Some(DynamicSelector::Theme(ThemeCondition::Dark))
4473 );
4474 assert_eq!(
4475 CssPropertyWithConditionsVec::parse_media_feature_inline(
4476 "prefers-reduced-motion",
4477 "reduce"
4478 ),
4479 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::True))
4480 );
4481 assert_eq!(
4482 CssPropertyWithConditionsVec::parse_media_feature_inline(
4483 "prefers-reduced-motion",
4484 "no-preference"
4485 ),
4486 Some(DynamicSelector::PrefersReducedMotion(BoolCondition::False))
4487 );
4488 assert_eq!(
4489 CssPropertyWithConditionsVec::parse_media_feature_inline("prefers-contrast", "more"),
4490 Some(DynamicSelector::PrefersHighContrast(BoolCondition::True))
4491 );
4492 assert_eq!(
4493 CssPropertyWithConditionsVec::parse_media_feature_inline(
4494 "prefers-high-contrast",
4495 "none"
4496 ),
4497 Some(DynamicSelector::PrefersHighContrast(BoolCondition::False))
4498 );
4499 }
4500
4501 #[cfg(feature = "parser")]
4502 #[test]
4503 fn parse_media_feature_inline_rejects_unknown_keys_and_values() {
4504 assert_eq!(
4505 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", ""),
4506 None
4507 );
4508 assert_eq!(
4509 CssPropertyWithConditionsVec::parse_media_feature_inline("orientation", "sideways"),
4510 None
4511 );
4512 assert_eq!(
4513 CssPropertyWithConditionsVec::parse_media_feature_inline("", ""),
4514 None
4515 );
4516 assert_eq!(
4517 CssPropertyWithConditionsVec::parse_media_feature_inline("nosuchkey", "dark"),
4518 None
4519 );
4520 assert_eq!(
4521 CssPropertyWithConditionsVec::parse_media_feature_inline(
4522 "prefers-color-scheme",
4523 "\u{1F600}"
4524 ),
4525 None
4526 );
4527 assert_eq!(
4529 CssPropertyWithConditionsVec::parse_media_feature_inline(" orientation", "portrait"),
4530 None
4531 );
4532 assert_eq!(
4533 CssPropertyWithConditionsVec::parse_media_feature_inline(
4534 &"k".repeat(200_000),
4535 &"v".repeat(200_000)
4536 ),
4537 None
4538 );
4539 }
4540
4541 #[cfg(feature = "parser")]
4542 #[test]
4543 fn parse_property_segment_valid_and_invalid() {
4544 let key_map = crate::props::property::CssKeyMap::get();
4545 let ok = CssPropertyWithConditionsVec::parse_property_segment("color: red", &[], &key_map)
4546 .expect("color: red must parse");
4547 assert_eq!(ok.len(), 1);
4548 assert!(!ok[0].is_conditional());
4549
4550 let inherited = vec![DynamicSelector::PseudoState(PseudoStateType::Hover)];
4552 let shorthand = CssPropertyWithConditionsVec::parse_property_segment(
4553 "padding: 10px",
4554 &inherited,
4555 &key_map,
4556 )
4557 .expect("padding shorthand must parse");
4558 assert!(shorthand.len() > 1, "padding must expand to >1 property");
4559 for p in &shorthand {
4560 assert_eq!(p.apply_if.as_slice(), inherited.as_slice());
4561 }
4562
4563 for s in ["", " ", "color", "color:", ": red", "nosuchprop: red", "\u{1F600}"] {
4564 assert!(
4565 CssPropertyWithConditionsVec::parse_property_segment(s, &[], &key_map).is_none(),
4566 "{s:?} must not parse"
4567 );
4568 }
4569 }
4570
4571 #[cfg(feature = "parser")]
4572 #[test]
4573 fn parse_block_segment_requires_balanced_braces() {
4574 let key_map = crate::props::property::CssKeyMap::get();
4575 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4577 "color: red",
4578 &[],
4579 &key_map
4580 )
4581 .is_none());
4582 assert!(CssPropertyWithConditionsVec::parse_block_segment(
4584 ":hover { color: red",
4585 &[],
4586 &key_map
4587 )
4588 .is_none());
4589 assert!(
4591 CssPropertyWithConditionsVec::parse_block_segment("}{", &[], &key_map).is_none()
4592 );
4593 let empty = CssPropertyWithConditionsVec::parse_block_segment(
4595 ":hover {}",
4596 &[],
4597 &key_map,
4598 );
4599 assert!(empty.is_none());
4601 let ok = CssPropertyWithConditionsVec::parse_block_segment(
4603 ":hover { color: red; }",
4604 &[],
4605 &key_map,
4606 )
4607 .expect("valid block");
4608 assert_eq!(ok.len(), 1);
4609 assert!(ok[0].is_pseudo_state_only());
4610 }
4611
4612 #[cfg(feature = "parser")]
4613 #[test]
4614 fn parse_with_conditions_prepends_inherited_conditions() {
4615 let inherited = vec![DynamicSelector::Os(OsCondition::Linux)];
4616 let props =
4617 CssPropertyWithConditionsVec::parse_with_conditions("color: red;", &inherited)
4618 .into_library_owned_vec();
4619 assert_eq!(props.len(), 1);
4620 assert_eq!(props[0].apply_if.as_slice(), inherited.as_slice());
4621
4622 let none = CssPropertyWithConditionsVec::parse_with_conditions(" ", &inherited)
4624 .into_library_owned_vec();
4625 assert!(none.is_empty());
4626 }
4627
4628 #[cfg(feature = "parser")]
4629 #[test]
4630 fn parsed_media_selector_evaluates_against_a_context() {
4631 let props =
4634 CssPropertyWithConditionsVec::parse("@media (min-width: 800px) { color: red; }")
4635 .into_library_owned_vec();
4636 assert_eq!(props.len(), 1);
4637 let base = DynamicSelectorContext::default();
4638 assert!(props[0].matches(&base.with_viewport(1024.0, 768.0)));
4639 assert!(props[0].matches(&base.with_viewport(800.0, 600.0)));
4640 assert!(!props[0].matches(&base.with_viewport(799.0, 600.0)));
4641 }
4642}