use alloc::string::{String, ToString};
use crate::{
codegen::format::FormatAsRustCode,
props::{
basic::pixel::{CssPixelValueParseError, PixelValue},
formatter::PrintAsCssValue,
},
};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum LayoutTableLayout {
#[default]
Auto,
Fixed,
}
impl PrintAsCssValue for LayoutTableLayout {
fn print_as_css_value(&self) -> String {
match self {
Self::Auto => "auto".to_string(),
Self::Fixed => "fixed".to_string(),
}
}
}
impl FormatAsRustCode for LayoutTableLayout {
fn format_as_rust_code(&self, _tabs: usize) -> String {
match self {
Self::Auto => "LayoutTableLayout::Auto".to_string(),
Self::Fixed => "LayoutTableLayout::Fixed".to_string(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleBorderCollapse {
#[default]
Separate,
Collapse,
}
impl PrintAsCssValue for StyleBorderCollapse {
fn print_as_css_value(&self) -> String {
match self {
Self::Separate => "separate".to_string(),
Self::Collapse => "collapse".to_string(),
}
}
}
impl FormatAsRustCode for StyleBorderCollapse {
fn format_as_rust_code(&self, _tabs: usize) -> String {
match self {
Self::Separate => "StyleBorderCollapse::Separate".to_string(),
Self::Collapse => "StyleBorderCollapse::Collapse".to_string(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub struct LayoutBorderSpacing {
pub horizontal: PixelValue,
pub vertical: PixelValue,
}
impl Default for LayoutBorderSpacing {
fn default() -> Self {
Self {
horizontal: PixelValue::const_px(0),
vertical: PixelValue::const_px(0),
}
}
}
impl LayoutBorderSpacing {
#[must_use] pub const fn new(spacing: PixelValue) -> Self {
Self {
horizontal: spacing,
vertical: spacing,
}
}
#[must_use] pub const fn new_separate(horizontal: PixelValue, vertical: PixelValue) -> Self {
Self {
horizontal,
vertical,
}
}
}
impl PrintAsCssValue for LayoutBorderSpacing {
fn print_as_css_value(&self) -> String {
if self.horizontal == self.vertical {
self.horizontal.to_string()
} else {
format!("{} {}", self.horizontal, self.vertical)
}
}
}
impl FormatAsRustCode for LayoutBorderSpacing {
fn format_as_rust_code(&self, _tabs: usize) -> String {
use crate::codegen::format::format_pixel_value;
format!(
"LayoutBorderSpacing {{ horizontal: {}, vertical: {} }}",
format_pixel_value(&self.horizontal),
format_pixel_value(&self.vertical)
)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleCaptionSide {
#[default]
Top,
Bottom,
}
impl PrintAsCssValue for StyleCaptionSide {
fn print_as_css_value(&self) -> String {
match self {
Self::Top => "top".to_string(),
Self::Bottom => "bottom".to_string(),
}
}
}
impl FormatAsRustCode for StyleCaptionSide {
fn format_as_rust_code(&self, _tabs: usize) -> String {
match self {
Self::Top => "StyleCaptionSide::Top".to_string(),
Self::Bottom => "StyleCaptionSide::Bottom".to_string(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
#[derive(Default)]
pub enum StyleEmptyCells {
#[default]
Show,
Hide,
}
impl PrintAsCssValue for StyleEmptyCells {
fn print_as_css_value(&self) -> String {
match self {
Self::Show => "show".to_string(),
Self::Hide => "hide".to_string(),
}
}
}
impl FormatAsRustCode for StyleEmptyCells {
fn format_as_rust_code(&self, _tabs: usize) -> String {
match self {
Self::Show => "StyleEmptyCells::Show".to_string(),
Self::Hide => "StyleEmptyCells::Hide".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum LayoutTableLayoutParseError<'a> {
InvalidKeyword(&'a str),
}
pub(crate) fn parse_table_layout(
input: &str,
) -> Result<LayoutTableLayout, LayoutTableLayoutParseError<'_>> {
match input.trim() {
"auto" => Ok(LayoutTableLayout::Auto),
"fixed" => Ok(LayoutTableLayout::Fixed),
other => Err(LayoutTableLayoutParseError::InvalidKeyword(other)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum StyleBorderCollapseParseError<'a> {
InvalidKeyword(&'a str),
}
pub(crate) fn parse_border_collapse(
input: &str,
) -> Result<StyleBorderCollapse, StyleBorderCollapseParseError<'_>> {
match input.trim() {
"separate" => Ok(StyleBorderCollapse::Separate),
"collapse" => Ok(StyleBorderCollapse::Collapse),
other => Err(StyleBorderCollapseParseError::InvalidKeyword(other)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum LayoutBorderSpacingParseError<'a> {
PixelValue(CssPixelValueParseError<'a>),
InvalidFormat,
}
pub(crate) fn parse_border_spacing(
input: &str,
) -> Result<LayoutBorderSpacing, LayoutBorderSpacingParseError<'_>> {
use crate::props::basic::parse_pixel_value;
let parts: Vec<&str> = input.split_whitespace().collect();
match parts.len() {
1 => {
let value =
parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
Ok(LayoutBorderSpacing::new(value))
}
2 => {
let horizontal =
parse_pixel_value(parts[0]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
let vertical =
parse_pixel_value(parts[1]).map_err(LayoutBorderSpacingParseError::PixelValue)?;
Ok(LayoutBorderSpacing::new_separate(horizontal, vertical))
}
_ => Err(LayoutBorderSpacingParseError::InvalidFormat),
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum StyleCaptionSideParseError<'a> {
InvalidKeyword(&'a str),
}
pub(crate) fn parse_caption_side(
input: &str,
) -> Result<StyleCaptionSide, StyleCaptionSideParseError<'_>> {
match input.trim() {
"top" => Ok(StyleCaptionSide::Top),
"bottom" => Ok(StyleCaptionSide::Bottom),
other => Err(StyleCaptionSideParseError::InvalidKeyword(other)),
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum StyleEmptyCellsParseError<'a> {
InvalidKeyword(&'a str),
}
pub(crate) fn parse_empty_cells(
input: &str,
) -> Result<StyleEmptyCells, StyleEmptyCellsParseError<'_>> {
match input.trim() {
"show" => Ok(StyleEmptyCells::Show),
"hide" => Ok(StyleEmptyCells::Hide),
other => Err(StyleEmptyCellsParseError::InvalidKeyword(other)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_table_layout() {
assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
assert_eq!(
parse_table_layout("fixed").unwrap(),
LayoutTableLayout::Fixed
);
assert!(parse_table_layout("invalid").is_err());
}
#[test]
fn test_parse_border_collapse() {
assert_eq!(
parse_border_collapse("separate").unwrap(),
StyleBorderCollapse::Separate
);
assert_eq!(
parse_border_collapse("collapse").unwrap(),
StyleBorderCollapse::Collapse
);
assert!(parse_border_collapse("invalid").is_err());
}
#[test]
fn test_parse_border_spacing() {
let spacing1 = parse_border_spacing("5px").unwrap();
assert_eq!(spacing1.horizontal, PixelValue::const_px(5));
assert_eq!(spacing1.vertical, PixelValue::const_px(5));
let spacing2 = parse_border_spacing("5px 10px").unwrap();
assert_eq!(spacing2.horizontal, PixelValue::const_px(5));
assert_eq!(spacing2.vertical, PixelValue::const_px(10));
}
#[test]
fn test_parse_caption_side() {
assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
assert_eq!(
parse_caption_side("bottom").unwrap(),
StyleCaptionSide::Bottom
);
assert!(parse_caption_side("invalid").is_err());
}
#[test]
fn test_parse_empty_cells() {
assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
assert!(parse_empty_cells("invalid").is_err());
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod autotest_generated {
use crate::props::basic::SizeMetric;
use super::*;
fn raw(p: PixelValue) -> isize {
p.number.number()
}
fn table_layout_ok(s: &str) -> bool {
parse_table_layout(s).is_ok()
}
fn border_collapse_ok(s: &str) -> bool {
parse_border_collapse(s).is_ok()
}
fn caption_side_ok(s: &str) -> bool {
parse_caption_side(s).is_ok()
}
fn empty_cells_ok(s: &str) -> bool {
parse_empty_cells(s).is_ok()
}
type KeywordParser = (&'static str, fn(&str) -> bool);
const KEYWORD_PARSERS: &[KeywordParser] = &[
("table-layout", table_layout_ok),
("border-collapse", border_collapse_ok),
("caption-side", caption_side_ok),
("empty-cells", empty_cells_ok),
];
fn assert_all_keyword_parsers_reject(input: &str, why: &str) {
for (prop, parse) in KEYWORD_PARSERS {
assert!(
!parse(input),
"{prop}: expected {input:?} to be rejected ({why}), but it parsed"
);
}
}
#[test]
fn new_applies_the_same_spacing_to_both_axes() {
let s = LayoutBorderSpacing::new(PixelValue::const_px(7));
assert_eq!(s.horizontal, PixelValue::const_px(7));
assert_eq!(s.vertical, PixelValue::const_px(7));
assert_eq!(s.horizontal, s.vertical);
}
#[test]
fn new_separate_preserves_argument_order() {
let s = LayoutBorderSpacing::new_separate(
PixelValue::const_px(3),
PixelValue::percent(50.0),
);
assert_eq!(s.horizontal, PixelValue::const_px(3));
assert_eq!(s.vertical, PixelValue::percent(50.0));
assert_ne!(s.horizontal, s.vertical);
}
#[test]
fn constructors_do_not_panic_on_extreme_floats() {
let extremes = [
0.0f32,
-0.0,
1.0,
-1.0,
f32::EPSILON,
f32::MIN_POSITIVE,
-f32::MIN_POSITIVE,
f32::MAX,
f32::MIN,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NAN,
];
for v in extremes {
let same = LayoutBorderSpacing::new(PixelValue::px(v));
assert_eq!(same.horizontal, same.vertical, "new({v}) must be symmetric");
for w in extremes {
let sep =
LayoutBorderSpacing::new_separate(PixelValue::px(v), PixelValue::em(w));
assert_eq!(sep.horizontal.metric, SizeMetric::Px);
assert_eq!(sep.vertical.metric, SizeMetric::Em);
assert!(sep.horizontal.number.get().is_finite());
assert!(sep.vertical.number.get().is_finite());
}
}
}
#[test]
fn nan_spacing_is_flattened_to_zero_and_stays_comparable() {
let nan = LayoutBorderSpacing::new(PixelValue::px(f32::NAN));
assert_eq!(raw(nan.horizontal), 0);
assert_eq!(raw(nan.vertical), 0);
assert_eq!(nan, nan);
assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(f32::NAN)));
assert_eq!(nan, LayoutBorderSpacing::new(PixelValue::px(0.0)));
assert_eq!(nan, LayoutBorderSpacing::default());
}
#[test]
fn infinite_spacing_saturates_instead_of_wrapping() {
for input in [f32::INFINITY, f32::MAX, 1e38] {
let s = LayoutBorderSpacing::new(PixelValue::px(input));
assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
assert!(s.horizontal.number.get().is_finite());
}
for input in [f32::NEG_INFINITY, f32::MIN, -1e38] {
let s = LayoutBorderSpacing::new(PixelValue::px(input));
assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
assert!(s.horizontal.number.get().is_finite());
}
}
#[test]
fn equal_border_spacings_hash_equal() {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
fn hash(s: LayoutBorderSpacing) -> u64 {
let mut h = DefaultHasher::new();
s.hash(&mut h);
h.finish()
}
assert_eq!(
hash(LayoutBorderSpacing::new(PixelValue::px(f32::NAN))),
hash(LayoutBorderSpacing::default())
);
assert_eq!(
hash(LayoutBorderSpacing::new(PixelValue::const_px(4))),
hash(LayoutBorderSpacing::new_separate(
PixelValue::const_px(4),
PixelValue::const_px(4)
))
);
}
#[test]
fn defaults_are_the_css_initial_values() {
assert_eq!(LayoutTableLayout::default(), LayoutTableLayout::Auto);
assert_eq!(StyleBorderCollapse::default(), StyleBorderCollapse::Separate);
assert_eq!(StyleCaptionSide::default(), StyleCaptionSide::Top);
assert_eq!(StyleEmptyCells::default(), StyleEmptyCells::Show);
let d = LayoutBorderSpacing::default();
assert_eq!(d, LayoutBorderSpacing::new(PixelValue::const_px(0)));
assert_eq!(raw(d.horizontal), 0);
assert_eq!(raw(d.vertical), 0);
assert_eq!(d.horizontal.metric, SizeMetric::Px);
}
#[test]
fn keyword_parsers_accept_every_variant() {
assert_eq!(parse_table_layout("auto").unwrap(), LayoutTableLayout::Auto);
assert_eq!(parse_table_layout("fixed").unwrap(), LayoutTableLayout::Fixed);
assert_eq!(
parse_border_collapse("separate").unwrap(),
StyleBorderCollapse::Separate
);
assert_eq!(
parse_border_collapse("collapse").unwrap(),
StyleBorderCollapse::Collapse
);
assert_eq!(parse_caption_side("top").unwrap(), StyleCaptionSide::Top);
assert_eq!(parse_caption_side("bottom").unwrap(), StyleCaptionSide::Bottom);
assert_eq!(parse_empty_cells("show").unwrap(), StyleEmptyCells::Show);
assert_eq!(parse_empty_cells("hide").unwrap(), StyleEmptyCells::Hide);
}
#[test]
fn keyword_parsers_reject_empty_input() {
assert_all_keyword_parsers_reject("", "empty input");
assert_eq!(
parse_table_layout(""),
Err(LayoutTableLayoutParseError::InvalidKeyword(""))
);
assert_eq!(
parse_border_collapse(""),
Err(StyleBorderCollapseParseError::InvalidKeyword(""))
);
assert_eq!(
parse_caption_side(""),
Err(StyleCaptionSideParseError::InvalidKeyword(""))
);
assert_eq!(
parse_empty_cells(""),
Err(StyleEmptyCellsParseError::InvalidKeyword(""))
);
}
#[test]
fn keyword_parsers_reject_whitespace_only_input() {
for input in [" ", "\t", "\n", "\r\n", "\t\n \x0c", "\u{a0}", "\u{2028}"] {
assert_all_keyword_parsers_reject(input, "whitespace only");
}
assert_eq!(
parse_table_layout(" \t\n "),
Err(LayoutTableLayoutParseError::InvalidKeyword(""))
);
}
#[test]
fn keyword_parsers_trim_surrounding_whitespace() {
assert_eq!(
parse_table_layout(" auto\t\n").unwrap(),
LayoutTableLayout::Auto
);
assert_eq!(
parse_border_collapse("\r\n collapse ").unwrap(),
StyleBorderCollapse::Collapse
);
assert_eq!(
parse_caption_side("\t bottom \t").unwrap(),
StyleCaptionSide::Bottom
);
assert_eq!(parse_empty_cells("\n hide \n").unwrap(), StyleEmptyCells::Hide);
}
#[test]
fn keyword_parsers_also_trim_non_css_unicode_whitespace() {
assert_eq!(
parse_table_layout("\u{a0}auto\u{a0}").unwrap(),
LayoutTableLayout::Auto
);
assert_eq!(
parse_empty_cells("\u{2028}show\u{2029}").unwrap(),
StyleEmptyCells::Show
);
}
#[test]
fn keyword_parse_errors_carry_the_trimmed_input() {
assert_eq!(
parse_table_layout(" bogus "),
Err(LayoutTableLayoutParseError::InvalidKeyword("bogus"))
);
assert_eq!(
parse_border_collapse(" separate collapse "),
Err(StyleBorderCollapseParseError::InvalidKeyword(
"separate collapse"
))
);
assert_eq!(
parse_caption_side("\ttop;\t"),
Err(StyleCaptionSideParseError::InvalidKeyword("top;"))
);
assert_eq!(
parse_empty_cells(" show hide "),
Err(StyleEmptyCellsParseError::InvalidKeyword("show hide"))
);
}
#[test]
fn keyword_parsers_are_case_sensitive() {
for input in [
"AUTO", "Auto", "aUtO", "FIXED", "SEPARATE", "Collapse", "TOP", "Bottom",
"SHOW", "Hide",
] {
assert_all_keyword_parsers_reject(input, "keyword matching is case-sensitive");
}
}
#[test]
fn keyword_parsers_reject_garbage() {
for input in [
"invalid",
"auto fixed",
"auto;",
"auto;garbage",
"auto/*c*/",
"/*auto*/",
"au to",
"a\0uto",
"\0",
"\u{7f}",
"-->",
"<!--",
"!important",
"\\61 uto",
"'auto'",
"\"auto\"",
"auto()",
"url(auto)",
] {
assert_all_keyword_parsers_reject(input, "garbage");
}
}
#[test]
fn keyword_parsers_reject_boundary_numeric_strings() {
for input in [
"0",
"-0",
"0.0",
"1",
"-1",
"9223372036854775807", "-9223372036854775808", "18446744073709551616", "1e400",
"-1e400",
"3.4028235e38",
"1.17549435e-38",
"NaN",
"nan",
"inf",
"-inf",
"infinity",
] {
assert_all_keyword_parsers_reject(input, "numeric input for a keyword property");
}
}
#[test]
fn keyword_parsers_reject_leading_and_trailing_junk() {
assert_all_keyword_parsers_reject("xauto", "leading junk");
assert_all_keyword_parsers_reject("autox", "trailing junk");
assert_all_keyword_parsers_reject("auto!", "trailing junk");
assert_all_keyword_parsers_reject("(fixed)", "wrapped in parens");
assert_all_keyword_parsers_reject("collapse,", "trailing comma");
assert_eq!(parse_table_layout("auto ").unwrap(), LayoutTableLayout::Auto);
}
#[test]
fn keyword_parsers_survive_unicode_input() {
for input in [
"\u{1F600}", "auto\u{0301}", "\u{FF41}\u{FF55}\u{FF54}\u{FF4F}", "\u{202E}auto", "\u{FEFF}auto", "аuto", "🇩🇪", "e\u{0301}\u{0301}\u{0301}",
"\u{10FFFF}", ] {
assert_all_keyword_parsers_reject(input, "non-ASCII input");
}
assert_eq!(
parse_table_layout(" \u{1F600} "),
Err(LayoutTableLayoutParseError::InvalidKeyword("\u{1F600}"))
);
}
#[test]
fn keyword_parsers_survive_extremely_long_input() {
let long = "a".repeat(1_000_000);
assert_all_keyword_parsers_reject(&long, "1M-char input");
assert_eq!(
parse_table_layout(&long),
Err(LayoutTableLayoutParseError::InvalidKeyword(long.as_str()))
);
let repeated = "auto".repeat(250_000);
assert_all_keyword_parsers_reject(&repeated, "repeated valid token");
let padded = format!("{}auto{}", " ".repeat(500_000), "\n".repeat(500_000));
assert_eq!(parse_table_layout(&padded).unwrap(), LayoutTableLayout::Auto);
}
#[test]
fn keyword_parsers_survive_deeply_nested_input() {
let nested = format!("{}auto{}", "(".repeat(10_000), ")".repeat(10_000));
assert_all_keyword_parsers_reject(&nested, "10k nested brackets");
let braces = "{".repeat(50_000);
assert_all_keyword_parsers_reject(&braces, "50k open braces");
}
#[test]
fn border_spacing_single_value_applies_to_both_axes() {
let s = parse_border_spacing("5px").unwrap();
assert_eq!(s.horizontal, PixelValue::const_px(5));
assert_eq!(s.vertical, PixelValue::const_px(5));
assert_eq!(s, LayoutBorderSpacing::new(PixelValue::const_px(5)));
}
#[test]
fn border_spacing_two_values_are_horizontal_then_vertical() {
let s = parse_border_spacing("5px 10px").unwrap();
assert_eq!(s.horizontal, PixelValue::const_px(5));
assert_eq!(s.vertical, PixelValue::const_px(10));
assert_ne!(s, parse_border_spacing("10px 5px").unwrap());
}
#[test]
fn border_spacing_accepts_mixed_metrics_per_axis() {
let s = parse_border_spacing("1em 50%").unwrap();
assert_eq!(s.horizontal.metric, SizeMetric::Em);
assert_eq!(raw(s.horizontal), 1000);
assert_eq!(s.vertical.metric, SizeMetric::Percent);
assert_eq!(raw(s.vertical), 50_000);
assert_eq!(
parse_border_spacing("2rem").unwrap().horizontal.metric,
SizeMetric::Rem
);
assert_eq!(
parse_border_spacing("2vmax 3vmin").unwrap().horizontal.metric,
SizeMetric::Vmax
);
}
#[test]
fn border_spacing_rejects_empty_and_whitespace_only_input() {
for input in ["", " ", "\t\n", "\r\n\x0c ", "\u{a0}", "\u{2028}\u{2029}"] {
assert_eq!(
parse_border_spacing(input),
Err(LayoutBorderSpacingParseError::InvalidFormat),
"expected InvalidFormat for {input:?}"
);
}
}
#[test]
fn border_spacing_rejects_more_than_two_components() {
for input in ["5px 10px 15px", "1px 2px 3px 4px", "0 0 0"] {
assert_eq!(
parse_border_spacing(input),
Err(LayoutBorderSpacingParseError::InvalidFormat),
"expected InvalidFormat for {input:?}"
);
}
}
#[test]
fn border_spacing_collapses_arbitrary_internal_whitespace() {
let expected = LayoutBorderSpacing::new_separate(
PixelValue::const_px(5),
PixelValue::const_px(10),
);
for input in [
"5px 10px",
" 5px 10px ",
"\t5px\n10px\r\n",
"5px\x0c10px",
] {
assert_eq!(parse_border_spacing(input).unwrap(), expected, "{input:?}");
}
}
#[test]
fn border_spacing_splits_on_non_css_unicode_whitespace() {
let s = parse_border_spacing("5px\u{a0}10px").unwrap();
assert_eq!(s.horizontal, PixelValue::const_px(5));
assert_eq!(s.vertical, PixelValue::const_px(10));
}
#[test]
fn border_spacing_propagates_pixel_value_errors() {
assert!(matches!(
parse_border_spacing("px"),
Err(LayoutBorderSpacingParseError::PixelValue(
CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
))
));
for input in ["abc", "5foo", "5px;", "#5px", "5 .. px"] {
assert!(
parse_border_spacing(input).is_err(),
"expected {input:?} to be rejected"
);
}
assert!(matches!(
parse_border_spacing("5 px"),
Err(LayoutBorderSpacingParseError::PixelValue(
CssPixelValueParseError::NoValueGiven("px", SizeMetric::Px)
))
));
assert!(matches!(
parse_border_spacing("5px zzz"),
Err(LayoutBorderSpacingParseError::PixelValue(
CssPixelValueParseError::InvalidPixelValue("zzz")
))
));
}
#[test]
fn border_spacing_accepts_unitless_numbers() {
assert_eq!(
parse_border_spacing("0").unwrap(),
LayoutBorderSpacing::new(PixelValue::const_px(0))
);
assert_eq!(
parse_border_spacing("5").unwrap(),
LayoutBorderSpacing::new(PixelValue::const_px(5))
);
assert_eq!(raw(parse_border_spacing("-0").unwrap().horizontal), 0);
assert_eq!(
parse_border_spacing("-0").unwrap(),
parse_border_spacing("0").unwrap()
);
}
#[test]
fn border_spacing_accepts_negative_values() {
let s = parse_border_spacing("-5px -10px").unwrap();
assert_eq!(raw(s.horizontal), -5000);
assert_eq!(raw(s.vertical), -10_000);
}
#[test]
fn border_spacing_flattens_nan_to_zero() {
let s = parse_border_spacing("NaNpx").unwrap();
assert_eq!(raw(s.horizontal), 0);
assert_eq!(raw(s.vertical), 0);
assert!(s.horizontal.number.get().is_finite());
assert_eq!(s, LayoutBorderSpacing::default());
let mixed = parse_border_spacing("NaNpx 4px").unwrap();
assert_eq!(raw(mixed.horizontal), 0);
assert_eq!(raw(mixed.vertical), 4000);
}
#[test]
fn border_spacing_saturates_infinite_and_overflowing_values() {
for input in ["infpx", "infinitypx", "1e40px", "3.5e38px"] {
let s = parse_border_spacing(input).unwrap();
assert_eq!(raw(s.horizontal), isize::MAX, "{input} must saturate high");
assert!(s.horizontal.number.get().is_finite(), "{input} must be finite");
}
for input in ["-infpx", "-1e40px", "-3.5e38px"] {
let s = parse_border_spacing(input).unwrap();
assert_eq!(raw(s.horizontal), isize::MIN, "{input} must saturate low");
assert!(s.horizontal.number.get().is_finite(), "{input} must be finite");
}
let s = parse_border_spacing("infpx -infpx").unwrap();
assert_eq!(raw(s.horizontal), isize::MAX);
assert_eq!(raw(s.vertical), isize::MIN);
assert_ne!(s.horizontal, s.vertical);
}
#[test]
fn border_spacing_truncates_below_milli_precision() {
assert_eq!(raw(parse_border_spacing("0.0005px").unwrap().horizontal), 0);
assert_eq!(raw(parse_border_spacing("0.9999px").unwrap().horizontal), 999);
assert_eq!(raw(parse_border_spacing("1.9999px").unwrap().horizontal), 1999);
let denormal = parse_border_spacing("1.17549435e-38px").unwrap();
assert_eq!(raw(denormal.horizontal), 0);
}
#[test]
fn border_spacing_survives_extremely_long_input() {
let long_token = "z".repeat(1_000_000);
assert!(parse_border_spacing(&long_token).is_err());
let many = "5px ".repeat(200_000);
assert_eq!(
parse_border_spacing(&many),
Err(LayoutBorderSpacingParseError::InvalidFormat)
);
let padded = format!("{}5px{}", " ".repeat(500_000), " ".repeat(500_000));
assert_eq!(
parse_border_spacing(&padded).unwrap(),
LayoutBorderSpacing::new(PixelValue::const_px(5))
);
}
#[test]
fn border_spacing_survives_deeply_nested_input() {
let nested = format!("{}5px{}", "(".repeat(10_000), ")".repeat(10_000));
assert!(
parse_border_spacing(&nested).is_err(),
"10k nested brackets must be rejected, not stack-overflow"
);
}
#[test]
fn border_spacing_unicode_input_does_not_panic() {
for input in [
"\u{1F600}",
"5\u{1F600}px",
"5px \u{1F600}",
"5px", "5\u{0301}px", "\u{FEFF}5px", ] {
assert!(
parse_border_spacing(input).is_err(),
"expected {input:?} to be rejected"
);
}
}
#[test]
fn keyword_enums_roundtrip_through_css() {
for v in [LayoutTableLayout::Auto, LayoutTableLayout::Fixed] {
assert_eq!(parse_table_layout(&v.print_as_css_value()).unwrap(), v);
}
for v in [StyleBorderCollapse::Separate, StyleBorderCollapse::Collapse] {
assert_eq!(parse_border_collapse(&v.print_as_css_value()).unwrap(), v);
}
for v in [StyleCaptionSide::Top, StyleCaptionSide::Bottom] {
assert_eq!(parse_caption_side(&v.print_as_css_value()).unwrap(), v);
}
for v in [StyleEmptyCells::Show, StyleEmptyCells::Hide] {
assert_eq!(parse_empty_cells(&v.print_as_css_value()).unwrap(), v);
}
assert_eq!(
parse_table_layout(&LayoutTableLayout::default().print_as_css_value()).unwrap(),
LayoutTableLayout::default()
);
}
#[test]
fn keyword_enum_css_spellings_are_distinct() {
assert_ne!(
LayoutTableLayout::Auto.print_as_css_value(),
LayoutTableLayout::Fixed.print_as_css_value()
);
assert_ne!(
StyleBorderCollapse::Separate.print_as_css_value(),
StyleBorderCollapse::Collapse.print_as_css_value()
);
assert_ne!(
StyleCaptionSide::Top.print_as_css_value(),
StyleCaptionSide::Bottom.print_as_css_value()
);
assert_ne!(
StyleEmptyCells::Show.print_as_css_value(),
StyleEmptyCells::Hide.print_as_css_value()
);
}
#[test]
fn border_spacing_roundtrips_through_css() {
let values = [
PixelValue::const_px(0),
PixelValue::const_px(5),
PixelValue::const_px(-5),
PixelValue::px(1.5),
PixelValue::px(-0.125),
PixelValue::em(2.0),
PixelValue::rem(0.5),
PixelValue::percent(50.0),
PixelValue::from_metric(SizeMetric::Pt, 12.0),
PixelValue::from_metric(SizeMetric::Vmin, 3.25),
];
for h in values {
let same = LayoutBorderSpacing::new(h);
assert_eq!(
parse_border_spacing(&same.print_as_css_value()).unwrap(),
same,
"single-value round-trip failed for {h:?}"
);
for v in values {
let sep = LayoutBorderSpacing::new_separate(h, v);
assert_eq!(
parse_border_spacing(&sep.print_as_css_value()).unwrap(),
sep,
"two-value round-trip failed for {h:?} / {v:?}"
);
}
}
}
#[test]
fn border_spacing_prints_one_value_only_when_both_axes_match() {
assert_eq!(
LayoutBorderSpacing::new(PixelValue::const_px(5)).print_as_css_value(),
"5px"
);
assert_eq!(
LayoutBorderSpacing::new_separate(
PixelValue::const_px(5),
PixelValue::const_px(10)
)
.print_as_css_value(),
"5px 10px"
);
assert_eq!(
LayoutBorderSpacing::new_separate(
PixelValue::const_px(0),
PixelValue::percent(0.0)
)
.print_as_css_value(),
"0px 0%"
);
assert_eq!(
LayoutBorderSpacing::default().print_as_css_value(),
"0px",
"the default must not print as a two-value form"
);
}
#[test]
fn border_spacing_saturated_value_roundtrips_stably() {
let saturated = parse_border_spacing("infpx").unwrap();
let printed = saturated.print_as_css_value();
assert_eq!(parse_border_spacing(&printed).unwrap(), saturated);
let low = parse_border_spacing("-infpx").unwrap();
assert_eq!(
parse_border_spacing(&low.print_as_css_value()).unwrap(),
low
);
}
#[test]
fn border_spacing_parse_print_is_idempotent_after_quantization() {
for input in ["0.0005px", "0.9999px", "0.1px", "1e40px", "NaNpx", "-0"] {
let once = parse_border_spacing(input).unwrap();
let printed = once.print_as_css_value();
let twice = parse_border_spacing(&printed).unwrap();
assert_eq!(once, twice, "not idempotent for {input:?}");
assert_eq!(printed, twice.print_as_css_value(), "not stable for {input:?}");
}
}
#[test]
fn format_as_rust_code_emits_the_variant_paths() {
assert_eq!(
LayoutTableLayout::Fixed.format_as_rust_code(0),
"LayoutTableLayout::Fixed"
);
assert_eq!(
StyleBorderCollapse::Collapse.format_as_rust_code(0),
"StyleBorderCollapse::Collapse"
);
assert_eq!(
StyleCaptionSide::Bottom.format_as_rust_code(0),
"StyleCaptionSide::Bottom"
);
assert_eq!(StyleEmptyCells::Hide.format_as_rust_code(0), "StyleEmptyCells::Hide");
assert_eq!(
LayoutTableLayout::Auto.format_as_rust_code(0),
LayoutTableLayout::Auto.format_as_rust_code(usize::MAX)
);
}
#[test]
fn format_as_rust_code_for_border_spacing_is_total() {
assert_eq!(
LayoutBorderSpacing::new(PixelValue::const_px(5)).format_as_rust_code(0),
"LayoutBorderSpacing { horizontal: \
PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0), vertical: \
PixelValue::const_from_metric_fractional(SizeMetric::Px, 5, 0) }"
);
for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX, f32::MIN, -2.5] {
let code = LayoutBorderSpacing::new(PixelValue::px(v)).format_as_rust_code(0);
assert!(code.starts_with("LayoutBorderSpacing {"), "{v}: {code}");
assert!(code.ends_with('}'), "{v}: {code}");
}
}
}