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
56impl PrintAsCssValue for StyleVisibility {
57 fn print_as_css_value(&self) -> String {
58 String::from(match self {
59 Self::Visible => "visible",
60 Self::Hidden => "hidden",
61 Self::Collapse => "collapse",
62 })
63 }
64}
65
66#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
71#[repr(C)]
72#[derive(Default)]
73pub enum StyleMixBlendMode {
74 #[default]
75 Normal,
76 Multiply,
77 Screen,
78 Overlay,
79 Darken,
80 Lighten,
81 ColorDodge,
82 ColorBurn,
83 HardLight,
84 SoftLight,
85 Difference,
86 Exclusion,
87 Hue,
88 Saturation,
89 Color,
90 Luminosity,
91}
92
93impl fmt::Display for StyleMixBlendMode {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(
96 f,
97 "{}",
98 match self {
99 Self::Normal => "normal",
100 Self::Multiply => "multiply",
101 Self::Screen => "screen",
102 Self::Overlay => "overlay",
103 Self::Darken => "darken",
104 Self::Lighten => "lighten",
105 Self::ColorDodge => "color-dodge",
106 Self::ColorBurn => "color-burn",
107 Self::HardLight => "hard-light",
108 Self::SoftLight => "soft-light",
109 Self::Difference => "difference",
110 Self::Exclusion => "exclusion",
111 Self::Hue => "hue",
112 Self::Saturation => "saturation",
113 Self::Color => "color",
114 Self::Luminosity => "luminosity",
115 }
116 )
117 }
118}
119
120impl PrintAsCssValue for StyleMixBlendMode {
121 fn print_as_css_value(&self) -> String {
122 self.to_string()
123 }
124}
125
126#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
131#[repr(C)]
132#[derive(Default)]
133pub enum StyleCursor {
134 Alias,
135 AllScroll,
136 Cell,
137 ColResize,
138 ContextMenu,
139 Copy,
140 Crosshair,
141 #[default]
142 Default,
143 EResize,
144 EwResize,
145 Grab,
146 Grabbing,
147 Help,
148 Move,
149 NResize,
150 NsResize,
151 NeswResize,
152 NwseResize,
153 Pointer,
154 Progress,
155 RowResize,
156 SResize,
157 SeResize,
158 Text,
159 Unset,
160 VerticalText,
161 WResize,
162 Wait,
163 ZoomIn,
164 ZoomOut,
165}
166
167impl PrintAsCssValue for StyleCursor {
168 fn print_as_css_value(&self) -> String {
169 String::from(match self {
170 Self::Alias => "alias",
171 Self::AllScroll => "all-scroll",
172 Self::Cell => "cell",
173 Self::ColResize => "col-resize",
174 Self::ContextMenu => "context-menu",
175 Self::Copy => "copy",
176 Self::Crosshair => "crosshair",
177 Self::Default => "default",
178 Self::EResize => "e-resize",
179 Self::EwResize => "ew-resize",
180 Self::Grab => "grab",
181 Self::Grabbing => "grabbing",
182 Self::Help => "help",
183 Self::Move => "move",
184 Self::NResize => "n-resize",
185 Self::NsResize => "ns-resize",
186 Self::NeswResize => "nesw-resize",
187 Self::NwseResize => "nwse-resize",
188 Self::Pointer => "pointer",
189 Self::Progress => "progress",
190 Self::RowResize => "row-resize",
191 Self::SResize => "s-resize",
192 Self::SeResize => "se-resize",
193 Self::Text => "text",
194 Self::Unset => "unset",
195 Self::VerticalText => "vertical-text",
196 Self::WResize => "w-resize",
197 Self::Wait => "wait",
198 Self::ZoomIn => "zoom-in",
199 Self::ZoomOut => "zoom-out",
200 })
201 }
202}
203
204#[cfg(feature = "parser")]
207pub mod parsers {
208 #[allow(clippy::wildcard_imports)]
209 use super::*;
211 use crate::corety::AzString;
212 use crate::props::basic::error::{InvalidValueErr, InvalidValueErrOwned};
213
214 #[derive(Clone, PartialEq, Eq)]
217 pub enum OpacityParseError<'a> {
218 ParsePercentage(PercentageParseError, &'a str),
219 OutOfRange(&'a str),
220 }
221 impl_debug_as_display!(OpacityParseError<'a>);
222 impl_display! { OpacityParseError<'a>, {
223 ParsePercentage(e, s) => format!("Invalid opacity value \"{}\": {}", s, e),
224 OutOfRange(s) => format!("Invalid opacity value \"{}\": must be between 0 and 1", s),
225 }}
226
227 #[derive(Debug, Clone, PartialEq, Eq)]
229 #[repr(C)]
230 pub struct PercentageParseErrorWithInput {
231 pub error: PercentageParseError,
232 pub input: AzString,
233 }
234
235 #[derive(Debug, Clone, PartialEq, Eq)]
236 #[repr(C, u8)]
237 pub enum OpacityParseErrorOwned {
238 ParsePercentage(PercentageParseErrorWithInput),
239 OutOfRange(AzString),
240 }
241
242 impl OpacityParseError<'_> {
243 #[must_use]
244 pub fn to_contained(&self) -> OpacityParseErrorOwned {
245 match self {
246 Self::ParsePercentage(err, s) => {
247 OpacityParseErrorOwned::ParsePercentage(PercentageParseErrorWithInput {
248 error: err.clone(),
249 input: (*s).to_string().into(),
250 })
251 }
252 Self::OutOfRange(s) => OpacityParseErrorOwned::OutOfRange((*s).to_string().into()),
253 }
254 }
255 }
256
257 impl OpacityParseErrorOwned {
258 #[must_use]
259 pub fn to_shared(&self) -> OpacityParseError<'_> {
260 match self {
261 Self::ParsePercentage(e) => {
262 OpacityParseError::ParsePercentage(e.error.clone(), e.input.as_str())
263 }
264 Self::OutOfRange(s) => OpacityParseError::OutOfRange(s.as_str()),
265 }
266 }
267 }
268
269 pub fn parse_style_opacity(input: &str) -> Result<StyleOpacity, OpacityParseError<'_>> {
273 let val = parse_percentage_value(input)
274 .map_err(|e| OpacityParseError::ParsePercentage(e, input))?;
275
276 let normalized = val.normalized();
277 if !(0.0..=1.0).contains(&normalized) {
278 return Err(OpacityParseError::OutOfRange(input));
279 }
280
281 Ok(StyleOpacity { inner: val })
282 }
283
284 #[derive(Clone, PartialEq, Eq)]
287 pub enum StyleVisibilityParseError<'a> {
288 InvalidValue(InvalidValueErr<'a>),
289 }
290 impl_debug_as_display!(StyleVisibilityParseError<'a>);
291 impl_display! { StyleVisibilityParseError<'a>, {
292 InvalidValue(e) => format!("Invalid visibility value: \"{}\"", e.0),
293 }}
294 impl_from!(InvalidValueErr<'a>, StyleVisibilityParseError::InvalidValue);
295
296 #[derive(Debug, Clone, PartialEq, Eq)]
297 #[repr(C, u8)]
298 pub enum StyleVisibilityParseErrorOwned {
299 InvalidValue(InvalidValueErrOwned),
300 }
301
302 impl StyleVisibilityParseError<'_> {
303 #[must_use]
304 pub fn to_contained(&self) -> StyleVisibilityParseErrorOwned {
305 match self {
306 Self::InvalidValue(e) => {
307 StyleVisibilityParseErrorOwned::InvalidValue(e.to_contained())
308 }
309 }
310 }
311 }
312
313 impl StyleVisibilityParseErrorOwned {
314 #[must_use]
315 pub fn to_shared(&self) -> StyleVisibilityParseError<'_> {
316 match self {
317 Self::InvalidValue(e) => StyleVisibilityParseError::InvalidValue(e.to_shared()),
318 }
319 }
320 }
321
322 pub fn parse_style_visibility(
326 input: &str,
327 ) -> Result<StyleVisibility, StyleVisibilityParseError<'_>> {
328 let input = input.trim();
329 match input {
330 "visible" => Ok(StyleVisibility::Visible),
331 "hidden" => Ok(StyleVisibility::Hidden),
332 "collapse" => Ok(StyleVisibility::Collapse),
333 _ => Err(InvalidValueErr(input).into()),
334 }
335 }
336
337 #[derive(Clone, PartialEq, Eq)]
340 pub enum MixBlendModeParseError<'a> {
341 InvalidValue(InvalidValueErr<'a>),
342 }
343 impl_debug_as_display!(MixBlendModeParseError<'a>);
344 impl_display! { MixBlendModeParseError<'a>, {
345 InvalidValue(e) => format!("Invalid mix-blend-mode value: \"{}\"", e.0),
346 }}
347 impl_from!(InvalidValueErr<'a>, MixBlendModeParseError::InvalidValue);
348
349 #[derive(Debug, Clone, PartialEq, Eq)]
350 #[repr(C, u8)]
351 pub enum MixBlendModeParseErrorOwned {
352 InvalidValue(InvalidValueErrOwned),
353 }
354
355 impl MixBlendModeParseError<'_> {
356 #[must_use]
357 pub fn to_contained(&self) -> MixBlendModeParseErrorOwned {
358 match self {
359 Self::InvalidValue(e) => {
360 MixBlendModeParseErrorOwned::InvalidValue(e.to_contained())
361 }
362 }
363 }
364 }
365
366 impl MixBlendModeParseErrorOwned {
367 #[must_use]
368 pub fn to_shared(&self) -> MixBlendModeParseError<'_> {
369 match self {
370 Self::InvalidValue(e) => MixBlendModeParseError::InvalidValue(e.to_shared()),
371 }
372 }
373 }
374
375 pub fn parse_style_mix_blend_mode(
379 input: &str,
380 ) -> Result<StyleMixBlendMode, MixBlendModeParseError<'_>> {
381 let input = input.trim();
382 match input {
383 "normal" => Ok(StyleMixBlendMode::Normal),
384 "multiply" => Ok(StyleMixBlendMode::Multiply),
385 "screen" => Ok(StyleMixBlendMode::Screen),
386 "overlay" => Ok(StyleMixBlendMode::Overlay),
387 "darken" => Ok(StyleMixBlendMode::Darken),
388 "lighten" => Ok(StyleMixBlendMode::Lighten),
389 "color-dodge" => Ok(StyleMixBlendMode::ColorDodge),
390 "color-burn" => Ok(StyleMixBlendMode::ColorBurn),
391 "hard-light" => Ok(StyleMixBlendMode::HardLight),
392 "soft-light" => Ok(StyleMixBlendMode::SoftLight),
393 "difference" => Ok(StyleMixBlendMode::Difference),
394 "exclusion" => Ok(StyleMixBlendMode::Exclusion),
395 "hue" => Ok(StyleMixBlendMode::Hue),
396 "saturation" => Ok(StyleMixBlendMode::Saturation),
397 "color" => Ok(StyleMixBlendMode::Color),
398 "luminosity" => Ok(StyleMixBlendMode::Luminosity),
399 _ => Err(InvalidValueErr(input).into()),
400 }
401 }
402
403 #[derive(Clone, PartialEq, Eq)]
406 pub enum CursorParseError<'a> {
407 InvalidValue(InvalidValueErr<'a>),
408 }
409 impl_debug_as_display!(CursorParseError<'a>);
410 impl_display! { CursorParseError<'a>, {
411 InvalidValue(e) => format!("Invalid cursor value: \"{}\"", e.0),
412 }}
413 impl_from!(InvalidValueErr<'a>, CursorParseError::InvalidValue);
414
415 #[derive(Debug, Clone, PartialEq, Eq)]
416 #[repr(C, u8)]
417 pub enum CursorParseErrorOwned {
418 InvalidValue(InvalidValueErrOwned),
419 }
420
421 impl CursorParseError<'_> {
422 #[must_use]
423 pub fn to_contained(&self) -> CursorParseErrorOwned {
424 match self {
425 Self::InvalidValue(e) => CursorParseErrorOwned::InvalidValue(e.to_contained()),
426 }
427 }
428 }
429
430 impl CursorParseErrorOwned {
431 #[must_use]
432 pub fn to_shared(&self) -> CursorParseError<'_> {
433 match self {
434 Self::InvalidValue(e) => CursorParseError::InvalidValue(e.to_shared()),
435 }
436 }
437 }
438
439 pub fn parse_style_cursor(input: &str) -> Result<StyleCursor, CursorParseError<'_>> {
443 let input = input.trim();
444 match input {
445 "alias" => Ok(StyleCursor::Alias),
446 "all-scroll" => Ok(StyleCursor::AllScroll),
447 "cell" => Ok(StyleCursor::Cell),
448 "col-resize" => Ok(StyleCursor::ColResize),
449 "context-menu" => Ok(StyleCursor::ContextMenu),
450 "copy" => Ok(StyleCursor::Copy),
451 "crosshair" => Ok(StyleCursor::Crosshair),
452 "default" => Ok(StyleCursor::Default),
453 "e-resize" => Ok(StyleCursor::EResize),
454 "ew-resize" => Ok(StyleCursor::EwResize),
455 "grab" => Ok(StyleCursor::Grab),
456 "grabbing" => Ok(StyleCursor::Grabbing),
457 "help" => Ok(StyleCursor::Help),
458 "move" => Ok(StyleCursor::Move),
459 "n-resize" => Ok(StyleCursor::NResize),
460 "ns-resize" => Ok(StyleCursor::NsResize),
461 "nesw-resize" => Ok(StyleCursor::NeswResize),
462 "nwse-resize" => Ok(StyleCursor::NwseResize),
463 "pointer" => Ok(StyleCursor::Pointer),
464 "progress" => Ok(StyleCursor::Progress),
465 "row-resize" => Ok(StyleCursor::RowResize),
466 "s-resize" => Ok(StyleCursor::SResize),
467 "se-resize" => Ok(StyleCursor::SeResize),
468 "text" => Ok(StyleCursor::Text),
469 "unset" => Ok(StyleCursor::Unset),
470 "vertical-text" => Ok(StyleCursor::VerticalText),
471 "w-resize" => Ok(StyleCursor::WResize),
472 "wait" => Ok(StyleCursor::Wait),
473 "zoom-in" => Ok(StyleCursor::ZoomIn),
474 "zoom-out" => Ok(StyleCursor::ZoomOut),
475 _ => Err(InvalidValueErr(input).into()),
476 }
477 }
478}
479
480#[cfg(feature = "parser")]
481pub use self::parsers::*;
482
483#[cfg(all(test, feature = "parser"))]
484mod tests {
485 #![allow(clippy::float_cmp)]
487 use super::*;
488
489 #[test]
490 fn test_parse_opacity() {
491 assert_eq!(parse_style_opacity("0.5").unwrap().inner.normalized(), 0.5);
492 assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
493 assert_eq!(parse_style_opacity("50%").unwrap().inner.normalized(), 0.5);
494 assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
495 assert_eq!(
496 parse_style_opacity(" 75% ").unwrap().inner.normalized(),
497 0.75
498 );
499 assert!(parse_style_opacity("1.1").is_err());
500 assert!(parse_style_opacity("-0.1").is_err());
501 assert!(parse_style_opacity("auto").is_err());
502 }
503
504 #[test]
505 fn test_parse_mix_blend_mode() {
506 assert_eq!(
507 parse_style_mix_blend_mode("multiply").unwrap(),
508 StyleMixBlendMode::Multiply
509 );
510 assert_eq!(
511 parse_style_mix_blend_mode("screen").unwrap(),
512 StyleMixBlendMode::Screen
513 );
514 assert_eq!(
515 parse_style_mix_blend_mode("color-dodge").unwrap(),
516 StyleMixBlendMode::ColorDodge
517 );
518 assert!(parse_style_mix_blend_mode("mix").is_err());
519 }
520
521 #[test]
522 fn test_parse_visibility() {
523 assert_eq!(
524 parse_style_visibility("visible").unwrap(),
525 StyleVisibility::Visible
526 );
527 assert_eq!(
528 parse_style_visibility("hidden").unwrap(),
529 StyleVisibility::Hidden
530 );
531 assert_eq!(
532 parse_style_visibility("collapse").unwrap(),
533 StyleVisibility::Collapse
534 );
535 assert_eq!(
536 parse_style_visibility(" visible ").unwrap(),
537 StyleVisibility::Visible
538 );
539 assert!(parse_style_visibility("none").is_err());
540 assert!(parse_style_visibility("show").is_err());
541 }
542
543 #[test]
544 fn test_parse_cursor() {
545 assert_eq!(parse_style_cursor("pointer").unwrap(), StyleCursor::Pointer);
546 assert_eq!(parse_style_cursor("wait").unwrap(), StyleCursor::Wait);
547 assert_eq!(
548 parse_style_cursor("col-resize").unwrap(),
549 StyleCursor::ColResize
550 );
551 assert_eq!(parse_style_cursor(" text ").unwrap(), StyleCursor::Text);
552 assert!(parse_style_cursor("hand").is_err()); }
554
555 #[test]
556 fn test_parse_object_fit() {
557 assert_eq!(
558 parse_style_object_fit("fill").unwrap(),
559 StyleObjectFit::Fill
560 );
561 assert_eq!(
562 parse_style_object_fit("contain").unwrap(),
563 StyleObjectFit::Contain
564 );
565 assert_eq!(
566 parse_style_object_fit("cover").unwrap(),
567 StyleObjectFit::Cover
568 );
569 assert_eq!(
570 parse_style_object_fit("none").unwrap(),
571 StyleObjectFit::None
572 );
573 assert_eq!(
574 parse_style_object_fit("scale-down").unwrap(),
575 StyleObjectFit::ScaleDown
576 );
577 assert_eq!(
578 parse_style_object_fit(" cover ").unwrap(),
579 StyleObjectFit::Cover
580 );
581 assert!(parse_style_object_fit("stretch").is_err());
582 assert!(parse_style_object_fit("").is_err());
583 }
584
585 #[test]
586 fn test_parse_text_orientation() {
587 assert_eq!(
588 parse_style_text_orientation("mixed").unwrap(),
589 StyleTextOrientation::Mixed
590 );
591 assert_eq!(
592 parse_style_text_orientation("upright").unwrap(),
593 StyleTextOrientation::Upright
594 );
595 assert_eq!(
596 parse_style_text_orientation("sideways").unwrap(),
597 StyleTextOrientation::Sideways
598 );
599 assert_eq!(
600 parse_style_text_orientation(" mixed ").unwrap(),
601 StyleTextOrientation::Mixed
602 );
603 assert!(parse_style_text_orientation("vertical").is_err());
604 }
605
606 #[test]
607 fn test_parse_object_position() {
608 use crate::props::style::background::{
609 BackgroundPositionHorizontal, BackgroundPositionVertical,
610 };
611 let centered = parse_style_object_position("center").unwrap();
612 assert_eq!(
613 centered,
614 parse_style_object_position("center center").unwrap()
615 );
616
617 let lt = parse_style_object_position("left top").unwrap();
618 assert_eq!(lt.horizontal, BackgroundPositionHorizontal::Left);
619 assert_eq!(lt.vertical, BackgroundPositionVertical::Top);
620
621 let rb = parse_style_object_position("right bottom").unwrap();
622 assert_eq!(rb.horizontal, BackgroundPositionHorizontal::Right);
623 assert_eq!(rb.vertical, BackgroundPositionVertical::Bottom);
624
625 assert!(parse_style_object_position("left top center").is_err());
626 assert!(parse_style_object_position("invalid").is_err());
627 }
628
629 #[test]
630 fn test_parse_aspect_ratio() {
631 assert_eq!(
632 parse_style_aspect_ratio("auto").unwrap(),
633 StyleAspectRatio::Auto
634 );
635 assert_eq!(
636 parse_style_aspect_ratio("16 / 9").unwrap(),
637 StyleAspectRatio::Ratio(AspectRatioValue {
638 width: 16000,
639 height: 9000
640 })
641 );
642 assert_eq!(
643 parse_style_aspect_ratio("16/9").unwrap(),
644 StyleAspectRatio::Ratio(AspectRatioValue {
645 width: 16000,
646 height: 9000
647 })
648 );
649 assert_eq!(
650 parse_style_aspect_ratio("1.5").unwrap(),
651 StyleAspectRatio::Ratio(AspectRatioValue {
652 width: 1500,
653 height: 1000
654 })
655 );
656 assert_eq!(
657 parse_style_aspect_ratio(" 4 / 3 ").unwrap(),
658 StyleAspectRatio::Ratio(AspectRatioValue {
659 width: 4000,
660 height: 3000
661 })
662 );
663 assert!(parse_style_aspect_ratio("0 / 1").is_err());
664 assert!(parse_style_aspect_ratio("1 / 0").is_err());
665 assert!(parse_style_aspect_ratio("-1 / 1").is_err());
666 assert!(parse_style_aspect_ratio("abc").is_err());
667 }
668}
669
670#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
675#[repr(C)]
676#[derive(Default)]
677pub enum StyleObjectFit {
678 #[default]
679 Fill,
680 Contain,
681 Cover,
682 None,
683 ScaleDown,
684}
685
686crate::impl_option!(
687 StyleObjectFit,
688 OptionStyleObjectFit,
689 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
690);
691
692impl PrintAsCssValue for StyleObjectFit {
693 fn print_as_css_value(&self) -> String {
694 String::from(match self {
695 Self::Fill => "fill",
696 Self::Contain => "contain",
697 Self::Cover => "cover",
698 Self::None => "none",
699 Self::ScaleDown => "scale-down",
700 })
701 }
702}
703
704#[cfg(feature = "parser")]
705#[derive(Clone, PartialEq, Eq)]
706pub enum StyleObjectFitParseError<'a> {
707 InvalidValue(&'a str),
708}
709
710#[cfg(feature = "parser")]
711crate::impl_debug_as_display!(StyleObjectFitParseError<'a>);
712
713#[cfg(feature = "parser")]
714crate::impl_display! { StyleObjectFitParseError<'a>, {
715 InvalidValue(val) => format!("Invalid object-fit value: \"{}\"", val),
716}}
717
718#[cfg(feature = "parser")]
719#[derive(Debug, Clone, PartialEq, Eq)]
720#[repr(C, u8)]
721pub enum StyleObjectFitParseErrorOwned {
722 InvalidValue(crate::AzString),
723}
724
725#[cfg(feature = "parser")]
726impl StyleObjectFitParseError<'_> {
727 #[must_use]
728 pub fn to_contained(&self) -> StyleObjectFitParseErrorOwned {
729 match self {
730 Self::InvalidValue(s) => {
731 StyleObjectFitParseErrorOwned::InvalidValue((*s).to_string().into())
732 }
733 }
734 }
735}
736
737#[cfg(feature = "parser")]
738impl StyleObjectFitParseErrorOwned {
739 #[must_use]
740 pub fn to_shared(&self) -> StyleObjectFitParseError<'_> {
741 match self {
742 Self::InvalidValue(s) => StyleObjectFitParseError::InvalidValue(s.as_str()),
743 }
744 }
745}
746
747#[cfg(feature = "parser")]
748pub fn parse_style_object_fit(input: &str) -> Result<StyleObjectFit, StyleObjectFitParseError<'_>> {
752 let input = input.trim();
753 match input {
754 "fill" => Ok(StyleObjectFit::Fill),
755 "contain" => Ok(StyleObjectFit::Contain),
756 "cover" => Ok(StyleObjectFit::Cover),
757 "none" => Ok(StyleObjectFit::None),
758 "scale-down" => Ok(StyleObjectFit::ScaleDown),
759 _ => Err(StyleObjectFitParseError::InvalidValue(input)),
760 }
761}
762
763#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
768#[repr(C)]
769#[derive(Default)]
770pub enum StyleTextOrientation {
771 #[default]
772 Mixed,
773 Upright,
774 Sideways,
775}
776
777crate::impl_option!(
778 StyleTextOrientation,
779 OptionStyleTextOrientation,
780 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
781);
782
783impl PrintAsCssValue for StyleTextOrientation {
784 fn print_as_css_value(&self) -> String {
785 String::from(match self {
786 Self::Mixed => "mixed",
787 Self::Upright => "upright",
788 Self::Sideways => "sideways",
789 })
790 }
791}
792
793#[cfg(feature = "parser")]
794#[derive(Clone, PartialEq, Eq)]
795pub enum StyleTextOrientationParseError<'a> {
796 InvalidValue(&'a str),
797}
798
799#[cfg(feature = "parser")]
800crate::impl_debug_as_display!(StyleTextOrientationParseError<'a>);
801
802#[cfg(feature = "parser")]
803crate::impl_display! { StyleTextOrientationParseError<'a>, {
804 InvalidValue(val) => format!("Invalid text-orientation value: \"{}\"", val),
805}}
806
807#[cfg(feature = "parser")]
808#[derive(Debug, Clone, PartialEq, Eq)]
809#[repr(C, u8)]
810pub enum StyleTextOrientationParseErrorOwned {
811 InvalidValue(crate::AzString),
812}
813
814#[cfg(feature = "parser")]
815impl StyleTextOrientationParseError<'_> {
816 #[must_use]
817 pub fn to_contained(&self) -> StyleTextOrientationParseErrorOwned {
818 match self {
819 Self::InvalidValue(s) => {
820 StyleTextOrientationParseErrorOwned::InvalidValue((*s).to_string().into())
821 }
822 }
823 }
824}
825
826#[cfg(feature = "parser")]
827impl StyleTextOrientationParseErrorOwned {
828 #[must_use]
829 pub fn to_shared(&self) -> StyleTextOrientationParseError<'_> {
830 match self {
831 Self::InvalidValue(s) => StyleTextOrientationParseError::InvalidValue(s.as_str()),
832 }
833 }
834}
835
836#[cfg(feature = "parser")]
837pub fn parse_style_text_orientation(
841 input: &str,
842) -> Result<StyleTextOrientation, StyleTextOrientationParseError<'_>> {
843 let input = input.trim();
844 match input {
845 "mixed" => Ok(StyleTextOrientation::Mixed),
846 "upright" => Ok(StyleTextOrientation::Upright),
847 "sideways" => Ok(StyleTextOrientation::Sideways),
848 _ => Err(StyleTextOrientationParseError::InvalidValue(input)),
849 }
850}
851
852#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
857#[repr(C)]
858pub struct StyleObjectPosition {
859 pub horizontal: crate::props::style::background::BackgroundPositionHorizontal,
860 pub vertical: crate::props::style::background::BackgroundPositionVertical,
861}
862
863impl Default for StyleObjectPosition {
864 fn default() -> Self {
865 use crate::props::basic::pixel::PixelValue;
866 Self {
867 horizontal: crate::props::style::background::BackgroundPositionHorizontal::Exact(
868 PixelValue::percent(50.0),
869 ),
870 vertical: crate::props::style::background::BackgroundPositionVertical::Exact(
871 PixelValue::percent(50.0),
872 ),
873 }
874 }
875}
876
877crate::impl_option!(
878 StyleObjectPosition,
879 OptionStyleObjectPosition,
880 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
881);
882
883impl PrintAsCssValue for StyleObjectPosition {
884 fn print_as_css_value(&self) -> String {
885 format!(
886 "{} {}",
887 self.horizontal.print_as_css_value(),
888 self.vertical.print_as_css_value()
889 )
890 }
891}
892
893#[cfg(feature = "parser")]
894#[derive(Clone, PartialEq, Eq)]
895pub enum StyleObjectPositionParseError<'a> {
896 InvalidValue(&'a str),
897}
898
899#[cfg(feature = "parser")]
900crate::impl_debug_as_display!(StyleObjectPositionParseError<'a>);
901
902#[cfg(feature = "parser")]
903crate::impl_display! { StyleObjectPositionParseError<'a>, {
904 InvalidValue(val) => format!("Invalid object-position value: \"{}\"", val),
905}}
906
907#[cfg(feature = "parser")]
908#[derive(Debug, Clone, PartialEq, Eq)]
909#[repr(C, u8)]
910pub enum StyleObjectPositionParseErrorOwned {
911 InvalidValue(crate::AzString),
912}
913
914#[cfg(feature = "parser")]
915impl StyleObjectPositionParseError<'_> {
916 #[must_use]
917 pub fn to_contained(&self) -> StyleObjectPositionParseErrorOwned {
918 match self {
919 Self::InvalidValue(s) => {
920 StyleObjectPositionParseErrorOwned::InvalidValue((*s).to_string().into())
921 }
922 }
923 }
924}
925
926#[cfg(feature = "parser")]
927impl StyleObjectPositionParseErrorOwned {
928 #[must_use]
929 pub fn to_shared(&self) -> StyleObjectPositionParseError<'_> {
930 match self {
931 Self::InvalidValue(s) => StyleObjectPositionParseError::InvalidValue(s.as_str()),
932 }
933 }
934}
935
936#[cfg(feature = "parser")]
939pub fn parse_style_object_position(
943 input: &str,
944) -> Result<StyleObjectPosition, StyleObjectPositionParseError<'_>> {
945 use crate::props::basic::pixel::parse_pixel_value;
946 use crate::props::style::background::{
947 BackgroundPositionHorizontal, BackgroundPositionVertical,
948 };
949
950 let input = input.trim();
951 let parts: Vec<&str> = input.split_whitespace().collect();
952
953 let (h, v) = match parts.len() {
954 1 => {
955 let val = parts[0];
956 match val {
957 "center" => (
958 BackgroundPositionHorizontal::Center,
959 BackgroundPositionVertical::Center,
960 ),
961 "left" => (
962 BackgroundPositionHorizontal::Left,
963 BackgroundPositionVertical::Center,
964 ),
965 "right" => (
966 BackgroundPositionHorizontal::Right,
967 BackgroundPositionVertical::Center,
968 ),
969 "top" => (
970 BackgroundPositionHorizontal::Center,
971 BackgroundPositionVertical::Top,
972 ),
973 "bottom" => (
974 BackgroundPositionHorizontal::Center,
975 BackgroundPositionVertical::Bottom,
976 ),
977 _ => {
978 let px = parse_pixel_value(val)
979 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
980 (
981 BackgroundPositionHorizontal::Exact(px),
982 BackgroundPositionVertical::Exact(px),
983 )
984 }
985 }
986 }
987 2 => {
988 let (a, b) = (parts[0], parts[1]);
993 let both_keywords = matches!(a, "left" | "center" | "right" | "top" | "bottom")
994 && matches!(b, "left" | "center" | "right" | "top" | "bottom");
995 let reversed =
996 both_keywords && (matches!(a, "top" | "bottom") || matches!(b, "left" | "right"));
997 let (h_str, v_str) = if reversed { (b, a) } else { (a, b) };
998
999 let h = match h_str {
1000 "left" => BackgroundPositionHorizontal::Left,
1001 "center" => BackgroundPositionHorizontal::Center,
1002 "right" => BackgroundPositionHorizontal::Right,
1003 other => {
1004 let px = parse_pixel_value(other)
1005 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
1006 BackgroundPositionHorizontal::Exact(px)
1007 }
1008 };
1009 let v = match v_str {
1010 "top" => BackgroundPositionVertical::Top,
1011 "center" => BackgroundPositionVertical::Center,
1012 "bottom" => BackgroundPositionVertical::Bottom,
1013 other => {
1014 let px = parse_pixel_value(other)
1015 .map_err(|_| StyleObjectPositionParseError::InvalidValue(input))?;
1016 BackgroundPositionVertical::Exact(px)
1017 }
1018 };
1019 (h, v)
1020 }
1021 _ => return Err(StyleObjectPositionParseError::InvalidValue(input)),
1022 };
1023
1024 Ok(StyleObjectPosition {
1025 horizontal: h,
1026 vertical: v,
1027 })
1028}
1029
1030#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1034#[repr(C)]
1035pub struct AspectRatioValue {
1036 pub width: u32,
1037 pub height: u32,
1038}
1039
1040impl AspectRatioValue {
1041 fn fmt_component(v: u32) -> String {
1046 let int = v / 1000;
1047 let frac = v % 1000;
1048 if frac == 0 {
1049 int.to_string()
1050 } else {
1051 let frac_str = format!("{frac:03}");
1052 format!("{int}.{}", frac_str.trim_end_matches('0'))
1053 }
1054 }
1055}
1056#[allow(variant_size_differences)]
1057#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1063#[repr(C, u8)]
1064#[derive(Default)]
1065pub enum StyleAspectRatio {
1066 #[default]
1068 Auto,
1069 Ratio(AspectRatioValue),
1071}
1072
1073crate::impl_option!(
1074 StyleAspectRatio,
1075 OptionStyleAspectRatio,
1076 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
1077);
1078
1079impl PrintAsCssValue for StyleAspectRatio {
1080 fn print_as_css_value(&self) -> String {
1081 match self {
1082 Self::Auto => String::from("auto"),
1083 Self::Ratio(r) => format!(
1084 "{} / {}",
1085 AspectRatioValue::fmt_component(r.width),
1086 AspectRatioValue::fmt_component(r.height)
1087 ),
1088 }
1089 }
1090}
1091
1092#[cfg(feature = "parser")]
1093#[derive(Clone, PartialEq, Eq)]
1094pub enum StyleAspectRatioParseError<'a> {
1095 InvalidValue(&'a str),
1096}
1097
1098#[cfg(feature = "parser")]
1099crate::impl_debug_as_display!(StyleAspectRatioParseError<'a>);
1100
1101#[cfg(feature = "parser")]
1102crate::impl_display! { StyleAspectRatioParseError<'a>, {
1103 InvalidValue(val) => format!("Invalid aspect-ratio value: \"{}\"", val),
1104}}
1105
1106#[cfg(feature = "parser")]
1107#[derive(Debug, Clone, PartialEq, Eq)]
1108#[repr(C, u8)]
1109pub enum StyleAspectRatioParseErrorOwned {
1110 InvalidValue(crate::AzString),
1111}
1112
1113#[cfg(feature = "parser")]
1114impl StyleAspectRatioParseError<'_> {
1115 #[must_use]
1116 pub fn to_contained(&self) -> StyleAspectRatioParseErrorOwned {
1117 match self {
1118 Self::InvalidValue(s) => {
1119 StyleAspectRatioParseErrorOwned::InvalidValue((*s).to_string().into())
1120 }
1121 }
1122 }
1123}
1124
1125#[cfg(feature = "parser")]
1126impl StyleAspectRatioParseErrorOwned {
1127 #[must_use]
1128 pub fn to_shared(&self) -> StyleAspectRatioParseError<'_> {
1129 match self {
1130 Self::InvalidValue(s) => StyleAspectRatioParseError::InvalidValue(s.as_str()),
1131 }
1132 }
1133}
1134
1135#[cfg(feature = "parser")]
1139#[inline]
1140#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1141const fn aspect_f32_to_u32(v: f32) -> u32 {
1142 v as u32
1143}
1144
1145#[cfg(feature = "parser")]
1152fn ratio_from_components(
1153 w: f32,
1154 h: f32,
1155 input: &str,
1156) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'_>> {
1157 if !(w > 0.0 && w <= 100_000.0 && h > 0.0 && h <= 100_000.0) {
1158 return Err(StyleAspectRatioParseError::InvalidValue(input));
1159 }
1160 let width = aspect_f32_to_u32((w * 1000.0).round());
1161 let height = aspect_f32_to_u32((h * 1000.0).round());
1162 if width == 0 || height == 0 {
1163 return Err(StyleAspectRatioParseError::InvalidValue(input));
1164 }
1165 Ok(StyleAspectRatio::Ratio(AspectRatioValue { width, height }))
1166}
1167
1168#[cfg(feature = "parser")]
1170pub fn parse_style_aspect_ratio(
1174 input: &str,
1175) -> Result<StyleAspectRatio, StyleAspectRatioParseError<'_>> {
1176 let input = input.trim();
1177 if input == "auto" {
1178 return Ok(StyleAspectRatio::Auto);
1179 }
1180 if let Some(slash_pos) = input.find('/') {
1182 let w_str = input[..slash_pos].trim();
1183 let h_str = input[slash_pos + 1..].trim();
1184 let w: f32 = w_str
1185 .parse()
1186 .map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1187 let h: f32 = h_str
1188 .parse()
1189 .map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1190 return ratio_from_components(w, h, input);
1191 }
1192 let w: f32 = input
1194 .parse()
1195 .map_err(|_| StyleAspectRatioParseError::InvalidValue(input))?;
1196 ratio_from_components(w, 1.0, input)
1197}
1198
1199#[cfg(all(test, feature = "parser"))]
1200#[allow(
1201 clippy::float_cmp,
1202 clippy::unreadable_literal,
1203 clippy::too_many_lines,
1204 clippy::cast_precision_loss
1205)]
1206mod autotest_generated {
1207 use super::*;
1208 use crate::props::{
1209 basic::{error::ParseFloatError as CssParseFloatError, pixel::PixelValue},
1210 formatter::PrintAsCssValue,
1211 style::background::{BackgroundPositionHorizontal, BackgroundPositionVertical},
1212 };
1213
1214 const ALL_VISIBILITY: [StyleVisibility; 3] = [
1215 StyleVisibility::Visible,
1216 StyleVisibility::Hidden,
1217 StyleVisibility::Collapse,
1218 ];
1219
1220 const ALL_BLEND_MODES: [StyleMixBlendMode; 16] = [
1221 StyleMixBlendMode::Normal,
1222 StyleMixBlendMode::Multiply,
1223 StyleMixBlendMode::Screen,
1224 StyleMixBlendMode::Overlay,
1225 StyleMixBlendMode::Darken,
1226 StyleMixBlendMode::Lighten,
1227 StyleMixBlendMode::ColorDodge,
1228 StyleMixBlendMode::ColorBurn,
1229 StyleMixBlendMode::HardLight,
1230 StyleMixBlendMode::SoftLight,
1231 StyleMixBlendMode::Difference,
1232 StyleMixBlendMode::Exclusion,
1233 StyleMixBlendMode::Hue,
1234 StyleMixBlendMode::Saturation,
1235 StyleMixBlendMode::Color,
1236 StyleMixBlendMode::Luminosity,
1237 ];
1238
1239 const ALL_CURSORS: [StyleCursor; 30] = [
1240 StyleCursor::Alias,
1241 StyleCursor::AllScroll,
1242 StyleCursor::Cell,
1243 StyleCursor::ColResize,
1244 StyleCursor::ContextMenu,
1245 StyleCursor::Copy,
1246 StyleCursor::Crosshair,
1247 StyleCursor::Default,
1248 StyleCursor::EResize,
1249 StyleCursor::EwResize,
1250 StyleCursor::Grab,
1251 StyleCursor::Grabbing,
1252 StyleCursor::Help,
1253 StyleCursor::Move,
1254 StyleCursor::NResize,
1255 StyleCursor::NsResize,
1256 StyleCursor::NeswResize,
1257 StyleCursor::NwseResize,
1258 StyleCursor::Pointer,
1259 StyleCursor::Progress,
1260 StyleCursor::RowResize,
1261 StyleCursor::SResize,
1262 StyleCursor::SeResize,
1263 StyleCursor::Text,
1264 StyleCursor::Unset,
1265 StyleCursor::VerticalText,
1266 StyleCursor::WResize,
1267 StyleCursor::Wait,
1268 StyleCursor::ZoomIn,
1269 StyleCursor::ZoomOut,
1270 ];
1271
1272 const ALL_OBJECT_FIT: [StyleObjectFit; 5] = [
1273 StyleObjectFit::Fill,
1274 StyleObjectFit::Contain,
1275 StyleObjectFit::Cover,
1276 StyleObjectFit::None,
1277 StyleObjectFit::ScaleDown,
1278 ];
1279
1280 const ALL_TEXT_ORIENTATION: [StyleTextOrientation; 3] = [
1281 StyleTextOrientation::Mixed,
1282 StyleTextOrientation::Upright,
1283 StyleTextOrientation::Sideways,
1284 ];
1285
1286 const HOSTILE_KEYWORDS: [&str; 14] = [
1289 "",
1290 " ",
1291 "\t\n\r",
1292 "\u{a0}", ";",
1294 "{}",
1295 "/*",
1296 "0",
1297 "-1",
1298 "NaN",
1299 "inf",
1300 "\u{1F600}", "e\u{0301}", "\u{0665}", ];
1304
1305 #[test]
1308 fn blend_mode_display_is_well_formed_for_every_variant() {
1309 for mode in ALL_BLEND_MODES {
1310 let shown = mode.to_string();
1311 assert!(!shown.is_empty(), "{mode:?} renders as an empty string");
1312 assert!(
1313 shown.chars().all(|c| c.is_ascii_lowercase() || c == '-'),
1314 "{mode:?} renders as {shown:?}, which is not a CSS ident"
1315 );
1316 assert_eq!(shown, mode.print_as_css_value());
1319 }
1320 }
1321
1322 #[test]
1323 fn blend_mode_display_of_default_is_normal() {
1324 assert_eq!(StyleMixBlendMode::default().to_string(), "normal");
1325 assert_eq!(StyleMixBlendMode::default(), StyleMixBlendMode::Normal);
1326 }
1327
1328 #[test]
1329 fn blend_mode_display_survives_width_and_precision_flags() {
1330 assert_eq!(format!("{:>10}", StyleMixBlendMode::Normal), "normal");
1334 assert_eq!(format!("{:.2}", StyleMixBlendMode::Multiply), "multiply");
1335 assert_eq!(
1336 format!("{:*^30}", StyleMixBlendMode::ColorDodge),
1337 "color-dodge"
1338 );
1339 }
1340
1341 #[test]
1344 fn opacity_rejects_empty_and_whitespace_only_input() {
1345 for input in ["", " ", " ", "\t\n", "\r\n\t ", "\u{a0}"] {
1346 assert!(
1347 parse_style_opacity(input).is_err(),
1348 "{input:?} must not parse as an opacity"
1349 );
1350 }
1351 }
1352
1353 #[test]
1354 fn opacity_rejects_garbage() {
1355 for input in [
1356 "auto",
1357 "abc",
1358 "%",
1359 ";;;",
1360 "50%%",
1361 "#0.5",
1362 "0.5;garbage",
1363 "1 2",
1364 "rgb(0,0,0)",
1365 "0,5",
1366 "--",
1367 "..",
1368 "-",
1369 ".",
1370 ] {
1371 assert!(
1372 parse_style_opacity(input).is_err(),
1373 "{input:?} must not parse as an opacity"
1374 );
1375 }
1376 }
1377
1378 #[test]
1379 fn opacity_boundary_numbers() {
1380 assert_eq!(parse_style_opacity("0").unwrap().inner.normalized(), 0.0);
1382 assert_eq!(parse_style_opacity("1").unwrap().inner.normalized(), 1.0);
1383 assert_eq!(parse_style_opacity("0%").unwrap().inner.normalized(), 0.0);
1384 assert_eq!(parse_style_opacity("100%").unwrap().inner.normalized(), 1.0);
1385 assert_eq!(parse_style_opacity("-0").unwrap().inner.normalized(), 0.0);
1387 assert_eq!(parse_style_opacity("-0%").unwrap().inner.normalized(), 0.0);
1388 assert!(parse_style_opacity("0.0000001").is_ok());
1390
1391 for input in ["1.001", "1.1", "2", "101%", "-0.001", "-1", "-100%"] {
1393 assert!(
1394 matches!(
1395 parse_style_opacity(input),
1396 Err(OpacityParseError::OutOfRange(_))
1397 ),
1398 "{input:?} should be rejected as out-of-range"
1399 );
1400 }
1401
1402 for input in ["1e39", "3.5e38", "9223372036854775807", "1e30"] {
1405 assert!(
1406 parse_style_opacity(input).is_err(),
1407 "{input:?} should be rejected as out-of-range"
1408 );
1409 }
1410
1411 for input in ["NaN", "nan", "inf", "infinity", "-inf", "-NaN"] {
1413 assert!(
1414 parse_style_opacity(input).is_err(),
1415 "{input:?} should be rejected"
1416 );
1417 }
1418 }
1419
1420 #[test]
1421 fn opacity_trims_but_rejects_trailing_junk() {
1422 assert_eq!(
1423 parse_style_opacity(" 0.5 ").unwrap().inner.normalized(),
1424 0.5
1425 );
1426 assert_eq!(
1427 parse_style_opacity("\t50%\n").unwrap().inner.normalized(),
1428 0.5
1429 );
1430 for input in ["0.5;", "0.5 !important", "0.5px", "0.5 0.5"] {
1431 assert!(
1432 parse_style_opacity(input).is_err(),
1433 "{input:?} must not parse as an opacity"
1434 );
1435 }
1436
1437 assert_eq!(parse_style_opacity("50 %").unwrap().inner.normalized(), 0.5);
1441 }
1442
1443 #[test]
1444 fn opacity_non_numeric_unicode_does_not_panic() {
1445 for input in [
1450 "\u{1F600}", "\u{1F600}0.5", "0.5\u{0301}", "\u{2603}%", "\u{4F60}\u{597D}", "\u{202E}0.5", ] {
1457 assert!(
1458 parse_style_opacity(input).is_err(),
1459 "{input:?} must not parse as an opacity"
1460 );
1461 }
1462 }
1463
1464 #[test]
1465 fn opacity_extremely_long_input_terminates() {
1466 let huge = "1".repeat(100_000);
1468 assert!(parse_style_opacity(&huge).is_err());
1469
1470 let tiny = format!("0.{}5", "0".repeat(100_000));
1473 assert_eq!(parse_style_opacity(&tiny).unwrap().inner.normalized(), 0.0);
1474
1475 let long_unit = format!("0.5{}", "z".repeat(100_000));
1477 assert!(parse_style_opacity(&long_unit).is_err());
1478 }
1479
1480 #[test]
1481 fn opacity_deeply_nested_brackets_do_not_stack_overflow() {
1482 let nested = "(".repeat(10_000);
1483 assert!(parse_style_opacity(&nested).is_err());
1484
1485 let wrapped = format!("{}0.5{}", "(".repeat(10_000), ")".repeat(10_000));
1486 assert!(parse_style_opacity(&wrapped).is_err());
1487 }
1488
1489 #[test]
1490 fn opacity_valid_minimal_positive_control() {
1491 assert!(parse_style_opacity("1").unwrap() == StyleOpacity::default());
1492 assert!(parse_style_opacity("50%").unwrap() == StyleOpacity::new(50.0));
1493 assert!(parse_style_opacity("0.5").unwrap() == StyleOpacity::new(50.0));
1494 assert!(parse_style_opacity("0.5").unwrap() == parse_style_opacity("50%").unwrap());
1496 }
1497
1498 #[test]
1499 fn opacity_round_trips_through_print_as_css_value_and_display() {
1500 for pct in [0.0f32, 12.5, 25.0, 50.0, 75.0, 99.9, 100.0] {
1501 let opacity = StyleOpacity::new(pct);
1502
1503 let printed = opacity.print_as_css_value();
1505 let reparsed = parse_style_opacity(&printed)
1506 .unwrap_or_else(|e| panic!("{printed:?} (from {pct}%) failed to re-parse: {e}"));
1507 assert_eq!(
1508 reparsed.inner.normalized(),
1509 opacity.inner.normalized(),
1510 "{pct}% printed as {printed:?} but re-parsed differently"
1511 );
1512
1513 let displayed = opacity.to_string();
1515 let reparsed = parse_style_opacity(&displayed)
1516 .unwrap_or_else(|e| panic!("{displayed:?} (from {pct}%) failed to re-parse: {e}"));
1517 assert_eq!(reparsed.inner.normalized(), opacity.inner.normalized());
1518 }
1519 }
1520
1521 #[test]
1524 fn visibility_parses_every_keyword_and_round_trips() {
1525 assert_eq!(
1526 parse_style_visibility("visible").unwrap(),
1527 StyleVisibility::Visible
1528 );
1529 assert_eq!(
1530 parse_style_visibility("hidden").unwrap(),
1531 StyleVisibility::Hidden
1532 );
1533 assert_eq!(
1534 parse_style_visibility("collapse").unwrap(),
1535 StyleVisibility::Collapse
1536 );
1537 assert_eq!(StyleVisibility::default(), StyleVisibility::Visible);
1538
1539 for v in ALL_VISIBILITY {
1540 let printed = v.print_as_css_value();
1541 assert!(!printed.is_empty());
1542 assert_eq!(parse_style_visibility(&printed).unwrap(), v);
1543 assert_eq!(
1545 parse_style_visibility(&format!(" {printed}\t")).unwrap(),
1546 v
1547 );
1548 }
1549 }
1550
1551 #[test]
1552 fn visibility_rejects_hostile_input() {
1553 for input in HOSTILE_KEYWORDS {
1554 assert!(
1555 parse_style_visibility(input).is_err(),
1556 "{input:?} must not parse as a visibility"
1557 );
1558 }
1559 for input in [
1560 "none",
1561 "show",
1562 "visible hidden",
1563 "visible;",
1564 "vis",
1565 "visibleX",
1566 ] {
1567 assert!(
1568 parse_style_visibility(input).is_err(),
1569 "{input:?} must not parse as a visibility"
1570 );
1571 }
1572 }
1573
1574 #[test]
1577 fn blend_mode_parses_every_keyword_and_round_trips() {
1578 for mode in ALL_BLEND_MODES {
1579 let printed = mode.print_as_css_value();
1580 assert_eq!(
1581 parse_style_mix_blend_mode(&printed).unwrap(),
1582 mode,
1583 "{printed:?} did not round-trip"
1584 );
1585 assert_eq!(
1586 parse_style_mix_blend_mode(&format!(" {printed} ")).unwrap(),
1587 mode
1588 );
1589 }
1590 assert_eq!(StyleMixBlendMode::default(), StyleMixBlendMode::Normal);
1591 }
1592
1593 #[test]
1594 fn blend_mode_rejects_hostile_input() {
1595 for input in HOSTILE_KEYWORDS {
1596 assert!(
1597 parse_style_mix_blend_mode(input).is_err(),
1598 "{input:?} must not parse as a mix-blend-mode"
1599 );
1600 }
1601 for input in [
1603 "mix",
1604 "color dodge",
1605 "color_dodge",
1606 "colordodge",
1607 "normal normal",
1608 "plus-lighter",
1609 "multiply;",
1610 "screen!",
1611 ] {
1612 assert!(
1613 parse_style_mix_blend_mode(input).is_err(),
1614 "{input:?} must not parse as a mix-blend-mode"
1615 );
1616 }
1617 }
1618
1619 #[test]
1622 fn cursor_parses_every_keyword_and_round_trips() {
1623 for cursor in ALL_CURSORS {
1624 let printed = cursor.print_as_css_value();
1625 assert_eq!(
1626 parse_style_cursor(&printed).unwrap(),
1627 cursor,
1628 "{printed:?} did not round-trip"
1629 );
1630 assert_eq!(
1631 parse_style_cursor(&format!("\n{printed} ")).unwrap(),
1632 cursor
1633 );
1634 }
1635 assert_eq!(StyleCursor::default(), StyleCursor::Default);
1636 }
1637
1638 #[test]
1639 fn cursor_keyword_printing_is_injective() {
1640 let mut printed: Vec<String> = ALL_CURSORS
1643 .iter()
1644 .map(PrintAsCssValue::print_as_css_value)
1645 .collect();
1646 printed.sort();
1647 let count = printed.len();
1648 printed.dedup();
1649 assert_eq!(
1650 printed.len(),
1651 count,
1652 "two StyleCursor variants print the same ident"
1653 );
1654 }
1655
1656 #[test]
1657 fn cursor_rejects_hostile_input() {
1658 for input in HOSTILE_KEYWORDS {
1659 assert!(
1660 parse_style_cursor(input).is_err(),
1661 "{input:?} must not parse as a cursor"
1662 );
1663 }
1664 for input in [
1665 "hand", "col resize", "e_resize",
1668 "pointer pointer",
1669 "url(cursor.png)",
1670 "auto", ] {
1672 assert!(
1673 parse_style_cursor(input).is_err(),
1674 "{input:?} must not parse as a cursor"
1675 );
1676 }
1677 }
1678
1679 #[test]
1682 fn object_fit_parses_every_keyword_and_round_trips() {
1683 for fit in ALL_OBJECT_FIT {
1684 let printed = fit.print_as_css_value();
1685 assert_eq!(parse_style_object_fit(&printed).unwrap(), fit);
1686 assert_eq!(
1687 parse_style_object_fit(&format!(" {printed} ")).unwrap(),
1688 fit
1689 );
1690 }
1691 assert_eq!(StyleObjectFit::default(), StyleObjectFit::Fill);
1692 }
1693
1694 #[test]
1695 fn object_fit_rejects_hostile_input() {
1696 for input in HOSTILE_KEYWORDS {
1697 assert!(
1698 parse_style_object_fit(input).is_err(),
1699 "{input:?} must not parse as an object-fit"
1700 );
1701 }
1702 for input in [
1703 "stretch",
1704 "scale_down",
1705 "scale down",
1706 "cover cover",
1707 "fill;",
1708 ] {
1709 assert!(
1710 parse_style_object_fit(input).is_err(),
1711 "{input:?} must not parse as an object-fit"
1712 );
1713 }
1714 }
1715
1716 #[test]
1719 fn text_orientation_parses_every_keyword_and_round_trips() {
1720 for orientation in ALL_TEXT_ORIENTATION {
1721 let printed = orientation.print_as_css_value();
1722 assert_eq!(parse_style_text_orientation(&printed).unwrap(), orientation);
1723 assert_eq!(
1724 parse_style_text_orientation(&format!("\t{printed}\n")).unwrap(),
1725 orientation
1726 );
1727 }
1728 assert_eq!(StyleTextOrientation::default(), StyleTextOrientation::Mixed);
1729 }
1730
1731 #[test]
1732 fn text_orientation_rejects_hostile_input() {
1733 for input in HOSTILE_KEYWORDS {
1734 assert!(
1735 parse_style_text_orientation(input).is_err(),
1736 "{input:?} must not parse as a text-orientation"
1737 );
1738 }
1739 for input in ["vertical", "sideways-right", "upright mixed", "mixed;"] {
1740 assert!(
1741 parse_style_text_orientation(input).is_err(),
1742 "{input:?} must not parse as a text-orientation"
1743 );
1744 }
1745 }
1746
1747 #[test]
1750 fn keyword_parsers_are_case_sensitive() {
1751 assert!(parse_style_visibility("VISIBLE").is_err());
1756 assert!(parse_style_mix_blend_mode("Multiply").is_err());
1757 assert!(parse_style_cursor("Pointer").is_err());
1758 assert!(parse_style_object_fit("COVER").is_err());
1759 assert!(parse_style_text_orientation("Upright").is_err());
1760 assert!(parse_style_aspect_ratio("AUTO").is_err());
1761 }
1762
1763 #[test]
1764 fn keyword_parsers_do_not_hang_on_extremely_long_input() {
1765 let long = "a".repeat(500_000);
1766 assert!(parse_style_visibility(&long).is_err());
1767 assert!(parse_style_mix_blend_mode(&long).is_err());
1768 assert!(parse_style_cursor(&long).is_err());
1769 assert!(parse_style_object_fit(&long).is_err());
1770 assert!(parse_style_text_orientation(&long).is_err());
1771
1772 let padded = format!("{}visible{}", " ".repeat(250_000), " ".repeat(250_000));
1775 assert_eq!(
1776 parse_style_visibility(&padded).unwrap(),
1777 StyleVisibility::Visible
1778 );
1779 }
1780
1781 #[test]
1782 fn keyword_parsers_do_not_stack_overflow_on_nested_input() {
1783 let nested = format!("{}center{}", "(".repeat(10_000), ")".repeat(10_000));
1784 assert!(parse_style_visibility(&nested).is_err());
1785 assert!(parse_style_cursor(&nested).is_err());
1786 assert!(parse_style_object_fit(&nested).is_err());
1787 assert!(parse_style_object_position(&nested).is_err());
1788 assert!(parse_style_aspect_ratio(&nested).is_err());
1789 }
1790
1791 #[test]
1794 fn object_position_parses_single_keywords() {
1795 use BackgroundPositionHorizontal as H;
1796 use BackgroundPositionVertical as V;
1797
1798 for (input, h, v) in [
1799 ("center", H::Center, V::Center),
1800 ("left", H::Left, V::Center),
1801 ("right", H::Right, V::Center),
1802 ("top", H::Center, V::Top),
1803 ("bottom", H::Center, V::Bottom),
1804 ] {
1805 let parsed = parse_style_object_position(input).unwrap();
1806 assert_eq!(parsed.horizontal, h, "{input:?} horizontal");
1807 assert_eq!(parsed.vertical, v, "{input:?} vertical");
1808 }
1809 }
1810
1811 #[test]
1812 fn object_position_parses_lengths_and_percentages() {
1813 let px = parse_style_object_position("10px 20px").unwrap();
1814 assert_eq!(
1815 px.horizontal,
1816 BackgroundPositionHorizontal::Exact(PixelValue::px(10.0))
1817 );
1818 assert_eq!(
1819 px.vertical,
1820 BackgroundPositionVertical::Exact(PixelValue::px(20.0))
1821 );
1822
1823 let pct = parse_style_object_position("50% 50%").unwrap();
1824 assert_eq!(
1825 pct.horizontal,
1826 BackgroundPositionHorizontal::Exact(PixelValue::percent(50.0))
1827 );
1828 assert_eq!(
1829 pct.vertical,
1830 BackgroundPositionVertical::Exact(PixelValue::percent(50.0))
1831 );
1832
1833 let single = parse_style_object_position("25%").unwrap();
1835 assert_eq!(
1836 single.horizontal,
1837 BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0))
1838 );
1839 assert_eq!(
1840 single.vertical,
1841 BackgroundPositionVertical::Exact(PixelValue::percent(25.0))
1842 );
1843
1844 assert_eq!(
1846 parse_style_object_position("left 25%").unwrap(),
1847 StyleObjectPosition {
1848 horizontal: BackgroundPositionHorizontal::Left,
1849 vertical: BackgroundPositionVertical::Exact(PixelValue::percent(25.0)),
1850 }
1851 );
1852 assert_eq!(
1853 parse_style_object_position("25% top").unwrap(),
1854 StyleObjectPosition {
1855 horizontal: BackgroundPositionHorizontal::Exact(PixelValue::percent(25.0)),
1856 vertical: BackgroundPositionVertical::Top,
1857 }
1858 );
1859 }
1860
1861 #[test]
1862 fn object_position_collapses_internal_whitespace() {
1863 let expected = parse_style_object_position("left top").unwrap();
1865 for input in ["left top", "left\ttop", " left \n top ", "left\r\ntop"] {
1866 assert_eq!(
1867 parse_style_object_position(input).unwrap(),
1868 expected,
1869 "{input:?} should be equivalent to \"left top\""
1870 );
1871 }
1872 }
1873
1874 #[test]
1875 fn object_position_rejects_wrong_component_counts_and_garbage() {
1876 for input in [
1877 "",
1878 " ",
1879 "\t\n",
1880 "left top center",
1881 "10px 20px 30px",
1882 "center center center center",
1883 "invalid",
1884 "left left", "top top", "left,top", ";",
1888 "\u{1F600}",
1889 "\u{1F600} \u{1F600}",
1890 ] {
1891 assert!(
1892 parse_style_object_position(input).is_err(),
1893 "{input:?} must not parse as an object-position"
1894 );
1895 }
1896 }
1897
1898 #[test]
1899 fn object_position_extreme_lengths_do_not_panic() {
1900 for input in [
1904 "NaN NaN",
1905 "inf inf",
1906 "-inf",
1907 "1e39px",
1908 "-1e39px",
1909 "340282350000000000000000000000000000000px",
1910 ] {
1911 let _ = parse_style_object_position(input);
1912 }
1913 let long = format!("{}px", "9".repeat(100_000));
1914 let _ = parse_style_object_position(&long);
1915 }
1916
1917 #[test]
1918 fn object_position_round_trips_through_print_as_css_value() {
1919 use BackgroundPositionHorizontal as H;
1920 use BackgroundPositionVertical as V;
1921
1922 let horizontals = [
1923 H::Left,
1924 H::Center,
1925 H::Right,
1926 H::Exact(PixelValue::percent(25.0)),
1927 ];
1928 let verticals = [V::Top, V::Center, V::Bottom, V::Exact(PixelValue::px(30.0))];
1929
1930 for horizontal in horizontals {
1931 for vertical in verticals {
1932 let position = StyleObjectPosition {
1933 horizontal,
1934 vertical,
1935 };
1936 let printed = position.print_as_css_value();
1937 let reparsed = parse_style_object_position(&printed).unwrap_or_else(|e| {
1938 panic!("{position:?} printed as {printed:?}, which failed to re-parse: {e}")
1939 });
1940 assert_eq!(reparsed, position, "{printed:?} did not round-trip");
1941 }
1942 }
1943
1944 let default = StyleObjectPosition::default();
1946 assert_eq!(default.print_as_css_value(), "50% 50%");
1947 assert_eq!(parse_style_object_position("50% 50%").unwrap(), default);
1948 assert_eq!(
1949 parse_style_object_position("center")
1950 .unwrap()
1951 .print_as_css_value(),
1952 "center center"
1953 );
1954 }
1955
1956 #[test]
1959 fn aspect_f32_to_u32_saturates_instead_of_panicking() {
1960 assert_eq!(aspect_f32_to_u32(0.0), 0);
1962 assert_eq!(aspect_f32_to_u32(-0.0), 0);
1963 assert_eq!(aspect_f32_to_u32(0.9), 0);
1964 assert_eq!(aspect_f32_to_u32(1.0), 1);
1965 assert_eq!(aspect_f32_to_u32(1.9), 1);
1966 assert_eq!(aspect_f32_to_u32(f32::MIN_POSITIVE), 0);
1967
1968 assert_eq!(aspect_f32_to_u32(-1.0), 0);
1970 assert_eq!(aspect_f32_to_u32(-0.5), 0);
1971 assert_eq!(aspect_f32_to_u32(-1e30), 0);
1972 assert_eq!(aspect_f32_to_u32(f32::MIN), 0);
1973 assert_eq!(aspect_f32_to_u32(f32::NEG_INFINITY), 0);
1974
1975 assert_eq!(aspect_f32_to_u32(f32::MAX), u32::MAX);
1977 assert_eq!(aspect_f32_to_u32(f32::INFINITY), u32::MAX);
1978 assert_eq!(aspect_f32_to_u32(1e30), u32::MAX);
1979 assert_eq!(aspect_f32_to_u32(u32::MAX as f32), u32::MAX);
1981
1982 assert_eq!(aspect_f32_to_u32(f32::NAN), 0);
1984 assert_eq!(aspect_f32_to_u32(-f32::NAN), 0);
1985
1986 assert_eq!(aspect_f32_to_u32(100_000.0 * 1000.0), 100_000_000);
1988 }
1989
1990 #[test]
1991 fn aspect_f32_to_u32_is_usable_in_const_context() {
1992 const TRUNCATED: u32 = aspect_f32_to_u32(1.999);
1993 const SATURATED: u32 = aspect_f32_to_u32(f32::INFINITY);
1994 const NEGATIVE: u32 = aspect_f32_to_u32(-5.0);
1995 const NOT_A_NUMBER: u32 = aspect_f32_to_u32(f32::NAN);
1996 assert_eq!(
1997 (TRUNCATED, SATURATED, NEGATIVE, NOT_A_NUMBER),
1998 (1, u32::MAX, 0, 0)
1999 );
2000 }
2001
2002 #[test]
2005 fn aspect_ratio_parses_valid_forms() {
2006 assert_eq!(
2007 parse_style_aspect_ratio("auto").unwrap(),
2008 StyleAspectRatio::Auto
2009 );
2010 assert_eq!(StyleAspectRatio::default(), StyleAspectRatio::Auto);
2011
2012 for input in ["16 / 9", "16/9", "16 /9", "16/ 9", " 16 / 9 "] {
2013 assert_eq!(
2014 parse_style_aspect_ratio(input).unwrap(),
2015 StyleAspectRatio::Ratio(AspectRatioValue {
2016 width: 16000,
2017 height: 9000
2018 }),
2019 "{input:?} should parse as 16/9"
2020 );
2021 }
2022
2023 assert_eq!(
2025 parse_style_aspect_ratio("1").unwrap(),
2026 StyleAspectRatio::Ratio(AspectRatioValue {
2027 width: 1000,
2028 height: 1000
2029 })
2030 );
2031 assert_eq!(
2032 parse_style_aspect_ratio("1.5").unwrap(),
2033 StyleAspectRatio::Ratio(AspectRatioValue {
2034 width: 1500,
2035 height: 1000
2036 })
2037 );
2038
2039 assert_eq!(
2041 parse_style_aspect_ratio("100000").unwrap(),
2042 StyleAspectRatio::Ratio(AspectRatioValue {
2043 width: 100_000_000,
2044 height: 1000
2045 })
2046 );
2047 assert!(parse_style_aspect_ratio("100001").is_err());
2048 assert!(parse_style_aspect_ratio("100000.1 / 1").is_err());
2049 assert!(parse_style_aspect_ratio("1 / 100001").is_err());
2050 }
2051
2052 #[test]
2053 fn aspect_ratio_rejects_non_positive_and_malformed_input() {
2054 for input in [
2055 "",
2056 " ",
2057 "\t\n",
2058 "abc",
2059 "auto / auto",
2060 "16 / 9 / 4",
2061 "1/2/3",
2062 "/",
2063 "//",
2064 "/9",
2065 "16/",
2066 "16 9",
2067 ";",
2068 "16,9",
2069 "\u{1F600}",
2070 "\u{1F600}/\u{1F600}",
2071 ] {
2072 assert!(
2073 parse_style_aspect_ratio(input).is_err(),
2074 "{input:?} must not parse as an aspect-ratio"
2075 );
2076 }
2077
2078 for input in [
2080 "0", "0 / 1", "1 / 0", "0/0", "-0", "-0 / 1", "1 / -0", "-1 / 1", "-1", "-1.5",
2081 ] {
2082 assert!(
2083 parse_style_aspect_ratio(input).is_err(),
2084 "{input:?} must not parse as an aspect-ratio"
2085 );
2086 }
2087
2088 for input in [
2090 "inf", "inf / 1", "1 / inf", "-inf", "-inf / 1", "1e39", "1e39 / 1",
2091 ] {
2092 assert!(
2093 parse_style_aspect_ratio(input).is_err(),
2094 "{input:?} should be rejected: out of the [0, 100_000] range"
2095 );
2096 }
2097 }
2098
2099 #[test]
2100 fn aspect_ratio_extremely_long_input_terminates() {
2101 let long = "9".repeat(100_000);
2102 assert!(parse_style_aspect_ratio(&long).is_err());
2103 assert!(parse_style_aspect_ratio(&format!("{long}/{long}")).is_err());
2104
2105 let slashes = "/".repeat(100_000);
2107 assert!(parse_style_aspect_ratio(&slashes).is_err());
2108 }
2109
2110 #[test]
2111 fn aspect_ratio_auto_round_trips() {
2112 let printed = StyleAspectRatio::Auto.print_as_css_value();
2113 assert_eq!(printed, "auto");
2114 assert_eq!(
2115 parse_style_aspect_ratio(&printed).unwrap(),
2116 StyleAspectRatio::Auto
2117 );
2118 }
2119
2120 #[test]
2123 fn opacity_parse_error_round_trips_through_the_owned_form() {
2124 let errors = [
2125 OpacityParseError::ParsePercentage(
2126 PercentageParseError::ValueParseErr(CssParseFloatError::Empty),
2127 "",
2128 ),
2129 OpacityParseError::ParsePercentage(
2130 PercentageParseError::ValueParseErr(CssParseFloatError::Invalid),
2131 "abc",
2132 ),
2133 OpacityParseError::ParsePercentage(PercentageParseError::NoPercentSign, "0.5"),
2134 OpacityParseError::ParsePercentage(
2135 PercentageParseError::InvalidUnit(String::from("px").into()),
2136 "5px",
2137 ),
2138 OpacityParseError::OutOfRange("1.5"),
2139 OpacityParseError::OutOfRange(""),
2140 OpacityParseError::OutOfRange("\u{1F600}"),
2141 ];
2142
2143 for error in errors {
2144 let owned = error.to_contained();
2145 assert_eq!(owned.to_shared(), error, "{error:?} did not round-trip");
2146 assert_eq!(owned.to_shared().to_contained(), owned);
2147
2148 let shown = error.to_string();
2149 assert!(!shown.is_empty(), "{error:?} renders as an empty message");
2150 assert_eq!(format!("{error:?}"), shown);
2152 }
2153 }
2154
2155 #[test]
2156 fn opacity_parse_error_to_contained_copies_the_borrowed_input() {
2157 let owned = {
2159 let input = String::from("1.5");
2160 parse_style_opacity(&input).unwrap_err().to_contained()
2161 };
2162 assert_eq!(
2163 owned,
2164 OpacityParseErrorOwned::OutOfRange(String::from("1.5").into())
2165 );
2166 assert!(owned.to_shared().to_string().contains("1.5"));
2167 }
2168
2169 #[test]
2170 fn keyword_parse_errors_round_trip_through_the_owned_form() {
2171 for payload in ["", "junk", " ", "\u{1F600}", "a\0b", "\u{0665}"] {
2173 let visibility = StyleVisibilityParseError::InvalidValue(InvalidValueErr(payload));
2174 assert_eq!(visibility.to_contained().to_shared(), visibility);
2175 assert!(!visibility.to_string().is_empty());
2176 assert_eq!(format!("{visibility:?}"), visibility.to_string());
2177
2178 let blend = MixBlendModeParseError::InvalidValue(InvalidValueErr(payload));
2179 assert_eq!(blend.to_contained().to_shared(), blend);
2180 assert!(!blend.to_string().is_empty());
2181
2182 let cursor = CursorParseError::InvalidValue(InvalidValueErr(payload));
2183 assert_eq!(cursor.to_contained().to_shared(), cursor);
2184 assert!(!cursor.to_string().is_empty());
2185
2186 let object_fit = StyleObjectFitParseError::InvalidValue(payload);
2188 assert_eq!(object_fit.to_contained().to_shared(), object_fit);
2189 assert!(!object_fit.to_string().is_empty());
2190
2191 let orientation = StyleTextOrientationParseError::InvalidValue(payload);
2192 assert_eq!(orientation.to_contained().to_shared(), orientation);
2193 assert!(!orientation.to_string().is_empty());
2194
2195 let position = StyleObjectPositionParseError::InvalidValue(payload);
2196 assert_eq!(position.to_contained().to_shared(), position);
2197 assert!(!position.to_string().is_empty());
2198
2199 let ratio = StyleAspectRatioParseError::InvalidValue(payload);
2200 assert_eq!(ratio.to_contained().to_shared(), ratio);
2201 assert!(!ratio.to_string().is_empty());
2202 }
2203 }
2204
2205 #[test]
2206 fn parse_errors_quote_the_offending_input() {
2207 assert!(parse_style_visibility("show")
2210 .unwrap_err()
2211 .to_string()
2212 .contains("show"));
2213 assert!(parse_style_mix_blend_mode("mix")
2214 .unwrap_err()
2215 .to_string()
2216 .contains("mix"));
2217 assert!(parse_style_cursor("hand")
2218 .unwrap_err()
2219 .to_string()
2220 .contains("hand"));
2221 assert!(parse_style_object_fit("stretch")
2222 .unwrap_err()
2223 .to_string()
2224 .contains("stretch"));
2225 assert!(parse_style_text_orientation("vertical")
2226 .unwrap_err()
2227 .to_string()
2228 .contains("vertical"));
2229 assert!(parse_style_object_position("nope")
2230 .unwrap_err()
2231 .to_string()
2232 .contains("nope"));
2233 assert!(parse_style_aspect_ratio("nope")
2234 .unwrap_err()
2235 .to_string()
2236 .contains("nope"));
2237 assert!(parse_style_opacity("1.5")
2238 .unwrap_err()
2239 .to_string()
2240 .contains("1.5"));
2241 }
2242
2243 #[test]
2244 fn parse_errors_report_the_trimmed_input_not_the_raw_slice() {
2245 let shown = parse_style_cursor(" hand ").unwrap_err().to_string();
2248 assert!(
2249 shown.contains("\"hand\""),
2250 "expected the trimmed value, got {shown:?}"
2251 );
2252
2253 let shown = parse_style_opacity(" 1.5 ").unwrap_err().to_string();
2256 assert!(
2257 shown.contains("\" 1.5 \""),
2258 "expected the raw value, got {shown:?}"
2259 );
2260 }
2261
2262 #[test]
2263 fn owned_error_forms_are_independent_of_the_source_buffer() {
2264 let owned = {
2267 let input = String::from("stretch");
2268 parse_style_object_fit(&input).unwrap_err().to_contained()
2269 };
2270 assert_eq!(
2271 owned,
2272 StyleObjectFitParseErrorOwned::InvalidValue(String::from("stretch").into())
2273 );
2274 assert!(owned.to_shared().to_string().contains("stretch"));
2275 }
2276
2277 #[test]
2283 fn known_bug_opacity_multibyte_numeric_char_panics() {
2284 for input in ["\u{00BD}", "\u{00BD}%", "0.5\u{0665}", "\u{FF15}%"] {
2292 assert!(
2293 parse_style_opacity(input).is_err(),
2294 "{input:?} should be rejected, not panic"
2295 );
2296 }
2297 }
2298
2299 #[test]
2300 fn known_bug_aspect_ratio_nan_bypasses_the_range_guards() {
2301 for input in ["NaN", "nan", "NaN / 1", "1 / NaN", "nan/nan", "-NaN"] {
2308 assert!(
2309 parse_style_aspect_ratio(input).is_err(),
2310 "{input:?} should be rejected, but parsed as {:?}",
2311 parse_style_aspect_ratio(input)
2312 );
2313 }
2314 }
2315
2316 #[test]
2317 fn known_bug_aspect_ratio_tiny_positive_values_round_down_to_zero() {
2318 for input in ["0.0001", "0.0004 / 1", "1 / 0.0001", "1e-10"] {
2322 let Ok(StyleAspectRatio::Ratio(ratio)) = parse_style_aspect_ratio(input) else {
2323 continue; };
2325 assert!(
2326 ratio.width > 0 && ratio.height > 0,
2327 "{input:?} produced the degenerate ratio {ratio:?}"
2328 );
2329 }
2330 }
2331
2332 #[test]
2333 fn known_bug_aspect_ratio_does_not_survive_a_print_reparse_cycle() {
2334 let ratio = parse_style_aspect_ratio("16 / 9").unwrap();
2339 let printed = ratio.print_as_css_value();
2340 assert_eq!(
2341 printed, "16 / 9",
2342 "printed the fixed-point form: {printed:?}"
2343 );
2344 assert_eq!(parse_style_aspect_ratio(&printed).unwrap(), ratio);
2345 }
2346
2347 #[test]
2348 fn known_bug_object_position_rejects_reversed_keyword_pairs() {
2349 assert_eq!(
2354 parse_style_object_position("top left").unwrap(),
2355 parse_style_object_position("left top").unwrap()
2356 );
2357 assert_eq!(
2358 parse_style_object_position("bottom right").unwrap(),
2359 parse_style_object_position("right bottom").unwrap()
2360 );
2361 }
2362}