1use alloc::string::{String, ToString};
5use core::fmt;
6
7#[cfg(feature = "parser")]
8use crate::props::basic::{
9 error::{InvalidValueErr, InvalidValueErrOwned},
10 length::parse_percentage_value,
11};
12use crate::props::{
13 basic::length::{PercentageParseError, PercentageValue},
14 formatter::PrintAsCssValue,
15};
16
17#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
21#[repr(C)]
22pub struct StyleOpacity {
23 pub inner: PercentageValue,
24}
25
26impl Default for StyleOpacity {
27 fn default() -> Self {
28 Self {
29 inner: PercentageValue::const_new(100),
30 }
31 }
32}
33
34impl PrintAsCssValue for StyleOpacity {
35 fn print_as_css_value(&self) -> String {
36 format!("{}", self.inner.normalized())
37 }
38}
39
40#[cfg(feature = "parser")]
41impl_percentage_value!(StyleOpacity);
42
43#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47#[repr(C)]
48#[derive(Default)]
49pub enum StyleVisibility {
50 #[default]
51 Visible,
52 Hidden,
53 Collapse,
54}
55
56
57impl PrintAsCssValue for StyleVisibility {
58 fn print_as_css_value(&self) -> String {
59 String::from(match self {
60 Self::Visible => "visible",
61 Self::Hidden => "hidden",
62 Self::Collapse => "collapse",
63 })
64 }
65}
66
67#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72#[repr(C)]
73#[derive(Default)]
74pub enum StyleMixBlendMode {
75 #[default]
76 Normal,
77 Multiply,
78 Screen,
79 Overlay,
80 Darken,
81 Lighten,
82 ColorDodge,
83 ColorBurn,
84 HardLight,
85 SoftLight,
86 Difference,
87 Exclusion,
88 Hue,
89 Saturation,
90 Color,
91 Luminosity,
92}
93
94
95impl fmt::Display for StyleMixBlendMode {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(
98 f,
99 "{}",
100 match self {
101 Self::Normal => "normal",
102 Self::Multiply => "multiply",
103 Self::Screen => "screen",
104 Self::Overlay => "overlay",
105 Self::Darken => "darken",
106 Self::Lighten => "lighten",
107 Self::ColorDodge => "color-dodge",
108 Self::ColorBurn => "color-burn",
109 Self::HardLight => "hard-light",
110 Self::SoftLight => "soft-light",
111 Self::Difference => "difference",
112 Self::Exclusion => "exclusion",
113 Self::Hue => "hue",
114 Self::Saturation => "saturation",
115 Self::Color => "color",
116 Self::Luminosity => "luminosity",
117 }
118 )
119 }
120}
121
122impl PrintAsCssValue for StyleMixBlendMode {
123 fn print_as_css_value(&self) -> String {
124 self.to_string()
125 }
126}
127
128#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
133#[repr(C)]
134#[derive(Default)]
135pub enum StyleCursor {
136 Alias,
137 AllScroll,
138 Cell,
139 ColResize,
140 ContextMenu,
141 Copy,
142 Crosshair,
143 #[default]
144 Default,
145 EResize,
146 EwResize,
147 Grab,
148 Grabbing,
149 Help,
150 Move,
151 NResize,
152 NsResize,
153 NeswResize,
154 NwseResize,
155 Pointer,
156 Progress,
157 RowResize,
158 SResize,
159 SeResize,
160 Text,
161 Unset,
162 VerticalText,
163 WResize,
164 Wait,
165 ZoomIn,
166 ZoomOut,
167}
168
169
170impl PrintAsCssValue for StyleCursor {
171 fn print_as_css_value(&self) -> String {
172 String::from(match self {
173 Self::Alias => "alias",
174 Self::AllScroll => "all-scroll",
175 Self::Cell => "cell",
176 Self::ColResize => "col-resize",
177 Self::ContextMenu => "context-menu",
178 Self::Copy => "copy",
179 Self::Crosshair => "crosshair",
180 Self::Default => "default",
181 Self::EResize => "e-resize",
182 Self::EwResize => "ew-resize",
183 Self::Grab => "grab",
184 Self::Grabbing => "grabbing",
185 Self::Help => "help",
186 Self::Move => "move",
187 Self::NResize => "n-resize",
188 Self::NsResize => "ns-resize",
189 Self::NeswResize => "nesw-resize",
190 Self::NwseResize => "nwse-resize",
191 Self::Pointer => "pointer",
192 Self::Progress => "progress",
193 Self::RowResize => "row-resize",
194 Self::SResize => "s-resize",
195 Self::SeResize => "se-resize",
196 Self::Text => "text",
197 Self::Unset => "unset",
198 Self::VerticalText => "vertical-text",
199 Self::WResize => "w-resize",
200 Self::Wait => "wait",
201 Self::ZoomIn => "zoom-in",
202 Self::ZoomOut => "zoom-out",
203 })
204 }
205}
206
207#[cfg(feature = "parser")]
210pub mod parsers {
211 #[allow(clippy::wildcard_imports)] use super::*;
213 use crate::corety::AzString;
214 use crate::props::basic::error::{InvalidValueErr, InvalidValueErrOwned};
215
216 #[derive(Clone, PartialEq, Eq)]
219 pub enum OpacityParseError<'a> {
220 ParsePercentage(PercentageParseError, &'a str),
221 OutOfRange(&'a str),
222 }
223 impl_debug_as_display!(OpacityParseError<'a>);
224 impl_display! { OpacityParseError<'a>, {
225 ParsePercentage(e, s) => format!("Invalid opacity value \"{}\": {}", s, e),
226 OutOfRange(s) => format!("Invalid opacity value \"{}\": must be between 0 and 1", s),
227 }}
228
229 #[derive(Debug, Clone, PartialEq, Eq)]
231 #[repr(C)]
232 pub struct PercentageParseErrorWithInput {
233 pub error: PercentageParseError,
234 pub input: AzString,
235 }
236
237 #[derive(Debug, Clone, PartialEq, Eq)]
238 #[repr(C, u8)]
239 pub enum OpacityParseErrorOwned {
240 ParsePercentage(PercentageParseErrorWithInput),
241 OutOfRange(AzString),
242 }
243
244 impl OpacityParseError<'_> {
245 #[must_use] pub fn to_contained(&self) -> OpacityParseErrorOwned {
246 match self {
247 Self::ParsePercentage(err, s) => {
248 OpacityParseErrorOwned::ParsePercentage(PercentageParseErrorWithInput { error: err.clone(), input: (*s).to_string().into() })
249 }
250 Self::OutOfRange(s) => OpacityParseErrorOwned::OutOfRange((*s).to_string().into()),
251 }
252 }
253 }
254
255 impl OpacityParseErrorOwned {
256 #[must_use] pub fn to_shared(&self) -> OpacityParseError<'_> {
257 match self {
258 Self::ParsePercentage(e) => {
259 OpacityParseError::ParsePercentage(e.error.clone(), e.input.as_str())
260 }
261 Self::OutOfRange(s) => OpacityParseError::OutOfRange(s.as_str()),
262 }
263 }
264 }
265
266 pub fn parse_style_opacity(input: &str) -> Result<StyleOpacity, OpacityParseError<'_>> {
270 let val = parse_percentage_value(input)
271 .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
272
273 let normalized = val.normalized();
274 if !(0.0..=1.0).contains(&normalized) {
275 return Err(OpacityParseError::OutOfRange(input));
276 }
277
278 Ok(StyleOpacity { inner: val })
279 }
280
281 #[derive(Clone, PartialEq, Eq)]
284 pub enum StyleVisibilityParseError<'a> {
285 InvalidValue(InvalidValueErr<'a>),
286 }
287 impl_debug_as_display!(StyleVisibilityParseError<'a>);
288 impl_display! { StyleVisibilityParseError<'a>, {
289 InvalidValue(e) => format!("Invalid visibility value: \"{}\"", e.0),
290 }}
291 impl_from!(InvalidValueErr<'a>, StyleVisibilityParseError::InvalidValue);
292
293 #[derive(Debug, Clone, PartialEq, Eq)]
294 #[repr(C, u8)]
295 pub enum StyleVisibilityParseErrorOwned {
296 InvalidValue(InvalidValueErrOwned),
297 }
298
299 impl StyleVisibilityParseError<'_> {
300 #[must_use] pub fn to_contained(&self) -> StyleVisibilityParseErrorOwned {
301 match self {
302 Self::InvalidValue(e) => {
303 StyleVisibilityParseErrorOwned::InvalidValue(e.to_contained())
304 }
305 }
306 }
307 }
308
309 impl StyleVisibilityParseErrorOwned {
310 #[must_use] pub fn to_shared(&self) -> StyleVisibilityParseError<'_> {
311 match self {
312 Self::InvalidValue(e) => StyleVisibilityParseError::InvalidValue(e.to_shared()),
313 }
314 }
315 }
316
317 pub fn parse_style_visibility(
321 input: &str,
322 ) -> Result<StyleVisibility, StyleVisibilityParseError<'_>> {
323 let input = input.trim();
324 match input {
325 "visible" => Ok(StyleVisibility::Visible),
326 "hidden" => Ok(StyleVisibility::Hidden),
327 "collapse" => Ok(StyleVisibility::Collapse),
328 _ => Err(InvalidValueErr(input).into()),
329 }
330 }
331
332 #[derive(Clone, PartialEq, Eq)]
335 pub enum MixBlendModeParseError<'a> {
336 InvalidValue(InvalidValueErr<'a>),
337 }
338 impl_debug_as_display!(MixBlendModeParseError<'a>);
339 impl_display! { MixBlendModeParseError<'a>, {
340 InvalidValue(e) => format!("Invalid mix-blend-mode value: \"{}\"", e.0),
341 }}
342 impl_from!(InvalidValueErr<'a>, MixBlendModeParseError::InvalidValue);
343
344 #[derive(Debug, Clone, PartialEq, Eq)]
345 #[repr(C, u8)]
346 pub enum MixBlendModeParseErrorOwned {
347 InvalidValue(InvalidValueErrOwned),
348 }
349
350 impl MixBlendModeParseError<'_> {
351 #[must_use] pub fn to_contained(&self) -> MixBlendModeParseErrorOwned {
352 match self {
353 Self::InvalidValue(e) => {
354 MixBlendModeParseErrorOwned::InvalidValue(e.to_contained())
355 }
356 }
357 }
358 }
359
360 impl MixBlendModeParseErrorOwned {
361 #[must_use] pub fn to_shared(&self) -> MixBlendModeParseError<'_> {
362 match self {
363 Self::InvalidValue(e) => MixBlendModeParseError::InvalidValue(e.to_shared()),
364 }
365 }
366 }
367
368 pub fn parse_style_mix_blend_mode(
372 input: &str,
373 ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'_>> {
374 let input = input.trim();
375 match input {
376 "normal" => Ok(StyleMixBlendMode::Normal),
377 "multiply" => Ok(StyleMixBlendMode::Multiply),
378 "screen" => Ok(StyleMixBlendMode::Screen),
379 "overlay" => Ok(StyleMixBlendMode::Overlay),
380 "darken" => Ok(StyleMixBlendMode::Darken),
381 "lighten" => Ok(StyleMixBlendMode::Lighten),
382 "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
383 "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
384 "hard-light" => Ok(StyleMixBlendMode::HardLight),
385 "soft-light" => Ok(StyleMixBlendMode::SoftLight),
386 "difference" => Ok(StyleMixBlendMode::Difference),
387 "exclusion" => Ok(StyleMixBlendMode::Exclusion),
388 "hue" => Ok(StyleMixBlendMode::Hue),
389 "saturation" => Ok(StyleMixBlendMode::Saturation),
390 "color" => Ok(StyleMixBlendMode::Color),
391 "luminosity" => Ok(StyleMixBlendMode::Luminosity),
392 _ => Err(InvalidValueErr(input).into()),
393 }
394 }
395
396 #[derive(Clone, PartialEq, Eq)]
399 pub enum CursorParseError<'a> {
400 InvalidValue(InvalidValueErr<'a>),
401 }
402 impl_debug_as_display!(CursorParseError<'a>);
403 impl_display! { CursorParseError<'a>, {
404 InvalidValue(e) => format!("Invalid cursor value: \"{}\"", e.0),
405 }}
406 impl_from!(InvalidValueErr<'a>, CursorParseError::InvalidValue);
407
408 #[derive(Debug, Clone, PartialEq, Eq)]
409 #[repr(C, u8)]
410 pub enum CursorParseErrorOwned {
411 InvalidValue(InvalidValueErrOwned),
412 }
413
414 impl CursorParseError<'_> {
415 #[must_use] pub fn to_contained(&self) -> CursorParseErrorOwned {
416 match self {
417 Self::InvalidValue(e) => CursorParseErrorOwned::InvalidValue(e.to_contained()),
418 }
419 }
420 }
421
422 impl CursorParseErrorOwned {
423 #[must_use] pub fn to_shared(&self) -> CursorParseError<'_> {
424 match self {
425 Self::InvalidValue(e) => CursorParseError::InvalidValue(e.to_shared()),
426 }
427 }
428 }
429
430 pub fn parse_style_cursor(input: &str) -> Result<StyleCursor, CursorParseError<'_>> {
434 let input = input.trim();
435 match input {
436 "alias" => Ok(StyleCursor::Alias),
437 "all-scroll" => Ok(StyleCursor::AllScroll),
438 "cell" => Ok(StyleCursor::Cell),
439 "col-resize" => Ok(StyleCursor::ColResize),
440 "context-menu" => Ok(StyleCursor::ContextMenu),
441 "copy" => Ok(StyleCursor::Copy),
442 "crosshair" => Ok(StyleCursor::Crosshair),
443 "default" => Ok(StyleCursor::Default),
444 "e-resize" => Ok(StyleCursor::EResize),
445 "ew-resize" => Ok(StyleCursor::EwResize),
446 "grab" => Ok(StyleCursor::Grab),
447 "grabbing" => Ok(StyleCursor::Grabbing),
448 "help" => Ok(StyleCursor::Help),
449 "move" => Ok(StyleCursor::Move),
450 "n-resize" => Ok(StyleCursor::NResize),
451 "ns-resize" => Ok(StyleCursor::NsResize),
452 "nesw-resize" => Ok(StyleCursor::NeswResize),
453 "nwse-resize" => Ok(StyleCursor::NwseResize),
454 "pointer" => Ok(StyleCursor::Pointer),
455 "progress" => Ok(StyleCursor::Progress),
456 "row-resize" => Ok(StyleCursor::RowResize),
457 "s-resize" => Ok(StyleCursor::SResize),
458 "se-resize" => Ok(StyleCursor::SeResize),
459 "text" => Ok(StyleCursor::Text),
460 "unset" => Ok(StyleCursor::Unset),
461 "vertical-text" => Ok(StyleCursor::VerticalText),
462 "w-resize" => Ok(StyleCursor::WResize),
463 "wait" => Ok(StyleCursor::Wait),
464 "zoom-in" => Ok(StyleCursor::ZoomIn),
465 "zoom-out" => Ok(StyleCursor::ZoomOut),
466 _ => Err(InvalidValueErr(input).into()),
467 }
468 }
469}
470
471#[cfg(feature = "parser")]
472pub use self::parsers::*;
473
474#[cfg(all(test, feature = "parser"))]
475mod tests {
476 #![allow(clippy::float_cmp)]
478 use super::*;
479
480 #[test]
481 fn test_parse_opacity() {
482 assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
483 assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
484 assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
485 assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
486 assert_eq!(
487 parse_style_opacity(" 75% ").unwrap().inner.normalized(),
488 0.75
489 );
490 assert!(parse_style_opacity("1.1").is_err());
491 assert!(parse_style_opacity("-0.1").is_err());
492 assert!(parse_style_opacity("auto").is_err());
493 }
494
495 #[test]
496 fn test_parse_mix_blend_mode() {
497 assert_eq!(
498 parse_style_mix_blend_mode("multiply").unwrap(),
499 StyleMixBlendMode::Multiply
500 );
501 assert_eq!(
502 parse_style_mix_blend_mode("screen").unwrap(),
503 StyleMixBlendMode::Screen
504 );
505 assert_eq!(
506 parse_style_mix_blend_mode("color-dodge").unwrap(),
507 StyleMixBlendMode::ColorDodge
508 );
509 assert!(parse_style_mix_blend_mode("mix").is_err());
510 }
511
512 #[test]
513 fn test_parse_visibility() {
514 assert_eq!(
515 parse_style_visibility("visible").unwrap(),
516 StyleVisibility::Visible
517 );
518 assert_eq!(
519 parse_style_visibility("hidden").unwrap(),
520 StyleVisibility::Hidden
521 );
522 assert_eq!(
523 parse_style_visibility("collapse").unwrap(),
524 StyleVisibility::Collapse
525 );
526 assert_eq!(
527 parse_style_visibility(" visible ").unwrap(),
528 StyleVisibility::Visible
529 );
530 assert!(parse_style_visibility("none").is_err());
531 assert!(parse_style_visibility("show").is_err());
532 }
533
534 #[test]
535 fn test_parse_cursor() {
536 assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
537 assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
538 assert_eq!(
539 parse_style_cursor("col-resize").unwrap(),
540 StyleCursor::ColResize
541 );
542 assert_eq!(parse_style_cursor(" text ").unwrap(), StyleCursor::Text);
543 assert!(parse_style_cursor("hand").is_err()); }
545
546 #[test]
547 fn test_parse_object_fit() {
548 assert_eq!(parse_style_object_fit("fill").unwrap(), StyleObjectFit::Fill);
549 assert_eq!(parse_style_object_fit("contain").unwrap(), StyleObjectFit::Contain);
550 assert_eq!(parse_style_object_fit("cover").unwrap(), StyleObjectFit::Cover);
551 assert_eq!(parse_style_object_fit("none").unwrap(), StyleObjectFit::None);
552 assert_eq!(parse_style_object_fit("scale-down").unwrap(), StyleObjectFit::ScaleDown);
553 assert_eq!(parse_style_object_fit(" cover ").unwrap(), StyleObjectFit::Cover);
554 assert!(parse_style_object_fit("stretch").is_err());
555 assert!(parse_style_object_fit("").is_err());
556 }
557
558 #[test]
559 fn test_parse_text_orientation() {
560 assert_eq!(parse_style_text_orientation("mixed").unwrap(), StyleTextOrientation::Mixed);
561 assert_eq!(parse_style_text_orientation("upright").unwrap(), StyleTextOrientation::Upright);
562 assert_eq!(parse_style_text_orientation("sideways").unwrap(), StyleTextOrientation::Sideways);
563 assert_eq!(parse_style_text_orientation(" mixed ").unwrap(), StyleTextOrientation::Mixed);
564 assert!(parse_style_text_orientation("vertical").is_err());
565 }
566
567 #[test]
568 fn test_parse_object_position() {
569 use crate::props::style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical};
570 let centered = parse_style_object_position("center").unwrap();
571 assert_eq!(centered, parse_style_object_position("center center").unwrap());
572
573 let lt = parse_style_object_position("left top").unwrap();
574 assert_eq!(lt.horizontal, BackgroundPositionHorizontal::Left);
575 assert_eq!(lt.vertical, BackgroundPositionVertical::Top);
576
577 let rb = parse_style_object_position("right bottom").unwrap();
578 assert_eq!(rb.horizontal, BackgroundPositionHorizontal::Right);
579 assert_eq!(rb.vertical, BackgroundPositionVertical::Bottom);
580
581 assert!(parse_style_object_position("left top center").is_err());
582 assert!(parse_style_object_position("invalid").is_err());
583 }
584
585 #[test]
586 fn test_parse_aspect_ratio() {
587 assert_eq!(parse_style_aspect_ratio("auto").unwrap(), StyleAspectRatio::Auto);
588 assert_eq!(
589 parse_style_aspect_ratio("16 / 9").unwrap(),
590 StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
591 );
592 assert_eq!(
593 parse_style_aspect_ratio("16/9").unwrap(),
594 StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 })
595 );
596 assert_eq!(
597 parse_style_aspect_ratio("1.5").unwrap(),
598 StyleAspectRatio::Ratio(AspectRatioValue { width: 1500, height: 1000 })
599 );
600 assert_eq!(
601 parse_style_aspect_ratio(" 4 / 3 ").unwrap(),
602 StyleAspectRatio::Ratio(AspectRatioValue { width: 4000, height: 3000 })
603 );
604 assert!(parse_style_aspect_ratio("0 / 1").is_err());
605 assert!(parse_style_aspect_ratio("1 / 0").is_err());
606 assert!(parse_style_aspect_ratio("-1 / 1").is_err());
607 assert!(parse_style_aspect_ratio("abc").is_err());
608 }
609}
610
611#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
616#[repr(C)]
617#[derive(Default)]
618pub enum StyleObjectFit {
619 #[default]
620 Fill,
621 Contain,
622 Cover,
623 None,
624 ScaleDown,
625}
626
627
628crate::impl_option!(StyleObjectFit, OptionStyleObjectFit, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
629
630impl PrintAsCssValue for StyleObjectFit {
631 fn print_as_css_value(&self) -> String {
632 String::from(match self {
633 Self::Fill => "fill",
634 Self::Contain => "contain",
635 Self::Cover => "cover",
636 Self::None => "none",
637 Self::ScaleDown => "scale-down",
638 })
639 }
640}
641
642#[cfg(feature = "parser")]
643#[derive(Clone, PartialEq, Eq)]
644pub enum StyleObjectFitParseError<'a> {
645 InvalidValue(&'a str),
646}
647
648#[cfg(feature = "parser")]
649crate::impl_debug_as_display!(StyleObjectFitParseError<'a>);
650
651#[cfg(feature = "parser")]
652crate::impl_display! { StyleObjectFitParseError<'a>, {
653 InvalidValue(val) => format!("Invalid object-fit value: \"{}\"", val),
654}}
655
656#[cfg(feature = "parser")]
657#[derive(Debug, Clone, PartialEq, Eq)]
658#[repr(C, u8)]
659pub enum StyleObjectFitParseErrorOwned {
660 InvalidValue(crate::AzString),
661}
662
663#[cfg(feature = "parser")]
664impl StyleObjectFitParseError<'_> {
665 #[must_use] pub fn to_contained(&self) -> StyleObjectFitParseErrorOwned {
666 match self {
667 Self::InvalidValue(s) => StyleObjectFitParseErrorOwned::InvalidValue((*s).to_string().into()),
668 }
669 }
670}
671
672#[cfg(feature = "parser")]
673impl StyleObjectFitParseErrorOwned {
674 #[must_use] pub fn to_shared(&self) -> StyleObjectFitParseError<'_> {
675 match self {
676 Self::InvalidValue(s) => StyleObjectFitParseError::InvalidValue(s.as_str()),
677 }
678 }
679}
680
681#[cfg(feature = "parser")]
682pub fn parse_style_object_fit(
686 input: &str,
687) -> Result<StyleObjectFit, StyleObjectFitParseError<'_>> {
688 let input = input.trim();
689 match input {
690 "fill" => Ok(StyleObjectFit::Fill),
691 "contain" => Ok(StyleObjectFit::Contain),
692 "cover" => Ok(StyleObjectFit::Cover),
693 "none" => Ok(StyleObjectFit::None),
694 "scale-down" => Ok(StyleObjectFit::ScaleDown),
695 _ => Err(StyleObjectFitParseError::InvalidValue(input)),
696 }
697}
698
699#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
704#[repr(C)]
705#[derive(Default)]
706pub enum StyleTextOrientation {
707 #[default]
708 Mixed,
709 Upright,
710 Sideways,
711}
712
713
714crate::impl_option!(StyleTextOrientation, OptionStyleTextOrientation, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
715
716impl PrintAsCssValue for StyleTextOrientation {
717 fn print_as_css_value(&self) -> String {
718 String::from(match self {
719 Self::Mixed => "mixed",
720 Self::Upright => "upright",
721 Self::Sideways => "sideways",
722 })
723 }
724}
725
726#[cfg(feature = "parser")]
727#[derive(Clone, PartialEq, Eq)]
728pub enum StyleTextOrientationParseError<'a> {
729 InvalidValue(&'a str),
730}
731
732#[cfg(feature = "parser")]
733crate::impl_debug_as_display!(StyleTextOrientationParseError<'a>);
734
735#[cfg(feature = "parser")]
736crate::impl_display! { StyleTextOrientationParseError<'a>, {
737 InvalidValue(val) => format!("Invalid text-orientation value: \"{}\"", val),
738}}
739
740#[cfg(feature = "parser")]
741#[derive(Debug, Clone, PartialEq, Eq)]
742#[repr(C, u8)]
743pub enum StyleTextOrientationParseErrorOwned {
744 InvalidValue(crate::AzString),
745}
746
747#[cfg(feature = "parser")]
748impl StyleTextOrientationParseError<'_> {
749 #[must_use] pub fn to_contained(&self) -> StyleTextOrientationParseErrorOwned {
750 match self {
751 Self::InvalidValue(s) => StyleTextOrientationParseErrorOwned::InvalidValue((*s).to_string().into()),
752 }
753 }
754}
755
756#[cfg(feature = "parser")]
757impl StyleTextOrientationParseErrorOwned {
758 #[must_use] pub fn to_shared(&self) -> StyleTextOrientationParseError<'_> {
759 match self {
760 Self::InvalidValue(s) => StyleTextOrientationParseError::InvalidValue(s.as_str()),
761 }
762 }
763}
764
765#[cfg(feature = "parser")]
766pub fn parse_style_text_orientation(
770 input: &str,
771) -> Result<StyleTextOrientation, StyleTextOrientationParseError<'_>> {
772 let input = input.trim();
773 match input {
774 "mixed" => Ok(StyleTextOrientation::Mixed),
775 "upright" => Ok(StyleTextOrientation::Upright),
776 "sideways" => Ok(StyleTextOrientation::Sideways),
777 _ => Err(StyleTextOrientationParseError::InvalidValue(input)),
778 }
779}
780
781#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
786#[repr(C)]
787pub struct StyleObjectPosition {
788 pub horizontal: crate::props::style::background::BackgroundPositionHorizontal,
789 pub vertical: crate::props::style::background::BackgroundPositionVertical,
790}
791
792impl Default for StyleObjectPosition {
793 fn default() -> Self {
794 use crate::props::basic::pixel::PixelValue;
795 Self {
796 horizontal: crate::props::style::background::BackgroundPositionHorizontal::Exact(
797 PixelValue::percent(50.0),
798 ),
799 vertical: crate::props::style::background::BackgroundPositionVertical::Exact(
800 PixelValue::percent(50.0),
801 ),
802 }
803 }
804}
805
806crate::impl_option!(StyleObjectPosition, OptionStyleObjectPosition, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
807
808impl PrintAsCssValue for StyleObjectPosition {
809 fn print_as_css_value(&self) -> String {
810 format!(
811 "{} {}",
812 self.horizontal.print_as_css_value(),
813 self.vertical.print_as_css_value()
814 )
815 }
816}
817
818#[cfg(feature = "parser")]
819#[derive(Clone, PartialEq, Eq)]
820pub enum StyleObjectPositionParseError<'a> {
821 InvalidValue(&'a str),
822}
823
824#[cfg(feature = "parser")]
825crate::impl_debug_as_display!(StyleObjectPositionParseError<'a>);
826
827#[cfg(feature = "parser")]
828crate::impl_display! { StyleObjectPositionParseError<'a>, {
829 InvalidValue(val) => format!("Invalid object-position value: \"{}\"", val),
830}}
831
832#[cfg(feature = "parser")]
833#[derive(Debug, Clone, PartialEq, Eq)]
834#[repr(C, u8)]
835pub enum StyleObjectPositionParseErrorOwned {
836 InvalidValue(crate::AzString),
837}
838
839#[cfg(feature = "parser")]
840impl StyleObjectPositionParseError<'_> {
841 #[must_use] pub fn to_contained(&self) -> StyleObjectPositionParseErrorOwned {
842 match self {
843 Self::InvalidValue(s) => StyleObjectPositionParseErrorOwned::InvalidValue((*s).to_string().into()),
844 }
845 }
846}
847
848#[cfg(feature = "parser")]
849impl StyleObjectPositionParseErrorOwned {
850 #[must_use] pub fn to_shared(&self) -> StyleObjectPositionParseError<'_> {
851 match self {
852 Self::InvalidValue(s) => StyleObjectPositionParseError::InvalidValue(s.as_str()),
853 }
854 }
855}
856
857#[cfg(feature = "parser")]
860pub fn parse_style_object_position(
864 input: &str,
865) -> Result<StyleObjectPosition, StyleObjectPositionParseError<'_>> {
866 use crate::props::style::background::{
867 BackgroundPositionHorizontal, BackgroundPositionVertical,
868 };
869 use crate::props::basic::pixel::parse_pixel_value;
870
871 let input = input.trim();
872 let parts: Vec<&str> = input.split_whitespace().collect();
873
874 let (h, v) = match parts.len() {
875 1 => {
876 let val = parts[0];
877 match val {
878 "center" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Center),
879 "left" => (BackgroundPositionHorizontal::Left, BackgroundPositionVertical::Center),
880 "right" => (BackgroundPositionHorizontal::Right, BackgroundPositionVertical::Center),
881 "top" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Top),
882 "bottom" => (BackgroundPositionHorizontal::Center, BackgroundPositionVertical::Bottom),
883 _ => {
884 let px = parse_pixel_value(val)
885 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
886 (BackgroundPositionHorizontal::Exact(px), BackgroundPositionVertical::Exact(px))
887 }
888 }
889 }
890 2 => {
891 let (a, b) = (parts[0], parts[1]);
896 let both_keywords = matches!(a, "left" | "center" | "right" | "top" | "bottom")
897 && matches!(b, "left" | "center" | "right" | "top" | "bottom");
898 let reversed = both_keywords
899 && (matches!(a, "top" | "bottom") || matches!(b, "left" | "right"));
900 let (h_str, v_str) = if reversed { (b, a) } else { (a, b) };
901
902 let h = match h_str {
903 "left" => BackgroundPositionHorizontal::Left,
904 "center" => BackgroundPositionHorizontal::Center,
905 "right" => BackgroundPositionHorizontal::Right,
906 other => {
907 let px = parse_pixel_value(other)
908 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
909 BackgroundPositionHorizontal::Exact(px)
910 }
911 };
912 let v = match v_str {
913 "top" => BackgroundPositionVertical::Top,
914 "center" => BackgroundPositionVertical::Center,
915 "bottom" => BackgroundPositionVertical::Bottom,
916 other => {
917 let px = parse_pixel_value(other)
918 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
919 BackgroundPositionVertical::Exact(px)
920 }
921 };
922 (h, v)
923 }
924 _ => return Err(StyleObjectPositionParseError::InvalidValue(input)),
925 };
926
927 Ok(StyleObjectPosition { horizontal: h, vertical: v })
928}
929
930#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
934#[repr(C)]
935pub struct AspectRatioValue {
936 pub width: u32,
937 pub height: u32,
938}
939
940impl AspectRatioValue {
941 fn fmt_component(v: u32) -> String {
946 let int = v / 1000;
947 let frac = v % 1000;
948 if frac == 0 {
949 int.to_string()
950 } else {
951 let frac_str = format!("{frac:03}");
952 format!("{int}.{}", frac_str.trim_end_matches('0'))
953 }
954 }
955}
956#[allow(variant_size_differences)] #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
962#[repr(C, u8)]
963#[derive(Default)]
964pub enum StyleAspectRatio {
965 #[default]
967 Auto,
968 Ratio(AspectRatioValue),
970}
971
972
973crate::impl_option!(StyleAspectRatio, OptionStyleAspectRatio, [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]);
974
975impl PrintAsCssValue for StyleAspectRatio {
976 fn print_as_css_value(&self) -> String {
977 match self {
978 Self::Auto => String::from("auto"),
979 Self::Ratio(r) => format!(
980 "{} / {}",
981 AspectRatioValue::fmt_component(r.width),
982 AspectRatioValue::fmt_component(r.height)
983 ),
984 }
985 }
986}
987
988#[cfg(feature = "parser")]
989#[derive(Clone, PartialEq, Eq)]
990pub enum StyleAspectRatioParseError<'a> {
991 InvalidValue(&'a str),
992}
993
994#[cfg(feature = "parser")]
995crate::impl_debug_as_display!(StyleAspectRatioParseError<'a>);
996
997#[cfg(feature = "parser")]
998crate::impl_display! { StyleAspectRatioParseError<'a>, {
999 InvalidValue(val) => format!("Invalid aspect-ratio value: \"{}\"", val),
1000}}
1001
1002#[cfg(feature = "parser")]
1003#[derive(Debug, Clone, PartialEq, Eq)]
1004#[repr(C, u8)]
1005pub enum StyleAspectRatioParseErrorOwned {
1006 InvalidValue(crate::AzString),
1007}
1008
1009#[cfg(feature = "parser")]
1010impl StyleAspectRatioParseError<'_> {
1011 #[must_use] pub fn to_contained(&self) -> StyleAspectRatioParseErrorOwned {
1012 match self {
1013 Self::InvalidValue(s) => StyleAspectRatioParseErrorOwned::InvalidValue((*s).to_string().into()),
1014 }
1015 }
1016}
1017
1018#[cfg(feature = "parser")]
1019impl StyleAspectRatioParseErrorOwned {
1020 #[must_use] pub fn to_shared(&self) -> StyleAspectRatioParseError<'_> {
1021 match self {
1022 Self::InvalidValue(s) => StyleAspectRatioParseError::InvalidValue(s.as_str()),
1023 }
1024 }
1025}
1026
1027#[cfg(feature = "parser")]
1031#[inline]
1032#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1033const fn aspect_f32_to_u32(v: f32) -> u32 {
1034 v as u32
1035}
1036
1037#[cfg(feature = "parser")]
1044fn ratio_from_components(
1045 w: f32,
1046 h: f32,
1047 input: &str,
1048) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'_>> {
1049 if !(w > 0.0 && w <= 100_000.0 && h > 0.0 && h <= 100_000.0) {
1050 return Err(StyleAspectRatioParseError::InvalidValue(input));
1051 }
1052 let width = aspect_f32_to_u32((w * 1000.0).round());
1053 let height = aspect_f32_to_u32((h * 1000.0).round());
1054 if width == 0 || height == 0 {
1055 return Err(StyleAspectRatioParseError::InvalidValue(input));
1056 }
1057 Ok(StyleAspectRatio::Ratio(AspectRatioValue { width, height }))
1058}
1059
1060#[cfg(feature = "parser")]
1062pub fn parse_style_aspect_ratio(
1066 input: &str,
1067) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'_>> {
1068 let input = input.trim();
1069 if input == "auto" {
1070 return Ok(StyleAspectRatio::Auto);
1071 }
1072 if let Some(slash_pos) = input.find('/') {
1074 let w_str = input[..slash_pos].trim();
1075 let h_str = input[slash_pos + 1..].trim();
1076 let w: f32 = w_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1077 let h: f32 = h_str.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1078 return ratio_from_components(w, h, input);
1079 }
1080 let w: f32 = input.parse().map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1082 ratio_from_components(w, 1.0, input)
1083}
1084
1085#[cfg(all(test, feature = "parser"))]
1086#[allow(
1087 clippy::float_cmp,
1088 clippy::unreadable_literal,
1089 clippy::too_many_lines,
1090 clippy::cast_precision_loss
1091)]
1092mod autotest_generated {
1093 use super::*;
1094 use crate::{
1095 props::{
1096 basic::{
1097 error::ParseFloatError as CssParseFloatError, pixel::PixelValue,
1098 },
1099 formatter::PrintAsCssValue,
1100 style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical},
1101 },
1102 };
1103
1104 const ALL_VISIBILITY: [StyleVisibility; 3] = [
1105 StyleVisibility::Visible,
1106 StyleVisibility::Hidden,
1107 StyleVisibility::Collapse,
1108 ];
1109
1110 const ALL_BLEND_MODES: [StyleMixBlendMode; 16] = [
1111 StyleMixBlendMode::Normal,
1112 StyleMixBlendMode::Multiply,
1113 StyleMixBlendMode::Screen,
1114 StyleMixBlendMode::Overlay,
1115 StyleMixBlendMode::Darken,
1116 StyleMixBlendMode::Lighten,
1117 StyleMixBlendMode::ColorDodge,
1118 StyleMixBlendMode::ColorBurn,
1119 StyleMixBlendMode::HardLight,
1120 StyleMixBlendMode::SoftLight,
1121 StyleMixBlendMode::Difference,
1122 StyleMixBlendMode::Exclusion,
1123 StyleMixBlendMode::Hue,
1124 StyleMixBlendMode::Saturation,
1125 StyleMixBlendMode::Color,
1126 StyleMixBlendMode::Luminosity,
1127 ];
1128
1129 const ALL_CURSORS: [StyleCursor; 30] = [
1130 StyleCursor::Alias,
1131 StyleCursor::AllScroll,
1132 StyleCursor::Cell,
1133 StyleCursor::ColResize,
1134 StyleCursor::ContextMenu,
1135 StyleCursor::Copy,
1136 StyleCursor::Crosshair,
1137 StyleCursor::Default,
1138 StyleCursor::EResize,
1139 StyleCursor::EwResize,
1140 StyleCursor::Grab,
1141 StyleCursor::Grabbing,
1142 StyleCursor::Help,
1143 StyleCursor::Move,
1144 StyleCursor::NResize,
1145 StyleCursor::NsResize,
1146 StyleCursor::NeswResize,
1147 StyleCursor::NwseResize,
1148 StyleCursor::Pointer,
1149 StyleCursor::Progress,
1150 StyleCursor::RowResize,
1151 StyleCursor::SResize,
1152 StyleCursor::SeResize,
1153 StyleCursor::Text,
1154 StyleCursor::Unset,
1155 StyleCursor::VerticalText,
1156 StyleCursor::WResize,
1157 StyleCursor::Wait,
1158 StyleCursor::ZoomIn,
1159 StyleCursor::ZoomOut,
1160 ];
1161
1162 const ALL_OBJECT_FIT: [StyleObjectFit; 5] = [
1163 StyleObjectFit::Fill,
1164 StyleObjectFit::Contain,
1165 StyleObjectFit::Cover,
1166 StyleObjectFit::None,
1167 StyleObjectFit::ScaleDown,
1168 ];
1169
1170 const ALL_TEXT_ORIENTATION: [StyleTextOrientation; 3] = [
1171 StyleTextOrientation::Mixed,
1172 StyleTextOrientation::Upright,
1173 StyleTextOrientation::Sideways,
1174 ];
1175
1176 const HOSTILE_KEYWORDS: [&str; 14] = [
1179 "",
1180 " ",
1181 "\t\n\r",
1182 "\u{a0}", ";",
1184 "{}",
1185 "/*",
1186 "0",
1187 "-1",
1188 "NaN",
1189 "inf",
1190 "\u{1F600}", "e\u{0301}", "\u{0665}", ];
1194
1195 #[test]
1198 fn blend_mode_display_is_well_formed_for_every_variant() {
1199 for mode in ALL_BLEND_MODES {
1200 let shown = mode.to_string();
1201 assert!(!shown.is_empty(), "{mode:?} renders as an empty string");
1202 assert!(
1203 shown
1204 .chars()
1205 .all(|c| c.is_ascii_lowercase() || c == '-'),
1206 "{mode:?} renders as {shown:?}, which is not a CSS ident"
1207 );
1208 assert_eq!(shown, mode.print_as_css_value());
1211 }
1212 }
1213
1214 #[test]
1215 fn blend_mode_display_of_default_is_normal() {
1216 assert_eq!(StyleMixBlendMode::default().to_string(), "normal");
1217 assert_eq!(StyleMixBlendMode::default(), StyleMixBlendMode::Normal);
1218 }
1219
1220 #[test]
1221 fn blend_mode_display_survives_width_and_precision_flags() {
1222 assert_eq!(format!("{:>10}", StyleMixBlendMode::Normal), "normal");
1226 assert_eq!(format!("{:.2}", StyleMixBlendMode::Multiply), "multiply");
1227 assert_eq!(format!("{:*^30}", StyleMixBlendMode::ColorDodge), "color-dodge");
1228 }
1229
1230 #[test]
1233 fn opacity_rejects_empty_and_whitespace_only_input() {
1234 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{a0}"] {
1235 assert!(
1236 parse_style_opacity(input).is_err(),
1237 "{input:?} must not parse as an opacity"
1238 );
1239 }
1240 }
1241
1242 #[test]
1243 fn opacity_rejects_garbage() {
1244 for input in [
1245 "auto", "abc", "%", ";;;", "50%%", "#0.5", "0.5;garbage", "1 2", "rgb(0,0,0)",
1246 "0,5", "--", "..", "-", ".",
1247 ] {
1248 assert!(
1249 parse_style_opacity(input).is_err(),
1250 "{input:?} must not parse as an opacity"
1251 );
1252 }
1253 }
1254
1255 #[test]
1256 fn opacity_boundary_numbers() {
1257 assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
1259 assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
1260 assert_eq!(parse_style_opacity("0%").unwrap().inner.normalized(), 0.0);
1261 assert_eq!(parse_style_opacity("100%").unwrap().inner.normalized(), 1.0);
1262 assert_eq!(parse_style_opacity("-0").unwrap().inner.normalized(), 0.0);
1264 assert_eq!(parse_style_opacity("-0%").unwrap().inner.normalized(), 0.0);
1265 assert!(parse_style_opacity("0.0000001").is_ok());
1267
1268 for input in ["1.001", "1.1", "2", "101%", "-0.001", "-1", "-100%"] {
1270 assert!(
1271 matches!(
1272 parse_style_opacity(input),
1273 Err(OpacityParseError::OutOfRange(_))
1274 ),
1275 "{input:?} should be rejected as out-of-range"
1276 );
1277 }
1278
1279 for input in ["1e39", "3.5e38", "9223372036854775807", "1e30"] {
1282 assert!(
1283 parse_style_opacity(input).is_err(),
1284 "{input:?} should be rejected as out-of-range"
1285 );
1286 }
1287
1288 for input in ["NaN", "nan", "inf", "infinity", "-inf", "-NaN"] {
1290 assert!(
1291 parse_style_opacity(input).is_err(),
1292 "{input:?} should be rejected"
1293 );
1294 }
1295 }
1296
1297 #[test]
1298 fn opacity_trims_but_rejects_trailing_junk() {
1299 assert_eq!(
1300 parse_style_opacity(" 0.5 ").unwrap().inner.normalized(),
1301 0.5
1302 );
1303 assert_eq!(
1304 parse_style_opacity("\t50%\n").unwrap().inner.normalized(),
1305 0.5
1306 );
1307 for input in ["0.5;", "0.5 !important", "0.5px", "0.5 0.5"] {
1308 assert!(
1309 parse_style_opacity(input).is_err(),
1310 "{input:?} must not parse as an opacity"
1311 );
1312 }
1313
1314 assert_eq!(parse_style_opacity("50 %").unwrap().inner.normalized(), 0.5);
1318 }
1319
1320 #[test]
1321 fn opacity_non_numeric_unicode_does_not_panic() {
1322 for input in [
1327 "\u{1F600}", "\u{1F600}0.5", "0.5\u{0301}", "\u{2603}%", "\u{4F60}\u{597D}", "\u{202E}0.5", ] {
1334 assert!(
1335 parse_style_opacity(input).is_err(),
1336 "{input:?} must not parse as an opacity"
1337 );
1338 }
1339 }
1340
1341 #[test]
1342 fn opacity_extremely_long_input_terminates() {
1343 let huge = "1".repeat(100_000);
1345 assert!(parse_style_opacity(&huge).is_err());
1346
1347 let tiny = format!("0.{}5", "0".repeat(100_000));
1350 assert_eq!(parse_style_opacity(&tiny).unwrap().inner.normalized(), 0.0);
1351
1352 let long_unit = format!("0.5{}", "z".repeat(100_000));
1354 assert!(parse_style_opacity(&long_unit).is_err());
1355 }
1356
1357 #[test]
1358 fn opacity_deeply_nested_brackets_do_not_stack_overflow() {
1359 let nested = "(".repeat(10_000);
1360 assert!(parse_style_opacity(&nested).is_err());
1361
1362 let wrapped = format!("{}0.5{}", "(".repeat(10_000), ")".repeat(10_000));
1363 assert!(parse_style_opacity(&wrapped).is_err());
1364 }
1365
1366 #[test]
1367 fn opacity_valid_minimal_positive_control() {
1368 assert!(parse_style_opacity("1").unwrap() == StyleOpacity::default());
1369 assert!(parse_style_opacity("50%").unwrap() == StyleOpacity::new(50.0));
1370 assert!(parse_style_opacity("0.5").unwrap() == StyleOpacity::new(50.0));
1371 assert!(parse_style_opacity("0.5").unwrap() == parse_style_opacity("50%").unwrap());
1373 }
1374
1375 #[test]
1376 fn opacity_round_trips_through_print_as_css_value_and_display() {
1377 for pct in [0.0f32, 12.5, 25.0, 50.0, 75.0, 99.9, 100.0] {
1378 let opacity = StyleOpacity::new(pct);
1379
1380 let printed = opacity.print_as_css_value();
1382 let reparsed = parse_style_opacity(&printed)
1383 .unwrap_or_else(|e| panic!("{printed:?} (from {pct}%) failed to re-parse: {e}"));
1384 assert_eq!(
1385 reparsed.inner.normalized(),
1386 opacity.inner.normalized(),
1387 "{pct}% printed as {printed:?} but re-parsed differently"
1388 );
1389
1390 let displayed = opacity.to_string();
1392 let reparsed = parse_style_opacity(&displayed)
1393 .unwrap_or_else(|e| panic!("{displayed:?} (from {pct}%) failed to re-parse: {e}"));
1394 assert_eq!(reparsed.inner.normalized(), opacity.inner.normalized());
1395 }
1396 }
1397
1398 #[test]
1401 fn visibility_parses_every_keyword_and_round_trips() {
1402 assert_eq!(parse_style_visibility("visible").unwrap(), StyleVisibility::Visible);
1403 assert_eq!(parse_style_visibility("hidden").unwrap(), StyleVisibility::Hidden);
1404 assert_eq!(parse_style_visibility("collapse").unwrap(), StyleVisibility::Collapse);
1405 assert_eq!(StyleVisibility::default(), StyleVisibility::Visible);
1406
1407 for v in ALL_VISIBILITY {
1408 let printed = v.print_as_css_value();
1409 assert!(!printed.is_empty());
1410 assert_eq!(parse_style_visibility(&printed).unwrap(), v);
1411 assert_eq!(parse_style_visibility(&format!(" {printed}\t")).unwrap(), v);
1413 }
1414 }
1415
1416 #[test]
1417 fn visibility_rejects_hostile_input() {
1418 for input in HOSTILE_KEYWORDS {
1419 assert!(
1420 parse_style_visibility(input).is_err(),
1421 "{input:?} must not parse as a visibility"
1422 );
1423 }
1424 for input in ["none", "show", "visible hidden", "visible;", "vis", "visibleX"] {
1425 assert!(
1426 parse_style_visibility(input).is_err(),
1427 "{input:?} must not parse as a visibility"
1428 );
1429 }
1430 }
1431
1432 #[test]
1435 fn blend_mode_parses_every_keyword_and_round_trips() {
1436 for mode in ALL_BLEND_MODES {
1437 let printed = mode.print_as_css_value();
1438 assert_eq!(
1439 parse_style_mix_blend_mode(&printed).unwrap(),
1440 mode,
1441 "{printed:?} did not round-trip"
1442 );
1443 assert_eq!(parse_style_mix_blend_mode(&format!(" {printed} ")).unwrap(), mode);
1444 }
1445 assert_eq!(StyleMixBlendMode::default(), StyleMixBlendMode::Normal);
1446 }
1447
1448 #[test]
1449 fn blend_mode_rejects_hostile_input() {
1450 for input in HOSTILE_KEYWORDS {
1451 assert!(
1452 parse_style_mix_blend_mode(input).is_err(),
1453 "{input:?} must not parse as a mix-blend-mode"
1454 );
1455 }
1456 for input in [
1458 "mix", "color dodge", "color_dodge", "colordodge", "normal normal", "plus-lighter",
1459 "multiply;", "screen!",
1460 ] {
1461 assert!(
1462 parse_style_mix_blend_mode(input).is_err(),
1463 "{input:?} must not parse as a mix-blend-mode"
1464 );
1465 }
1466 }
1467
1468 #[test]
1471 fn cursor_parses_every_keyword_and_round_trips() {
1472 for cursor in ALL_CURSORS {
1473 let printed = cursor.print_as_css_value();
1474 assert_eq!(
1475 parse_style_cursor(&printed).unwrap(),
1476 cursor,
1477 "{printed:?} did not round-trip"
1478 );
1479 assert_eq!(parse_style_cursor(&format!("\n{printed} ")).unwrap(), cursor);
1480 }
1481 assert_eq!(StyleCursor::default(), StyleCursor::Default);
1482 }
1483
1484 #[test]
1485 fn cursor_keyword_printing_is_injective() {
1486 let mut printed: Vec<String> = ALL_CURSORS.iter().map(PrintAsCssValue::print_as_css_value).collect();
1489 printed.sort();
1490 let count = printed.len();
1491 printed.dedup();
1492 assert_eq!(printed.len(), count, "two StyleCursor variants print the same ident");
1493 }
1494
1495 #[test]
1496 fn cursor_rejects_hostile_input() {
1497 for input in HOSTILE_KEYWORDS {
1498 assert!(
1499 parse_style_cursor(input).is_err(),
1500 "{input:?} must not parse as a cursor"
1501 );
1502 }
1503 for input in [
1504 "hand", "col resize", "e_resize",
1507 "pointer pointer",
1508 "url(cursor.png)",
1509 "auto", ] {
1511 assert!(
1512 parse_style_cursor(input).is_err(),
1513 "{input:?} must not parse as a cursor"
1514 );
1515 }
1516 }
1517
1518 #[test]
1521 fn object_fit_parses_every_keyword_and_round_trips() {
1522 for fit in ALL_OBJECT_FIT {
1523 let printed = fit.print_as_css_value();
1524 assert_eq!(parse_style_object_fit(&printed).unwrap(), fit);
1525 assert_eq!(parse_style_object_fit(&format!(" {printed} ")).unwrap(), fit);
1526 }
1527 assert_eq!(StyleObjectFit::default(), StyleObjectFit::Fill);
1528 }
1529
1530 #[test]
1531 fn object_fit_rejects_hostile_input() {
1532 for input in HOSTILE_KEYWORDS {
1533 assert!(
1534 parse_style_object_fit(input).is_err(),
1535 "{input:?} must not parse as an object-fit"
1536 );
1537 }
1538 for input in ["stretch", "scale_down", "scale down", "cover cover", "fill;"] {
1539 assert!(
1540 parse_style_object_fit(input).is_err(),
1541 "{input:?} must not parse as an object-fit"
1542 );
1543 }
1544 }
1545
1546 #[test]
1549 fn text_orientation_parses_every_keyword_and_round_trips() {
1550 for orientation in ALL_TEXT_ORIENTATION {
1551 let printed = orientation.print_as_css_value();
1552 assert_eq!(parse_style_text_orientation(&printed).unwrap(), orientation);
1553 assert_eq!(
1554 parse_style_text_orientation(&format!("\t{printed}\n")).unwrap(),
1555 orientation
1556 );
1557 }
1558 assert_eq!(StyleTextOrientation::default(), StyleTextOrientation::Mixed);
1559 }
1560
1561 #[test]
1562 fn text_orientation_rejects_hostile_input() {
1563 for input in HOSTILE_KEYWORDS {
1564 assert!(
1565 parse_style_text_orientation(input).is_err(),
1566 "{input:?} must not parse as a text-orientation"
1567 );
1568 }
1569 for input in ["vertical", "sideways-right", "upright mixed", "mixed;"] {
1570 assert!(
1571 parse_style_text_orientation(input).is_err(),
1572 "{input:?} must not parse as a text-orientation"
1573 );
1574 }
1575 }
1576
1577 #[test]
1580 fn keyword_parsers_are_case_sensitive() {
1581 assert!(parse_style_visibility("VISIBLE").is_err());
1586 assert!(parse_style_mix_blend_mode("Multiply").is_err());
1587 assert!(parse_style_cursor("Pointer").is_err());
1588 assert!(parse_style_object_fit("COVER").is_err());
1589 assert!(parse_style_text_orientation("Upright").is_err());
1590 assert!(parse_style_aspect_ratio("AUTO").is_err());
1591 }
1592
1593 #[test]
1594 fn keyword_parsers_do_not_hang_on_extremely_long_input() {
1595 let long = "a".repeat(500_000);
1596 assert!(parse_style_visibility(&long).is_err());
1597 assert!(parse_style_mix_blend_mode(&long).is_err());
1598 assert!(parse_style_cursor(&long).is_err());
1599 assert!(parse_style_object_fit(&long).is_err());
1600 assert!(parse_style_text_orientation(&long).is_err());
1601
1602 let padded = format!("{}visible{}", " ".repeat(250_000), " ".repeat(250_000));
1605 assert_eq!(parse_style_visibility(&padded).unwrap(), StyleVisibility::Visible);
1606 }
1607
1608 #[test]
1609 fn keyword_parsers_do_not_stack_overflow_on_nested_input() {
1610 let nested = format!("{}center{}", "(".repeat(10_000), ")".repeat(10_000));
1611 assert!(parse_style_visibility(&nested).is_err());
1612 assert!(parse_style_cursor(&nested).is_err());
1613 assert!(parse_style_object_fit(&nested).is_err());
1614 assert!(parse_style_object_position(&nested).is_err());
1615 assert!(parse_style_aspect_ratio(&nested).is_err());
1616 }
1617
1618 #[test]
1621 fn object_position_parses_single_keywords() {
1622 use BackgroundPositionHorizontal as H;
1623 use BackgroundPositionVertical as V;
1624
1625 for (input, h, v) in [
1626 ("center", H::Center, V::Center),
1627 ("left", H::Left, V::Center),
1628 ("right", H::Right, V::Center),
1629 ("top", H::Center, V::Top),
1630 ("bottom", H::Center, V::Bottom),
1631 ] {
1632 let parsed = parse_style_object_position(input).unwrap();
1633 assert_eq!(parsed.horizontal, h, "{input:?} horizontal");
1634 assert_eq!(parsed.vertical, v, "{input:?} vertical");
1635 }
1636 }
1637
1638 #[test]
1639 fn object_position_parses_lengths_and_percentages() {
1640 let px = parse_style_object_position("10px 20px").unwrap();
1641 assert_eq!(px.horizontal, BackgroundPositionHorizontal::Exact(PixelValue::px(10.0)));
1642 assert_eq!(px.vertical, BackgroundPositionVertical::Exact(PixelValue::px(20.0)));
1643
1644 let pct = parse_style_object_position("50% 50%").unwrap();
1645 assert_eq!(
1646 pct.horizontal,
1647 BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0))
1648 );
1649 assert_eq!(
1650 pct.vertical,
1651 BackgroundPositionVertical::Exact(PixelValue::percent(50.0))
1652 );
1653
1654 let single = parse_style_object_position("25%").unwrap();
1656 assert_eq!(
1657 single.horizontal,
1658 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
1659 );
1660 assert_eq!(
1661 single.vertical,
1662 BackgroundPositionVertical::Exact(PixelValue::percent(25.0))
1663 );
1664
1665 assert_eq!(
1667 parse_style_object_position("left 25%").unwrap(),
1668 StyleObjectPosition {
1669 horizontal: BackgroundPositionHorizontal::Left,
1670 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(25.0)),
1671 }
1672 );
1673 assert_eq!(
1674 parse_style_object_position("25% top").unwrap(),
1675 StyleObjectPosition {
1676 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
1677 vertical: BackgroundPositionVertical::Top,
1678 }
1679 );
1680 }
1681
1682 #[test]
1683 fn object_position_collapses_internal_whitespace() {
1684 let expected = parse_style_object_position("left top").unwrap();
1686 for input in ["left top", "left\ttop", " left \n top ", "left\r\ntop"] {
1687 assert_eq!(
1688 parse_style_object_position(input).unwrap(),
1689 expected,
1690 "{input:?} should be equivalent to \"left top\""
1691 );
1692 }
1693 }
1694
1695 #[test]
1696 fn object_position_rejects_wrong_component_counts_and_garbage() {
1697 for input in [
1698 "",
1699 " ",
1700 "\t\n",
1701 "left top center",
1702 "10px 20px 30px",
1703 "center center center center",
1704 "invalid",
1705 "left left", "top top", "left,top", ";",
1709 "\u{1F600}",
1710 "\u{1F600} \u{1F600}",
1711 ] {
1712 assert!(
1713 parse_style_object_position(input).is_err(),
1714 "{input:?} must not parse as an object-position"
1715 );
1716 }
1717 }
1718
1719 #[test]
1720 fn object_position_extreme_lengths_do_not_panic() {
1721 for input in [
1725 "NaN NaN", "inf inf", "-inf", "1e39px", "-1e39px", "340282350000000000000000000000000000000px",
1726 ] {
1727 let _ = parse_style_object_position(input);
1728 }
1729 let long = format!("{}px", "9".repeat(100_000));
1730 let _ = parse_style_object_position(&long);
1731 }
1732
1733 #[test]
1734 fn object_position_round_trips_through_print_as_css_value() {
1735 use BackgroundPositionHorizontal as H;
1736 use BackgroundPositionVertical as V;
1737
1738 let horizontals = [H::Left, H::Center, H::Right, H::Exact(PixelValue::percent(25.0))];
1739 let verticals = [V::Top, V::Center, V::Bottom, V::Exact(PixelValue::px(30.0))];
1740
1741 for horizontal in horizontals {
1742 for vertical in verticals {
1743 let position = StyleObjectPosition { horizontal, vertical };
1744 let printed = position.print_as_css_value();
1745 let reparsed = parse_style_object_position(&printed)
1746 .unwrap_or_else(|e| panic!("{position:?} printed as {printed:?}, which failed to re-parse: {e}"));
1747 assert_eq!(reparsed, position, "{printed:?} did not round-trip");
1748 }
1749 }
1750
1751 let default = StyleObjectPosition::default();
1753 assert_eq!(default.print_as_css_value(), "50% 50%");
1754 assert_eq!(parse_style_object_position("50% 50%").unwrap(), default);
1755 assert_eq!(parse_style_object_position("center").unwrap().print_as_css_value(), "center center");
1756 }
1757
1758 #[test]
1761 fn aspect_f32_to_u32_saturates_instead_of_panicking() {
1762 assert_eq!(aspect_f32_to_u32(0.0), 0);
1764 assert_eq!(aspect_f32_to_u32(-0.0), 0);
1765 assert_eq!(aspect_f32_to_u32(0.9), 0);
1766 assert_eq!(aspect_f32_to_u32(1.0), 1);
1767 assert_eq!(aspect_f32_to_u32(1.9), 1);
1768 assert_eq!(aspect_f32_to_u32(f32::MIN_POSITIVE), 0);
1769
1770 assert_eq!(aspect_f32_to_u32(-1.0), 0);
1772 assert_eq!(aspect_f32_to_u32(-0.5), 0);
1773 assert_eq!(aspect_f32_to_u32(-1e30), 0);
1774 assert_eq!(aspect_f32_to_u32(f32::MIN), 0);
1775 assert_eq!(aspect_f32_to_u32(f32::NEG_INFINITY), 0);
1776
1777 assert_eq!(aspect_f32_to_u32(f32::MAX), u32::MAX);
1779 assert_eq!(aspect_f32_to_u32(f32::INFINITY), u32::MAX);
1780 assert_eq!(aspect_f32_to_u32(1e30), u32::MAX);
1781 assert_eq!(aspect_f32_to_u32(u32::MAX as f32), u32::MAX);
1783
1784 assert_eq!(aspect_f32_to_u32(f32::NAN), 0);
1786 assert_eq!(aspect_f32_to_u32(-f32::NAN), 0);
1787
1788 assert_eq!(aspect_f32_to_u32(100_000.0 * 1000.0), 100_000_000);
1790 }
1791
1792 #[test]
1793 fn aspect_f32_to_u32_is_usable_in_const_context() {
1794 const TRUNCATED: u32 = aspect_f32_to_u32(1.999);
1795 const SATURATED: u32 = aspect_f32_to_u32(f32::INFINITY);
1796 const NEGATIVE: u32 = aspect_f32_to_u32(-5.0);
1797 const NOT_A_NUMBER: u32 = aspect_f32_to_u32(f32::NAN);
1798 assert_eq!((TRUNCATED, SATURATED, NEGATIVE, NOT_A_NUMBER), (1, u32::MAX, 0, 0));
1799 }
1800
1801 #[test]
1804 fn aspect_ratio_parses_valid_forms() {
1805 assert_eq!(parse_style_aspect_ratio("auto").unwrap(), StyleAspectRatio::Auto);
1806 assert_eq!(StyleAspectRatio::default(), StyleAspectRatio::Auto);
1807
1808 for input in ["16 / 9", "16/9", "16 /9", "16/ 9", " 16 / 9 "] {
1809 assert_eq!(
1810 parse_style_aspect_ratio(input).unwrap(),
1811 StyleAspectRatio::Ratio(AspectRatioValue { width: 16000, height: 9000 }),
1812 "{input:?} should parse as 16/9"
1813 );
1814 }
1815
1816 assert_eq!(
1818 parse_style_aspect_ratio("1").unwrap(),
1819 StyleAspectRatio::Ratio(AspectRatioValue { width: 1000, height: 1000 })
1820 );
1821 assert_eq!(
1822 parse_style_aspect_ratio("1.5").unwrap(),
1823 StyleAspectRatio::Ratio(AspectRatioValue { width: 1500, height: 1000 })
1824 );
1825
1826 assert_eq!(
1828 parse_style_aspect_ratio("100000").unwrap(),
1829 StyleAspectRatio::Ratio(AspectRatioValue { width: 100_000_000, height: 1000 })
1830 );
1831 assert!(parse_style_aspect_ratio("100001").is_err());
1832 assert!(parse_style_aspect_ratio("100000.1 / 1").is_err());
1833 assert!(parse_style_aspect_ratio("1 / 100001").is_err());
1834 }
1835
1836 #[test]
1837 fn aspect_ratio_rejects_non_positive_and_malformed_input() {
1838 for input in [
1839 "", " ", "\t\n", "abc", "auto / auto", "16 / 9 / 4", "1/2/3", "/", "//", "/9",
1840 "16/", "16 9", ";", "16,9", "\u{1F600}", "\u{1F600}/\u{1F600}",
1841 ] {
1842 assert!(
1843 parse_style_aspect_ratio(input).is_err(),
1844 "{input:?} must not parse as an aspect-ratio"
1845 );
1846 }
1847
1848 for input in ["0", "0 / 1", "1 / 0", "0/0", "-0", "-0 / 1", "1 / -0", "-1 / 1", "-1", "-1.5"] {
1850 assert!(
1851 parse_style_aspect_ratio(input).is_err(),
1852 "{input:?} must not parse as an aspect-ratio"
1853 );
1854 }
1855
1856 for input in ["inf", "inf / 1", "1 / inf", "-inf", "-inf / 1", "1e39", "1e39 / 1"] {
1858 assert!(
1859 parse_style_aspect_ratio(input).is_err(),
1860 "{input:?} should be rejected: out of the [0, 100_000] range"
1861 );
1862 }
1863 }
1864
1865 #[test]
1866 fn aspect_ratio_extremely_long_input_terminates() {
1867 let long = "9".repeat(100_000);
1868 assert!(parse_style_aspect_ratio(&long).is_err());
1869 assert!(parse_style_aspect_ratio(&format!("{long}/{long}")).is_err());
1870
1871 let slashes = "/".repeat(100_000);
1873 assert!(parse_style_aspect_ratio(&slashes).is_err());
1874 }
1875
1876 #[test]
1877 fn aspect_ratio_auto_round_trips() {
1878 let printed = StyleAspectRatio::Auto.print_as_css_value();
1879 assert_eq!(printed, "auto");
1880 assert_eq!(parse_style_aspect_ratio(&printed).unwrap(), StyleAspectRatio::Auto);
1881 }
1882
1883 #[test]
1886 fn opacity_parse_error_round_trips_through_the_owned_form() {
1887 let errors = [
1888 OpacityParseError::ParsePercentage(
1889 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
1890 "",
1891 ),
1892 OpacityParseError::ParsePercentage(
1893 PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
1894 "abc",
1895 ),
1896 OpacityParseError::ParsePercentage(PercentageParseError::NoPercentSign, "0.5"),
1897 OpacityParseError::ParsePercentage(
1898 PercentageParseError::InvalidUnit(String::from("px").into()),
1899 "5px",
1900 ),
1901 OpacityParseError::OutOfRange("1.5"),
1902 OpacityParseError::OutOfRange(""),
1903 OpacityParseError::OutOfRange("\u{1F600}"),
1904 ];
1905
1906 for error in errors {
1907 let owned = error.to_contained();
1908 assert_eq!(owned.to_shared(), error, "{error:?} did not round-trip");
1909 assert_eq!(owned.to_shared().to_contained(), owned);
1910
1911 let shown = error.to_string();
1912 assert!(!shown.is_empty(), "{error:?} renders as an empty message");
1913 assert_eq!(format!("{error:?}"), shown);
1915 }
1916 }
1917
1918 #[test]
1919 fn opacity_parse_error_to_contained_copies_the_borrowed_input() {
1920 let owned = {
1922 let input = String::from("1.5");
1923 parse_style_opacity(&input).unwrap_err().to_contained()
1924 };
1925 assert_eq!(owned, OpacityParseErrorOwned::OutOfRange(String::from("1.5").into()));
1926 assert!(owned.to_shared().to_string().contains("1.5"));
1927 }
1928
1929 #[test]
1930 fn keyword_parse_errors_round_trip_through_the_owned_form() {
1931 for payload in ["", "junk", " ", "\u{1F600}", "a\0b", "\u{0665}"] {
1933 let visibility = StyleVisibilityParseError::InvalidValue(InvalidValueErr(payload));
1934 assert_eq!(visibility.to_contained().to_shared(), visibility);
1935 assert!(!visibility.to_string().is_empty());
1936 assert_eq!(format!("{visibility:?}"), visibility.to_string());
1937
1938 let blend = MixBlendModeParseError::InvalidValue(InvalidValueErr(payload));
1939 assert_eq!(blend.to_contained().to_shared(), blend);
1940 assert!(!blend.to_string().is_empty());
1941
1942 let cursor = CursorParseError::InvalidValue(InvalidValueErr(payload));
1943 assert_eq!(cursor.to_contained().to_shared(), cursor);
1944 assert!(!cursor.to_string().is_empty());
1945
1946 let object_fit = StyleObjectFitParseError::InvalidValue(payload);
1948 assert_eq!(object_fit.to_contained().to_shared(), object_fit);
1949 assert!(!object_fit.to_string().is_empty());
1950
1951 let orientation = StyleTextOrientationParseError::InvalidValue(payload);
1952 assert_eq!(orientation.to_contained().to_shared(), orientation);
1953 assert!(!orientation.to_string().is_empty());
1954
1955 let position = StyleObjectPositionParseError::InvalidValue(payload);
1956 assert_eq!(position.to_contained().to_shared(), position);
1957 assert!(!position.to_string().is_empty());
1958
1959 let ratio = StyleAspectRatioParseError::InvalidValue(payload);
1960 assert_eq!(ratio.to_contained().to_shared(), ratio);
1961 assert!(!ratio.to_string().is_empty());
1962 }
1963 }
1964
1965 #[test]
1966 fn parse_errors_quote_the_offending_input() {
1967 assert!(parse_style_visibility("show").unwrap_err().to_string().contains("show"));
1970 assert!(parse_style_mix_blend_mode("mix").unwrap_err().to_string().contains("mix"));
1971 assert!(parse_style_cursor("hand").unwrap_err().to_string().contains("hand"));
1972 assert!(parse_style_object_fit("stretch").unwrap_err().to_string().contains("stretch"));
1973 assert!(parse_style_text_orientation("vertical").unwrap_err().to_string().contains("vertical"));
1974 assert!(parse_style_object_position("nope").unwrap_err().to_string().contains("nope"));
1975 assert!(parse_style_aspect_ratio("nope").unwrap_err().to_string().contains("nope"));
1976 assert!(parse_style_opacity("1.5").unwrap_err().to_string().contains("1.5"));
1977 }
1978
1979 #[test]
1980 fn parse_errors_report_the_trimmed_input_not_the_raw_slice() {
1981 let shown = parse_style_cursor(" hand ").unwrap_err().to_string();
1984 assert!(shown.contains("\"hand\""), "expected the trimmed value, got {shown:?}");
1985
1986 let shown = parse_style_opacity(" 1.5 ").unwrap_err().to_string();
1989 assert!(shown.contains("\" 1.5 \""), "expected the raw value, got {shown:?}");
1990 }
1991
1992 #[test]
1993 fn owned_error_forms_are_independent_of_the_source_buffer() {
1994 let owned = {
1997 let input = String::from("stretch");
1998 parse_style_object_fit(&input).unwrap_err().to_contained()
1999 };
2000 assert_eq!(
2001 owned,
2002 StyleObjectFitParseErrorOwned::InvalidValue(String::from("stretch").into())
2003 );
2004 assert!(owned.to_shared().to_string().contains("stretch"));
2005 }
2006
2007 #[test]
2013 fn known_bug_opacity_multibyte_numeric_char_panics() {
2014 for input in ["\u{00BD}", "\u{00BD}%", "0.5\u{0665}", "\u{FF15}%"] {
2022 assert!(
2023 parse_style_opacity(input).is_err(),
2024 "{input:?} should be rejected, not panic"
2025 );
2026 }
2027 }
2028
2029 #[test]
2030 fn known_bug_aspect_ratio_nan_bypasses_the_range_guards() {
2031 for input in ["NaN", "nan", "NaN / 1", "1 / NaN", "nan/nan", "-NaN"] {
2038 assert!(
2039 parse_style_aspect_ratio(input).is_err(),
2040 "{input:?} should be rejected, but parsed as {:?}",
2041 parse_style_aspect_ratio(input)
2042 );
2043 }
2044 }
2045
2046 #[test]
2047 fn known_bug_aspect_ratio_tiny_positive_values_round_down_to_zero() {
2048 for input in ["0.0001", "0.0004 / 1", "1 / 0.0001", "1e-10"] {
2052 let Ok(StyleAspectRatio::Ratio(ratio)) = parse_style_aspect_ratio(input) else {
2053 continue; };
2055 assert!(
2056 ratio.width > 0 && ratio.height > 0,
2057 "{input:?} produced the degenerate ratio {ratio:?}"
2058 );
2059 }
2060 }
2061
2062 #[test]
2063 fn known_bug_aspect_ratio_does_not_survive_a_print_reparse_cycle() {
2064 let ratio = parse_style_aspect_ratio("16 / 9").unwrap();
2069 let printed = ratio.print_as_css_value();
2070 assert_eq!(printed, "16 / 9", "printed the fixed-point form: {printed:?}");
2071 assert_eq!(parse_style_aspect_ratio(&printed).unwrap(), ratio);
2072 }
2073
2074 #[test]
2075 fn known_bug_object_position_rejects_reversed_keyword_pairs() {
2076 assert_eq!(
2081 parse_style_object_position("top left").unwrap(),
2082 parse_style_object_position("left top").unwrap()
2083 );
2084 assert_eq!(
2085 parse_style_object_position("bottom right").unwrap(),
2086 parse_style_object_position("right bottom").unwrap()
2087 );
2088 }
2089}