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    /// 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    /// Counts tables, images, and charts in this section.
584    ///
585    /// Traverses all paragraph runs once and returns aggregate counts.
586    ///
587    /// # Examples
588    ///
589    /// ```
590    /// use hwpforge_core::section::{ContentCounts, Section};
591    /// use hwpforge_core::PageSettings;
592    ///
593    /// let section = Section::new(PageSettings::a4());
594    /// let counts = section.content_counts();
595    /// assert_eq!(counts.tables, 0);
596    /// assert_eq!(counts.images, 0);
597    /// assert_eq!(counts.charts, 0);
598    /// ```
599    pub fn content_counts(&self) -> ContentCounts {
600        let mut tables: usize = 0;
601        let mut images: usize = 0;
602        let mut charts: usize = 0;
603
604        for para in &self.paragraphs {
605            for run in &para.runs {
606                match &run.content {
607                    crate::RunContent::Table(_) => tables += 1,
608                    crate::RunContent::Image(_) => images += 1,
609                    crate::RunContent::Control(c) => {
610                        if matches!(**c, crate::control::Control::Chart { .. }) {
611                            charts += 1;
612                        }
613                    }
614                    _ => {}
615                }
616            }
617        }
618
619        ContentCounts { tables, images, charts }
620    }
621}
622
623/// Aggregate content counts for a section.
624#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
625pub struct ContentCounts {
626    /// Number of tables.
627    pub tables: usize,
628    /// Number of images.
629    pub images: usize,
630    /// Number of charts.
631    pub charts: usize,
632}
633
634impl std::fmt::Display for Section {
635    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
636        let n = self.paragraphs.len();
637        let word = if n == 1 { "paragraph" } else { "paragraphs" };
638        write!(f, "Section({n} {word})")
639    }
640}
641
642#[cfg(test)]
643mod tests {
644    use super::*;
645    use crate::run::Run;
646    use hwpforge_foundation::{
647        ApplyPageType, CharShapeIndex, NumberFormatType, PageNumberPosition, ParaShapeIndex,
648    };
649
650    fn simple_paragraph() -> Paragraph {
651        Paragraph::with_runs(
652            vec![Run::text("text", CharShapeIndex::new(0))],
653            ParaShapeIndex::new(0),
654        )
655    }
656
657    #[test]
658    fn new_is_empty() {
659        let section = Section::new(PageSettings::a4());
660        assert!(section.is_empty());
661        assert_eq!(section.paragraph_count(), 0);
662    }
663
664    #[test]
665    fn with_paragraphs() {
666        let section = Section::with_paragraphs(
667            vec![simple_paragraph(), simple_paragraph()],
668            PageSettings::a4(),
669        );
670        assert_eq!(section.paragraph_count(), 2);
671        assert!(!section.is_empty());
672    }
673
674    #[test]
675    fn add_paragraph() {
676        let mut section = Section::new(PageSettings::a4());
677        section.add_paragraph(simple_paragraph());
678        section.add_paragraph(simple_paragraph());
679        assert_eq!(section.paragraph_count(), 2);
680    }
681
682    #[test]
683    fn page_settings_preserved() {
684        let section = Section::new(PageSettings::letter());
685        assert_eq!(section.page_settings, PageSettings::letter());
686    }
687
688    #[test]
689    fn display_singular() {
690        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
691        assert_eq!(section.to_string(), "Section(1 paragraph)");
692    }
693
694    #[test]
695    fn display_plural() {
696        let section = Section::with_paragraphs(
697            vec![simple_paragraph(), simple_paragraph()],
698            PageSettings::a4(),
699        );
700        assert_eq!(section.to_string(), "Section(2 paragraphs)");
701    }
702
703    #[test]
704    fn equality() {
705        let a = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
706        let b = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
707        assert_eq!(a, b);
708    }
709
710    #[test]
711    fn inequality_different_page_settings() {
712        let a = Section::new(PageSettings::a4());
713        let b = Section::new(PageSettings::letter());
714        assert_ne!(a, b);
715    }
716
717    #[test]
718    fn clone_independence() {
719        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
720        let mut cloned = section.clone();
721        cloned.add_paragraph(simple_paragraph());
722        assert_eq!(section.paragraph_count(), 1);
723        assert_eq!(cloned.paragraph_count(), 2);
724    }
725
726    #[test]
727    fn serde_roundtrip() {
728        let section = Section::with_paragraphs(vec![simple_paragraph()], PageSettings::a4());
729        let json = serde_json::to_string(&section).unwrap();
730        let back: Section = serde_json::from_str(&json).unwrap();
731        assert_eq!(section, back);
732    }
733
734    #[test]
735    fn serde_empty_section() {
736        let section = Section::new(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_letter_page() {
744        let section = Section::new(PageSettings::letter());
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    // -----------------------------------------------------------------------
751    // HeaderFooter tests
752    // -----------------------------------------------------------------------
753
754    #[test]
755    fn header_footer_new() {
756        let hf =
757            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
758        assert_eq!(hf.paragraphs.len(), 1);
759        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
760    }
761
762    #[test]
763    fn header_footer_even_odd() {
764        let even = HeaderFooter::new(vec![], ApplyPageType::Even);
765        let odd = HeaderFooter::new(vec![], ApplyPageType::Odd);
766        assert_eq!(even.apply_page_type, ApplyPageType::Even);
767        assert_eq!(odd.apply_page_type, ApplyPageType::Odd);
768        assert_ne!(even, odd);
769    }
770
771    #[test]
772    fn header_footer_display() {
773        let hf =
774            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
775        let s = hf.to_string();
776        assert!(s.contains("1 paragraph"), "display: {s}");
777        assert!(s.contains("Both"), "display: {s}");
778    }
779
780    #[test]
781    fn header_footer_serde_roundtrip() {
782        let hf = HeaderFooter::new(
783            vec![Paragraph::with_runs(
784                vec![Run::text("Header text", CharShapeIndex::new(0))],
785                ParaShapeIndex::new(0),
786            )],
787            ApplyPageType::Both,
788        );
789        let json = serde_json::to_string(&hf).unwrap();
790        let back: HeaderFooter = serde_json::from_str(&json).unwrap();
791        assert_eq!(hf, back);
792    }
793
794    #[test]
795    fn header_footer_clone_independence() {
796        let hf =
797            HeaderFooter::new(vec![Paragraph::new(ParaShapeIndex::new(0))], ApplyPageType::Both);
798        let mut cloned = hf.clone();
799        cloned.paragraphs.push(Paragraph::new(ParaShapeIndex::new(1)));
800        assert_eq!(hf.paragraphs.len(), 1);
801        assert_eq!(cloned.paragraphs.len(), 2);
802    }
803
804    // -----------------------------------------------------------------------
805    // PageNumber tests
806    // -----------------------------------------------------------------------
807
808    #[test]
809    fn page_number_new() {
810        let pn = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
811        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
812        assert_eq!(pn.number_format, NumberFormatType::Digit);
813        assert!(pn.decoration.is_empty());
814    }
815
816    #[test]
817    fn page_number_with_decoration() {
818        let pn = PageNumber::with_decoration(
819            PageNumberPosition::BottomCenter,
820            NumberFormatType::RomanCapital,
821            "- ",
822        );
823        assert_eq!(pn.decoration, "- ");
824        assert_eq!(pn.number_format, NumberFormatType::RomanCapital);
825    }
826
827    #[test]
828    #[allow(deprecated)]
829    fn page_number_with_side_char_deprecated() {
830        let pn = PageNumber::with_side_char(
831            PageNumberPosition::BottomCenter,
832            NumberFormatType::Digit,
833            "- ",
834        );
835        assert_eq!(pn.decoration, "- ");
836    }
837
838    #[test]
839    fn page_number_display() {
840        let pn = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
841        let s = pn.to_string();
842        assert!(s.contains("TopCenter"), "display: {s}");
843        assert!(s.contains("Digit"), "display: {s}");
844    }
845
846    #[test]
847    fn page_number_serde_roundtrip() {
848        let pn = PageNumber::with_decoration(
849            PageNumberPosition::BottomCenter,
850            NumberFormatType::CircledDigit,
851            "< ",
852        );
853        let json = serde_json::to_string(&pn).unwrap();
854        let back: PageNumber = serde_json::from_str(&json).unwrap();
855        assert_eq!(pn, back);
856    }
857
858    #[test]
859    fn page_number_equality() {
860        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
861        let b = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
862        assert_eq!(a, b);
863    }
864
865    #[test]
866    fn page_number_inequality() {
867        let a = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
868        let b = PageNumber::new(PageNumberPosition::TopCenter, NumberFormatType::Digit);
869        assert_ne!(a, b);
870    }
871
872    // -----------------------------------------------------------------------
873    // Section with header/footer/page_number
874    // -----------------------------------------------------------------------
875
876    #[test]
877    fn section_new_has_none_fields() {
878        let section = Section::new(PageSettings::a4());
879        assert!(section.header.is_none());
880        assert!(section.footer.is_none());
881        assert!(section.page_number.is_none());
882        assert!(section.column_settings.is_none());
883    }
884
885    #[test]
886    fn section_with_header_footer() {
887        let mut section = Section::new(PageSettings::a4());
888        section.header = Some(HeaderFooter::new(
889            vec![Paragraph::with_runs(
890                vec![Run::text("Header", CharShapeIndex::new(0))],
891                ParaShapeIndex::new(0),
892            )],
893            ApplyPageType::Both,
894        ));
895        section.footer = Some(HeaderFooter::new(
896            vec![Paragraph::with_runs(
897                vec![Run::text("Footer", CharShapeIndex::new(0))],
898                ParaShapeIndex::new(0),
899            )],
900            ApplyPageType::Both,
901        ));
902        assert!(section.header.is_some());
903        assert!(section.footer.is_some());
904    }
905
906    #[test]
907    fn section_with_page_number() {
908        let mut section = Section::new(PageSettings::a4());
909        section.page_number =
910            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
911        assert!(section.page_number.is_some());
912    }
913
914    #[test]
915    fn section_serde_with_optional_fields() {
916        let mut section = Section::new(PageSettings::a4());
917        section.header = Some(HeaderFooter::new(vec![], ApplyPageType::Both));
918        section.page_number =
919            Some(PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit));
920        let json = serde_json::to_string(&section).unwrap();
921        let back: Section = serde_json::from_str(&json).unwrap();
922        assert_eq!(section, back);
923    }
924
925    #[test]
926    fn section_serde_none_fields_skipped() {
927        let section = Section::new(PageSettings::a4());
928        let json = serde_json::to_string(&section).unwrap();
929        // Section-level header/footer/page_number/column_settings should not appear
930        // (PageSettings has header_margin/footer_margin, which is different)
931        assert!(!json.contains("\"header\""));
932        assert!(!json.contains("\"footer\""));
933        assert!(!json.contains("\"page_number\""));
934        assert!(!json.contains("\"column_settings\""));
935        let back: Section = serde_json::from_str(&json).unwrap();
936        assert_eq!(section, back);
937    }
938
939    // -----------------------------------------------------------------------
940    // HeaderFooter::all_pages tests
941    // -----------------------------------------------------------------------
942
943    #[test]
944    fn header_footer_all_pages_apply_page_type() {
945        let hf = HeaderFooter::all_pages(vec![Paragraph::new(ParaShapeIndex::new(0))]);
946        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
947    }
948
949    #[test]
950    fn header_footer_all_pages_preserves_paragraphs() {
951        let paras = vec![simple_paragraph(), simple_paragraph()];
952        let hf = HeaderFooter::all_pages(paras);
953        assert_eq!(hf.paragraphs.len(), 2);
954    }
955
956    #[test]
957    fn header_footer_all_pages_empty_paragraphs() {
958        let hf = HeaderFooter::all_pages(vec![]);
959        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
960        assert!(hf.paragraphs.is_empty());
961    }
962
963    #[test]
964    #[allow(deprecated)]
965    fn header_footer_both_deprecated_alias() {
966        let hf = HeaderFooter::both(vec![Paragraph::new(ParaShapeIndex::new(0))]);
967        assert_eq!(hf.apply_page_type, ApplyPageType::Both);
968    }
969
970    // -----------------------------------------------------------------------
971    // PageNumber::bottom_center tests
972    // -----------------------------------------------------------------------
973
974    #[test]
975    fn page_number_bottom_center_position() {
976        let pn = PageNumber::bottom_center();
977        assert_eq!(pn.position, PageNumberPosition::BottomCenter);
978    }
979
980    #[test]
981    fn page_number_bottom_center_format() {
982        let pn = PageNumber::bottom_center();
983        assert_eq!(pn.number_format, NumberFormatType::Digit);
984    }
985
986    #[test]
987    fn page_number_bottom_center_no_decoration() {
988        let pn = PageNumber::bottom_center();
989        assert!(pn.decoration.is_empty());
990    }
991
992    #[test]
993    fn page_number_bottom_center_equals_explicit() {
994        let shortcut = PageNumber::bottom_center();
995        let explicit = PageNumber::new(PageNumberPosition::BottomCenter, NumberFormatType::Digit);
996        assert_eq!(shortcut, explicit);
997    }
998
999    #[test]
1000    fn section_backward_compat_deserialize() {
1001        // JSON without header/footer/page_number fields (pre-4.5 format)
1002        let a4 = PageSettings::a4();
1003        let json = serde_json::to_string(&Section::with_paragraphs(vec![], a4)).unwrap();
1004        let section: Section = serde_json::from_str(&json).unwrap();
1005        assert!(section.header.is_none());
1006        assert!(section.footer.is_none());
1007        assert!(section.page_number.is_none());
1008    }
1009
1010    #[test]
1011    fn all_pages_equals_new_with_both() {
1012        let paras = vec![simple_paragraph()];
1013        let from_all_pages = HeaderFooter::all_pages(paras.clone());
1014        let from_new = HeaderFooter::new(paras, ApplyPageType::Both);
1015        assert_eq!(from_all_pages, from_new);
1016    }
1017}