1use alloc::{
13 string::{String, ToString},
14 vec::Vec,
15};
16
17use crate::{
18 impl_option, impl_option_inner, impl_vec, impl_vec_clone, impl_vec_debug, impl_vec_eq,
19 impl_vec_hash, impl_vec_mut, impl_vec_ord, impl_vec_partialeq, impl_vec_partialord,
20 props::{
21 basic::pixel::{CssPixelValueParseError, CssPixelValueParseErrorOwned, PixelValue},
22 formatter::PrintAsCssValue,
23 macros::PixelValueTaker,
24 },
25};
26
27#[allow(variant_size_differences)]
29#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
45#[repr(C, u8)]
46pub enum CalcAstItem {
47 Value(PixelValue),
49 Add,
51 Sub,
53 Mul,
55 Div,
57 BraceOpen,
59 BraceClose,
61}
62
63impl_vec!(
65 CalcAstItem,
66 CalcAstItemVec,
67 CalcAstItemVecDestructor,
68 CalcAstItemVecDestructorType,
69 CalcAstItemVecSlice,
70 OptionCalcAstItem
71);
72impl_vec_clone!(CalcAstItem, CalcAstItemVec, CalcAstItemVecDestructor);
73impl_vec_debug!(CalcAstItem, CalcAstItemVec);
74impl_vec_partialeq!(CalcAstItem, CalcAstItemVec);
75impl_vec_eq!(CalcAstItem, CalcAstItemVec);
76impl_vec_partialord!(CalcAstItem, CalcAstItemVec);
77impl_vec_ord!(CalcAstItem, CalcAstItemVec);
78impl_vec_hash!(CalcAstItem, CalcAstItemVec);
79impl_vec_mut!(CalcAstItem, CalcAstItemVec);
80
81impl_option!(
82 CalcAstItem,
83 OptionCalcAstItem,
84 copy = false,
85 [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
86);
87
88#[cfg(feature = "parser")]
102fn parse_calc_expression(input: &str) -> Result<CalcAstItemVec, ()> {
103 use crate::props::basic::pixel::parse_pixel_value;
104
105 let mut items: Vec<CalcAstItem> = Vec::new();
106 let input = input.trim();
107 let bytes = input.as_bytes();
108 let mut i = 0;
109
110 while i < bytes.len() {
111 if bytes[i].is_ascii_whitespace() {
113 i += 1;
114 continue;
115 }
116
117 match bytes[i] {
118 b'+' => {
119 items.push(CalcAstItem::Add);
120 i += 1;
121 }
122 b'*' => {
123 items.push(CalcAstItem::Mul);
124 i += 1;
125 }
126 b'/' => {
127 items.push(CalcAstItem::Div);
128 i += 1;
129 }
130 b'(' => {
131 items.push(CalcAstItem::BraceOpen);
132 i += 1;
133 }
134 b')' => {
135 items.push(CalcAstItem::BraceClose);
136 i += 1;
137 }
138 b'-' => {
139 let is_negative_number = items.is_empty()
144 || matches!(
145 items.last(),
146 Some(
147 CalcAstItem::Add
148 | CalcAstItem::Sub
149 | CalcAstItem::Mul
150 | CalcAstItem::Div
151 | CalcAstItem::BraceOpen
152 )
153 );
154
155 if is_negative_number {
156 let rest = &input[i..];
158 let end = find_value_end(rest);
159 if end == 0 {
160 return Err(());
161 }
162 let val_str = &rest[..end];
163 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
164 items.push(CalcAstItem::Value(pv));
165 i += end;
166 } else {
167 items.push(CalcAstItem::Sub);
168 i += 1;
169 }
170 }
171 _ => {
172 let rest = &input[i..];
174 let end = find_value_end(rest);
175 if end == 0 {
176 return Err(());
177 }
178 let val_str = &rest[..end];
179 let pv = parse_pixel_value(val_str).map_err(|_| ())?;
180 items.push(CalcAstItem::Value(pv));
181 i += end;
182 }
183 }
184 }
185
186 if items.is_empty() {
187 return Err(());
188 }
189
190 Ok(CalcAstItemVec::from(items))
191}
192
193#[cfg(feature = "parser")]
196fn find_value_end(s: &str) -> usize {
197 let bytes = s.as_bytes();
198 let mut i = 0;
199
200 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
202 i += 1;
203 }
204
205 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
207 i += 1;
208 }
209
210 while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
212 i += 1;
213 }
214
215 i
216}
217
218fn calc_ast_to_css_string(items: &CalcAstItemVec) -> String {
220 let inner: Vec<String> = items
221 .iter()
222 .map(|i| match i {
223 CalcAstItem::Value(v) => v.to_string(),
224 CalcAstItem::Add => "+".to_string(),
225 CalcAstItem::Sub => "-".to_string(),
226 CalcAstItem::Mul => "*".to_string(),
227 CalcAstItem::Div => "/".to_string(),
228 CalcAstItem::BraceOpen => "(".to_string(),
229 CalcAstItem::BraceClose => ")".to_string(),
230 })
231 .collect();
232 alloc::format!("calc({})", inner.join(" "))
233}
234
235macro_rules! define_dimension_property {
238 ($struct_name:ident, $default_fn:expr) => {
239 #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
240 #[repr(C)]
241 pub struct $struct_name {
242 pub inner: PixelValue,
243 }
244
245 impl Default for $struct_name {
246 fn default() -> Self {
247 $default_fn()
248 }
249 }
250
251 impl PixelValueTaker for $struct_name {
252 fn from_pixel_value(inner: PixelValue) -> Self {
253 Self { inner }
254 }
255 }
256
257 impl_pixel_value!($struct_name);
258
259 impl PrintAsCssValue for $struct_name {
260 fn print_as_css_value(&self) -> String {
261 self.inner.to_string()
262 }
263 }
264 };
265}
266
267macro_rules! define_sizing_enum {
268 ($name:ident) => {
269 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
270 #[repr(C, u8)]
271 #[derive(Default)]
272 pub enum $name {
273 #[default]
274 Auto,
275 Px(PixelValue),
276 MinContent,
277 MaxContent,
278 FitContent(PixelValue),
280 Calc(CalcAstItemVec),
282 }
283
284 impl PixelValueTaker for $name {
285 fn from_pixel_value(inner: PixelValue) -> Self {
286 $name::Px(inner)
287 }
288 }
289
290 impl PrintAsCssValue for $name {
291 fn print_as_css_value(&self) -> String {
292 match self {
293 $name::Auto => "auto".to_string(),
294 $name::Px(v) => v.to_string(),
295 $name::MinContent => "min-content".to_string(),
296 $name::MaxContent => "max-content".to_string(),
297 $name::FitContent(v) => alloc::format!("fit-content({})", v),
298 $name::Calc(items) => calc_ast_to_css_string(items),
299 }
300 }
301 }
302
303 impl $name {
304 #[must_use]
305 pub fn px(value: f32) -> Self {
306 $name::Px(PixelValue::px(value))
307 }
308
309 #[must_use]
310 pub const fn const_px(value: isize) -> Self {
311 $name::Px(PixelValue::const_px(value))
312 }
313
314 #[must_use]
315 pub fn interpolate(&self, other: &Self, t: f32) -> Self {
316 match (self, other) {
317 ($name::Px(a), $name::Px(b)) => $name::Px(a.interpolate(b, t)),
318 ($name::FitContent(a), $name::FitContent(b)) => {
319 $name::FitContent(a.interpolate(b, t))
320 }
321 (_, $name::Px(b)) if t >= 0.5 => $name::Px(*b),
322 ($name::Px(a), _) if t < 0.5 => $name::Px(*a),
323 ($name::Auto, $name::Auto) => $name::Auto,
324 (a, _) if t < 0.5 => a.clone(),
325 (_, b) => b.clone(),
326 }
327 }
328 }
329 };
330}
331
332define_sizing_enum!(LayoutWidth);
333define_sizing_enum!(LayoutHeight);
334
335define_dimension_property!(LayoutMinWidth, || Self {
337 inner: PixelValue::zero()
338});
339define_dimension_property!(LayoutMinHeight, || Self {
341 inner: PixelValue::zero()
342});
343define_dimension_property!(LayoutMaxWidth, || Self {
348 inner: PixelValue::px(core::f32::MAX)
349});
350define_dimension_property!(LayoutMaxHeight, || Self {
355 inner: PixelValue::px(core::f32::MAX)
356});
357
358#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
360#[repr(C)]
361#[derive(Default)]
362pub enum LayoutBoxSizing {
363 #[default]
364 ContentBox,
365 BorderBox,
366}
367
368impl PrintAsCssValue for LayoutBoxSizing {
369 fn print_as_css_value(&self) -> String {
370 String::from(match self {
371 Self::ContentBox => "content-box",
372 Self::BorderBox => "border-box",
373 })
374 }
375}
376
377#[cfg(feature = "parser")]
380pub mod parser {
381
382 use crate::corety::AzString;
383 use alloc::string::ToString;
384
385 #[allow(clippy::wildcard_imports)]
386 use super::*;
388 use crate::props::basic::pixel::parse_pixel_value;
389
390 macro_rules! define_pixel_dimension_parser {
391 ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
392 #[derive(Clone, PartialEq, Eq)]
393 pub enum $error_name<'a> {
394 PixelValue(CssPixelValueParseError<'a>),
395 }
396
397 impl_debug_as_display!($error_name<'a>);
398 impl_display! { $error_name<'a>, {
399 PixelValue(e) => format!("{}", e),
400 }}
401
402 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
403
404 #[derive(Debug, Clone, PartialEq, Eq)]
405 #[repr(C, u8)]
406 pub enum $error_owned_name {
407 PixelValue(CssPixelValueParseErrorOwned),
408 }
409
410 impl $error_name<'_> {
411 #[must_use]
412 pub fn to_contained(&self) -> $error_owned_name {
413 match self {
414 $error_name::PixelValue(e) => {
415 $error_owned_name::PixelValue(e.to_contained())
416 }
417 }
418 }
419 }
420
421 impl $error_owned_name {
422 #[must_use]
423 pub fn to_shared(&self) -> $error_name<'_> {
424 match self {
425 $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
426 }
427 }
428 }
429
430 pub fn $fn_name(input: &str) -> Result<$struct_name, $error_name<'_>> {
434 parse_pixel_value(input)
435 .map(|v| $struct_name { inner: v })
436 .map_err($error_name::PixelValue)
437 }
438 };
439 }
440
441 macro_rules! define_sizing_parser {
442 ($fn_name:ident, $enum_name:ident, $error_name:ident, $error_owned_name:ident, $keyword_label:expr) => {
443 #[derive(Clone, PartialEq, Eq)]
444 pub enum $error_name<'a> {
445 PixelValue(CssPixelValueParseError<'a>),
446 InvalidKeyword(&'a str),
447 }
448
449 impl_debug_as_display!($error_name<'a>);
450 impl_display! { $error_name<'a>, {
451 PixelValue(e) => format!("{}", e),
452 InvalidKeyword(k) => format!("Invalid {} keyword: \"{}\"", $keyword_label, k),
453 }}
454
455 impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
456
457 #[derive(Debug, Clone, PartialEq, Eq)]
458 #[repr(C, u8)]
459 pub enum $error_owned_name {
460 PixelValue(CssPixelValueParseErrorOwned),
461 InvalidKeyword(AzString),
462 }
463
464 impl $error_name<'_> {
465 #[must_use]
466 pub fn to_contained(&self) -> $error_owned_name {
467 match self {
468 $error_name::PixelValue(e) => {
469 $error_owned_name::PixelValue(e.to_contained())
470 }
471 $error_name::InvalidKeyword(k) => {
472 $error_owned_name::InvalidKeyword(k.to_string().into())
473 }
474 }
475 }
476 }
477
478 impl $error_owned_name {
479 #[must_use]
480 pub fn to_shared(&self) -> $error_name<'_> {
481 match self {
482 $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
483 $error_owned_name::InvalidKeyword(k) => $error_name::InvalidKeyword(k),
484 }
485 }
486 }
487
488 pub fn $fn_name(input: &str) -> Result<$enum_name, $error_name<'_>> {
492 let trimmed = input.trim();
493 match trimmed {
494 "auto" => Ok($enum_name::Auto),
495 "min-content" => Ok($enum_name::MinContent),
496 "max-content" => Ok($enum_name::MaxContent),
497 s if s.starts_with("fit-content(") && s.ends_with(')') => {
498 let inner = &s[12..s.len() - 1].trim();
499 parse_pixel_value(inner)
500 .map(|pv| {
501 if pv.number.get() < 0.0 {
502 $enum_name::FitContent(PixelValue::zero())
503 } else {
504 $enum_name::FitContent(pv)
505 }
506 })
507 .map_err($error_name::PixelValue)
508 }
509 s if s.starts_with("calc(") && s.ends_with(')') => {
510 let inner = &s[5..s.len() - 1];
511 parse_calc_expression(inner)
512 .map($enum_name::Calc)
513 .map_err(|_| $error_name::InvalidKeyword(input))
514 }
515 _ => parse_pixel_value(trimmed)
516 .map($enum_name::Px)
517 .map_err($error_name::PixelValue),
518 }
519 }
520 };
521 }
522
523 define_sizing_parser!(
524 parse_layout_width,
525 LayoutWidth,
526 LayoutWidthParseError,
527 LayoutWidthParseErrorOwned,
528 "width"
529 );
530 define_sizing_parser!(
531 parse_layout_height,
532 LayoutHeight,
533 LayoutHeightParseError,
534 LayoutHeightParseErrorOwned,
535 "height"
536 );
537 define_pixel_dimension_parser!(
538 parse_layout_min_width,
539 LayoutMinWidth,
540 LayoutMinWidthParseError,
541 LayoutMinWidthParseErrorOwned
542 );
543 define_pixel_dimension_parser!(
544 parse_layout_min_height,
545 LayoutMinHeight,
546 LayoutMinHeightParseError,
547 LayoutMinHeightParseErrorOwned
548 );
549 define_pixel_dimension_parser!(
550 parse_layout_max_width,
551 LayoutMaxWidth,
552 LayoutMaxWidthParseError,
553 LayoutMaxWidthParseErrorOwned
554 );
555 define_pixel_dimension_parser!(
556 parse_layout_max_height,
557 LayoutMaxHeight,
558 LayoutMaxHeightParseError,
559 LayoutMaxHeightParseErrorOwned
560 );
561
562 #[derive(Clone, PartialEq, Eq)]
565 pub enum LayoutBoxSizingParseError<'a> {
566 InvalidValue(&'a str),
567 }
568
569 impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
570 impl_display! { LayoutBoxSizingParseError<'a>, {
571 InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
572 }}
573
574 #[derive(Debug, Clone, PartialEq, Eq)]
575 #[repr(C, u8)]
576 pub enum LayoutBoxSizingParseErrorOwned {
577 InvalidValue(AzString),
578 }
579
580 impl LayoutBoxSizingParseError<'_> {
581 #[must_use]
582 pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
583 match self {
584 LayoutBoxSizingParseError::InvalidValue(s) => {
585 LayoutBoxSizingParseErrorOwned::InvalidValue((*s).to_string().into())
586 }
587 }
588 }
589 }
590
591 impl LayoutBoxSizingParseErrorOwned {
592 #[must_use]
593 pub fn to_shared(&self) -> LayoutBoxSizingParseError<'_> {
594 match self {
595 Self::InvalidValue(s) => LayoutBoxSizingParseError::InvalidValue(s),
596 }
597 }
598 }
599
600 pub fn parse_layout_box_sizing(
604 input: &str,
605 ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'_>> {
606 match input.trim() {
607 "content-box" => Ok(LayoutBoxSizing::ContentBox),
608 "border-box" => Ok(LayoutBoxSizing::BorderBox),
609 other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
610 }
611 }
612}
613
614#[cfg(feature = "parser")]
615pub use self::parser::*;
616
617#[cfg(all(test, feature = "parser"))]
618mod tests {
619 use super::*;
620 use crate::props::basic::pixel::PixelValue;
621
622 #[test]
623 fn test_parse_layout_width() {
624 assert_eq!(
625 parse_layout_width("150px").unwrap(),
626 LayoutWidth::Px(PixelValue::px(150.0))
627 );
628 assert_eq!(
629 parse_layout_width("2.5em").unwrap(),
630 LayoutWidth::Px(PixelValue::em(2.5))
631 );
632 assert_eq!(
633 parse_layout_width("75%").unwrap(),
634 LayoutWidth::Px(PixelValue::percent(75.0))
635 );
636 assert_eq!(
637 parse_layout_width("0").unwrap(),
638 LayoutWidth::Px(PixelValue::px(0.0))
639 );
640 assert_eq!(
641 parse_layout_width(" 100pt ").unwrap(),
642 LayoutWidth::Px(PixelValue::pt(100.0))
643 );
644 assert_eq!(
645 parse_layout_width("min-content").unwrap(),
646 LayoutWidth::MinContent
647 );
648 assert_eq!(
649 parse_layout_width("max-content").unwrap(),
650 LayoutWidth::MaxContent
651 );
652 }
653
654 #[test]
655 fn test_parse_layout_height_invalid() {
656 assert!(parse_layout_height("auto").is_ok());
658 assert!(parse_layout_height("150 px").is_ok());
660 assert!(parse_layout_height("px").is_err());
661 assert!(parse_layout_height("invalid").is_err());
662 }
663
664 #[test]
665 fn test_parse_layout_box_sizing() {
666 assert_eq!(
667 parse_layout_box_sizing("content-box").unwrap(),
668 LayoutBoxSizing::ContentBox
669 );
670 assert_eq!(
671 parse_layout_box_sizing("border-box").unwrap(),
672 LayoutBoxSizing::BorderBox
673 );
674 assert_eq!(
675 parse_layout_box_sizing(" border-box ").unwrap(),
676 LayoutBoxSizing::BorderBox
677 );
678 }
679
680 #[test]
681 fn test_parse_layout_box_sizing_invalid() {
682 assert!(parse_layout_box_sizing("padding-box").is_err());
683 assert!(parse_layout_box_sizing("borderbox").is_err());
684 assert!(parse_layout_box_sizing("").is_err());
685 }
686}
687
688#[cfg(all(test, feature = "parser"))]
689mod autotest_generated {
690 #[allow(clippy::wildcard_imports)]
691 use super::*;
692 use alloc::{
693 format,
694 string::{String, ToString},
695 vec,
696 vec::Vec,
697 };
698
699 const fn tag(item: &CalcAstItem) -> u8 {
702 match item {
703 CalcAstItem::Value(_) => 0,
704 CalcAstItem::Add => 1,
705 CalcAstItem::Sub => 2,
706 CalcAstItem::Mul => 3,
707 CalcAstItem::Div => 4,
708 CalcAstItem::BraceOpen => 5,
709 CalcAstItem::BraceClose => 6,
710 }
711 }
712
713 fn shape(items: &CalcAstItemVec) -> Vec<u8> {
714 items.iter().map(tag).collect()
715 }
716
717 fn calc_items(w: &LayoutWidth) -> Vec<CalcAstItem> {
718 match w {
719 LayoutWidth::Calc(items) => items.as_slice().to_vec(),
720 other => panic!("expected LayoutWidth::Calc, got {other:?}"),
721 }
722 }
723
724 fn shape_of_width(w: &LayoutWidth) -> Vec<u8> {
725 calc_items(w).iter().map(tag).collect()
726 }
727
728 #[test]
733 fn calc_empty_and_whitespace_only_input_is_err() {
734 assert!(parse_calc_expression("").is_err());
735 assert!(parse_calc_expression(" ").is_err());
736 assert!(parse_calc_expression("\t\n\r ").is_err());
737 }
738
739 #[test]
740 fn calc_garbage_input_is_err_never_panics() {
741 for garbage in [
742 "???",
743 "@@@",
744 "px",
745 "em",
746 "%",
747 "#",
748 "1px;",
749 "abc",
750 "!!!",
751 "\0",
752 "\u{7f}",
753 ",",
754 ";",
755 "1,2",
756 "10 px 20 %%",
757 "--",
758 "-",
759 "-.",
760 "1..px",
761 "1.2.3px",
762 ] {
763 assert!(
764 parse_calc_expression(garbage).is_err(),
765 "expected Err for {garbage:?}"
766 );
767 }
768 }
769
770 #[test]
771 fn calc_valid_minimal_matches_documented_ast() {
772 let parsed = parse_calc_expression("100% - 20px").unwrap();
774 let expected = vec![
775 CalcAstItem::Value(PixelValue::percent(100.0)),
776 CalcAstItem::Sub,
777 CalcAstItem::Value(PixelValue::px(20.0)),
778 ];
779 assert_eq!(parsed.as_slice(), expected.as_slice());
780 }
781
782 #[test]
783 fn calc_documented_nested_example_parses_exactly() {
784 let parsed = parse_calc_expression("(100% - 20px) / 3").unwrap();
785 let expected = vec![
786 CalcAstItem::BraceOpen,
787 CalcAstItem::Value(PixelValue::percent(100.0)),
788 CalcAstItem::Sub,
789 CalcAstItem::Value(PixelValue::px(20.0)),
790 CalcAstItem::BraceClose,
791 CalcAstItem::Div,
792 CalcAstItem::Value(PixelValue::px(3.0)),
794 ];
795 assert_eq!(parsed.as_slice(), expected.as_slice());
796 }
797
798 #[test]
799 fn calc_minus_disambiguates_between_sub_and_negative_literal() {
800 assert_eq!(
802 parse_calc_expression("-10px").unwrap().as_slice(),
803 [CalcAstItem::Value(PixelValue::px(-10.0))].as_slice()
804 );
805 assert_eq!(
807 parse_calc_expression("100% * -2").unwrap().as_slice(),
808 [
809 CalcAstItem::Value(PixelValue::percent(100.0)),
810 CalcAstItem::Mul,
811 CalcAstItem::Value(PixelValue::px(-2.0)),
812 ]
813 .as_slice()
814 );
815 assert_eq!(
817 parse_calc_expression("(-5px)").unwrap().as_slice(),
818 [
819 CalcAstItem::BraceOpen,
820 CalcAstItem::Value(PixelValue::px(-5.0)),
821 CalcAstItem::BraceClose,
822 ]
823 .as_slice()
824 );
825 assert_eq!(
828 parse_calc_expression("5px -10px").unwrap().as_slice(),
829 [
830 CalcAstItem::Value(PixelValue::px(5.0)),
831 CalcAstItem::Sub,
832 CalcAstItem::Value(PixelValue::px(10.0)),
833 ]
834 .as_slice()
835 );
836 assert_eq!(
838 shape(&parse_calc_expression("(1px) - 2px").unwrap()),
839 vec![5, 0, 6, 2, 0]
840 );
841 }
842
843 #[test]
844 fn calc_leading_minus_followed_by_space_is_rejected() {
845 assert!(parse_calc_expression("- 10px").is_err());
847 assert!(parse_calc_expression("(- 10px)").is_err());
848 }
849
850 #[test]
851 fn calc_unicode_input_is_rejected_without_panic() {
852 for input in [
855 "\u{1F600}", "100px\u{1F600}", "10px\u{0301}", "10px\u{00A0}- 5px", "\u{FF11}\u{FF10}px", "100%", "π",
862 "10\u{2212}5", "\u{202E}10px", "e\u{0301}m",
865 ] {
866 assert!(
867 parse_calc_expression(input).is_err(),
868 "expected Err for {input:?}"
869 );
870 }
871 }
872
873 #[test]
874 fn calc_nan_literal_is_accepted_but_coerced_to_zero() {
875 let parsed = parse_calc_expression("NaN").unwrap();
880 match parsed.get(0).unwrap() {
881 CalcAstItem::Value(v) => {
882 assert!(
883 !v.number.get().is_nan(),
884 "NaN must not survive into the AST"
885 );
886 assert_eq!(v.number.get(), 0.0);
887 }
888 other => panic!("expected a Value, got {other:?}"),
889 }
890 }
891
892 #[test]
893 fn calc_huge_and_infinite_literals_saturate_to_a_finite_value() {
894 let huge = "9".repeat(50); for input in [
898 "inf",
899 "-inf",
900 huge.as_str(),
901 "9223372036854775807", "-9223372036854775808",
903 "340282350000000000000000000000000000000px", ] {
905 let parsed = parse_calc_expression(input)
906 .unwrap_or_else(|()| panic!("expected Ok for {input:?}"));
907 match parsed.get(0).unwrap() {
908 CalcAstItem::Value(v) => {
909 let n = v.number.get();
910 assert!(n.is_finite(), "{input:?} produced a non-finite value: {n}");
911 }
912 other => panic!("expected a Value for {input:?}, got {other:?}"),
913 }
914 }
915 }
916
917 #[test]
918 fn calc_zero_and_negative_zero() {
919 for input in ["0", "-0", "0px", "-0px", "0%"] {
920 let parsed = parse_calc_expression(input).unwrap();
921 match parsed.get(0).unwrap() {
922 CalcAstItem::Value(v) => assert_eq!(
923 v.number.get(),
924 0.0,
925 "{input:?} should quantise to exactly zero"
926 ),
927 other => panic!("expected a Value for {input:?}, got {other:?}"),
928 }
929 }
930 assert_eq!(
932 calc_ast_to_css_string(&parse_calc_expression("-0px").unwrap()),
933 "calc(0px)"
934 );
935 }
936
937 #[test]
938 fn calc_sub_millisecond_precision_is_quantised_to_zero() {
939 let parsed = parse_calc_expression("0.0005px").unwrap();
941 match parsed.get(0).unwrap() {
942 CalcAstItem::Value(v) => assert_eq!(v.number.get(), 0.0),
943 other => panic!("expected a Value, got {other:?}"),
944 }
945 let parsed = parse_calc_expression("0.001px").unwrap();
946 match parsed.get(0).unwrap() {
947 CalcAstItem::Value(v) => assert!((v.number.get() - 0.001).abs() < 1e-6),
948 other => panic!("expected a Value, got {other:?}"),
949 }
950 }
951
952 #[test]
953 fn calc_deeply_nested_braces_do_not_stack_overflow() {
954 const DEPTH: usize = 10_000;
956 let input = format!("{}1px{}", "(".repeat(DEPTH), ")".repeat(DEPTH));
957 let parsed = parse_calc_expression(&input).unwrap();
958 assert_eq!(parsed.len(), DEPTH * 2 + 1);
959 assert_eq!(*parsed.get(0).unwrap(), CalcAstItem::BraceOpen);
960 assert_eq!(
961 *parsed.get(parsed.len() - 1).unwrap(),
962 CalcAstItem::BraceClose
963 );
964 let printed = calc_ast_to_css_string(&parsed);
966 assert_eq!(printed.matches('(').count(), DEPTH + 1); assert_eq!(printed.matches(')').count(), DEPTH + 1);
968 }
969
970 #[test]
971 fn calc_unbalanced_braces_are_accepted_without_validation() {
972 assert_eq!(shape(&parse_calc_expression("(((").unwrap()), vec![5, 5, 5]);
977 assert_eq!(shape(&parse_calc_expression(")))").unwrap()), vec![6, 6, 6]);
978 assert_eq!(
979 shape(&parse_calc_expression(")1px(").unwrap()),
980 vec![6, 0, 5]
981 );
982
983 assert_eq!(
985 shape_of_width(&parse_layout_width("calc(()").unwrap()),
986 vec![5]
987 );
988 assert_eq!(
989 shape_of_width(&parse_layout_width("calc()))").unwrap()),
990 vec![6, 6]
991 );
992 }
993
994 #[test]
995 fn calc_dangling_operators_and_missing_operands_are_accepted() {
996 assert_eq!(shape(&parse_calc_expression("+").unwrap()), vec![1]);
998 assert_eq!(shape(&parse_calc_expression("*/").unwrap()), vec![3, 4]);
999 assert_eq!(
1000 shape(&parse_calc_expression("1px 2px").unwrap()),
1001 vec![0, 0]
1002 );
1003 assert_eq!(
1004 shape(&parse_calc_expression("1px + + 2px").unwrap()),
1005 vec![0, 1, 1, 0]
1006 );
1007 }
1008
1009 #[test]
1010 fn calc_extremely_long_expression_terminates() {
1011 const TERMS: usize = 50_000;
1013 let mut input = String::from("1px");
1014 for _ in 0..TERMS {
1015 input.push_str(" + 1px");
1016 }
1017 let parsed = parse_calc_expression(&input).unwrap();
1018 assert_eq!(parsed.len(), TERMS * 2 + 1);
1019 }
1020
1021 #[test]
1022 fn calc_extremely_long_garbage_token_is_err() {
1023 let long_alpha = "a".repeat(100_000);
1024 assert!(parse_calc_expression(&long_alpha).is_err());
1025
1026 let long_digits = "1".repeat(100_000);
1029 let parsed = parse_calc_expression(&long_digits).unwrap();
1030 match parsed.get(0).unwrap() {
1031 CalcAstItem::Value(v) => assert!(v.number.get().is_finite()),
1032 other => panic!("expected a Value, got {other:?}"),
1033 }
1034 }
1035
1036 #[test]
1037 fn calc_leading_and_trailing_junk_is_handled_deterministically() {
1038 assert_eq!(
1040 parse_calc_expression(" 100% - 20px ").unwrap().as_slice(),
1041 parse_calc_expression("100% - 20px").unwrap().as_slice()
1042 );
1043 assert!(parse_calc_expression("100% - 20px;").is_err());
1045 assert!(parse_calc_expression("100% - 20px garbage").is_err());
1046 assert!(parse_calc_expression(";100% - 20px").is_err());
1047 }
1048
1049 #[test]
1050 fn calc_scientific_notation_is_rejected() {
1051 assert!(parse_calc_expression("1e3px").is_err());
1054 assert!(parse_calc_expression("1e40").is_err());
1055 assert!(parse_calc_expression("1E3px").is_err());
1056 }
1057
1058 #[test]
1059 fn calc_every_single_ascii_char_is_panic_free() {
1060 for b in 0u8..128 {
1061 let s = String::from(b as char);
1062 let _ = parse_calc_expression(&s);
1064 }
1065 }
1066
1067 #[test]
1068 fn calc_fuzz_triples_never_panic_and_reprint_keeps_the_shape() {
1069 const ALPHABET: [&str; 16] = [
1072 "(",
1073 ")",
1074 "+",
1075 "-",
1076 "*",
1077 "/",
1078 ".",
1079 "0",
1080 "9",
1081 "p",
1082 "x",
1083 "%",
1084 " ",
1085 "e",
1086 "é",
1087 "\u{1F600}",
1088 ];
1089
1090 for a in ALPHABET {
1091 for b in ALPHABET {
1092 for c in ALPHABET {
1093 let input = format!("{a}{b}{c}");
1094 let Ok(ast) = parse_calc_expression(&input) else {
1095 continue;
1096 };
1097 assert!(!ast.is_empty(), "Ok(..) must never be an empty AST");
1098
1099 let printed = calc_ast_to_css_string(&ast);
1102 assert!(printed.starts_with("calc(") && printed.ends_with(')'));
1103 let inner = &printed[5..printed.len() - 1];
1104 let reparsed = parse_calc_expression(inner).unwrap_or_else(|()| {
1105 panic!("re-printed AST {printed:?} (from {input:?}) failed to re-parse")
1106 });
1107 assert_eq!(
1108 shape(&ast),
1109 shape(&reparsed),
1110 "round-trip changed the AST shape: {input:?} -> {printed:?}"
1111 );
1112 }
1113 }
1114 }
1115 }
1116
1117 #[test]
1122 fn find_value_end_basic_offsets() {
1123 assert_eq!(find_value_end(""), 0);
1124 assert_eq!(find_value_end("10px"), 4);
1125 assert_eq!(find_value_end("100%"), 4);
1126 assert_eq!(find_value_end("-1.5em"), 6);
1127 assert_eq!(find_value_end("+2px"), 4);
1128 assert_eq!(find_value_end("3"), 1);
1129 assert_eq!(find_value_end("10px)"), 4);
1131 assert_eq!(find_value_end("10px + 2px"), 4);
1132 assert_eq!(find_value_end("(1px)"), 0);
1133 assert_eq!(find_value_end(")"), 0);
1134 assert_eq!(find_value_end("-"), 1);
1136 assert_eq!(find_value_end("- 10px"), 1);
1137 }
1138
1139 #[test]
1140 fn find_value_end_is_lax_and_hands_junk_to_the_pixel_parser() {
1141 assert_eq!(find_value_end("..."), 3);
1144 assert_eq!(find_value_end("1.2.3px"), 7);
1145 assert_eq!(find_value_end("1px%em"), 6);
1146 assert_eq!(find_value_end("--"), 1);
1147 for junk in ["...", "1.2.3px", "1px%em"] {
1149 assert!(parse_calc_expression(junk).is_err(), "{junk:?}");
1150 }
1151 }
1152
1153 #[test]
1154 fn find_value_end_stops_at_an_exponent_marker() {
1155 assert_eq!(find_value_end("1e40"), 2);
1157 assert_eq!(find_value_end("1e40px"), 2);
1158 }
1159
1160 #[test]
1161 fn find_value_end_result_is_always_an_in_bounds_char_boundary() {
1162 for s in [
1165 "",
1166 " ",
1167 "10px",
1168 "\u{1F600}",
1169 "10px\u{1F600}",
1170 "1\u{0301}px",
1171 "é",
1172 "9é",
1173 "%é",
1174 "9%é",
1175 "10px",
1176 "\u{00A0}10px",
1177 "10\u{2212}5",
1178 "px\u{4e2d}\u{6587}",
1179 ] {
1180 let end = find_value_end(s);
1181 assert!(end <= s.len(), "{s:?}: end {end} out of bounds");
1182 assert!(
1183 s.is_char_boundary(end),
1184 "{s:?}: end {end} is not a char boundary"
1185 );
1186 let _ = &s[..end];
1188 }
1189 }
1190
1191 #[test]
1192 fn find_value_end_long_input_terminates() {
1193 let long = "9".repeat(200_000);
1194 assert_eq!(find_value_end(&long), 200_000);
1195 let long_unit = format!("{}{}", "9".repeat(100_000), "x".repeat(100_000));
1196 assert_eq!(find_value_end(&long_unit), 200_000);
1197 }
1198
1199 #[test]
1204 fn calc_ast_to_css_string_of_empty_vec_is_empty_calc() {
1205 assert_eq!(calc_ast_to_css_string(&CalcAstItemVec::new()), "calc()");
1206 assert!(parse_layout_width("calc()").is_err());
1209 }
1210
1211 #[test]
1212 fn calc_ast_to_css_string_prints_every_variant() {
1213 let items = CalcAstItemVec::from_vec(vec![
1214 CalcAstItem::Value(PixelValue::px(1.0)),
1215 CalcAstItem::Add,
1216 CalcAstItem::Sub,
1217 CalcAstItem::Mul,
1218 CalcAstItem::Div,
1219 CalcAstItem::BraceOpen,
1220 CalcAstItem::BraceClose,
1221 ]);
1222 assert_eq!(calc_ast_to_css_string(&items), "calc(1px + - * / ( ))");
1223 }
1224
1225 #[test]
1226 fn calc_ast_to_css_string_never_prints_nan_or_inf() {
1227 for v in [
1230 f32::NAN,
1231 f32::INFINITY,
1232 f32::NEG_INFINITY,
1233 f32::MAX,
1234 f32::MIN,
1235 f32::MIN_POSITIVE,
1236 ] {
1237 let items = CalcAstItemVec::from_vec(vec![CalcAstItem::Value(PixelValue::px(v))]);
1238 let printed = calc_ast_to_css_string(&items);
1239 assert!(!printed.contains("NaN"), "{v} printed as {printed:?}");
1240 assert!(!printed.contains("inf"), "{v} printed as {printed:?}");
1241 assert!(printed.starts_with("calc(") && printed.ends_with("px)"));
1242 let inner = &printed[5..printed.len() - 1];
1244 assert!(
1245 parse_calc_expression(inner).is_ok(),
1246 "{printed:?} did not re-parse"
1247 );
1248 }
1249 let items = CalcAstItemVec::from_vec(vec![CalcAstItem::Value(PixelValue::px(f32::NAN))]);
1251 assert_eq!(calc_ast_to_css_string(&items), "calc(0px)");
1252 }
1253
1254 #[test]
1255 fn calc_ast_print_parse_roundtrip_is_exact_for_representable_values() {
1256 for src in [
1257 "100% - 20px",
1258 "(100% - 20px) / 3",
1259 "-10px + 5px",
1260 "10px - -5px",
1261 "1.5em * 2",
1262 "100vw - 2rem",
1263 "50% + 1.25in",
1264 "((1px + 2px) * (3px - 4px))",
1265 ] {
1266 let ast = parse_calc_expression(src).unwrap();
1267 let printed = calc_ast_to_css_string(&ast);
1268 let inner = &printed[5..printed.len() - 1];
1269 let reparsed = parse_calc_expression(inner).unwrap();
1270 assert_eq!(
1271 ast.as_slice(),
1272 reparsed.as_slice(),
1273 "round-trip mismatch for {src:?} (printed as {printed:?})"
1274 );
1275 }
1276 }
1277
1278 #[test]
1279 fn calc_ast_to_css_string_is_lossy_for_adjacent_values() {
1280 let items = CalcAstItemVec::from_vec(vec![
1285 CalcAstItem::Value(PixelValue::px(1.0)),
1286 CalcAstItem::Value(PixelValue::px(-1.0)),
1287 ]);
1288 let printed = calc_ast_to_css_string(&items);
1289 assert_eq!(printed, "calc(1px -1px)");
1290
1291 let reparsed = parse_calc_expression(&printed[5..printed.len() - 1]).unwrap();
1292 assert_eq!(shape(&items), vec![0, 0]);
1293 assert_eq!(shape(&reparsed), vec![0, 2, 0]); assert_ne!(items.as_slice(), reparsed.as_slice());
1295 }
1296
1297 #[test]
1298 fn calc_ast_to_css_string_handles_a_huge_ast() {
1299 let items = CalcAstItemVec::from_vec(vec![CalcAstItem::BraceOpen; 100_000]);
1300 let printed = calc_ast_to_css_string(&items);
1301 assert_eq!(printed.len(), 100_000 * 2 - 1 + 6);
1303 }
1304
1305 #[test]
1310 fn box_sizing_valid_inputs_and_trimming() {
1311 assert_eq!(
1312 parse_layout_box_sizing("content-box").unwrap(),
1313 LayoutBoxSizing::ContentBox
1314 );
1315 assert_eq!(
1316 parse_layout_box_sizing("border-box").unwrap(),
1317 LayoutBoxSizing::BorderBox
1318 );
1319 assert_eq!(
1320 parse_layout_box_sizing("\t\n border-box \r\n ").unwrap(),
1321 LayoutBoxSizing::BorderBox
1322 );
1323 for v in [LayoutBoxSizing::ContentBox, LayoutBoxSizing::BorderBox] {
1325 assert_eq!(parse_layout_box_sizing(&v.print_as_css_value()).unwrap(), v);
1326 }
1327 }
1328
1329 #[test]
1330 fn box_sizing_empty_whitespace_and_garbage_are_err() {
1331 for input in [
1332 "",
1333 " ",
1334 "\t\n",
1335 "padding-box",
1336 "borderbox",
1337 "border box",
1338 "content-box;",
1339 "content-box border-box",
1340 "content-box!",
1341 "\0",
1342 "-",
1343 "\u{1F600}",
1344 "content-box\u{0301}",
1345 "cöntent-box",
1346 "content\u{2010}box", ] {
1348 assert!(
1349 parse_layout_box_sizing(input).is_err(),
1350 "expected Err for {input:?}"
1351 );
1352 }
1353 }
1354
1355 #[test]
1356 fn box_sizing_rejects_numeric_boundary_strings() {
1357 for input in [
1358 "0",
1359 "-0",
1360 "NaN",
1361 "inf",
1362 "9223372036854775807",
1363 "-9223372036854775808",
1364 "3.4028235e38",
1365 "1e-45",
1366 ] {
1367 assert!(
1368 parse_layout_box_sizing(input).is_err(),
1369 "expected Err for {input:?}"
1370 );
1371 }
1372 }
1373
1374 #[test]
1375 fn box_sizing_keyword_matching_is_case_sensitive() {
1376 assert!(parse_layout_box_sizing("Content-Box").is_err());
1379 assert!(parse_layout_box_sizing("BORDER-BOX").is_err());
1380 }
1381
1382 #[test]
1383 fn box_sizing_extremely_long_input_is_err_and_terminates() {
1384 let long = "a".repeat(1_000_000);
1385 assert!(parse_layout_box_sizing(&long).is_err());
1386
1387 let padded = format!("{}border-box{}", " ".repeat(100_000), " ".repeat(100_000));
1389 assert_eq!(
1390 parse_layout_box_sizing(&padded).unwrap(),
1391 LayoutBoxSizing::BorderBox
1392 );
1393 }
1394
1395 #[test]
1396 fn box_sizing_error_payload_is_the_trimmed_input() {
1397 let err = parse_layout_box_sizing(" bogus ").unwrap_err();
1398 match &err {
1399 LayoutBoxSizingParseError::InvalidValue(s) => assert_eq!(*s, "bogus"),
1400 }
1401 assert_eq!(format!("{err}"), "Invalid box-sizing value: \"bogus\"");
1402 }
1403
1404 #[test]
1405 fn box_sizing_error_to_contained_to_shared_roundtrip() {
1406 let err = parse_layout_box_sizing("padding-box").unwrap_err();
1407 let owned = err.to_contained();
1408 assert_eq!(
1409 owned,
1410 LayoutBoxSizingParseErrorOwned::InvalidValue("padding-box".to_string().into())
1411 );
1412 assert_eq!(owned.to_shared(), err);
1414 assert_eq!(owned.to_shared().to_contained(), owned);
1416 }
1417
1418 #[test]
1419 fn box_sizing_error_roundtrip_with_empty_and_unicode_payloads() {
1420 for input in ["", " ", "\u{1F600}\u{0301}é", "a\0b"] {
1421 let err = parse_layout_box_sizing(input).unwrap_err();
1422 let owned = err.to_contained();
1423 assert_eq!(owned.to_shared(), err, "round-trip failed for {input:?}");
1424
1425 match &owned {
1426 LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
1427 assert_eq!(s.as_str(), input.trim());
1428 }
1429 }
1430 }
1431 }
1432
1433 #[test]
1434 fn box_sizing_error_roundtrip_with_a_huge_payload() {
1435 let long = "x".repeat(200_000);
1436 let err = parse_layout_box_sizing(&long).unwrap_err();
1437 let owned = err.to_contained();
1438 match &owned {
1439 LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
1440 assert_eq!(s.as_str().len(), 200_000);
1441 }
1442 }
1443 assert_eq!(owned.to_shared(), err);
1444 }
1445
1446 #[test]
1451 fn sizing_parser_paren_slicing_is_panic_free() {
1452 for input in [
1455 "calc()",
1456 "fit-content()",
1457 "fit-content(\u{1F600})",
1458 "calc(\u{1F600})",
1459 "calc(é)",
1460 "fit-content(é)",
1461 "calc( )",
1462 "fit-content( )",
1463 "calc(1px)garbage)",
1464 "fit-content(1px)garbage)",
1465 "fit-content(1px",
1466 "calc(1px",
1467 ] {
1468 assert!(
1470 parse_layout_width(input).is_err(),
1471 "expected Err for {input:?}"
1472 );
1473 assert!(
1474 parse_layout_height(input).is_err(),
1475 "expected Err for {input:?}"
1476 );
1477 }
1478 }
1479
1480 #[test]
1481 fn sizing_parser_keywords_and_calc_roundtrip() {
1482 let cases = [
1483 (LayoutWidth::Auto, "auto"),
1484 (LayoutWidth::MinContent, "min-content"),
1485 (LayoutWidth::MaxContent, "max-content"),
1486 (LayoutWidth::Px(PixelValue::px(150.0)), "150px"),
1487 (
1488 LayoutWidth::FitContent(PixelValue::percent(50.0)),
1489 "fit-content(50%)",
1490 ),
1491 ];
1492 for (value, css) in cases {
1493 assert_eq!(parse_layout_width(css).unwrap(), value, "parse of {css:?}");
1494 assert_eq!(value.print_as_css_value(), css, "print of {css:?}");
1495 }
1496
1497 let parsed = parse_layout_width("calc(100% - 20px)").unwrap();
1499 assert_eq!(parsed.print_as_css_value(), "calc(100% - 20px)");
1500 assert_eq!(
1501 parse_layout_width(&parsed.print_as_css_value()).unwrap(),
1502 parsed
1503 );
1504 assert_eq!(
1505 calc_items(&parsed),
1506 vec![
1507 CalcAstItem::Value(PixelValue::percent(100.0)),
1508 CalcAstItem::Sub,
1509 CalcAstItem::Value(PixelValue::px(20.0)),
1510 ]
1511 );
1512 }
1513
1514 #[test]
1515 fn sizing_parser_keywords_are_case_sensitive() {
1516 for input in [
1518 "AUTO",
1519 "Auto",
1520 "MIN-CONTENT",
1521 "CALC(1px)",
1522 "FIT-CONTENT(1px)",
1523 ] {
1524 assert!(
1525 parse_layout_width(input).is_err(),
1526 "expected Err for {input:?}"
1527 );
1528 }
1529 }
1530
1531 #[test]
1532 fn fit_content_clamps_negative_values_to_zero() {
1533 assert_eq!(
1535 parse_layout_width("fit-content(-10px)").unwrap(),
1536 LayoutWidth::FitContent(PixelValue::zero())
1537 );
1538 assert_eq!(
1539 parse_layout_height("fit-content(-99999%)").unwrap(),
1540 LayoutHeight::FitContent(PixelValue::zero())
1541 );
1542 assert_eq!(
1544 parse_layout_width("fit-content(NaN)").unwrap(),
1545 LayoutWidth::FitContent(PixelValue::zero())
1546 );
1547 match parse_layout_width("fit-content(99999999999999999999999999999999999999999px)")
1549 .unwrap()
1550 {
1551 LayoutWidth::FitContent(v) => assert!(v.number.get().is_finite()),
1552 other => panic!("expected FitContent, got {other:?}"),
1553 }
1554 }
1555
1556 #[test]
1557 fn sizing_parser_deeply_nested_calc_does_not_stack_overflow() {
1558 const DEPTH: usize = 5_000;
1559 let input = format!("calc({}1px{})", "(".repeat(DEPTH), ")".repeat(DEPTH));
1560 let parsed = parse_layout_width(&input).unwrap();
1561 assert_eq!(calc_items(&parsed).len(), DEPTH * 2 + 1);
1562 let printed = parsed.print_as_css_value();
1564 assert_eq!(printed.matches('(').count(), DEPTH + 1);
1565 assert_eq!(printed.matches(')').count(), DEPTH + 1);
1566 }
1567
1568 #[test]
1569 fn sizing_parser_error_carries_the_untrimmed_input_for_bad_calc() {
1570 let err = parse_layout_width(" calc(??) ").unwrap_err();
1573 match &err {
1574 LayoutWidthParseError::InvalidKeyword(k) => assert_eq!(*k, " calc(??) "),
1575 other => panic!("expected InvalidKeyword, got {other:?}"),
1576 }
1577 let owned = err.to_contained();
1579 assert_eq!(owned.to_shared(), err);
1580 }
1581
1582 #[test]
1583 fn pixel_dimension_parser_errors_roundtrip() {
1584 for input in ["", " ", "px", "garbage", "\u{1F600}", "1.2.3px"] {
1585 let err = parse_layout_min_width(input)
1586 .err()
1587 .unwrap_or_else(|| panic!("expected Err for {input:?}"));
1588 let owned = err.to_contained();
1589 assert_eq!(owned.to_shared(), err, "for {input:?}");
1590
1591 assert!(parse_layout_max_height(input).is_err(), "for {input:?}");
1592 }
1593 assert_eq!(
1594 parse_layout_min_width("0").unwrap(),
1595 LayoutMinWidth {
1596 inner: PixelValue::px(0.0)
1597 }
1598 );
1599 }
1600
1601 #[test]
1606 fn max_dimension_defaults_are_finite_but_not_actually_f32_max() {
1607 for got in [
1613 LayoutMaxWidth::default().inner.number.get(),
1614 LayoutMaxHeight::default().inner.number.get(),
1615 ] {
1616 assert!(
1617 got.is_finite(),
1618 "default max dimension is not finite: {got}"
1619 );
1620 assert!(got > 0.0);
1621 assert_ne!(got, f32::MAX);
1622 }
1623 assert_eq!(LayoutMinWidth::default().inner.number.get(), 0.0);
1625 assert_eq!(LayoutMinHeight::default().inner.number.get(), 0.0);
1626 assert_eq!(LayoutWidth::default(), LayoutWidth::Auto);
1627 assert_eq!(LayoutHeight::default(), LayoutHeight::Auto);
1628 assert_eq!(LayoutBoxSizing::default(), LayoutBoxSizing::ContentBox);
1629 }
1630
1631 #[test]
1632 fn sizing_interpolate_endpoints_and_nan_t() {
1633 let a = LayoutWidth::px(10.0);
1634 let b = LayoutWidth::px(20.0);
1635 assert_eq!(a.interpolate(&b, 0.0), a);
1636 assert_eq!(a.interpolate(&b, 1.0), b);
1637 assert_eq!(a.interpolate(&b, 0.5), LayoutWidth::px(15.0));
1638
1639 match a.interpolate(&b, f32::NAN) {
1641 LayoutWidth::Px(v) => {
1642 assert!(!v.number.get().is_nan());
1643 assert_eq!(v.number.get(), 0.0);
1644 }
1645 other => panic!("expected Px, got {other:?}"),
1646 }
1647
1648 let auto = LayoutWidth::Auto;
1650 let min = LayoutWidth::MinContent;
1651 assert_eq!(auto.interpolate(&min, 0.0), LayoutWidth::Auto);
1652 assert_eq!(auto.interpolate(&min, 1.0), LayoutWidth::MinContent);
1653 assert_eq!(auto.interpolate(&min, f32::NAN), LayoutWidth::MinContent);
1654
1655 let calc = parse_layout_width("calc(100% - 20px)").unwrap();
1657 assert_eq!(calc.interpolate(&auto, 0.0), calc);
1658 assert_eq!(auto.interpolate(&calc, 1.0), calc);
1659 }
1660
1661 #[test]
1662 fn sizing_parser_px_quantisation_limits() {
1663 match parse_layout_width("0.0005px").unwrap() {
1665 LayoutWidth::Px(v) => assert_eq!(v.number.get(), 0.0),
1666 other => panic!("expected Px, got {other:?}"),
1667 }
1668 match parse_layout_width(&format!("{}px", "9".repeat(60))).unwrap() {
1670 LayoutWidth::Px(v) => assert!(v.number.get().is_finite()),
1671 other => panic!("expected Px, got {other:?}"),
1672 }
1673 match parse_layout_width("NaN").unwrap() {
1675 LayoutWidth::Px(v) => assert_eq!(v.number.get(), 0.0),
1676 other => panic!("expected Px, got {other:?}"),
1677 }
1678 }
1679}