Skip to main content

hwpforge_core/
section.rs

1//! Document sections.
2//!
3//! A [`Section`] is a contiguous block of paragraphs sharing the same
4//! [`PageSettings`]. Typical HWP documents have one section, but
5//! complex reports may mix portrait and landscape sections.
6//!
7//! # Examples
8//!
9//! ```
10//! use hwpforge_core::section::Section;
11//! use hwpforge_core::PageSettings;
12//! use hwpforge_core::paragraph::Paragraph;
13//! use hwpforge_core::run::Run;
14//! use hwpforge_foundation::{CharShapeIndex, ParaShapeIndex};
15//!
16//! let mut section = Section::new(PageSettings::a4());
17//! section.add_paragraph(Paragraph::with_runs(
18//!     vec![Run::text("Hello", CharShapeIndex::new(0))],
19//!     ParaShapeIndex::new(0),
20//! ));
21//! assert_eq!(section.paragraph_count(), 1);
22//! ```
23
24use hwpforge_foundation::{
25    ApplyPageType, HwpUnit, NumberFormatType, PageNumberPosition, ShowMode, TextDirection,
26};
27use schemars::JsonSchema;
28use serde::{Deserialize, Serialize};
29
30use crate::column::ColumnSettings;
31use crate::page::PageSettings;
32use crate::paragraph::Paragraph;
33
34// ---------------------------------------------------------------------------
35// Visibility
36// ---------------------------------------------------------------------------
37
38/// Controls visibility of headers, footers, master pages, borders, and fills.
39///
40/// Maps to `<hp:visibility>` inside `<hp:secPr>`. All flags default to
41/// the standard 한글 values (show everything, no hiding).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43pub struct Visibility {
44    /// Hide header on the first page.
45    #[serde(default)]
46    pub hide_first_header: bool,
47    /// Hide footer on the first page.
48    #[serde(default)]
49    pub hide_first_footer: bool,
50    /// Hide master page on the first page.
51    #[serde(default)]
52    pub hide_first_master_page: bool,
53    /// Hide page number on the first page.
54    #[serde(default)]
55    pub hide_first_page_num: bool,
56    /// Hide empty line on the first page.
57    #[serde(default)]
58    pub hide_first_empty_line: bool,
59    /// Show line numbers in the section.
60    #[serde(default)]
61    pub show_line_number: bool,
62    /// Border visibility mode.
63    #[serde(default)]
64    pub border: ShowMode,
65    /// Fill visibility mode.
66    #[serde(default)]
67    pub fill: ShowMode,
68}
69
70impl Default for Visibility {
71    fn default() -> Self {
72        Self {
73            hide_first_header: false,
74            hide_first_footer: false,
75            hide_first_master_page: false,
76            hide_first_page_num: false,
77            hide_first_empty_line: false,
78            show_line_number: false,
79            border: ShowMode::ShowAll,
80            fill: ShowMode::ShowAll,
81        }
82    }
83}
84
85// ---------------------------------------------------------------------------
86// LineNumberShape
87// ---------------------------------------------------------------------------
88
89/// Line numbering settings for a section.
90///
91/// Maps to `<hp:lineNumberShape>` inside `<hp:secPr>`.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
93pub struct LineNumberShape {
94    /// Restart type: 0 = continuous, 1 = per page, 2 = per section.
95    #[serde(default)]
96    pub restart_type: u8,
97    /// Count by N (show number every N lines, 0 = disabled).
98    #[serde(default)]
99    pub count_by: u16,
100    /// Distance from text to line number (HwpUnit).
101    #[serde(default)]
102    pub distance: HwpUnit,
103    /// Starting line number.
104    #[serde(default)]
105    pub start_number: u32,
106}
107
108// ---------------------------------------------------------------------------
109// PageBorderFillEntry
110// ---------------------------------------------------------------------------
111
112/// A single page border/fill entry for the section.
113///
114/// Maps to `<hp:pageBorderFill>` inside `<hp:secPr>`.
115/// Standard 한글 documents have 3 entries: BOTH, EVEN, ODD.
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct PageBorderFillEntry {
118    /// Which pages this border fill applies to: `"BOTH"`, `"EVEN"`, `"ODD"`.
119    pub apply_type: String,
120    /// Reference to a borderFill definition (1-based index).
121    #[serde(default = "PageBorderFillEntry::default_border_fill_id")]
122    pub border_fill_id: u32,
123    /// Whether the border is relative to text or paper.
124    #[serde(default = "PageBorderFillEntry::default_text_border")]
125    pub text_border: String,
126    /// Whether header is inside the border.
127    #[serde(default)]
128    pub header_inside: bool,
129    /// Whether footer is inside the border.
130    #[serde(default)]
131    pub footer_inside: bool,
132    /// Fill area: `"PAPER"` or `"PAGE"`.
133    #[serde(default = "PageBorderFillEntry::default_fill_area")]
134    pub fill_area: String,
135    /// Offset from page edge (left, right, top, bottom) in HwpUnit.
136    #[serde(default = "PageBorderFillEntry::default_offset")]
137    pub offset: [HwpUnit; 4],
138}
139
140impl PageBorderFillEntry {
141    fn default_border_fill_id() -> u32 {
142        1
143    }
144    fn default_text_border() -> String {
145        "PAPER".to_string()
146    }
147    fn default_fill_area() -> String {
148        "PAPER".to_string()
149    }
150    fn default_offset() -> [HwpUnit; 4] {
151        // 1417 HwpUnit ≈ 5mm default offset
152        [
153            HwpUnit::new(1417).unwrap(),
154            HwpUnit::new(1417).unwrap(),
155            HwpUnit::new(1417).unwrap(),
156            HwpUnit::new(1417).unwrap(),
157        ]
158    }
159}
160
161impl Default for PageBorderFillEntry {
162    fn default() -> Self {
163        Self {
164            apply_type: "BOTH".to_string(),
165            border_fill_id: 1,
166            text_border: "PAPER".to_string(),
167            header_inside: false,
168            footer_inside: false,
169            fill_area: "PAPER".to_string(),
170            offset: Self::default_offset(),
171        }
172    }
173}
174
175// ---------------------------------------------------------------------------
176// BeginNum
177// ---------------------------------------------------------------------------
178
179/// Starting numbers for various auto-numbering sequences.
180///
181/// Maps to `<hh:beginNum>` in header.xml.
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
183pub struct BeginNum {
184    /// Starting page number (default: 1).
185    #[serde(default = "BeginNum::one")]
186    pub page: u32,
187    /// Starting footnote number (default: 1).
188    #[serde(default = "BeginNum::one")]
189    pub footnote: u32,
190    /// Starting endnote number (default: 1).
191    #[serde(default = "BeginNum::one")]
192    pub endnote: u32,
193    /// Starting picture number (default: 1).
194    #[serde(default = "BeginNum::one")]
195    pub pic: u32,
196    /// Starting table number (default: 1).
197    #[serde(default = "BeginNum::one")]
198    pub tbl: u32,
199    /// Starting equation number (default: 1).
200    #[serde(default = "BeginNum::one")]
201    pub equation: u32,
202}
203
204impl BeginNum {
205    fn one() -> u32 {
206        1
207    }
208}
209
210impl Default for BeginNum {
211    fn default() -> Self {
212        Self { page: 1, footnote: 1, endnote: 1, pic: 1, tbl: 1, equation: 1 }
213    }
214}
215
216// ---------------------------------------------------------------------------
217// MasterPage
218// ---------------------------------------------------------------------------
219
220/// A master page (background/watermark page) for a section.
221///
222/// Master pages provide background content rendered behind the main body.
223/// Maps to `<masterPage>` elements inside `<hp:secPr>`.
224///
225/// In HWPX, each master page has an `applyPageType` attribute
226/// (`BOTH`, `EVEN`, or `ODD`) and contains its own paragraphs.
227#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
228pub struct MasterPage {
229    /// Which pages this master page applies to.
230    pub apply_page_type: ApplyPageType,
231    /// Paragraphs composing the master page content.
232    pub paragraphs: Vec<Paragraph>,
233}
234
235impl MasterPage {
236    /// Creates a new master page with the given page type and paragraphs.
237    pub fn new(apply_page_type: ApplyPageType, paragraphs: Vec<Paragraph>) -> Self {
238        Self { apply_page_type, paragraphs }
239    }
240}
241
242impl std::fmt::Display for MasterPage {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        let n = self.paragraphs.len();
245        let word = if n == 1 { "paragraph" } else { "paragraphs" };
246        write!(f, "MasterPage({n} {word}, {:?})", self.apply_page_type)
247    }
248}
249
250// ---------------------------------------------------------------------------
251// HeaderFooter
252// ---------------------------------------------------------------------------
253
254/// A header or footer region containing paragraphs.
255///
256/// In HWPX, headers and footers appear as `<hp:header>` / `<hp:footer>`
257/// elements inside `<hp:ctrl>` in the section body. Each contains its own
258/// paragraphs and an [`ApplyPageType`] controlling which pages it applies to.
259///
260/// # Examples
261///
262/// ```
263/// use hwpforge_core::section::HeaderFooter;
264/// use hwpforge_core::paragraph::Paragraph;
265/// use hwpforge_foundation::{ApplyPageType, ParaShapeIndex};
266///
267/// let hf = HeaderFooter::new(
268///     vec![Paragraph::new(ParaShapeIndex::new(0))],
269///     ApplyPageType::Both,
270/// );
271/// assert_eq!(hf.paragraphs.len(), 1);
272/// assert_eq!(hf.apply_page_type, ApplyPageType::Both);
273/// ```
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
275pub struct HeaderFooter {
276    /// Paragraphs composing the header/footer content.
277    pub paragraphs: Vec<Paragraph>,
278    /// Which pages this header/footer applies to.
279    pub apply_page_type: ApplyPageType,
280}
281
282impl HeaderFooter {
283    /// Creates a new header/footer with the given paragraphs and page scope.
284    pub fn new(paragraphs: Vec<Paragraph>, apply_page_type: ApplyPageType) -> Self {
285        Self { paragraphs, apply_page_type }
286    }
287
288    /// Creates a header/footer applied to **all** pages (both odd and even).
289    ///
290    /// This is the most common case for simple documents that use a single
291    /// header or footer on every page.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use hwpforge_core::section::HeaderFooter;
297    /// use hwpforge_core::paragraph::Paragraph;
298    /// use hwpforge_foundation::{ApplyPageType, ParaShapeIndex};
299    ///
300    /// let hf = HeaderFooter::all_pages(vec![Paragraph::new(ParaShapeIndex::new(0))]);
301    /// assert_eq!(hf.apply_page_type, ApplyPageType::Both);
302    /// assert_eq!(hf.paragraphs.len(), 1);
303    /// ```
304    pub fn all_pages(paragraphs: Vec<Paragraph>) -> Self {
305        Self { paragraphs, apply_page_type: ApplyPageType::Both }
306    }
307
308    /// Creates a header/footer applied to all pages.
309    #[deprecated(since = "0.2.0", note = "Use `all_pages()` instead")]
310    pub fn both(paragraphs: Vec<Paragraph>) -> Self {
311        Self::all_pages(paragraphs)
312    }
313}
314
315impl std::fmt::Display for HeaderFooter {
316    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
317        let n = self.paragraphs.len();
318        let word = if n == 1 { "paragraph" } else { "paragraphs" };
319        write!(f, "HeaderFooter({n} {word}, {:?})", self.apply_page_type)
320    }
321}
322
323// ---------------------------------------------------------------------------
324// PageNumber
325// ---------------------------------------------------------------------------
326
327/// Page number display settings for a section.
328///
329/// In HWPX, page numbers appear as `<hp:pageNum>` inside `<hp:ctrl>`.
330/// This struct controls position, format, and optional decoration characters.
331///
332/// # Examples
333///
334/// ```
335/// use hwpforge_core::section::PageNumber;
336/// use hwpforge_foundation::{NumberFormatType, PageNumberPosition};
337///
338/// let pn = PageNumber::new(
339///     PageNumberPosition::BottomCenter,
340///     NumberFormatType::Digit,
341/// );
342/// assert_eq!(pn.position, PageNumberPosition::BottomCenter);
343/// ```
344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
345pub struct PageNumber {
346    /// Where to display the page number.
347    pub position: PageNumberPosition,
348    /// Numbering format (digits, roman, etc.).
349    pub number_format: NumberFormatType,
350    /// Optional decoration string placed around the number
351    /// (e.g. `"- "` for `"- 1 -"`). Empty means no decoration.
352    pub decoration: String,
353}
354
355impl PageNumber {
356    /// Creates a new page number with no decoration.
357    pub fn new(position: PageNumberPosition, number_format: NumberFormatType) -> Self {
358        Self { position, number_format, decoration: String::new() }
359    }
360
361    /// Creates a page number at the bottom-center in plain digit format.
362    ///
363    /// This is the most common page number layout for Korean documents.
364    /// Equivalent to `PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit)`
365    /// with an empty `decoration`.
366    ///
367    /// # Examples
368    ///
369    /// ```
370    /// use hwpforge_core::section::PageNumber;
371    /// use hwpforge_foundation::{NumberFormatType, PageNumberPosition};
372    ///
373    /// let pn = PageNumber::bottom_center();
374    /// assert_eq!(pn.position, PageNumberPosition::BottomCenter);
375    /// assert_eq!(pn.number_format, NumberFormatType::Digit);
376    /// assert!(pn.decoration.is_empty());
377    /// ```
378    pub fn bottom_center() -> Self {
379        Self {
380            position: PageNumberPosition::BottomCenter,
381            number_format: NumberFormatType::Digit,
382            decoration: String::new(),
383        }
384    }
385
386    /// Creates a new page number with decoration characters placed around the number.
387    ///
388    /// # Examples
389    ///
390    /// ```
391    /// use hwpforge_core::section::PageNumber;
392    /// use hwpforge_foundation::{NumberFormatType, PageNumberPosition};
393    ///
394    /// let pn = PageNumber::with_decoration(
395    ///     PageNumberPosition::BottomCenter,
396    ///     NumberFormatType::Digit,
397    ///     "- ",
398    /// );
399    /// assert_eq!(pn.decoration, "- ");
400    /// ```
401    pub fn with_decoration(
402        position: PageNumberPosition,
403        number_format: NumberFormatType,
404        decoration: impl Into<String>,
405    ) -> Self {
406        Self { position, number_format, decoration: decoration.into() }
407    }
408
409    /// Creates a new page number with side decoration characters.
410    #[deprecated(since = "0.2.0", note = "Use `with_decoration()` instead")]
411    pub fn with_side_char(
412        position: PageNumberPosition,
413        number_format: NumberFormatType,
414        side_char: impl Into<String>,
415    ) -> Self {
416        Self::with_decoration(position, number_format, side_char)
417    }
418}
419
420impl std::fmt::Display for PageNumber {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "PageNumber({:?}, {:?})", self.position, self.number_format)
423    }
424}
425
426// ---------------------------------------------------------------------------
427// Section
428// ---------------------------------------------------------------------------
429
430/// A document section: paragraphs + page geometry.
431///
432/// # Examples
433///
434/// ```
435/// use hwpforge_core::section::Section;
436/// use hwpforge_core::PageSettings;
437/// use hwpforge_core::paragraph::Paragraph;
438/// use hwpforge_foundation::ParaShapeIndex;
439///
440/// let section = Section::with_paragraphs(
441///     vec![Paragraph::new(ParaShapeIndex::new(0))],
442///     PageSettings::a4(),
443/// );
444/// assert_eq!(section.paragraph_count(), 1);
445/// ```
446#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
447pub struct Section {
448    /// Ordered paragraphs in this section.
449    pub paragraphs: Vec<Paragraph>,
450    /// Page dimensions and margins for this section.
451    pub page_settings: PageSettings,
452    /// Headers for this section, ordered as in HWPX wire (`<hp:header>` × N).
453    ///
454    /// HWPX allows multiple `<hp:header>` elements in a single section,
455    /// differentiated by `applyPageType` (`BOTH` / `ODD` / `EVEN`). HWP5
456    /// stores the same information as multiple `head` ctrl records.
457    /// Empty `Vec` = "no header on this section". See
458    /// [ADR-002](../../../.docs/architecture/adr/ADR-002-section-multi-header-footer-cardinality.md)
459    /// for the cardinality decision.
460    #[serde(default, skip_serializing_if = "Vec::is_empty")]
461    pub headers: Vec<HeaderFooter>,
462    /// Footers for this section, ordered as in HWPX wire (`<hp:footer>` × N).
463    /// See `headers` for the cardinality rationale.
464    #[serde(default, skip_serializing_if = "Vec::is_empty")]
465    pub footers: Vec<HeaderFooter>,
466    /// Optional page number settings for this section.
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub page_number: Option<PageNumber>,
469    /// Multi-column layout. `None` = single column (default).
470    #[serde(default, skip_serializing_if = "Option::is_none")]
471    pub column_settings: Option<ColumnSettings>,
472    /// Visibility flags for headers, footers, borders, etc.
473    /// `None` = default visibility (show everything).
474    #[serde(default, skip_serializing_if = "Option::is_none")]
475    pub visibility: Option<Visibility>,
476    /// Line numbering settings. `None` = no line numbers.
477    #[serde(default, skip_serializing_if = "Option::is_none")]
478    pub line_number_shape: Option<LineNumberShape>,
479    /// Page border/fill entries. `None` = default 3 entries (BOTH/EVEN/ODD with borderFillIDRef=1).
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub page_border_fills: Option<Vec<PageBorderFillEntry>>,
482    /// Master pages (background content rendered behind the body).
483    /// `None` = no master pages (default).
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub master_pages: Option<Vec<MasterPage>>,
486    /// Starting numbers for auto-numbering sequences.
487    /// `None` = default values (all start at 1).
488    #[serde(default, skip_serializing_if = "Option::is_none")]
489    pub begin_num: Option<BeginNum>,
490    /// Text writing direction for this section.
491    /// Defaults to [`TextDirection::Horizontal`] (가로쓰기).
492    #[serde(default)]
493    pub text_direction: TextDirection,
494}
495
496impl Section {
497    /// Creates an empty section with the given page settings.
498    ///
499    /// # Examples
500    ///
501    /// ```
502    /// use hwpforge_core::section::Section;
503    /// use hwpforge_core::PageSettings;
504    ///
505    /// let section = Section::new(PageSettings::a4());
506    /// assert!(section.is_empty());
507    /// ```
508    pub fn new(page_settings: PageSettings) -> Self {
509        Self {
510            paragraphs: Vec::new(),
511            page_settings,
512            headers: Vec::new(),
513            footers: Vec::new(),
514            page_number: None,
515            column_settings: None,
516            visibility: None,
517            line_number_shape: None,
518            page_border_fills: None,
519            master_pages: None,
520            begin_num: None,
521            text_direction: TextDirection::Horizontal,
522        }
523    }
524
525    /// Creates a section with pre-built paragraphs.
526    ///
527    /// # Examples
528    ///
529    /// ```
530    /// use hwpforge_core::section::Section;
531    /// use hwpforge_core::PageSettings;
532    /// use hwpforge_core::paragraph::Paragraph;
533    /// use hwpforge_foundation::ParaShapeIndex;
534    ///
535    /// let section = Section::with_paragraphs(
536    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
537    ///     PageSettings::letter(),
538    /// );
539    /// assert_eq!(section.paragraph_count(), 1);
540    /// ```
541    pub fn with_paragraphs(paragraphs: Vec<Paragraph>, page_settings: PageSettings) -> Self {
542        Self {
543            paragraphs,
544            page_settings,
545            headers: Vec::new(),
546            footers: Vec::new(),
547            page_number: None,
548            column_settings: None,
549            visibility: None,
550            line_number_shape: None,
551            page_border_fills: None,
552            master_pages: None,
553            begin_num: None,
554            text_direction: TextDirection::Horizontal,
555        }
556    }
557
558    /// Sets the text writing direction for this section and returns `self`.
559    ///
560    /// # Examples
561    ///
562    /// ```
563    /// use hwpforge_core::section::Section;
564    /// use hwpforge_core::PageSettings;
565    /// use hwpforge_foundation::TextDirection;
566    ///
567    /// let section = Section::new(PageSettings::a4())
568    ///     .with_text_direction(TextDirection::Vertical);
569    /// assert_eq!(section.text_direction, TextDirection::Vertical);
570    /// ```
571    pub fn with_text_direction(mut self, dir: TextDirection) -> Self {
572        self.text_direction = dir;
573        self
574    }
575
576    /// Appends a paragraph to this section.
577    pub fn add_paragraph(&mut self, paragraph: Paragraph) {
578        self.paragraphs.push(paragraph);
579    }
580
581    /// Returns the number of paragraphs.
582    pub fn paragraph_count(&self) -> usize {
583        self.paragraphs.len()
584    }
585
586    /// Returns `true` if this section has no paragraphs.
587    pub fn is_empty(&self) -> bool {
588        self.paragraphs.is_empty()
589    }
590
591    /// Counts tables, images, and charts in this section.
592    ///
593    /// Traverses all paragraph runs once and returns aggregate counts.
594    ///
595    /// # Examples
596    ///
597    /// ```
598    /// use hwpforge_core::section::{ContentCounts, Section};
599    /// use hwpforge_core::PageSettings;
600    ///
601    /// let section = Section::new(PageSettings::a4());
602    /// let counts = section.content_counts();
603    /// assert_eq!(counts.tables, 0);
604    /// assert_eq!(counts.images, 0);
605    /// assert_eq!(counts.charts, 0);
606    /// ```
607    pub fn content_counts(&self) -> ContentCounts {
608        let mut tables: usize = 0;
609        let mut images: usize = 0;
610        let mut charts: usize = 0;
611
612        for para in &self.paragraphs {
613            for run in &para.runs {
614                match &run.content {
615                    crate::RunContent::Table(_) => tables += 1,
616                    crate::RunContent::Image(_) => images += 1,
617                    crate::RunContent::Control(c) => {
618                        if matches!(**c, crate::control::Control::Chart { .. }) {
619                            charts += 1;
620                        }
621                    }
622                    _ => {}
623                }
624            }
625        }
626
627        ContentCounts { tables, images, charts }
628    }
629}
630
631/// Aggregate content counts for a section.
632#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
633pub struct ContentCounts {
634    /// Number of tables.
635    pub tables: usize,
636    /// Number of images.
637    pub images: usize,
638    /// Number of charts.
639    pub charts: usize,
640}
641
642impl std::fmt::Display for Section {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        let n = self.paragraphs.len();
645        let word = if n == 1 { "paragraph" } else { "paragraphs" };
646        write!(f, "Section({n} {word})")
647    }
648}
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653    use crate::run::Run;
654    use hwpforge_foundation::{
655        ApplyPageType, CharShapeIndex, NumberFormatType, PageNumberPosition, ParaShapeIndex,
656    };
657
658    fn simple_paragraph() -> Paragraph {
659        Paragraph::with_runs(
660            vec![Run::text("text", CharShapeIndex::new(0))],
661            ParaShapeIndex::new(0),
662        )
663    }
664
665    #[test]
666    fn new_is_empty() {
667        let section = Section::new(PageSettings::a4());
668        assert!(section.is_empty());
669        assert_eq!(section.paragraph_count(), 0);
670    }
671
672    #[test]
673    fn with_paragraphs() {
674        let section = Section::with_paragraphs(
675            vec![simple_paragraph(), simple_paragraph()],
676            PageSettings::a4(),
677        );
678        assert_eq!(section.paragraph_count(), 2);
679        assert!(!section.is_empty());
680    }
681
682    #[test]
683    fn add_paragraph() {
684        let mut section = Section::new(PageSettings::a4());
685        section.add_paragraph(simple_paragraph());
686        section.add_paragraph(simple_paragraph());
687        assert_eq!(section.paragraph_count(), 2);
688    }
689
690    #[test]
691    fn page_settings_preserved() {
692        let section = Section::new(PageSettings::letter());
693        assert_eq!(section.page_settings, PageSettings::letter());
694    }
695
696    #[test]
697    fn display_singular() {
698        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
699        assert_eq!(section.to_string(), "Section(1 paragraph)");
700    }
701
702    #[test]
703    fn display_plural() {
704        let section = Section::with_paragraphs(
705            vec![simple_paragraph(), simple_paragraph()],
706            PageSettings::a4(),
707        );
708        assert_eq!(section.to_string(), "Section(2 paragraphs)");
709    }
710
711    #[test]
712    fn equality() {
713        let a = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
714        let b = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
715        assert_eq!(a, b);
716    }
717
718    #[test]
719    fn inequality_different_page_settings() {
720        let a = Section::new(PageSettings::a4());
721        let b = Section::new(PageSettings::letter());
722        assert_ne!(a, b);
723    }
724
725    #[test]
726    fn clone_independence() {
727        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
728        let mut cloned = section.clone();
729        cloned.add_paragraph(simple_paragraph());
730        assert_eq!(section.paragraph_count(), 1);
731        assert_eq!(cloned.paragraph_count(), 2);
732    }
733
734    #[test]
735    fn serde_roundtrip() {
736        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
737        let json = serde_json::to_string(&section).unwrap();
738        let back: Section = serde_json::from_str(&json).unwrap();
739        assert_eq!(section, back);
740    }
741
742    #[test]
743    fn serde_empty_section() {
744        let section = Section::new(PageSettings::a4());
745        let json = serde_json::to_string(&section).unwrap();
746        let back: Section = serde_json::from_str(&json).unwrap();
747        assert_eq!(section, back);
748    }
749
750    #[test]
751    fn serde_letter_page() {
752        let section = Section::new(PageSettings::letter());
753        let json = serde_json::to_string(&section).unwrap();
754        let back: Section = serde_json::from_str(&json).unwrap();
755        assert_eq!(section, back);
756    }
757
758    // -----------------------------------------------------------------------
759    // HeaderFooter tests
760    // -----------------------------------------------------------------------
761
762    #[test]
763    fn header_footer_new() {
764        let hf =
765            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
766        assert_eq!(hf.paragraphs.len(), 1);
767        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
768    }
769
770    #[test]
771    fn header_footer_even_odd() {
772        let even = HeaderFooter::new(vec![], ApplyPageType::Even);
773        let odd = HeaderFooter::new(vec![], ApplyPageType::Odd);
774        assert_eq!(even.apply_page_type, ApplyPageType::Even);
775        assert_eq!(odd.apply_page_type, ApplyPageType::Odd);
776        assert_ne!(even, odd);
777    }
778
779    #[test]
780    fn header_footer_display() {
781        let hf =
782            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
783        let s = hf.to_string();
784        assert!(s.contains("1 paragraph"), "display: {s}");
785        assert!(s.contains("Both"), "display: {s}");
786    }
787
788    #[test]
789    fn header_footer_serde_roundtrip() {
790        let hf = HeaderFooter::new(
791            vec![Paragraph::with_runs(
792                vec![Run::text("Header text", CharShapeIndex::new(0))],
793                ParaShapeIndex::new(0),
794            )],
795            ApplyPageType::Both,
796        );
797        let json = serde_json::to_string(&hf).unwrap();
798        let back: HeaderFooter = serde_json::from_str(&json).unwrap();
799        assert_eq!(hf, back);
800    }
801
802    #[test]
803    fn header_footer_clone_independence() {
804        let hf =
805            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
806        let mut cloned = hf.clone();
807        cloned.paragraphs.push(Paragraph::new(ParaShapeIndex::new(1)));
808        assert_eq!(hf.paragraphs.len(), 1);
809        assert_eq!(cloned.paragraphs.len(), 2);
810    }
811
812    // -----------------------------------------------------------------------
813    // PageNumber tests
814    // -----------------------------------------------------------------------
815
816    #[test]
817    fn page_number_new() {
818        let pn = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
819        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
820        assert_eq!(pn.number_format, NumberFormatType::Digit);
821        assert!(pn.decoration.is_empty());
822    }
823
824    #[test]
825    fn page_number_with_decoration() {
826        let pn = PageNumber::with_decoration(
827            PageNumberPosition::BottomCenter,
828            NumberFormatType::RomanCapital,
829            "- ",
830        );
831        assert_eq!(pn.decoration, "- ");
832        assert_eq!(pn.number_format, NumberFormatType::RomanCapital);
833    }
834
835    #[test]
836    #[allow(deprecated)]
837    fn page_number_with_side_char_deprecated() {
838        let pn = PageNumber::with_side_char(
839            PageNumberPosition::BottomCenter,
840            NumberFormatType::Digit,
841            "- ",
842        );
843        assert_eq!(pn.decoration, "- ");
844    }
845
846    #[test]
847    fn page_number_display() {
848        let pn = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
849        let s = pn.to_string();
850        assert!(s.contains("TopCenter"), "display: {s}");
851        assert!(s.contains("Digit"), "display: {s}");
852    }
853
854    #[test]
855    fn page_number_serde_roundtrip() {
856        let pn = PageNumber::with_decoration(
857            PageNumberPosition::BottomCenter,
858            NumberFormatType::CircledDigit,
859            "< ",
860        );
861        let json = serde_json::to_string(&pn).unwrap();
862        let back: PageNumber = serde_json::from_str(&json).unwrap();
863        assert_eq!(pn, back);
864    }
865
866    #[test]
867    fn page_number_equality() {
868        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
869        let b = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
870        assert_eq!(a, b);
871    }
872
873    #[test]
874    fn page_number_inequality() {
875        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
876        let b = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
877        assert_ne!(a, b);
878    }
879
880    // -----------------------------------------------------------------------
881    // Section with header/footer/page_number
882    // -----------------------------------------------------------------------
883
884    #[test]
885    fn section_new_has_empty_header_footer_vecs() {
886        let section = Section::new(PageSettings::a4());
887        assert!(section.headers.is_empty());
888        assert!(section.footers.is_empty());
889        assert!(section.page_number.is_none());
890        assert!(section.column_settings.is_none());
891    }
892
893    #[test]
894    fn section_with_header_footer() {
895        let mut section = Section::new(PageSettings::a4());
896        section.headers.push(HeaderFooter::new(
897            vec![Paragraph::with_runs(
898                vec![Run::text("Header", CharShapeIndex::new(0))],
899                ParaShapeIndex::new(0),
900            )],
901            ApplyPageType::Both,
902        ));
903        section.footers.push(HeaderFooter::new(
904            vec![Paragraph::with_runs(
905                vec![Run::text("Footer", CharShapeIndex::new(0))],
906                ParaShapeIndex::new(0),
907            )],
908            ApplyPageType::Both,
909        ));
910        assert_eq!(section.headers.len(), 1);
911        assert_eq!(section.footers.len(), 1);
912    }
913
914    #[test]
915    fn section_with_page_number() {
916        let mut section = Section::new(PageSettings::a4());
917        section.page_number =
918            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
919        assert!(section.page_number.is_some());
920    }
921
922    #[test]
923    fn section_serde_with_optional_fields() {
924        let mut section = Section::new(PageSettings::a4());
925        section.headers.push(HeaderFooter::new(vec![], ApplyPageType::Both));
926        section.page_number =
927            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
928        let json = serde_json::to_string(&section).unwrap();
929        let back: Section = serde_json::from_str(&json).unwrap();
930        assert_eq!(section, back);
931    }
932
933    #[test]
934    fn section_serde_none_fields_skipped() {
935        let section = Section::new(PageSettings::a4());
936        let json = serde_json::to_string(&section).unwrap();
937        // Section-level header/footer/page_number/column_settings should not appear
938        // (PageSettings has header_margin/footer_margin, which is different)
939        assert!(!json.contains("\"header\""));
940        assert!(!json.contains("\"footer\""));
941        assert!(!json.contains("\"page_number\""));
942        assert!(!json.contains("\"column_settings\""));
943        let back: Section = serde_json::from_str(&json).unwrap();
944        assert_eq!(section, back);
945    }
946
947    // -----------------------------------------------------------------------
948    // HeaderFooter::all_pages tests
949    // -----------------------------------------------------------------------
950
951    #[test]
952    fn header_footer_all_pages_apply_page_type() {
953        let hf = HeaderFooter::all_pages(vec![Paragraph::new(ParaShapeIndex::new(0))]);
954        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
955    }
956
957    #[test]
958    fn header_footer_all_pages_preserves_paragraphs() {
959        let paras = vec![simple_paragraph(), simple_paragraph()];
960        let hf = HeaderFooter::all_pages(paras);
961        assert_eq!(hf.paragraphs.len(), 2);
962    }
963
964    #[test]
965    fn header_footer_all_pages_empty_paragraphs() {
966        let hf = HeaderFooter::all_pages(vec![]);
967        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
968        assert!(hf.paragraphs.is_empty());
969    }
970
971    #[test]
972    #[allow(deprecated)]
973    fn header_footer_both_deprecated_alias() {
974        let hf = HeaderFooter::both(vec![Paragraph::new(ParaShapeIndex::new(0))]);
975        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
976    }
977
978    // -----------------------------------------------------------------------
979    // PageNumber::bottom_center tests
980    // -----------------------------------------------------------------------
981
982    #[test]
983    fn page_number_bottom_center_position() {
984        let pn = PageNumber::bottom_center();
985        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
986    }
987
988    #[test]
989    fn page_number_bottom_center_format() {
990        let pn = PageNumber::bottom_center();
991        assert_eq!(pn.number_format, NumberFormatType::Digit);
992    }
993
994    #[test]
995    fn page_number_bottom_center_no_decoration() {
996        let pn = PageNumber::bottom_center();
997        assert!(pn.decoration.is_empty());
998    }
999
1000    #[test]
1001    fn page_number_bottom_center_equals_explicit() {
1002        let shortcut = PageNumber::bottom_center();
1003        let explicit = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
1004        assert_eq!(shortcut, explicit);
1005    }
1006
1007    #[test]
1008    fn section_backward_compat_deserialize() {
1009        // JSON without header/footer/page_number fields (pre-4.5 format)
1010        let a4 = PageSettings::a4();
1011        let json = serde_json::to_string(&Section::with_paragraphs(vec![], a4)).unwrap();
1012        let section: Section = serde_json::from_str(&json).unwrap();
1013        assert!(section.headers.is_empty());
1014        assert!(section.footers.is_empty());
1015        assert!(section.page_number.is_none());
1016    }
1017
1018    #[test]
1019    fn all_pages_equals_new_with_both() {
1020        let paras = vec![simple_paragraph()];
1021        let from_all_pages = HeaderFooter::all_pages(paras.clone());
1022        let from_new = HeaderFooter::new(paras, ApplyPageType::Both);
1023        assert_eq!(from_all_pages, from_new);
1024    }
1025}