Skip to main content

oxidize_pdf/structure/
outline.rs

1//! Document outline (bookmarks) according to ISO 32000-1 Section 12.3.3
2
3use crate::graphics::Color;
4use crate::objects::{Array, Dictionary, Object, ObjectId};
5use crate::structure::destination::Destination;
6use std::collections::VecDeque;
7
8/// Outline item flags
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
10pub struct OutlineFlags {
11    /// Italic text
12    pub italic: bool,
13    /// Bold text
14    pub bold: bool,
15}
16
17impl OutlineFlags {
18    /// Convert to integer flags
19    #[allow(clippy::wrong_self_convention)]
20    pub fn to_int(&self) -> i64 {
21        let mut flags = 0;
22        if self.italic {
23            flags |= 1;
24        }
25        if self.bold {
26            flags |= 2;
27        }
28        flags
29    }
30}
31
32/// Outline item (bookmark)
33#[derive(Debug, Clone, PartialEq)]
34pub struct OutlineItem {
35    /// Item title
36    pub title: String,
37    /// Destination
38    pub destination: Option<Destination>,
39    /// Child items
40    pub children: Vec<OutlineItem>,
41    /// Text color
42    pub color: Option<Color>,
43    /// Text style flags
44    pub flags: OutlineFlags,
45    /// Whether item is open by default
46    pub open: bool,
47}
48
49impl OutlineItem {
50    /// Create new outline item
51    pub fn new(title: impl Into<String>) -> Self {
52        Self {
53            title: title.into(),
54            destination: None,
55            children: Vec::new(),
56            color: None,
57            flags: OutlineFlags::default(),
58            open: true,
59        }
60    }
61
62    /// Set destination
63    pub fn with_destination(mut self, dest: Destination) -> Self {
64        self.destination = Some(dest);
65        self
66    }
67
68    /// Add child item
69    pub fn add_child(&mut self, child: OutlineItem) {
70        self.children.push(child);
71    }
72
73    /// Set color
74    pub fn with_color(mut self, color: Color) -> Self {
75        self.color = Some(color);
76        self
77    }
78
79    /// Set bold
80    pub fn bold(mut self) -> Self {
81        self.flags.bold = true;
82        self
83    }
84
85    /// Set italic
86    pub fn italic(mut self) -> Self {
87        self.flags.italic = true;
88        self
89    }
90
91    /// Set closed by default
92    pub fn closed(mut self) -> Self {
93        self.open = false;
94        self
95    }
96
97    /// Count total items in subtree
98    pub fn count_all(&self) -> i64 {
99        let mut count = 1; // Self
100        for child in &self.children {
101            count += child.count_all();
102        }
103        count
104    }
105
106    /// Count visible items (respecting open/closed state)
107    pub fn count_visible(&self) -> i64 {
108        let mut count = 1; // Self
109        if self.open {
110            for child in &self.children {
111                count += child.count_visible();
112            }
113        }
114        count
115    }
116}
117
118/// Outline tree structure
119#[derive(Debug, Clone, PartialEq)]
120pub struct OutlineTree {
121    /// Root items
122    pub items: Vec<OutlineItem>,
123}
124
125impl Default for OutlineTree {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131impl OutlineTree {
132    /// Create new outline tree
133    pub fn new() -> Self {
134        Self { items: Vec::new() }
135    }
136
137    /// Add root item
138    pub fn add_item(&mut self, item: OutlineItem) {
139        self.items.push(item);
140    }
141
142    /// Get total item count
143    pub fn total_count(&self) -> i64 {
144        self.items.iter().map(|item| item.count_all()).sum()
145    }
146
147    /// Get visible item count
148    pub fn visible_count(&self) -> i64 {
149        self.items.iter().map(|item| item.count_visible()).sum()
150    }
151}
152
153/// Outline builder for creating outline hierarchy
154pub struct OutlineBuilder {
155    /// Current outline tree
156    tree: OutlineTree,
157    /// Stack for building hierarchy
158    stack: VecDeque<OutlineItem>,
159}
160
161impl Default for OutlineBuilder {
162    fn default() -> Self {
163        Self::new()
164    }
165}
166
167impl OutlineBuilder {
168    /// Create new builder
169    pub fn new() -> Self {
170        Self {
171            tree: OutlineTree::new(),
172            stack: VecDeque::new(),
173        }
174    }
175
176    /// Add item at current level
177    pub fn add_item(&mut self, item: OutlineItem) {
178        if let Some(parent) = self.stack.back_mut() {
179            parent.add_child(item);
180        } else {
181            self.tree.add_item(item);
182        }
183    }
184
185    /// Push item and make it current parent
186    pub fn push_item(&mut self, item: OutlineItem) {
187        self.stack.push_back(item);
188    }
189
190    /// Pop current parent and add to tree
191    pub fn pop_item(&mut self) {
192        if let Some(item) = self.stack.pop_back() {
193            if let Some(parent) = self.stack.back_mut() {
194                parent.add_child(item);
195            } else {
196                self.tree.add_item(item);
197            }
198        }
199    }
200
201    /// Build the outline tree
202    pub fn build(mut self) -> OutlineTree {
203        // Pop any remaining items
204        while !self.stack.is_empty() {
205            self.pop_item();
206        }
207        self.tree
208    }
209}
210
211/// Convert outline item to dictionary (for PDF generation)
212pub fn outline_item_to_dict(
213    item: &OutlineItem,
214    parent_ref: ObjectId,
215    first_ref: Option<ObjectId>,
216    last_ref: Option<ObjectId>,
217    prev_ref: Option<ObjectId>,
218    next_ref: Option<ObjectId>,
219) -> Dictionary {
220    let mut dict = Dictionary::new();
221
222    // Title
223    dict.set("Title", Object::String(item.title.clone()));
224
225    // Parent
226    dict.set("Parent", Object::Reference(parent_ref));
227
228    // Siblings
229    if let Some(prev) = prev_ref {
230        dict.set("Prev", Object::Reference(prev));
231    }
232    if let Some(next) = next_ref {
233        dict.set("Next", Object::Reference(next));
234    }
235
236    // Children
237    if !item.children.is_empty() {
238        if let Some(first) = first_ref {
239            dict.set("First", Object::Reference(first));
240        }
241        if let Some(last) = last_ref {
242            dict.set("Last", Object::Reference(last));
243        }
244
245        // Count (negative if closed)
246        let count = if item.open {
247            item.count_visible() - 1 // Exclude self
248        } else {
249            item.count_all() - 1 // For closed items, count all children
250        };
251        dict.set(
252            "Count",
253            Object::Integer(if item.open { count } else { -count }),
254        );
255    }
256
257    // Destination
258    if let Some(dest) = &item.destination {
259        dict.set("Dest", Object::Array(dest.to_array().into()));
260    }
261
262    // Color
263    if let Some(color) = &item.color {
264        let color_array = match color {
265            Color::Rgb(r, g, b) => {
266                Array::from(vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)])
267            }
268            Color::Gray(g) => {
269                Array::from(vec![Object::Real(*g), Object::Real(*g), Object::Real(*g)])
270            }
271            Color::Cmyk(c, m, y, k) => {
272                // Convert CMYK to RGB approximation for outline color
273                let r = (1.0 - c) * (1.0 - k);
274                let g = (1.0 - m) * (1.0 - k);
275                let b = (1.0 - y) * (1.0 - k);
276                Array::from(vec![Object::Real(r), Object::Real(g), Object::Real(b)])
277            }
278        };
279        dict.set("C", Object::Array(color_array.into()));
280    }
281
282    // Flags
283    let flags = item.flags.to_int();
284    if flags != 0 {
285        dict.set("F", Object::Integer(flags));
286    }
287
288    dict
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use crate::structure::destination::PageDestination;
295
296    #[test]
297    fn test_outline_item_new() {
298        let item = OutlineItem::new("Chapter 1");
299        assert_eq!(item.title, "Chapter 1");
300        assert!(item.destination.is_none());
301        assert!(item.children.is_empty());
302        assert!(item.color.is_none());
303        assert!(!item.flags.bold);
304        assert!(!item.flags.italic);
305        assert!(item.open);
306    }
307
308    #[test]
309    fn test_outline_item_builder() {
310        let dest = Destination::fit(PageDestination::PageNumber(0));
311        let item = OutlineItem::new("Bold Chapter")
312            .with_destination(dest)
313            .with_color(Color::rgb(1.0, 0.0, 0.0))
314            .bold()
315            .closed();
316
317        assert!(item.destination.is_some());
318        assert!(item.color.is_some());
319        assert!(item.flags.bold);
320        assert!(!item.open);
321    }
322
323    #[test]
324    fn test_outline_hierarchy() {
325        let mut chapter1 = OutlineItem::new("Chapter 1");
326        chapter1.add_child(OutlineItem::new("Section 1.1"));
327        chapter1.add_child(OutlineItem::new("Section 1.2"));
328
329        assert_eq!(chapter1.children.len(), 2);
330        assert_eq!(chapter1.count_all(), 3); // Chapter + 2 sections
331    }
332
333    #[test]
334    fn test_outline_count() {
335        let mut root = OutlineItem::new("Book");
336
337        let mut ch1 = OutlineItem::new("Chapter 1");
338        ch1.add_child(OutlineItem::new("Section 1.1"));
339        ch1.add_child(OutlineItem::new("Section 1.2"));
340
341        let mut ch2 = OutlineItem::new("Chapter 2").closed();
342        ch2.add_child(OutlineItem::new("Section 2.1"));
343
344        root.add_child(ch1);
345        root.add_child(ch2);
346
347        assert_eq!(root.count_all(), 6); // Book + 2 chapters + 3 sections
348        assert_eq!(root.count_visible(), 5); // Ch2's child hidden
349    }
350
351    #[test]
352    fn test_outline_builder() {
353        let mut builder = OutlineBuilder::new();
354
355        // Add root items
356        builder.add_item(OutlineItem::new("Preface"));
357
358        // Add chapter with sections
359        builder.push_item(OutlineItem::new("Chapter 1"));
360        builder.add_item(OutlineItem::new("Section 1.1"));
361        builder.add_item(OutlineItem::new("Section 1.2"));
362        builder.pop_item();
363
364        builder.add_item(OutlineItem::new("Chapter 2"));
365
366        let tree = builder.build();
367        assert_eq!(tree.items.len(), 3); // Preface, Ch1, Ch2
368        assert_eq!(tree.total_count(), 5); // All items
369    }
370
371    #[test]
372    fn test_outline_flags() {
373        let flags = OutlineFlags {
374            italic: true,
375            bold: true,
376        };
377        assert_eq!(flags.to_int(), 3);
378
379        let flags2 = OutlineFlags {
380            italic: true,
381            bold: false,
382        };
383        assert_eq!(flags2.to_int(), 1);
384
385        let flags3 = OutlineFlags::default();
386        assert_eq!(flags3.to_int(), 0);
387    }
388
389    #[test]
390    fn test_outline_flags_debug_clone_default() {
391        let flags = OutlineFlags {
392            italic: true,
393            bold: false,
394        };
395        let debug_str = format!("{flags:?}");
396        assert!(debug_str.contains("OutlineFlags"));
397        assert!(debug_str.contains("italic: true"));
398        assert!(debug_str.contains("bold: false"));
399
400        let cloned = flags;
401        assert_eq!(cloned.italic, flags.italic);
402        assert_eq!(cloned.bold, flags.bold);
403
404        let default_flags = OutlineFlags::default();
405        assert!(!default_flags.italic);
406        assert!(!default_flags.bold);
407    }
408
409    #[test]
410    fn test_outline_item_italic() {
411        let item = OutlineItem::new("Italic Text").italic();
412        assert!(item.flags.italic);
413        assert!(!item.flags.bold);
414    }
415
416    #[test]
417    fn test_outline_item_bold_italic() {
418        let item = OutlineItem::new("Bold Italic").bold().italic();
419        assert!(item.flags.italic);
420        assert!(item.flags.bold);
421        assert_eq!(item.flags.to_int(), 3);
422    }
423
424    #[test]
425    fn test_outline_item_with_complex_destination() {
426        use crate::geometry::{Point, Rectangle};
427
428        let dest = Destination::fit_r(
429            PageDestination::PageNumber(5),
430            Rectangle::new(Point::new(100.0, 200.0), Point::new(300.0, 400.0)),
431        );
432        let item = OutlineItem::new("Complex Destination").with_destination(dest);
433
434        assert!(item.destination.is_some());
435        match &item.destination {
436            Some(d) => match &d.page {
437                PageDestination::PageNumber(n) => assert_eq!(*n, 5),
438                _ => panic!("Wrong destination type"),
439            },
440            None => panic!("Destination should be set"),
441        }
442    }
443
444    #[test]
445    fn test_outline_item_with_different_colors() {
446        let rgb_item = OutlineItem::new("RGB Color").with_color(Color::rgb(0.5, 0.7, 1.0));
447        assert!(rgb_item.color.is_some());
448
449        let gray_item = OutlineItem::new("Gray Color").with_color(Color::gray(0.5));
450        assert!(gray_item.color.is_some());
451
452        let cmyk_item = OutlineItem::new("CMYK Color").with_color(Color::cmyk(0.1, 0.2, 0.3, 0.4));
453        assert!(cmyk_item.color.is_some());
454    }
455
456    #[test]
457    fn test_outline_item_debug_clone() {
458        let item = OutlineItem::new("Test Item")
459            .bold()
460            .with_color(Color::rgb(1.0, 0.0, 0.0));
461
462        let debug_str = format!("{item:?}");
463        assert!(debug_str.contains("OutlineItem"));
464        assert!(debug_str.contains("Test Item"));
465
466        let cloned = item.clone();
467        assert_eq!(cloned.title, item.title);
468        assert_eq!(cloned.flags.bold, item.flags.bold);
469        assert_eq!(cloned.open, item.open);
470    }
471
472    #[test]
473    fn test_outline_tree_default() {
474        let tree = OutlineTree::default();
475        assert!(tree.items.is_empty());
476        assert_eq!(tree.total_count(), 0);
477        assert_eq!(tree.visible_count(), 0);
478    }
479
480    #[test]
481    fn test_outline_tree_add_multiple_items() {
482        let mut tree = OutlineTree::new();
483
484        tree.add_item(OutlineItem::new("First"));
485        tree.add_item(OutlineItem::new("Second"));
486        tree.add_item(OutlineItem::new("Third"));
487
488        assert_eq!(tree.items.len(), 3);
489        assert_eq!(tree.total_count(), 3);
490        assert_eq!(tree.visible_count(), 3);
491    }
492
493    #[test]
494    fn test_outline_tree_with_closed_items() {
495        let mut tree = OutlineTree::new();
496
497        let mut chapter = OutlineItem::new("Chapter").closed();
498        chapter.add_child(OutlineItem::new("Hidden Section 1"));
499        chapter.add_child(OutlineItem::new("Hidden Section 2"));
500
501        tree.add_item(chapter);
502        tree.add_item(OutlineItem::new("Visible Item"));
503
504        assert_eq!(tree.total_count(), 4); // All items
505        assert_eq!(tree.visible_count(), 2); // Only chapter and visible item
506    }
507
508    #[test]
509    fn test_outline_builder_default() {
510        let builder = OutlineBuilder::default();
511        let tree = builder.build();
512        assert!(tree.items.is_empty());
513    }
514
515    #[test]
516    fn test_outline_builder_nested_structure() {
517        let mut builder = OutlineBuilder::new();
518
519        // Build a complex nested structure
520        builder.push_item(OutlineItem::new("Part I"));
521        builder.push_item(OutlineItem::new("Chapter 1"));
522        builder.add_item(OutlineItem::new("Section 1.1"));
523        builder.add_item(OutlineItem::new("Section 1.2"));
524        builder.pop_item(); // Pop Chapter 1
525        builder.push_item(OutlineItem::new("Chapter 2"));
526        builder.add_item(OutlineItem::new("Section 2.1"));
527        builder.pop_item(); // Pop Chapter 2
528        builder.pop_item(); // Pop Part I
529
530        builder.add_item(OutlineItem::new("Part II"));
531
532        let tree = builder.build();
533        assert_eq!(tree.items.len(), 2); // Part I and Part II
534        assert_eq!(tree.total_count(), 7); // All items
535    }
536
537    #[test]
538    fn test_outline_builder_auto_pop() {
539        let mut builder = OutlineBuilder::new();
540
541        // Push items without popping - should auto-pop on build
542        builder.push_item(OutlineItem::new("Root"));
543        builder.push_item(OutlineItem::new("Child"));
544        builder.add_item(OutlineItem::new("Grandchild"));
545
546        let tree = builder.build();
547        assert_eq!(tree.items.len(), 1); // Only root
548        assert_eq!(tree.total_count(), 3); // Root + Child + Grandchild
549    }
550
551    #[test]
552    fn test_outline_item_count_deep_hierarchy() {
553        let mut root = OutlineItem::new("Root");
554
555        let mut level1 = OutlineItem::new("Level 1");
556        let mut level2 = OutlineItem::new("Level 2");
557        let mut level3 = OutlineItem::new("Level 3");
558        level3.add_child(OutlineItem::new("Level 4"));
559        level2.add_child(level3);
560        level1.add_child(level2);
561        root.add_child(level1);
562
563        assert_eq!(root.count_all(), 5); // All 5 levels
564        assert_eq!(root.count_visible(), 5); // All visible
565
566        // Close level2 - should hide level 3 and 4
567        root.children[0].children[0].open = false;
568        assert_eq!(root.count_visible(), 3); // Root, Level1, Level2 (closed)
569    }
570
571    #[test]
572    fn test_outline_item_to_dict_basic() {
573        let item = OutlineItem::new("Test Title");
574        let parent_ref = ObjectId::new(1, 0);
575
576        let dict = outline_item_to_dict(&item, parent_ref, None, None, None, None);
577
578        assert_eq!(
579            dict.get("Title"),
580            Some(&Object::String("Test Title".to_string()))
581        );
582        assert_eq!(dict.get("Parent"), Some(&Object::Reference(parent_ref)));
583        assert!(dict.get("Prev").is_none());
584        assert!(dict.get("Next").is_none());
585        assert!(dict.get("First").is_none());
586        assert!(dict.get("Last").is_none());
587    }
588
589    #[test]
590    fn test_outline_item_to_dict_with_siblings() {
591        let item = OutlineItem::new("Middle Child");
592        let parent_ref = ObjectId::new(1, 0);
593        let prev_ref = Some(ObjectId::new(2, 0));
594        let next_ref = Some(ObjectId::new(3, 0));
595
596        let dict = outline_item_to_dict(&item, parent_ref, None, None, prev_ref, next_ref);
597
598        assert_eq!(
599            dict.get("Prev"),
600            Some(&Object::Reference(ObjectId::new(2, 0)))
601        );
602        assert_eq!(
603            dict.get("Next"),
604            Some(&Object::Reference(ObjectId::new(3, 0)))
605        );
606    }
607
608    #[test]
609    fn test_outline_item_to_dict_with_children() {
610        let mut item = OutlineItem::new("Parent");
611        item.add_child(OutlineItem::new("Child 1"));
612        item.add_child(OutlineItem::new("Child 2"));
613
614        let parent_ref = ObjectId::new(1, 0);
615        let first_ref = Some(ObjectId::new(10, 0));
616        let last_ref = Some(ObjectId::new(11, 0));
617
618        let dict = outline_item_to_dict(&item, parent_ref, first_ref, last_ref, None, None);
619
620        assert_eq!(
621            dict.get("First"),
622            Some(&Object::Reference(ObjectId::new(10, 0)))
623        );
624        assert_eq!(
625            dict.get("Last"),
626            Some(&Object::Reference(ObjectId::new(11, 0)))
627        );
628        assert_eq!(dict.get("Count"), Some(&Object::Integer(2))); // 2 visible children
629    }
630
631    #[test]
632    fn test_outline_item_to_dict_closed_with_children() {
633        let mut item = OutlineItem::new("Closed Parent").closed();
634        item.add_child(OutlineItem::new("Hidden 1"));
635        item.add_child(OutlineItem::new("Hidden 2"));
636        item.add_child(OutlineItem::new("Hidden 3"));
637
638        let dict = outline_item_to_dict(
639            &item,
640            ObjectId::new(1, 0),
641            Some(ObjectId::new(10, 0)),
642            Some(ObjectId::new(12, 0)),
643            None,
644            None,
645        );
646
647        // Count should be negative for closed items
648        assert_eq!(dict.get("Count"), Some(&Object::Integer(-3)));
649    }
650
651    #[test]
652    fn test_outline_item_to_dict_with_destination() {
653        let dest = Destination::xyz(
654            PageDestination::PageNumber(5),
655            Some(100.0),
656            Some(200.0),
657            Some(1.5),
658        );
659        let item = OutlineItem::new("With Destination").with_destination(dest);
660
661        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
662
663        assert!(dict.get("Dest").is_some());
664        match dict.get("Dest") {
665            Some(Object::Array(arr)) => {
666                // Should be the destination array
667                assert!(!arr.is_empty());
668            }
669            _ => panic!("Dest should be an array"),
670        }
671    }
672
673    #[test]
674    fn test_outline_item_to_dict_with_color_rgb() {
675        let item = OutlineItem::new("Red Item").with_color(Color::rgb(1.0, 0.0, 0.0));
676
677        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
678
679        match dict.get("C") {
680            Some(Object::Array(arr)) => {
681                assert_eq!(arr.len(), 3);
682                assert_eq!(arr.first(), Some(&Object::Real(1.0)));
683                assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
684                assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
685            }
686            _ => panic!("C should be an array"),
687        }
688    }
689
690    #[test]
691    fn test_outline_item_to_dict_with_color_gray() {
692        let item = OutlineItem::new("Gray Item").with_color(Color::gray(0.5));
693
694        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
695
696        match dict.get("C") {
697            Some(Object::Array(arr)) => {
698                assert_eq!(arr.len(), 3);
699                // Gray color should be converted to RGB with equal components
700                assert_eq!(arr.first(), Some(&Object::Real(0.5)));
701                assert_eq!(arr.get(1), Some(&Object::Real(0.5)));
702                assert_eq!(arr.get(2), Some(&Object::Real(0.5)));
703            }
704            _ => panic!("C should be an array"),
705        }
706    }
707
708    #[test]
709    fn test_outline_item_to_dict_with_color_cmyk() {
710        let item = OutlineItem::new("CMYK Item").with_color(Color::cmyk(0.0, 1.0, 1.0, 0.0));
711
712        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
713
714        match dict.get("C") {
715            Some(Object::Array(arr)) => {
716                assert_eq!(arr.len(), 3);
717                // CMYK (0,1,1,0) should convert to RGB (1,0,0) - red
718                assert_eq!(arr.first(), Some(&Object::Real(1.0)));
719                assert_eq!(arr.get(1), Some(&Object::Real(0.0)));
720                assert_eq!(arr.get(2), Some(&Object::Real(0.0)));
721            }
722            _ => panic!("C should be an array"),
723        }
724    }
725
726    #[test]
727    fn test_outline_item_to_dict_with_flags() {
728        let item = OutlineItem::new("Styled Item").bold().italic();
729
730        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
731
732        assert_eq!(dict.get("F"), Some(&Object::Integer(3))); // Both bold and italic
733    }
734
735    #[test]
736    fn test_outline_item_to_dict_no_flags() {
737        let item = OutlineItem::new("Plain Item");
738
739        let dict = outline_item_to_dict(&item, ObjectId::new(1, 0), None, None, None, None);
740
741        // F field should not be present when flags are 0
742        assert!(dict.get("F").is_none());
743    }
744
745    #[test]
746    fn test_outline_tree_empty_counts() {
747        let tree = OutlineTree::new();
748        assert_eq!(tree.total_count(), 0);
749        assert_eq!(tree.visible_count(), 0);
750    }
751
752    #[test]
753    fn test_outline_builder_empty_pop() {
754        let mut builder = OutlineBuilder::new();
755        // Popping from empty stack should not panic
756        builder.pop_item();
757        let tree = builder.build();
758        assert!(tree.items.is_empty());
759    }
760
761    #[test]
762    fn test_outline_complex_visibility() {
763        let mut root = OutlineItem::new("Book");
764
765        let mut part1 = OutlineItem::new("Part 1"); // open
766        let mut ch1 = OutlineItem::new("Chapter 1").closed();
767        ch1.add_child(OutlineItem::new("Section 1.1"));
768        ch1.add_child(OutlineItem::new("Section 1.2"));
769        part1.add_child(ch1);
770
771        let mut ch2 = OutlineItem::new("Chapter 2"); // open
772        ch2.add_child(OutlineItem::new("Section 2.1"));
773        part1.add_child(ch2);
774
775        root.add_child(part1);
776
777        // Structure:
778        // Book (open)
779        //   Part 1 (open)
780        //     Chapter 1 (closed)
781        //       Section 1.1 (hidden)
782        //       Section 1.2 (hidden)
783        //     Chapter 2 (open)
784        //       Section 2.1 (visible)
785
786        assert_eq!(root.count_all(), 7); // All items
787        assert_eq!(root.count_visible(), 5); // Hidden: Section 1.1, 1.2
788    }
789}