1
//! CSS properties related to dimensions and sizing.
2
//!
3
//! Key types: [`LayoutWidth`] / [`LayoutHeight`] (support `auto`, pixel values,
4
//! `min-content`, `max-content`, `fit-content()`, and `calc()` expressions),
5
//! [`LayoutMinWidth`], [`LayoutMinHeight`], [`LayoutMaxWidth`], [`LayoutMaxHeight`]
6
//! (simple pixel-value constraints), and [`LayoutBoxSizing`].
7
//!
8
//! `calc()` expressions use a flat stack-machine representation via [`CalcAstItem`]
9
//! — see its documentation for the encoding scheme. The layout solver in
10
//! `layout/src/solver3/calc.rs` evaluates these at resolve time.
11

            
12
use alloc::{
13
    string::{String, ToString},
14
    vec::Vec,
15
};
16

            
17
use 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
// -- Calc AST --
28

            
29
/// A single item in a `calc()` expression, stored as a flat stack-machine representation.
30
///
31
/// The expression `calc(33.333% - 10px)` is stored as:
32
/// ```text
33
/// [Value(33.333%), Sub, Value(10px)]
34
/// ```
35
///
36
/// For nested expressions like `calc(100% - (20px + 5%))`:
37
/// ```text
38
/// [Value(100%), Sub, BraceOpen, Value(20px), Add, Value(5%), BraceClose]
39
/// ```
40
///
41
/// **Resolution**: Walk left to right. When `BraceClose` is hit, resolve everything
42
/// back to the matching `BraceOpen`, replace that span with a single `Value`, and continue.
43
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44
#[repr(C, u8)]
45
pub enum CalcAstItem {
46
    /// A literal value (e.g. `10px`, `33.333%`, `2em`)
47
    Value(PixelValue),
48
    /// `+` operator
49
    Add,
50
    /// `-` operator
51
    Sub,
52
    /// `*` operator
53
    Mul,
54
    /// `/` operator
55
    Div,
56
    /// `(` — opens a sub-expression
57
    BraceOpen,
58
    /// `)` — closes a sub-expression; triggers resolution of the inner span
59
    BraceClose,
60
}
61

            
62
/// C-compatible `Vec<CalcAstItem>` for FFI interop.
63
impl_vec!(
64
    CalcAstItem,
65
    CalcAstItemVec,
66
    CalcAstItemVecDestructor,
67
    CalcAstItemVecDestructorType,
68
    CalcAstItemVecSlice,
69
    OptionCalcAstItem
70
);
71
impl_vec_clone!(CalcAstItem, CalcAstItemVec, CalcAstItemVecDestructor);
72
impl_vec_debug!(CalcAstItem, CalcAstItemVec);
73
impl_vec_partialeq!(CalcAstItem, CalcAstItemVec);
74
impl_vec_eq!(CalcAstItem, CalcAstItemVec);
75
impl_vec_partialord!(CalcAstItem, CalcAstItemVec);
76
impl_vec_ord!(CalcAstItem, CalcAstItemVec);
77
impl_vec_hash!(CalcAstItem, CalcAstItemVec);
78
impl_vec_mut!(CalcAstItem, CalcAstItemVec);
79

            
80
impl_option!(
81
    CalcAstItem,
82
    OptionCalcAstItem,
83
    copy = false,
84
    [Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
85
);
86

            
87
/// Parse a calc() inner expression (the part between the parentheses) into
88
/// a flat `CalcAstItemVec` suitable for stack-machine evaluation.
89
///
90
/// Examples:
91
/// - `"100% - 20px"` → `[Value(100%), Sub, Value(20px)]`
92
/// - `"(100% - 20px) / 3"` → `[BraceOpen, Value(100%), Sub, Value(20px), BraceClose, Div, Value(3)]`
93
///
94
/// **Tokenisation rules**:
95
///  - Whitespace is skipped between tokens.
96
///  - `+`, `-`, `*`, `/` are operators (but `-` at the start of a number is
97
///    part of the number literal, e.g. `-10px`).
98
///  - `(` / `)` produce `BraceOpen` / `BraceClose`.
99
///  - Anything else is parsed as a `PixelValue` via `parse_pixel_value`.
100
#[cfg(feature = "parser")]
101
26
fn parse_calc_expression(input: &str) -> Result<CalcAstItemVec, ()> {
102
    use crate::props::basic::pixel::parse_pixel_value;
103

            
104
26
    let mut items: Vec<CalcAstItem> = Vec::new();
105
26
    let input = input.trim();
106
26
    let bytes = input.as_bytes();
107
26
    let mut i = 0;
108

            
109
156
    while i < bytes.len() {
110
        // Skip whitespace
111
130
        if bytes[i].is_ascii_whitespace() {
112
52
            i += 1;
113
52
            continue;
114
78
        }
115

            
116
78
        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
                // Decide: is this a subtraction operator or a negative number?
124
                // It's a negative number if:
125
                //   - it's the first token, OR
126
                //   - the previous token is an operator or BraceOpen
127
26
                let is_negative_number = items.is_empty()
128
26
                    || matches!(
129
26
                        items.last(),
130
                        Some(CalcAstItem::Add)
131
                            | Some(CalcAstItem::Sub)
132
                            | Some(CalcAstItem::Mul)
133
                            | Some(CalcAstItem::Div)
134
                            | Some(CalcAstItem::BraceOpen)
135
                    );
136

            
137
26
                if is_negative_number {
138
                    // Parse as negative number value
139
                    let rest = &input[i..];
140
                    let end = find_value_end(rest);
141
                    if end == 0 { return Err(()); }
142
                    let val_str = &rest[..end];
143
                    let pv = parse_pixel_value(val_str).map_err(|_| ())?;
144
                    items.push(CalcAstItem::Value(pv));
145
                    i += end;
146
26
                } else {
147
26
                    items.push(CalcAstItem::Sub);
148
26
                    i += 1;
149
26
                }
150
            }
151
            _ => {
152
                // Must be a numeric value (e.g. 100%, 20px, 3, 1.5em)
153
52
                let rest = &input[i..];
154
52
                let end = find_value_end(rest);
155
52
                if end == 0 { return Err(()); }
156
52
                let val_str = &rest[..end];
157
52
                let pv = parse_pixel_value(val_str).map_err(|_| ())?;
158
52
                items.push(CalcAstItem::Value(pv));
159
52
                i += end;
160
            }
161
        }
162
    }
163

            
164
26
    if items.is_empty() {
165
        return Err(());
166
26
    }
167

            
168
26
    Ok(CalcAstItemVec::from(items))
169
26
}
170

            
171
/// Find the end of a numeric value token in a calc() expression.
172
/// Returns the byte offset where the value ends.
173
#[cfg(feature = "parser")]
174
52
fn find_value_end(s: &str) -> usize {
175
52
    let bytes = s.as_bytes();
176
52
    let mut i = 0;
177

            
178
    // Optional leading sign
179
52
    if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
180
        i += 1;
181
52
    }
182

            
183
    // Digits and decimal point
184
182
    while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
185
130
        i += 1;
186
130
    }
187

            
188
    // Unit suffix (alphabetic characters like px, %, em, rem, vw, vh, etc.)
189
130
    while i < bytes.len() && (bytes[i].is_ascii_alphabetic() || bytes[i] == b'%') {
190
78
        i += 1;
191
78
    }
192

            
193
52
    i
194
52
}
195

            
196
/// Format a `CalcAstItemVec` as a CSS `calc(...)` string.
197
fn calc_ast_to_css_string(items: &CalcAstItemVec) -> String {
198
    let inner: Vec<String> = items.iter().map(|i| match i {
199
        CalcAstItem::Value(v) => v.to_string(),
200
        CalcAstItem::Add => "+".to_string(),
201
        CalcAstItem::Sub => "-".to_string(),
202
        CalcAstItem::Mul => "*".to_string(),
203
        CalcAstItem::Div => "/".to_string(),
204
        CalcAstItem::BraceOpen => "(".to_string(),
205
        CalcAstItem::BraceClose => ")".to_string(),
206
    }).collect();
207
    alloc::format!("calc({})", inner.join(" "))
208
}
209

            
210
// -- Type Definitions --
211

            
212
macro_rules! define_dimension_property {
213
    ($struct_name:ident, $default_fn:expr) => {
214
        #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
215
        #[repr(C)]
216
        pub struct $struct_name {
217
            pub inner: PixelValue,
218
        }
219

            
220
        impl Default for $struct_name {
221
            fn default() -> Self {
222
                $default_fn()
223
            }
224
        }
225

            
226
        impl PixelValueTaker for $struct_name {
227
910
            fn from_pixel_value(inner: PixelValue) -> Self {
228
910
                Self { inner }
229
910
            }
230
        }
231

            
232
        impl_pixel_value!($struct_name);
233

            
234
        impl PrintAsCssValue for $struct_name {
235
            fn print_as_css_value(&self) -> String {
236
                self.inner.to_string()
237
            }
238
        }
239
    };
240
}
241

            
242
macro_rules! define_sizing_enum {
243
    ($name:ident) => {
244
        #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
245
        #[repr(C, u8)]
246
        #[derive(Default)]
247
        pub enum $name {
248
            #[default]
249
            Auto,
250
            Px(PixelValue),
251
            MinContent,
252
            MaxContent,
253
            /// `fit-content(<length-percentage>)` = `min(max-content, max(min-content, <length-percentage>))`
254
            FitContent(PixelValue),
255
            /// `calc()` expression stored as a flat stack-machine AST
256
            Calc(CalcAstItemVec),
257
        }
258

            
259
        impl PixelValueTaker for $name {
260
            fn from_pixel_value(inner: PixelValue) -> Self {
261
                $name::Px(inner)
262
            }
263
        }
264

            
265
        impl PrintAsCssValue for $name {
266
            fn print_as_css_value(&self) -> String {
267
                match self {
268
                    $name::Auto => "auto".to_string(),
269
                    $name::Px(v) => v.to_string(),
270
                    $name::MinContent => "min-content".to_string(),
271
                    $name::MaxContent => "max-content".to_string(),
272
                    $name::FitContent(v) => alloc::format!("fit-content({})", v),
273
                    $name::Calc(items) => calc_ast_to_css_string(items),
274
                }
275
            }
276
        }
277

            
278
        impl $name {
279
            pub fn px(value: f32) -> Self {
280
                $name::Px(PixelValue::px(value))
281
            }
282

            
283
2
            pub const fn const_px(value: isize) -> Self {
284
2
                $name::Px(PixelValue::const_px(value))
285
2
            }
286

            
287
            pub fn interpolate(&self, other: &Self, t: f32) -> Self {
288
                match (self, other) {
289
                    ($name::Px(a), $name::Px(b)) => $name::Px(a.interpolate(b, t)),
290
                    ($name::FitContent(a), $name::FitContent(b)) => $name::FitContent(a.interpolate(b, t)),
291
                    (_, $name::Px(b)) if t >= 0.5 => $name::Px(*b),
292
                    ($name::Px(a), _) if t < 0.5 => $name::Px(*a),
293
                    ($name::Auto, $name::Auto) => $name::Auto,
294
                    (a, _) if t < 0.5 => a.clone(),
295
                    (_, b) => b.clone(),
296
                }
297
            }
298
        }
299
    };
300
}
301

            
302
define_sizing_enum!(LayoutWidth);
303
define_sizing_enum!(LayoutHeight);
304

            
305
/// CSS `min-width` property. Defaults to `0px`.
306
define_dimension_property!(LayoutMinWidth, || Self {
307
    inner: PixelValue::zero()
308
});
309
/// CSS `min-height` property. Defaults to `0px`.
310
define_dimension_property!(LayoutMinHeight, || Self {
311
    inner: PixelValue::zero()
312
});
313
/// CSS `max-width` property. Defaults to `f32::MAX` pixels (i.e. unconstrained).
314
///
315
/// NOTE: The layout solver must handle `f32::MAX` gracefully — adding
316
/// padding/margin to this sentinel would overflow to infinity.
317
define_dimension_property!(LayoutMaxWidth, || Self {
318
    inner: PixelValue::px(core::f32::MAX)
319
});
320
/// CSS `max-height` property. Defaults to `f32::MAX` pixels (i.e. unconstrained).
321
///
322
/// NOTE: The layout solver must handle `f32::MAX` gracefully — adding
323
/// padding/margin to this sentinel would overflow to infinity.
324
define_dimension_property!(LayoutMaxHeight, || Self {
325
    inner: PixelValue::px(core::f32::MAX)
326
});
327

            
328
/// Represents a `box-sizing` attribute
329
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
330
#[repr(C)]
331
#[derive(Default)]
332
pub enum LayoutBoxSizing {
333
    #[default]
334
    ContentBox,
335
    BorderBox,
336
}
337

            
338

            
339
impl PrintAsCssValue for LayoutBoxSizing {
340
    fn print_as_css_value(&self) -> String {
341
        String::from(match self {
342
            LayoutBoxSizing::ContentBox => "content-box",
343
            LayoutBoxSizing::BorderBox => "border-box",
344
        })
345
    }
346
}
347

            
348
// -- Parser --
349

            
350
#[cfg(feature = "parser")]
351
pub mod parser {
352

            
353
    use alloc::string::ToString;
354
    use crate::corety::AzString;
355

            
356
    use super::*;
357
    use crate::props::basic::pixel::parse_pixel_value;
358

            
359
    macro_rules! define_pixel_dimension_parser {
360
        ($fn_name:ident, $struct_name:ident, $error_name:ident, $error_owned_name:ident) => {
361
            #[derive(Clone, PartialEq)]
362
            pub enum $error_name<'a> {
363
                PixelValue(CssPixelValueParseError<'a>),
364
            }
365

            
366
            impl_debug_as_display!($error_name<'a>);
367
            impl_display! { $error_name<'a>, {
368
                PixelValue(e) => format!("{}", e),
369
            }}
370

            
371
            impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
372

            
373
            #[derive(Debug, Clone, PartialEq)]
374
            #[repr(C, u8)]
375
            pub enum $error_owned_name {
376
                PixelValue(CssPixelValueParseErrorOwned),
377
            }
378

            
379
            impl<'a> $error_name<'a> {
380
                pub fn to_contained(&self) -> $error_owned_name {
381
                    match self {
382
                        $error_name::PixelValue(e) => {
383
                            $error_owned_name::PixelValue(e.to_contained())
384
                        }
385
                    }
386
                }
387
            }
388

            
389
            impl $error_owned_name {
390
                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
391
                    match self {
392
                        $error_owned_name::PixelValue(e) => $error_name::PixelValue(e.to_shared()),
393
                    }
394
                }
395
            }
396

            
397
336
            pub fn $fn_name<'a>(input: &'a str) -> Result<$struct_name, $error_name<'a>> {
398
336
                parse_pixel_value(input)
399
336
                    .map(|v| $struct_name { inner: v })
400
336
                    .map_err($error_name::PixelValue)
401
336
            }
402
        };
403
    }
404

            
405
    macro_rules! define_sizing_parser {
406
        ($fn_name:ident, $enum_name:ident, $error_name:ident, $error_owned_name:ident, $keyword_label:expr) => {
407
            #[derive(Clone, PartialEq)]
408
            pub enum $error_name<'a> {
409
                PixelValue(CssPixelValueParseError<'a>),
410
                InvalidKeyword(&'a str),
411
            }
412

            
413
            impl_debug_as_display!($error_name<'a>);
414
            impl_display! { $error_name<'a>, {
415
                PixelValue(e) => format!("{}", e),
416
                InvalidKeyword(k) => format!("Invalid {} keyword: \"{}\"", $keyword_label, k),
417
            }}
418

            
419
            impl_from! { CssPixelValueParseError<'a>, $error_name::PixelValue }
420

            
421
            #[derive(Debug, Clone, PartialEq)]
422
            #[repr(C, u8)]
423
            pub enum $error_owned_name {
424
                PixelValue(CssPixelValueParseErrorOwned),
425
                InvalidKeyword(AzString),
426
            }
427

            
428
            impl<'a> $error_name<'a> {
429
                pub fn to_contained(&self) -> $error_owned_name {
430
                    match self {
431
                        $error_name::PixelValue(e) => {
432
                            $error_owned_name::PixelValue(e.to_contained())
433
                        }
434
                        $error_name::InvalidKeyword(k) => {
435
                            $error_owned_name::InvalidKeyword(k.to_string().into())
436
                        }
437
                    }
438
                }
439
            }
440

            
441
            impl $error_owned_name {
442
                pub fn to_shared<'a>(&'a self) -> $error_name<'a> {
443
                    match self {
444
                        $error_owned_name::PixelValue(e) => {
445
                            $error_name::PixelValue(e.to_shared())
446
                        }
447
                        $error_owned_name::InvalidKeyword(k) => {
448
                            $error_name::InvalidKeyword(k)
449
                        }
450
                    }
451
                }
452
            }
453

            
454
11219
            pub fn $fn_name<'a>(
455
11219
                input: &'a str,
456
11219
            ) -> Result<$enum_name, $error_name<'a>> {
457
11219
                let trimmed = input.trim();
458
11216
                match trimmed {
459
11219
                    "auto" => Ok($enum_name::Auto),
460
11218
                    "min-content" => Ok($enum_name::MinContent),
461
11217
                    "max-content" => Ok($enum_name::MaxContent),
462
11216
                    s if s.starts_with("fit-content(") && s.ends_with(')') => {
463
                        let inner = &s[12..s.len() - 1].trim();
464
                        parse_pixel_value(inner)
465
                            .map(|pv| {
466
                                if pv.number.get() < 0.0 {
467
                                    $enum_name::FitContent(PixelValue::zero())
468
                                } else {
469
                                    $enum_name::FitContent(pv)
470
                                }
471
                            })
472
                            .map_err($error_name::PixelValue)
473
                    }
474
11216
                    s if s.starts_with("calc(") && s.ends_with(')') => {
475
26
                        let inner = &s[5..s.len() - 1];
476
26
                        parse_calc_expression(inner)
477
26
                            .map($enum_name::Calc)
478
26
                            .map_err(|_| $error_name::InvalidKeyword(input))
479
                    }
480
11190
                    _ => parse_pixel_value(trimmed)
481
11190
                        .map($enum_name::Px)
482
11190
                        .map_err($error_name::PixelValue),
483
                }
484
11219
            }
485
        };
486
    }
487

            
488
    define_sizing_parser!(parse_layout_width, LayoutWidth, LayoutWidthParseError, LayoutWidthParseErrorOwned, "width");
489
    define_sizing_parser!(parse_layout_height, LayoutHeight, LayoutHeightParseError, LayoutHeightParseErrorOwned, "height");
490
    define_pixel_dimension_parser!(
491
        parse_layout_min_width,
492
        LayoutMinWidth,
493
        LayoutMinWidthParseError,
494
        LayoutMinWidthParseErrorOwned
495
    );
496
    define_pixel_dimension_parser!(
497
        parse_layout_min_height,
498
        LayoutMinHeight,
499
        LayoutMinHeightParseError,
500
        LayoutMinHeightParseErrorOwned
501
    );
502
    define_pixel_dimension_parser!(
503
        parse_layout_max_width,
504
        LayoutMaxWidth,
505
        LayoutMaxWidthParseError,
506
        LayoutMaxWidthParseErrorOwned
507
    );
508
    define_pixel_dimension_parser!(
509
        parse_layout_max_height,
510
        LayoutMaxHeight,
511
        LayoutMaxHeightParseError,
512
        LayoutMaxHeightParseErrorOwned
513
    );
514

            
515
    // -- Box Sizing Parser --
516

            
517
    #[derive(Clone, PartialEq)]
518
    pub enum LayoutBoxSizingParseError<'a> {
519
        InvalidValue(&'a str),
520
    }
521

            
522
    impl_debug_as_display!(LayoutBoxSizingParseError<'a>);
523
    impl_display! { LayoutBoxSizingParseError<'a>, {
524
        InvalidValue(v) => format!("Invalid box-sizing value: \"{}\"", v),
525
    }}
526

            
527
    #[derive(Debug, Clone, PartialEq)]
528
    #[repr(C, u8)]
529
    pub enum LayoutBoxSizingParseErrorOwned {
530
        InvalidValue(AzString),
531
    }
532

            
533
    impl<'a> LayoutBoxSizingParseError<'a> {
534
        pub fn to_contained(&self) -> LayoutBoxSizingParseErrorOwned {
535
            match self {
536
                LayoutBoxSizingParseError::InvalidValue(s) => {
537
                    LayoutBoxSizingParseErrorOwned::InvalidValue(s.to_string().into())
538
                }
539
            }
540
        }
541
    }
542

            
543
    impl LayoutBoxSizingParseErrorOwned {
544
        pub fn to_shared<'a>(&'a self) -> LayoutBoxSizingParseError<'a> {
545
            match self {
546
                LayoutBoxSizingParseErrorOwned::InvalidValue(s) => {
547
                    LayoutBoxSizingParseError::InvalidValue(s)
548
                }
549
            }
550
        }
551
    }
552

            
553
300
    pub fn parse_layout_box_sizing<'a>(
554
300
        input: &'a str,
555
300
    ) -> Result<LayoutBoxSizing, LayoutBoxSizingParseError<'a>> {
556
300
        match input.trim() {
557
300
            "content-box" => Ok(LayoutBoxSizing::ContentBox),
558
299
            "border-box" => Ok(LayoutBoxSizing::BorderBox),
559
3
            other => Err(LayoutBoxSizingParseError::InvalidValue(other)),
560
        }
561
300
    }
562
}
563

            
564
#[cfg(feature = "parser")]
565
pub use self::parser::*;
566

            
567
#[cfg(all(test, feature = "parser"))]
568
mod tests {
569
    use super::*;
570
    use crate::props::basic::pixel::PixelValue;
571

            
572
    #[test]
573
1
    fn test_parse_layout_width() {
574
1
        assert_eq!(
575
1
            parse_layout_width("150px").unwrap(),
576
1
            LayoutWidth::Px(PixelValue::px(150.0))
577
        );
578
1
        assert_eq!(
579
1
            parse_layout_width("2.5em").unwrap(),
580
1
            LayoutWidth::Px(PixelValue::em(2.5))
581
        );
582
1
        assert_eq!(
583
1
            parse_layout_width("75%").unwrap(),
584
1
            LayoutWidth::Px(PixelValue::percent(75.0))
585
        );
586
1
        assert_eq!(
587
1
            parse_layout_width("0").unwrap(),
588
1
            LayoutWidth::Px(PixelValue::px(0.0))
589
        );
590
1
        assert_eq!(
591
1
            parse_layout_width("  100pt  ").unwrap(),
592
1
            LayoutWidth::Px(PixelValue::pt(100.0))
593
        );
594
1
        assert_eq!(
595
1
            parse_layout_width("min-content").unwrap(),
596
            LayoutWidth::MinContent
597
        );
598
1
        assert_eq!(
599
1
            parse_layout_width("max-content").unwrap(),
600
            LayoutWidth::MaxContent
601
        );
602
1
    }
603

            
604
    #[test]
605
1
    fn test_parse_layout_height_invalid() {
606
        // "auto" is now a valid value for height (CSS spec)
607
1
        assert!(parse_layout_height("auto").is_ok());
608
        // Liberal parsing accepts whitespace between number and unit
609
1
        assert!(parse_layout_height("150 px").is_ok());
610
1
        assert!(parse_layout_height("px").is_err());
611
1
        assert!(parse_layout_height("invalid").is_err());
612
1
    }
613

            
614
    #[test]
615
1
    fn test_parse_layout_box_sizing() {
616
1
        assert_eq!(
617
1
            parse_layout_box_sizing("content-box").unwrap(),
618
            LayoutBoxSizing::ContentBox
619
        );
620
1
        assert_eq!(
621
1
            parse_layout_box_sizing("border-box").unwrap(),
622
            LayoutBoxSizing::BorderBox
623
        );
624
1
        assert_eq!(
625
1
            parse_layout_box_sizing("  border-box  ").unwrap(),
626
            LayoutBoxSizing::BorderBox
627
        );
628
1
    }
629

            
630
    #[test]
631
1
    fn test_parse_layout_box_sizing_invalid() {
632
1
        assert!(parse_layout_box_sizing("padding-box").is_err());
633
1
        assert!(parse_layout_box_sizing("borderbox").is_err());
634
1
        assert!(parse_layout_box_sizing("").is_err());
635
1
    }
636
}