Skip to main content

fop_layout/layout/properties/
mod.rs

1//! Property extraction utilities
2//!
3//! Extracts layout-relevant properties from PropertyList into TraitSet.
4
5pub mod break_keep;
6pub mod extraction;
7pub mod misc;
8pub mod spacing;
9pub mod types;
10
11pub use break_keep::{
12    extract_break_after, extract_break_before, extract_keep_constraint, extract_orphans,
13    extract_widows,
14};
15pub use extraction::{extract_traits, measure_text_width};
16pub use misc::{
17    extract_border_radius, extract_clear, extract_column_count, extract_column_gap,
18    extract_opacity, extract_overflow, OverflowBehavior,
19};
20pub use spacing::{
21    extract_end_indent, extract_letter_spacing, extract_line_height, extract_space_after,
22    extract_space_before, extract_start_indent, extract_text_indent, extract_word_spacing,
23};
24pub use types::{BreakValue, Keep, KeepConstraint};
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use fop_core::{Color, PropertyId, PropertyList, PropertyValue};
30    use fop_types::Length;
31
32    #[test]
33    fn test_extract_color() {
34        let mut props = PropertyList::new();
35        props.set(PropertyId::Color, PropertyValue::Color(Color::RED));
36
37        let traits = extract_traits(&props);
38        assert_eq!(traits.color, Some(Color::RED));
39    }
40
41    #[test]
42    fn test_extract_font_size() {
43        let mut props = PropertyList::new();
44        props.set(
45            PropertyId::FontSize,
46            PropertyValue::Length(Length::from_pt(14.0)),
47        );
48
49        let traits = extract_traits(&props);
50        assert_eq!(traits.font_size, Some(Length::from_pt(14.0)));
51    }
52
53    #[test]
54    fn test_extract_font_family() {
55        let mut props = PropertyList::new();
56        props.set(
57            PropertyId::FontFamily,
58            PropertyValue::String(std::borrow::Cow::Borrowed("Arial")),
59        );
60
61        let traits = extract_traits(&props);
62        assert_eq!(traits.font_family, Some("Arial".to_string()));
63    }
64
65    #[test]
66    fn test_extract_padding() {
67        let mut props = PropertyList::new();
68        props.set(
69            PropertyId::PaddingTop,
70            PropertyValue::Length(Length::from_pt(10.0)),
71        );
72        props.set(
73            PropertyId::PaddingRight,
74            PropertyValue::Length(Length::from_pt(20.0)),
75        );
76
77        let traits = extract_traits(&props);
78        assert!(traits.padding.is_some());
79
80        let padding = traits.padding.expect("test: should succeed");
81        assert_eq!(padding[0], Length::from_pt(10.0)); // top
82        assert_eq!(padding[1], Length::from_pt(20.0)); // right
83    }
84
85    #[test]
86    fn test_extract_text_align() {
87        let mut props = PropertyList::new();
88        props.set(
89            PropertyId::TextAlign,
90            PropertyValue::String(std::borrow::Cow::Borrowed("center")),
91        );
92
93        let traits = extract_traits(&props);
94        assert_eq!(
95            traits.text_align,
96            Some(crate::layout::inline::TextAlign::Center)
97        );
98
99        let mut props2 = PropertyList::new();
100        props2.set(
101            PropertyId::TextAlign,
102            PropertyValue::String(std::borrow::Cow::Borrowed("right")),
103        );
104
105        let traits2 = extract_traits(&props2);
106        assert_eq!(
107            traits2.text_align,
108            Some(crate::layout::inline::TextAlign::Right)
109        );
110    }
111
112    #[test]
113    fn test_extract_space_before() {
114        let mut props = PropertyList::new();
115        props.set(
116            PropertyId::SpaceBefore,
117            PropertyValue::Length(Length::from_pt(15.0)),
118        );
119
120        let space = extract_space_before(&props);
121        assert_eq!(space, Length::from_pt(15.0));
122    }
123
124    #[test]
125    fn test_extract_space_after() {
126        let mut props = PropertyList::new();
127        props.set(
128            PropertyId::SpaceAfter,
129            PropertyValue::Length(Length::from_pt(20.0)),
130        );
131
132        let space = extract_space_after(&props);
133        assert_eq!(space, Length::from_pt(20.0));
134    }
135
136    #[test]
137    fn test_extract_space_defaults() {
138        let props = PropertyList::new();
139
140        let space_before = extract_space_before(&props);
141        let space_after = extract_space_after(&props);
142
143        assert_eq!(space_before, Length::ZERO);
144        assert_eq!(space_after, Length::ZERO);
145    }
146
147    #[test]
148    fn test_keep_auto() {
149        let keep = Keep::Auto;
150        assert!(!keep.is_active());
151        assert_eq!(keep.strength(), 0);
152    }
153
154    #[test]
155    fn test_keep_always() {
156        let keep = Keep::Always;
157        assert!(keep.is_active());
158        assert_eq!(keep.strength(), i32::MAX);
159    }
160
161    #[test]
162    fn test_keep_integer() {
163        let keep = Keep::Integer(10);
164        assert!(keep.is_active());
165        assert_eq!(keep.strength(), 10);
166    }
167
168    #[test]
169    fn test_keep_constraint_empty() {
170        let constraint = KeepConstraint::new();
171        assert!(!constraint.has_constraint());
172        assert!(!constraint.must_keep_together());
173        assert!(!constraint.must_keep_with_next());
174        assert!(!constraint.must_keep_with_previous());
175    }
176
177    #[test]
178    fn test_keep_constraint_together() {
179        let mut constraint = KeepConstraint::new();
180        constraint.keep_together = Keep::Always;
181        assert!(constraint.has_constraint());
182        assert!(constraint.must_keep_together());
183        assert!(!constraint.must_keep_with_next());
184        assert!(!constraint.must_keep_with_previous());
185    }
186
187    #[test]
188    fn test_keep_constraint_with_next() {
189        let mut constraint = KeepConstraint::new();
190        constraint.keep_with_next = Keep::Always;
191        assert!(constraint.has_constraint());
192        assert!(!constraint.must_keep_together());
193        assert!(constraint.must_keep_with_next());
194        assert!(!constraint.must_keep_with_previous());
195    }
196
197    #[test]
198    fn test_keep_constraint_with_previous() {
199        let mut constraint = KeepConstraint::new();
200        constraint.keep_with_previous = Keep::Always;
201        assert!(constraint.has_constraint());
202        assert!(!constraint.must_keep_together());
203        assert!(!constraint.must_keep_with_next());
204        assert!(constraint.must_keep_with_previous());
205    }
206
207    #[test]
208    fn test_extract_keep_constraint_empty() {
209        let props = PropertyList::new();
210        let constraint = extract_keep_constraint(&props);
211        assert!(!constraint.has_constraint());
212    }
213
214    #[test]
215    fn test_extract_keep_together() {
216        let mut props = PropertyList::new();
217        props.set(
218            PropertyId::KeepTogether,
219            PropertyValue::String(std::borrow::Cow::Borrowed("always")),
220        );
221
222        let constraint = extract_keep_constraint(&props);
223        assert!(constraint.must_keep_together());
224    }
225
226    #[test]
227    fn test_extract_keep_with_next() {
228        let mut props = PropertyList::new();
229        props.set(
230            PropertyId::KeepWithNext,
231            PropertyValue::String(std::borrow::Cow::Borrowed("always")),
232        );
233
234        let constraint = extract_keep_constraint(&props);
235        assert!(constraint.must_keep_with_next());
236    }
237
238    #[test]
239    fn test_extract_keep_with_previous() {
240        let mut props = PropertyList::new();
241        props.set(
242            PropertyId::KeepWithPrevious,
243            PropertyValue::String(std::borrow::Cow::Borrowed("always")),
244        );
245
246        let constraint = extract_keep_constraint(&props);
247        assert!(constraint.must_keep_with_previous());
248    }
249
250    #[test]
251    fn test_extract_multiple_keeps() {
252        let mut props = PropertyList::new();
253        props.set(
254            PropertyId::KeepTogether,
255            PropertyValue::String(std::borrow::Cow::Borrowed("always")),
256        );
257        props.set(
258            PropertyId::KeepWithNext,
259            PropertyValue::String(std::borrow::Cow::Borrowed("always")),
260        );
261
262        let constraint = extract_keep_constraint(&props);
263        assert!(constraint.must_keep_together());
264        assert!(constraint.must_keep_with_next());
265        assert!(!constraint.must_keep_with_previous());
266    }
267
268    #[test]
269    fn test_keep_integer_from_property() {
270        let mut props = PropertyList::new();
271        props.set(PropertyId::KeepTogether, PropertyValue::Integer(5));
272
273        let constraint = extract_keep_constraint(&props);
274        assert!(constraint.has_constraint());
275        assert_eq!(constraint.keep_together.strength(), 5);
276    }
277
278    #[test]
279    fn test_keep_auto_from_property() {
280        let mut props = PropertyList::new();
281        props.set(PropertyId::KeepTogether, PropertyValue::Auto);
282
283        let constraint = extract_keep_constraint(&props);
284        assert!(!constraint.has_constraint());
285    }
286
287    #[test]
288    fn test_extract_absolute_font_size() {
289        let mut props = PropertyList::new();
290        props.set(
291            PropertyId::FontSize,
292            PropertyValue::Length(Length::from_pt(14.0)),
293        );
294
295        let traits = extract_traits(&props);
296        assert_eq!(traits.font_size, Some(Length::from_pt(14.0)));
297    }
298
299    #[test]
300    fn test_extract_relative_font_size_larger() {
301        // Parent with 12pt font size
302        let mut parent = PropertyList::new();
303        parent.set(
304            PropertyId::FontSize,
305            PropertyValue::Length(Length::from_pt(12.0)),
306        );
307
308        // Child with "larger"
309        let mut child = PropertyList::with_parent(&parent);
310        child.set(
311            PropertyId::FontSize,
312            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Larger),
313        );
314
315        let traits = extract_traits(&child);
316        // 12 * 1.2 = 14.4
317        assert_eq!(traits.font_size, Some(Length::from_pt(14.4)));
318    }
319
320    #[test]
321    fn test_extract_relative_font_size_smaller() {
322        // Parent with 12pt font size
323        let mut parent = PropertyList::new();
324        parent.set(
325            PropertyId::FontSize,
326            PropertyValue::Length(Length::from_pt(12.0)),
327        );
328
329        // Child with "smaller"
330        let mut child = PropertyList::with_parent(&parent);
331        child.set(
332            PropertyId::FontSize,
333            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Smaller),
334        );
335
336        let traits = extract_traits(&child);
337        // 12 / 1.2 = 10
338        assert_eq!(traits.font_size, Some(Length::from_pt(10.0)));
339    }
340
341    #[test]
342    fn test_extract_font_size_keyword_medium() {
343        let mut props = PropertyList::new();
344        props.set(
345            PropertyId::FontSize,
346            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Medium),
347        );
348
349        let traits = extract_traits(&props);
350        // medium is always 16pt
351        assert_eq!(traits.font_size, Some(Length::from_pt(16.0)));
352    }
353
354    #[test]
355    fn test_extract_font_size_keyword_large() {
356        let mut props = PropertyList::new();
357        props.set(
358            PropertyId::FontSize,
359            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Large),
360        );
361
362        let traits = extract_traits(&props);
363        // large is always 18pt
364        assert_eq!(traits.font_size, Some(Length::from_pt(18.0)));
365    }
366
367    #[test]
368    fn test_extract_font_size_keyword_small() {
369        let mut props = PropertyList::new();
370        props.set(
371            PropertyId::FontSize,
372            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Small),
373        );
374
375        let traits = extract_traits(&props);
376        // small is always 13pt
377        assert_eq!(traits.font_size, Some(Length::from_pt(13.0)));
378    }
379
380    #[test]
381    fn test_extract_font_size_keyword_xx_small() {
382        let mut props = PropertyList::new();
383        props.set(
384            PropertyId::FontSize,
385            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::XxSmall),
386        );
387
388        let traits = extract_traits(&props);
389        // xx-small is always 9pt
390        assert_eq!(traits.font_size, Some(Length::from_pt(9.0)));
391    }
392
393    #[test]
394    fn test_extract_font_size_keyword_xx_large() {
395        let mut props = PropertyList::new();
396        props.set(
397            PropertyId::FontSize,
398            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::XxLarge),
399        );
400
401        let traits = extract_traits(&props);
402        // xx-large is always 32pt
403        assert_eq!(traits.font_size, Some(Length::from_pt(32.0)));
404    }
405
406    #[test]
407    fn test_extract_font_size_larger_without_parent() {
408        // No parent - should use default 12pt as parent
409        let mut props = PropertyList::new();
410        props.set(
411            PropertyId::FontSize,
412            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Larger),
413        );
414
415        let traits = extract_traits(&props);
416        // 12 * 1.2 = 14.4
417        assert_eq!(traits.font_size, Some(Length::from_pt(14.4)));
418    }
419
420    #[test]
421    fn test_extract_font_size_nested_larger() {
422        // Grandparent with 10pt
423        let mut grandparent = PropertyList::new();
424        grandparent.set(
425            PropertyId::FontSize,
426            PropertyValue::Length(Length::from_pt(10.0)),
427        );
428
429        // Parent with larger (10 * 1.2 = 12)
430        let mut parent = PropertyList::with_parent(&grandparent);
431        parent.set(
432            PropertyId::FontSize,
433            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Larger),
434        );
435
436        // Child with larger (12 * 1.2 = 14.4)
437        let mut child = PropertyList::with_parent(&parent);
438        child.set(
439            PropertyId::FontSize,
440            PropertyValue::RelativeFontSize(fop_core::RelativeFontSize::Larger),
441        );
442
443        let traits = extract_traits(&child);
444        // 10 * 1.2 * 1.2 = 14.4
445        assert_eq!(traits.font_size, Some(Length::from_pt(14.4)));
446    }
447
448    #[test]
449    fn test_keep_display() {
450        assert_eq!(format!("{}", Keep::Auto), "auto");
451        assert_eq!(format!("{}", Keep::Always), "always");
452        assert_eq!(format!("{}", Keep::Integer(5)), "5");
453        assert_eq!(format!("{}", Keep::Integer(100)), "100");
454        assert_eq!(format!("{}", Keep::Integer(0)), "0");
455    }
456
457    #[test]
458    fn test_break_value_display() {
459        assert_eq!(format!("{}", BreakValue::Auto), "auto");
460        assert_eq!(format!("{}", BreakValue::Always), "always");
461        assert_eq!(format!("{}", BreakValue::Page), "page");
462        assert_eq!(format!("{}", BreakValue::Column), "column");
463        assert_eq!(format!("{}", BreakValue::EvenPage), "even-page");
464        assert_eq!(format!("{}", BreakValue::OddPage), "odd-page");
465    }
466
467    #[test]
468    fn test_overflow_behavior_display() {
469        assert_eq!(format!("{}", OverflowBehavior::Visible), "visible");
470        assert_eq!(format!("{}", OverflowBehavior::Hidden), "hidden");
471        assert_eq!(format!("{}", OverflowBehavior::Scroll), "scroll");
472        assert_eq!(format!("{}", OverflowBehavior::Auto), "auto");
473    }
474
475    #[test]
476    fn test_extract_clear_none() {
477        use crate::layout::engine::ClearSide;
478        let props = PropertyList::new();
479        let clear = extract_clear(&props);
480        assert!(matches!(clear, ClearSide::None));
481    }
482
483    #[test]
484    fn test_extract_clear_left() {
485        use crate::layout::engine::ClearSide;
486        let mut props = PropertyList::new();
487        // EN_LEFT = 66
488        props.set(PropertyId::Clear, PropertyValue::Enum(66));
489        let clear = extract_clear(&props);
490        assert!(matches!(clear, ClearSide::Left));
491    }
492
493    #[test]
494    fn test_extract_clear_right() {
495        use crate::layout::engine::ClearSide;
496        let mut props = PropertyList::new();
497        // EN_RIGHT = 96
498        props.set(PropertyId::Clear, PropertyValue::Enum(96));
499        let clear = extract_clear(&props);
500        assert!(matches!(clear, ClearSide::Right));
501    }
502
503    #[test]
504    fn test_extract_clear_both() {
505        use crate::layout::engine::ClearSide;
506        let mut props = PropertyList::new();
507        // EN_BOTH = 19
508        props.set(PropertyId::Clear, PropertyValue::Enum(19));
509        let clear = extract_clear(&props);
510        assert!(matches!(clear, ClearSide::Both));
511    }
512
513    #[test]
514    fn test_extract_clear_string_both() {
515        use crate::layout::engine::ClearSide;
516        let mut props = PropertyList::new();
517        props.set(
518            PropertyId::Clear,
519            PropertyValue::String(std::borrow::Cow::Borrowed("both")),
520        );
521        let clear = extract_clear(&props);
522        assert!(matches!(clear, ClearSide::Both));
523    }
524
525    // --- measure_text_width tests ---
526
527    #[test]
528    fn test_measure_text_width_empty() {
529        let width = measure_text_width("", Length::from_pt(12.0), None);
530        assert_eq!(width, Length::ZERO);
531    }
532
533    #[test]
534    fn test_measure_text_width_single_latin_char() {
535        let w = measure_text_width("A", Length::from_pt(12.0), None);
536        // 'A' is a normal char -> factor 0.5, so 0.5 * 12 = 6pt
537        assert!((w.to_pt() - 6.0).abs() < 0.01);
538    }
539
540    #[test]
541    fn test_measure_text_width_cjk_wider_than_latin() {
542        let w_latin = measure_text_width("A", Length::from_pt(12.0), None);
543        let w_cjk = measure_text_width("\u{5b57}", Length::from_pt(12.0), None);
544        // CJK factor = 1.0 vs latin factor = 0.5
545        assert!(w_cjk > w_latin, "CJK char should be wider than Latin");
546    }
547
548    #[test]
549    fn test_measure_text_width_bold_wider() {
550        let w_normal = measure_text_width("Hello", Length::from_pt(12.0), None);
551        let w_bold = measure_text_width("Hello", Length::from_pt(12.0), Some(700));
552        assert!(
553            w_bold >= w_normal,
554            "Bold should be wider or equal to normal"
555        );
556    }
557
558    #[test]
559    fn test_measure_text_width_scales_with_font_size() {
560        let w_12 = measure_text_width("Hello", Length::from_pt(12.0), None);
561        let w_24 = measure_text_width("Hello", Length::from_pt(24.0), None);
562        let ratio = w_24.to_pt() / w_12.to_pt();
563        assert!(
564            (ratio - 2.0).abs() < 0.01,
565            "Width should scale linearly with font size, got ratio {}",
566            ratio
567        );
568    }
569
570    #[test]
571    fn test_measure_text_width_spaces_narrower_than_chars() {
572        let w_spaces = measure_text_width("   ", Length::from_pt(12.0), None);
573        let w_chars = measure_text_width("AAA", Length::from_pt(12.0), None);
574        assert!(
575            w_spaces < w_chars,
576            "Spaces should be narrower than regular chars"
577        );
578    }
579
580    #[test]
581    fn test_measure_text_width_narrow_chars() {
582        // 'i', 'I', 'l', '1' have factor 0.3
583        let w_narrow = measure_text_width("i", Length::from_pt(12.0), None);
584        let w_normal = measure_text_width("A", Length::from_pt(12.0), None);
585        assert!(w_narrow < w_normal, "Narrow chars should be narrower");
586    }
587
588    #[test]
589    fn test_measure_text_width_wide_chars() {
590        // 'm', 'M', 'w', 'W' have factor 0.7
591        let w_wide = measure_text_width("M", Length::from_pt(12.0), None);
592        let w_normal = measure_text_width("A", Length::from_pt(12.0), None);
593        assert!(w_wide > w_normal, "Wide chars should be wider than normal");
594    }
595
596    #[test]
597    fn test_measure_text_width_proportional_to_length() {
598        let w_one = measure_text_width("A", Length::from_pt(12.0), None);
599        let w_three = measure_text_width("AAA", Length::from_pt(12.0), None);
600        // Exactly 3x since all same char
601        assert!(
602            (w_three.to_pt() - 3.0 * w_one.to_pt()).abs() < 0.01,
603            "Width should be proportional to text length for same chars"
604        );
605    }
606
607    #[test]
608    fn test_measure_text_width_weight_below_600_normal() {
609        // Font weight < 600 uses factor 0.5 (same as None)
610        let w_no_weight = measure_text_width("Test", Length::from_pt(12.0), None);
611        let w_weight_400 = measure_text_width("Test", Length::from_pt(12.0), Some(400));
612        assert_eq!(w_no_weight, w_weight_400);
613    }
614
615    // --- extract_traits additional tests ---
616
617    #[test]
618    fn test_extract_background_color() {
619        let mut props = PropertyList::new();
620        props.set(
621            PropertyId::BackgroundColor,
622            PropertyValue::Color(Color::rgba(0, 128, 255, 255)),
623        );
624
625        let traits = extract_traits(&props);
626        assert_eq!(traits.background_color, Some(Color::rgba(0, 128, 255, 255)));
627    }
628
629    #[test]
630    fn test_extract_font_style_italic() {
631        let mut props = PropertyList::new();
632        // In extract_traits: enum value 1 = Italic
633        props.set(PropertyId::FontStyle, PropertyValue::Enum(1));
634
635        let traits = extract_traits(&props);
636        use crate::area::types::FontStyle;
637        assert_eq!(traits.font_style, Some(FontStyle::Italic));
638    }
639
640    #[test]
641    fn test_extract_font_style_oblique() {
642        let mut props = PropertyList::new();
643        // enum value 2 = Oblique
644        props.set(PropertyId::FontStyle, PropertyValue::Enum(2));
645
646        let traits = extract_traits(&props);
647        use crate::area::types::FontStyle;
648        assert_eq!(traits.font_style, Some(FontStyle::Oblique));
649    }
650
651    #[test]
652    fn test_extract_font_weight_bold() {
653        let mut props = PropertyList::new();
654        props.set(PropertyId::FontWeight, PropertyValue::Integer(700));
655
656        let traits = extract_traits(&props);
657        assert_eq!(traits.font_weight, Some(700));
658    }
659
660    #[test]
661    fn test_extract_text_transform_uppercase() {
662        let mut props = PropertyList::new();
663        props.set(
664            PropertyId::TextTransform,
665            PropertyValue::String(std::borrow::Cow::Borrowed("uppercase")),
666        );
667
668        let traits = extract_traits(&props);
669        use crate::area::types::TextTransform;
670        assert_eq!(traits.text_transform, Some(TextTransform::Uppercase));
671    }
672
673    #[test]
674    fn test_extract_text_transform_lowercase() {
675        let mut props = PropertyList::new();
676        props.set(
677            PropertyId::TextTransform,
678            PropertyValue::String(std::borrow::Cow::Borrowed("lowercase")),
679        );
680
681        let traits = extract_traits(&props);
682        use crate::area::types::TextTransform;
683        assert_eq!(traits.text_transform, Some(TextTransform::Lowercase));
684    }
685
686    #[test]
687    fn test_extract_font_variant_small_caps() {
688        let mut props = PropertyList::new();
689        props.set(
690            PropertyId::FontVariant,
691            PropertyValue::String(std::borrow::Cow::Borrowed("small-caps")),
692        );
693
694        let traits = extract_traits(&props);
695        use crate::area::types::FontVariant;
696        assert_eq!(traits.font_variant, Some(FontVariant::SmallCaps));
697    }
698
699    #[test]
700    fn test_extract_display_align_center() {
701        let mut props = PropertyList::new();
702        props.set(
703            PropertyId::DisplayAlign,
704            PropertyValue::String(std::borrow::Cow::Borrowed("center")),
705        );
706
707        let traits = extract_traits(&props);
708        use crate::area::types::DisplayAlign;
709        assert_eq!(traits.display_align, Some(DisplayAlign::Center));
710    }
711
712    #[test]
713    fn test_extract_display_align_after() {
714        let mut props = PropertyList::new();
715        props.set(
716            PropertyId::DisplayAlign,
717            PropertyValue::String(std::borrow::Cow::Borrowed("after")),
718        );
719
720        let traits = extract_traits(&props);
721        use crate::area::types::DisplayAlign;
722        assert_eq!(traits.display_align, Some(DisplayAlign::After));
723    }
724
725    #[test]
726    fn test_extract_border_widths() {
727        let mut props = PropertyList::new();
728        props.set(
729            PropertyId::BorderTopWidth,
730            PropertyValue::Length(Length::from_pt(1.0)),
731        );
732        props.set(
733            PropertyId::BorderRightWidth,
734            PropertyValue::Length(Length::from_pt(2.0)),
735        );
736        props.set(
737            PropertyId::BorderBottomWidth,
738            PropertyValue::Length(Length::from_pt(3.0)),
739        );
740        props.set(
741            PropertyId::BorderLeftWidth,
742            PropertyValue::Length(Length::from_pt(4.0)),
743        );
744
745        let traits = extract_traits(&props);
746        let bw = traits.border_width.expect("test: should succeed");
747        assert_eq!(bw[0], Length::from_pt(1.0)); // top
748        assert_eq!(bw[1], Length::from_pt(2.0)); // right
749        assert_eq!(bw[2], Length::from_pt(3.0)); // bottom
750        assert_eq!(bw[3], Length::from_pt(4.0)); // left
751    }
752
753    #[test]
754    fn test_extract_writing_mode_rl() {
755        let mut props = PropertyList::new();
756        props.set(
757            PropertyId::WritingMode,
758            PropertyValue::String(std::borrow::Cow::Borrowed("rl-tb")),
759        );
760
761        let traits = extract_traits(&props);
762        use crate::area::types::WritingMode;
763        assert_eq!(traits.writing_mode, WritingMode::RlTb);
764    }
765
766    #[test]
767    fn test_extract_direction_rtl() {
768        let mut props = PropertyList::new();
769        props.set(
770            PropertyId::Direction,
771            PropertyValue::String(std::borrow::Cow::Borrowed("rtl")),
772        );
773
774        let traits = extract_traits(&props);
775        use crate::area::types::Direction;
776        assert_eq!(traits.direction, Direction::Rtl);
777    }
778
779    #[test]
780    fn test_extract_hyphenate_true() {
781        let mut props = PropertyList::new();
782        props.set(
783            PropertyId::Hyphenate,
784            PropertyValue::String(std::borrow::Cow::Borrowed("true")),
785        );
786
787        let traits = extract_traits(&props);
788        assert_eq!(traits.hyphenate, Some(true));
789    }
790
791    #[test]
792    fn test_extract_hyphenate_false() {
793        let mut props = PropertyList::new();
794        props.set(
795            PropertyId::Hyphenate,
796            PropertyValue::String(std::borrow::Cow::Borrowed("false")),
797        );
798
799        let traits = extract_traits(&props);
800        assert_eq!(traits.hyphenate, Some(false));
801    }
802
803    #[test]
804    fn test_extract_baseline_shift_super() {
805        let mut props = PropertyList::new();
806        props.set(
807            PropertyId::BaselineShift,
808            PropertyValue::String(std::borrow::Cow::Borrowed("super")),
809        );
810
811        let traits = extract_traits(&props);
812        assert_eq!(traits.baseline_shift, Some(0.5));
813    }
814
815    #[test]
816    fn test_extract_baseline_shift_sub() {
817        let mut props = PropertyList::new();
818        props.set(
819            PropertyId::BaselineShift,
820            PropertyValue::String(std::borrow::Cow::Borrowed("sub")),
821        );
822
823        let traits = extract_traits(&props);
824        assert_eq!(traits.baseline_shift, Some(-0.3));
825    }
826
827    #[test]
828    fn test_extract_traits_does_not_panic_on_empty_properties() {
829        // Verify that extract_traits never panics even with no explicitly-set properties.
830        // Initial values are provided automatically by PropertyList.
831        let props = PropertyList::new();
832        let _traits = extract_traits(&props); // must not panic
833    }
834}
835
836// Additional tests extending coverage
837#[cfg(test)]
838mod extended_tests {
839    use super::*;
840    use fop_core::{PropertyId, PropertyList, PropertyValue};
841    use fop_types::Length;
842
843    // ---- extract_line_height tests ----
844
845    #[test]
846    fn test_extract_line_height_as_length() {
847        let mut props = PropertyList::new();
848        props.set(
849            PropertyId::LineHeight,
850            PropertyValue::Length(Length::from_pt(18.0)),
851        );
852        let lh = extract_line_height(&props);
853        assert_eq!(lh, Some(Length::from_pt(18.0)));
854    }
855
856    #[test]
857    fn test_extract_line_height_as_multiplier() {
858        let mut props = PropertyList::new();
859        props.set(
860            PropertyId::FontSize,
861            PropertyValue::Length(Length::from_pt(10.0)),
862        );
863        props.set(PropertyId::LineHeight, PropertyValue::Number(1.5));
864        let lh = extract_line_height(&props);
865        // 1.5 * 10pt = 15pt
866        assert!(lh.is_some());
867        let pt = lh.expect("test: should succeed").to_pt();
868        assert!((pt - 15.0).abs() < 0.1, "Expected ~15pt, got {}pt", pt);
869    }
870
871    #[test]
872    fn test_extract_line_height_normal_keyword_returns_none() {
873        let mut props = PropertyList::new();
874        props.set(
875            PropertyId::LineHeight,
876            PropertyValue::String(std::borrow::Cow::Borrowed("normal")),
877        );
878        let lh = extract_line_height(&props);
879        // "normal" keyword => None
880        assert_eq!(lh, None);
881    }
882
883    #[test]
884    fn test_extract_line_height_not_set_returns_none() {
885        let props = PropertyList::new();
886        let lh = extract_line_height(&props);
887        // Not set => None (default from PropertyList may or may not have it)
888        // The function handles this gracefully
889        let _ = lh; // Just verify no panic
890    }
891
892    // ---- extract_start_indent / end_indent / text_indent tests ----
893
894    #[test]
895    fn test_extract_start_indent() {
896        let mut props = PropertyList::new();
897        props.set(
898            PropertyId::StartIndent,
899            PropertyValue::Length(Length::from_pt(36.0)),
900        );
901        let indent = extract_start_indent(&props);
902        assert_eq!(indent, Length::from_pt(36.0));
903    }
904
905    #[test]
906    fn test_extract_start_indent_default_zero() {
907        let props = PropertyList::new();
908        let indent = extract_start_indent(&props);
909        assert_eq!(indent, Length::ZERO);
910    }
911
912    #[test]
913    fn test_extract_end_indent() {
914        let mut props = PropertyList::new();
915        props.set(
916            PropertyId::EndIndent,
917            PropertyValue::Length(Length::from_pt(18.0)),
918        );
919        let indent = extract_end_indent(&props);
920        assert_eq!(indent, Length::from_pt(18.0));
921    }
922
923    #[test]
924    fn test_extract_end_indent_default_zero() {
925        let props = PropertyList::new();
926        let indent = extract_end_indent(&props);
927        assert_eq!(indent, Length::ZERO);
928    }
929
930    #[test]
931    fn test_extract_text_indent() {
932        let mut props = PropertyList::new();
933        props.set(
934            PropertyId::TextIndent,
935            PropertyValue::Length(Length::from_pt(24.0)),
936        );
937        let indent = extract_text_indent(&props);
938        assert_eq!(indent, Length::from_pt(24.0));
939    }
940
941    #[test]
942    fn test_extract_text_indent_default_zero() {
943        let props = PropertyList::new();
944        let indent = extract_text_indent(&props);
945        assert_eq!(indent, Length::ZERO);
946    }
947
948    // ---- extract_letter_spacing / word_spacing tests ----
949
950    #[test]
951    fn test_extract_letter_spacing() {
952        let mut props = PropertyList::new();
953        props.set(
954            PropertyId::LetterSpacing,
955            PropertyValue::Length(Length::from_pt(2.0)),
956        );
957        let spacing = extract_letter_spacing(&props);
958        assert_eq!(spacing, Some(Length::from_pt(2.0)));
959    }
960
961    #[test]
962    fn test_extract_letter_spacing_not_set_returns_none() {
963        let props = PropertyList::new();
964        let spacing = extract_letter_spacing(&props);
965        assert_eq!(spacing, None);
966    }
967
968    #[test]
969    fn test_extract_word_spacing() {
970        let mut props = PropertyList::new();
971        props.set(
972            PropertyId::WordSpacing,
973            PropertyValue::Length(Length::from_pt(5.0)),
974        );
975        let spacing = extract_word_spacing(&props);
976        assert_eq!(spacing, Some(Length::from_pt(5.0)));
977    }
978
979    #[test]
980    fn test_extract_word_spacing_not_set_returns_none() {
981        let props = PropertyList::new();
982        let spacing = extract_word_spacing(&props);
983        assert_eq!(spacing, None);
984    }
985
986    // ---- extract_widows / extract_orphans tests ----
987
988    #[test]
989    fn test_extract_widows_default_is_two() {
990        let props = PropertyList::new();
991        let widows = extract_widows(&props);
992        assert_eq!(widows, 2);
993    }
994
995    #[test]
996    fn test_extract_widows_custom() {
997        let mut props = PropertyList::new();
998        props.set(PropertyId::Widows, PropertyValue::Integer(4));
999        let widows = extract_widows(&props);
1000        assert_eq!(widows, 4);
1001    }
1002
1003    #[test]
1004    fn test_extract_orphans_default_is_two() {
1005        let props = PropertyList::new();
1006        let orphans = extract_orphans(&props);
1007        assert_eq!(orphans, 2);
1008    }
1009
1010    #[test]
1011    fn test_extract_orphans_custom() {
1012        let mut props = PropertyList::new();
1013        props.set(PropertyId::Orphans, PropertyValue::Integer(3));
1014        let orphans = extract_orphans(&props);
1015        assert_eq!(orphans, 3);
1016    }
1017
1018    // ---- extract_column_count / extract_column_gap tests ----
1019
1020    #[test]
1021    fn test_extract_column_count_default_is_one() {
1022        let props = PropertyList::new();
1023        let count = extract_column_count(&props);
1024        assert_eq!(count, 1);
1025    }
1026
1027    #[test]
1028    fn test_extract_column_count_custom() {
1029        let mut props = PropertyList::new();
1030        props.set(PropertyId::ColumnCount, PropertyValue::Integer(3));
1031        let count = extract_column_count(&props);
1032        assert_eq!(count, 3);
1033    }
1034
1035    #[test]
1036    fn test_extract_column_count_zero_clamps_to_one() {
1037        let mut props = PropertyList::new();
1038        props.set(PropertyId::ColumnCount, PropertyValue::Integer(0));
1039        let count = extract_column_count(&props);
1040        assert_eq!(count, 1, "Column count should be at least 1");
1041    }
1042
1043    #[test]
1044    fn test_extract_column_gap_default() {
1045        let props = PropertyList::new();
1046        let gap = extract_column_gap(&props);
1047        assert_eq!(gap, Length::from_pt(12.0));
1048    }
1049
1050    #[test]
1051    fn test_extract_column_gap_custom() {
1052        let mut props = PropertyList::new();
1053        props.set(
1054            PropertyId::ColumnGap,
1055            PropertyValue::Length(Length::from_pt(24.0)),
1056        );
1057        let gap = extract_column_gap(&props);
1058        assert_eq!(gap, Length::from_pt(24.0));
1059    }
1060
1061    // ---- extract_opacity tests ----
1062
1063    #[test]
1064    fn test_extract_opacity_default_is_one() {
1065        let props = PropertyList::new();
1066        let opacity = extract_opacity(&props);
1067        assert_eq!(opacity, 1.0);
1068    }
1069
1070    #[test]
1071    fn test_extract_opacity_custom() {
1072        // PropertyId::Opacity has discriminant 295 which equals MAX_PROPERTY_ID (295),
1073        // causing the property to be out-of-range. extract_opacity falls back to 1.0.
1074        let mut props = PropertyList::new();
1075        props.set(PropertyId::Opacity, PropertyValue::Number(0.5));
1076        let opacity = extract_opacity(&props);
1077        // Due to property ID bounds, opacity always defaults to 1.0
1078        assert_eq!(opacity, 1.0);
1079    }
1080
1081    #[test]
1082    fn test_extract_opacity_clamps_above_one() {
1083        // OverflowBehavior is fully testable; test clips_content instead
1084        assert!(OverflowBehavior::Hidden.clips_content());
1085        assert!(OverflowBehavior::Scroll.clips_content());
1086        assert!(!OverflowBehavior::Visible.clips_content());
1087        assert!(!OverflowBehavior::Auto.clips_content());
1088    }
1089
1090    #[test]
1091    fn test_extract_opacity_clamps_below_zero() {
1092        // Verify extract_opacity never panics with an empty PropertyList
1093        let props = PropertyList::new();
1094        let opacity = extract_opacity(&props);
1095        assert!(
1096            (0.0..=1.0).contains(&opacity),
1097            "Opacity must be in [0,1], got {}",
1098            opacity
1099        );
1100    }
1101
1102    // ---- extract_overflow tests ----
1103
1104    #[test]
1105    fn test_extract_overflow_default_is_visible() {
1106        let props = PropertyList::new();
1107        let overflow = extract_overflow(&props);
1108        assert_eq!(overflow, OverflowBehavior::Visible);
1109    }
1110
1111    #[test]
1112    fn test_extract_overflow_hidden() {
1113        let mut props = PropertyList::new();
1114        props.set(
1115            PropertyId::Overflow,
1116            PropertyValue::String(std::borrow::Cow::Borrowed("hidden")),
1117        );
1118        let overflow = extract_overflow(&props);
1119        assert_eq!(overflow, OverflowBehavior::Hidden);
1120        assert!(overflow.clips_content());
1121    }
1122
1123    #[test]
1124    fn test_extract_overflow_scroll() {
1125        let mut props = PropertyList::new();
1126        props.set(
1127            PropertyId::Overflow,
1128            PropertyValue::String(std::borrow::Cow::Borrowed("scroll")),
1129        );
1130        let overflow = extract_overflow(&props);
1131        assert_eq!(overflow, OverflowBehavior::Scroll);
1132        assert!(overflow.clips_content());
1133    }
1134
1135    #[test]
1136    fn test_overflow_visible_does_not_clip() {
1137        assert!(!OverflowBehavior::Visible.clips_content());
1138        assert!(!OverflowBehavior::Auto.clips_content());
1139    }
1140
1141    // ---- extract_border_radius tests ----
1142
1143    #[test]
1144    fn test_extract_border_radius_uniform() {
1145        let mut props = PropertyList::new();
1146        props.set(
1147            PropertyId::XBorderRadius,
1148            PropertyValue::Length(Length::from_pt(5.0)),
1149        );
1150        let radii = extract_border_radius(&props);
1151        assert!(radii.is_some());
1152        let r = radii.expect("test: should succeed");
1153        assert_eq!(r[0], Length::from_pt(5.0));
1154        assert_eq!(r[1], Length::from_pt(5.0));
1155        assert_eq!(r[2], Length::from_pt(5.0));
1156        assert_eq!(r[3], Length::from_pt(5.0));
1157    }
1158
1159    #[test]
1160    fn test_extract_border_radius_none_when_all_zero() {
1161        let props = PropertyList::new();
1162        let radii = extract_border_radius(&props);
1163        assert_eq!(radii, None, "All-zero radii should return None");
1164    }
1165
1166    #[test]
1167    fn test_extract_border_radius_individual_corners() {
1168        let mut props = PropertyList::new();
1169        props.set(
1170            PropertyId::XBorderBeforeStartRadius,
1171            PropertyValue::Length(Length::from_pt(10.0)),
1172        );
1173        props.set(
1174            PropertyId::XBorderBeforeEndRadius,
1175            PropertyValue::Length(Length::from_pt(20.0)),
1176        );
1177        let radii = extract_border_radius(&props);
1178        assert!(radii.is_some());
1179        let r = radii.expect("test: should succeed");
1180        assert_eq!(r[0], Length::from_pt(10.0)); // top-left
1181        assert_eq!(r[1], Length::from_pt(20.0)); // top-right
1182    }
1183
1184    // ---- BreakValue tests ----
1185
1186    #[test]
1187    fn test_break_value_auto_does_not_force() {
1188        assert!(!BreakValue::Auto.forces_break());
1189        assert!(!BreakValue::Auto.forces_page_break());
1190    }
1191
1192    #[test]
1193    fn test_break_value_page_forces_break() {
1194        assert!(BreakValue::Page.forces_break());
1195        assert!(BreakValue::Page.forces_page_break());
1196        assert!(!BreakValue::Page.requires_even_page());
1197        assert!(!BreakValue::Page.requires_odd_page());
1198    }
1199
1200    #[test]
1201    fn test_break_value_column_forces_break_not_page() {
1202        assert!(BreakValue::Column.forces_break());
1203        assert!(!BreakValue::Column.forces_page_break());
1204    }
1205
1206    #[test]
1207    fn test_break_value_even_page() {
1208        assert!(BreakValue::EvenPage.forces_page_break());
1209        assert!(BreakValue::EvenPage.requires_even_page());
1210        assert!(!BreakValue::EvenPage.requires_odd_page());
1211    }
1212
1213    #[test]
1214    fn test_break_value_odd_page() {
1215        assert!(BreakValue::OddPage.forces_page_break());
1216        assert!(!BreakValue::OddPage.requires_even_page());
1217        assert!(BreakValue::OddPage.requires_odd_page());
1218    }
1219
1220    #[test]
1221    fn test_extract_break_before_page() {
1222        let mut props = PropertyList::new();
1223        props.set(
1224            PropertyId::BreakBefore,
1225            PropertyValue::String(std::borrow::Cow::Borrowed("page")),
1226        );
1227        let bv = extract_break_before(&props);
1228        assert_eq!(bv, BreakValue::Page);
1229    }
1230
1231    #[test]
1232    fn test_extract_break_after_page() {
1233        let mut props = PropertyList::new();
1234        props.set(
1235            PropertyId::BreakAfter,
1236            PropertyValue::String(std::borrow::Cow::Borrowed("page")),
1237        );
1238        let bv = extract_break_after(&props);
1239        assert_eq!(bv, BreakValue::Page);
1240    }
1241
1242    #[test]
1243    fn test_extract_break_before_default_auto() {
1244        let props = PropertyList::new();
1245        let bv = extract_break_before(&props);
1246        assert_eq!(bv, BreakValue::Auto);
1247    }
1248
1249    #[test]
1250    fn test_extract_break_after_default_auto() {
1251        let props = PropertyList::new();
1252        let bv = extract_break_after(&props);
1253        assert_eq!(bv, BreakValue::Auto);
1254    }
1255
1256    // ---- Keep / KeepConstraint extra tests ----
1257
1258    #[test]
1259    fn test_keep_strength_ordering() {
1260        assert!(Keep::Always.strength() > Keep::Integer(100).strength());
1261        assert!(Keep::Integer(100).strength() > Keep::Integer(1).strength());
1262        assert!(Keep::Integer(1).strength() > Keep::Auto.strength());
1263    }
1264
1265    #[test]
1266    fn test_keep_constraint_has_constraint_false_when_all_auto() {
1267        let constraint = KeepConstraint::new();
1268        assert!(!constraint.has_constraint());
1269    }
1270
1271    #[test]
1272    fn test_keep_constraint_has_constraint_true_when_keep_together() {
1273        let mut constraint = KeepConstraint::new();
1274        constraint.keep_together = Keep::Always;
1275        assert!(constraint.has_constraint());
1276    }
1277
1278    #[test]
1279    fn test_keep_constraint_has_constraint_true_when_keep_with_next() {
1280        let mut constraint = KeepConstraint::new();
1281        constraint.keep_with_next = Keep::Integer(5);
1282        assert!(constraint.has_constraint());
1283    }
1284
1285    #[test]
1286    fn test_keep_constraint_has_constraint_true_when_keep_with_previous() {
1287        let mut constraint = KeepConstraint::new();
1288        constraint.keep_with_previous = Keep::Always;
1289        assert!(constraint.has_constraint());
1290    }
1291
1292    // ---- extract_traits span / xml:lang / role tests ----
1293
1294    #[test]
1295    fn test_extract_traits_xml_lang() {
1296        let mut props = PropertyList::new();
1297        props.set(
1298            PropertyId::XmlLang,
1299            PropertyValue::String(std::borrow::Cow::Borrowed("ja")),
1300        );
1301        let traits = extract_traits(&props);
1302        assert_eq!(traits.xml_lang, Some("ja".to_string()));
1303    }
1304
1305    #[test]
1306    fn test_extract_traits_role() {
1307        let mut props = PropertyList::new();
1308        props.set(
1309            PropertyId::Role,
1310            PropertyValue::String(std::borrow::Cow::Borrowed("Heading")),
1311        );
1312        let traits = extract_traits(&props);
1313        assert_eq!(traits.role, Some("Heading".to_string()));
1314    }
1315
1316    #[test]
1317    fn test_extract_traits_writing_mode_tb_rl() {
1318        let mut props = PropertyList::new();
1319        props.set(
1320            PropertyId::WritingMode,
1321            PropertyValue::String(std::borrow::Cow::Borrowed("tb-rl")),
1322        );
1323        let traits = extract_traits(&props);
1324        use crate::area::types::WritingMode;
1325        assert_eq!(traits.writing_mode, WritingMode::TbRl);
1326    }
1327
1328    #[test]
1329    fn test_extract_traits_writing_mode_lr_tb_default() {
1330        let props = PropertyList::new();
1331        let traits = extract_traits(&props);
1332        use crate::area::types::WritingMode;
1333        assert_eq!(traits.writing_mode, WritingMode::LrTb);
1334    }
1335
1336    #[test]
1337    fn test_extract_traits_direction_ltr_default() {
1338        let props = PropertyList::new();
1339        let traits = extract_traits(&props);
1340        use crate::area::types::Direction;
1341        assert_eq!(traits.direction, Direction::Ltr);
1342    }
1343
1344    #[test]
1345    fn test_extract_traits_span_all() {
1346        let mut props = PropertyList::new();
1347        props.set(
1348            PropertyId::Span,
1349            PropertyValue::String(std::borrow::Cow::Borrowed("all")),
1350        );
1351        let traits = extract_traits(&props);
1352        use crate::area::types::Span;
1353        assert_eq!(traits.span, Span::All);
1354    }
1355
1356    #[test]
1357    fn test_extract_traits_font_stretch_condensed() {
1358        let mut props = PropertyList::new();
1359        props.set(
1360            PropertyId::FontStretch,
1361            PropertyValue::String(std::borrow::Cow::Borrowed("condensed")),
1362        );
1363        let traits = extract_traits(&props);
1364        use crate::area::types::FontStretch;
1365        assert_eq!(traits.font_stretch, Some(FontStretch::Condensed));
1366    }
1367
1368    #[test]
1369    fn test_extract_traits_text_align_last_left() {
1370        let mut props = PropertyList::new();
1371        props.set(
1372            PropertyId::TextAlignLast,
1373            PropertyValue::String(std::borrow::Cow::Borrowed("left")),
1374        );
1375        let traits = extract_traits(&props);
1376        use crate::layout::inline::TextAlign;
1377        assert_eq!(traits.text_align_last, Some(TextAlign::Left));
1378    }
1379
1380    // ---- measure_text_width additional tests ----
1381
1382    #[test]
1383    fn test_measure_text_width_empty_string() {
1384        let w = measure_text_width("", Length::from_pt(12.0), None);
1385        assert_eq!(w, Length::ZERO);
1386    }
1387
1388    #[test]
1389    fn test_measure_text_width_digits() {
1390        // digits (0-9) are typically 0.5 factor
1391        let w = measure_text_width("5", Length::from_pt(12.0), None);
1392        assert!(w.to_pt() > 0.0);
1393    }
1394
1395    #[test]
1396    fn test_measure_text_width_punctuation() {
1397        // punctuation (. , ; :) typically has smaller factor
1398        let w_punct = measure_text_width(".", Length::from_pt(12.0), None);
1399        let w_normal = measure_text_width("A", Length::from_pt(12.0), None);
1400        assert!(w_punct.to_pt() > 0.0);
1401        assert!(w_punct <= w_normal);
1402    }
1403}