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