oxidize_pdf/text/
list.rs

1//! List rendering support for PDF documents
2//!
3//! This module provides ordered and unordered list functionality
4//! with advanced formatting options including:
5//! - Multiple numbering styles (decimal, alphabetic, roman)
6//! - Custom bullet styles and symbols
7//! - Nested lists with automatic indentation
8//! - Text wrapping for long items
9//! - Custom spacing and alignment
10//! - Rich formatting options
11
12use crate::error::PdfError;
13use crate::graphics::{Color, GraphicsContext};
14use crate::text::{Font, TextAlign};
15
16/// List style for ordered lists
17#[derive(Debug, Clone, Copy, PartialEq)]
18pub enum OrderedListStyle {
19    /// Arabic numerals (1, 2, 3, ...)
20    Decimal,
21    /// Lowercase letters (a, b, c, ...)
22    LowerAlpha,
23    /// Uppercase letters (A, B, C, ...)
24    UpperAlpha,
25    /// Lowercase Roman numerals (i, ii, iii, ...)
26    LowerRoman,
27    /// Uppercase Roman numerals (I, II, III, ...)
28    UpperRoman,
29    /// Decimal with leading zeros (01, 02, 03, ...)
30    DecimalLeadingZero,
31    /// Greek lowercase letters (α, β, γ, ...)
32    GreekLower,
33    /// Greek uppercase letters (Α, Β, Γ, ...)
34    GreekUpper,
35    /// Hebrew letters (א, ב, ג, ...)
36    Hebrew,
37    /// Japanese hiragana (あ, い, う, ...)
38    Hiragana,
39    /// Japanese katakana (ア, イ, ウ, ...)
40    Katakana,
41    /// Chinese numbers (一, 二, 三, ...)
42    ChineseSimplified,
43}
44
45/// Bullet style for unordered lists
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum BulletStyle {
48    /// Filled circle (•)
49    Disc,
50    /// Empty circle (○)
51    Circle,
52    /// Filled square (■)
53    Square,
54    /// Dash (-)
55    Dash,
56    /// Custom character
57    Custom(char),
58}
59
60/// Combined list style enum
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub enum ListStyle {
63    /// Ordered list with specific style
64    Ordered(OrderedListStyle),
65    /// Unordered list with specific bullet
66    Unordered(BulletStyle),
67}
68
69/// Options for list rendering
70#[derive(Debug, Clone)]
71pub struct ListOptions {
72    /// Font for list text
73    pub font: Font,
74    /// Font size in points
75    pub font_size: f64,
76    /// Text color
77    pub text_color: Color,
78    /// Indentation per level in points
79    pub indent: f64,
80    /// Line spacing multiplier
81    pub line_spacing: f64,
82    /// Space between bullet/number and text
83    pub marker_spacing: f64,
84    /// Maximum width for text wrapping (None = no wrapping)
85    pub max_width: Option<f64>,
86    /// Text alignment for wrapped lines
87    pub text_align: TextAlign,
88    /// Font for markers (bullets/numbers)
89    pub marker_font: Font,
90    /// Marker color (None = same as text)
91    pub marker_color: Option<Color>,
92    /// Paragraph spacing after each item
93    pub paragraph_spacing: f64,
94    /// Whether to draw a line after each item
95    pub draw_separator: bool,
96    /// Separator line color
97    pub separator_color: Color,
98    /// Separator line width
99    pub separator_width: f64,
100    /// Custom prefix before markers (e.g., "Chapter ", "Section ")
101    pub marker_prefix: String,
102    /// Custom suffix after markers (e.g., ")", "]", ":")
103    pub marker_suffix: String,
104}
105
106impl Default for ListOptions {
107    fn default() -> Self {
108        Self {
109            font: Font::Helvetica,
110            font_size: 10.0,
111            text_color: Color::black(),
112            indent: 20.0,
113            line_spacing: 1.2,
114            marker_spacing: 10.0,
115            max_width: None,
116            text_align: TextAlign::Left,
117            marker_font: Font::Helvetica,
118            marker_color: None,
119            paragraph_spacing: 0.0,
120            draw_separator: false,
121            separator_color: Color::gray(0.8),
122            separator_width: 0.5,
123            marker_prefix: String::new(),
124            marker_suffix: ".".to_string(),
125        }
126    }
127}
128
129/// Represents an ordered list
130#[derive(Debug, Clone)]
131pub struct OrderedList {
132    items: Vec<ListItem>,
133    style: OrderedListStyle,
134    start_number: u32,
135    options: ListOptions,
136    position: (f64, f64),
137}
138
139/// Represents an unordered list
140#[derive(Debug, Clone)]
141pub struct UnorderedList {
142    items: Vec<ListItem>,
143    bullet_style: BulletStyle,
144    options: ListOptions,
145    position: (f64, f64),
146}
147
148/// A single list item that can contain text and nested lists
149#[derive(Debug, Clone)]
150pub struct ListItem {
151    text: String,
152    children: Vec<ListElement>,
153}
154
155/// Element that can be in a list (for nested lists)
156#[derive(Debug, Clone)]
157pub enum ListElement {
158    Ordered(OrderedList),
159    Unordered(UnorderedList),
160}
161
162impl OrderedList {
163    /// Create a new ordered list
164    pub fn new(style: OrderedListStyle) -> Self {
165        Self {
166            items: Vec::new(),
167            style,
168            start_number: 1,
169            options: ListOptions::default(),
170            position: (0.0, 0.0),
171        }
172    }
173
174    /// Set the starting number
175    pub fn set_start_number(&mut self, start: u32) -> &mut Self {
176        self.start_number = start;
177        self
178    }
179
180    /// Set list options
181    pub fn set_options(&mut self, options: ListOptions) -> &mut Self {
182        self.options = options;
183        self
184    }
185
186    /// Set list position
187    pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
188        self.position = (x, y);
189        self
190    }
191
192    /// Add a simple text item
193    pub fn add_item(&mut self, text: String) -> &mut Self {
194        self.items.push(ListItem {
195            text,
196            children: Vec::new(),
197        });
198        self
199    }
200
201    /// Add an item with nested lists
202    pub fn add_item_with_children(
203        &mut self,
204        text: String,
205        children: Vec<ListElement>,
206    ) -> &mut Self {
207        self.items.push(ListItem { text, children });
208        self
209    }
210
211    /// Generate the marker for a given index
212    fn generate_marker(&self, index: usize) -> String {
213        let number = self.start_number + index as u32;
214        let marker_core = match self.style {
215            OrderedListStyle::Decimal => format!("{number}"),
216            OrderedListStyle::DecimalLeadingZero => format!("{number:02}"),
217            OrderedListStyle::LowerAlpha => {
218                let letter = char::from_u32('a' as u32 + (number - 1) % 26).unwrap_or('?');
219                format!("{letter}")
220            }
221            OrderedListStyle::UpperAlpha => {
222                let letter = char::from_u32('A' as u32 + (number - 1) % 26).unwrap_or('?');
223                format!("{letter}")
224            }
225            OrderedListStyle::LowerRoman => to_roman(number).to_lowercase(),
226            OrderedListStyle::UpperRoman => to_roman(number),
227            OrderedListStyle::GreekLower => get_greek_letter(number, false),
228            OrderedListStyle::GreekUpper => get_greek_letter(number, true),
229            OrderedListStyle::Hebrew => get_hebrew_letter(number),
230            OrderedListStyle::Hiragana => get_hiragana_letter(number),
231            OrderedListStyle::Katakana => get_katakana_letter(number),
232            OrderedListStyle::ChineseSimplified => get_chinese_number(number),
233        };
234
235        format!(
236            "{}{}{}",
237            self.options.marker_prefix, marker_core, self.options.marker_suffix
238        )
239    }
240
241    /// Calculate the total height of the list
242    pub fn get_height(&self) -> f64 {
243        self.calculate_height_recursive(0)
244    }
245
246    fn calculate_height_recursive(&self, _level: usize) -> f64 {
247        let mut height = 0.0;
248        for item in &self.items {
249            height += self.options.font_size * self.options.line_spacing;
250            for child in &item.children {
251                height += match child {
252                    ListElement::Ordered(list) => list.calculate_height_recursive(_level + 1),
253                    ListElement::Unordered(list) => list.calculate_height_recursive(_level + 1),
254                };
255            }
256        }
257        height
258    }
259
260    /// Render the list to a graphics context
261    pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
262        let (x, y) = self.position;
263        self.render_recursive(graphics, x, y, 0)?;
264        Ok(())
265    }
266
267    fn render_recursive(
268        &self,
269        graphics: &mut GraphicsContext,
270        x: f64,
271        mut y: f64,
272        level: usize,
273    ) -> Result<f64, PdfError> {
274        let indent = x + (level as f64 * self.options.indent);
275
276        for (index, item) in self.items.iter().enumerate() {
277            // Draw marker
278            let marker = self.generate_marker(index);
279            graphics.save_state();
280            graphics.set_font(self.options.marker_font.clone(), self.options.font_size);
281            let marker_color = self.options.marker_color.unwrap_or(self.options.text_color);
282            graphics.set_fill_color(marker_color);
283            graphics.begin_text();
284            graphics.set_text_position(indent, y);
285            graphics.show_text(&marker)?;
286            graphics.end_text();
287            graphics.restore_state();
288
289            // Draw text (with wrapping support)
290            let text_x =
291                indent + self.calculate_marker_width(&marker) + self.options.marker_spacing;
292
293            let text_lines = if let Some(max_width) = self.options.max_width {
294                let available_width = max_width - text_x;
295                self.wrap_text(&item.text, available_width)
296            } else {
297                vec![item.text.clone()]
298            };
299
300            // Draw each line of text
301            let mut line_y = y;
302            for (line_index, line) in text_lines.iter().enumerate() {
303                graphics.save_state();
304                graphics.set_font(self.options.font.clone(), self.options.font_size);
305                graphics.set_fill_color(self.options.text_color);
306                graphics.begin_text();
307
308                // For wrapped lines (not the first), add extra indent
309                let line_x = if line_index == 0 {
310                    text_x
311                } else {
312                    text_x + self.options.font_size // Additional indent for wrapped lines
313                };
314
315                graphics.set_text_position(line_x, line_y);
316                graphics.show_text(line)?;
317                graphics.end_text();
318                graphics.restore_state();
319
320                if line_index < text_lines.len() - 1 {
321                    line_y += self.options.font_size * self.options.line_spacing;
322                }
323            }
324
325            y = line_y;
326
327            y +=
328                self.options.font_size * self.options.line_spacing + self.options.paragraph_spacing;
329
330            // Draw separator if enabled
331            if self.options.draw_separator && index < self.items.len() - 1 {
332                graphics.save_state();
333                graphics.set_stroke_color(self.options.separator_color);
334                graphics.set_line_width(self.options.separator_width);
335                graphics.move_to(indent, y + 2.0);
336                graphics.line_to(
337                    indent + (self.options.max_width.unwrap_or(500.0) - indent),
338                    y + 2.0,
339                );
340                graphics.stroke();
341                graphics.restore_state();
342                y += 5.0;
343            }
344
345            // Render children
346            for child in &item.children {
347                y = match child {
348                    ListElement::Ordered(list) => {
349                        let mut child_list = list.clone();
350                        child_list.options = self.options.clone();
351                        child_list.render_recursive(graphics, x, y, level + 1)?
352                    }
353                    ListElement::Unordered(list) => {
354                        let mut child_list = list.clone();
355                        child_list.options = self.options.clone();
356                        child_list.render_recursive(graphics, x, y, level + 1)?
357                    }
358                };
359            }
360        }
361
362        Ok(y)
363    }
364
365    fn calculate_marker_width(&self, marker: &str) -> f64 {
366        // Simple approximation: average character width * marker length
367        marker.len() as f64 * self.options.font_size * 0.5
368    }
369
370    /// Wrap text to fit within the given width
371    fn wrap_text(&self, text: &str, max_width: f64) -> Vec<String> {
372        // Simple character-based wrapping
373        // In a real implementation, this would use proper font metrics
374        let avg_char_width = self.options.font_size * 0.5;
375        let chars_per_line = (max_width / avg_char_width) as usize;
376
377        if chars_per_line == 0 || text.len() <= chars_per_line {
378            return vec![text.to_string()];
379        }
380
381        let mut lines = Vec::new();
382        let words: Vec<&str> = text.split_whitespace().collect();
383        let mut current_line = String::new();
384
385        for word in words {
386            let test_line = if current_line.is_empty() {
387                word.to_string()
388            } else {
389                format!("{current_line} {word}")
390            };
391
392            if test_line.len() <= chars_per_line {
393                current_line = test_line;
394            } else {
395                if !current_line.is_empty() {
396                    lines.push(current_line);
397                }
398                current_line = word.to_string();
399            }
400        }
401
402        if !current_line.is_empty() {
403            lines.push(current_line);
404        }
405
406        lines
407    }
408}
409
410impl UnorderedList {
411    /// Create a new unordered list
412    pub fn new(bullet_style: BulletStyle) -> Self {
413        Self {
414            items: Vec::new(),
415            bullet_style,
416            options: ListOptions::default(),
417            position: (0.0, 0.0),
418        }
419    }
420
421    /// Set list options
422    pub fn set_options(&mut self, options: ListOptions) -> &mut Self {
423        self.options = options;
424        self
425    }
426
427    /// Set list position
428    pub fn set_position(&mut self, x: f64, y: f64) -> &mut Self {
429        self.position = (x, y);
430        self
431    }
432
433    /// Add a simple text item
434    pub fn add_item(&mut self, text: String) -> &mut Self {
435        self.items.push(ListItem {
436            text,
437            children: Vec::new(),
438        });
439        self
440    }
441
442    /// Add an item with nested lists
443    pub fn add_item_with_children(
444        &mut self,
445        text: String,
446        children: Vec<ListElement>,
447    ) -> &mut Self {
448        self.items.push(ListItem { text, children });
449        self
450    }
451
452    /// Get the bullet character
453    fn get_bullet_char(&self) -> &str {
454        match self.bullet_style {
455            BulletStyle::Disc => "•",
456            BulletStyle::Circle => "○",
457            BulletStyle::Square => "■",
458            BulletStyle::Dash => "-",
459            BulletStyle::Custom(ch) => {
460                // This is a bit hacky but works for single characters
461                match ch {
462                    '→' => "→",
463                    '▸' => "▸",
464                    '▹' => "▹",
465                    '★' => "★",
466                    '☆' => "☆",
467                    _ => "•", // Fallback
468                }
469            }
470        }
471    }
472
473    /// Calculate the total height of the list
474    pub fn get_height(&self) -> f64 {
475        self.calculate_height_recursive(0)
476    }
477
478    fn calculate_height_recursive(&self, _level: usize) -> f64 {
479        let mut height = 0.0;
480        for item in &self.items {
481            height += self.options.font_size * self.options.line_spacing;
482            for child in &item.children {
483                height += match child {
484                    ListElement::Ordered(list) => list.calculate_height_recursive(_level + 1),
485                    ListElement::Unordered(list) => list.calculate_height_recursive(_level + 1),
486                };
487            }
488        }
489        height
490    }
491
492    /// Render the list to a graphics context
493    pub fn render(&self, graphics: &mut GraphicsContext) -> Result<(), PdfError> {
494        let (x, y) = self.position;
495        self.render_recursive(graphics, x, y, 0)?;
496        Ok(())
497    }
498
499    fn render_recursive(
500        &self,
501        graphics: &mut GraphicsContext,
502        x: f64,
503        mut y: f64,
504        level: usize,
505    ) -> Result<f64, PdfError> {
506        let indent = x + (level as f64 * self.options.indent);
507        let bullet = self.get_bullet_char();
508
509        for (index, item) in self.items.iter().enumerate() {
510            // Draw bullet
511            graphics.save_state();
512            graphics.set_font(self.options.marker_font.clone(), self.options.font_size);
513            let marker_color = self.options.marker_color.unwrap_or(self.options.text_color);
514            graphics.set_fill_color(marker_color);
515            graphics.begin_text();
516            graphics.set_text_position(indent, y);
517            graphics.show_text(bullet)?;
518            graphics.end_text();
519            graphics.restore_state();
520
521            // Draw text (with wrapping support)
522            let text_x = indent + self.options.font_size + self.options.marker_spacing;
523
524            let text_lines = if let Some(max_width) = self.options.max_width {
525                let available_width = max_width - text_x;
526                self.wrap_text(&item.text, available_width)
527            } else {
528                vec![item.text.clone()]
529            };
530
531            // Draw each line of text
532            let mut line_y = y;
533            for (line_index, line) in text_lines.iter().enumerate() {
534                graphics.save_state();
535                graphics.set_font(self.options.font.clone(), self.options.font_size);
536                graphics.set_fill_color(self.options.text_color);
537                graphics.begin_text();
538
539                // For wrapped lines (not the first), add extra indent
540                let line_x = if line_index == 0 {
541                    text_x
542                } else {
543                    text_x + self.options.font_size // Additional indent for wrapped lines
544                };
545
546                graphics.set_text_position(line_x, line_y);
547                graphics.show_text(line)?;
548                graphics.end_text();
549                graphics.restore_state();
550
551                if line_index < text_lines.len() - 1 {
552                    line_y += self.options.font_size * self.options.line_spacing;
553                }
554            }
555
556            y = line_y;
557
558            y +=
559                self.options.font_size * self.options.line_spacing + self.options.paragraph_spacing;
560
561            // Draw separator if enabled
562            if self.options.draw_separator && (index < self.items.len() - 1) {
563                graphics.save_state();
564                graphics.set_stroke_color(self.options.separator_color);
565                graphics.set_line_width(self.options.separator_width);
566                graphics.move_to(indent, y + 2.0);
567                graphics.line_to(
568                    indent + (self.options.max_width.unwrap_or(500.0) - indent),
569                    y + 2.0,
570                );
571                graphics.stroke();
572                graphics.restore_state();
573                y += 5.0;
574            }
575
576            // Render children
577            for child in &item.children {
578                y = match child {
579                    ListElement::Ordered(list) => {
580                        let mut child_list = list.clone();
581                        child_list.options = self.options.clone();
582                        child_list.render_recursive(graphics, x, y, level + 1)?
583                    }
584                    ListElement::Unordered(list) => {
585                        let mut child_list = list.clone();
586                        child_list.options = self.options.clone();
587                        child_list.render_recursive(graphics, x, y, level + 1)?
588                    }
589                };
590            }
591        }
592
593        Ok(y)
594    }
595
596    /// Wrap text to fit within the given width
597    fn wrap_text(&self, text: &str, max_width: f64) -> Vec<String> {
598        // Simple character-based wrapping
599        // In a real implementation, this would use proper font metrics
600        let avg_char_width = self.options.font_size * 0.5;
601        let chars_per_line = (max_width / avg_char_width) as usize;
602
603        if chars_per_line == 0 || text.len() <= chars_per_line {
604            return vec![text.to_string()];
605        }
606
607        let mut lines = Vec::new();
608        let words: Vec<&str> = text.split_whitespace().collect();
609        let mut current_line = String::new();
610
611        for word in words {
612            let test_line = if current_line.is_empty() {
613                word.to_string()
614            } else {
615                format!("{current_line} {word}")
616            };
617
618            if test_line.len() <= chars_per_line {
619                current_line = test_line;
620            } else {
621                if !current_line.is_empty() {
622                    lines.push(current_line);
623                }
624                current_line = word.to_string();
625            }
626        }
627
628        if !current_line.is_empty() {
629            lines.push(current_line);
630        }
631
632        lines
633    }
634}
635
636/// Convert a number to Roman numerals
637fn to_roman(num: u32) -> String {
638    let values = [
639        (1000, "M"),
640        (900, "CM"),
641        (500, "D"),
642        (400, "CD"),
643        (100, "C"),
644        (90, "XC"),
645        (50, "L"),
646        (40, "XL"),
647        (10, "X"),
648        (9, "IX"),
649        (5, "V"),
650        (4, "IV"),
651        (1, "I"),
652    ];
653
654    let mut result = String::new();
655    let mut n = num;
656
657    for (value, numeral) in &values {
658        while n >= *value {
659            result.push_str(numeral);
660            n -= value;
661        }
662    }
663
664    result
665}
666
667/// Get Greek letter for a given number
668fn get_greek_letter(num: u32, uppercase: bool) -> String {
669    let lower = [
670        "α", "β", "γ", "δ", "ε", "ζ", "η", "θ", "ι", "κ", "λ", "μ", "ν", "ξ", "ο", "π", "ρ", "σ",
671        "τ", "υ", "φ", "χ", "ψ", "ω",
672    ];
673    let upper = [
674        "Α", "Β", "Γ", "Δ", "Ε", "Ζ", "Η", "Θ", "Ι", "Κ", "Λ", "Μ", "Ν", "Ξ", "Ο", "Π", "Ρ", "Σ",
675        "Τ", "Υ", "Φ", "Χ", "Ψ", "Ω",
676    ];
677
678    let index = ((num - 1) % 24) as usize;
679    if uppercase {
680        upper[index].to_string()
681    } else {
682        lower[index].to_string()
683    }
684}
685
686/// Get Hebrew letter for a given number
687fn get_hebrew_letter(num: u32) -> String {
688    let letters = [
689        "א", "ב", "ג", "ד", "ה", "ו", "ז", "ח", "ט", "י", "כ", "ל", "מ", "נ", "ס", "ע", "פ", "צ",
690        "ק", "ר", "ש", "ת",
691    ];
692    let index = ((num - 1) % 22) as usize;
693    letters[index].to_string()
694}
695
696/// Get Hiragana letter for a given number
697fn get_hiragana_letter(num: u32) -> String {
698    let letters = [
699        "あ", "い", "う", "え", "お", "か", "き", "く", "け", "こ", "さ", "し", "す", "せ", "そ",
700        "た", "ち", "つ", "て", "と", "な", "に", "ぬ", "ね", "の", "は", "ひ", "ふ", "へ", "ほ",
701        "ま", "み", "む", "め", "も", "や", "ゆ", "よ", "ら", "り", "る", "れ", "ろ", "わ", "を",
702        "ん",
703    ];
704    let index = ((num - 1) % 46) as usize;
705    letters[index].to_string()
706}
707
708/// Get Katakana letter for a given number
709fn get_katakana_letter(num: u32) -> String {
710    let letters = [
711        "ア", "イ", "ウ", "エ", "オ", "カ", "キ", "ク", "ケ", "コ", "サ", "シ", "ス", "セ", "ソ",
712        "タ", "チ", "ツ", "テ", "ト", "ナ", "ニ", "ヌ", "ネ", "ノ", "ハ", "ヒ", "フ", "ヘ", "ホ",
713        "マ", "ミ", "ム", "メ", "モ", "ヤ", "ユ", "ヨ", "ラ", "リ", "ル", "レ", "ロ", "ワ", "ヲ",
714        "ン",
715    ];
716    let index = ((num - 1) % 46) as usize;
717    letters[index].to_string()
718}
719
720/// Get Chinese simplified number for a given number
721fn get_chinese_number(num: u32) -> String {
722    let numbers = [
723        "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", "十三", "十四",
724        "十五", "十六", "十七", "十八", "十九", "二十",
725    ];
726    if num <= 20 {
727        numbers[(num - 1) as usize].to_string()
728    } else {
729        format!("{num}") // Fallback to Arabic for larger numbers
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn test_ordered_list_creation() {
739        let list = OrderedList::new(OrderedListStyle::Decimal);
740        assert_eq!(list.style, OrderedListStyle::Decimal);
741        assert_eq!(list.start_number, 1);
742        assert!(list.items.is_empty());
743    }
744
745    #[test]
746    fn test_unordered_list_creation() {
747        let list = UnorderedList::new(BulletStyle::Disc);
748        assert_eq!(list.bullet_style, BulletStyle::Disc);
749        assert!(list.items.is_empty());
750    }
751
752    #[test]
753    fn test_add_items() {
754        let mut list = OrderedList::new(OrderedListStyle::Decimal);
755        list.add_item("First item".to_string())
756            .add_item("Second item".to_string());
757        assert_eq!(list.items.len(), 2);
758        assert_eq!(list.items[0].text, "First item");
759        assert_eq!(list.items[1].text, "Second item");
760    }
761
762    #[test]
763    fn test_marker_generation_decimal() {
764        let list = OrderedList::new(OrderedListStyle::Decimal);
765        assert_eq!(list.generate_marker(0), "1.");
766        assert_eq!(list.generate_marker(1), "2.");
767        assert_eq!(list.generate_marker(9), "10.");
768    }
769
770    #[test]
771    fn test_marker_generation_lower_alpha() {
772        let list = OrderedList::new(OrderedListStyle::LowerAlpha);
773        assert_eq!(list.generate_marker(0), "a.");
774        assert_eq!(list.generate_marker(1), "b.");
775        assert_eq!(list.generate_marker(25), "z.");
776    }
777
778    #[test]
779    fn test_marker_generation_upper_alpha() {
780        let list = OrderedList::new(OrderedListStyle::UpperAlpha);
781        assert_eq!(list.generate_marker(0), "A.");
782        assert_eq!(list.generate_marker(1), "B.");
783        assert_eq!(list.generate_marker(25), "Z.");
784    }
785
786    #[test]
787    fn test_marker_generation_roman() {
788        let list = OrderedList::new(OrderedListStyle::LowerRoman);
789        assert_eq!(list.generate_marker(0), "i.");
790        assert_eq!(list.generate_marker(3), "iv.");
791        assert_eq!(list.generate_marker(8), "ix.");
792
793        let list_upper = OrderedList::new(OrderedListStyle::UpperRoman);
794        assert_eq!(list_upper.generate_marker(0), "I.");
795        assert_eq!(list_upper.generate_marker(3), "IV.");
796        assert_eq!(list_upper.generate_marker(8), "IX.");
797    }
798
799    #[test]
800    fn test_start_number() {
801        let mut list = OrderedList::new(OrderedListStyle::Decimal);
802        list.set_start_number(5);
803        assert_eq!(list.generate_marker(0), "5.");
804        assert_eq!(list.generate_marker(1), "6.");
805    }
806
807    #[test]
808    fn test_bullet_styles() {
809        let disc = UnorderedList::new(BulletStyle::Disc);
810        assert_eq!(disc.get_bullet_char(), "•");
811
812        let circle = UnorderedList::new(BulletStyle::Circle);
813        assert_eq!(circle.get_bullet_char(), "○");
814
815        let square = UnorderedList::new(BulletStyle::Square);
816        assert_eq!(square.get_bullet_char(), "■");
817
818        let dash = UnorderedList::new(BulletStyle::Dash);
819        assert_eq!(dash.get_bullet_char(), "-");
820    }
821
822    #[test]
823    fn test_custom_bullet() {
824        let arrow = UnorderedList::new(BulletStyle::Custom('→'));
825        assert_eq!(arrow.get_bullet_char(), "→");
826
827        let star = UnorderedList::new(BulletStyle::Custom('★'));
828        assert_eq!(star.get_bullet_char(), "★");
829    }
830
831    #[test]
832    fn test_list_options_default() {
833        let options = ListOptions::default();
834        assert_eq!(options.font_size, 10.0);
835        assert_eq!(options.indent, 20.0);
836        assert_eq!(options.line_spacing, 1.2);
837        assert_eq!(options.marker_spacing, 10.0);
838    }
839
840    #[test]
841    fn test_roman_numerals() {
842        assert_eq!(to_roman(1), "I");
843        assert_eq!(to_roman(4), "IV");
844        assert_eq!(to_roman(5), "V");
845        assert_eq!(to_roman(9), "IX");
846        assert_eq!(to_roman(10), "X");
847        assert_eq!(to_roman(40), "XL");
848        assert_eq!(to_roman(50), "L");
849        assert_eq!(to_roman(90), "XC");
850        assert_eq!(to_roman(100), "C");
851        assert_eq!(to_roman(400), "CD");
852        assert_eq!(to_roman(500), "D");
853        assert_eq!(to_roman(900), "CM");
854        assert_eq!(to_roman(1000), "M");
855        assert_eq!(to_roman(1994), "MCMXCIV");
856    }
857
858    #[test]
859    fn test_nested_lists() {
860        let mut parent = OrderedList::new(OrderedListStyle::Decimal);
861
862        let mut child = UnorderedList::new(BulletStyle::Dash);
863        child.add_item("Nested item 1".to_string());
864        child.add_item("Nested item 2".to_string());
865
866        parent.add_item_with_children(
867            "Parent item".to_string(),
868            vec![ListElement::Unordered(child)],
869        );
870
871        assert_eq!(parent.items.len(), 1);
872        assert_eq!(parent.items[0].children.len(), 1);
873    }
874
875    #[test]
876    fn test_list_position() {
877        let mut list = OrderedList::new(OrderedListStyle::Decimal);
878        list.set_position(100.0, 200.0);
879        assert_eq!(list.position, (100.0, 200.0));
880    }
881
882    #[test]
883    fn test_list_height_calculation() {
884        let mut list = OrderedList::new(OrderedListStyle::Decimal);
885        list.add_item("Item 1".to_string())
886            .add_item("Item 2".to_string())
887            .add_item("Item 3".to_string());
888
889        let height = list.get_height();
890        let expected = 3.0 * list.options.font_size * list.options.line_spacing;
891        assert_eq!(height, expected);
892    }
893
894    #[test]
895    fn test_list_item_structure() {
896        let item = ListItem {
897            text: "Test item".to_string(),
898            children: vec![],
899        };
900        assert_eq!(item.text, "Test item");
901        assert!(item.children.is_empty());
902    }
903
904    #[test]
905    fn test_list_element_enum() {
906        let ordered = OrderedList::new(OrderedListStyle::Decimal);
907        let unordered = UnorderedList::new(BulletStyle::Disc);
908
909        let elements = vec![
910            ListElement::Ordered(ordered),
911            ListElement::Unordered(unordered),
912        ];
913
914        assert_eq!(elements.len(), 2);
915        match &elements[0] {
916            ListElement::Ordered(_) => (),
917            _ => panic!("Expected ordered list"),
918        }
919        match &elements[1] {
920            ListElement::Unordered(_) => (),
921            _ => panic!("Expected unordered list"),
922        }
923    }
924
925    #[test]
926    fn test_advanced_numbering_styles() {
927        // Test decimal with leading zeros
928        let list = OrderedList::new(OrderedListStyle::DecimalLeadingZero);
929        assert_eq!(list.generate_marker(0), "01.");
930        assert_eq!(list.generate_marker(8), "09.");
931        assert_eq!(list.generate_marker(9), "10.");
932        assert_eq!(list.generate_marker(99), "100.");
933
934        // Test Greek lowercase
935        let greek_lower = OrderedList::new(OrderedListStyle::GreekLower);
936        assert_eq!(greek_lower.generate_marker(0), "α.");
937        assert_eq!(greek_lower.generate_marker(1), "β.");
938        assert_eq!(greek_lower.generate_marker(23), "ω.");
939        assert_eq!(greek_lower.generate_marker(24), "α."); // Wraps around
940
941        // Test Greek uppercase
942        let greek_upper = OrderedList::new(OrderedListStyle::GreekUpper);
943        assert_eq!(greek_upper.generate_marker(0), "Α.");
944        assert_eq!(greek_upper.generate_marker(1), "Β.");
945        assert_eq!(greek_upper.generate_marker(23), "Ω.");
946
947        // Test Hebrew
948        let hebrew = OrderedList::new(OrderedListStyle::Hebrew);
949        assert_eq!(hebrew.generate_marker(0), "א.");
950        assert_eq!(hebrew.generate_marker(1), "ב.");
951        assert_eq!(hebrew.generate_marker(21), "ת.");
952
953        // Test Hiragana
954        let hiragana = OrderedList::new(OrderedListStyle::Hiragana);
955        assert_eq!(hiragana.generate_marker(0), "あ.");
956        assert_eq!(hiragana.generate_marker(1), "い.");
957        assert_eq!(hiragana.generate_marker(4), "お.");
958
959        // Test Katakana
960        let katakana = OrderedList::new(OrderedListStyle::Katakana);
961        assert_eq!(katakana.generate_marker(0), "ア.");
962        assert_eq!(katakana.generate_marker(1), "イ.");
963        assert_eq!(katakana.generate_marker(4), "オ.");
964
965        // Test Chinese simplified
966        let chinese = OrderedList::new(OrderedListStyle::ChineseSimplified);
967        assert_eq!(chinese.generate_marker(0), "一.");
968        assert_eq!(chinese.generate_marker(1), "二.");
969        assert_eq!(chinese.generate_marker(9), "十.");
970        assert_eq!(chinese.generate_marker(19), "二十.");
971        assert_eq!(chinese.generate_marker(20), "21."); // Fallback to Arabic
972    }
973
974    #[test]
975    fn test_custom_prefix_suffix() {
976        let mut list = OrderedList::new(OrderedListStyle::Decimal);
977        let mut options = ListOptions::default();
978        options.marker_prefix = "Chapter ".to_string();
979        options.marker_suffix = ":".to_string();
980        list.set_options(options);
981
982        assert_eq!(list.generate_marker(0), "Chapter 1:");
983        assert_eq!(list.generate_marker(1), "Chapter 2:");
984
985        // Test with Roman numerals
986        let mut roman_list = OrderedList::new(OrderedListStyle::UpperRoman);
987        let mut roman_options = ListOptions::default();
988        roman_options.marker_prefix = "Part ".to_string();
989        roman_options.marker_suffix = " -".to_string();
990        roman_list.set_options(roman_options);
991
992        assert_eq!(roman_list.generate_marker(0), "Part I -");
993        assert_eq!(roman_list.generate_marker(3), "Part IV -");
994    }
995
996    #[test]
997    fn test_text_wrapping() {
998        let list = OrderedList::new(OrderedListStyle::Decimal);
999
1000        // Test short text (no wrapping)
1001        let wrapped = list.wrap_text("Short text", 100.0);
1002        assert_eq!(wrapped.len(), 1);
1003        assert_eq!(wrapped[0], "Short text");
1004
1005        // Test long text with wrapping
1006        let long_text =
1007            "This is a very long line that should be wrapped because it exceeds the maximum width";
1008        let wrapped = list.wrap_text(long_text, 50.0); // ~10 chars per line at font size 10
1009        assert!(wrapped.len() > 1);
1010
1011        // Test empty text
1012        let wrapped = list.wrap_text("", 100.0);
1013        assert_eq!(wrapped.len(), 1);
1014        assert_eq!(wrapped[0], "");
1015
1016        // Test zero width (no wrapping possible)
1017        let wrapped = list.wrap_text("Test", 0.0);
1018        assert_eq!(wrapped.len(), 1);
1019        assert_eq!(wrapped[0], "Test");
1020    }
1021
1022    #[test]
1023    fn test_list_options_advanced() {
1024        let mut options = ListOptions::default();
1025
1026        // Test all new fields
1027        options.max_width = Some(300.0);
1028        options.text_align = TextAlign::Center;
1029        options.marker_font = Font::HelveticaBold;
1030        options.marker_color = Some(Color::red());
1031        options.paragraph_spacing = 5.0;
1032        options.draw_separator = true;
1033        options.separator_color = Color::gray(0.5);
1034        options.separator_width = 2.0;
1035        options.marker_prefix = "Item ".to_string();
1036        options.marker_suffix = ")".to_string();
1037
1038        assert_eq!(options.max_width, Some(300.0));
1039        assert_eq!(options.text_align, TextAlign::Center);
1040        assert_eq!(options.marker_font, Font::HelveticaBold);
1041        assert!(options.marker_color.is_some());
1042        assert_eq!(options.paragraph_spacing, 5.0);
1043        assert!(options.draw_separator);
1044        assert_eq!(options.separator_width, 2.0);
1045        assert_eq!(options.marker_prefix, "Item ");
1046        assert_eq!(options.marker_suffix, ")");
1047    }
1048
1049    #[test]
1050    fn test_unordered_list_custom_bullets() {
1051        // Test standard bullets
1052        let disc_list = UnorderedList::new(BulletStyle::Disc);
1053        assert_eq!(disc_list.get_bullet_char(), "•");
1054
1055        let circle_list = UnorderedList::new(BulletStyle::Circle);
1056        assert_eq!(circle_list.get_bullet_char(), "○");
1057
1058        let square_list = UnorderedList::new(BulletStyle::Square);
1059        assert_eq!(square_list.get_bullet_char(), "■");
1060
1061        let dash_list = UnorderedList::new(BulletStyle::Dash);
1062        assert_eq!(dash_list.get_bullet_char(), "-");
1063
1064        // Test custom bullets
1065        let arrow_list = UnorderedList::new(BulletStyle::Custom('→'));
1066        assert_eq!(arrow_list.get_bullet_char(), "→");
1067
1068        let star_list = UnorderedList::new(BulletStyle::Custom('★'));
1069        assert_eq!(star_list.get_bullet_char(), "★");
1070
1071        // Test fallback for unknown custom character
1072        let unknown_list = UnorderedList::new(BulletStyle::Custom('Z'));
1073        assert_eq!(unknown_list.get_bullet_char(), "•"); // Falls back to disc
1074    }
1075
1076    #[test]
1077    fn test_deeply_nested_lists() {
1078        let mut level1 = OrderedList::new(OrderedListStyle::Decimal);
1079
1080        // Create level 2
1081        let mut level2 = UnorderedList::new(BulletStyle::Circle);
1082
1083        // Create level 3
1084        let mut level3 = OrderedList::new(OrderedListStyle::LowerAlpha);
1085        level3.add_item("Deep item a".to_string());
1086        level3.add_item("Deep item b".to_string());
1087
1088        // Add level 3 to level 2
1089        level2.add_item_with_children(
1090            "Level 2 item with children".to_string(),
1091            vec![ListElement::Ordered(level3)],
1092        );
1093        level2.add_item("Level 2 item without children".to_string());
1094
1095        // Add level 2 to level 1
1096        level1.add_item_with_children(
1097            "Level 1 item with nested list".to_string(),
1098            vec![ListElement::Unordered(level2)],
1099        );
1100
1101        assert_eq!(level1.items.len(), 1);
1102        assert_eq!(level1.items[0].children.len(), 1);
1103
1104        // Verify the structure
1105        if let ListElement::Unordered(ref list) = level1.items[0].children[0] {
1106            assert_eq!(list.items.len(), 2);
1107            assert_eq!(list.items[0].children.len(), 1);
1108        } else {
1109            panic!("Expected unordered list at level 2");
1110        }
1111    }
1112
1113    #[test]
1114    fn test_height_calculation_with_nested() {
1115        let mut list = OrderedList::new(OrderedListStyle::Decimal);
1116        list.add_item("Item 1".to_string());
1117        list.add_item("Item 2".to_string());
1118
1119        let height_simple = list.get_height();
1120        let expected_simple = 2.0 * 10.0 * 1.2; // 2 items * font_size * line_spacing
1121        assert_eq!(height_simple, expected_simple);
1122
1123        // Add nested list
1124        let mut nested = UnorderedList::new(BulletStyle::Dash);
1125        nested.add_item("Nested 1".to_string());
1126        nested.add_item("Nested 2".to_string());
1127
1128        list.add_item_with_children(
1129            "Item 3 with children".to_string(),
1130            vec![ListElement::Unordered(nested)],
1131        );
1132
1133        let height_with_nested = list.get_height();
1134        let expected_with_nested = 5.0 * 10.0 * 1.2; // 5 total items * font_size * line_spacing
1135        assert_eq!(height_with_nested, expected_with_nested);
1136    }
1137
1138    #[test]
1139    fn test_helper_functions() {
1140        // Test Greek letter generation
1141        assert_eq!(get_greek_letter(1, false), "α");
1142        assert_eq!(get_greek_letter(2, false), "β");
1143        assert_eq!(get_greek_letter(24, false), "ω");
1144        assert_eq!(get_greek_letter(25, false), "α"); // Wraps
1145
1146        assert_eq!(get_greek_letter(1, true), "Α");
1147        assert_eq!(get_greek_letter(2, true), "Β");
1148        assert_eq!(get_greek_letter(24, true), "Ω");
1149
1150        // Test Hebrew letter generation
1151        assert_eq!(get_hebrew_letter(1), "א");
1152        assert_eq!(get_hebrew_letter(22), "ת");
1153        assert_eq!(get_hebrew_letter(23), "א"); // Wraps
1154
1155        // Test Hiragana generation
1156        assert_eq!(get_hiragana_letter(1), "あ");
1157        assert_eq!(get_hiragana_letter(5), "お");
1158        assert_eq!(get_hiragana_letter(46), "ん");
1159        assert_eq!(get_hiragana_letter(47), "あ"); // Wraps
1160
1161        // Test Katakana generation
1162        assert_eq!(get_katakana_letter(1), "ア");
1163        assert_eq!(get_katakana_letter(5), "オ");
1164        assert_eq!(get_katakana_letter(46), "ン");
1165
1166        // Test Chinese number generation
1167        assert_eq!(get_chinese_number(1), "一");
1168        assert_eq!(get_chinese_number(10), "十");
1169        assert_eq!(get_chinese_number(20), "二十");
1170        assert_eq!(get_chinese_number(21), "21"); // Fallback
1171    }
1172
1173    #[test]
1174    fn test_list_cloning() {
1175        let mut original = OrderedList::new(OrderedListStyle::Decimal);
1176        original.add_item("Item 1".to_string());
1177        original.set_position(100.0, 200.0);
1178        original.set_start_number(5);
1179
1180        let cloned = original.clone();
1181        assert_eq!(cloned.items.len(), 1);
1182        assert_eq!(cloned.position, (100.0, 200.0));
1183        assert_eq!(cloned.start_number, 5);
1184        assert_eq!(cloned.style, OrderedListStyle::Decimal);
1185    }
1186}