1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43pub struct Visibility {
44 #[serde(default)]
46 pub hide_first_header: bool,
47 #[serde(default)]
49 pub hide_first_footer: bool,
50 #[serde(default)]
52 pub hide_first_master_page: bool,
53 #[serde(default)]
55 pub hide_first_page_num: bool,
56 #[serde(default)]
58 pub hide_first_empty_line: bool,
59 #[serde(default)]
61 pub show_line_number: bool,
62 #[serde(default)]
64 pub border: ShowMode,
65 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
93pub struct LineNumberShape {
94 #[serde(default)]
96 pub restart_type: u8,
97 #[serde(default)]
99 pub count_by: u16,
100 #[serde(default)]
102 pub distance: HwpUnit,
103 #[serde(default)]
105 pub start_number: u32,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
117pub struct PageBorderFillEntry {
118 pub apply_type: String,
120 #[serde(default = "PageBorderFillEntry::default_border_fill_id")]
122 pub border_fill_id: u32,
123 #[serde(default = "PageBorderFillEntry::default_text_border")]
125 pub text_border: String,
126 #[serde(default)]
128 pub header_inside: bool,
129 #[serde(default)]
131 pub footer_inside: bool,
132 #[serde(default = "PageBorderFillEntry::default_fill_area")]
134 pub fill_area: String,
135 #[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 [
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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
183pub struct BeginNum {
184 #[serde(default = "BeginNum::one")]
186 pub page: u32,
187 #[serde(default = "BeginNum::one")]
189 pub footnote: u32,
190 #[serde(default = "BeginNum::one")]
192 pub endnote: u32,
193 #[serde(default = "BeginNum::one")]
195 pub pic: u32,
196 #[serde(default = "BeginNum::one")]
198 pub tbl: u32,
199 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
228pub struct MasterPage {
229 pub apply_page_type: ApplyPageType,
231 pub paragraphs: Vec<Paragraph>,
233}
234
235impl MasterPage {
236 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
275pub struct HeaderFooter {
276 pub paragraphs: Vec<Paragraph>,
278 pub apply_page_type: ApplyPageType,
280}
281
282impl HeaderFooter {
283 pub fn new(paragraphs: Vec<Paragraph>, apply_page_type: ApplyPageType) -> Self {
285 Self { paragraphs, apply_page_type }
286 }
287
288 pub fn all_pages(paragraphs: Vec<Paragraph>) -> Self {
305 Self { paragraphs, apply_page_type: ApplyPageType::Both }
306 }
307
308 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
345pub struct PageNumber {
346 pub position: PageNumberPosition,
348 pub number_format: NumberFormatType,
350 pub decoration: String,
353}
354
355impl PageNumber {
356 pub fn new(position: PageNumberPosition, number_format: NumberFormatType) -> Self {
358 Self { position, number_format, decoration: String::new() }
359 }
360
361 pub fn bottom_center() -> Self {
379 Self {
380 position: PageNumberPosition::BottomCenter,
381 number_format: NumberFormatType::Digit,
382 decoration: String::new(),
383 }
384 }
385
386 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 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
447pub struct Section {
448 pub paragraphs: Vec<Paragraph>,
450 pub page_settings: PageSettings,
452 #[serde(default, skip_serializing_if = "Option::is_none")]
454 pub header: Option<HeaderFooter>,
455 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub footer: Option<HeaderFooter>,
458 #[serde(default, skip_serializing_if = "Option::is_none")]
460 pub page_number: Option<PageNumber>,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
463 pub column_settings: Option<ColumnSettings>,
464 #[serde(default, skip_serializing_if = "Option::is_none")]
467 pub visibility: Option<Visibility>,
468 #[serde(default, skip_serializing_if = "Option::is_none")]
470 pub line_number_shape: Option<LineNumberShape>,
471 #[serde(default, skip_serializing_if = "Option::is_none")]
473 pub page_border_fills: Option<Vec<PageBorderFillEntry>>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub master_pages: Option<Vec<MasterPage>>,
478 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub begin_num: Option<BeginNum>,
482 #[serde(default)]
485 pub text_direction: TextDirection,
486}
487
488impl Section {
489 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 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 pub fn with_text_direction(mut self, dir: TextDirection) -> Self {
564 self.text_direction = dir;
565 self
566 }
567
568 pub fn add_paragraph(&mut self, paragraph: Paragraph) {
570 self.paragraphs.push(paragraph);
571 }
572
573 pub fn paragraph_count(&self) -> usize {
575 self.paragraphs.len()
576 }
577
578 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(§ion).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(§ion).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(§ion).unwrap();
696 let back: Section = serde_json::from_str(&json).unwrap();
697 assert_eq!(section, back);
698 }
699
700 #[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 #[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 #[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(§ion).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(§ion).unwrap();
879 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 #[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 #[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 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}