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    /// Optional header for this section.
453    #[serde(default, skip_serializing_if = "Option::is_none")]
454    pub header: Option<HeaderFooter>,
455    /// Optional footer for this section.
456    #[serde(default, skip_serializing_if = "Option::is_none")]
457    pub footer: Option<HeaderFooter>,
458    /// Optional page number settings for this section.
459    #[serde(default, skip_serializing_if = "Option::is_none")]
460    pub page_number: Option<PageNumber>,
461    /// Multi-column layout. `None` = single column (default).
462    #[serde(default, skip_serializing_if = "Option::is_none")]
463    pub column_settings: Option<ColumnSettings>,
464    /// Visibility flags for headers, footers, borders, etc.
465    /// `None` = default visibility (show everything).
466    #[serde(default, skip_serializing_if = "Option::is_none")]
467    pub visibility: Option<Visibility>,
468    /// Line numbering settings. `None` = no line numbers.
469    #[serde(default, skip_serializing_if = "Option::is_none")]
470    pub line_number_shape: Option<LineNumberShape>,
471    /// Page border/fill entries. `None` = default 3 entries (BOTH/EVEN/ODD with borderFillIDRef=1).
472    #[serde(default, skip_serializing_if = "Option::is_none")]
473    pub page_border_fills: Option<Vec<PageBorderFillEntry>>,
474    /// Master pages (background content rendered behind the body).
475    /// `None` = no master pages (default).
476    #[serde(default, skip_serializing_if = "Option::is_none")]
477    pub master_pages: Option<Vec<MasterPage>>,
478    /// Starting numbers for auto-numbering sequences.
479    /// `None` = default values (all start at 1).
480    #[serde(default, skip_serializing_if = "Option::is_none")]
481    pub begin_num: Option<BeginNum>,
482    /// Text writing direction for this section.
483    /// Defaults to [`TextDirection::Horizontal`] (가로쓰기).
484    #[serde(default)]
485    pub text_direction: TextDirection,
486}
487
488impl Section {
489    /// Creates an empty section with the given page settings.
490    ///
491    /// # Examples
492    ///
493    /// ```
494    /// use hwpforge_core::section::Section;
495    /// use hwpforge_core::PageSettings;
496    ///
497    /// let section = Section::new(PageSettings::a4());
498    /// assert!(section.is_empty());
499    /// ```
500    pub fn new(page_settings: PageSettings) -> Self {
501        Self {
502            paragraphs: Vec::new(),
503            page_settings,
504            header: None,
505            footer: None,
506            page_number: None,
507            column_settings: None,
508            visibility: None,
509            line_number_shape: None,
510            page_border_fills: None,
511            master_pages: None,
512            begin_num: None,
513            text_direction: TextDirection::Horizontal,
514        }
515    }
516
517    /// Creates a section with pre-built paragraphs.
518    ///
519    /// # Examples
520    ///
521    /// ```
522    /// use hwpforge_core::section::Section;
523    /// use hwpforge_core::PageSettings;
524    /// use hwpforge_core::paragraph::Paragraph;
525    /// use hwpforge_foundation::ParaShapeIndex;
526    ///
527    /// let section = Section::with_paragraphs(
528    ///     vec![Paragraph::new(ParaShapeIndex::new(0))],
529    ///     PageSettings::letter(),
530    /// );
531    /// assert_eq!(section.paragraph_count(), 1);
532    /// ```
533    pub fn with_paragraphs(paragraphs: Vec<Paragraph>, page_settings: PageSettings) -> Self {
534        Self {
535            paragraphs,
536            page_settings,
537            header: None,
538            footer: None,
539            page_number: None,
540            column_settings: None,
541            visibility: None,
542            line_number_shape: None,
543            page_border_fills: None,
544            master_pages: None,
545            begin_num: None,
546            text_direction: TextDirection::Horizontal,
547        }
548    }
549
550    /// Sets the text writing direction for this section and returns `self`.
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use hwpforge_core::section::Section;
556    /// use hwpforge_core::PageSettings;
557    /// use hwpforge_foundation::TextDirection;
558    ///
559    /// let section = Section::new(PageSettings::a4())
560    ///     .with_text_direction(TextDirection::Vertical);
561    /// assert_eq!(section.text_direction, TextDirection::Vertical);
562    /// ```
563    pub fn with_text_direction(mut self, dir: TextDirection) -> Self {
564        self.text_direction = dir;
565        self
566    }
567
568    /// Appends a paragraph to this section.
569    pub fn add_paragraph(&mut self, paragraph: Paragraph) {
570        self.paragraphs.push(paragraph);
571    }
572
573    /// Returns the number of paragraphs.
574    pub fn paragraph_count(&self) -> usize {
575        self.paragraphs.len()
576    }
577
578    /// Returns `true` if this section has no paragraphs.
579    pub fn is_empty(&self) -> bool {
580        self.paragraphs.is_empty()
581    }
582}
583
584impl std::fmt::Display for Section {
585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586        let n = self.paragraphs.len();
587        let word = if n == 1 { "paragraph" } else { "paragraphs" };
588        write!(f, "Section({n} {word})")
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595    use crate::run::Run;
596    use hwpforge_foundation::{
597        ApplyPageType, CharShapeIndex, NumberFormatType, PageNumberPosition, ParaShapeIndex,
598    };
599
600    fn simple_paragraph() -> Paragraph {
601        Paragraph::with_runs(
602            vec![Run::text("text", CharShapeIndex::new(0))],
603            ParaShapeIndex::new(0),
604        )
605    }
606
607    #[test]
608    fn new_is_empty() {
609        let section = Section::new(PageSettings::a4());
610        assert!(section.is_empty());
611        assert_eq!(section.paragraph_count(), 0);
612    }
613
614    #[test]
615    fn with_paragraphs() {
616        let section = Section::with_paragraphs(
617            vec![simple_paragraph(), simple_paragraph()],
618            PageSettings::a4(),
619        );
620        assert_eq!(section.paragraph_count(), 2);
621        assert!(!section.is_empty());
622    }
623
624    #[test]
625    fn add_paragraph() {
626        let mut section = Section::new(PageSettings::a4());
627        section.add_paragraph(simple_paragraph());
628        section.add_paragraph(simple_paragraph());
629        assert_eq!(section.paragraph_count(), 2);
630    }
631
632    #[test]
633    fn page_settings_preserved() {
634        let section = Section::new(PageSettings::letter());
635        assert_eq!(section.page_settings, PageSettings::letter());
636    }
637
638    #[test]
639    fn display_singular() {
640        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
641        assert_eq!(section.to_string(), "Section(1 paragraph)");
642    }
643
644    #[test]
645    fn display_plural() {
646        let section = Section::with_paragraphs(
647            vec![simple_paragraph(), simple_paragraph()],
648            PageSettings::a4(),
649        );
650        assert_eq!(section.to_string(), "Section(2 paragraphs)");
651    }
652
653    #[test]
654    fn equality() {
655        let a = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
656        let b = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
657        assert_eq!(a, b);
658    }
659
660    #[test]
661    fn inequality_different_page_settings() {
662        let a = Section::new(PageSettings::a4());
663        let b = Section::new(PageSettings::letter());
664        assert_ne!(a, b);
665    }
666
667    #[test]
668    fn clone_independence() {
669        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
670        let mut cloned = section.clone();
671        cloned.add_paragraph(simple_paragraph());
672        assert_eq!(section.paragraph_count(), 1);
673        assert_eq!(cloned.paragraph_count(), 2);
674    }
675
676    #[test]
677    fn serde_roundtrip() {
678        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
679        let json = serde_json::to_string(&section).unwrap();
680        let back: Section = serde_json::from_str(&json).unwrap();
681        assert_eq!(section, back);
682    }
683
684    #[test]
685    fn serde_empty_section() {
686        let section = Section::new(PageSettings::a4());
687        let json = serde_json::to_string(&section).unwrap();
688        let back: Section = serde_json::from_str(&json).unwrap();
689        assert_eq!(section, back);
690    }
691
692    #[test]
693    fn serde_letter_page() {
694        let section = Section::new(PageSettings::letter());
695        let json = serde_json::to_string(&section).unwrap();
696        let back: Section = serde_json::from_str(&json).unwrap();
697        assert_eq!(section, back);
698    }
699
700    // -----------------------------------------------------------------------
701    // HeaderFooter tests
702    // -----------------------------------------------------------------------
703
704    #[test]
705    fn header_footer_new() {
706        let hf =
707            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
708        assert_eq!(hf.paragraphs.len(), 1);
709        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
710    }
711
712    #[test]
713    fn header_footer_even_odd() {
714        let even = HeaderFooter::new(vec![], ApplyPageType::Even);
715        let odd = HeaderFooter::new(vec![], ApplyPageType::Odd);
716        assert_eq!(even.apply_page_type, ApplyPageType::Even);
717        assert_eq!(odd.apply_page_type, ApplyPageType::Odd);
718        assert_ne!(even, odd);
719    }
720
721    #[test]
722    fn header_footer_display() {
723        let hf =
724            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
725        let s = hf.to_string();
726        assert!(s.contains("1 paragraph"), "display: {s}");
727        assert!(s.contains("Both"), "display: {s}");
728    }
729
730    #[test]
731    fn header_footer_serde_roundtrip() {
732        let hf = HeaderFooter::new(
733            vec![Paragraph::with_runs(
734                vec![Run::text("Header text", CharShapeIndex::new(0))],
735                ParaShapeIndex::new(0),
736            )],
737            ApplyPageType::Both,
738        );
739        let json = serde_json::to_string(&hf).unwrap();
740        let back: HeaderFooter = serde_json::from_str(&json).unwrap();
741        assert_eq!(hf, back);
742    }
743
744    #[test]
745    fn header_footer_clone_independence() {
746        let hf =
747            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
748        let mut cloned = hf.clone();
749        cloned.paragraphs.push(Paragraph::new(ParaShapeIndex::new(1)));
750        assert_eq!(hf.paragraphs.len(), 1);
751        assert_eq!(cloned.paragraphs.len(), 2);
752    }
753
754    // -----------------------------------------------------------------------
755    // PageNumber tests
756    // -----------------------------------------------------------------------
757
758    #[test]
759    fn page_number_new() {
760        let pn = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
761        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
762        assert_eq!(pn.number_format, NumberFormatType::Digit);
763        assert!(pn.decoration.is_empty());
764    }
765
766    #[test]
767    fn page_number_with_decoration() {
768        let pn = PageNumber::with_decoration(
769            PageNumberPosition::BottomCenter,
770            NumberFormatType::RomanCapital,
771            "- ",
772        );
773        assert_eq!(pn.decoration, "- ");
774        assert_eq!(pn.number_format, NumberFormatType::RomanCapital);
775    }
776
777    #[test]
778    #[allow(deprecated)]
779    fn page_number_with_side_char_deprecated() {
780        let pn = PageNumber::with_side_char(
781            PageNumberPosition::BottomCenter,
782            NumberFormatType::Digit,
783            "- ",
784        );
785        assert_eq!(pn.decoration, "- ");
786    }
787
788    #[test]
789    fn page_number_display() {
790        let pn = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
791        let s = pn.to_string();
792        assert!(s.contains("TopCenter"), "display: {s}");
793        assert!(s.contains("Digit"), "display: {s}");
794    }
795
796    #[test]
797    fn page_number_serde_roundtrip() {
798        let pn = PageNumber::with_decoration(
799            PageNumberPosition::BottomCenter,
800            NumberFormatType::CircledDigit,
801            "< ",
802        );
803        let json = serde_json::to_string(&pn).unwrap();
804        let back: PageNumber = serde_json::from_str(&json).unwrap();
805        assert_eq!(pn, back);
806    }
807
808    #[test]
809    fn page_number_equality() {
810        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
811        let b = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
812        assert_eq!(a, b);
813    }
814
815    #[test]
816    fn page_number_inequality() {
817        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
818        let b = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
819        assert_ne!(a, b);
820    }
821
822    // -----------------------------------------------------------------------
823    // Section with header/footer/page_number
824    // -----------------------------------------------------------------------
825
826    #[test]
827    fn section_new_has_none_fields() {
828        let section = Section::new(PageSettings::a4());
829        assert!(section.header.is_none());
830        assert!(section.footer.is_none());
831        assert!(section.page_number.is_none());
832        assert!(section.column_settings.is_none());
833    }
834
835    #[test]
836    fn section_with_header_footer() {
837        let mut section = Section::new(PageSettings::a4());
838        section.header = Some(HeaderFooter::new(
839            vec![Paragraph::with_runs(
840                vec![Run::text("Header", CharShapeIndex::new(0))],
841                ParaShapeIndex::new(0),
842            )],
843            ApplyPageType::Both,
844        ));
845        section.footer = Some(HeaderFooter::new(
846            vec![Paragraph::with_runs(
847                vec![Run::text("Footer", CharShapeIndex::new(0))],
848                ParaShapeIndex::new(0),
849            )],
850            ApplyPageType::Both,
851        ));
852        assert!(section.header.is_some());
853        assert!(section.footer.is_some());
854    }
855
856    #[test]
857    fn section_with_page_number() {
858        let mut section = Section::new(PageSettings::a4());
859        section.page_number =
860            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
861        assert!(section.page_number.is_some());
862    }
863
864    #[test]
865    fn section_serde_with_optional_fields() {
866        let mut section = Section::new(PageSettings::a4());
867        section.header = Some(HeaderFooter::new(vec![], ApplyPageType::Both));
868        section.page_number =
869            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
870        let json = serde_json::to_string(&section).unwrap();
871        let back: Section = serde_json::from_str(&json).unwrap();
872        assert_eq!(section, back);
873    }
874
875    #[test]
876    fn section_serde_none_fields_skipped() {
877        let section = Section::new(PageSettings::a4());
878        let json = serde_json::to_string(&section).unwrap();
879        // Section-level header/footer/page_number/column_settings should not appear
880        // (PageSettings has header_margin/footer_margin, which is different)
881        assert!(!json.contains("\"header\""));
882        assert!(!json.contains("\"footer\""));
883        assert!(!json.contains("\"page_number\""));
884        assert!(!json.contains("\"column_settings\""));
885        let back: Section = serde_json::from_str(&json).unwrap();
886        assert_eq!(section, back);
887    }
888
889    // -----------------------------------------------------------------------
890    // HeaderFooter::all_pages tests
891    // -----------------------------------------------------------------------
892
893    #[test]
894    fn header_footer_all_pages_apply_page_type() {
895        let hf = HeaderFooter::all_pages(vec![Paragraph::new(ParaShapeIndex::new(0))]);
896        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
897    }
898
899    #[test]
900    fn header_footer_all_pages_preserves_paragraphs() {
901        let paras = vec![simple_paragraph(), simple_paragraph()];
902        let hf = HeaderFooter::all_pages(paras);
903        assert_eq!(hf.paragraphs.len(), 2);
904    }
905
906    #[test]
907    fn header_footer_all_pages_empty_paragraphs() {
908        let hf = HeaderFooter::all_pages(vec![]);
909        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
910        assert!(hf.paragraphs.is_empty());
911    }
912
913    #[test]
914    #[allow(deprecated)]
915    fn header_footer_both_deprecated_alias() {
916        let hf = HeaderFooter::both(vec![Paragraph::new(ParaShapeIndex::new(0))]);
917        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
918    }
919
920    // -----------------------------------------------------------------------
921    // PageNumber::bottom_center tests
922    // -----------------------------------------------------------------------
923
924    #[test]
925    fn page_number_bottom_center_position() {
926        let pn = PageNumber::bottom_center();
927        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
928    }
929
930    #[test]
931    fn page_number_bottom_center_format() {
932        let pn = PageNumber::bottom_center();
933        assert_eq!(pn.number_format, NumberFormatType::Digit);
934    }
935
936    #[test]
937    fn page_number_bottom_center_no_decoration() {
938        let pn = PageNumber::bottom_center();
939        assert!(pn.decoration.is_empty());
940    }
941
942    #[test]
943    fn page_number_bottom_center_equals_explicit() {
944        let shortcut = PageNumber::bottom_center();
945        let explicit = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
946        assert_eq!(shortcut, explicit);
947    }
948
949    #[test]
950    fn section_backward_compat_deserialize() {
951        // JSON without header/footer/page_number fields (pre-4.5 format)
952        let a4 = PageSettings::a4();
953        let json = serde_json::to_string(&Section::with_paragraphs(vec![], a4)).unwrap();
954        let section: Section = serde_json::from_str(&json).unwrap();
955        assert!(section.header.is_none());
956        assert!(section.footer.is_none());
957        assert!(section.page_number.is_none());
958    }
959
960    #[test]
961    fn all_pages_equals_new_with_both() {
962        let paras = vec![simple_paragraph()];
963        let from_all_pages = HeaderFooter::all_pages(paras.clone());
964        let from_new = HeaderFooter::new(paras, ApplyPageType::Both);
965        assert_eq!(from_all_pages, from_new);
966    }
967}